这事发生在一次版本上线后的第二周。监控先报的是 P99 从 300ms 涨到 1.2s,第一反应是代码问题,回看了变更,没有明显嫌疑。然后去看数据库,慢查询日志里全是同一条 SQL 的变体。
定位:别猜,把执行计划拉出来
排障工具链的事先不展开(另一篇有完整链路),直接说结论:慢 SQL 治理的第一动作永远是 EXPLAIN ANALYZE,不是看索引清单。
那条 SQL 长这样(字段已脱敏):
SELECT * FROM orders o
JOIN order_items i ON o.id = i.order_id
WHERE o.user_id = ?
AND o.status IN ('paid','shipped')
ORDER BY o.created_at DESC
LIMIT 20 OFFSET 1900;
执行计划显示两个问题叠加:orders 走的是全表扫描(user_id 上没有索引),排序用的是 filesort。
三个问题叠加,才造成 1.2 秒
继续往下挖,慢的根源其实有三个,单看任何一个都不至于这么慢:
- 缺索引:
orders(user_id, status)没有复合索引,每次过滤都扫全表。这是最大头。 - 深分页:
OFFSET 1900意味着数据库要先扫出前 1900 行再丢掉,越翻越慢。分页接口的耗时和页码成正比——这是线上最常见的隐性杀手。 - N+1:接口在应用层对每页订单又各查了一次明细(
order_items按订单循环查询),20 个订单 = 21 条 SQL。
修复:三个动作,各自验证
- 加复合索引
(user_id, status, created_at DESC):过滤和排序同时受益,filesort 消失。 - 深分页改成游标分页(
WHERE created_at < 上一页最后一条的时间),耗时不再随页码增长。对「我就要跳到第 100 页」的需求,产品上做了限制,只允许往前往后翻,不允许跳页。 - N+1 改成一次
JOIN拉明细或批量查询,应用层循环查询的代码删掉。
改完压测:P99 从 1.2s 到 90ms,数据库 CPU 下降六成。上线两周无回弹。
沉淀成方法:慢查询治理我固定走这几步
- 发现:慢查询日志 + 指标告警(
max_query_time阈值),不要等用户报。 - 定位:
EXPLAIN ANALYZE看执行计划,按「扫描行数 → 排序 → 回表」三步找大头。 - 根因分类:缺索引 / 深分页 / N+1 / 无缓存策略,四类原因按频率排序,逐类治理。
- 验证:压测对比 + 上线后观察 P99 与慢查询数量一周,不回弹才算完。
- 预防:上线前把「联表查询有没有必要」「分页会不会翻深」「循环里有没有查询」写进代码评审检查项。
慢查询和缓存的关系我写过一篇反模式——索引和 SQL 是地基,缓存是地板,地基没打平就铺地板,早晚要返工。
能照做的清单(也可以放进你的评审模板):查询都过一遍执行计划;分页默认游标式;循环内禁查询;慢查询阈值告警常开。架构评审清单里我把这几条也加了进去。