PostgreSQL Field Guide

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 FULLCLUSTER——都要在整个重写期间持有 ACCESS EXCLUSIVE 锁,所以团队要么排维护窗口,要么装第三方的 pg_repack 扩展。PostgreSQL 19 给出了内建的第三条路:REPACK,其 CONCURRENTLY 模式在重建期间保持表可读写。

VACUUM FULL 与 CLUSTER 的锁问题

VACUUM FULL 把表的存活元组重写到新文件并重建所有索引;CLUSTER 做同样的事,只是按索引排序。两者从头到尾持有 ACCESS EXCLUSIVE,期间表上的所有读写都排在锁后面。大表重写要几分钟到几小时,光是锁等待堆积的被阻塞会话就足以拖垮应用。膨胀的测量与目标表选择见 autovacuum 与表膨胀;本页只讲重建这一步。

REPACK:统一的命令语义

REPACKVACUUM FULLCLUSTER 的行为折进一条语句:

REPACK [ ( option [, ...] ) ] [ table_and_columns [ USING INDEX [ index_name ] ] ]
REPACK [ ( option [, ...] ) ] USING INDEX

选项有 VERBOSEANALYZECONCURRENTLY

  • 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 安全的

文档明确警告 REPACKCONCURRENTLY 选项不是 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 heapsorting tupleswriting new heap),然后是 catch-up(应用缓冲的变更)、swapping relation files(短暂的排他锁)、rebuilding indexperforming final cleanupcatch-up 阶段拖得长,说明表足够热、最终持锁窗口会可感知——考虑换到更空闲的窗口重跑。该视图同时跟踪 CLUSTERVACUUM FULL,用 command 列区分。

repack 中途失败时,临时文件和复制槽由命令负责清理,原表不受影响。排除原因后重试即可——最常见的原因是并发 DDL、缺 replica identity、槽位池耗尽。

提示词:规划消膨胀战役

规划我的 REPACK 方案
用 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

On this page