慢查询问题最常见的误区是:
- 先改 SQL,再看执行计划
- 先加索引,再看数据分布
- 先上缓存,再看访问模式
结果通常是“短期有效,长期反复”。
本文给出一条可复用的排查路径:先定位,再解释,再优化,再验证。
【场景:接口 RT 从 120ms 升到 2.8s】
线上告警通常长这样:
- 某个查询接口 P95 延迟明显上升。
- 应用层 CPU 不高,但数据库 QPS 和 IO 增长。
- 同一 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/refkey:是否命中预期索引rows:扫描行数是否过大Extra:是否出现Using filesort、Using 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步:优化后一定要做“对比验证”】
不要只看“感觉变快”,至少做三组对比:
- 优化前后
EXPLAIN对比(扫描行数、是否 filesort) - 压测 RT 对比(P50/P95/P99)
- 数据库指标对比(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,按这个顺序检查:
- 是否明确慢的是哪条 SQL(不是哪个接口)?
- 是否拿到真实参数与执行计划?
- 是否命中正确索引,扫描行数是否可控?
- 是否存在函数、隐式转换、深分页等反模式?
- 优化是否经过压测与指标对比验证?
- 是否把结论沉淀为评审规则与索引规范?
把这份清单固化后,慢查询治理会从“救火”变成“可治理”。
下期预告:
《Redis 三大问题实战:穿透、击穿、雪崩如何设计防线》