MySQL 慢查询排查:从现象到索引命中的一条完整路径

4 阅读3分钟

慢查询问题最常见的误区是:

  • 先改 SQL,再看执行计划
  • 先加索引,再看数据分布
  • 先上缓存,再看访问模式

结果通常是“短期有效,长期反复”。

本文给出一条可复用的排查路径:先定位,再解释,再优化,再验证


【场景:接口 RT 从 120ms 升到 2.8s】

线上告警通常长这样:

  1. 某个查询接口 P95 延迟明显上升。
  2. 应用层 CPU 不高,但数据库 QPS 和 IO 增长。
  3. 同一 SQL 在低峰正常,高峰超时。

这类问题大多不是“数据库不行”,而是查询路径在数据量变化后失效。


【第1步:先锁定“是哪条 SQL”慢】

先从慢日志和应用日志交叉定位:

-- 开启慢查询阈值(示例)
SET GLOBAL long_query_time = 0.5;
SET GLOBAL slow_query_log = 'ON';

应用侧务必打印 requestId + SQL模板 + 参数摘要 + 耗时,避免只看到“慢”却找不到请求上下文。


【第2步:用 EXPLAIN 看执行路径,而不是猜】

假设慢 SQL:

SELECT id, user_id, status, created_at
FROM orders
WHERE user_id = 1024
  AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 20;

先执行:

EXPLAIN SELECT id, user_id, status, created_at
FROM orders
WHERE user_id = 1024
  AND status = 'PAID'
ORDER BY created_at DESC
LIMIT 20;

重点看 4 个字段:

  • type:是否从 ALL 降到 range/ref
  • key:是否命中预期索引
  • rows:扫描行数是否过大
  • Extra:是否出现 Using filesortUsing temporary

【第3步:索引设计要贴合“过滤 + 排序”】

上面这条 SQL 常见错误是只建单列索引:

CREATE INDEX idx_user_id ON orders(user_id);
CREATE INDEX idx_status ON orders(status);

这通常无法同时覆盖过滤和排序。

更合理的索引是:

CREATE INDEX idx_user_status_created_at
ON orders(user_id, status, created_at DESC);

原则:

  • 等值过滤列优先(user_id, status
  • 排序列放后面(created_at
  • 与查询模式保持一致,减少回表与额外排序

【第4步:避免“看起来没问题”的写法】

反例1:在索引列上做函数

-- 反例:created_at 上套函数,索引可能失效
WHERE DATE(created_at) = '2026-02-27'

改为范围查询:

WHERE created_at >= '2026-02-27 00:00:00'
  AND created_at <  '2026-02-28 00:00:00'

反例2:隐式类型转换

-- user_id 是 bigint,却传字符串
WHERE user_id = '1024'

参数类型必须与列类型一致,避免优化器走非预期路径。


【第5步:分页性能要分场景】

当页码很深时,LIMIT offset, size 会越来越慢:

-- 深分页反例
SELECT id, user_id, created_at
FROM orders
ORDER BY created_at DESC
LIMIT 100000, 20;

可改为游标式分页:

SELECT id, user_id, created_at
FROM orders
WHERE created_at < :lastCreatedAt
ORDER BY created_at DESC
LIMIT 20;

这在时间序列表、订单表、日志表里更稳定。


【第6步:优化后一定要做“对比验证”】

不要只看“感觉变快”,至少做三组对比:

  1. 优化前后 EXPLAIN 对比(扫描行数、是否 filesort)
  2. 压测 RT 对比(P50/P95/P99)
  3. 数据库指标对比(QPS、IO、Buffer 命中率)

可复用的伪代码流程:

SlowQueryCase caseInfo = locateByRequestId(requestId);
ExplainPlan before = db.explain(caseInfo.sql());
OptimizationPlan plan = optimizer.designIndex(caseInfo.sqlPattern());
db.apply(plan);
ExplainPlan after = db.explain(caseInfo.sql());
BenchmarkResult result = benchmark.compare(caseInfo.endpoint());
report.output(before, after, result);

【一份可复用的慢查询排查清单】

每次遇到慢 SQL,按这个顺序检查:

  1. 是否明确慢的是哪条 SQL(不是哪个接口)?
  2. 是否拿到真实参数与执行计划?
  3. 是否命中正确索引,扫描行数是否可控?
  4. 是否存在函数、隐式转换、深分页等反模式?
  5. 优化是否经过压测与指标对比验证?
  6. 是否把结论沉淀为评审规则与索引规范?

把这份清单固化后,慢查询治理会从“救火”变成“可治理”。


下期预告:

《Redis 三大问题实战:穿透、击穿、雪崩如何设计防线》