我看过太多人一听到慢 SQL,第一反应就是“加索引”。这个思路不能说错,但很容易把一个系统的性能拖进更深的坑。上个月我处理过一条线上慢查询,单次执行将近 8 秒,慢查询日志里半小时内累积了几百次。表面看 SQL 确实简单,就是一张大表按条件过滤后取订单数据,可问题恰恰出在索引和 JOIN 顺序的配合上。我最初也试过直接加索引,结果执行计划根本没选它,反而因为索引维护成本把写入拖慢了。
这类问题处理多了之后,我沉淀了一套自己的排查流程:先看慢日志确认瓶颈在哪一层,再用执行计划定位扫描路径,最后才是动手改写 SQL 或者调整索引。今天我把这套流程完整还原出来,里面会用几个我真实处理过的 SQL 优化实例做拆解,包括深分页、OR 条件索引失效、函数运算导致无法命中索引,以及并行 SQL 优化到底应该用在什么场景。如果你是后端开发、DBA,或者正在处理接口超时但不知道从哪下手,这篇应该能给你一条清晰的排查主线。
1. 拿到一条慢 SQL,我先看什么:监控与分级
很多团队定位慢 SQL 的方式是“线上报错了再登录数据库查一下”,这其实已经晚了。慢 SQL 优化的第一步不是改 SQL,而是建立能持续观测的机制,否则你连“哪条 SQL 慢、慢了多少次、影响哪些接口”都答不上来。
1.1 慢查询日志是全量事实
MySQL 的慢查询日志是最直接的线索来源。线上环境一般会把阈值调整到 1 秒以内,而不是默认的 10 秒,否则很多影响体验的查询根本不会被记录。你可以在 MySQL 配置文件中加入:
ini复制slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow-query.log
long_query_time = 1
log_queries_not_using_indexes = 0
log_queries_not_using_indexes 这个参数我建议谨慎开启。它会把所有没走索引的查询都记录进来,在小表居多的系统里会产生大量噪音日志,反而淹没了真正需要关注的慢查询。我见过有人开完之后日志文件每小时涨几个 GB,最后只能紧急关掉。
慢日志里每一行关键信息都很重要,尤其是下面这几项:
| 字段 | 实际含义 | 为什么要关注 |
|---|---|---|
| Query_time | SQL 执行总耗时 | 决定这条 SQL 是否达到优化标准 |
| Lock_time | 锁等待时间 | 如果占比高,问题可能在并发事务而不是 SQL 本身 |
| Rows_examined | 实际扫描行数 | 扫描行数和返回行数差距越大,优化空间通常越大 |
| Rows_sent | 最终返回行数 | 用来和 Rows_examined 做对比 |
| 执行时间戳 | 语句发生的时间点 | 判断是偶发抖动还是持续慢 |
我处理过的案例里,有相当一部分初看是“慢 SQL”,实际看 Lock_time 占掉大半,根因是行锁竞争,那 SQL 本身改不改都不解决核心问题。
1.2 先分清是真慢还是偶尔慢
拿到一条慢 SQL 之后,别急着 EXPLAIN,先把它出现的时间规律拉出来看。如果只在每天凌晨某段时间出现,很可能是定时任务和核心业务在抢资源;如果只出现在每周一大促流量高峰,那要考虑连接池和缓存命中率;如果没有任何规律,全天都在出现,那才是 SQL 本身的设计问题。
我自己习惯把慢日志里的 SQL 按小时聚合,统计出现次数和平均耗时。一个非常实用的经验是:同样的 SQL 偶发慢和持续慢,处理策略完全不同。偶发慢可能是 Buffer Pool 冷启动、磁盘 IO 抖动或者备库延迟导致;持续慢则往往是执行计划已经走偏,或者数据量增长超出了当初的设计水位。
1.3 同类慢 SQL 合并,按“综合损失”排优先级
优化资源是有限的,所以我会把慢日志里的同类 SQL 先做合并。不同业务系统慢日志格式不一样,但逻辑上都可以按 SQL 模版去重,也就是把参数值替换成占位符之后归组,然后算一个综合损失值:
综合损失 = 平均执行时间 × 单位时间出现次数
这个值越大,优先处理的等级越高。很多时候一条执行 2 秒的 SQL 如果每秒跑几十次,比一条执行 30 秒但每天只跑一次的分析 SQL 对系统伤害更大。前者会持续占满数据库连接,最终拖垮整个实例;后者虽然单次痛苦,但有充足的时间窗口去处理。
分级之后,我们再对每一条慢 SQL 做执行计划层面的检查。这也是下一章要展开的部分。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从执行计划开始一次系统排查
执行计划是数据库优化器给出来的“执行路线图”。拿到慢 SQL 先别猜,用 EXPLAIN 看优化器实际打算怎么跑,这一步能省下大量试错时间。
2.1 EXPLAIN 里的每个核心字段都在说什么
MySQL 的 EXPLAIN 输出有几个字段需要特别养成条件反射级的敏感度:type、key、rows、Extra。
type 是访问类型,性能从好到差大致是:system > const > eq_ref > ref > range > index > ALL。ALL 代表全表扫描,index 代表扫了整棵索引树,这两个在慢 SQL 里出现时通常意味着需要优化。range 是索引范围扫描,在业务查询里已经算不错的状态。
rows 是优化器预估需要扫描的行数,Extra 里则会出现一些特殊标记。这里我提前说一个最常误导新人的点:Using filesort 不代表在磁盘上排序,它只是说明 MySQL 需要额外做一次排序操作,可能在内存中完成,也可能落盘。出现这个标记时,优先考虑排序字段是否和索引顺序一致。
下面是一个典型的执行计划片段,我故意保留了这个案例原本的样子:
text复制+----+-------------+-------+-
