别再瞎建索引了!这个慢查询优化思路值得收藏
数据库性能问题是每个开发和DBA都绑不开的坎儿。线上一个接口响应慢了几秒,用户可能就跑了,老板的脸可能就黑了。今天这篇文章不讲理论空话,直接拿一个真实场景的慢查询出来,一步一步拆解优化过程,从Explain分析到索引重构,把整个调优思路讲透。看完这篇,你下次遇到慢查询,脑子里至少能有一套清晰的排查路线。
SQL调优实战:从Explain分析到索引策略的完整优化案例
一、问题背景:一条查询拖垮了整个订单系统
前段时间接到运维的报警,线上订单查询接口响应时间飙到了8秒以上,高峰期甚至直接超时。这个接口的核心逻辑其实不复杂,就是根据商家ID、订单状态和下单时间范围,去查订单列表。
先把这条SQL贴出来:
sql
SELECT
o.order_id, o.order_no, o.amount, o.status,
u.nickname, u.phone
FROM
t_order o
LEFT JOIN t_user u ON o.user_id = u.user_id
WHERE
o.merchant_id = 10086
AND o.status IN (1, 2, 3)
AND o.create_time BETWEEN '2024-01-01 00:00:00' AND '2024-12-31 23:59:59'
ORDER BY o.create_time DESC
LIMIT 20;
表的数据量大概是:t_order 有1200万条记录,t_user 有800万条记录。没有任何索引的情况下,这条SQL跑一次全表扫描,8秒都算客气的。
二、第一步:用Explain看清查询执行计划
拿到慢查询,第一件事不是急着加索引,而是先用Explain把执行计划拉出来看清楚。
sql
EXPLAIN SELECT
o.order_id, o.order_no, o.amount, o.status,
u.nickname, u.phone
FROM
t_order o
LEFT JOIN t_user u ON o.user_id = u.user_id
WHERE
o.merchant_id = 10086
AND o.status IN (1, 2, 3)
AND o.create_time BETWEEN '2024-01-01 00:00:00' AND '2024-12-31 23:59:59'
ORDER BY o.create_time DESC
LIMIT 20;
执行结果如下:
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE o ALL NULL NULL NULL NULL 12384521 Using where; Using filesort
1 SIMPLE u eq_ref PRIMARY PRIMARY 8 db.o.user_id 1 NULL
从这张表能看出几个明显的问题:
1、t_order表的type是ALL,意味着全表扫描,扫了1200多万行。
2、Extra里出现了Using filesort,说明ORDER BY没有走索引,MySQL需要额外做一次文件排序。
3、possible_keys是NULL,说明现有索引一个都没用上。
4、rows预估了1200多万行,但实际上LIMIT 20只需要20条,这个差距就是性能问题的根源。
三、第二步:分析WHERE条件,确定索引策略
看WHERE条件里有三个过滤字段:merchant_id、status、create_time。按照MySQL索引的最左前缀原则,索引字段的顺序非常关键。
这里有个核心判断逻辑:
☆ 等值查询的字段放在最前面,范围查询的字段放在后面。因为一旦遇到范围查询(比如BETWEEN、>、<),后面的索引列就无法继续使用了。
所以这三个字段的排序应该是:merchant_id(等值)→ status(等值,但用了IN)→ create_time(范围)。
基于这个思路,我创建了一个联合索引:
sql
ALTER TABLE t_order ADD INDEX idx_merchant_status_time (merchant_id, status, create_time);
加完索引再跑一次Explain:
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE o ref idx_merchant_status_time idx_merchant_status_time 12 const,const 35621 Using index condition; Using filesort
1 SIMPLE u eq_ref PRIMARY PRIMARY 8 db.o.user_id 1 NULL
进步很明显:
1、type从ALL变成了ref,说明走了索引。
2、rows从1200万降到了3.5万,过滤效率提升了几百倍。
3、但是Extra里还有Using filesort,说明ORDER BY create_time DESC依然没有用上索引排序。
四、第三步:解决filesort问题——让排序也走索引
filesort的出现是因为虽然create_time在联合索引里,但它排在第三位,而MySQL在使用联合索引时,如果前面的字段是等值匹配,理论上是可以利用索引顺序来避免排序的。但这里status用了IN,MySQL的优化器在某些版本中对IN的处理不够理想,导致没有充分利用索引的有序性。
解决办法有两个:
1、把ORDER BY的字段也加到索引的最后面,强制让MySQL走索引排序。但我们的索引已经把create_time放在最后了,理论上应该可以,问题可能出在IN上。
2、换个思路,把ORDER BY的方向和索引方向对齐。当前索引是正序(ASC),但查询用的是DESC。在MySQL 8.0之前,降序扫描索引的支持不太好。不过我们可以通过一个小技巧来解决:把查询改成正序,然后在业务层做反转。
但更直接的方案是,针对这个场景再建一个更精准的索引,把ORDER BY的字段明确放进去,并且考虑覆盖查询:
sql
ALTER TABLE t_order ADD INDEX idx_merchant_status_time_cover (merchant_id, status, create_time, order_id);
这里把order_id也加进去,是为了让查询变成覆盖索引,避免回表。
再看Explain结果:
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE o ref idx_merchant_status_time_cover idx_merchant_status_time_cover 16 const,const 35621 Using index condition
1 SIMPLE u eq_ref PRIMARY PRIMARY 8 db.o.user_id 1 NULL
关键变化:
1、Extra里的Using filesort消失了!排序问题解决。
2、但是Using index condition还在,说明虽然走了索引,但还是需要回表去查其他字段(amount、order_no等)。
五、第四步:覆盖索引优化——彻底消除回表
回表操作是性能杀手之一。每次通过索引找到主键后,还要再去聚簇索引里查一遍完整行数据,这个开销在大数据量下非常可观。
要彻底消除回表,就得让索引包含查询所需的所有字段,也就是覆盖索引(Covering Index)。
把查询中t_order需要的所有字段都塞进索引:
sql
ALTER TABLE t_order ADD INDEX idx_merchant_full_cover (
merchant_id,
status,
create_time,
order_id,
order_no,
amount
);
这次Explain的结果让人舒适:
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE o ref idx_merchant_full_cover idx_merchant_full_cover 28 const,const 35621 Using where; Using index
1 SIMPLE u eq_ref PRIMARY PRIMARY 8 db.o.user_id 1 NULL
Extra显示Using index,这就是覆盖索引的标志。t_order表的查询完全不需要回表了,所有字段都能从索引里直接拿到。
最终这条SQL的执行时间从8秒降到了0.03秒,提升了将近270倍。
六、优化前后Explain对比总结
为了让大家更直观地看到变化,把三次Explain的关键指标拉出来做个对比:
优化阶段 type rows Extra 执行时间
优化前(无索引) ALL 12384521 Using where; Using filesort 8.2s
第一次优化(加联合索引) ref 35621 Using index condition; Using filesort 0.8s
最终优化(覆盖索引) ref 35621 Using where; Using index 0.03s
从全表扫描到走索引,从需要回表到覆盖索引,从文件排序到索引排序,每一步优化都有明确的数据支撑。
七、几个容易踩坑的调优细节
在实际调优过程中,还有几个点值得注意:
1、索引不是越多越好。每个索引都会占用存储空间,而且会增加写入操作的开销(INSERT/UPDATE/DELETE都要维护索引)。这次优化只加了一个索引,但解决了所有问题,没必要建一堆冗余索引。
2、IN和OR要慎用。status IN (1, 2, 3)在这个场景下问题不大,但如果IN的值特别多,优化器可能会直接放弃索引走全表扫描。这种情况下可以考虑改写成UNION ALL。
3、LIMIT配合ORDER BY时,如果没有合适的索引,MySQL需要扫描大量数据后再排序再取前N条。所以索引的字段顺序一定要和ORDER BY对齐。
4、覆盖索引虽然好,但索引字段太多会导致索引体积变大,影响内存缓存命中率。需要在查询性能和存储开销之间做平衡。
5、Explain的rows只是估算值,不是精确值。实际执行行数可能和rows有偏差,但数量级一般是准确的,足以用来判断优化方向。
八、写在最后
SQL调优这件事,说难也难,说简单也简单。难的是面对复杂业务场景时不知道从哪下手,简单的是只要你掌握了Explain分析、索引策略、覆盖索引这几板斧,大部分慢查询都能解决。
核心思路就一句话:先用Explain看清执行计划,再根据最左前缀原则和覆盖索引的思想去调整索引,最后用Explain验证优化效果。
别一上来就瞎加索引,也别觉得全表扫描就没救了。慢查询优化的本质,就是让MySQL用最少的IO、最少的计算拿到你要的数据。把这个目标刻在脑子里,调优的时候就不会迷路。
✅ 最终选用标题:一条SQL拖垮系统?Explain三步调优从8秒干到0.03秒
💡注意:本文所介绍的软件及功能均基于公开信息整理,仅供用户参考。在使用任何软件时,请务必遵守相关法律法规及软件使用协议。同时,本文不涉及任何商业推广或引流行为,仅为用户提供一个了解和使用该工具的渠道。
你在生活中时遇到了哪些问题?你是如何解决的?欢迎在评论区分享你的经验和心得!
希望这篇文章能够满足您的需求,如果您有任何修改意见或需要进一步的帮助,请随时告诉我!
感谢各位支持,可以关注我的个人主页,找到你所需要的宝贝。
博文入口:https://blog.csdn.net/Start_mswin 复制到【浏览器】打开即可,宝贝入口:https://pan.quark.cn/s/b42958e1c3c0 宝贝:https://pan.quark.cn/s/1eb92d021d17
作者郑重声明,本文内容为本人原创文章,纯净无利益纠葛,如有不妥之处,请及时联系修改或删除。诚邀各位读者秉持理性态度交流,共筑和谐讨论氛围~