内容提要
PostgreSQL 19 新增 REPACK 命令,整合 VACUUM FULL 与 CLUSTER,其 CONCURRENTLY 模式可在线重写表,无需扩展,托管服务可用。实测比 pg_repack、pg_squeeze 更快、WAL 更少、失败后无残留。但运行期间会阻碍 VACUUM 清理死元组,末尾换表需短暂 ACCESS EXCLUSIVE 锁,更新删除超约 1.05 亿行会内存耗尽失败,旧快照可能读到空表。
延伸解读
在线重写期间 VACUUM 为何被抑制
REPACK (CONCURRENTLY) 依赖逻辑解码获取一致性快照,该快照在整个运行期间必须保持有效。因此 PostgreSQL 无法清理旧快照可能仍需要的死元组,导致 VACUUM 无法回收空间。更关键的是,REPACK 使用的临时复制槽属于整个服务器,会阻止所有数据库中的死元组清理,而 pg_repack 仅影响当前数据库。运行期间应监控繁忙表的 n_dead_tup,避免同时安排大批量作业。
末尾换表锁的代价与风险
REPACK 在最后阶段需要短暂的 ACCESS EXCLUSIVE 锁来重放变更并交换文件。若存在长查询或空闲事务,所有新查询将排队等待,造成服务中断。设置 lock_timeout 可限制等待时间,但一旦超时,整个重写工作(可能已进行数十分钟)将被丢弃。与 pg_repack 不同,REPACK 不会重试,因此需权衡锁超时与重跑成本。
并发变更的内存硬上限
REPACK 后端为运行期间每一行被更新或删除的记录保留约 50 字节内存,且不受 maintenance_work_mem 限制。当变更行数达到约 1.05 亿时,内部结构翻倍将超出 PostgreSQL 单次分配上限,导致失败。若内存不足,可能触发 OOM 杀死后端并导致集群重启。运行前应估算表的更新删除速率,确保总变更行数远低于 1 亿。
旧快照可能读到空表
REPACK (CONCURRENTLY) 在 PostgreSQL 19 中不是 MVCC 安全的。一个在重写前启动的 REPEATABLE READ 只读事务,在换表后读取该表会看到 0 行,因为新副本由 REPACK 的事务写入,对旧快照不可见。pg_repack 通过等待所有事务来缩小窗口,但并非完全安全。若存在长 REPEATABLE READ 导出任务,应提前告知或安排避开重写。
Q&A
PostgreSQL 19 的 REPACK (CONCURRENTLY) 和 pg_repack、pg_squeeze 相比,性能上有什么优势?
REPACK (CONCURRENTLY) 在每次测试中都是最快的在线重打包工具,写入时产生的 WAL 最少,并且失败后不会留下任何残留对象。pg_repack 的 WAL 是它的 1.8 倍,pg_squeeze 在写入下需要更多磁盘。
运行 REPACK (CONCURRENTLY) 时,为什么 VACUUM 无法清理死元组?
因为在线重打包需要保持操作开始时的表快照,PostgreSQL 因此不能移除旧快照可能仍需要的行版本。VACUUM 仍会运行,但无法清理这些行版本。
REPACK (CONCURRENTLY) 在运行期间对并发更新和删除有什么硬性限制?
每个被其他会话更新或删除的行会在 REPACK 后端内存中保留约 50 字节,直到提交。当更新和删除的总行数超过约 1.05 亿时,会因内存分配请求过大而失败,且原表保持不变。
REPACK (CONCURRENTLY) 在最后换表时会对表加什么锁?会有什么影响?
最后换表时需要短暂的 ACCESS EXCLUSIVE 锁。如果此时有长查询或事务持有锁,REPACK 会等待,导致所有新查询排队;若设置了 lock_timeout,超时会导致整个运行被丢弃。
使用 REPACK (CONCURRENTLY) 需要满足哪些前提条件?
表需要有一个副本标识索引(主键或 REPLICA IDENTITY USING INDEX),不支持可延迟主键、REPLICA IDENTITY FULL 和 NOTHING。不能在分区表的父表上运行,只能对单个分区运行。需要空闲的复制槽(受 max_repack_replication_slots 限制,默认 5)和后台工作进程槽(max_worker_processes)。wal_level = replica 即可。
运行 REPACK (CONCURRENTLY) 前应该做哪些准备和检查?
首先估算运行时长,确保更新和删除速率乘以预计时长远低于 1 亿行。检查 pg_stat_activity 中的长事务和长查询,确保磁盘能容纳表的第二份副本、索引和 WAL,决定是否设置 lock_timeout,并告知可能运行长 REPEATABLE READ 事务的用户。