数据库慢查询治理:从 EXPLAIN 到查询重写的完整流程
慢查询治理需遵循先定位、再解释、后重写、终验证的四步闭环,即用慢查询日志并按总耗时排序锁定真正耗时的 SQL,用 EXPLAIN 重点查看 type、key、rows、filtered、Extra 判断瓶颈,再对照索引缺失、回表过多、排序临时表或索引失效等病征归因,最后才修改 SQL 或索引并回归验证,避免凭感觉加索引导致无效优化。
慢查询治理的标准流程是「先定位、再解释、后重写、终验证」四步闭环:用慢查询日志锁定真正耗时的 SQL,用 EXPLAIN 读出执行计划,判断瓶颈属于索引缺失、回表过多、排序临时表还是索引失效,最后才动 SQL 或索引,并回归验证。跳过任何一步,都容易变成「凭感觉加索引」的无效优化。
第一步:先定位——别猜,让慢日志告诉你是哪条 SQL
结论:优化的起点是慢查询日志,而不是你最怀疑的那条 SQL。很多人凭感觉给某张表加索引,结果真正的耗时来自另一条统计类查询。
开启方式(MySQL 5.7 及以上通用):
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 先设 1 秒,别一上来就 0.1
SET GLOBAL log_queries_not_using_indexes = ON; -- 小表全扫会刷屏,调完记得关
拿到日志后用 mysqldumpslow -s t -t 20 slow.log 或 pt-query-digest 按「总耗时」排序,而不是按单次耗时。一条 5ms 但每秒执行 3000 次的查询,危害远大于偶发的 3 秒查询。
如果没法改配置,直接查 performance_schema.events_statements_summary_by_digest,按 SUM_TIMER_WAIT 排序同样有效。
第二步:读懂 EXPLAIN,只看 5 个字段就够
结论:EXPLAIN 里 90% 的信息量集中在 type、key、rows、filtered、Extra 这五列,其余可以先不看。
type访问类型,从优到劣:system > const > eq_ref > ref > range > index > ALL。出现ALL就是全表扫描,是重点嫌疑对象;index是全索引扫描,同样要警惕。key实际用到的索引。如果是NULL,索引压根没生效。rows预估扫描行数,与实际行数差一个数量级,通常意味着统计信息过期,可以跑ANALYZE TABLE。Extra是信息最密集的一列:Using filesort(需要额外排序)、Using temporary(用了临时表)、Using index(覆盖索引,好现象)、Using where配合高 rows 说明过滤发生在回表之后。
补充一句:MySQL 5.7 没有 EXPLAIN ANALYZE,只有 8.0.18 及以上才支持真实执行耗时对比,5.7 环境请用 EXPLAIN FORMAT=JSON 看 cost_info。
第三步:按病征归类,慢查询基本逃不出这四类
结论:把 EXPLAIN 结果对照下面四类病征,通常能直接锁定病因,不需要逐条试错。
- 索引缺失或选择性差。联合索引要遵守最左前缀,
WHERE a=? AND b=?却建了(b,a)就用不上。 - 回表次数过多。
Using index condition说明还在回表取数据,把 SELECT 的字段补进索引做成覆盖索引即可消除。 - 排序、分组产生临时表。
ORDER BY create_time DESC LIMIT 20如果 create_time 上没有可用的有序索引,就会走 filesort。 - 索引失效。字段上套函数(
WHERE DATE(create_time)=?)、隐式类型转换(字符串列传数字)、LIKE '%关键词'前导通配符,这三种写法都会让索引直接作废。改写为范围查询create_time >= ? AND create_time < ?即可恢复。
以典型的社区帖子列表为例,「按版块筛选 + 按时间倒序 + 分页」这种组合查询,建 (版块ID, 创建时间) 联合索引就能同时解决过滤和排序,避免 filesort。
第四步:查询重写——不改业务语义,只换写法
结论:加索引解决不了的问题,多半要靠重写 SQL 解决,最常见的三类是深分页、子查询和大 IN 列表。
深分页:LIMIT 100000, 20 会先扫 100020 行再丢弃。两种改法——延迟关联:
SELECT t.* FROM article t
JOIN (SELECT id FROM article ORDER BY id LIMIT 100000, 20) x ON t.id = x.id;
或者游标分页:WHERE id > 上一页最大id ORDER BY id LIMIT 20,翻到第几千页都不会变慢。
子查询:MySQL 5.7 对 IN (SELECT ...) 的物化策略未必最优,实测比 JOIN 慢时改写成 JOIN。
大 IN 列表:IN 里塞几千个值会解析缓慢,拆成每批 200-500 个循环执行更稳。此外,COUNT(*) 全表统计在大表上很贵,列表页的总数可以考虑缓存或改用近似值。
第五步:验证与防回归,别优化完就撒手
结论:任何优化都必须用同一份数据、同一条 SQL 做前后对比,才算完成。
验证方法:对比 EXPLAIN 的 rows 变化、SHOW SESSION STATUS LIKE 'Handler_read%' 的扫描行数,以及实测执行时间。加索引要评估写入成本——索引不是越多越好,每多一个索引,写入和更新就多一份维护开销。线上大表加索引请走 online DDL 或安排在低峰期执行。
最后把这次的分析结论沉淀下来:哪条 SQL、什么病征、怎么改的,下次同类问题可以直接复用。慢查询治理的真正价值不在单次提速,而在于把「定位 → 解释 → 归类 → 重写 → 验证」变成团队固定的排查习惯。
转载请注明出处,版权归原作者所有。
正式会员






