线上SQL拖垮整个系统,我用Explain十分钟就救回来了
上周四凌晨两点,手机突然炸了,运维群里十几条@我的消息刷屏了。线上订单系统响应慢得离谱,用户下单接口超时率直接飙到40%。我赶紧登上服务器,一顿排查下来,发现罪魁祸首竟然是一条再普通不过的SELECT语句。这条SQL在测试环境跑得溜溜的,上了生产环境就原形毕露,直接把数据库CPU干到98%。其实这种事我这些年见太多了,SQL写的时候觉得挺顺眼,上线之后才发现是个大坑。今天就把我这些年踩过的坑、总结
线上SQL拖垮整个系统,我用Explain十分钟就救回来了

上周四凌晨两点,手机突然炸了,运维群里十几条@我的消息刷屏了。线上订单系统响应慢得离谱,用户下单接口超时率直接飙到40%。我赶紧登上服务器,一顿排查下来,发现罪魁祸首竟然是一条再普通不过的SELECT语句。这条SQL在测试环境跑得溜溜的,上了生产环境就原形毕露,直接把数据库CPU干到98%。其实这种事我这些年见太多了,SQL写的时候觉得挺顺眼,上线之后才发现是个大坑。今天就把我这些年踩过的坑、总结出来的方法论,一次性全掏出来给大家看看,特别是怎么用Explain做前后对比、怎么选索引策略,都是实打实的经验。

一、用Explain对比找瓶颈:慢查询的照妖镜
很多开发者对SQL优化有个误解,觉得那是DBA的事,自己只管写业务逻辑就行。但真实情况是,80%以上的慢查询问题,都是开发阶段埋下的雷。想要提前排雷,你至少得学会看懂Explain的输出结果。
1、Explain到底告诉你什么
在任何SELECT语句前面加上EXPLAIN,MySQL就会把这条SQL的执行计划吐出来。关键字段不多,但每个都很重要:
字段名 含义 重点关注点
id 执行顺序编号 id越大越先执行,相同id从上到下执行
select_type 查询类型 出现"DEPENDENT SUBQUERY"说明有相关子查询,要警惕
type 访问类型 性能从好到差依次为:system > const > eq_ref > ref > range > index > ALL
possible_keys 可能用到的索引 看看有没有命中你预期的索引
key 实际使用的索引 和possible_keys对比,看数据库到底选了哪个
key_len 索引使用的字节长度 越短说明索引利用率越高
rows 预估扫描行数 这个数字越小越好,直接反映查询代价
Extra 额外信息 出现"Using filesort""Using temporary"就是红旗警告
☆ 比如下面这条SQL,看起来没什么毛病对吧?
sql
EXPLAIN SELECT * FROM orders WHERE YEAR(create_time) = 2024;
执行完一看,type是ALL,也就是全表扫描,rows可能是几十万甚至上百万。Extra里面会出现"Using where",说明连索引都没碰着。问题出在哪儿?WHERE条件里对create_time字段做了YEAR()函数运算,直接把索引干废了。这就是最典型的"函数操作导致索引失效"。
2、优化前后用Explain对比,数据不说谎
调优最靠谱的办法就是做对比。改之前跑一次Explain,改完之后再跑一次,数字一对比,效果一目了然。
sql
-- 优化前:全表扫描,扫描行数485万
EXPLAIN SELECT * FROM orders
WHERE YEAR(create_time) = 2024 AND status = 1;
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE orders ALL NULL NULL NULL 4852310 Using where
sql
-- 优化后:走索引范围扫描,扫描行数降到1.5万
EXPLAIN SELECT * FROM orders
WHERE create_time >= '2024-01-01'
AND create_time < '2025-01-01'
AND status = 1;
id select_type table type possible_keys key key_len rows Extra
1 SIMPLE orders range idx_create_time idx_create_time 8 15234 Using index condition
rows从485万降到了1.5万,type从ALL变成了range。同样一条业务逻辑的查询,性能差了几百倍,就因为改写了一下WHERE条件。这就是Explain对比的威力。

二、索引策略示例:建了索引不等于有了性能
我见过太多项目,表上挂了十几个索引,查询该慢还是慢。索引这东西,用对了是加速器,用错了就是累赘。
1、最左前缀原则是铁律,没得商量
假设你建了一个联合索引idx_a_b_c,包含(a, b, c)三个字段。那么查询条件必须从最左边的a开始匹配,索引才能生效。
sql
-- 联合索引 idx_a_b_c (a, b, c)
-- ✅ 能用上索引
SELECT * FROM t WHERE a = 1;
SELECT * FROM t WHERE a = 1 AND b = 2;
SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3;
-- ❌ 用不上索引
SELECT * FROM t WHERE b = 2;
SELECT * FROM t WHERE b = 2 AND c = 3;
SELECT * FROM t WHERE c = 3;
☆ 这个规则说起来简单,但实际开发中踩坑的人特别多。有些同学写WHERE条件的时候喜欢把顺序随便排,觉得反正都是AND连接,谁前谁后没区别。但数据库的B+树不这么想,它只认最左边那一列。你跳过a直接查b,就等于告诉数据库:索引我不用了,你自己看着办吧。
2、索引覆盖能省掉回表,这个提升很实在
所谓索引覆盖,就是查询需要的所有字段都包含在索引里面,数据库不需要再根据主键去查原始数据行,也就是不需要"回表"。
sql
-- 假设有联合索引 idx_user_status (user_id, status, create_time)
-- ✅ 索引覆盖,Extra显示"Using index"
SELECT user_id, status, create_time
FROM orders
WHERE user_id = 1001 AND status = 1;
-- ❌ 需要回表,因为order_amount不在索引里
SELECT user_id, status, create_time, order_amount
FROM orders
WHERE user_id = 1001 AND status = 1;
如果Explain的Extra字段显示"Using index",恭喜你,触发了索引覆盖,这是性能最好的情况之一。
3、索引不是越多越好,这是个坑
每多建一个索引,INSERT、UPDATE、DELETE的代价就会增加,因为数据库要同时维护更多的索引树。我的经验是,一张业务表的索引尽量控制在5个以内。如果是那种读多写少的报表类表,可以适当放宽,但也别超过8个。

三、查询优化案例:三个生产环境的真实翻车现场
光讲理论不过瘾,下面直接上三个我实际处理过的案例,都是线上真实发生的。
1、案例一:深分页把数据库拖死了
有个运营后台需要展示订单列表,支持分页。原来的SQL是这么写的:
sql
SELECT * FROM orders
WHERE status = 1
ORDER BY create_time DESC
LIMIT 100000, 20;
前几页还好,翻到第500页以后就开始卡。原因很简单:LIMIT的偏移量太大,数据库需要先扫描100020行,然后扔掉前面100000行,只返回后面20行。越往后翻,扫描的行数越多。
☆ 优化方案是用游标分页代替偏移分页:
sql
-- 记录上一页最后一条记录的create_time和id
SELECT * FROM orders
WHERE status = 1
AND (create_time < '2024-06-01 10:00:00'
OR (create_time = '2024-06-01 10:00:00' AND id < 99999))
ORDER BY create_time DESC, id DESC
LIMIT 20;
不管翻到第几页,都只扫描20行数据,性能非常稳定。
2、案例二:IN子查询改成JOIN,性能翻了20倍
有条统计SQL需要查询在某个时间段内下过单的用户:
sql
-- 优化前:IN子查询,执行时间约3.2秒
SELECT * FROM users
WHERE id IN (
SELECT user_id FROM orders
WHERE create_time > '2024-01-01'
);
Explain显示子查询部分type是ALL,也就是全表扫描。
sql
-- 优化后:改成JOIN,执行时间降到0.15秒
SELECT DISTINCT u.*
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.create_time > '2024-01-01';
不过这里要注意一点:如果orders表里同一个user_id有多条记录,必须加DISTINCT或者用GROUP BY去重,不然结果会膨胀,数据就不对了。
3、案例三:LIKE模糊查询的前导通配符是索引杀手
sql
-- 优化前:%在最前面,索引完全失效
SELECT * FROM products
WHERE name LIKE '%手机%';
这条SQL的type是ALL,因为%在最前面,B+树根本没法用。你想想,B+树是按顺序排的,你让它找"中间包含某个词"的记录,它只能从头到尾扫一遍。
sql
-- 优化后:如果业务允许,改成后缀匹配
SELECT * FROM products
WHERE name LIKE '华为%';
如果业务确实需要支持任意位置的模糊搜索,那就别在MySQL上硬扛了,老老实实上Elasticsearch或者走全文索引Fulltext Index。

四、几个容易被忽略但很要命的细节
1、SELECT * 是慢性毒药
这话都说烂了,但还是有人不当回事。SELECT *不光增加网络传输开销,更要命的是会让索引覆盖失效。你明明只需要5个字段,结果把整行数据都拉出来了,IO代价直接翻倍。
2、JOIN的时候让小表驱动大表
MySQL的嵌套循环JOIN算法,是用外层表的每一行去内层表里找匹配。所以外层表越小,循环次数就越少,性能就越好。
sql
-- ✅ 小表users(1万行)驱动大表orders(500万行)
SELECT * FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- ❌ 反过来写,性能差很多
SELECT * FROM orders o
INNER JOIN users u ON o.user_id = u.id;
3、MySQL 8.0以上一定要用EXPLAIN ANALYZE
普通的EXPLAIN给出的rows是估算值,跟实际情况可能差很远。如果你用的是MySQL 8.0以上版本,强烈建议换成EXPLAIN ANALYZE,它会真正把SQL跑一遍,给你真实的执行时间和实际扫描行数。
sql
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE create_time >= '2024-01-01' AND status = 1;
输出结果里会有actual time字段,精确到毫秒,比普通Explain靠谱太多了。

五、总结:SQL调优的核心就三步
干了这么多年,我总结下来SQL调优就三步,非常简单:
1、先用Explain找到瓶颈,看type是不是ALL,看Extra有没有"Using filesort"或"Using temporary"这种红旗警告;
2、根据瓶颈选择对应策略——缺索引就建索引,索引没选对就调整联合索引的字段顺序,能用覆盖索引就尽量用;
3、改完之后一定要用Explain对比,用数据说话,别靠感觉。
☆ SQL优化这件事没有什么银弹,但只要你养成看Explain的习惯,遇到慢查询就不会慌。大部分问题都能在开发阶段就解决掉,别等到线上出了事故才着急,那时候再改就被动了。

💡注意:本文所介绍的软件及功能均基于公开信息整理,仅供用户参考。在使用任何软件时,请务必遵守相关法律法规及软件使用协议。同时,本文不涉及任何商业推广或引流行为,仅为用户提供一个了解和使用该工具的渠道。
你在生活中时遇到了哪些问题?你是如何解决的?欢迎在评论区分享你的经验和心得!
希望这篇文章能够满足您的需求,如果您有任何修改意见或需要进一步的帮助,请随时告诉我!
感谢各位支持,可以关注我的个人主页,找到你所需要的宝贝。
博文入口:https://blog.csdn.net/Start_mswin 复制到【浏览器】打开即可,宝贝入口:https://pan.quark.cn/s/b42958e1c3c0 宝贝:https://pan.quark.cn/s/1eb92d021d17
作者郑重声明,本文内容为本人原创文章,纯净无利益纠葛,如有不妥之处,请及时联系修改或删除。诚邀各位读者秉持理性态度交流,共筑和谐讨论氛围~
📋 复制整篇文章
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)