我用Explain揪出了藏了半年的性能杀手
我用Explain揪出了藏了半年的性能杀手
同样一条SQL语句,有人写完跑了30秒还在转圈,有人花5分钟调优后0.02秒就出结果。这中间差的不是运气,是方法。今天这篇文章,我把这些年踩过的坑、总结出的SQL优化思路,一次性全掏给你。不讲虚的,全是实战案例和可直接复用的代码,看完就能上手。
一、SQL优化核心思路与Explain对比分析
1、为什么要学Explain
很多人写完SQL就觉得完事了,至于为什么慢、慢在哪儿,完全靠猜。其实MySQL早就给我们准备好了一把"透视镜"——EXPLAIN命令。它能让你看到查询执行计划的每一个细节:全表扫描还是走了索引、扫描了多少行、有没有用到临时表。说白了,不会看Explain,优化SQL就跟闭着眼开车一样。
下面这张表是我整理的Explain关键字段含义对照,建议收藏:
字段名 含义说明 关注重点
id 查询的序列号,相同id表示从属关系 复杂查询时注意执行顺序
select_type 查询类型(SIMPLE、PRIMARY、SUBQUERY等) 判断是否有子查询
table 当前操作的表名 —
type 访问类型,性能从好到差:system > const > eq_ref > ref > range > index > ALL 最核心字段,至少要达到range级别
possible_keys 可能用到的索引 看有没有命中预期索引
key 实际使用的索引 和possible_keys对比看是否选对了
key_len 索引使用的字节数 判断联合索引是否完全命中
rows 预估需要扫描的行数 越小越好
Extra 额外信息 出现Using filesort、Using temporary时要警惕
2、一个真实的Explain对比案例
我之前接手过一个订单查询接口,原始SQL是这样的:
sql
SELECT o.order_id, o.user_id, o.amount, u.nickname, u.level
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
WHERE o.status = 1
AND o.create_time >= '2025-01-01'
ORDER BY o.create_time DESC
LIMIT 20;
这条SQL在数据量达到200万行时,查询耗时超过8秒。我用EXPLAIN看了一下执行计划:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE o ALL idx_status NULL NULL 1850000 Using where; Using filesort
1 SIMPLE u eq_ref PRIMARY PRIMARY 4 1 —
问题一目了然:orders表走了全表扫描(type=ALL),扫描了185万行,还用了filesort排序。这能不慢吗?
优化之后的SQL:
sql
SELECT o.order_id, o.user_id, o.amount, u.nickname, u.level
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
WHERE o.status = 1
AND o.create_time >= '2025-01-01'
ORDER BY o.create_time DESC
LIMIT 20;
我做了两件事:第一,在orders表上建了联合索引idx_status_createtime (status, create_time);第二,SQL本身没有改,因为逻辑是对的,问题出在索引上。
再次EXPLAIN:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE o range idx_status_createtime idx_status_createtime 9 3200 Using index condition
1 SIMPLE u eq_ref PRIMARY PRIMARY 4 1 —
type从ALL变成了range,扫描行数从185万降到3200,查询时间直接从8秒掉到0.03秒。这就是索引策略带来的降维打击。
二、索引策略示例:联合索引到底怎么建
1、最左前缀原则是铁律
联合索引(a, b, c),查询条件必须从最左边开始匹配才能生效。WHERE a = 1 AND b = 2能用到索引,但WHERE b = 2 AND c = 3就用不上。很多人建索引喜欢把常用字段全塞进去,结果索引又大又慢,因为MySQL只能从最左边开始用。
举个例子,我见过一个同学在用户表上建了(age, city, gender, name)四列联合索引,结果他的查询是:
sql
SELECT * FROM users WHERE city = 'Beijing' AND gender = 1;
这条SQL压根用不上这个索引,因为最左边的age没有出现。后来我建议他把city放到最左边,改成(city, gender, age, name),问题立刻解决。
2、覆盖索引能省掉回表操作
回表这个概念很多人知道但不重视。简单说,如果查询需要的字段都在索引里,MySQL就不需要再去查主键索引(也就是不需要"回表"),这叫覆盖索引。
比如上面的订单查询,我把索引改成(status, create_time, order_id, user_id, amount),把SELECT要的字段都包进去,EXPLAIN的Extra里就会出现Using index,说明完全不需要回表:
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE o range idx_status_createtime idx_status_createtime 9 3200 Using index condition; Using index
Extra里同时出现Using index condition和Using index,这就是覆盖索引的标志。
3、索引不是越多越好
这个坑我自己也踩过。曾经有张表我建了6个索引,以为查什么都快。结果写入性能暴跌,因为每插入一行数据,MySQL要维护6棵B+树。后来我用SHOW INDEX FROM table_name把所有索引列出来,发现有3个索引的使用频率极低,直接删掉之后,写入速度提升了40%,查询性能几乎没受影响。
三、查询优化案例:那些年我遇到的奇葩慢查询
1、LIKE左模糊是索引杀手
sql
SELECT * FROM users WHERE name LIKE '%张%';
这条SQL如果name上有普通索引,基本等于没用。因为B+树是按前缀排序的,你让它从中间开始找,它只能全表扫描。解决办法有两个:要么用全文索引(FULLTEXT),要么改业务逻辑,比如让用户输入姓名的前几个字来搜。
2、OR条件可能导致索引失效
sql
SELECT * FROM orders WHERE status = 1 OR status = 3;
很多人以为status上有索引就能用,但实际上当OR两边的字段不一致时,MySQL可能会放弃索引。更好的写法是用UNION ALL:
sql
SELECT * FROM orders WHERE status = 1
UNION ALL
SELECT * FROM orders WHERE status = 3;
这样每条分支都能走索引,实际测试下来比OR快了将近3倍。
3、子查询改写成JOIN
这个案例我印象特别深。有条SQL是这样的:
sql
SELECT * FROM orders
WHERE user_id IN (
SELECT user_id FROM users WHERE level >= 5
);
当users表有10万行、orders表有200万行时,这条SQL跑了15秒。把它改成JOIN:
sql
SELECT o.*
FROM orders o
INNER JOIN users u ON o.user_id = u.user_id
WHERE u.level >= 5;
同样的逻辑,改完之后只要0.8秒。原因是MySQL对子查询的优化在老版本里很差,JOIN的执行计划通常更优。
四、Explain实战对比:优化前后差距有多大
我把前面几个案例的优化前后数据汇总成表格,这样对比更直观:
案例 优化前type 优化前rows 优化后type 优化后rows 耗时对比
订单查询 ALL 1850000 range 3200 8s → 0.03s
姓名模糊搜索 ALL 500000 fulltext 1 4s → 0.01s
OR条件查询 ALL 320000 ref 160000 2.5s → 0.8s
子查询改写 ALL 2000000 ref 48000 15s → 0.8s
你看,同样是SQL调优,思路对了,性能提升不是百分之几十的事,是几十倍甚至上千倍的事。
五、日常优化的几个实用习惯
☆ 1、慢查询日志一定要开。在my.cnf里设置slow_query_log = 1和long_query_time = 1,把超过1秒的查询都记下来,定期分析。
☆ 2、建索引之前先用EXPLAIN验证。不要凭感觉建索引,先写好SQL,看执行计划,确认索引能被用到再建。
☆ 3、定期用ANALYZE TABLE table_name更新统计信息。MySQL的查询优化器依赖统计信息来选执行计划,数据变动大了但统计信息没更新,就可能选错索引。
☆ 4、能用EXISTS就别用IN。当子查询结果集很大时,EXISTS一旦找到匹配就会停止扫描,而IN要把所有结果都拿出来比对。
六、写在最后
SQL优化这件事,说难也难,说简单也简单。难的是每个业务场景都不一样,需要具体问题具体分析;简单的是方法论就那么几条:看Explain、建对索引、少用函数、避免全表扫描。把这几条刻进脑子里,遇到慢查询的时候就不会慌。
希望这篇文章能帮你少走一些弯路。如果你手里也有经典的SQL优化案例,欢迎在评论区一起交流。
💡注意:本文所介绍的软件及功能均基于公开信息整理,仅供用户参考。在使用任何软件时,请务必遵守相关法律法规及软件使用协议。同时,本文不涉及任何商业推广或引流行为,仅为用户提供一个了解和使用该工具的渠道。
你在生活中时遇到了哪些问题?你是如何解决的?欢迎在评论区分享你的经验和心得!
希望这篇文章能够满足您的需求,如果您有任何修改意见或需要进一步的帮助,请随时告诉我!
感谢各位支持,可以关注我的个人主页,找到你所需要的宝贝。
博文入口:https://blog.csdn.net/Start_mswin 复制到【浏览器】打开即可,宝贝入口:https://pan.quark.cn/s/b42958e1c3c0 宝贝:https://pan.quark.cn/s/1eb92d021d17
作者郑重声明,本文内容为本人原创文章,纯净无利益纠葛,如有不妥之处,请及时联系修改或删除。诚邀各位读者秉持理性态度交流,共筑和谐讨论氛围~