REPACK CONCURRENTLY rebuilt a 355 MB bloated table down to 178 MB on a live database while an INSERT landed mid-rebuild and committed without waiting. That single command removes the last excuse for scheduling downtime around VACUUM FULL, and it is only one of several changes in PostgreSQL 19 that alter day-to-day operations rather than just the release notes. The second beta shipped in mid July, which puts GA on the usual September or October track.
Every claim below was executed on a lab server, not paraphrased from the draft release notes. The measured features: the new REPACK command, parallel autovacuum, online data checksum toggling, in-place partition merge and split, property graph queries, and a set of changed defaults that will surprise anyone upgrading from an existing PostgreSQL deployment. The packages come from the same PGDG repository used to install PostgreSQL 19 on any of the major distributions.
Tested July 2026 on PostgreSQL 19 Beta 2.
REPACK replaces VACUUM FULL and CLUSTER
Table bloat has always had two bad answers: VACUUM FULL, which takes an ACCESS EXCLUSIVE lock and blocks everything, or the external pg_repack extension. PostgreSQL 19 folds the fix into core as a first-class REPACK command, and the CONCURRENTLY option rebuilds the table while normal reads and writes continue. The test case below creates half a million rows, then doubles the table’s physical size with a full-table UPDATE:
CREATE TABLE bloated (id int PRIMARY KEY, data text);
INSERT INTO bloated SELECT g, repeat(md5(g::text), 10) FROM generate_series(1, 500000) g;
UPDATE bloated SET data = data || 'x';
SELECT pg_size_pretty(pg_relation_size('bloated'));
The dead tuples left by the UPDATE push the heap to 355 MB. One command reclaims it without an exclusive lock:
REPACK (CONCURRENTLY) bloated;
SELECT pg_size_pretty(pg_relation_size('bloated')) AS after_repack;
The heap comes back at exactly half its bloated size:
after_repack
--------------
178 MB
The full session, captured on the test box:

To confirm the concurrency claim rather than trust it, a second session inserted a row two seconds into a running repack. The INSERT returned immediately, the REPACK completed after it, and the row survived the table swap. Internally the concurrent mode uses logical decoding to replay changes made during the copy, which is why a new max_repack_replication_slots setting exists (default 5). Notably, the whole test ran at the stock wal_level = replica, no configuration change required. Progress is observable in a dedicated pg_stat_progress_repack view, alongside the existing vacuum and CLUSTER progress views.
Plain REPACK without the option behaves like VACUUM FULL (exclusive lock, faster rewrite), and REPACK ... USING INDEX covers what CLUSTER did. Both old commands still work in 19, but REPACK is the one that will get new features from here on.
Parallel autovacuum ships disabled
Autovacuum can finally use parallel workers for index vacuuming, the same machinery manual VACUUM (PARALLEL n) has had since version 13. The catch: it ships disabled. On a fresh Beta 2 cluster:
SHOW autovacuum_max_parallel_workers;
The default is 0, meaning no autovacuum worker will parallelize anything until this is raised:
autovacuum_max_parallel_workers
---------------------------------
0
Enabling it is a two-step opt-in. The server-wide cap comes from autovacuum_max_parallel_workers (further limited by max_parallel_workers), and each table that should benefit needs its own storage parameter:
ALTER TABLE big_events SET (autovacuum_parallel_workers = 2);
The setting shows up in pg_class.reloptions and only pays off on tables with several large indexes, which is exactly where autovacuum falls behind on busy systems today. Deciding which tables deserve it just got easier too: a new pg_stat_autovacuum_scores view exposes the per-table urgency scores the launcher computes, including wraparound pressure, so the tables autovacuum struggles with are no longer a guessing game. That view pairs well with an existing Prometheus and Grafana monitoring setup for tracking vacuum debt over time.
Data checksums toggle online
Since version 18, initdb enables data checksums by default, but changing that decision on an existing cluster meant taking it offline for pg_checksums. PostgreSQL 19 adds two functions that flip checksums on a running cluster:
SELECT pg_enable_data_checksums(cost_delay => 0, cost_limit => 100);
SELECT pg_disable_data_checksums();
The transition is asynchronous. A background worker rewrites every page, and SHOW data_checksums reports intermediate states while it runs. Captured mid-transition on the test cluster:
data_checksums
----------------
inprogress-off
A few seconds later the state settled to its final value. On a multi-terabyte cluster that window is hours, not seconds, and the cost_delay argument exists to throttle the rewrite so it does not saturate storage. Progress is visible in the new pg_stat_progress_data_checksums view. For anyone who initialized a cluster years ago without checksums and has regretted it since, this closes the loop without a dump-and-restore or a full backup and restore cycle.
Partitions merge and split in place
MERGE PARTITIONS
Repartitioning used to mean detaching, copying and reattaching by hand. Two new ALTER TABLE operations do it directly. Merging two half-year range partitions into one:
ALTER TABLE metrics MERGE PARTITIONS (metrics_h1, metrics_h2) INTO metrics_2026;
SPLIT PARTITION
The reverse operation carves one partition into several, with the new bounds declared inline:
ALTER TABLE metrics SPLIT PARTITION metrics_2026 INTO
(PARTITION metrics_q1 FOR VALUES FROM ('2026-01-01') TO ('2026-04-01'),
PARTITION metrics_q2 FOR VALUES FROM ('2026-04-01') TO ('2026-07-01'),
PARTITION metrics_h2b FOR VALUES FROM ('2026-07-01') TO ('2027-01-01'));
Both ran cleanly on Beta 2 and \d+ confirmed the resulting partition layout. One caution for production use: these operations take strong locks on the parent while they run, so they belong in a maintenance window on hot tables. The win is correctness and simplicity, not concurrency.
Property graph queries land in core SQL
PostgreSQL 19 implements SQL/PGQ from the SQL:2023 standard. A property graph is defined as a view over ordinary tables, then queried with graph pattern syntax instead of recursive joins. A minimal topology over two tables:
CREATE PROPERTY GRAPH net_topo
VERTEX TABLES (hosts KEY (id) LABEL host PROPERTIES (name))
EDGE TABLES (links KEY (src, dst)
SOURCE KEY (src) REFERENCES hosts (id)
DESTINATION KEY (dst) REFERENCES hosts (id)
LABEL connects);
SELECT * FROM GRAPH_TABLE (net_topo
MATCH (a IS host)-[IS connects]->(b IS host)
COLUMNS (a.name AS from_host, b.name AS to_host));
The MATCH pattern returns each edge as a row:
from_host | to_host
-----------+---------
lb01 | web01
web01 | db01
This will not displace a dedicated graph database for deep traversals, but for dependency chains, network paths and org charts that already live in relational tables, the pattern syntax reads far better than a WITH RECURSIVE ladder. It follows the same trajectory as pgvector for embeddings: workloads that used to justify a second database keep collapsing into Postgres.
Smaller changes that show up in daily work
GROUP BY ALL groups by every non-aggregate column in the target list, which kills the tedium of repeating column lists in ad-hoc analytics. INSERT ... ON CONFLICT DO SELECT finally returns the existing row on conflict instead of forcing the do-nothing-then-select dance, and it accepts FOR UPDATE to lock what it returns. Both worked exactly as documented on Beta 2:
SELECT region, product, sum(qty) FROM orders GROUP BY ALL;
INSERT INTO users (email) VALUES ('[email protected]')
ON CONFLICT (email) DO SELECT RETURNING id, email;
The utility layer picked up range-aware randoms like random('2026-01-01'::date, '2026-12-31'::date) for generating test data, base64url and base32hex in encode()/decode(), direct bytea to uuid casts, and COPY ... TO ... (FORMAT json) for exporting rows as JSON without a client-side loop. Logical replication gains sequence synchronization, so failovers no longer land on stale sequence values, and a per-lock-type pg_stat_lock view joins the monitoring catalog.
Defaults that change under you
Version 19 flips several defaults that upgrades inherit silently. These were all verified on a stock Beta 2 install:
| Setting | PostgreSQL 18 | PostgreSQL 19 |
|---|---|---|
default_toast_compression | pglz | lz4 |
jit | on | off |
log_lock_waits | off | on |
max_locks_per_transaction | 64 | 128 |
| RADIUS authentication | available | removed |
The JIT flip matters most for analytics workloads: anything that relied on JIT compilation of long-running queries silently loses it after an upgrade and must set jit = on explicitly. The lz4 TOAST default is a clean win (faster compression at similar ratios) but means newly toasted values are no longer byte-identical to pglz output, which occasionally matters for storage-level dedup. And any pg_hba.conf still carrying a radius line will stop the server from starting after the upgrade. One more subtlety on the lock table: lock size allocation changed in 19, so the doubled max_locks_per_transaction default holds roughly the same capacity as before; anyone carrying a custom value should double it on upgrade rather than copy it over.
What to test before GA
Beta 2 is not for production, and the on-disk format can still change before the release candidate, so test clusters should be rebuilt per beta rather than upgraded. The tests worth running now against a copy of a real workload: REPACK CONCURRENTLY against the most bloated table (measure the disk high-water mark during the rebuild, since the table briefly exists twice), a checksum enable run to time the rewrite at a realistic cost_delay, and an EXPLAIN diff of the top ten slowest queries with JIT off. GA typically lands in late September or October; the teams that upgrade smoothly will be the ones whose surprises were all used up on the beta.