1. 大导入场景下的 pg_stat_activity 核心视图解读
1.1 为什么大导入要死盯这个视图
PostgreSQL 的 pg_stat_activity 可以说是每个 DBA 都会装进脑子里的"进程体检表"。平时小打小闹的查询,没人会去翻它,但一旦遇到大导入——几十 GB 的 CSV 灌进去、pg_restore 恢复整个库、或者跑一个 UPDATE 全表的大事务——事情就完全不一样了。这种时候你脑子里通常只有三个问题:它到底跑没跑?跑到哪了?还是已经卡死了?
这三个问题,pg_stat_activity 都能给你答案,前提是你得知道怎么看、看哪些字段、怎么排除干扰项。
这个视图在每个数据库会话上保留一行记录,只要有人连上了 PostgreSQL,就会有一行。大导入的本质也是一条普通的 SQL 或者 COPY 命令,所以它在 pg_stat_activity 里同样有对应的会话。你需要做的,就是学会从这一大堆字段里快速提炼出对"大导入是否正在执行"这件事最有价值的信息。
1.2 定位导入进程的基础查询
先给出一套我在生产环境里反复用的查询模板。它能把所有非后台的、正在跑或最近有动作的会话都捞出来,一眼定位到你的导入进程:
sql复制SELECT
pid,
usename,
application_name,
client_addr,
state,
wait_event_type,
wait_event,
now() - query_start AS query_runtime,
now() - xact_start AS xact_runtime,
left(regexp_replace(query, '\s+', ' ', 'g'), 200) AS query_sample
FROM pg_stat_activity
WHERE datname = current_database()
AND backend_type = 'client backend'
AND pid <> pg_backend_pid()
ORDER BY query_start ASC;
这里我刻意做了一些处理和最初的裸查询不一样的地方,每个字段都有用意。比如 backend_type = 'client backend' 是 PG 11 之后才引入的字段,用来过滤掉 autovacuum worker、logical replication launcher 这些后台进程,不然会把结果搞得很乱。regexp_replace 把换行和连续空格压缩成单个空格,避免 query 里带换行导致终端显示错乱。left(..., 200) 是因为 pg_stat_activity.query 默认只记录前 1024 字节(由 track_activity_query_size 参数控制),但你展示的时候截断到 200 字符就足够判断会话身份了。
用这个查询跑出来的结果,如果表格里出现了 state=active 的行,query_sample 里又带着 COPY、INSERT、UPDATE、CREATE INDEX 这类关键词,那基本就能确定你的大导入还在执行。但这里有个陷阱——仅仅是 state=active 并不代表数据在写入,这个我们放到第 2 部分细说。
1.3 关键字段的语义需要先对齐
mermaid 不能画,但字段理解这事用表格最清楚。我把判断大导入时最常用的字段,按重要程度排了个序:
| 字段 | 字段含义 | 大导入场景下的解读 |
|---|---|---|
| pid | 后端进程 ID | 多个导入任务可以靠 pid 区分,kill 的时候也靠它 |
| state | 当前会话状态 | active 表示正在执行语句;idle 表示空闲;idle in transaction 表示事务开着但没执行语句 |
| query_start | 当前这条语句开始的时间 | 配合 now() 能算出跑了多久,判断超时阈值 |
| xact_start | 当前事务开始的时间 | 导入通常是一个大事务,xact_start 比 query_start 更早代表长时间占用事务 |
| wait_event_type / wait_event | 等待事件 | Lock 代表锁等待,IO 代表在读写磁盘,Client 代表在等客户端 |
| query | 当前执行的完整 SQL | 大导入时通常显示 COPY、INSERT 等 |
| backend_type | 后端类型 | 区分客户端会话、autovacuum、后台 worker |
| client_addr | 客户端地址 | 判断是不是来自你预期的主机在导入 |
| application_name | 应用名称 | 导入工具(pg_restore、psql、pgAdmin)通常会写入自己的应用名 |
这些字段单独拎出来每一个都好懂,但组合起来才是判断大导入是否"真正在干活"的关键。光看 state 不够,光看 query 也不够,三个维度(状态、时间、等待事件)一起看才靠谱。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 判断"正在执行"的关键:状态机与时间戳分析
2.1 active 不等于一定有进展
这是我见过最多的误判。很多同学查了 pg_stat_activity,看到 state=active 就觉得"导入正在跑,没问题",转身就走了。实际上 active 只代表"这条语句处于执行中"——它可能在跑 CPU,可能在等磁盘 IO,也可能什么事情都没做,纯粹因为持有锁而阻塞了别的会话。
在 PostgreSQL 进程模型里,state=active 意味着后端进程"认为自己正在执行语句"。这个"认为"和真正有数据写入是两码事。比如你的导入语句在等一把被别的事务持有的锁时,它在 pg_stat_activity 里通常是 active + wait_event_type=Lock,但这一秒它没有任何实质性进展。再比如网络抖动导致客户端暂停接收数据,COPY 语句在等客户端把剩余数据发过来,这时的状态可能是 active + wait_event=ClientRead,但它实际上是在干等。
所以判断"是否正在执行"的正确姿势是三重校验:
- state 是否为 active(或者出现了预期的空闲事务状态)。
- query_start 之后,进程是否持续占用 CPU/IO 或持续产生锁等待以外的进度。
- 如果用的是 COPY 或 CREATE INDEX,结合 PG 12+ 的进度视图确认记录数/块数在增长。
只看第一条,一定会踩坑。
2.2 时间戳组合拳计算执行时长
光说"active"不够,你得知道这个 active 已持续多久。pg_stat_activity 给了一组时间戳,是判断问题最锋利的工具:
sql复制SELECT
pid,
state,
round(EXTRACT(EPOCH FROM (now() - query_start))) AS query_seconds,
round(EXTRACT(EPOCH FROM (now() - xact_start))) AS xact_seconds,
round(EXTRACT(EPOCH FROM (now() - state_change))) AS state_seconds,
wait_event_type,
wait_event
FROM pg_stat_activity
WHERE pid = 12345; -- 替换成你的导入进程 PID
这三个时间戳各有各的用途:
- query_seconds 告诉你当前这条语句跑了多久。如果一条 COPY 已经跑了 6 个小时,而你知道这批数据正常只需要 40 分钟,那大概率出问题了。
- xact_seconds 告诉你事务开了多久。大导入通常是单条 COPY 或者 pg_restore 里的一个事务块。如果事务时间远大于语句时间,说明事务里有过多次语句执行,或者事务开着但当前处于 idle in transaction 状态。
- state_seconds 告诉你当前状态持续了多久。这个字段非常有用——如果 state=active 但 state_change 已经是 30 分钟前,而等待事件又是 ClientRead,说明它等客户端等了半小时,这不是"正在执行",这是"卡住了"。
在实际排查时,我习惯设定一个简单的规则:导入语句执行时间超过平时正常时长的 3 倍,或者 state_change 长时间未变化同时等待事件属于 Client/Extension 类型,就应当进入"疑似异常"状态,需要进一步通过进度视图确认。
2.3 三种典型"假运行"状态识别
结合自己几年和 PostgreSQL 打交道的经验,我把大导入时最常见的"看起来在跑、其实没跑"的场景归成三类,你在 pg_stat_activity 里看到的样子也写出来了:
第一类,客户端消失但进程还在。你用 Navicat 或者 DBeaver 导大数据,导入到一半客户端崩了或者网络断了。服务端进程未必立刻收到断开通知,尤其 TCP 连接还在保持时,PostgreSQL 会持续等待客户端数据。pg_stat_activity 里表现为 state=active,wait_event=ClientRead,query 显示 COPY FROM stdin,query_start 一直不更新。很多人看到 active 就放心走了,其实这数据已经永远导不进去了。
第二类,锁等待造成的假 active。你的导入要往目标表写数据,结果另一个会话刚好锁住了这张表,或者目标表上有未提交的长事务。导入进程在 pg_stat_activity 里 state=active,wait_event_type=Lock,但它不是在干活,是在排队等锁。等多久取决于持有锁的事务何时释放,可能是一分钟,也可能是一整天。
第三类,idle in transaction 的长事务。对应场景是客户端程序先 begin,然后一条一条执行 INSERT,中间停顿很久。pg_stat_activity 里状态是 idle in transaction,query 显示最后一条 INSERT 的语句,xact_start 很早。这种情况比较隐蔽,因为 state 就不是 active,但很多人不懂 idle in transaction 是什么,看到有一个很老的会话就以为是导入还在跑。
这三类"假运行"在生产环境里轮番出现过,每次都能让团队虚惊一场甚至造成误判延迟处理。所以我在团队里反复强调:不要只看 state,要把 state、wait_event、时间戳、进度四样一起看。
3. 进度级判断:结合进度视图与系统层指标
3.1 pg_stat_progress_copy 看 COPY 进度
pg_stat_activity 能回答"有没有在执行",但要回答"执行到哪一步了",就得请出 PG 12 开始引入的进度视图全家桶。其中最常用到的是 pg_stat_progress_copy,专治 COPY 大导入。
这个视图在 PG 14 里字段比较完善,它能报告 COPY FROM/COPY TO 每个阶段的进度信息。查询方式很直接:
sql复制SELECT
pid,
datname,
relid::regclass AS target_table,
phase,
tuples_done,
tuples_total,
round(100.0 * tuples_done / NULLIF(tuples_total, 0), 2) AS pct_done,
bytes_processed,
now() - query_start AS runtime
FROM pg_stat_progress_copy
JOIN pg_stat_activity USING (pid)
WHERE relid IS NOT NULL;
如果在 COPY 数据文件特别大时,tuples_total 可能是 0,因为 PostgreSQL 不一定知道总量。这时候判断进度就得靠 tuples_done 的"增长速度",等价于看每秒处理多少行。我在实际环境里处理过一个 5000 万行的 CSV 导入,tuples_total 是 0,当时就是靠间隔几秒查询两次 tuples_done,算出每秒约 12 万行的处理速度,估出总耗时的。
用进度视图判断的核心逻辑很简单:连续两次查询,间隔 10~20 秒,如果 tuples_done 或者 bytes_processed 在增长,那么导入确实在推进;如果两次查出来数值完全一样,说明卡住了,不用再干等。
3.2 pg_stat_progress_create_index 看索引构建
大导入的另一半时间经常花在索引构建上。尤其你先把数据 COPY 进去,再一次性 CREATE INDEX,索引构建的进度要看 pg_stat_progress_create_index:
sql复制SELECT
p.pid,
p.relid::regclass AS table_name,
p.index_relid::regclass AS index_name,
p.phase,
p.blocks_done,
p.blocks_total,
p.tuples_done,
p.tuples_total,
round(100.0 * p.blocks_done / NULLIF(p.blocks_total, 0), 2) AS pct_blocks_done
FROM pg_stat_progress_create_index p
JOIN pg_stat_activity a USING (pid)
WHERE a.state = 'active';
phase 字段会经历 building index、scanning table、sorting tuples 等阶段。最有用的是 blocks_done/blocks_total 的比值,能直接给你一个百分比。如果两次查询 blocks_done 不增长,而 pg_stat_activity 里等待事件又不是 CPU/IO 相关,那大概率是遇到了锁冲突,或者并行 workers 之间在相互等待。
3.3 看看系统层才能交叉验证
数据库视图是 PostgreSQL 内部的视角,但它有时候会说谎——不是说数据造假,而是说"在等磁盘 IO"这种信息你自己感知不到。所以我做判断大导入是否"真的在干活"时,从来不会只看 pg_stat_activity,一定会交叉查看操作系统层的证据:
iostat -x 2:看 %util 和 rkB/s、wkB/s。如果导入在正常写数据,磁盘 write 应该有持续流量。如果 pg_stat_activity 显示 active,但磁盘长时间几乎零写入,说明它大概率在等锁或者等客户端。top/htop:看对应 postgres 进程的 CPU 使用率。COPY 大量文本数据需要 CPU 做类型转换、tuple 构造,CPU 不会太低。如果是 CREATE INDEX 默认并行,能看到多个 postgres worker 吃满 CPU。ss -tnp:查看 PostgreSQL 端口(通常 5432)上对应 pid 的 TCP 连接状态,确认客户端是不是还连着、有没有数据在传。
判断"还在执行"这件事,不能只靠数据库单方面陈述,二重验证甚至三重验证才是稳妥做法。这也是为什么我在每次临时的值班群里都会反复和同事讲:pg_stat_activity 是第一现场,但别只信第一现场。
3.4 需要注意的权限问题
这里顺带提醒一下:pg_stat_progress_copy 和 pg_stat_progress_create_index 不是所有用户都能看到全量数据的。普通用户只能看到自己发起的导入的进度信息,要看别人的,需要超级用户或者具备 pg_read_all_stats 角色权限。生产环境里如果团队共用账号,建议给排查人员提前授予:
sql复制GRANT pg_read_all_stats TO your_monitor_role;
没有这个权限,你去查进度视图可能只得到一个空结果,还会误以为"没有导入在跑"。这个坑我在交接文档里强调过,这里也提一下。
4. 实操:一套可直接抄作业的监控方案
4.1 单次快照查询
有了前面这些掌握,一个大导入启动后,你不在现场也能判断它是否在跑。我的标准流程是先在客户端起一个轮询脚本,第一步就是打一个带进度信息的快照。
下面的 SQL 把 pg_stat_activity 和几个进度视图 join 成一张宽表,一次性把状态、等待、时间、进度全展示出来。这是我处理大数据量导入时的"第一板斧":
sql复制SELECT
a.pid,
a.state,
a.wait_event_type,
a.wait_event,
round(EXTRACT(EPOCH FROM (now() - a.query_start)))::text || 's' AS query_age,
left(regexp_replace(a.query, '\s+', ' ', 'g'), 100) AS query,
c.phase AS copy_phase,
c.tuples_done AS copy_tuples_done,
c.bytes_processed AS copy_bytes
FROM pg_stat_activity a
LEFT JOIN pg_stat_progress_copy c ON c.pid = a.pid
LEFT JOIN pg_stat_progress_create_index ci ON ci.pid = a.pid
WHERE a.datname = current_database()
AND a.backend_type = 'client backend'
AND a.state <> 'idle'
ORDER BY a.query_start;
一次跑出来的结果里,你会同时看到进程状态和 COPY 进度。如果查出来的行里 copy_tuples_done 有值,说明 COPY 阶段正在推进;如果 copy_tuples_done 是空但有 ci 的字段,那就是建索引阶段。
4.2 连续监控轮询脚本
单次快照只能看当下,要做"是否正在执行"的动态判断,需要连续采样。我习惯写一个简单的 bash 循环,每 15 秒查询一次关键指标,输出成一行话,方便盯屏:
bash复制#!/bin/bash
# 监控 PostgreSQL 大导入进程的轮询脚本
# 用法: ./monitor_import.sh <pid>
PID=${1:-}
INTERVAL=15
while true; do
psql -h /var/run/postgresql -U postgres -d mydb -X -A -t -F ' | ' <<SQL
SELECT
to_char(now(), 'HH24:MI:SS'),
(SELECT state FROM pg_stat_activity WHERE pid = $PID),
(SELECT coalesce(wait_event_type, '') FROM pg_stat_activity WHERE pid = $PID),
(SELECT coalesce(wait_event, '') FROM pg_stat_activity WHERE pid = $PID),
(SELECT round(EXTRACT(EPOCH FROM (now() - query_start))) FROM pg_stat_activity WHERE pid = $PID),
(SELECT tuples_done FROM pg_stat_progress_copy WHERE pid = $PID),
(SELECT phase FROM pg_stat_progress_create_index WHERE pid = $PID)
SQL
sleep $INTERVAL
done
这段脚本每次输出一行,包含当前时间、状态、等待事件、已执行秒数、COPY 已处理行数、索引构建阶段。通过观察连续多行的变化,你就能判断导入是否在推进:如果已执行秒数在涨,但 COPY 行数一直不变,要么进了 CREATE INDEX 阶段,要么就出问题了,需要回头查等待事件。
你也可以把输出重定向到文件,回头分析时间线:
bash复制./monitor_import.sh 12345 | tee import_monitor.log
日志里每一行都是一次快照。事后复盘大导入故障时,拿着这个日志对照数据库错误日志,基本能把问题定位到具体时间点。
4.3 后处理与留痕
监控的目的是发现问题后能处理,所以每一次大导入,我建议至少留下两种痕迹:
一是导入脚本本身的日志,记录开始时间、结束时间、影响行数。这部分的来源可以是命令行工具的输出,比如 psql 的 \timing 或者 pg_restore 的 verbose 输出。
二是上面的轮询监控日志。别小看这个文本文件,它记录了整个导入过程中 PostgreSQL 侧的状态变化。有一次排查"导入中断但没报错"的问题,就是靠这个日志发现进程的 state 从 active 变成了 idle,然后立刻转为 idle in transaction,再无变化——很明显是客户端断开了但服务端会话没有退出。没有这个时间线,单靠事后翻数据库日志,很难还原现场。
5. 大导入监控的常见误判与避坑
5.1 idle in transaction 是最大的隐形坑
如果你监控一个大导入时看到 state=idle in transaction,要立刻产生警觉,这既不是正常的"正在执行",也不等于已经结束。它表示事务还开着,只是当前没有在执行任何 SQL 语句。
在大导入中最常见的产生方式是:客户端程序分批 INSERT,先 begin,然后循环执行,但中途停顿了几分钟。pg_stat_activity 里看到的状态就是 idle in transaction,query 显示的是事务里最后一条 INSERT 语句。此时事务持有的锁一个都不会释放,所有对这个表的写操作和 DDL 都会被卡住。
判断这种状态是否"还在继续"很困难。我的经验是看 xact_start 和当前时间差,配合检查客户端进程是否存活。如果事务已持续了远超预期时间,且客户端进程也没有在传输数据,基本可以确定它已经"死掉"了。此时需要人工确认后,通过 pg_terminate_backend(pid) 把会话终结掉。
5.2 客户端断连但服务端进程还在
还有一种高频误判:客户端已经崩溃,服务端进程看起来还在运行。在使用 psql 或 pg_restore 做导入时,如果最终客户端强制关闭,TCP 连接如果设置了 keepalive,可能很久才能被 PostgreSQL 感知到。在这期间,pg_stat_activity 里 session 一直存在,state 可能是 active,query 显示 COPY FROM stdin,wait_event 是 ClientRead。
怎么确认它已经"死"了?最直接的办法是看 pg_stat_activity 的 client_addr 和 client_port,然后在客户端机器上确认对应连接是否还存在。如果客户端机器已经重启,但这个数据库连接还挂着,说明是一个死连接,需要清理。
注意:不要一看到 ClientRead 的 COPY 就直接杀掉进程。如果是你正在导入数据,客户端和服务端之间数据量较大,出现短暂等待是正常的。要先看持续时间,如果超过 5 分钟没有再变化,才需要考虑它已经失效。
5.3 锁等待与等待事件的辨识
锁等待是让 PostgreSQL 大导入看起来像卡死的经典原因。一条正常只需要 20 分钟的 COPY,可能因为另一条未提交事务拿着表锁,直接等上 10 个小时。pg_stat_activity 里能清晰看到 wait_event_type=Lock、wait_event=relation。
要确认当前导入进程是被谁阻塞,可以用 pg_locks 来关联:
sql复制SELECT
blocked.pid AS blocked_pid,
blocking.pid AS blocking_pid,
blocked_query.query AS blocked_query,
blocking_query.query AS blocking_query
FROM pg_locks blocked
JOIN pg_locks blocking
ON blocking.locktype = blocked.locktype
AND blocking.database IS NOT DISTINCT FROM blocked.database
AND blocking.relation IS NOT DISTINCT FROM blocked.relation
AND blocking.pid <> blocked.pid
JOIN pg_stat_activity blocked_query ON blocked_query.pid = blocked.pid
JOIN pg_stat_activity blocking_query ON blocking_query.pid = blocking.pid
WHERE NOT blocked.granted;
执行完这个查询,你会看到哪条 SQL 在被等,哪条 SQL 拿着锁不放。大部分情况下,解开阻塞源(提交或回滚那个事务)就能让导入继续跑。
有趣的是,锁等待状态下 pg_stat_activity 的 state 依然是 active。这里再强调一遍:active 和"有效推进"不能画等号。这也是我在每次培训里都会要求团队记住的口诀。
5.4 进度视图查不到数据时的排查思路
还有一类让人摸不着头脑的情况:明明导入在跑,pg_stat_progress_copy 却查不到对应进程。可能的原因有三个:
一是 PostgreSQL 版本太老。pg_stat_progress_copy 在 PG 12 之前是没有的,碰上 PG 11 及更早版本的数据库,只能看 pg_stat_activity。二是你用的不是 COPY,而是普通的 INSERT 批量插入。进度视图只覆盖 COPY、CREATE INDEX 等特定命令。第三种是权限限制,前面提到过需要 pg_read_all_stats 权限。
针对这种情况,我的替代方案是:用 pg_stat_activity 里 query_start 和 state_change 做前后对比,配合系统层磁盘 IO 指标判断进度。虽然不像进度视图那么精确,但也能达到监控目的。
5.5 大导入后的事务空转与 vacuum 误判
大导入完成后,你可能会在 pg_stat_activity 里看到 autovacuum worker 在跑,state 是 active,query 内容类似 autovacuum: VACUUM public.my_table。有同学会误以为是大导入还在执行,其实导入早已结束,这只是导入产生的垃圾数据触发的自动清理。
分辨方法看 backend_type 字段。前面提到的查询里我们一直用 backend_type = 'client backend' 过滤,就是要把这些后台进程排除掉。如果你忘了加这个条件,就会在结果里看到大量 autovacuum 工作进程,干扰判断。反过来,如果 autovacuum 长期阻塞了你的导入进程,你也需要从 wait_event 里识别出来,这个时候就要考虑调低 autovacuum 的负载参数,或者临时调整相关表的目标阈值。
6. 一些个人习惯和补充建议
我自己处理大导入问题时,从来不把 pg_stat_activity 作为唯一的判断依据,而是把它、进度视图、系统层指标组合起来使用。
有一个简单的流程通常能快速定位问题:先看 pg_stat_activity 里的 pid、state、wait_event,判断会话是否存活;再看 query_start 和 state_change,判断当前语句持续时间;再看进度视图或者系统 IO,判断是否真的有数据在流动。三步走完,大导入的问题基本能定位到八成。
另外,如果你把 PostgreSQL 部署在 Docker 里(这也是挺多人困惑的地方),pg_stat_activity 的查询方式完全一样,注意宿主机上执行 psql 命令需要用 docker exec 进容器,或者暴露端口后从宿主机连接。容器环境下用 docker exec -it <container> psql -U postgres 进入交互环境,查出来的 pg_stat_activity 跟物理机部署没有任何差别。
关于 dashboard 可视化,我在团队里搭过一个简单的 Grafana 看板,数据源是 PostgreSQL 的 pg_stat_activity 和 pg_stat_progress_copy,每 5 秒采集一次,能画出导入进度的时间曲线。不过工具只是辅助,真正踩坑总结下来,还是那几个字段的理解最重要。如果只想手工排查,按照本文第 4 部分的脚本和查询,完全能应对绝大多数场景。
