PostgreSQL 19 REPACK: Removing Table Bloat Online

Author : Mafiree Team | Published On : 24 Sep 2026

PostgreSQL 19 REPACK is a native command that unifies VACUUM FULL and CLUSTER and adds a CONCURRENTLY option, so you no longer have to choose between reclaiming disk space and keeping a table online. After a large delete or an update-heavy workload, a table can bloat, and autovacuum can't return that space to the operating system. DBAs have had two poor choices: VACUUM FULL, which locks the table for the whole rewrite, or the third-party pg_repack extension, with the hope that version, permissions and maintenance window all line up.

Why the Old Tools Fell Short

VACUUM FULL rewrites a table into a new file, discards dead tuples and returns the space to the OS. CLUSTER does the same but also reorders rows by an index, which helps range scans. Both take an ACCESS EXCLUSIVE lock for the entire rewrite. On a table of several hundred GB, that is an outage window that must be scheduled and approved, with no guarantee it finishes on time. This is why pg_repack became a standard tool: it builds a shadow table, captures changes with triggers, and swaps the tables at the end.

What REPACK Is

Both older commands were always doing the same job: copying live tuples into a new file and swapping it in. They differed only in whether index order was preserved. REPACK treats them as two modes of one operation, which gives the project a single place to add features like concurrency. It copies live rows, rebuilds every index into new files, and swaps the new files in.

The blog gives the syntax as of Beta 2. The options are VERBOSE, ANALYZE and CONCURRENTLY. Plain REPACK table; behaves like VACUUM FULL, adding USING INDEX idx makes it behave like CLUSTER, and specific columns can be named, for example to repack storage after dropping a wide column. REPACK (VERBOSE) shows progress output, and REPACK (ANALYZE) runs ANALYZE after the rewrite. The h repack command in psql shows the full syntax, and the official PostgreSQL 19 release notes and REPACK documentation are the sources of record as the beta evolves.

How CONCURRENTLY Works

With CONCURRENTLY, REPACK does not hold an ACCESS EXCLUSIVE lock while it copies the table. Any inserts, updates and deletes that happen during the copy are captured through logical decoding and replayed onto the new copy just before the final file swap. An exclusive lock is needed only for that last step, and it is held briefly. 

The Hands-On Test

The author built a one-million-row table with a primary key and two secondary indexes, then simulated churn: every row updated twice, then half, then a third, followed by a 10% delete. The table used 1341 MB, its indexes 241 MB, and the total was 1582 MB, despite holding under a million live rows.

REPACK (VERBOSE, ANALYZE) took about 27 seconds and reported removable and nonremovable row versions, plus CPU, buffer and WAL statistics. Afterward the table was 370 MB, the indexes 89 MB, and the total 459 MB.

Monitoring with pg_stat_progress_repack

This new view lets you watch a running repack from another session. It exposes the phase, command, relation ID, index in use, heap tuples scanned, inserted, updated and deleted, block progress, and index rebuild count. The phase column tells you which stage the job is in, so you don't have to guess whether it's stuck.

Limitations

REPACK (CONCURRENTLY) cannot be used when:

  • the table is UNLOGGED;

  • the table is partitioned (individual partitions can still be repacked one at a time);

  • the table lacks a primary key and index-based replica identity;

  • the command runs inside a transaction block.

Any repack, concurrent or not, requires the MAINTAIN privilege on the table. This matters if you run it as a lower-privileged migration or automation user, and superusers need no extra permission changes.

Should You Keep pg_repack?

Yes. pg_repack is the only online option on PostgreSQL 18 and earlier. On PostgreSQL 19, its per-partition batch tooling and its track record on very large, long-running jobs still make it worth keeping for edge cases the new core command doesn't cover yet, particularly partitioned tables as a whole. 

For the common case of a single logged table with a primary key bloated by update or delete churn, native REPACK (CONCURRENTLY) removes a dependency and a maintenance window, and gives you a progress view for free.

Conclusion

Bloat has always been well understood. What was missing was a fix that didn't force a trade-off between reclaiming space and keeping the table online. VACUUM FULL gave the first at the cost of the second, and pg_repack gave both but required an extension installed and version-matched on every environment. REPACK in PostgreSQL 19 removes that trade-off inside the core, with a progress view so you aren't flying blind.