先说结论

先确认影响范围并止损,再通过监控、慢查询日志、执行计划和锁等待定位根因;根据访问路径、索引、SQL 写法、事务和数据模型实施优化,最后用真实数据验证并建立长期防复发机制。

慢 SQL 处理不能上来就“加索引”。线上响应必须兼顾业务可用性、数据正确性和变更风险。

核心处理流程

告警或用户反馈
      ↓
确认影响范围与瓶颈位置
      ↓
限流、降级、熔断或终止异常查询
      ↓
保存 SQL、参数、执行计划和现场指标
      ↓
分析扫描、索引、排序、锁、事务和资源
      ↓
制定并灰度实施优化
      ↓
对比验证、持续观察、复盘防复发

第一步:确认是不是数据库慢

接口耗时高不一定由 SQL 引起。首先通过链路追踪拆分连接池等待、数据库执行、网络传输、对象映射和业务计算耗时。

重点确认:

  • 哪个接口、租户或业务受影响?
  • 是所有请求变慢,还是某类参数变慢?
  • 是单条 SQL 很慢,还是大量小 SQL 累积形成 N+1?
  • 数据库 CPU、IO、连接数、活跃事务和锁等待是否异常?
  • 慢是在数据库执行阶段,还是连接池已经耗尽?

如果数据库执行只有 10ms,但应用等待连接 3 秒,应该先解决连接泄漏、连接池配置或突发并发问题。

第二步:先止损,再深入分析

线上已经影响用户时,优先恢复服务:

  • 对非核心查询限流、熔断或临时关闭复杂筛选。
  • 将大查询切换到只读副本,但要评估复制延迟和副本负载。
  • 暂停报表、批处理或异常定时任务。
  • 对明确失控且可以安全终止的查询执行 KILL QUERY
  • 降低单次返回量,暂时限制查询时间范围和分页深度。
  • 必要时扩容只读实例或应用实例,但扩容只是应急措施。

终止事务前必须确认它是否在执行重要写入。盲目杀死大事务会触发长时间回滚,可能进一步放大 IO 和锁等待。

第三步:保存现场证据

不要在证据丢失后只凭一条 SQL 文本分析。至少保存:

  • SQL 指纹和真实参数范围。
  • 调用次数、平均值、P95、P99 和最大耗时。
  • 扫描行数、返回行数、临时表和排序量。
  • EXPLAINEXPLAIN ANALYZE 结果。
  • 表结构、索引、数据量和字段分布。
  • 当时的锁等待、活跃事务和服务器资源曲线。
  • MySQL 版本、隔离级别和关键配置。

相同 SQL 使用不同参数时可能选择相同计划却表现完全不同。例如查询热门租户可能命中数百万行,普通租户只命中几十行。

第四步:使用慢查询日志定位

慢查询日志可以记录超过阈值的语句。生产中应合理设置阈值和采样策略,避免日志量失控。

常见关注指标:

  • Query_time:总执行时间。
  • Lock_time:等待锁的时间。
  • Rows_examined:扫描行数。
  • Rows_sent:返回行数。

扫描一千万行只返回十行,通常说明访问路径需要优化;但扫描行数不高、Lock_time 很高,则重点应转向锁竞争和长事务。

可以按 SQL 指纹聚合慢日志,排序时同时看总耗时、平均耗时、最大耗时和执行次数。一条 50ms、每秒执行数万次的 SQL,整体资源消耗可能比偶发的 2 秒查询更大。

第五步:分析执行计划

EXPLAIN ANALYZE
SELECT id, order_no, created_at
FROM orders
WHERE tenant_id = 1001
  AND status = 'PAID'
  AND created_at >= '2026-07-01'
ORDER BY created_at DESC
LIMIT 50;

重点检查:

  1. 实际使用哪个索引?为什么没有使用预期索引?
  2. 预计行数与实际行数是否差异巨大?
  3. 扫描行数与返回行数比例是否合理?
  4. 是否出现大量回表?
  5. 是否有额外排序或内部临时表?
  6. 嵌套循环连接是否被放大执行很多次?
  7. 最耗时的算子究竟是哪一个?

Using filesort 不代表一定使用磁盘,Using temporary 也不代表 SQL 必须修改。它们只是分析线索,要结合数据量和实际耗时判断。

六类常见根因及处理

1. 缺少合适索引

查询条件、关联条件和排序字段没有匹配的联合索引,导致大范围扫描。

索引设计要考虑等值条件、范围条件、排序、选择性和返回列,不能简单把 WHERE 中所有字段都建单列索引。

CREATE INDEX idx_orders_tenant_status_time
ON orders(tenant_id, status, created_at, id);

新增索引会占空间并降低写入性能,必须检查是否与已有索引重复,并评估在线 DDL 对生产的影响。

2. 索引存在但访问路径不理想

常见原因包括:

  • 联合索引不满足最左前缀。
  • 索引列发生隐式类型转换。
  • 对索引列做函数或表达式计算。
  • 前导模糊查询,例如 LIKE '%keyword'
  • 条件命中比例过高,回表比全表扫描更贵。
  • 统计信息过期或数据分布严重倾斜。

不要看到 key = NULL 就直接 FORCE INDEX。强制索引可能在当前参数下变快,却在数据变化后更慢。

3. 深分页

SELECT * FROM orders
ORDER BY id
LIMIT 1000000, 20;

数据库需要找到并丢弃前一百万行。可以使用上次结果的稳定排序键进行 Seek 分页:

SELECT id, order_no, created_at
FROM orders
WHERE id > ?
ORDER BY id
LIMIT 20;

多字段排序时应使用唯一且稳定的组合游标,避免同一时间值造成重复或遗漏。

4. 返回数据过多

SELECT * 会读取无关列,增加页读取、回表、网络和对象创建成本。列表接口应只返回必要字段并限制最大页大小;大批量导出应异步化、分批读取并写入文件,而不是占用在线连接。

5. 锁等待和长事务

SQL 本身执行很快,但等待锁数秒。应找到阻塞源事务,检查是否存在事务中远程调用、空闲未提交、无索引更新或批量修改。

此时增加索引可能缩小锁范围,但真正根因也可能是错误的事务边界。排查必须查看完整事务,而不是只看等待 SQL。

6. 数据库资源饱和

CPU 满可能来自复杂计算、解析压力或并发过高;IO 满可能来自扫描、脏页刷新或临时文件;连接数满可能来自慢查询堆积、连接泄漏或连接池总量失控。

资源饱和时所有 SQL 都会变慢,形成请求堆积和重试风暴。要限制重试、实施背压,并找到最主要的负载来源。

常见 SQL 改写

避免在索引列上计算

-- 不推荐
WHERE DATE(created_at) = '2026-07-21'

-- 推荐
WHERE created_at >= '2026-07-21 00:00:00'
  AND created_at <  '2026-07-22 00:00:00'

避免循环查询形成 N+1

先查询 100 个订单,再循环查询 100 次用户,会放大网络往返和连接占用。可以批量使用 IN、合理 JOIN 或应用层批量加载,但也要限制 IN 列表规模。

拆分超大事务

DELETE FROM operation_log
WHERE id >= ? AND id < ?;

按主键范围分批删除,每批提交并观察复制延迟、锁等待和 IO。不要直接对亿级表执行一次无边界删除。

优化后的验证

优化完成不等于工作结束。必须比较:

  • P95/P99 和最大耗时是否降低。
  • 扫描行数、回表次数和返回行数是否合理。
  • CPU、IO、Buffer Pool 命中率和临时文件是否改善。
  • 新索引对 INSERT、UPDATE、存储和备份的影响。
  • 其他 SQL 是否因执行计划或缓存变化发生退化。
  • 高峰流量和不同参数分布下是否仍稳定。

防止慢 SQL 再次进入生产

  • 在测试和预发布使用接近生产规模的数据。
  • 对新增 SQL 做执行计划和索引评审。
  • 建立最大查询时间、分页大小和导出量限制。
  • 持续采集 SQL 指纹、扫描行数和长尾耗时。
  • 对表结构变更进行线上影响评估和回滚演练。
  • 为核心接口设置数据库耗时预算和自动告警。

常见问题

追问 1:线上突然出现慢 SQL,第一件事做什么?

先确认影响范围和当前瓶颈,必要时限流、降级或暂停非核心任务恢复服务,同时保存 SQL、参数、锁等待和资源指标。不要先执行高风险 DDL。

追问 2:扫描行数很少但 SQL 仍然慢,可能是什么原因?

可能在等待锁、等待连接、存储 IO 延迟高、网络返回慢,或单行包含大字段。应拆分等待时间而不是只盯执行计划。

追问 3:给慢 SQL 加索引一定有效吗?

不一定。低选择性条件、返回大量数据、排序聚合、锁等待或资源饱和都可能让索引收益很小;索引还会增加写放大和存储成本。

追问 4:如何判断联合索引字段顺序?

综合高频查询的等值条件、选择性、范围、排序和覆盖需求,而不是机械按选择性排序。还要兼顾已有索引复用与写入成本。

追问 5:为什么平均耗时不高还要优化?

平均值会掩盖长尾和参数倾斜。核心接口的 P99、最大耗时以及高频 SQL 的总资源消耗更能反映用户体验和系统风险。

追问 6:可以直接在生产执行 EXPLAIN ANALYZE 吗?

它会真实执行查询。对高成本查询或写语句存在风险,应先在测试环境、只读副本或受控条件下进行,并设置超时与资源保护。