Skip to main content
Postgres for Backend Engineers

Upgrades, Majors and Minors

Ravinder··6 min read
PostgresDatabaseSQLUpgradesOperations
Share:
Upgrades, Majors and Minors

Postgres has two categories of version change with entirely different risk profiles and operational procedures. Minor versions (14.1 → 14.12) fix bugs and security issues; they share the same on-disk format and can be applied by replacing the binaries and restarting. Major versions (14 → 15 → 16) can change the on-disk catalog format, the WAL format, and the behavior of existing features; they require a coordinated migration process. Conflating the two is how teams end up either skipping critical security patches or approaching a routine minor upgrade with unnecessary ceremony.

Minor Upgrades: Simple and Safe

Minor version upgrades are binary replacements. The data directory format does not change. The procedure on a single instance:

# 1. Verify the new package is available
apt list --upgradeable | grep postgresql-14
 
# 2. Install the new minor version
apt-get install --only-upgrade postgresql-14
 
# 3. Restart to activate (brief downtime)
pg_ctlcluster 14 main restart
 
# 4. Confirm version
psql -c 'SELECT version();'

For a streaming replication cluster, the standbys can be upgraded first (they accept any WAL the primary ships as long as the major version matches), then the primary is restarted. This achieves near-zero downtime: fail over to a standby, upgrade the old primary, promote back or leave the topology promoted.

sequenceDiagram participant PRI as Primary (14.8) participant S1 as Standby 1 (14.8) participant S2 as Standby 2 (14.8) Note over S1,S2: Step 1: Upgrade standbys (no downtime) S1->>S1: upgrade binary → 14.12, restart S2->>S2: upgrade binary → 14.12, restart Note over PRI: Step 2: Rolling restart primary PRI->>S1: failover (promote S1) PRI->>PRI: upgrade binary → 14.12, restart S1->>PRI: demote back to standby (optional)

Major Upgrades: Two Strategies

pg_upgrade

pg_upgrade upgrades the data directory in place (or via copy) by migrating the catalog and rewriting system tables while leaving user data files mostly unchanged. It is the standard tool for major upgrades when downtime is acceptable.

# Pre-flight: install both the old and new Postgres binaries
apt-get install postgresql-14 postgresql-16
 
# Stop the old cluster
pg_ctlcluster 14 main stop
 
# Run pg_upgrade (--link mode: hard-links data files, fastest, but old cluster unusable after)
pg_upgrade \
  --old-datadir /var/lib/postgresql/14/main \
  --new-datadir /var/lib/postgresql/16/main \
  --old-bindir /usr/lib/postgresql/14/bin \
  --new-bindir /usr/lib/postgresql/16/bin \
  --link
 
# Start the new cluster
pg_ctlcluster 16 main start
 
# Post-upgrade: run ANALYZE (statistics are not migrated)
/usr/lib/postgresql/16/bin/vacuumdb --all --analyze-in-stages
 
# Optionally: rebuild indexes (some may be invalidated across major versions)
# pg_upgrade generates a reindex_hash.sql script when needed

The --link flag hard-links data files rather than copying them—upgrade time is proportional to catalog size, not table size. The old cluster directory becomes unusable after a linked upgrade; keep a backup before starting.

pg_upgrade checklist:

  • Run pg_upgrade --check first (dry run, no changes made)
  • Extensions must be installed in the new cluster before pg_upgrade runs
  • Replication slots, logical replication subscriptions, and prepared transactions block pg_upgrade; drain or drop them first
  • After upgrade, run ANALYZE before opening to traffic—the planner starts with no statistics

Logical Replication Migration

For upgrades requiring near-zero downtime, set up logical replication from the old cluster to the new cluster. Once the new cluster has caught up, fail over the application.

-- On old cluster (source, e.g., Postgres 14)
-- wal_level must be logical
ALTER SYSTEM SET wal_level = 'logical';
-- Restart required
 
CREATE PUBLICATION upgrade_pub FOR ALL TABLES;
 
-- On new cluster (target, e.g., Postgres 16)
-- Apply all DDL first: pg_dump --schema-only | psql new_db
 
CREATE SUBSCRIPTION upgrade_sub
  CONNECTION 'host=old-db user=replicator dbname=app'
  PUBLICATION upgrade_pub;
 
-- Monitor replication lag
SELECT now() - pg_last_xact_replay_timestamp() AS lag;
 
-- When lag is near zero: cutover
-- 1. Put old cluster in read-only mode (or stop writes at app layer)
-- 2. Wait for subscriber to catch up (lag → 0)
-- 3. Drop subscription
-- 4. Switch application connection strings to new cluster

Logical replication migration does not replicate sequences—capture current sequence values and set them on the new cluster before or during cutover:

-- On old cluster: dump sequence values
SELECT 'SELECT setval(' || quote_literal(schemaname || '.' || sequencename)
  || ', ' || last_value || ');'
FROM pg_sequences;
 
-- Run the output on the new cluster after replication catches up

Pre-Upgrade Validation

-- Check for deprecated features that may break on the target version
-- pg_upgrade --check covers most cases, but also verify:
 
-- 1. Extensions: all must be available in the new version
SELECT name, default_version FROM pg_available_extensions;
 
-- 2. Functions with deprecated signatures (check release notes)
 
-- 3. Replication slots that must be dropped before pg_upgrade
SELECT slot_name, slot_type, active FROM pg_replication_slots;
 
-- 4. Prepared transactions (must be committed/rolled back)
SELECT gid FROM pg_prepared_xacts;
 
-- 5. pg_upgrade check mode
pg_upgrade --check \
  --old-datadir /var/lib/postgresql/14/main \
  --new-datadir /var/lib/postgresql/16/main \
  --old-bindir /usr/lib/postgresql/14/bin \
  --new-bindir /usr/lib/postgresql/16/bin

Post-Upgrade Steps

-- ANALYZE is mandatory after pg_upgrade (statistics not transferred)
-- Use analyze-in-stages for large databases: runs quickly first, then thoroughly
-- vacuumdb --all --analyze-in-stages (run as postgres OS user)
 
-- Rebuild any indexes that pg_upgrade flagged as needing rebuild
-- (Hash indexes before Postgres 10, some GIN/GiST across certain versions)
 
-- Update pg_hba.conf and postgresql.conf for new version defaults
-- Notable changes per version: check the official release notes
 
-- Re-enable autovacuum (sometimes disabled before upgrade for faster pg_upgrade)
ALTER SYSTEM RESET autovacuum;
SELECT pg_reload_conf();
 
-- Verify logical replication subscriptions (if any) are working
SELECT subname, subenabled, subslotname FROM pg_subscription;

Version Lifecycle

gantt title Postgres Major Version Support (approximate) dateFormat YYYY-MM axisFormat %Y section Active Postgres 15 : 2022-10, 2027-11 Postgres 16 : 2023-09, 2028-11 Postgres 17 : 2024-09, 2029-11 section Planning Postgres 18 : 2025-09, 2030-11

Postgres versions are supported for five years. Running an EOL version means no security patches—plan major upgrades before the version goes EOL, not after.

Key Takeaways

  • Minor upgrades are binary replacements with no on-disk format changes; upgrade standbys first, then do a rolling restart of the primary for near-zero downtime.
  • pg_upgrade --check is a dry run that catches most blocking issues before you commit to the upgrade; always run it first.
  • Drop replication slots and prepared transactions before pg_upgrade; they are hard blockers that the check mode will flag.
  • ANALYZE is mandatory after pg_upgrade—statistics are not transferred and the planner starts blind, which causes plan regressions on first traffic.
  • Logical replication migration enables near-zero downtime major upgrades but does not replicate sequences; capture and replay sequence values as part of the cutover runbook.
  • Plan major upgrades before the version reaches end-of-life (five years from release); unplanned upgrades under pressure produce incidents.
Share: