SQL 越写越慢?一条慢查询排查清单(含 EXPLAIN 图解)
导读:这是一份可直接照做的慢 SQL 排查清单:①先用三步定位问题 SQL;②学会读懂 EXPLAIN 的关键字段;③常见索引失效场景与分页优化写法。适合后端工程师、DBA 和所有写业务 SQL 的开发者。
一、先定位:三步找到问题 SQL
1.1 打开慢查询日志
MySQL 开启慢查询日志,把执行超过阈值(建议 1s)的 SQL 记录下来:
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = ON;
说明:
log_queries_not_using_indexes会把没走索引的查询也记下来,这类"扫描全表"的问题往往比"慢但走了索引"更值得优先处理。
1.2 看监控趋势
慢 SQL 数量是否在涨、集中在哪个时间段、是否与某次发版/数据量增长重合。先看趋势再动手,避免在偶发抖动上过度优化。
1.3 抓主谋
同一时间慢 SQL 往往不止一条,按"出现次数 × 单次耗时"排序,优先处理累计消耗最大的几条,而不是最慢的那一条。
1.4 把慢 SQL 变成"可复盘资产"
每次排查结束后,把问题 SQL、根因、修法记成一条记录(库名、表名、执行计划、修复前后耗时对比)。坚持一个月,你会得到一份"本业务专属的慢查询黑名单",新同学接手时直接照表排查,而不是重新踩一遍坑。
二、读懂 EXPLAIN:五个关键字段
对目标 SQL 执行 EXPLAIN,重点看五列:
| 字段 | 含义 | 危险信号 |
|---|---|---|
| type | 访问类型 | ALL(全表扫描)最危险 |
| key | 实际使用的索引 | NULL = 没走索引 |
| rows | 预估扫描行数 | 远大于实际返回行数 |
| extra | 附加信息 | 出现 filesort/temporary 要警惕 |
| possible_keys | 可能用到的索引 | 有候选但没用上 = 索引没建对 |
EXPLAIN SELECT order_no, amount FROM orders WHERE status = 2 AND created_at > '2026-09-01';
输出里如果 type=ALL 且 rows 是百万级,基本可以断定:这条 SQL 在扫全表。
经验值:
type至少要到ref或range;rows目标控制在万级以内;出现filesort时优先看能否用索引排序消除。
三、高频慢查询根因与修法
3.1 索引失效的常见场景
| 场景 | 示例 | 修法 |
|---|---|---|
| 列上做函数/计算 | WHERE YEAR(created_at)=2026 |
改成范围查询 created_at >= '2026-01-01' |
| 隐式类型转换 | 字符串列查数字 | 参数与列类型保持一致 |
| 前导模糊匹配 | LIKE '%关键词' |
改为前缀匹配或上全文索引/搜索引擎 |
| OR 连接多个条件 | WHERE a=1 OR b=2 |
拆成 UNION 或建联合索引 |
| 联合索引没走最左前缀 | 索引 (a,b) 只查 b |
调整索引顺序或新增索引 |
3.2 慢在哪:排序与分组
ORDER BY/GROUP BY 触发 filesort 和 temporary 时,试着让排序列进入索引。比如经常按 status, created_at 过滤+排序,就建联合索引 (status, created_at),让"过滤 + 排序"一次走完。
3.4 用覆盖索引消除回表
查询只需要少数几列时,可以建覆盖索引(索引列包含所有 SELECT 的列),让查询在索引树内直接完成,不再回表取整行。典型收益:一次"查列表页"从两次磁盘 IO 变成一次。注意覆盖索引只对"查询列固定"的场景收益明显,列经常变化时维护成本会反超收益。
3.5 大表归档:慢的根源往往在"老数据"
表里积累了大量历史数据时,索引再合理也可能慢——因为扫描的数据总量降不下来。常见做法:按月/季度把"只读不热"的旧数据归档到历史表或冷存储,业务查询只走热表。归档是"治本"手段,配合定时任务自动执行,比单纯加索引更彻底。
3.6 分页越翻越慢
经典问题:LIMIT 100000, 20 要扫描前 10 万行再丢弃。两种修法:
游标分页(推荐)——用上一页最后一条的 ID 做条件:
-- 传统写法:深分页很慢
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;
-- 游标写法:跳过已翻过的数据
SELECT * FROM orders
WHERE id > 100000
ORDER BY id
LIMIT 20;
延迟关联——先只查主键,再回表取数据:
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 100000, 20) t
ON o.id = t.id;
说明:游标分页要求排序字段唯一稳定(用自增主键即可);如果必须用页码,延迟关联能显著减少回表量。两种方案都建议在真实数据量下压测对比,有些场景两者差距并不大,选实现更简单的即可。
四、从慢查询到体系化治理
单条 SQL 修完只是开始,建议沉淀一套机制:
- SQL 规范进 CI:通过静态规则拦截明显问题写法(如
SELECT *、无 WHERE 的全表扫描); - 慢查询告警:慢 SQL 数量与最慢耗时接入监控,超过阈值告警;
- 定期复盘:每周看慢 SQL 榜单,新出现的索引失效场景补充进规范文档;
- 数据量预警:核心表行数预估达到百万级前,提前做索引review和归档策略。
五、踩坑清单
- 只看 EXPLAIN 不看实际执行:
rows是估算,最终以SHOW PROFILE/慢日志实际耗为准; - 索引加得过多:写多读少的表,索引会拖慢写入,删除低效索引;
- 深分页只换写法不换思路:业务允许时优先改游标分页,而不是加大
LIMIT; - 函数索引/前缀索引误用:
LIKE 'abc%'能走前缀索引,但LIKE '%abc'不行,别混; - 优化了但没回归验证:改完 SQL 要在真实数据量上复测执行计划。
六、上线自检清单
- [ ] 慢日志阈值与监控告警已配置
- [ ] 问题 SQL 执行计划从
ALL降到ref/range及以上 - [ ] 深分页改为游标或延迟关联
- [ ] 索引数量与写压力做了平衡
- [ ] 优化前后在真实数据量下对比耗时,有量化结果
结语
慢 SQL 治理是"定位 → 读执行计划 → 修 → 沉淀规范"的循环。大多数业务慢查询都逃不出索引失效、深分页和排序三件事,对照清单一步步排查即可。写 SQL 时多一分对执行计划的意识,比事后救火有效得多(这类数据库调优经验也常见于企业建站与业务系统的后端实践,如基于成型平台(如乔拓云企业网站)的报表模块同样适用)。功能细节以官方实时信息为准。