REPACK / REPACK CONCURRENTLY 在线消膨胀
PostgreSQL 19 的 REPACK 统一了 VACUUM FULL 与 CLUSTER,REPACK CONCURRENTLY 基于逻辑解码在线重建膨胀表
PostgreSQL 19 仍处于 Beta
截至 2026-08,PostgreSQL 19 尚未正式发布。下文语法、GUC 名与视图以 PostgreSQL 19 文档为准;用于生产前请对照正式 release notes 复核。
普通 VACUUM 让死空间在关系内部复用,但几乎不会把空间还给操作系统。两种经典的收缩手段——VACUUM FULL 和 CLUSTER——都要在整个重写期间持有 ACCESS EXCLUSIVE 锁,所以团队要么排维护窗口,要么装第三方的 pg_repack 扩展。PostgreSQL 19 给出了内建的第三条路:REPACK,其 CONCURRENTLY 模式在重建期间保持表可读写。
VACUUM FULL 与 CLUSTER 的锁问题
VACUUM FULL 把表的存活元组重写到新文件并重建所有索引;CLUSTER 做同样的事,只是按索引排序。两者从头到尾持有 ACCESS EXCLUSIVE,期间表上的所有读写都排在锁后面。大表重写要几分钟到几小时,光是锁等待堆积的被阻塞会话就足以拖垮应用。膨胀的测量与目标表选择见 autovacuum 与表膨胀;本页只讲重建这一步。
REPACK:统一的命令语义
REPACK 把 VACUUM FULL 与 CLUSTER 的行为折进一条语句:
REPACK [ ( option [, ...] ) ] [ table_and_columns [ USING INDEX [ index_name ] ] ]
REPACK [ ( option [, ...] ) ] USING INDEX选项有 VERBOSE、ANALYZE、CONCURRENTLY:
REPACK t;——纯重写回收磁盘,等价于VACUUM FULL。REPACK t USING INDEX i;——额外按索引重排行,等价于CLUSTER。省略索引名时使用之前ALTER TABLE ... CLUSTER ON设置的索引。REPACK;——不带表名,处理当前数据库里你持有MAINTAIN权限的所有表和物化视图。该形式不能在事务块里执行,也不能与CONCURRENTLY组合。REPACK (ANALYZE) t;——重写后执行ANALYZE。目前只支持单张非分区表;重写后统计信息重置,无论如何都值得做。
需要对表持有 MAINTAIN 权限。不带 CONCURRENTLY 时,REPACK 仍然全程持有 ACCESS EXCLUSIVE——锁语义没变,统一的只是命令表面。
CONCURRENTLY 的实现原理
CONCURRENTLY 模式下,REPACK 把存活元组拷贝到新文件(每个索引各一个新文件),期间对旧文件的读写照常进行。拷贝过程中发生的变更通过逻辑解码捕获并应用到新文件;之后命令才获取短暂的 ACCESS EXCLUSIVE 锁交换新旧文件、删除旧文件。锁通常只持有交换所需的时间——但如果积压的变更很多,它们必须在持锁期间 replay 完,所以热点表在收尾阶段仍可能有可感知的阻塞窗口。
文档里两条行为值得记住:
- repack 开始后插入的行不参与排序,即使指定
USING INDEX——clustering 是一次性的物理重排。 - 其他会话在 repack 期间对该表执行 DDL 可能导致
REPACK CONCURRENTLY失败。进行中的 repack 要避开迁移窗口。
CONCURRENTLY 不是 MVCC 安全的
文档明确警告 REPACK 的 CONCURRENTLY 选项不是 MVCC 安全的(见 MVCC caveats)。把它当作需要刻意安排的维护操作,而不是无感的后台动作。
硬性限制
以下任一情况都会拒绝 CONCURRENTLY:
- 表是
UNLOGGED; - 表是分区表(普通
REPACK可以处理分区表——逐个分区 repack——但不能并发,也不能在事务块里); - 表既没有主键也没有基于索引的 replica identity——逻辑解码需要标识行的手段;
- 表是系统目录或 TOAST 表;
REPACK在事务块里执行;max_repack_replication_slots没有空闲槽位(见下文)。
另一个硬限制是磁盘,与是否 CONCURRENTLY 无关:重写需要表加全部索引的临时副本,空闲空间至少是表大小加索引大小。顺序扫描加排序的路径还会产生临时排序文件,峰值可能接近表大小的两倍再加索引(可以对会话设置 enable_sort = off 强制走索引扫描路径)。CONCURRENTLY 在此之上还要缓冲拷贝期间的并发 DML。开始前给会话配一个宽裕的 maintenance_work_mem。
max_repack_replication_slots 与槽位隔离
CONCURRENTLY 需要一个复制槽来做逻辑解码。PostgreSQL 19 为此给 REPACK 单开了一个池子:max_repack_replication_slots(默认 5,只能在启动时设置)在 max_replication_slots 之外为 REPACK 独占预留槽位。隔离是双向的:
- repack 作业永远不会占用逻辑复制发布/订阅依赖的槽位;
- 订阅端也永远饿不死 repack 作业;
- 但默认同时只能跑 5 个
REPACK CONCURRENTLY,第六个直接失败。
实操建议:串行执行 repack 作业(出于 I/O 考虑,一两个并发通常也是上限);确实需要更多并行度时,计划一次重启来调大 max_repack_replication_slots。repack 失败时不要去调 max_replication_slots——耗尽的并不是那个池子。
内建 REPACK 与 pg_repack 扩展的选型
内建命令与 pg_repack 扩展是解决同一问题的两套独立实现。选型对照:
REPACK(PostgreSQL 19 内建) | pg_repack(扩展) | |
|---|---|---|
| 可用性 | PostgreSQL 19+,无需安装 | 扩展 + CLI;PostgreSQL 18 及以前的生产方案 |
| 在线变更捕获 | 基于逻辑解码的专用复制槽 | 触发器写日志表,交换前 replay |
| 行标识要求 | 主键或基于索引的 replica identity | 主键或合适的唯一索引 |
| 锁形态 | 仅最终交换时短暂 ACCESS EXCLUSIVE | 开始和结束时的短暂排他锁 |
| 物理重排 | USING INDEX | --order-by |
| 只重建索引 | 不支持——用 REINDEX CONCURRENTLY | --index / --only-indexes |
| 运维形态 | SQL 语句,pg_stat_progress_repack 看进度 | 外部 CLI,有独立的发布节奏与版本匹配要求 |
在 PostgreSQL 19 上,内建命令去掉了扩展依赖、版本匹配负担和基于触发器的日志表。在更老的版本上,pg_repack 仍是工具——内建命令在那些版本不存在。
实操 runbook
一次完整的 repack 流程:
-- 1. 确认表确实膨胀(pg_stat_user_tables 的估算不够)
CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT * FROM pgstattuple('app.events');
-- 看 dead_tuple_percent 和 free_percent
-- 2. 确认可以用 CONCURRENTLY:replica identity 必须是默认(有主键)或索引
SELECT relreplident FROM pg_class WHERE oid = 'app.events'::regclass;
-- 'd'(默认,需有主键)或 'i'(replica identity 索引)可以;'n' 和 'f' 不行
-- 3. 确认磁盘:空闲空间 >= 表 + 索引大小
SELECT pg_size_pretty(pg_total_relation_size('app.events'));-- 4. 在独立会话中执行
SET maintenance_work_mem = '1GB';
REPACK (CONCURRENTLY, ANALYZE, VERBOSE) app.events;-- 5. 从另一个会话观察进度
SELECT pid, datname, relid::regclass AS relation, command, phase,
heap_blks_scanned, heap_blks_total,
heap_tuples_scanned, heap_tuples_inserted,
index_rebuild_count
FROM pg_stat_progress_repack;阶段从 initializing 开始,经过堆扫描/拷贝(seq scanning heap / index scanning heap、sorting tuples、writing new heap),然后是 catch-up(应用缓冲的变更)、swapping relation files(短暂的排他锁)、rebuilding index 和 performing final cleanup。catch-up 阶段拖得长,说明表足够热、最终持锁窗口会可感知——考虑换到更空闲的窗口重跑。该视图同时跟踪 CLUSTER 和 VACUUM FULL,用 command 列区分。
repack 中途失败时,临时文件和复制槽由命令负责清理,原表不受影响。排除原因后重试即可——最常见的原因是并发 DDL、缺 replica identity、槽位池耗尽。
提示词:规划消膨胀战役
用 PostgreSQL 19 的 REPACK CONCURRENTLY 规划一次在线消膨胀。 背景: - 数据库大小 <GB>,膨胀最严重的 <N> 张表:<表名及大致大小> - 最热表的写入速率:<rows/s>;空闲窗口:<时间段> - max_repack_replication_slots:5(默认);当前在用的逻辑复制槽:<n> - 数据盘剩余空间:<GB> 输出: 1. 表的 repack 顺序(收益 vs 风险),附具体的 REPACK 语句。 2. 前置检查 SQL:膨胀确认(pgstattuple)、replica identity、磁盘余量。 3. 符合 max_repack_replication_slots 与 I/O 上限的并发计划。 4. pg_stat_progress_repack 的监控查询与中止标准(复制延迟、锁等待、catch-up 时长)。 5. repack 中途异常时的回滚/中止方案。
命令参考:PostgreSQL 19 REPACK 与进度报告视图。另见 PostgreSQL 19 总览。第三方实测:depesz 的走查与 digoal 关于槽位隔离的分析。
Last updated on