Java

explain 看起来没问题,SQL 还是很慢,下一步该查执行、等待还是连接池?

explain 看着正常,不等于 SQL 在线上就真的没问题。先把时间花在哪里拆开,往往比继续争论索引对不对更有用:卡在拿连接、库内执行、数据库等待,还是返回后又被应用放大了。

  • MySQL
  • explain
  • 慢查询
  • 数据库
  • 性能排查
16 分钟阅读

这类 SQL 排查我见得最多的一种卡法,是 explain 已经看过,索引也不像完全走歪,可接口还是慢,团队一下子就僵在原地。

  • typekeyrows 都不算离谱
  • 索引看起来也走上了
  • 但线上 RT 还是高,接口还是慢
  • 团队内部开始争论,到底该继续查数据库,还是转去查应用

这类场景最容易出现的误判是:把 explain 正常,当成 SQL 一定没问题。

更接近真实现场的说法应该是:explain 只能说明访问路径没有明显走歪,但它回答不了运行时的很多关键问题,比如锁等待、长事务、连接占用、高峰期路况变化,以及数据库返回后应用自己又把整体耗时放大了多少。

所以这篇不再重复 Explain 字段本身,而是专门处理另一个更容易卡人的判断:访问路径看着还行时,慢到底慢在执行、等待,还是连接池和应用侧感知。

explain 看起来没问题,但 SQL 还是很慢。下一步最值得查的是执行成本、等待成本,还是连接池放大的感知?

如果你现在连慢点是不是落在数据库都还没坐实,只是先看到接口 RT 高,那先把影响面和数据库阶段耗时对上,再回来拆这几段时间。

下面这些现场,先从这篇下手

如果你碰到的是下面这些现场,就先从这里拆:

  • 你已经看到某条 SQL 或某段数据库阶段耗时偏高
  • explain 看起来正常,但高峰期还是慢
  • 慢的不只是单条 SQL 文本本身,而是整段数据库停留时间
  • 团队已经在争论“到底是数据库慢,还是连接池 / 应用把数据库问题放大了”

如果你现在连影响面都还没分清,比如只是“某个接口慢了”,先回到影响面判断那条线;如果你最先看到的是获取连接时间变长,就直接接 连接池等待时间变长时,如何判断是数据库慢还是应用拿着不放?

第一轮先把慢点分型,别继续只盯 explain

如果你现在已经拿着 explain 在看,我建议先暂停一下,把这 4 段时间补齐:拿连接耗时、数据库内部执行时间、数据库等待时间、结果返回后应用处理时间。为什么我要先拆这 4 段?因为 explain 只解释访问路径,不解释运行时等待,更不解释数据库返回以后应用自己又把总耗时放大了多少。时间不先拆开,后面再看索引也很容易越看越虚。

先不要急着再刷一遍执行计划,先把你现在看到的现象放进下面这张表里。

你现在看到的现象更像哪一类下一步先查什么
平峰也慢,数据量一大就更差,返回行数和排序开销都比较重执行成本偏高先核对下面“执行慢”这一段里的结果集、参数分布和返回后处理
高峰更容易慢,写请求更敏感,RT 抖动大于均值抬高等待成本偏高先核对下面“等待慢”这一段里的锁、事务和高峰期等待
获取连接先变慢,随后接口 RT 和线程排队一起变差连接池放大感知先去核对连接为什么回不来
单接口慢得明显,其他接口基本正常更像局部链路问题先回头确认影响面,再决定要不要继续查数据库
数据库不高负载,但 active sessions、连接占用、排队感一起抬高仍在数据库等待链先看等待链,再决定是不是锁或事务
SQL 执行时间不算夸张,但接口整体很慢可能是数据库返回后又被应用放大同时核对连接持有和应用处理

这一步的意义是:先把“慢”拆成类型,而不是继续把所有问题都塞回 Explain 里。

先把“SQL 很慢”拆成四段时间

排查 explain 正常但 SQL 仍慢,最值钱的动作不是继续抠某个字段,而是先把总耗时拆开。

至少先区分下面四段:

  1. 拿连接之前:线程是不是先卡在连接池里
  2. 进入数据库后:SQL 真正在执行什么
  3. 数据库内部等待:是不是在等锁、等事务、等资源路况
  4. 数据库返回之后:结果集处理、对象组装、事务内其他逻辑有没有继续放大

如果这四段不拆开,你看到的“SQL 很慢”很可能只是一个总感知,真实问题却已经分叉成三条完全不同的线:

  • SQL 本身执行成本高
  • SQL 跑得不算离谱,但等待时间更长
  • 数据库阶段只是起点,后面被连接池和应用继续放大

如果更像执行慢,先核对哪几件事

当 explain 正常但平峰也慢、数据量一大就更差,这类问题通常更接近执行成本本身。

1. 看返回结果是不是比接口真实需要的更重

常见问题包括:

  • 本来只需要一页数据,却返回了更大的结果集
  • select * 把大字段一起带回
  • 排序字段和过滤字段没形成更稳定的访问路径
  • 深分页、回表、临时表把单次成本悄悄拉高

这类问题 explain 可能看起来“不算差”,但真实耗时依然会被结果集规模和返回路径放大。

2. 看慢是不是强依赖参数分布

如果只有某类参数、某个租户、某种时间区间更慢,说明问题不一定是“整条 SQL 设计错了”,而可能是这类请求命中了更坏的数据分布。

这时要回到具体场景看:

  • 是不是大页码或大范围查询更慢
  • 是不是某批热点数据导致扫描范围变大
  • 是不是单接口特有的查询模式在放大问题

如果你还没确认这是不是单接口独有问题,可以先回到 单接口变慢和整个服务变慢,排查入口为什么完全不同?

3. 看数据库返回后,应用是不是又把耗时接着放大

有些现场会把这类问题也统称成“SQL 慢”,但真实情况可能是:

  • 数据库阶段只占其中一部分
  • 返回结果以后还有重对象组装
  • 事务里还有循环处理、二次查询或序列化

如果这一层明显成立,就不要继续只盯 SQL 文本本身,而要连同连接持有时间一起看。

如果更像等待慢,优先查什么

当 explain 正常、高峰明显更差、写请求或事务型接口更敏感时,更应该先查等待,而不是继续证明访问路径没问题。

1. 先看是不是锁等待或事务竞争

这类场景最常见的特征是:

  • 写接口比纯读接口更容易慢
  • 同一批业务键值在高峰时更容易出问题
  • RT 抬高和并发写入、状态流转同时出现
  • 数据库 CPU 不一定高,但停留时间明显拉长

Explain 在这里经常“看着没问题”,因为真正慢掉的不是路径,而是等待别人释放锁、提交事务、归还资源。

2. 再看是不是长事务把等待整体包了进去

很多团队会盯着 SQL 语句本身,却忽略更本质的一层:事务边界太大。

只要事务里还有额外业务逻辑,即便 SQL 本身不算特别慢,也会表现成:

  • 锁持有时间更长
  • 连接归还更慢
  • 后续请求开始排队
  • 团队统一感知成“数据库又慢了”

如果你已经看到连接池也开始紧,下一步别只留在 SQL 这条线上,要一起接 连接池等待时间变长时,如何判断是数据库慢还是应用拿着不放?

3. 再看是不是高峰期路况变化,而不是静态计划问题

Explain 给的是路线,线上高峰给的是路况。

你需要额外确认:

  • 问题是不是只在高峰或批任务时间窗出现
  • active sessions、连接占用、接口排队是否同一时间段一起抬高
  • 数据库没打满,但等待型信号是不是已经先坏了

如果这些都成立,更说明你仍然在数据库等待链里,而不是已经可以把数据库排除了。这个判断边界,正是 数据库没打满,为什么 API 和连接池已经开始变慢? 那篇文章重点想帮你切清的地方。

什么情况下该从 SQL 线切回连接池或应用侧

这一步很重要,因为 explain 正常的现场里,最容易出现“数据库和应用互相甩锅”。

1. 获取连接先变慢

如果顺序是:

  • 获取连接耗时先抬高
  • 活跃连接接近上限
  • 接口 RT 和线程排队随后变差

那就别继续只在 SQL 语句上打转了。你更需要先回答:连接为什么回不来。

2. SQL 执行不算夸张,但连接持有很久

如果 SQL 时间和事务时间不成比例,说明连接可能被应用拿去做了数据库之外的事,比如:

  • 事务里调 RPC / HTTP
  • 事务里做循环计算或对象处理
  • 事务里放了不该放的下游等待

这时更适合转去看连接池那条线,而不是继续只看 Explain。

3. 只有一个接口或一类参数慢

这说明问题可能还没有广泛到整个数据库链路,而更像局部路径、数据特征或业务参数。先回头确认影响面,更能避免过度泛化。

最容易误判的几个地方

误判 1:explain 正常,数据库就没问题

不成立。Explain 只能排掉最表层的访问路径异常,不能排掉锁等待、事务持有、连接占用和运行时路况变化。

误判 2:只要走了索引,就不该慢

也不成立。走了索引,也可能慢在等待、回表、排序、结果集过大,或者数据库返回后应用处理过重。

误判 3:既然 explain 正常,那就一定是应用问题

也不成立。很多问题依然在数据库等待链里,只是已经不再是“没走索引”这种最表层症状。

误判 4:只看 SQL 文本,不看时间结构

真正更稳的判断,需要一起看:获取连接、SQL 执行、事务时长、连接持有和接口总 RT。只盯其中一项,很容易错判。

FAQ

1. explain 正常,是不是说明索引已经没问题了?

不是。它只能说明优化器当前选择的路径没有明显走歪,不能说明真实运行时一定没有锁等待、长事务或返回成本问题。

2. explain 正常但高峰期还是慢,优先查什么?

优先查等待链,尤其是锁等待、事务竞争、active sessions、连接占用和高峰期路况变化,而不是再做一轮静态字段解释。

3. 什么时候应该从这篇转去看连接池那条线?

当你已经看到获取连接变慢、活跃连接上升、连接归还变慢,或者 SQL 时间不算离谱但事务和连接持有明显更长时,就该切过去。

4. 如果我还不确定是不是数据库方向,应该回哪篇?

优先回 数据库没打满,为什么 API 和连接池已经开始变慢?。如果连影响面都还没切清,再往上回 单接口变慢和整个服务变慢,排查入口为什么完全不同?接口响应慢怎么排查?后端 API 变慢与超时的定位步骤。这篇更适合放在你已经确认问题还留在数据库链路里之后再接着看,别把前面的分流判断和库内细查混在一起。

如果你现在更缺这类判断

如果你顺着这篇往下排,通常会接到下面几种相邻问题:

最后一句收束

遇到 explain 正常但 SQL 还是慢,不要继续在静态执行计划里兜圈子。更稳的顺序是:先拆拿连接、数据库执行、数据库等待、返回后处理四段时间,再决定继续查执行成本、等待成本,还是连接池和应用持有连接这条线。