一句话回答
先确认影响范围并止损,再通过监控、慢查询日志、执行计划和锁等待定位根因;根据访问路径、索引、SQL 写法、事务和数据模型实施优化,最后用真实数据验证并建立长期防复发机制。
慢 SQL 处理不能上来就“加索引”。线上响应必须兼顾业务可用性、数据正确性和变更风险。
核心处理流程
告警或用户反馈
↓
确认影响范围与瓶颈位置
↓
限流、降级、熔断或终止异常查询
↓
保存 SQL、参数、执行计划和现场指标
↓
分析扫描、索引、排序、锁、事务和资源
↓
制定并灰度实施优化
↓
对比验证、持续观察、复盘防复发
第一步:确认是不是数据库慢
接口耗时高不一定由 SQL 引起。首先通过链路追踪拆分连接池等待、数据库执行、网络传输、对象映射和业务计算耗时。
重点确认:
- 哪个接口、租户或业务受影响?
- 是所有请求变慢,还是某类参数变慢?
- 是单条 SQL 很慢,还是大量小 SQL 累积形成 N+1?
- 数据库 CPU、IO、连接数、活跃事务和锁等待是否异常?
- 慢是在数据库执行阶段,还是连接池已经耗尽?
如果数据库执行只有 10ms,但应用等待连接 3 秒,应该先解决连接泄漏、连接池配置或突发并发问题。
第二步:先止损,再深入分析
线上已经影响用户时,优先恢复服务:
- 对非核心查询限流、熔断或临时关闭复杂筛选。
- 将大查询切换到只读副本,但要评估复制延迟和副本负载。
- 暂停报表、批处理或异常定时任务。
- 对明确失控且可以安全终止的查询执行
KILL QUERY。 - 降低单次返回量,暂时限制查询时间范围和分页深度。
- 必要时扩容只读实例或应用实例,但扩容只是应急措施。
终止事务前必须确认它是否在执行重要写入。盲目杀死大事务会触发长时间回滚,可能进一步放大 IO 和锁等待。
第三步:保存现场证据
不要在证据丢失后只凭一条 SQL 文本分析。至少保存:
- SQL 指纹和真实参数范围。
- 调用次数、平均值、P95、P99 和最大耗时。
- 扫描行数、返回行数、临时表和排序量。
EXPLAIN或EXPLAIN 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;
重点检查:
- 实际使用哪个索引?为什么没有使用预期索引?
- 预计行数与实际行数是否差异巨大?
- 扫描行数与返回行数比例是否合理?
- 是否出现大量回表?
- 是否有额外排序或内部临时表?
- 嵌套循环连接是否被放大执行很多次?
- 最耗时的算子究竟是哪一个?
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 排查要从请求端到数据库形成时间分解;只有证明时间花在执行器和具体节点,索引优化才是正确动作。