PostgreSQL 19 Beta 2:用内置并发 REPACK 解决表膨胀

2026-07-23 16 预计阅读时间: 1 分钟
来源: planetscale.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:7 分钟

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 量和复制延迟,等语法与行为稳定后再纳入正式运维流程。


相关推荐