#稳定性 #数据库 #性能优化 #排障

慢查询治理实录:一次接口从 1.2 秒到 90 毫秒的完整过程

一个接口 P99 1.2s 的完整治理过程:定位、三个叠加的问题、修复与验证,以及沉淀下来的慢查询治理方法。

✍️ diunilaomei 📅 2026-06-17 📝 约 963 字 ⏱️ 约 2 分钟
📑 章节目录
字号

这事发生在一次版本上线后的第二周。监控先报的是 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 秒

继续往下挖,慢的根源其实有三个,单看任何一个都不至于这么慢:

  1. 缺索引orders(user_id, status) 没有复合索引,每次过滤都扫全表。这是最大头。
  2. 深分页OFFSET 1900 意味着数据库要先扫出前 1900 行再丢掉,越翻越慢。分页接口的耗时和页码成正比——这是线上最常见的隐性杀手。
  3. 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 下降六成。上线两周无回弹。

沉淀成方法:慢查询治理我固定走这几步

  1. 发现:慢查询日志 + 指标告警(max_query_time 阈值),不要等用户报。
  2. 定位EXPLAIN ANALYZE 看执行计划,按「扫描行数 → 排序 → 回表」三步找大头。
  3. 根因分类:缺索引 / 深分页 / N+1 / 无缓存策略,四类原因按频率排序,逐类治理。
  4. 验证:压测对比 + 上线后观察 P99 与慢查询数量一周,不回弹才算完。
  5. 预防:上线前把「联表查询有没有必要」「分页会不会翻深」「循环里有没有查询」写进代码评审检查项。

慢查询和缓存的关系我写过一篇反模式——索引和 SQL 是地基,缓存是地板,地基没打平就铺地板,早晚要返工

能照做的清单(也可以放进你的评审模板):查询都过一遍执行计划;分页默认游标式;循环内禁查询;慢查询阈值告警常开。架构评审清单里我把这几条也加了进去。

相关阅读

栏目全部 →