PostgreSQL 的常规 VACUUM 会回收死元组占用的空间,让后续写入复用这些数据页,却通常不会缩小表文件。PostgreSQL 19 Beta 2 引入了内核内置的在线重写能力,并提供 REPACK (CONCURRENTLY),让治理严重表膨胀不再完全依赖长时间阻塞式重写或外部扩展。
VACUUM 回收了什么,又没有回收什么
MVCC 更新一行时,通常会生成新版本,并把旧版本留在表中。旧版本不再被任何事务需要后,就成为死元组。常规 VACUUM 主要完成几件事:
- 标记死元组空间,使其可以被同一张表后续复用;
- 维护可见性映射等内部结构;
- 配合冻结机制,控制事务 ID 老化风险;
- 通常不把散落在文件内部的空闲页归还给操作系统。
因此,下面两句话可以同时成立:
- 表已经完成
VACUUM,内部空间可以复用; pg_relation_size()显示的物理文件仍然很大。
VACUUM FULL 可以通过重写表来压缩物理文件,但它的锁影响通常不适合繁忙业务。PostgreSQL 19 Beta 2 的重要变化,是把在线重写能力带入内核,并用并发 REPACK 降低重写期间对读写流量的影响。
先用实验看清普通 VACUUM 的边界
下面的脚本可以在测试数据库中直接运行。它创建一张表、删除大部分数据,然后比较常规 VACUUM 前后的表文件大小。测试会写入数十万行,请不要在生产数据库执行。
createdb vacuum_lab
psql -v ON_ERROR_STOP=1 vacuum_lab <<'SQL'
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
payload text NOT NULL
);
INSERT INTO events (payload)
SELECT repeat(md5(g::text), 8)
FROM generate_series(1, 300000) AS g;
SELECT pg_size_pretty(pg_relation_size('events')) AS size_before_delete;
DELETE FROM events WHERE id <= 270000;
VACUUM (ANALYZE) events;
SELECT
pg_size_pretty(pg_relation_size('events')) AS table_size,
n_live_tup,
n_dead_tup
FROM pg_stat_user_tables
WHERE relname = 'events';
SQL
这里不应只盯着 n_dead_tup。统计值可能下降,但 table_size 往往不会按删除比例同步缩小,因为 VACUUM 的目标是复用空间,而不是紧凑地重写整张表。
如需观察总占用,还应把索引算进去:
SELECT
pg_size_pretty(pg_relation_size('events')) AS heap,
pg_size_pretty(pg_indexes_size('events')) AS indexes,
pg_size_pretty(pg_total_relation_size('events')) AS total;
在 Beta 环境验证 REPACK,而不是猜测语法
来源摘要确认 PostgreSQL 19 Beta 2 提供 REPACK (CONCURRENTLY),但 Beta 阶段的命令细节仍可能变化。部署脚本应以目标构建版本自带的帮助和官方文档为准。可以先这样确认服务器版本与实际语法:
psql -v ON_ERROR_STOP=1 "$DATABASE_URL" <<'SQL'
SELECT version();
\h REPACK
SQL
然后根据 \h REPACK 返回的语法,在测试表上执行非并发与并发模式,对比锁等待、执行时间、WAL 增量和磁盘峰值。不要把网上某个 Beta 快照的命令形式直接固化进生产自动化。
“在线”也不等于“没有锁”。在线重写通常仍要在某些阶段取得锁,并需要处理重写期间发生的并发变更。上线前至少要验证:
- 开始和切换阶段会申请什么锁,最长等待多久;
- 长事务、未结束的查询和逻辑复制槽是否会阻碍清理;
- 重写是否产生大量 WAL,以及副本能否及时回放;
- 临时空间与目标表、索引的额外副本需要多少磁盘;
- 失败、中断和取消后留下哪些对象,如何恢复或重试。
可以用下面的查询在演练期间观察相关会话与锁等待:
SELECT
a.pid,
a.state,
a.wait_event_type,
a.wait_event,
age(clock_timestamp(), a.query_start) AS running_for,
l.mode,
l.granted,
left(a.query, 120) AS query
FROM pg_stat_activity AS a
LEFT JOIN pg_locks AS l ON l.pid = a.pid
WHERE a.datname = current_database()
ORDER BY a.query_start NULLS LAST;
不要把 REPACK 当成 autovacuum 的替代品
REPACK 处理的是物理布局和文件膨胀问题;autovacuum 处理的是持续产生的死元组、统计信息维护和事务 ID 安全。即使 PostgreSQL 19 的并发重写降低了维护门槛,也不应该用周期性 REPACK 掩盖以下问题:
- 更新频率很高,但
autovacuum_vacuum_scale_factor对大表过于宽松; - 长事务阻止死元组被回收;
- 表的
fillfactor不适合实际更新模式; - 索引重复、失效或因写入模式持续膨胀;
- 应用进行无意义的整行更新,制造了额外版本和 WAL。
更稳妥的采用顺序是:先修正 vacuum 与事务管理,再把并发 REPACK 用于确实需要归还磁盘、重整物理布局的大表。由于 PostgreSQL 19 Beta 2 仍是测试版本,应先在接近生产规模的数据副本上压测,记录锁窗口、磁盘峰值、WAL 量和复制延迟,等语法与行为稳定后再纳入正式运维流程。