先说结论
先确认影响范围并止损,再通过监控、慢查询日志、执行计划和锁等待定位根因;根据访问路径、索引、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 指纹、扫描行数和长尾耗时。
- 对表结构变更进行线上影响评估和回滚演练。
- 为核心接口设置数据库耗时预算和自动告警。
常见问题
追问 1:线上突然出现慢 SQL,第一件事做什么?
先确认影响范围和当前瓶颈,必要时限流、降级或暂停非核心任务恢复服务,同时保存 SQL、参数、锁等待和资源指标。不要先执行高风险 DDL。
追问 2:扫描行数很少但 SQL 仍然慢,可能是什么原因?
可能在等待锁、等待连接、存储 IO 延迟高、网络返回慢,或单行包含大字段。应拆分等待时间而不是只盯执行计划。
追问 3:给慢 SQL 加索引一定有效吗?
不一定。低选择性条件、返回大量数据、排序聚合、锁等待或资源饱和都可能让索引收益很小;索引还会增加写放大和存储成本。
追问 4:如何判断联合索引字段顺序?
综合高频查询的等值条件、选择性、范围、排序和覆盖需求,而不是机械按选择性排序。还要兼顾已有索引复用与写入成本。
追问 5:为什么平均耗时不高还要优化?
平均值会掩盖长尾和参数倾斜。核心接口的 P99、最大耗时以及高频 SQL 的总资源消耗更能反映用户体验和系统风险。
追问 6:可以直接在生产执行 EXPLAIN ANALYZE 吗?
它会真实执行查询。对高成本查询或写语句存在风险,应先在测试环境、只读副本或受控条件下进行,并设置超时与资源保护。