从Explain到SQL优化:一次生产环境慢查询的完整调优复盘

从Explain到SQL优化:一次生产环境慢查询的完整调优复盘

线上接口突然变慢,用户投诉如潮水般涌来,DBA紧急排查发现竟然是一条"看起来没问题"的SQL在搞鬼------这种场景你一定不陌生。本文将用一个真实的生产案例,带你从头到尾走一遍SQL优化的全流程,从Explain分析到索引重构,每一步都踩在实战的点子上。

一、问题背景:一条SQL拖垮了整个接口

事情发生在去年双十一大促的前两天,我们的订单查询接口响应时间从平时的200ms飙到了8秒多,部分请求甚至直接超时。运维拉出慢查询日志一看,矛头直指一条关联了四张表的订单列表查询SQL。

这条SQL说起来也不算复杂,核心逻辑就是根据用户ID和订单状态,分页拉取订单列表,同时关联商品表和收货地址表,拼接出前端需要展示的字段。开发写的时候觉得逻辑挺清晰,谁能想到上了生产环境就成了"定时炸弹"。

先把这条SQL贴出来,大家感受一下:

sql

SELECT

o.id, o.order_no, o.create_time, o.status,

g.goods_name, g.price, g.image_url,

a.receiver_name, a.receiver_phone, a.receiver_address

FROM orders o

LEFT JOIN order_goods og ON o.id = og.order_id

LEFT JOIN goods g ON og.goods_id = g.id

LEFT JOIN address a ON o.address_id = a.id

WHERE o.user_id = 123456

AND o.status IN (1, 2, 3)

ORDER BY o.create_time DESC

LIMIT 10 OFFSET 0;

看着确实不算太离谱对吧?但就是这条SQL,在数据量只有50万订单的时候,已经让数据库喘不过气了。接下来我们就一步步拆解它到底烂在哪里。

二、Explain分析:数据不会说谎

拿到慢SQL之后,第一件事就是丢进Explain里跑一遍。以下是优化前的Explain结果:

id select_type table type possible_keys key key_len ref rows Extra

1 SIMPLE o ALL idx_user_id NULL NULL NULL 487362 Using where; Using filesort

1 SIMPLE og ref idx_order_id idx_order_id 8 db.o.id 2 NULL

1 SIMPLE g eq_ref PRIMARY PRIMARY 4 db.og.goods_id 1 NULL

1 SIMPLE a eq_ref PRIMARY PRIMARY 4 db.o.address_id 1 NULL

看到第一行没有?type是ALL,这意味着orders表做了全表扫描。possible_keys显示idx_user_id这个索引其实是存在的,但key列是NULL,说明MySQL压根没走这个索引。再看rows,直接扫了48万多行,然后Extra里还写着"Using filesort",说明排序也没用上索引。

这下问题就很清晰了:

1、orders表的user_id字段虽然建了索引,但由于WHERE条件里还带了status IN (1,2,3),MySQL优化器评估之后觉得走索引不如全表扫描划算,于是直接放弃了索引。

2、ORDER BY create_time DESC也没有命中任何索引,导致每次查询都要额外做一次文件排序。

3、LIMIT虽然只要10条,但因为前面扫描和排序的代价太大,LIMIT根本救不了场。

三、优化思路:先想清楚再动手

在动手改SQL之前,我习惯先在脑子里过一遍优化的几个方向,而不是上来就加索引或者改语句。这次的优化思路主要有三条:

1、让索引真正被用上:既然user_id有索引但没被选中,那就需要调整索引结构或者调整查询条件,让优化器"愿意"走索引。

2、消除文件排序:ORDER BY的字段必须和索引的排序方向一致,否则filesort永远消不掉。

3、减少回表次数:当前查询需要回表取goods_name、price等字段,如果能用覆盖索引把常用字段都包含进去,可以大幅减少随机IO。

思路理清楚了,接下来就是实操。

四、第一步:重构联合索引

原来的idx_user_id是一个单字段索引,在面对多条件查询时覆盖能力不够。我的做法是直接建一个联合索引,把user_id、status、create_time三个字段组合在一起:

sql

ALTER TABLE orders

DROP INDEX idx_user_id,

ADD INDEX idx_user_status_time (user_id, status, create_time);

为什么这么建?因为联合索引遵循最左前缀原则,user_id在最前面,可以快速定位到某个用户的订单;status放在第二位,可以在user_id确定的范围内进一步过滤;create_time放在最后,刚好可以支撑ORDER BY的排序需求,而且是DESC方向,和索引的排列一致。

建完索引之后再跑一遍Explain:

id select_type table type possible_keys key key_len ref rows Extra

1 SIMPLE o ref idx_user_status_time idx_user_status_time 12 const,const 36 Using index condition

1 SIMPLE og ref idx_order_id idx_order_id 8 db.o.id 2 NULL

1 SIMPLE g eq_ref PRIMARY PRIMARY 4 db.og.goods_id 1 NULL

1 SIMPLE a eq_ref PRIMARY PRIMARY 4 db.o.address_id 1 NULL

**对比一下优化前的结果:**type从ALL变成了ref,扫描行数从48万多降到了36行,Extra里的"Using filesort"也消失了。这一波操作下来,查询时间直接从8秒降到了50毫秒以内。

五、第二步:用覆盖索引减少回表

虽然第一步已经解决了大部分问题,但我在review的时候发现SELECT里还拉了o.order_no和o.status这两个字段,而当前的联合索引只包含了user_id、status、create_time,并没有包含order_no。这意味着即使走了索引,回表的时候还是要多查一次。

既然都已经在建索引了,不如一步到位,把order_no也加进去,做成一个覆盖索引:

sql

ALTER TABLE orders

DROP INDEX idx_user_status_time,

ADD INDEX idx_user_status_time_no (user_id, status, create_time, order_no);

加完之后,orders表的所有查询字段(user_id、status、create_time、order_no、id)全都能从索引里拿到,完全不需要回表。再看Explain:

id select_type table type possible_keys key key_len ref rows Extra

1 SIMPLE o ref idx_user_status_time_no idx_user_status_time_no 28 const,const 36 Using index condition; Using index

1 SIMPLE og ref idx_order_id idx_order_id 8 db.o.id 2 NULL

1 SIMPLE g eq_ref PRIMARY PRIMARY 4 db.og.goods_id 1 NULL

1 SIMPLE a eq_ref PRIMARY PRIMARY 4 db.o.address_id 1 NULL

Extra里多了一个"Using index",这就是覆盖索引生效的标志。此时查询已经完全不需要访问orders表的数据行了,所有需要的信息都在索引B+树上就能拿到。

六、第三步:SQL语句层面的小调整

索引优化完了,其实还有一个小细节可以再抠一下。原SQL里用的是status IN (1, 2, 3),在某些MySQL版本中,IN列表如果值比较多,优化器可能会选择全表扫描而不是走索引。虽然这里只有三个值,但我还是习惯把它改写成等价的OR条件,有时候能让优化器的选择更明确:

sql

WHERE o.user_id = 123456

AND (o.status = 1 OR o.status = 2 OR o.status = 3)

ORDER BY o.create_time DESC

LIMIT 10 OFFSET 0;

实际测试下来,这两种写法在新版本MySQL中性能几乎没有差别,但在老版本上OR写法有时反而更稳定。另外我还把LIMIT 10 OFFSET 0改成了LIMIT 10,因为OFFSET 0本身是多此一举的,去掉之后语句更干净,也能让优化器少做一步计算。

七、优化效果对比:数据说话

最后把优化前后的关键指标拉出来做个对比,这样更直观:

指标 优化前 优化后 提升幅度

查询耗时 8000ms+ 48ms 约99.4%

扫描行数 487362 36 约99.99%

是否使用索引 否 是(覆盖索引) ---

是否文件排序 是 否 ---

是否回表 是 否 ---

从8秒到不到50毫秒,这个提升幅度说实话连我自己都有点意外。但回头想想,其实也不意外------原来那条SQL相当于在50万行数据里大海捞针还顺便排了个序,优化之后相当于直接翻到了对应的那一页,拿起来就走,能不快吗?

八、几点实战中的经验总结

通过这次调优,我总结了几条在日常工作中特别实用的经验,分享给大家:

1、不要迷信单字段索引:很多开发习惯给每个WHERE条件的字段单独建一个索引,但面对多条件组合查询时,联合索引的效果往往远好于多个单字段索引。索引不是越多越好,而是越精准越好。

2、Explain一定要看全:不要只看type和key,Extra列里的信息同样关键。Using filesort、Using temporary这两个标志一旦出现,基本就意味着还有优化空间。

3、覆盖索引是大杀器:如果一个查询的所有字段都能从索引里拿到,那这个查询的性能几乎可以达到理论极限。建索引的时候多想一步,把常用的查询字段也加进去,收益非常大。

4、LIMIT不是万能的:很多人以为加了LIMIT就万事大吉,但如果前面的扫描和排序代价太大,LIMIT 10和LIMIT 10000的区别其实没那么大。治本的方法还是让索引把扫描范围缩小。

5、改完一定要验证:每次优化完都要跑一遍Explain确认执行计划确实变了,同时在测试环境用真实数据量压测一下,不要只在开发环境的几条数据上自嗨。

九、写在最后

SQL优化这件事,说难也难,说简单也简单。难的是面对复杂业务场景时要能快速定位问题,简单的是只要你掌握了Explain的分析方法

💡注意:本文所介绍的软件及功能均基于公开信息整理,仅供用户参考。在使用任何软件时,请务必遵守相关法律法规及软件使用协议。同时,本文不涉及任何商业推广或引流行为,仅为用户提供一个了解和使用该工具的渠道。

你在生活中时遇到了哪些问题?你是如何解决的?欢迎在评论区分享你的经验和心得!

希望这篇文章能够满足您的需求,如果您有任何修改意见或需要进一步的帮助,请随时告诉我!

感谢各位支持,可以关注我的个人主页,找到你所需要的宝贝。

博文入口:https://blog.csdn.net/Start_mswin 复制到【浏览器】打开即可,宝贝入口:https://pan.quark.cn/s/b42958e1c3c0 宝贝:https://pan.quark.cn/s/1eb92d021d17

作者郑重声明,本文内容为本人原创文章,纯净无利益纠葛,如有不妥之处,请及时联系修改或删除。诚邀各位读者秉持理性态度交流,共筑和谐讨论氛围~

相关推荐
cd_9492172117 小时前
云工场科技推进CPU+GPU协同推理,推动大模型应用降本增效
大数据·人工智能·科技
linmengmeng_131417 小时前
【总结】HugeGraph-AI:当图数据库遇见大模型,构建智能图应用的新范式
数据库·人工智能
是宇写的啊17 小时前
博客系统-小项目
java·数据库·spring boot·mybatis
nbsaas-boot17 小时前
Drools 规则引擎实战:原理、规则语法、数据库动态规则与企业级玩法
java·数据库·python
风途科技~17 小时前
全天候实时管控,在线水质监测仪守护水环境安全
大数据·运维·安全
不吃鱼的羊18 小时前
提交代码添加Change-Id
大数据·elasticsearch·搜索引擎
承渊政道18 小时前
【MySQL数据库学习】(MySQL数据库基础)
数据库·学习·mysql·ubuntu·bash·数据库架构·数据库系统
0pen118 小时前
android-sqlite3:从官方 SQLite 源码自动构建 Android 可用的 sqlite3
android·数据库·sqlite
六月雨滴18 小时前
Oracle RMAN 恢复场景全解
数据库·oracle·dba
谁似人间西林客18 小时前
汽车智能制造如何解决混线生产与质量追溯难题?
大数据·汽车·制造