数据库慢查询治理:从 EXPLAIN 到查询重写的完整流程
慢查询治理的标准流程是「先定位、再解释、后重写、终验证」四步闭环:用慢查询日志锁定真正耗时的 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、什么病征、怎么改的,下次同类问题可以直接复用。慢查询治理的真正价值不在单次提速,而在于把「定位 → 解释 → 归类 → 重写 → 验证」变成团队固定的排查习惯。
转载请注明出处,版权归原作者所有。
星耀SVIP
管理员
黑卡会员





