做后端开发或者专职数据库运维的,对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 | 线程连接的唯一标识,也就是会话 ID | KILL 的时候要用的就是这个数字 |
| 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 在做什么、为什么卡住、该处理谁”的时候,这头牛就算真正解开了。