Skip to main content

PostgreSQL 19 features I'm excited about

Tianzhou · Apr 30, 2026

PostgreSQL 19 Beta 1 shipped on June 4, 2026. The feature set is now frozen for the release cycle, so every item below is confirmed in beta rather than drawn from a draft. Minor behavioral changes are still possible through the release candidates; we will track them here.

Update HistoryComment
2026/06/08Updated for the PostgreSQL 19 Beta 1 release.
2026/04/30Initial version.

PostgreSQL 18 shipped asynchronous I/O as raw capability. PostgreSQL 19 is the operational counterpart, and the items below are the ones I'm most excited about. In-core plan advice finally closes the longest-running gap in the planner. DDL extraction lands in core. REPACK and checksums move out of the maintenance window. Lock contention and standby recovery state get first-class system views. The 32-bit MultiXact ceiling that has caused real production outages is gone. Autovacuum becomes parallel and, just as important, observable.

pg_plan_advice

PostgreSQL's planner is excellent at the average case and occasionally wrong at the long tail. Skewed distributions, stale statistics, and many-table joins can push it onto a plan that is simply unacceptable in production. For years the workarounds all lived outside core: the pg_hint_plan extension, application-level SET enable_* flags, third-party plan-locking extensions. Each one has its own semantics and its own lifecycle to babysit.

This is not a theoretical pain. In February 2026, Clerk traced an outage to exactly this pattern: a column whose values were 99.9996% NULL, an ANALYZE whose sample happened to contain only NULLs, and a planner that concluded the column was 100% NULL. The plan that followed assumed zero non-null rows where there were over 17,000.

PostgreSQL 19 brings the answer inside core, in two layers.

pg_plan_advice introduces a planner-advisor framework. It generates a plan-advice string that captures the relevant plan choice (join order, scan method, and so on) and exposes a hook for other modules to inject advice at planning time. The advice can be passed inline via EXPLAIN (PLAN_ADVICE '...') for one-shot use.

pg_stash_advice persists plan-advice strings keyed by (stash_name, queryId) in dynamic shared memory and injects them automatically at planning time via the pg_plan_advice hook. One write. Effective across all sessions.

-- 1. Load the extension (must already be in shared_preload_libraries)
CREATE EXTENSION pg_stash_advice;

-- 2. Create a stash
SELECT pg_create_advice_stash('events_stash');

-- 3. Find the queryId
EXPLAIN (VERBOSE) SELECT * FROM events WHERE tenant_id = 42;
-- → Query Identifier: 9876543210

-- 4. Stash the advice for that queryId
SELECT pg_set_stashed_advice('events_stash', 9876543210,
    'INDEX_SCAN(events events_tenant_idx)');

-- 5. Activate the stash in this session
SET pg_stash_advice.stash_name = 'events_stash';

-- 6. Verify: the planner now picks the indexed path
EXPLAIN (COSTS OFF) SELECT count(*) FROM events WHERE tenant_id = 42;
-- → Index Scan using events_tenant_idx

Scope is controlled by the pg_stash_advice.stash_name GUC: ALTER DATABASE, ALTER ROLE, or session-level SET.

My verdict: this is plan stability without touching the application. One write to a stash takes effect across every session that runs the matching queryId, with no pg_hint_plan and no third-party tooling. The right use case is a small set of queries with known planner regressions on production data. Applying advice has a cost even when it does not change the plan, so don't reach for it across a whole workload. Use it as a scalpel, not a blanket.

DDL extraction function

PostgreSQL has never shipped a way to pull the CREATE statement for a database, role, or tablespace out of the catalog. The data is there. The DDL is not. So every team that needs it ends up writing its own extractor: a shell loop around pg_dump --schema-only, a hand-rolled catalog query inside a migration tool, or a schema-management platform like Bytebase that reads the catalog directly and assembles the DDL. The implementations differ. The maintenance burden does not. Every major Postgres release brings new catalog columns and new attribute encodings to absorb, and somebody has to keep up.

PostgreSQL 19 replaces those bespoke layers with a stable, in-core function-call contract. Three functions, one option shape, returning the DDL as text:

SELECT pg_get_database_ddl('mydb'::regdatabase);
SELECT pg_get_role_ddl('myrole'::regrole);
SELECT pg_get_tablespace_ddl('mytbs');

All three accept pretty := true. pg_get_database_ddl() additionally accepts owner := false and tablespace := false to suppress those clauses when the target environment differs.

My verdict: a stable in-core contract retires every team's homegrown extractor. Migration prep, CI/CD schema diffs, audit snapshots of role grants and database properties, embedded calls from schema-management tools, all of it stops parsing pg_dump text.

These functions complement pg_dump rather than replace it: single-object only, no dependency ordering, no indexes, triggers, or constraints. Permissions are scoped sensibly: CONNECT on the database, USAGE on the tablespace, superuser or self for roles.

Online maintenance

Two long-standing maintenance pain points shrink. Both used to need a window.

REPACK and REPACK CONCURRENTLY. A single in-core command consolidates VACUUM FULL, CLUSTER, and the third-party pg_repack extension. With CONCURRENTLY, the rebuild runs without an ACCESS EXCLUSIVE lock: concurrent reads and writes proceed against the original heap while the new one is built and switched. max_repack_replication_slots controls the slot pool that backs the concurrent variant.

REPACK TABLE my_table;
REPACK TABLE my_table CONCURRENTLY;
REPACK INDEX my_index CONCURRENTLY;

Online data checksum enable/disable. Toggling checksums no longer requires stopping the cluster and running pg_checksums against a halted data directory. The change applies progressively in the running cluster.

My verdict: both items kill off a class of "schedule a window, hope nothing breaks" operations, and that hope-as-a-strategy approach is exactly what you want gone from a runbook.

One caveat. REPACK CONCURRENTLY deserves the same caution as any logical-decoding-backed feature: it consumes a replication slot, so a slow consumer or a stuck transaction will hold WAL. Pin max_repack_replication_slots to a reasonable upper bound and watch slot lag during long repacks. Online checksum toggling is the safer of the two, because the failure mode is a clean abort, not a locked-out cluster.

Observability

Two diagnostic gaps close in 19. Lock history and standby recovery state get first-class system views.

Locks

pg_stat_lock exposes per-lock-type statistics: counts and waits broken out by lock mode. pg_locks shows the snapshot. pg_stat_lock shows the history.

log_lock_waits is on by default. Long lock waits are now logged out of the box. Threshold and noise floor are unchanged, so operators no longer need to flip the GUC in every new install.

My verdict: the combination matters more than either piece alone. pg_locks answers "who is blocked right now." pg_stat_lock answers "where is contention concentrated over time." The first informs immediate intervention. The second informs schema and indexing decisions. Build a dashboard query against pg_stat_lock filtered by relation and mode, and you'll find most contention surprises sitting in two or three hot tables.

Recovery

Standby monitoring used to mean calling functions in sequence (pg_last_wal_replay_lsn(), pg_last_wal_receive_lsn(), pg_get_wal_replay_pause_state(), pg_last_xact_replay_timestamp()) and then reconciling the results yourself. Each call was its own snapshot. The answer to "where is this standby right now?" was a composite, not a reading.

PostgreSQL 19 adds pg_stat_recovery, a single system view that snapshots all of it from shared memory in one read. Fields cover:

  • Last replayed WAL position and timeline
  • End position and timeline of the WAL record currently being replayed
  • Current WAL chunk start time
  • Promotion trigger state
  • Last commit/abort timestamp, the freshness signal for replication lag
  • Recovery pause state
SELECT * FROM pg_stat_recovery;

The view returns no rows on a primary. Read access requires the pg_read_all_stats role.

My verdict: the atomic snapshot is the part that matters. Previously, when alerts fired on "replica too far behind," telling real lag apart from a query that raced with a state transition meant correlating timestamps across multiple function calls. One view collapses that into a coherent reading for dashboards and runbooks. The current-WAL-chunk-start-time field has no prior function equivalent; it diagnoses standbys stuck mid-record rather than between records.

Logical replication: sequence sync

Logical replication has reproduced table data faithfully since 10. What it has not reproduced is the sequences backing SERIAL and IDENTITY columns. Promote a subscriber to primary and the next INSERT triggers a primary-key violation against rows the application thought it owned. A nasty surprise to discover at cutover.

PostgreSQL 19 closes the gap. Publications gain sequence support, a new REFRESH SEQUENCES subcommand triggers the copy, and a sequencesync worker batches the work.

CREATE PUBLICATION upgrade_pub FOR ALL TABLES, ALL SEQUENCES;

ALTER SUBSCRIPTION upgrade_sub REFRESH SEQUENCES;

pg_subscription_rel.srsubstate exposes per-sequence progress (i = INIT, r = READY).

My verdict: zero-downtime upgrades and failover get meaningfully safer. The behavioral catch worth circling: sequences sync only when REFRESH SEQUENCES runs. They do not track continuously. So build the call into cutover runbooks, right before promoting the subscriber, or you'll re-discover the same primary-key violation the feature was meant to prevent.

Beta 1 pairs this with a second win for the same upgrade story: logical replication can now be enabled without a server restart. When wal_level is replica, the server promotes to logical decoding on demand, and a new read-only effective_wal_level reports what it is actually running. Standing up a temporary subscriber for a major-version upgrade no longer begins with a restart of the primary.

MultiXact ceiling, eliminated

PostgreSQL tracks row-level locks shared across transactions in MultiXact structures. The pointer into the member array (MultiXactOffset) was 32 bits, capping total members at roughly 4 billion. Transaction-ID wraparound is well-known and widely monitored. MultiXact member exhaustion is not. It does not show up on standard XID-consumption dashboards, and high-concurrency workloads with foreign keys can hit it in days, not years. That asymmetry is what makes it dangerous.

In May 2025, Metronome experienced four separate outages from this exact ceiling during a data migration, requiring hours of emergency vacuuming on a 30TB cluster.

PostgreSQL 19 widens MultiXact members to 64 bits. The ceiling is gone. Not raised, eliminated.

My verdict: the "vacuum or die" emergency for member exhaustion is over. The aggressive anti-wraparound vacuum that ran specifically against MultiXact member space is no longer needed, though routine vacuum still matters for disk reclamation. One operational note: upgrading to 19 rewrites the pg_multixact SLRU files via pg_upgrade automatically, so size the upgrade window accordingly on clusters with large multixact histories.

Vacuum

Two long-standing complaints land together. Autovacuum can finally use parallel workers, and the pg_stat_* views expose enough state that diagnosing a slow vacuum no longer relies on guesswork.

Parallel autovacuum. autovacuum_max_parallel_workers enables parallel index processing for autovacuum runs. Per-table tuning lives in the new autovacuum_parallel_workers storage parameter. Manual VACUUM (PARALLEL N) has worked since 13; autovacuum has been single-threaded throughout. About time.

Autovacuum priority is now visible. pg_stat_autovacuum_scores exposes why a given table is queued ahead of another. A family of autovacuum_*_score_weight GUCs (covering freeze, vacuum, vacuum-insert, multixact-freeze, and analyze pressure) lets operators tune the priority function. Before 19, autovacuum picked tables in discovery order; tuning amounted to changing thresholds and hoping.

pg_stat_progress_vacuum gains two columns. started_by distinguishes manual / autovacuum / autovacuum_wraparound. mode distinguishes normal / aggressive / failsafe. Routine sweep or emergency, in one column.

Planned vs launched parallel workers logged. VACUUM (VERBOSE) output and autovacuum logs gain two numbers: how many parallel workers the operation planned, and how many it actually launched. The two diverge often. max_parallel_maintenance_workers, min_parallel_index_scan_size, the eligible-index count, and runtime worker availability all gate the count. Before 19, when launched fell below planned, no log evidence told you which gate closed. You were left guessing.

My verdict: faster autovacuum on heavily-indexed tables, and for once a feedback loop to verify it actually ran the way you expected. Parallel autovacuum gates on the same conditions as manual VACUUM (PARALLEL N) (eligible-index count, min_parallel_index_scan_size, available worker slots), so verify workers actually launch before assuming you got the speedup.

The investigation order changes too. The old loop (raise maintenance_work_mem, lower autovacuum_vacuum_cost_delay, hope) was guesswork because the inputs were not observable. The new sequence:

  1. Read pg_stat_autovacuum_scores to confirm priority.
  2. Check the autovacuum log for planned vs launched workers. If the gap is large, the bottleneck is gating, not throughput.
  3. Watch pg_stat_progress_vacuum.mode for failsafe events. A failsafe cluster needs priority weights tuned, not more workers.

Closing thoughts

pg_plan_advice is the one I'm most excited about, and it closes another long-standing gap with Oracle and SQL Server. It defines an in-core hook for injecting planner advice, which is the foundation a proper SQL Plan Management (SPM) module can be built on: automatic baseline capture, plan evolution, regression detection, the pieces Oracle SPM and SQL Server's Query Store users have taken for granted for years. PostgreSQL 19 doesn't ship SPM itself, but for the first time it ships the substrate that lets the community build one without forking the planner. That distinction is the whole point.

The rest of the list is the kind of steady, hard-won progress that keeps PostgreSQL ahead in production. Vacuum and MultiXact are really the same (debatable) architectural choice surfacing as two different problems: to implement MVCC, PostgreSQL keeps a separate version of every row an UPDATE touches, and a background process cleans them up. Until something like OrioleDB (a storage engine that implements MVCC via undo log instead of duplicating tuples) lands in core, we will keep duct-taping. So the open question for 19: is this the release where the duct tape finally starts to come off, or just a sturdier roll of it?

Further Readings

Back to blog

Explore the standard for database development