← 返回文章

慢 SQL 排查与优化

发布于 2026/10/17

慢 SQL 排查与优化

1. 完整回答

慢 SQL 指执行时间超过预期的 SQL 语句,可能导致接口响应变慢、数据库资源占用升高,严重时还会影响整个系统的吞吐量。

对于慢 SQL,通常按照定位问题、分析执行计划、针对性优化、验证效果这几个阶段处理。

首先,通过慢查询日志、应用监控或者 Performance Schema 定位具体 SQL,重点关注执行时间、执行次数、扫描行数和锁等待情况。对于执行频率高、累计耗时大的 SQL,会优先处理。

找到 SQL 后,使用 EXPLAIN 分析执行计划,重点看 type、key、rows 和 Extra,判断有没有全表扫描、索引使用不合理、额外排序或者临时表等问题。必要时使用 EXPLAIN ANALYZE 对比预估行数和实际执行情况。

接下来根据原因优化。如果是索引问题,可以结合查询条件和排序字段设计联合索引,尽量减少扫描行数和回表次数;如果是 SQL 本身的问题,可以优化查询条件、减少返回字段,或者通过游标分页解决深分页性能问题。

如果执行计划正常,但 SQL 偶尔变慢,还需要排查锁等待、长事务、数据库 CPU、磁盘 I/O 和连接数等因素。

最后,在接近生产环境的数据量和负载下验证优化前后的执行时间、扫描行数以及资源消耗,确认没有影响其他查询或写入性能后再上线。

整体思路就是:先定位真正的耗时环节,再减少无效的数据访问和资源消耗,最后通过实际指标验证优化效果。

2. 核心原理与关键概念

2.1 核心原理

慢 SQL 的底层原因通常集中在两个方面:执行过程中处理的数据过多,或者等待资源的时间过长。

以 MySQL InnoDB 为例,排查流程可以分成五步。

步骤一:发现慢 SQL → 确定影响范围

通过慢查询日志、Performance Schema 或应用监控定位问题。

重点关注:

  • 执行耗时:单次执行的平均耗时、最大耗时和 P95 延迟。
  • 执行频率:单次较快但执行量巨大的 SQL,也可能造成较高负载。
  • 扫描行数:Rows_examined 明显高于 Rows_sent,说明可能进行了大量无效扫描。

可以使用 mysqldumpslow 或 pt-query-digest 对慢查询日志进行聚合分析,也可以查看 sys.statement_analysis。

步骤二:分析执行计划 → 找到性能瓶颈

使用 EXPLAIN 查看优化器选择的访问路径,判断索引、连接、排序和临时表的使用情况。

字段 含义 关注点
type 表访问方式 ALL 为全表扫描,index 为全索引扫描,range 为索引范围扫描
possible_keys 候选索引 有哪些索引可能参与查询
key 实际选用的索引 是否符合预期
rows 预计检查的行数 是否扫描过多数据
filtered 预计条件过滤比例 过滤效果是否合理
Extra 额外执行信息 关注 Using filesort、Using temporary 等

EXPLAIN 中的行数主要是估算值。需要确认实际扫描情况时,可以使用 EXPLAIN ANALYZE,查看实际耗时、返回行数和循环次数。

注意:EXPLAIN ANALYZE 会实际执行语句,应优先在测试环境验证,避免直接对生产环境中的高开销 SQL 使用。

步骤三:判断原因 → 区分扫描开销和等待开销

扫描开销主要包括全表扫描、大范围索引扫描、频繁回表、排序、临时表和低效关联。

等待开销主要包括行锁等待、元数据锁等待、磁盘 I/O 和系统资源竞争。

这一步决定后续优化方向,避免所有问题都通过添加索引解决。

步骤四:针对性优化 → 降低实际执行成本

  • 索引优化:设计联合索引、提高过滤效率、尽量利用覆盖索引。
  • SQL 优化:避免无效扫描、减少返回字段、优化排序和分页。
  • 事务优化:缩短事务时间、及时提交事务、减少锁冲突。
  • 架构优化:在单条 SQL 已合理优化后,根据瓶颈考虑缓存、预聚合、归档或读写扩展。

步骤五:验证效果 → 持续监控

优化前后对比执行计划、实际耗时、扫描行数、CPU 和 I/O 消耗,并关注并发场景下的整体吞吐量。

2.2 关键概念

概念 核心含义
慢查询日志 记录满足慢查询条件的 SQL,用于发现历史慢查询
执行计划 优化器选择的执行路径,包括表访问方式、索引和连接策略
联合索引 由多个字段组成的索引,可以同时服务于过滤、排序等操作
回表 通过二级索引取得主键后,再访问聚簇索引读取完整数据
覆盖索引 查询需要的列均能从选用的索引中获取,从而避免额外读取完整数据行
索引下推 在存储引擎读取二级索引时先判断部分条件,减少不必要的回表
锁等待 SQL 因其他事务持有冲突锁而被阻塞

3. 高频面试追问

3.1 明明创建了索引,为什么 SQL 还是很慢?

创建索引不代表查询一定高效,关键要看索引能减少多少数据访问。

常见原因有四种。

第一,索引选择性较差。比如 status 字段只有几个状态值,查询某个状态可能仍然需要扫描大量记录。

第二,发生大量回表。即使通过二级索引定位了记录,如果查询字段没有被索引覆盖,仍然需要根据主键访问聚簇索引。

第三,索引只能帮助过滤部分数据,排序、分组或者关联操作仍然可能产生较大开销。

第四,SQL 本身执行效率正常,但存在锁等待或系统资源竞争。

因此,排查时需要结合实际扫描行数、回表次数、执行计划和等待事件综合判断。

继续追问:优化器选择了不合适的索引怎么办?

先检查数据分布和统计信息,确认优化器有没有错误估算扫描行数。统计信息不准确时,可以通过 ANALYZE TABLE 更新相关统计信息,再重新观察执行计划。

如果问题仍然存在,可以调整联合索引设计。索引提示,例如 FORCE INDEX,适合作为经过验证后的特定优化手段,因为数据分布发生变化后,强制指定的索引也可能变得不合适。

3.2 EXPLAIN 中 type=ALL,就一定需要优化吗?

不一定,ALL 表示全表扫描,但全表扫描并不总是性能问题。

如果表很小,直接扫描整张表可能比访问索引更高效。

另外,如果查询需要返回表中大部分数据,使用二级索引可能产生大量随机回表操作,成本反而比顺序扫描更高。

因此,判断重点是实际扫描量、数据规模和执行耗时。

同样,type=index 表示全索引扫描,虽然访问了索引,但仍然可能需要扫描大量索引记录。

EXPLAIN 主要用来分析访问路径,最终优化效果需要通过实际执行数据确认。

3.3 联合索引应该如何设计?

联合索引需要结合查询条件、排序方式和数据分布设计,通常优先考虑等值过滤字段,再考虑范围和排序字段。

例如订单查询经常使用以下条件:

SELECT id, status, create_time
FROM orders
WHERE user_id = 1001 AND status = 1
ORDER BY create_time DESC
LIMIT 20;

可以考虑建立 (user_id, status, create_time) 联合索引。

其中,user_id 和 status 用于等值过滤,create_time 可以支持索引有序读取,减少额外排序。

它的底层依据是 B+ 树按照联合索引的字段顺序组织数据,只有前面的字段确定后,后续字段才能充分利用有序性。

具体字段顺序还需要结合其他查询模式、字段选择性和执行计划判断。索引设计的最终目标是减少实际访问的数据量,同时控制索引维护成本。

3.4 联合索引遇到范围查询后,后面的字段就完全失效了吗?

不会完全失效,但后续字段通常难以继续用于缩小索引扫描范围。

例如存在联合索引 (a, b, c):

WHERE a = 1 AND b > 10 AND c = 5

a 可以进行等值定位,b 用于确定扫描范围。

由于 b 已经是范围条件,c 通常无法继续缩小 B+ 树需要扫描的索引区间。

但 c 仍然可能通过索引下推参与过滤,减少回表次数。如果查询满足覆盖索引条件,也可能直接从索引获取数据。

因此,需要区分索引范围定位能力和索引过滤能力,不能直接认为范围条件之后的字段完全没有作用。

3.5 哪些 SQL 写法会导致索引利用不充分?

主要是破坏了索引列原有的有序比较方式,或者让优化器认为使用索引成本过高。

常见情况包括:

  • 对索引列使用函数:例如 DATE(create_time) = '2026-10-01',通常可以改成时间范围查询。
  • 对索引列进行计算:例如 age + 1 = 20,可以改为 age = 19。
  • 前置模糊匹配:例如 LIKE '%abc',普通 B+ 树索引通常无法直接进行前缀范围定位。
  • 隐式类型转换:例如字符串字段与数字比较,可能导致索引列发生转换。
  • 联合索引缺少有效前缀:可能无法直接使用索引定位目标范围。

需要注意,这些情况并非一定导致全表扫描。MySQL 可能选择全索引扫描、索引下推或其他优化方式,部分函数表达式也可以通过函数索引优化。

因此,最终仍然需要检查实际执行计划。

3.6 Using filesort 和 Using temporary 一定代表 SQL 很慢吗?

不一定,它们表示执行过程中存在额外操作,具体性能影响取决于数据量和执行成本。

Using filesort 表示 MySQL 需要执行额外排序。这里的 filesort 只是排序操作名称,数据量较小时可能完全在内存中完成。

Using temporary 表示执行过程中使用了内部临时表,常见于某些分组、去重和复杂查询。

优化时可以先检查 WHERE 与 ORDER BY 是否能够使用同一个合适的联合索引。

如果查询本身必须排序或聚合,应该重点减少进入排序和聚合阶段的数据量,同时观察是否产生磁盘临时表。

3.7 为什么深分页会越来越慢?应该怎么优化?

深分页的主要问题是数据库需要处理大量前置记录,最终却只返回很少的数据。

例如:

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

即使按照主键索引顺序读取,数据库通常仍然需要跳过前面的十万条记录,才能返回目标数据。

如果业务按照上一页继续加载,可以使用游标分页,也叫键集分页。

例如按照主键递增分页:

SELECT * FROM orders
WHERE id > 100000
ORDER BY id
LIMIT 20;

这样可以通过索引直接定位到游标附近,减少跳过大量记录的开销。

如果按照创建时间排序,通常需要结合创建时间和唯一 ID 构造稳定的排序游标,避免时间重复导致分页不稳定。

游标分页适合连续翻页、消息列表和订单流水等场景,但无法方便地直接跳转到任意页码。

3.8 多表 JOIN 查询很慢,应该如何优化?

JOIN 优化的核心是减少参与关联的数据量,并降低关联匹配的成本。

首先,通过 EXPLAIN 查看连接顺序、访问方式以及每张表预计扫描的行数。

其次,确保关联字段具有合适的索引,尤其需要关注被驱动表的关联字段。通过索引定位匹配记录,通常可以减少重复扫描。

同时,检查关联字段的数据类型、字符集和排序规则是否一致,避免不必要的转换。

如果关联结果非常大,还需要尽量提前过滤无关记录,减少返回字段,并检查是否存在一对多关联导致结果集膨胀的问题。

对于复杂 JOIN,可以使用 EXPLAIN ANALYZE 查看各执行节点的实际行数和循环次数,确认主要开销出现在哪个环节。

3.9 为什么一条 SQL 平时很快,偶尔突然变慢?

这种情况需要优先检查锁等待、数据分布变化和数据库运行状态。

如果 SQL 的执行计划长期稳定,但只有部分请求明显变慢,可能是其他事务持有冲突锁,导致当前 SQL 等待。

可以通过 SHOW FULL PROCESSLIST 查看正在执行的 SQL,通过 information_schema.innodb_trx 和 sys.innodb_lock_waits 分析事务及锁等待关系。

另外,还需要检查数据库 CPU、磁盘 I/O、连接数,以及慢请求是否集中出现在业务高峰期。

如果只有某些查询参数特别慢,则要进一步检查数据倾斜、过滤条件的选择性和执行计划变化。

最终需要根据实际等待事件和监控指标确定原因,避免单纯通过增加索引处理并发问题。

3.10 项目中遇到订单列表查询很慢,具体怎么优化?

可以从订单列表的过滤条件、排序方式和回表成本入手。

例如,订单表数据量比较大,业务需要根据用户 ID 和订单状态查询订单,并按照创建时间倒序分页。

排查时先通过慢查询日志定位 SQL,再查看执行计划。如果发现扫描行数很大,并且存在 Using filesort,就需要检查现有索引是否与查询条件匹配。

假设原来只有 user_id 单列索引,那么数据库找到该用户的订单后,仍然可能需要过滤状态并进行排序。

针对这个查询模式,可以考虑建立 (user_id, status, create_time) 联合索引,让过滤和排序尽可能利用同一个索引。

如果查询只需要少量字段,还可以结合覆盖索引减少回表。

对于翻页越来越慢的问题,可以将深分页改成基于稳定排序键的游标分页。

最后使用相同查询条件和接近生产环境的数据量,对比优化前后的执行时间、实际扫描行数和资源消耗,同时检查新增索引对写入性能的影响。

继续追问:是不是索引越多,查询就越快?

不是所有查询都会因为增加索引而变快。每增加一个索引,都需要额外占用存储空间,而且插入、更新和删除数据时还需要维护对应的索引结构。

多个功能相近的索引还可能造成冗余。因此,需要结合主要查询模式设计少量有效的索引,并定期评估重复索引和低价值索引。

4. 易混淆概念对比

4.1 核心概念区别

对比概念 区别
EXPLAIN vs EXPLAIN ANALYZE 前者主要展示优化器计划,后者实际执行并展示各节点的实际耗时和行数
type=ALL vs type=index 前者扫描整张表,后者扫描整棵索引;两者都可能访问大量记录
Using index vs Using index condition 前者通常表示覆盖索引读取,后者表示使用索引下推进行条件过滤,仍可能回表
回表 vs 覆盖索引 回表需要根据二级索引中的主键再次查找数据;覆盖索引可以直接获取所需列
Using filesort vs Using temporary 前者表示额外排序,后者表示使用内部临时表;两种操作可以同时出现
OFFSET 分页 vs 游标分页 前者通过跳过指定数量的记录定位页面,后者通过上页的排序位置继续查询

4.2 索引扫描与索引优化的区别

操作 是否能减少扫描范围 主要作用
索引定位 通常可以 根据索引条件直接定位目标记录
索引范围扫描 通常可以 只扫描符合范围条件的索引区间
全索引扫描 通常不能 遍历索引记录,可能比全表扫描成本低
索引下推 不一定能缩小扫描区间 提前过滤索引记录,减少回表
覆盖索引 不一定能缩小扫描区间 避免额外访问完整数据行

因此,判断索引是否真正发挥作用,需要同时关注扫描范围、过滤效率和回表成本。

5. 总结

慢 SQL 排查与优化的核心是找到 SQL 执行过程中真正耗时的环节。通常先通过慢查询日志和监控定位问题,再通过 EXPLAIN 或 EXPLAIN ANALYZE 分析索引使用、扫描行数、排序和关联成本。优化时优先减少无效扫描,通过合理的联合索引、覆盖索引、SQL 改写和分页优化降低数据访问成本。如果 SQL 执行计划正常却偶尔变慢,还需要关注锁等待和系统负载。最终以实际执行耗时、扫描行数和并发性能作为验证依据,同时兼顾索引维护成本,避免局部查询优化影响系统整体性能。