一句话回答

先确认影响范围并止损,再通过监控、慢查询日志、执行计划和锁等待定位根因;根据访问路径、索引、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 指纹、扫描行数和长尾耗时。
  • 对表结构变更进行线上影响评估和回滚演练。
  • 为核心接口设置数据库耗时预算和自动告警。

核心考点清单

  • 先确认瓶颈并止损,再保存现场,不能直接在线上盲目加索引。
  • 慢查询要同时看耗时、频率、扫描行数、锁等待和资源消耗。
  • 执行计划是成本估算,EXPLAIN ANALYZE 才提供真实执行数据。
  • 索引、SQL、事务边界、数据模型和容量都可能是根因。
  • 优化必须灰度验证,并评估对写入和其他查询的副作用。

高频追问与参考回答

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

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

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

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

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

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

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

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

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

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

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

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

机制全景图

「线上遇到慢 SQL 应该怎么处理?」的实现链路如下,节点可与后面的源码和运行证据逐一对应。

flowchart LR
    A["确认慢请求时间窗"]
    A --> B["从慢日志定位 SQL 指纹"]
    B --> C["分析计划与等待"]
    C --> D["最小改动优化"]
    D --> E["回放灰度并持续观测"]

源码与实现定位

入口 阅读重点
performance_schema.events_statements_summary_by_digest 按指纹聚合总成本
sys.statement_analysis 扫描、延迟和临时表视图

源码或系统表应按上表顺序追踪:先确认入口实际走到哪条路径,再用运行时数据验证,而不是仅凭类名或配置推测。

参数配置与可复现实验

SELECT DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT, SUM_ROWS_EXAMINED
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC LIMIT 20;

同时注入慢 SQL 与连接池等待,拆分客户端等待、锁等待和执行时间,避免误把排队当 SQL 慢。

验证步骤与预期结果

1. 固定输入和基线

先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「总耗时贡献」为主基线,记录值应满足「按指纹排序」;同时保存 SQL 指纹总耗时、连接池等待,使后续变化能够回到同一时间轴比较。

2. 从实现入口确认路径

在「performance_schema.events_statements_summary_by_digest」确认请求确实进入「按指纹聚合总成本」对应的实现,再沿「sys.statement_analysis」观察「扫描、延迟和临时表视图」。如果入口路径都未命中,就不应继续调整下游参数,而应先检查调用条件、版本或路由是否与假设一致。

3. 注入本文特有的失败模式

优先复现「只按耗时排序忽略调用次数」,并把单一变量逐级放大,直到「总耗时贡献」越过「Top1>30%」。随后再分别验证「优化一条 SQL 却压垮写入」和「参数偏斜导致测试计划与生产不同」,三类故障分开执行,避免多个变量同时变化而无法归因。

4. 执行止损和根因修复

第一轮只应用「先按总成本而非单次最慢排序」,确认它能控制影响范围;第二轮应用「隔离报表连接池」,验证核心链路恢复;最后落实「真实参数回放后灰度」,消除同类问题再次出现的条件。每一步都保留变更前后数据,不用“感觉变快了”替代测量。

5. 通过退出条件

实验只有同时满足三项才算通过:「总耗时贡献」回到「按指纹排序」、「连接等待」回到「<执行时间 10%」、「临时磁盘表」回到「低比例」,并且业务结果差异为零。若性能恢复但结果不一致,仍应视为失败;若指标恢复后很快再次越线,则说明只完成了临时止损,没有消除根因。

量化基线

指标 样例基线/口径 风险线 结论
总耗时贡献 按指纹排序 Top1>30% 优先治理
连接等待 <执行时间 10% 超过执行 池/下游饱和
临时磁盘表 低比例 突增 排序聚合溢写

这些数值是实验口径或示例告警线,不是可复制到所有系统的固定答案;上线阈值应由本系统稳态、峰值和故障演练共同确定。

事故复盘:大促期间接口从 50ms 升到 3s

慢日志显示 SQL 本身执行约 80ms,但连接池等待超过 2s;根因是另一批报表查询占满连接。隔离报表池、限制并发并优化扫描后才恢复,单纯给接口 SQL 加索引没有作用。

失败模式 首要证据 第一处置动作
只按耗时排序忽略调用次数 SQL 指纹总耗时 先按总成本而非单次最慢排序
优化一条 SQL 却压垮写入 连接池等待 隔离报表连接池
参数偏斜导致测试计划与生产不同 实际扫描行数 真实参数回放后灰度

发布与回滚检查点

  • 发布前:确认「performance_schema.events_statements_summary_by_digest」对应实现和上述配置在目标版本仍然有效,并保存「总耗时贡献」基线。
  • 灰度中:同时观察 SQL 指纹总耗时、连接池等待、实际扫描行数;任一指标越过表中风险线,就停止继续扩量。
  • 回滚时:先执行「先按总成本而非单次最慢排序」控制影响,再回退代码或参数;涉及持久状态时必须额外核对结果差异。
  • 发布后:至少覆盖一个完整峰值周期,确认「只按耗时排序忽略调用次数」没有再次出现,才关闭变更观察窗口。

设计边界与工程取舍

慢 SQL 排查要从请求端到数据库形成时间分解;只有证明时间花在执行器和具体节点,索引优化才是正确动作。