PostgreSQLXVII · Capacity, Maintenance and UpgradesMaintenance
pg_upgrade in practice
What you'll learn
- Run pg_upgrade end to end with the correct sequence
- Use --check as a pre-flight days in advance
- Know what survives the upgrade and what does not
- Complete the post-upgrade steps that are not optional
Prerequisites
Practice
Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27
Everything below was performed on a cluster running PostgreSQL 17.11, upgraded to 18.6, with both sets of binaries installed on one host.
What must exist first
Both major versions’ binaries, on the same machine. pg_upgrade
runs the old server and the new one. On Debian that means
postgresql-17 and postgresql-18 installed together.
A new, initialised, empty cluster. initdb for the new version,
with the same locale and encoding as the old, or the upgrade
refuses.
Every extension installed for the new version, before you start.
A verified backup. Part XIII.
--check first, days in advance
$ pg_upgrade --old-datadir=/var/lib/postgresql/17data \
--new-datadir=/var/lib/postgresql/18data \
--old-bindir=/usr/lib/postgresql/17/bin \
--new-bindir=/usr/lib/postgresql/18/bin --checkChecking for objects affected by Unicode update ok
Checking for not-null constraint inconsistencies ok
Checking for presence of required libraries ok
Checking database user is the install user ok
Checking for prepared transactions ok
Checking for new cluster tablespace directories ok
*Clusters are compatible*--check ran against a live old cluster. That is what makes it
usable as a pre-flight days or weeks ahead, and it is the step that
finds the problems you do not want to find during the window — missing
extension libraries above all.
The real run refuses a running cluster
$ pg_upgrade --old-datadir=... --new-datadir=... --old-bindir=... --new-bindir=...There seems to be a postmaster servicing the old cluster.
Please shutdown that postmaster and try again.
Failure, exitingStop it cleanly — pg_ctl stop -m fast — and confirm.
The upgrade
$ pg_upgrade --old-datadir=... --new-datadir=... --old-bindir=... --new-bindir=...Setting next transaction ID and epoch for new cluster ok
Resetting WAL archives ok
Setting frozenxid and minmxid counters in new cluster ok
Restoring global objects in the new cluster ok
Restoring database schemas in the new cluster ok
Copying user relation files ok
Setting next OID for new cluster ok
Sync data directory to disk ok
Creating script to delete old cluster ok
Checking for extension updates ok
Upgrade Complete
----------------
Some statistics are not transferred by pg_upgrade.
Once you start the new server, consider running these two commands:
/usr/lib/postgresql/18/bin/vacuumdb --all --analyze-in-stages --missing-stats-only
/usr/lib/postgresql/18/bin/vacuumdb --all --analyze-only
Running this script will delete the old cluster's data files:
./delete_old_cluster.sh
ELAPSED: 2 secondsNote Resetting WAL archives. The new cluster starts a fresh WAL
sequence, which means your archive and every standby are now
irrelevant to it. Standbys must be rebuilt, and archiving reconfigured
— which lesson XVII-08 puts in the post-upgrade checklist.
What survived, measured
$ SELECT tablename, attname, n_distinct, null_frac, correlation FROM pg_stats WHERE tablename='orders';--- BEFORE, on 17.11 ---
tablename | attname | n_distinct | null_frac | correlation
-----------+----------+------------+-----------+---------------
orders | amount | -0.20237 | 0 | -0.0049658697
orders | customer | 9980 | 0 | 0.008761019
--- AFTER, on 18.6 ---
orders | amount | -0.20237 | 0 | -0.0049658697
orders | customer | 9980 | 0 | 0.008761019Identical to the last digit. The planner statistics crossed the major version boundary.
$ SELECT relname, last_analyze, last_autoanalyze, n_live_tup FROM pg_stat_user_tables WHERE relname='orders'; relname | last_analyze | last_autoanalyze | n_live_tup
---------+--------------+------------------+------------
orders | | | 0Nothing. pg_stat_user_tables starts from zero, which means
autovacuum’s thresholds start from zero too.
After the upgrade
Not optional, in this order:
# 1. Start the new cluster and confirm what it is
psql -c "SELECT version();"
# 2. Fill in the statistics that did not transfer
vacuumdb --all --analyze-in-stages --missing-stats-only
vacuumdb --all --analyze-only
# 3. Verify data
psql -d appdb -c "SELECT count(*) FROM orders;"
# 4. Rebuild standbys — the WAL sequence was reset
# 5. Reconfigure archiving to a new location
# 6. Check extensions
psql -c "SELECT extname, extversion FROM pg_extension;"
# 7. Update monitoring for changed views (lesson XVI-03)
What to take from this
- Both versions’ binaries, an empty new cluster with matching locale, extensions installed, and a verified backup.
--checkruns against a live old cluster. Use it days ahead.- The real run refuses a running old cluster.
- Measured: 2 seconds for 78 MB in copy mode. Mechanism, not a projection.
- Planner statistics survived identically; cumulative statistics did not. 17 said “Optimizer statistics are not transferred”; 18 says “Some”.
Resetting WAL archivesmeans standbys must be rebuilt and archiving reconfigured.- Run
vacuumdb --analyze-in-stages --missing-stats-onlyafterwards. - Keep
delete_old_cluster.shunrun for days, and take a new base backup as soon as you have verified.
Cross-course references
- Linux for Production Sysadmins — Part XI (Package management)
covers having two majors installed at once, which
pg_upgraderequires and some packagings make awkward. - Ansible for Production Sysadmins — Part XXIV (Assertions and
guardrails) covers refusing to proceed when a precondition is unmet,
which is what
--checkis doing.
Quiz
Knowledge check · 6 questions
Q1. After a pg_upgrade to 18, pg_stats shows the same n_distinct and correlation values as before, but pg_stat_user_tables shows n_live_tup of zero and no last_analyze. What is going on?
Q2. Why must standbys be rebuilt after a pg_upgrade rather than simply repointed at the new cluster?
Q3. Which pg_upgrade step is where failures almost always occur, and why?
Q4. Which must be true before running pg_upgrade? Select all that apply.
Q5. pg_upgrade --check can be run against a live old cluster without downtime.
Q6. Why should a new base backup be taken immediately after a verified pg_upgrade?
Passing score: 75%. Answers are checked in this browser.