☰
庖丁解牛:MySQL show processlist 慢查询排查实战指南
2026/10/19 10:53:33 网站建设 项目流程

做后端开发或者专职数据库运维的,对show processlist应该都不陌生。线上数据库一慢,告警群里第一个跳出来的命令大概率就是它:登录上去,跑一句show processlist,看看哪个会话的 TIME 最大,把那条 SQL 复制出来,丢给 EXPLAIN,加个索引,收工。如果每次都能这么顺利,那当然皆大欢喜,但现实往往不是这样。更多的时候,这条命令的输出会带来上百行结果,每行状态五花八门,有的Sending data,有的Waiting for table metadata lock,还有一堆Sleep。面对这些密密麻麻的状态,很多人会陷入两种极端:要么盯着 TIME 最大的那条 SQL 截图发给 DBA 等回复,要么实在看不下去就随便挑一个线程 KILL 掉,结果误伤了还在跑着的重要事务,反而把故障面扩大。

工具本身不难,难的是容易把这条命令当成一个截屏来看,而没把它当成一张解剖图去读。这篇文章就按“庖丁解牛”的思路,把show processlist完整拆一遍——从输出里的每一列是什么意思,到 State 背后对应数据库正在执行的哪个阶段,再到几个典型慢查询现场的完整排查链路,最后说说 KILL 这个动作什么时候该下手、什么时候不能碰。无论你是刚入门两年的后端开发,还是已经在负责生产库的运维,应该都能拿到一些能直接落地的东西。

1. Show Processlist 输出的第一课:先用 FULL 版本看清全貌

1.1 SHOW FULL PROCESSLIST 与 SHOW PROCESSLIST 的差别

先说一个很多人踩过但没意识到的坑:直接执行show processlist时,输出里的 INFO 列和 STATE 列会被截断,默认只保留大概 100 个字节。也就是说,一条 SQL 特别长的时候,你在终端里看到的可能只是 SQL 的前半段,后半段直接变成省略号。遇到这种情况,不少人第一反应是“终端宽度不够”,于是放大字体、拖宽窗口,折腾半天也没用。

真正的解决办法是加一个 FULL 关键字:

SHOW FULL PROCESSLIST;

加上 FULL 之后,INFO 列会展示完整的 SQL 文本,STATE 列也不再被截断。这个细节对排查慢查询来说非常重要,不然你连这条慢 SQL 到底查了哪几张表、用了哪些 WHERE 条件都看不全,后面的一切判断都是瞎猜。我现在有个习惯:只要登录 MySQL 做问题排查,第一句话永远是SHOW FULL PROCESSLIST,而不是不带 FULL 的版本。省掉一次来回执行命令的时间,也能避免因为 SQL 被截断而理解偏差。

1.2 输出里的八列,每一列到底在说什么

SHOW FULL PROCESSLIST的输出固定有八列,看起来结构很简单,但每一列背后对应的信息量其实不小。

列名含义排障时的主要用途
Id线程连接的唯一标识,也就是会话 IDKILL 的时候要用的就是这个数字
User当前连接使用的 MySQL 账号判断是哪个业务线或哪个应用账号在消耗连接
Host客户端来源地址和端口判断请求来自哪台应用服务器
db当前连接默认操作的数据库为空说明连接还没执行 USE 或语句里带了完整库名
Command连接当前所处的命令类别Sleep、Query、Daemon 等,判断会话是否在执行语句
Time线程处于当前状态持续的时间(秒)慢查询最直观的信号,但注意它不等于 SQL 总耗时
State线程当前正在执行的一步操作整个命令里最值得深入研究的一列,下章重点拆
Info当前正在执行的 SQL 文本没执行语句时就显示 NULL

这八列里,最容易让人误读的是Time和Command。

Time的单位是秒,它记录的是线程进入当前状态以后的持续时长,而不是这条 SQL 从开始到现在的总耗时。比如一个连接进入了Sleep状态,它可能已经在数据库上挂了十分钟,Time 显示 600,但这 600 秒里数据库基本不干活,只是在等客户端发下一条命令。所以看到 Time 大的会话,先别急着下结论,要结合 State 一起看。

Command这一列常见取值有Sleep、Query、Daemon、Quit等。线上看到大量Sleep连接非常正常,尤其应用用了连接池以后,池子里总会有一些空闲连接保持着 TCP 链路。排除故障时要关注的,不是有没有 Sleep,而是 Sleep 连接的数量是不是异常高、是不是把max_connections的额度快吃完了。如果一个业务库连接数快打满,同时大量连接是 Sleep,那问题的第一嫌疑是应用侧连接池配置或数据库端连接回收参数,而不是慢 SQL。

Info列在会话没有执行语句时是 NULL,这是正常现象。如果一个连接的 State 是Sleep同时 Info 也是 NULL,那它就是一个纯粹的空闲连接。

2. 看不懂 State,等于没解牛:高频状态实战释义

2.1 别把 State 当进度条

State 这一列可以说是整条命令里最核心的信息,也是大多数人最看不懂的部分。它描述的是:当前线程正在执行的这条 SQL,此刻正处在整个处理流程的哪一个步骤。

举个例子,你可以把它理解成一个做菜的流程。你问一个人正在做什么,他会告诉你“正在切菜”“正在烧水”“正在下锅翻炒”,而不是告诉你“已经完成了 27%”。数据库的 State 就是这种“正在做什么”的实时快照,而不是一个进度条。一个查询正常情况下会飞快地经过一系列 State,从解析、优化、执行到返回结果,整个过程可能几毫秒就完成了。慢查询之所以慢,本质上是线程长时间停在某一个 State 里出不来。

所以排查慢查询时,如果某一类会话的 State 长时间不变,Time 不断增长,那就说明数据库的资源和执行流程被卡在这一个环节,真正的根因大概率就潜伏在这个 State 背后。

2.2 高频 State 逐一拆解

下面这些 State 是生产环境里出现频率最高、也最有排查价值的几类。

Sending data

这个 State 名字最有迷惑性。光看字面像是“正在把数据发送给客户端”,很多人以为瓶颈在网络传输,其实不是。在 MySQL 的执行流程里,Sending data覆盖的是从存储引擎读取记录、逐行过滤 WHERE 条件、准备返回结果集的全过程,真正通过网络把数据包发出去的阶段只是其中很小一部分。所以当你看到大量会话停在Sending data且 Time 很大,优先怀疑方向是当前 SQL 的扫描行数太大、缺索引或者过滤条件写得不合理,而不是网卡带宽不够。

Copying to tmp table / Copying to tmp table on disk

这两个状态对应的是服务端在内部创建临时表。常见于GROUP BY、DISTINCT、ORDER BY以及一些复杂的多表连接场景。临时表如果只存在内存里,性能问题还不算太严重,一旦状态变成Copying to tmp table on disk,意味着临时表已经落盘,大量磁盘读写会拖垮整个查询。这个状态一旦出现,基本说明 SQL 的排序去重逻辑用得不够优雅,需要从执行计划层面找原因。

Sorting result

这个状态表示正在对结果集进行排序。MySQL 通常会用 filesort 来解决排序需求,虽然叫 filesort,但只要内存够用,不一定会落盘。与其纠结名字,不如去看EXPLAIN里的Extra字段是不是出现了Using filesort。如果排序字段本来有索引却还出现 filesort,那就要检查查询条件是否让索引失效了。

statistics

优化器在分析统计信息、估算扫描行数时会进入这个状态。正常来说它非常短暂,但如果你发现很多会话卡在这里,说明优化器在做代价估算时很吃力,常见于表非常大、统计信息陈旧或者内存压力大的场景。此时可以考虑运行ANALYZE TABLE刷新统计信息,但要注意操作窗口。

Waiting for table metadata lock

这个状态可以说是最需要警惕的,它代表线程在等待元数据锁(MDL)。典型场景是:一个长事务一直没提交,持有某张表的元数据锁,后面对这张表执行 DDL 的 ALTER 语句排在后面等待,而所有访问这张表的 SELECT 也要拿元数据读锁,于是整张表的所有访问全部卡死。这种问题如果不及时发现,业务会大面积报错。

Waiting for handler commit

这种状态常见于事务提交阶段。一个事务在提交时,要完成写 undo、更新 redo、持久化 binlog 等一系列动作,如果某一步比较慢,后续提交全部排队等待。遇到这个状态,优先看事务是不是太大,commit 的时候刷盘压力是不是过高。

为了方便速查,我把这些高频状态整理成一张简表:

State 关键词直观理解优先排查方向
Sending data正在读数据和处理 WHERE 条件索引、扫描行数、查询写法
Copying to tmp table正在创建临时表GROUP BY / DISTINCT / 多表连接写法
Copying to tmp table on disk临时表已经落盘磁盘性能、排序去重逻辑
Sorting result正在排序排序字段索引、filesort 出现原因
statistics优化器估算代价统计信息是否过期、表过大
Waiting for table metadata lock等待元数据锁长事务、DDL 阻塞链
Waiting for handler commit等待事务提交流程完成大事务、redo/binlog 刷盘压力

看到这里你可能会问:那行锁等待呢?为什么 State 里没有直接写Waiting for row lock。实际上,MySQL 在等待行锁时,State 往往还是显示为Sending data或Updating,因为它在执行计划层面仍处于数据处理阶段,具体的锁等待信息藏在事务和锁相关的系统表里。这个区分很重要,后面第三个场景会详细展开。

3. 三个典型慢查询排障现场:从现象到根因的完整链路

3.1 场景一:全表扫描拖垮报表接口

这个场景特别典型。某个报表接口平时也就几百毫秒,有一天突然变成几十秒,监控面板上数据库 CPU 并没有打满,但连接数在增加,一条查询都没法快速结束。

执行SHOW FULL PROCESSLIST后的画面是这样的:几十个会话的 State 都是Sending data,TIME 普遍在 5 秒以上,INFO 列里是同一张订单表的一个 SELECT,查询条件看起来也有 WHERE 语句,字段类型也比较合理。这时候不能急着加索引,而是先把其中一条 SQL 完整拿出来,执行EXPLAIN。发现type列显示的是ALL,rows估算接近全表记录数,Extra里没有任何用到索引的迹象。

初步判断是对的,问题确实出在扫描行数上。但为什么明明有 WHERE 条件,索引还是没生效?继续看查询条件,发现它在日期字段上套了一个格式化函数。字段本身是 datetime,但为了让统计报表按天汇总,SQL 里写了类似DATE_FORMAT(create_time, '%Y-%m-%d') = '2025-xx-xx'这样的写法。函数套在索引字段上,索引自然失效,MySQL 只能把全部数据扫一遍再去算函数结果。

处理方向分两步,先把当前问题消除:这个接口对应的查询是分析型语句,直接 KILL 掉已经堆积的慢会话,让业务先喘口气。然后从源头解决:改写 SQL,把日期函数去掉,改成create_time >= ... AND create_time < ...这样的范围条件,让索引能用上。实测把函数去掉、改成范围查询之后,同样的报表查询从几十秒降到了几百毫秒。

这个场景里SHOW FULL PROCESSLIST的意义在于快速定位“哪类 SQL 在拖后腿、同一类 SQL 有多少个在并发执行”。真正定位根因还要靠EXPLAIN看执行计划,两者配合才能既不冤枉索引,也不放过函数失效这类隐蔽写法。

3.2 场景二:行锁堆积:另一个“幽灵”在卡会话

第二个场景处理起来要比全表扫描更隐晦。业务方反馈某个核心交易接口变慢,执行SHOW FULL PROCESSLIST之后,确实能看到不少 State 为Sending data或Updating的会话,TIME 很大,Info 里是一些 UPDATE 语句,看起来和正常的写操作没什么区别。但如果只看show processlist,很容易误以为它们是慢 SQL 本身。

关键线索在于这些 UPDATE 语句都是一些很小范围的更新,理论上一瞬间就该完成。既然它们在学校里完全可以用毫秒计算,为什么现在都卡住了?这时需要跳出 processlist,去查看事务和锁的信息:

SELECT * FROM information_schema.INNODB_TRX\G

这条语句能看到当前所有 InnoDB 事务的运行状态、启动时间、锁定的行数等信息。执行后重点看trx_state、trx_started和trx_rows_locked。很快会发现有一个事务已经开了十几分钟没提交,trx_rows_locked数量很大。再回到SHOW FULL PROCESSLIST里核对,这个事务对应的会话 State 其实是Sleep,Info 是 NULL,看起来人畜无害,但它的线程 ID 对应的正是那个未提交事务的连接。

问题的链条一下就清晰了:某个应用连接开启事务后忘掉提交,锁住了一批行,导致所有要更新这些行的后续会话全部排队,排队过程中 State 停在执行阶段,表现得像慢查询。如果这时候贸然去 KILL 那些看似很慢的 UPDATE 会话,根本没意义——它们只是排队者,锁还在源头会话手里,KILL 掉一个排队者,后面的请求又会补上。

正确做法是确认源头事务是谁发起的、是否还能正常提交。确认无法继续完成业务后,再决定是让业务侧手动提交还是 KILL 这个空闲但持有锁的会话。这个场景给我的教训是:看到 TIME 很大的 UPDATE 不能想当然,先查事务,再决定 KILL 谁,顺序反了会引发更大问题。

3.3 场景三:metadata lock 风波:一条 DDL 锁住整张表

第三个场景在夜间变更或者高峰期做表结构变更的时候很容易碰到。现象比前两个都要吓人:某个应用的所有数据库连接几乎全部卡死,SHOW FULL PROCESSLIST一看,几乎所有会话的 State 都是同一个值——Waiting for table metadata lock,Info 里有 SELECT 也有 UPDATE,而且都指向同一张表。初始反应容易以为是表坏了,或者实例出了系统性故障。

其实这是 MDL 阻塞的典型画面。元数据锁是 MySQL 在表结构变更和普通 SQL 之间做的协调机制:任何一条 SQL 执行前都要先拿到表上的元数据锁(读锁),ALTER TABLE 这类 DDL 需要的是排他元数据锁。问题发生时,多是因为有一个长事务一直占着这张表的元数据锁没释放,DDL 排队等待排他锁;后续新进来的普通 SQL 因为看到排队的 DDL,也全部卡在获取读锁这一步。一锁未平,又等一锁,整张表对外表现为“假死”。

排查链路大致如下。第一步,在SHOW FULL PROCESSLIST输出里找到 State 为Waiting for table metadata lock的会话,把它们按 TIME 从小到大看,找出第一个卡住的线程——那通常是真正执行 DDL 的会话,它前面还挡着一个持有 MDL 的会话。第二步,去当前连接列表里找有没有一个看似完全不相关的 Idle 会话,State 为Sleep,但它背后存在一个未提交事务。第三步,在 MySQL 8.0 里可以直接查锁等待链路:

SELECT * FROM performance_schema.metadata_locks\G

或者使用 sys 库下的现成视图,能直接看到“谁持有锁、谁在等锁”的依赖关系。

定位到根因后,处理方式是:让持有锁的那个会话尽快提交或回滚事务,DDL 自然就能跑下去,所有排队的 SQL 也会瞬间恢复。这件事给我们的长期教训是,任何 DDL 操作之前,先查一下information_schema.INNODB_TRX,确认没有长事务占坑,再动手做结构变更。凌晨跑变更脚本时,不能只看时间窗口合适,还要看业务有没有遗留未提交的事务。

4. 光看快照远远不够:把 Processlist 和慢日志、EXPLAIN、性能视图配合起来

4.1 Processlist 是“现在”,慢查询日志是“过去”

SHOW FULL PROCESSLIST是有局限性的,它只能反映当前一瞬间的会话状态。故障发生在凌晨两点,你早上九点才登录上去看,可能什么都看不到——慢 SQL 早就执行完了,现场也没留下任何痕迹。

所以一个靠谱的排障习惯是:把慢查询日志作为最主要的“历史监控”。MySQL 里与慢日志相关的核心参数有四个:slow_query_log是否开启,slow_query_log_file日志文件路径,long_query_time判定阈值,log_queries_not_using_indexes记录所有没走索引的查询。MySQL 8.0 默认long_query_time是 10 秒,也就是超过 10 秒的查询才会被记录,这个阈值对大多数业务来说太宽松了。建议生产环境调到 1 秒到 2 秒之间,同时开启log_queries_not_using_indexes,把所有没走索引的查询也记录下来。

第二天一早来上班,只需要做一件事:打开昨天的慢日志文件,按照执行次数从多到少排序,一眼就能看出哪些 SQL 是常态化的隐患。这和show processlist恰好形成互补——一个是当下,一个是昨天。

4.2 information_schema.PROCESSLIST:比 show 命令更自由

SHOW FULL PROCESSLIST是一个命令,输出格式固定,不太方便过滤。其实同样一份数据还可以通过information_schema.PROCESSLIST表来查,它能直接用 SQL 语法过滤和排序,排查效率高很多。

比如想找出所有执行时间超过 30 秒且不是 Sleep 的会话:

SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, LEFT(INFO, 120) AS sample_sql FROM information_schema.PROCESSLIST WHERE TIME > 30 AND COMMAND <> 'Sleep' ORDER BY TIME DESC;

再比如想统计当前到底有多少连接、State 分布情况如何:

SELECT STATE, COUNT(*) AS cnt, MAX(TIME) AS max_time FROM information_schema.PROCESSLIST GROUP BY STATE ORDER BY cnt DESC;

这类查询的灵活性比命令行高很多,写一次存成常用的 SQL 脚本,以后每次排查直接改条件就能用。要注意的是,如果你用的数据库账号权限不够,可能只能看到自己的会话,看不到其他用户的全量信息,生产环境排查时需要一个具备足够权限的账号。

4.3 用 EXPLAIN 和 performance_schema 锁死根因

show processlist能告诉你“哪条 SQL 慢”,但要说清楚“为什么慢”,必须靠EXPLAIN。EXPLAIN输出的type列如果出现ALL,几乎可以断定查询在做全表扫描;key列如果是 NULL,说明该用的索引一个都没用上;rows列给出的是优化器估算的扫描行数,如果这个数字异常偏大,就算表不大,也要继续深挖查询条件。

另外再补充一个容易被忽略的工具视角:performance_schema。MySQL 5.7 开始默认开启performance_schema,其中events_statements_summary_by_digest这张表会把结构相似的 SQL 聚合后统计总耗时、平均耗时、执行次数等指标,非常适合做常态化的慢语句分析。排障时如果想知道“这一类 SQL 在过去一小时到底执行了多少次、累计耗了多少时间”,直接查这张表比翻日志更快。

SELECT DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT / 1000000000 AS total_sec, AVG_TIMER_WAIT / 1000000000 AS avg_sec FROM performance_schema.events_statements_summary_by_digest ORDER BY total_sec DESC LIMIT 10;

这条查询可以看到当前实例累计统计里最耗时的十类语句。它的价值在于把散落的慢查询按模式归并,让你快速识别出真正的高频、高耗时语句,而不是只盯着某一条偶发的异常 SQL。

5. 止损分寸:KILL 会话的正确姿势与日常防线

5.1 KILL QUERY 与 KILL CONNECTION

看到慢查询堆积,第一反应往往是 KILL,这很正常。但 KILL 不是只有一种姿势,MySQL 支持两种回收方式:

-- 只终止当前正在执行的语句,保留连接 KILL QUERY 12345; -- 终止整个连接 KILL CONNECTION 12345; -- 或者直接简写 KILL 12345;

KILL QUERY只会终止会话当前正在执行的 SQL,连接本身还保留。KILL CONNECTION则会把整个会话断掉,客户端需要重新建立连接。优先推荐先使用KILL QUERY,因为断掉连接本身会产生额外的连接重建开销,在应用使用短连接或者中间件管理连接池的情况下,大量KILL CONNECTION会引起一波连接风暴。

有一个很关键的点:KILL 的时候要确认你杀的是哪一类会话。如果会话是排在锁等待队列里的受害者,KILL 掉它只是解决了表象,持有锁的源头会话还在那里,新请求还会继续堆积。看清楚了 State、确认了事务链,再动手,这才叫止损,否则叫添乱。

5.2 不能急着 KILL 的几种情况

KILL 看起来是最快的手段,但有几个场景必须谨慎。

第一个是已经运行了很长时间的大事务写操作。比如一个 UPDATE 已经跑了 20 分钟,你执行KILL QUERY之后,InnoDB 需要回滚已经修改的数据,回滚过程要重新扫描 undo 日志,耗时很可能比原本执行的 20 分钟还要长。所以遇到 DML 大事务卡住,先评估是让它跑完还是让它回滚,两害相权取其轻,不能一杀了之。

第二个是持有 MDL 锁的空闲事务。在 3.3 的场景里,问题会话 State 是Sleep,Info 是 NULL,看起来根本不像需要杀的对象。但如果这个空闲连接背后挂着一个未提交事务,它就是所有卡顿的根源。杀它之前最好能联系到业务侧,确认这个事务是否还要提交,因为KILL会直接触发回滚,业务准备好的数据可能就白忙活了。

第三个是连接数已经接近max_connections上限、新的运维连接都很难建立的情况。此时SHOW FULL PROCESSLIST都可能因为拿不到连接而无法执行。所以生产环境最好预留一条管理通道,或者定期清理连接,避免把连接配额用满。运维连接本身也是一条连接,把自己关在门外这种事并不少见。

5.3 把排查经验沉淀成日常监控

最后,把本文所有经验串起来,其实就是一件事:让show processlist从“故障时才用的急救工具”变成“日常就在跑的信号捕捉器”。

我自己在生产库里做了一个很简单的巡检脚本:每隔 30 秒抓一次information_schema.PROCESSLIST,筛选COMMAND <> 'Sleep'且TIME > 10的会话,写入本地日志。平时这个日志没什么存在感,但一旦哪天有问题回溯,往前翻就能看到故障发生时数据库到底在跑什么、哪个 State 最先冒头。

再配合慢日志按天落盘、定时把慢日志里 TOP 语句发到群里,以及performance_schema里按语句摘要聚合出的 TOP 耗时模式,整个数据库的“实时状态—历史记录—根因分析”就闭环了。真正能在下一次故障里救你的,不是某一条命令本身,而是你把这条命令放到完整判断链路里去的习惯。

庖丁解牛到最后,看到的其实不是牛的骨架,而是结构本身。show processlist也是一样——当你哪天看到满屏状态不再慌张,而是能从一列 State 里读出“哪条 SQL 在做什么、为什么卡住、该处理谁”的时候,这头牛就算真正解开了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询