前阵子一个朋友在群里问:跑了一个 COPY 导入,两亿行的表,已经二十分钟没动静了,想确认它到底是在认真干活还是卡死了,但又不敢随便动。这个场景我太熟了。做数据库的人,最怕的就是大导入在后台无声无息地跑着,你以为它结束了,其实它在等锁;你以为它卡住了,其实它在闷头写盘。判断“大导入是否正在执行”,是 PostgreSQL 日常运维里非常高频的需求,而解决这个问题的入口,就是系统视图 pg_stat_activity。
pg_stat_activity 不是只有 DBA 才能用的东西,只要你知道几个关键字段,开发、测试、运维都能在几秒内判断出当前正在执行的数据库任务。这篇文章我会从字段解读讲起,配合实际场景和一堆可直接复制的 SQL,把“如何判断大导入是否正在执行”这件事讲透。
1. 为什么需要判断大导入是否正在执行
1.1 大导入的真实战场:不是只有 COPY
先说明一下判断对象。大导入在实际工程里通常分几类:第一类是 COPY FROM 或者 \copy,这是 PostgreSQL 最高效的文本/二进制导入方式,常用于数据迁移和初始数据装载;第二类是 pg_restore,恢复一个 dmp 或归档备份,背后其实是 COPY、CREATE INDEX、ALTER TABLE 等一系列语句的组合;第三类是 psql -f 跑大 SQL 脚本,一个文件里几万条 INSERT,或者一组调整表结构的 DDL;第四类是应用层批量 INSERT,Java、Python、Golang 程序用 JDBC 或 ORM 批量写数据,在 pg_stat_activity 里看到的是一条 INSERT 语句,可能是带几百行 VALUES 的超长 SQL。
这些操作只要数据量大,跑起来少则几分钟、多则几小时。过程中遇到的实际问题集中在三类:第一,它到底在不在执行?很多导入是静默的,没有进度条也没有日志,尤其在远程恢复、后台任务、无人值守调度的场景里,你只能通过数据库内部状态来判断。第二,它执行到哪一步了?没人想只是盯着“还在跑”三个字,能知道“已经导了 30%”和“大概还剩多少”是完全不同的体验。第三,它是不是挡住了别人?大导入通常伴随 IO 压力、锁等待,不及时识别就会把在线业务一起拖垮。
我之前排查一个生产事故时,就是因为没人意识到后台还有一个凌晨跑到现在都没结束的 pg_restore,导致白天业务删表重建时一直等锁,前端接口全部超时。后来定位到问题后,第一件事就是去看 pg_stat_activity。这个视图在关键时刻能省下大量排查时间。
1.2 为什么 pg_stat_activity 是切入点
pg_stat_activity 是 PostgreSQL 内置的系统视图,它实时记录了每个后端进程的当前状态。你可以把它理解成给数据库装了一个“监控摄像头”:谁的连接、在跑什么 SQL、跑了多久、在等什么资源,全都能查到。
关键点在于,PostgreSQL 的执行模型是“一个后端进程一次执行一个查询语句”。所以只要导入操作正在执行,pg_stat_activity 里必然能看到一条状态为 active 的会话记录,并且 query 字段会显示当前正在执行的语句。这就是判断大导入是否在跑的最直接方法。
但直接看还不够。“正在执行”和“正在干活”是两个概念:一个会话显示 active,可能它正在等待锁;一个会话显示 idle in transaction,可能它的事务已经执行完 COPY 但迟迟不提交,看起来“没在跑”,数据却确实占着空间。所以判断大导入,不能只看一种状态,要把 state、等待事件、时间字段,甚至进度视图结合起来看。
1.3 适合谁来读
如果你正在做数据迁移、恢复测试、初次把千万级数据装进 PostgreSQL,或者只是负责维护一个偶尔被大数据写入的业务库,这篇文章都适合你。我会默认你没看过多少官方文档,但至少能打开 psql 连上库执行 SQL。信息密度不低,但我会把每个概念都拆开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 读懂这几列,判断才能在几分钟内完成
2.1 state 四种状态,判断大导入的分水岭
pg_stat_activity 里最核心的一列是 state,它告诉每个连接当前处于什么阶段。对应到大导入场景,我一般这么理解:
| state | 含义 | 对大导入意味着什么 |
|---|---|---|
| active | 正在执行查询 | 导入语句正在执行的直接证据 |
| idle | 空闲,等待客户端发指令 | 导入已经结束,或者尚未开始,需结合其他字段 |
| idle in transaction | 事务内空闲,已执行完当前语句但未提交/回滚 | 大导入可能已完成数据装载,但事务还没提交,最容易被漏判 |
| idle in transaction (aborted) | 事务处于失败状态 | 导入可能出错,但事务没被关闭,需要处理 |
判断大导入是否正在执行,最常见的 SQL 是过滤 state='active',再看 query 内容是不是 COPY、INSERT、CREATE INDEX 之类。但有个大坑:idle in transaction 也是“导入流程还没结束”的状态。比如你用 psql 启动一个事务,执行了一个耗时 30 分钟的 COPY,执行完后 psql 停在事务里等你输入 commit,此时 state 就是 idle in transaction,xact_start 显示事务开始时间。如果只看 state='active',就会漏掉这种情况。
所以我的判断逻辑里,state='active' 表示“正在执行 SQL 语句”,state='idle in transaction' 表示“事务还开着但当前没有语句在跑”。这两种都算“大导入流程还没结束”,只是阶段不同。如果只关心“SQL 是否正在执行”,只看 active 就够了;但如果你关心“导入这个动作是否真正完成”,必须把 idle in transaction 也纳入考虑。
2.2 wait_event_type 和 wait_event:它是真的在跑,还是在等?
只看到 active 还不够,还要看进程在等待什么。PostgreSQL 的进程在等待某个事件时,wait_event_type 和 wait_event 会给出线索。这里列几个判断大导入时最常遇到的等待事件:
| wait_event_type | wait_event | 说明 |
|---|---|---|
| IO | DataFileWrite | 正在写数据文件,大导入刷盘的典型信号 |
| IO | DataFileExtend | 正在扩展数据文件,说明表文件在增长 |
| IO | WALWrite / WALWriteLock | 正在写 WAL,大量写入时常见 |
| Lock | RelationLock 等 | 在等其他会话释放锁,常见于锁冲突场景 |
| Activity | ClientRead | 正在等客户端发送下一条指令,通常出现在 idle 或事务间 |
需要注意,等待事件本身不能单独判断是不是大导入,因为普通查询也可能出现 DataFileWrite。但它能帮你排除“卡死”的误判:如果 wait_event 一直落在 IO 类,说明进程是真的在读写磁盘、在干活;如果长时间停在 Lock 类上不动,那大概率是在等锁,而不是在闷头导入。
实际排查时,一旦看到 wait_event_type='Lock',我会马上执行 SELECT pg_blocking_pids(pid),看是谁把锁捏在手里。很多时候,大导入变成“假死”状态,其实是在等一个持有锁的事务,而这个事务本身已经 idle in transaction 很久了。
2.3 query、query_start、xact_start、backend_start:把时间线拉出来
这几个时间字段是判断导入“已经跑了多久”的关键:
backend_start:进程创建时间。如果连接是新的,说明导入刚启动;如果连接已经存在很久,说明是复用连接之后才发的导入命令。xact_start:当前事务开始时间。如果一个 COPY 在事务里执行,xact_start 几乎等于导入的开始时间;如果导入用多条语句分批跑,事务开始时间可能比当前语句更早。query_start:当前正在执行的 SQL 开始时间。这个最直接,表示当前这条语句已经跑了多久。state_change:状态变更时间。平时用得少,但在判断 idle in transaction 到底空闲了多久时很有用。
判断大导入是否正在执行,最实操的办法是比较 now() - query_start 和导入的预期耗时。比如你可以写:
sql复制SELECT pid, usename, datname, state,
now() - backend_start AS backend_age,
now() - xact_start AS xact_age,
now() - query_start AS query_age,
wait_event_type, wait_event,
query
FROM pg_stat_activity
WHERE backend_type = 'client backend';
这样你能一眼看到每个客户端连接上,当前语句跑了多久、当前事务开了多久。如果一条 COPY 的 query_age 已经 1 小时,而预期只要 10 分钟,那就要警惕是不是有别的因素拖慢了。
2.4 一个可以直接跑的“识别大导入”查询
把上面几个字段组合起来,给大家一个可以直接抄的版本。这个查询会列出所有客户端后端中,状态为 active 或 idle in transaction,且 query 疑似导入操作的会话:
sql复制SELECT pid,
usename,
datname,
state,
wait_event_type,
wait_event,
now() - query_start AS query_age,
now() - xact_start AS xact_age,
left(query, 200) AS query_preview
FROM pg_stat_activity
WHERE backend_type = 'client backend'
AND state IN ('active', 'idle in transaction')
AND (
query ILIKE 'copy %'
OR query ILIKE '%copy %from%'
OR query ILIKE 'insert%'
OR query ILIKE 'create index%'
OR query ILIKE 'alter table%'
OR query ILIKE 'truncate%'
);
注意 query 字段有长度上限,默认 1024 字节,所以 left(query, 200) 只是预览。过滤条件里我同时放入了 active 和 idle in transaction,原因上面讲过了:导入可能处于“语句执行完但事务未提交”的半程状态。
3. 深入 pg_stat_progress_copy:直接看到导入进度
3.1 PG14+ 的进度视图怎么用
如果你用的是 PostgreSQL 14 及以上版本,判断“COPY 大导入是否正在执行”还有更直观的办法:pg_stat_progress_copy。这个视图是 PostgreSQL 专门为 COPY 命令提供的进度监控视图,在 COPY 执行期间,视图里会有对应 session 的一行记录,实时统计已经处理了多少字节、多少行。
在 PostgreSQL 14 里,pg_stat_progress_copy 的字段包括 pid、datid、datname、relid、command(COPY FROM / COPY TO)、type(导入还是导出)、bytes_processed、bytes_total、tuples_processed。PostgreSQL 16 又加了 phase 字段,用来表示 COPY 当前在哪个阶段,比如 FILE_FROM 表示从文件导入、STDIN_FROM 表示从客户端的 stdin 导入。PostgreSQL 17 还进一步增加了每个文件的进度统计。
判断是否正在执行大导入,最实用的查询是这样的:
sql复制SELECT p.pid,
p.datname,
p.command,
p.type,
p.bytes_total / 1024 / 1024 AS total_mb,
p.bytes_processed / 1024 / 1024 AS processed_mb,
round(
100.0 * p.bytes_processed / NULLIF(p.bytes_total, 0),
2
) AS pct_done,
p.tuples_processed,
a.query_start,
now() - a.query_start AS query_age,
a.wait_event_type,
a.wait_event,
a.state
FROM pg_stat_progress_copy p
JOIN pg_stat_activity a ON a.pid = p.pid;
这个查询把进度视图和 pg_stat_activity 关联起来,既能看进展,又能看等待事件和耗时。执行之后,你就能看到类似输出:一个 COPY FROM 进程,总量 2048MB,已经处理了 1024MB,进度 50%,跑了 3 分 25 秒。这种信息量,直接看 pg_stat_activity 是得不到的。
3.2 bytes_total 为 0 的坑
说一个踩过的坑:用上面这个 SQL 判断进度时,如果 bytes_total 是 0,百分比兜底逻辑会显示异常,这并不代表导入完成。bytes_total 为 0 的原因通常是 COPY 数据来自 stdin,客户端通过协议流式发送数据,服务端在开始时并不知道总长度。
这种情况下,判断“是否正在执行”就不能依赖百分比,而要依赖两点:一是 pg_stat_progress_copy 里有没有这行记录;二是 pg_stat_activity 中的 state 和等待事件。只要 pg_stat_progress_copy 里还有记录,就说明 COPY 还在执行;一旦 COPY 结束,这行记录会立刻消失。所以我把“进度视图是否有记录”当作最可靠的“是否正在导入”的判断标准。
在很多真实场景里,比如用 pg_restore 恢复数据,或者用 \copy 从客户端文件导入,服务端看到的都是 COPY FROM STDIN,bytes_total 自然为 0。这时不用慌,看 tuples_processed 是否在增长,比看百分比更有意义。你可以间隔几秒跑两次查询,对比 tuples_processed,只要行数在涨,就说明导入在推进。
3.3 旧版本 PostgreSQL 怎么退而求其次
如果你的 PostgreSQL 是 13 及以下版本,或者无法访问 pg_stat_progress_copy,就只能靠 pg_stat_activity 加辅助手段来判断。我的建议是同时做三件事:
第一,看等待事件。大导入在写大量数据时,wait_event_type 通常会落在 IO 上,比如 DataFileWrite、DataFileExtend、WALWrite。如果一个会话长时间处于 active 且 wait_event 在 IO 类之间跳动,说明它在干活。
第二,看目标表大小是否持续增长。执行两次带间隔的查询,如果表大小在增加,说明导入正在写入数据:
sql复制SELECT pg_size_pretty(pg_total_relation_size('demo_import'));
第三,看 WAL 目录或数据目录的大小变化。这个需要 ssh 到服务器上看数据目录,比如 pg_wal 目录下文件数量是否快速增加。这一步对普通用户比较重,但在生产排障时非常有效,因为大量导入必然产生 WAL。
这套组合方法没有 pg_stat_progress_copy 那么优雅,但在旧版本上算是比较靠谱的替代方案。我从前维护 PostgreSQL 11 集群时,就是靠“等待事件 + 表大小变化 + WAL 增长”三件套来判断大导入是否真在跑。
4. 实操演练:模拟一次大导入,实时观察
4.1 准备测试环境
光讲理论不够,我来实际模拟一次。假设你有一台本机运行的 PostgreSQL,版本至少 14,我这里用 PostgreSQL 16 演示。先建一张表,生成一个 1000 万行的 CSV 文件,然后用 COPY 导入,边导边用 pg_stat_activity 观察。
首先建表和准备数据文件:
sql复制CREATE TABLE demo_import (
id bigint,
payload text,
created_at timestamptz DEFAULT now()
);
生成 1000 万行测试数据可以用 generate_series 快速完成。这里我直接在 psql 客户端用 \copy 把查询结果导出成文件:
sql复制\copy (SELECT g AS id, md5(g::text) AS payload FROM generate_series(1, 10000000) AS g) TO '/tmp/demo_import.csv' WITH CSV
这个命令会执行一小会儿,生成大约几百 MB 的 CSV 文件。文件越大,导入阶段观察窗口越长,所以如果你想要更长的观察时间,可以把行数调到 5000 万甚至 1 亿。
接着在 psql 里执行真正的服务端 COPY:
sql复制COPY demo_import (id, payload) FROM '/tmp/demo_import.csv' WITH CSV;
注意,这里我用的是服务端 COPY,不是 \copy。两者的区别在于:服务端 COPY 是 PostgreSQL 服务端直接读取服务器上的文件,而 \copy 是 psql 客户端读本地文件再通过协议传过去。大文件导入用服务端 COPY 通常更快,因为少了客户端与服务器之间的协议传输限制,但前提是文件必须在数据库服务器上。
4.2 打开第二个会话观察 pg_stat_activity
在 COPY 执行期间,另开一个 psql 窗口,执行前面给出的识别大导入查询:
sql复制SELECT pid, usename, datname, state,
wait_event_type, wait_event,
now() - query_start AS query_age,
query
FROM pg_stat_activity
WHERE backend_type = 'client backend'
AND state = 'active';
你会看到其中一行,query 是 COPY demo_import,state 是 active。wait_event_type 大概率是 IO,wait_event 可能是 DataFileWrite 或 DataFileExtend。query_age 会随着时间增长,说明这条导入语句正在执行。
如果 PostgreSQL 版本是 14+,再执行进度查询:
sql复制SELECT p.command, p.type,
p.bytes_total / 1024 / 1024 AS total_mb,
p.bytes_processed / 1024 / 1024 AS processed_mb,
round(100.0 * p.bytes_processed / NULLIF(p.bytes_total, 0), 2) AS pct,
p.tuples_processed,
now() - a.query_start AS query_age
FROM pg_stat_progress_copy p
JOIN pg_stat_activity a ON a.pid = p.pid;
因为 COPY 是从固定文件读取,bytes_total 不会为 0,所以你能看到进度百分比在稳步上升。我实测导入 1000 万行的 CSV,几秒内就能看到 10%、20%、50% 的变化。tuples_processed 也在同步增长,到 1000 万行时,进度接近 100%。
4.3 观察锁等待场景
我还想演示一种更隐蔽的情况。假设在 COPY 执行前,另外一个会话已经打开了一个未提交事务,对 demo_import 表执行过 INSERT,拿到了行锁;然后 COPY 需要在该表上写入,某些情况下会产生锁等待。但更常见的锁冲突场景是:导入期间有人对同一张表执行 ALTER TABLE,或者反过来,导入在等一个长时间未提交事务产生的锁。
制造锁等待可以直接在一个会话里执行:
sql复制BEGIN;
INSERT INTO demo_import(id, payload) VALUES (-1, 'lock holder');
-- 先不 COMMIT,把事务开着
然后在另一个会话执行:
sql复制TRUNCATE demo_import;
TRUNCATE 需要更强的表级锁,会被前面
