我怎么沿着一条慢 SQL,把“像索引问题”一路查到真实瓶颈
慢 SQL 排查最容易一上来就盯 explain,然后把所有问题都讲成索引问题。这篇不做总纲,只沿一条真实排查线往下走:从接口变慢开始,先确认是不是这条 SQL,为什么 explain 看起来还行却还是慢,最后怎么把瓶颈坐实到锁等待和连接占用上。
慢 SQL 这件事,最容易把人带进一个很熟悉的动作:先把 SQL 拿出来,跑一遍 EXPLAIN,然后开始盯 type、rows、key。
这个动作当然不算错,但我后来越来越警惕一件事:很多“慢 SQL 排查”其实根本不是被执行计划卡住,而是被我们太早把问题定性成“索引问题”。
这篇不打算再把慢查询排查讲成一套大而全总纲。我只想讲一条我自己觉得很典型的线:
- 一个订单列表接口在高峰期突然变慢
- 我们第一反应觉得是分页 SQL 退化了
EXPLAIN看起来也不能说太差- 但线上就是慢,而且越高峰越明显
- 最后真正坐实的瓶颈,不是执行计划本身,而是锁等待把连接占用时间拖长了
我很喜欢这条线,因为它几乎把慢 SQL 现场最常见的误判都带了一遍:
- 看见列表查询,就默认深分页或索引有问题
- explain 还行,就以为数据库没问题
- 接口耗时高,就把“SQL 执行慢”和“连接等待长”混成一件事
如果你也经常碰到“看起来像 SQL 慢,但又不完全像”的现场,这条线比总览更接近真实排查。
一、事情是从一个很普通的慢接口开始的
那次先冒出来的是订单后台列表。
现象很常见:
- 高峰期 RT 从几百毫秒慢慢抬到两三秒
- 低峰又会恢复
- 不是所有接口都慢,主要集中在订单写后立刻查询、运营筛单这几个入口
- 应用日志里能看到数据库调用时间变长,但错误率并不高
因为是列表查询,团队第一反应很自然:
- 是不是分页太深了
- 是不是
order by create_time desc没吃到索引 - 是不是最近数据量涨到某个拐点,访问路径退化了
这些怀疑都很合理,所以我们先把最可疑的那条 SQL 捞了出来。
大致长这样:
select id, order_no, user_id, status, create_time
from orders
where tenant_id = ?
and status = ?
order by create_time desc
limit 20;
它看起来非常像那种“如果索引没配好就会慢”的标准案例。
二、第一轮看 explain,确实像是条值得怀疑的 SQL,但还没到能直接定罪的程度
我们先跑了 EXPLAIN。
第一眼看下来,不算完美,但也没有差到一眼宣判:
- 用到了联合索引
type不是ALLrows估算值也还在能接受的范围Extra里虽然有需要继续警惕的地方,但不像那种完全失控的全表扫
这一步给我的感觉是:
这条 SQL 不是完全没问题,但它也不像“为什么高峰期会突然慢成这样”的全部答案。
因为如果执行计划本身已经坏到很离谱,那低峰通常也不会那么稳。可当时的现象是:
- 平时还能跑
- 流量一上来就明显变差
- 甚至同样一条 SQL,有时 200ms,有时 2s+
这就让我不太愿意太早把结论落成“去补索引”或“重写 SQL”。
为什么我没有停在 explain 这一步
因为 explain 更擅长回答的是:访问路径像不像有问题。
但那次我真正想确认的是另一句:
这条 SQL 之所以慢,到底是它自己执行得慢,还是它在某个等待上把时间耗掉了?
这两种情况在应用里都会表现成“数据库耗时高”,但处理方式完全不同。
三、接下来我先去对时间窗,而不是继续猜 SQL 文本
那次真正把方向拉回来的,不是继续盯 SQL 本身,而是把几个时间窗对在一起看:
- 接口 RT 的抬升时间
- 数据库调用耗时的抬升时间
- 连接池 pending 和获取连接耗时
- 数据库里的活跃事务、锁等待和慢 SQL 数量
一对就发现了一个很关键的细节:
- 慢 SQL 数量确实在涨
- 但连接池 pending 上升得更早
- 锁等待耗时也比慢 SQL TopN 更早冒头
这个先后关系很重要。
如果真是 SQL 访问路径先退化,通常我更期待先看到:
- 这条 SQL 本身执行时间明显拉长
- 慢日志里它稳定上榜
- 然后连接池才因为连接占用变长被拖起来
可那次不是这个节奏。更像是:
- 数据库里某种等待先发生了
- 连接归还慢了
- 应用获取连接时间变长
- 最后业务统一感知成“这个列表 SQL 变慢了”
也就是说,SQL 确实慢了,但慢得不一定只在执行本身。
四、这时再回头看 SQL,问题就不再是“它走没走索引”,而是“它为什么会被拖在这里”
方向一变,排查重点也跟着变了。
我不再只问:
key用得对不对rows大不大- 有没有 filesort
而是开始问:
- 这条查询是不是刚好在读某批被频繁更新的订单
- 高峰期是不是有写事务把这些记录锁得更久
- 列表查询本身不慢,但它是不是经常排在等待后面
后来顺着事务和锁去查,现场就越来越像了。
那段时间刚好有一组订单状态批量更新任务,和在线订单写链路撞在一起。它们不是每次都锁很久,但高峰时竞争明显变多。
于是就出现了一个特别容易误判的表象:
- 列表查询作为读请求,本身 SQL 文本看着没那么糟
- explain 也没有明显失控
- 但它经常卡在前面事务释放之后才能真正跑起来
换句话说,接口侧感知到的“这条 SQL 很慢”,里面其实混了等待时间。
五、真正把问题坐实的,不是 explain,而是锁等待和连接占用一起对上了
最后最关键的证据,其实有两组。
第一组:数据库内部等待
我们看到:
- 活跃事务在高峰时明显拉长
- 锁等待耗时和热点对象集中在订单相关记录
- 慢查询抬升和锁等待时间窗高度重合
这说明所谓“慢 SQL”至少有相当一部分,不是纯扫描慢、排序慢,而是在等。
第二组:应用侧连接池
同时还能看到:
- 获取连接耗时在同一时间窗里上升
- pending 数量先涨起来
- 一部分接口的数据库总耗时里,连接等待已经占了可观比例
这两组证据一拼,就比“这条 SQL explain 不够漂亮”更有说服力了。
因为它把整条链串起来了:
- 写事务竞争加重
- 锁等待变长
- 连接占用时间被拖长
- 获取连接开始排队
- 最后接口统一表现成数据库查询慢
这时候再回头看,一开始那句“这是一条慢 SQL”并不算错,但它其实太粗了。更准确一点说,是:
这条 SQL 在高峰期被锁等待和连接占用一起拖慢了。
六、最后怎么处理,反而不是先改 explain 里最显眼的那一点
既然瓶颈已经收窄到等待链,动作就不会只盯 SQL 文本本身。
那次我们优先做的几件事,大致是:
- 先把高峰期撞上的批量更新任务错峰
- 收紧某段事务边界,避免拿着连接做过多非必要逻辑
- 复查热点写路径的更新方式,减少同批记录上的竞争
- 最后才回头检查列表查询索引是否还能再收紧一点
这里最想强调的一点是:索引和 SQL 优化不是没价值,而是那次它不是第一落点。
如果一开始就把全部精力花在 explain 上,最后可能也会做一点优化,但止血速度不会快,因为真正把时间拖长的那一截没有被先拿掉。
七、这条线给我最大的提醒,是慢 SQL 里那个“慢”字一定要拆开问
我现在再碰到类似现场,脑子里通常会先过一遍下面这几个问题:
1. 这条 SQL 是一直慢,还是只在高峰慢
如果只在高峰慢,我会更警惕等待链、资源争抢、事务竞争,而不是立刻把锅甩给访问路径。
2. explain 是真差,还是只是没那么漂亮
如果 explain 已经明显失控,那当然要优先处理。但如果 explain 只是“能优化”,却不足以解释现场抖成那样,就别太早定罪。
3. 应用感知到的数据库耗时里,混没混获取连接时间
这一步特别关键。很多“SQL 慢”其实已经掺进了连接等待。
4. 数据库里这个“慢”到底更多发生在执行,还是发生在等待
锁等待、长事务、热点写冲突,都会把“慢 SQL”这四个字变得很含糊。
八、如果你也在排一条慢 SQL,我建议先把这一条线记住
不是说不要看 explain,而是别只看 explain。
更接近真实现场的顺序,通常是:
- 先确认是不是这条 SQL 真在出问题
- 看 explain,但只把它当成访问路径证据
- 把接口 RT、数据库调用耗时、连接池、事务、锁等待放到同一时间窗
- 先分清慢在执行,还是慢在等待
- 最后再决定动作该落在索引、SQL 写法、事务边界,还是任务错峰
慢 SQL 最容易误导人的地方,就是那个“慢”字太像一个结论。
可真实线上里,它常常只是一个表象。
有时候慢的是扫描,有时候慢的是排序,有时候慢的是锁,有时候慢的是连接,有时候慢的是整条等待链一起被放长。
把这件事拆清楚,才比较像真的在排障,而不是在对着一条 SQL 做想象题。