💰 Calculate Gross-Net Salary→

PostgreSQL 19 RC1: REPACK (CONCURRENTLY) Replaces VACUUM FULL, and the Defaults to Check Before Upgrading

· Aloka Technology· 0 views· 7 min read
PostgreSQL 19 RC1: REPACK (CONCURRENTLY) Replaces VACUUM FULL, and the Defaults to Check Before Upgrading

PostgreSQL 19 adds an online table rewrite that holds its exclusive lock only briefly. Its limits, and the default changes that affect upgrades, decide whether it is safe for you.

A bloated 400 GB table forces a familiar choice. VACUUM FULL and CLUSTER rewrite it into a compact file, but both hold an ACCESS EXCLUSIVE lock for the whole rewrite, so every read and write on that table waits. Most teams therefore live with the bloat, or reach for an extension such as pg_repack. PostgreSQL 19 puts an online rewrite into core: REPACK (CONCURRENTLY).

The question for this article: what does the core implementation actually guarantee, where does it still lock or fail, and which PostgreSQL 19 defaults will surprise you at upgrade time?

Status check first. PostgreSQL 19 is not generally available as of October 28, 2026. The RC 1 announcement (October 15, 2026) says GA is planned for November 19, 2026, with RC 2 on November 12 if needed. The release notes page I read still carried the placeholder date "2026-??-??" and was marked as RC1-level, so details may still change.

Background: why table rewrites hurt

PostgreSQL's MVCC leaves dead row versions in the heap. Plain VACUUM makes that space reusable inside the file but rarely shrinks the file. To return space to the OS or to restore physical order you must write a new file. Until now that meant VACUUM FULL or CLUSTER (both exclusive), or a trigger-based extension. In 19, REPACK is the single command for this; per the release notes the older commands remain for compatibility.

How REPACK (CONCURRENTLY) works

The 19 documentation describes the mechanism in three steps:

  1. The table is copied into a new file, skipping dead tuples, and a new file is built for each index.
  2. While that copy runs, the bulk of the operation holds only a SHARE UPDATE EXCLUSIVE lock. Concurrent changes are captured with logical decoding.
  3. The captured changes are applied before ACCESS EXCLUSIVE is requested. After the lock is obtained, a final pass applies what remains and the files are swapped.

The docs say the exclusive lock is therefore "typically held for a short time, depending on the amount of changes accumulated while the lock was being waited for." Without CONCURRENTLY the exclusive lock is held from the start to the end. Because logical decoding needs a replication slot, the release notes add a server setting, max_repack_replication_slots, that limits how many concurrent repacks can run.

Deep analysis: what "online" does not mean

The documentation is unusually candid about restrictions, and they matter more than the headline:

  • Not MVCC-safe. After the repack commits, the table appears empty to concurrent transactions that use a snapshot taken before the repack started. Long-running repeatable-read or reporting transactions are the exposed population.
  • Identity requirement. It cannot be used on a table without a primary key or an index-based replica identity. This follows from logical decoding having to identify changed rows.
  • Excluded objects. Not supported for unlogged tables, partitioned tables, system catalogs, TOAST tables, non-heap access methods, materialized views, or tables with the user_catalog_table option, and not inside a transaction block.
  • Change-volume ceiling. If more than about 104 million rows are concurrently updated or deleted during the run, the command fails.
  • DDL conflicts. Concurrent DDL on the table can make it fail.
  • Disk and horizon. You need free space at least equal to the table plus all its indexes, and a repack counts as one long transaction that stops VACUUM from cleaning dead rows in other tables. The docs suggest watching pg_stat_activity.backend_xid.

It is also not a wraparound remedy: the docs say plain VACUUM is the right tool because a repack takes much longer.

Alongside it, 19 adds parallel index vacuuming in autovacuum (autovacuum_max_parallel_workers, per-table autovacuum_parallel_workers) and a scoring system that orders tables for autovacuum. RC 1 lists several fixes for both REPACK (CONCURRENTLY) and parallel autovacuum since beta 4, which is a signal that these are young code paths.

Comparison with alternatives

ApproachLock during rewriteNotable constraint
VACUUM FULL / CLUSTER / plain REPACKACCESS EXCLUSIVE throughoutBlocks reads and writes for the whole rewrite
REPACK (CONCURRENTLY) (19)SHARE UPDATE EXCLUSIVE, brief ACCESS EXCLUSIVE at swapNeeds PK or replica-identity index, a replication slot, not MVCC-safe
pg_repack extensionBrief exclusive locks (from its own design)Out-of-core, trigger-based; I did not re-verify its current behavior for this article
Plain VACUUM plus tuned autovacuumNo exclusive lockReuses space; seldom shrinks the file

Practical example

The following is untested; it assumes PostgreSQL 19 (RC1 or later), a table orders with a primary key, and enough free disk for table plus indexes.

-- Check how many replication slots a concurrent repack may use
SHOW max_repack_replication_slots;

-- Online rewrite with progress messages
REPACK (CONCURRENTLY, VERBOSE) orders;

-- Reorder by a clustering index set earlier, then refresh statistics
-- (ANALYZE is only supported for a single, non-partitioned table)
ALTER TABLE orders CLUSTER ON orders_created_at_idx;
REPACK (CONCURRENTLY, ANALYZE) orders USING INDEX;

-- While it runs, watch for the long transaction it creates
SELECT pid, backend_xid, state, query
FROM pg_stat_activity
WHERE backend_xid IS NOT NULL;

Because I have no verified benchmark, here is a methodology instead: clone a production-sized table, delete roughly half the rows, run a pgbench custom script with your real read/write mix at a fixed rate, then run VACUUM FULL and later REPACK (CONCURRENTLY) in separate trials. Record p99 latency, the longest lock wait on the table, WAL volume, and peak disk use for each.

Upgrade checks that are not about REPACK

The 19 migration notes list several default changes. Review these before pg_upgrade:

ChangeAction
jit disabled by defaultAnalytical workloads that relied on JIT must enable it
max_locks_per_transaction 64 to 128Explicit settings from 18 or earlier need doubling to keep capacity, per the notes
standard_conforming_strings forced onRe-dump old dumps made with it off
Default TOAST compression pglz to lz4Storage and CPU effect; no action required
MD5 authentication warns; RADIUS removedMove to scram-sha-256; replace RADIUS
btree_gist inet/cidr indexespg_upgrade rejects them; reindex first

Limitations and failure modes

Treat REPACK (CONCURRENTLY) as a tool for tables with a clear key, moderate write rates during the run, and no long snapshots. Partitioned tables must be repacked per partition or without CONCURRENTLY, if I read the restriction list correctly; I did not test that. A write-heavy table can exceed the change ceiling or make the final catch-up long. The replication-slot dependency means a failed or abandoned run is an operational item to check. One secondary source (a blog on beta 3) says features such as FOR PORTION OF, SQL/PGQ and online data checksums were reverted from the 19 branch later; the release notes page I read did not list them, but verify against the final notes.

Engineering recommendations

Do not plan a production upgrade around RC features; wait for the GA notes and the first minor release. Where bloat is a real cost, inventory tables by size and key presence, and schedule repacks outside long reporting windows. Keep autovacuum as the first line of defense and use repack as a corrective, not routine, operation. Alert on repack-related replication slots and on the long transaction it introduces.

Conclusion

REPACK (CONCURRENTLY) removes the multi-hour exclusive lock but keeps a short one, and it trades that for a set of explicit limits: primary key, replication slot, no MVCC safety, a change ceiling. The concrete next step: on a staging copy of your largest bloated table, restore it under PostgreSQL 19 RC1, run the pgbench plan above, and compare lock wait and p99 against VACUUM FULL before deciding.

Sources

Information as of October 28, 2026.

I read these pages but did not install PostgreSQL 19 or run any SQL or benchmark; the SQL above is untested.

Share:

Leave a comment

Explore More

Discover More Articles

Browse Articles
↑Zalo