凌晨被叫起来修BUG,居然是一条SQL在全表扫800万行
凌晨被叫起来修BUG,居然是一条SQL在全表扫800万行

上周上午八点半,我被运维的夺命连环call吵醒,说生产环境数据库CPU飙到99%,整个订单系统瘫痪。我迷迷糊糊登上服务器,拉出慢查询日志一看,一条看起来人畜无害的查询语句,居然在全表扫描800万行数据。更离谱的是,这个字段明明建了索引,但Explain一跑,type显示ALL——索引压根没生效。就这么一条SQL,差点让公司损失几十万。从那以后我就养成了一个习惯:写完SQL先跑Explain,不看执行计划绝不上线。今天这篇文章,我就用三个亲身经历的真实案例,手把手带你看懂Explain,学会用它揪出那些藏在系统里的性能杀手。
数据库工程与Explain执行计划对比分析实战

一、为什么我说不看Explain就别碰SQL优化
☆ 干了这么多年后端开发,我发现一个特别普遍的现象:很多程序员写SQL的时候,只关心"能不能查出来",从来不关心"数据库是怎么查出来的"。等到数据量一上来,页面加载从200毫秒变成5秒,才开始慌。
☆ 这时候大部分人的反应是什么?加索引。但说实话,加索引这事跟赌博差不多,你不知道加在哪、加什么类型的索引,很可能加了等于白加。我见过太多这种情况了:索引建了,SQL写得有问题,优化器根本不走索引;或者索引建对了,但建的顺序不对,效果大打折扣。
☆ 所以在你动手改任何东西之前,第一件事应该是跑一下EXPLAIN。这个命令会告诉你MySQL打算怎么执行这条SQL,用了哪个索引、扫描了多少行、有没有额外的排序开销,全都一清二楚。说白了,Explain就是SQL的体检报告,不看报告就动刀,跟盲人做手术没啥区别。

二、Explain的核心字段,哪些才是真正要看的
☆ Explain输出的结果有十几列,但说实话,你真正需要盯着看的就那么四五个。我把这些年总结出来的重点整理成了下面这张表:
字段名 含义 怎么判断好坏
type 访问类型,决定查询效率 system > const > eq_ref > ref > range > index > ALL,至少要到range级别
possible_keys 可能用到的索引 有值比NULL强,说明优化器有选择余地
key 实际使用的索引 NULL说明没用上任何索引,必须优化
key_len 索引使用的字节长度 越长说明索引用得越完整
rows 预估扫描的行数 越少越好,最好是个位数或两位数
Extra 额外执行信息 出现Using filesort或Using temporary要高度警惕
☆ 上面这张表里,最核心的就是type和rows。type告诉你数据库用什么方式在扫数据,rows告诉你它打算扫多少行。这两个数字一看,基本就能判断这条SQL的性能处于什么水平。
☆ 举个例子,如果type是ALL,那就是全表扫描,这是最差的情况,必须优化。如果是ref或者range,说明用上了索引,已经及格了。如果是const或者eq_ref,那基本就是秒回级别,可以放心上线。

三、案例一:一条查询订单的SQL,从5秒优化到3毫秒
1、问题是怎么发现的
☆ 去年我接手了一个电商项目的订单模块,用户反馈"我的订单"页面加载要等四五秒。我打开代码一看,SQL写得特别朴素:
sql
SELECT * FROM orders WHERE user_id = 10086 ORDER BY create_time DESC LIMIT 20;
☆ orders表有将近600万条数据,user_id字段上压根没建索引。我当时就预感这条SQL有问题,果不其然,跑了一下Explain:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE orders ALL NULL NULL NULL 6000000 Using where; Using filesort
☆ 看到这张表我就知道完蛋了:type = ALL说明全表扫描,rows = 6000000说明要扫600万行,Extra里还出现了Using filesort,说明还要额外做文件排序。三重debuff叠满了,能不慢吗?
2、我是怎么优化的
☆ 第一步,建联合索引,把查询条件和排序字段都放进去:
sql
CREATE INDEX idx_user_time ON orders(user_id, create_time DESC);
☆ 为什么要把create_time也放进索引?因为查询里有ORDER BY create_time DESC,如果排序字段在索引里,数据库可以直接按索引顺序读取,根本不需要额外做文件排序。
☆ 第二步,把SELECT *改成只查需要的字段:
sql
SELECT order_id, create_time, total_amount, status
FROM orders
WHERE user_id = 10086
ORDER BY create_time DESC
LIMIT 20;
3、优化后的Explain对比
对比项 优化前 优化后
type ALL(全表扫描) ref(索引范围扫描)
rows 6000000 20
Extra Using where; Using filesort 无
执行时间 4.8秒 0.003秒
☆ 从4.8秒降到3毫秒,提升了1600倍。就加了一个索引、改了几个字段,效果就是这么炸裂。你说Explain重不重要?不看Explain,你可能花一整天都找不到问题在哪。

四、案例二:报表统计接口,换个写法快了20倍
1、问题描述
☆ 另一个项目有个运营后台,需要统计每个商品分类的销售总额。原始SQL长这样:
sql
SELECT c.category_name, SUM(o.total_amount) as total_sales
FROM categories c
LEFT JOIN orders o ON c.id = o.category_id
GROUP BY c.category_name;
☆ categories表大概300条数据,orders表有1200万条。这条SQL跑一次要将近10秒,运营的同事天天在群里催。
2、为什么慢
☆ 很多人一看"统计每个分类的总额",第一反应就是JOIN加GROUP BY。逻辑上没毛病,但性能上是个大坑。
☆ LEFT JOIN会先把两张表做关联,orders有1200万行,关联之后中间结果集依然是1200万行级别。然后再对这1200万行做GROUP BY聚合,MySQL得开临时表、做文件排序,开销巨大。
☆ 我跑了一下Explain看看:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE c ALL NULL NULL NULL 300 Using temporary; Using filesort
1 SIMPLE o ALL NULL NULL NULL 12000000 Using where; Using join buffer
☆ 你看,orders表那边type = ALL,1200万行全表扫描,还用了join buffer。categories表那边也出现了Using temporary; Using filesort。两边都有问题。
3、优化方案:先聚合再关联
sql
SELECT c.category_name,
COALESCE(t.total_sales, 0) as total_sales
FROM categories c
LEFT JOIN (
SELECT category_id, SUM(total_amount) as total_sales
FROM orders
GROUP BY category_id
) t ON c.id = t.category_id;
☆ 核心思路特别简单:先在orders表内部完成聚合,把1200万行压缩成几百行(每个分类一行),然后再和categories表关联。这样JOIN的数据量就从1200万降到了几百。
4、优化后的Explain
id select_type table type possible_keys key key_len rows Extra
1 PRIMARY c ALL NULL NULL NULL 300 —
1 PRIMARY ref 5 1 —
2 DERIVED orders index NULL idx_category 5 450 Using index
☆ orders表那边type = index,只扫描了450行,而且Extra显示Using index,说明走了覆盖索引。categories表只有300行,影响可以忽略。
5、效果对比
对比项 直接JOIN写法 子查询先聚合写法
JOIN时数据量 约1200万行 约450行
是否使用临时表 是 否
执行时间 9.8秒 0.45秒
性能提升 基准 约22倍
☆ 同样的业务结果,换一种写法,性能直接提升二十多倍。所以说不要迷信JOIN,有时候把子查询用对了,比JOIN快得多。

五、案例三:Explain揪出了一个隐藏的类型不匹配问题
1、问题背景
☆ 有一次线上出现慢查询告警,SQL看着特别简单:
sql
SELECT * FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'paid';
☆ orders表大概800万行,users表大概50万行。这条SQL执行要6秒多,我觉得不对劲,就跑了一下Explain:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE o ALL NULL NULL NULL 8000000 Using where
1 SIMPLE u eq_ref PRIMARY PRIMARY 4 1 —
☆ orders表那边type = ALL,800万行全表扫描!但users表那边type = eq_ref,说明主键关联没问题。问题出在orders表。
2、真相大白
☆ 我仔细一看表结构,发现问题了:orders表的user_id是varchar(20)类型,而users表的id是int类型。
☆ 两个字段类型不一致,MySQL在做JOIN的时候没法直接用索引关联,只能把orders表全扫一遍,然后逐行转换类型再去匹配。这就是为什么800万行全表扫描的原因。
3、优化方案
☆ 把字段类型统一,都改成int:
sql
ALTER TABLE orders MODIFY user_id INT;
☆ 然后在user_id上建个索引:
sql
CREATE INDEX idx_orders_user ON orders(user_id);
4、优化后的Explain
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE o ref idx_orders_user idx_orders_user 4 3200 Using where
1 SIMPLE u eq_ref PRIMARY PRIMARY 4 1 —
☆ orders表从全表扫描800万行,变成了索引扫描3200行。就因为一个字段类型不匹配,性能差了两千多倍。这种坑你不跑Explain根本发现不了,光看SQL语句是看不出问题的。

六、关于索引策略的几条实战心得
1、联合索引必须遵守最左前缀原则
☆ 这条我说多少遍都不嫌多。比如你建了(a, b, c)的联合索引,那么WHERE a=1能用上索引,WHERE a=1 AND b=2也能用上,但如果你只写WHERE b=2,索引就完全废了。所以建联合索引的时候,一定要把最常用的、区分度最高的字段放在最左边。
2、能用覆盖索引就别回表
☆ 所谓覆盖索引,就是索引本身就包含了查询需要的所有字段,数据库根本不用去查原始数据行。比如:
sql
CREATE INDEX idx_cover ON orders(user_id, create_time, order_id, total_amount);
SELECT order_id, create_time, total_amount
FROM orders
WHERE user_id = 10086
ORDER BY create_time DESC
LIMIT 20;
☆ 你跑一下Explain,Extra里会出现Using index,这就是覆盖索引的标志。这种情况下数据库连数据行都不用访问,性能是最优的。
3、索引不是越多越好
☆ 我见过有些团队,恨不得给每个字段都建个索引,结果写入性能暴跌。因为每次INSERT、UPDATE、DELETE都要同步更新所有索引。而且索引太多,优化器反而不知道选哪个,可能选错索引,性能更差。所以索引要建在刀刃上,不是越多越好。

七、写在最后
☆ SQL优化这件事,说穿了就那么几招:看Explain、建对索引、写好SQL。但就是这几招,很多人干了好几年都没用明白。
☆ 我的建议是养成一个习惯:每次写完SQL,先跑一下EXPLAIN看看。不要等出了问题再去排查,那时候往往已经火烧眉毛了。
☆ 记住几个关键信号:type = ALL是全表扫描,必须优化;Extra里出现Using filesort或Using temporary,说明有额外开销;rows超过一千,就该警惕了。
☆ 数据库优化不是一次性的活,是个持续的过程。业务在变,数据量在涨,今天的最优解可能半年后就不够用了。所以定期review慢查询日志、定期检查索引使用情况,这个习惯比什么都值钱。
☆ 希望今天这几个案例能帮你在实际工作中少踩点坑。如果你也有什么SQL优化的血泪故事,欢迎在评论区聊聊,咱们一起交流。

💡注意:本文所介绍的软件及功能均基于公开信息整理,仅供用户参考。在使用任何软件时,请务必遵守相关法律法规及软件使用协议。同时,本文不涉及任何商业推广或引流行为,仅为用户提供一个了解和使用该工具的渠道。
你在生活中时遇到了哪些问题?你是如何解决的?欢迎在评论区分享你的经验和心得!
希望这篇文章能够满足您的需求,如果您有任何修改意见或需要进一步的帮助,请随时告诉我!
感谢各位支持,可以关注我的个人主页,找到你所需要的宝贝。
博文入口:https://blog.csdn.net/Start_mswin 复制到【浏览器】打开即可,宝贝入口:https://pan.quark.cn/s/b42958e1c3c0 宝贝:https://pan.quark.cn/s/1eb92d021d17
作者郑重声明,本文内容为本人原创文章,纯净无利益纠葛,如有不妥之处,请及时联系修改或删除。诚邀各位读者秉持理性态度交流,共筑和谐讨论氛围~
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)