What's new in Postgres 19 — PlanetScale📹 The future of AI infrastructure: optimize and shard your database with agents.Watch the talk
Navigation<br>Blog|Engineering<br>Table of contents «Close »Table of contents<br>REPACK: Keep Your Database MovingREPACKING tables online<br>Requirements and caveats
JIT disabled by default<br>Your query planner is smarter, sometimes<br>The rest, in brief<br>PlanetScale Postgres is the fastest way to run Postgres in the cloud. Plans start at just $5 per month.<br>Learn more
Get the RSS feed
What's new in Postgres 19<br>Ahmed Darwich | July 23, 2026<br>Postgres has never had a great built-in way to shrink a bloated table. VACUUM identifies dead space for reuse, but doesn't return it to the operating system. VACUUM FULL and CLUSTER do, but they lock the table for the whole rewrite. For a decade, operators have been forced to reach for extensions like pg_repack and pg_squeeze.<br>Postgres 19 finally brings that capability into core. Among many, the three most notable changes in Postgres 19 are:<br>REPACK rewrites a bloated table, online<br>JIT disabled by default<br>The query planner is smarter, sometimes<br>Let's take a look!<br>Note<br>Postgres 19 Beta 2 was released on 2026-07-16 with GA expected September/October. Defaults and details may change but feature freeze has been announced.
REPACK: Keep Your Database Moving<br>Before Postgres 19, two commands could rewrite a table to reclaim space from dead tuples. VACUUM FULL compacts the table while CLUSTER both compacts and orders it. Both, however, hold an ACCESS EXCLUSIVE lock on the table for the entire rewrite.<br>REPACK, the headline feature of Postgres 19, absorbs the functionality of VACUUM FULL and CLUSTER and adds an online mode to keep your table accepting reads and writes while the rewrite occurs.<br>To demonstrate REPACK we need a bloated table. Here's how to create one with six million rows, and then cause 70% of them to churn into dead tuples.<br>DROP TABLE IF EXISTS t;<br>CREATE TABLE t (<br>id bigint PRIMARY KEY,<br>val integer NOT NULL,<br>payload text NOT NULL<br>);
INSERT INTO t (id, val, payload)<br>SELECT g, g % 1000, repeat('a', 120)<br>FROM generate_series(1, 6000000) AS g;
UPDATE t SET payload = payload || 'x' WHERE val 700; -- churns 4.2M rows<br>ANALYZE t;
You can see how bloated a table is by looking at its size and its dead tuple count.<br>SELECT pg_size_pretty(pg_total_relation_size('t')) AS size,<br>n_dead_tup AS dead_tuples<br>FROM pg_stat_user_tables WHERE relname = 't';
size | dead_tuples<br>---------+-------------<br>1884 MB | 4200347<br>(1 row)
The table sits at 1884 MB, and about 4.2 million of its rows are dead.<br>Note<br>n_dead_tup is a statistical estimate so the value might be a little different in your local run. It gets updated intermittently or after an ANALYZE.
In Postgres 19, there are three different ways to utilize REPACK. All three will reclaim the space but they differ in whether they order the rows and whether they lock the table while the rewrite occurs.<br>REPACK t; -- unordered, like VACUUM FULL<br>REPACK t USING INDEX t_pkey; -- ordered by an index, like CLUSTER<br>REPACK (CONCURRENTLY) t; -- online: reads and writes continue
Run each one against a freshly bloated table and it drops to ~1085 MB, with zero dead tuples remaining.<br>CommandBeforeAfterTable locked?REPACK t;1884 MB1085 MByes, the whole timeREPACK t USING INDEX t_pkey;1884 MB1085 MByes, the whole timeREPACK (CONCURRENTLY) t;1884 MB1086 MBonly during the final swap<br>REPACKING tables online<br>pg_repack, a popular open-source extension used for compacting tables in Postgres, copies the live rows into a new table while the old table continues to accept writes. Triggers are used to record every change into a log table. This log table is then replayed on the new copy before the two are swapped.<br>The pg_squeeze extension uses the same approach but without the triggers. The changes are already in the WAL, so it decodes them from there.<br>REPACK (CONCURRENTLY) is essentially pg_squeeze brought into Postgres core. The design and most of the code are by Antonin Houska, the creator of pg_squeeze. It works by keeping the most expensive part of the operation non-blocking:<br>Takes a SHARE UPDATE EXCLUSIVE lock on the table, which allows reads and writes to continue<br>Takes an MVCC snapshot, a frozen view of the table at that moment<br>Opens a replication slot at that snapshot<br>Builds a new copy of the table and its indexes from the snapshot<br>While the copy builds, the incoming writes to the old table are decoded from the WAL into an ordered backlog in temporary files on disk<br>Once the copy is built, the backlog of writes is replayed onto it. The old table is still accepting writes, however, so the backlog is refilling<br>Upgrades to an ACCESS EXCLUSIVE lock, replays the refilled backlog, swaps the files, and finally releases the lock<br>Steps 1-6 run under the weak lock, so writes can continue. Only step 7 blocks. The swap is a file switch. This ACCESS EXCLUSIVE lock is held for the final batch of changes, including the writes that landed during the first...