Build a disposable old/new PostgreSQL pair, run pg_upgrade --check, compare copy/link/clone/swap semantics and rollback boundaries, install new-major extension libraries first, preserve/refresh statistics correctly, and verify before deleting the old cluster.

pg_upgrade Workflows, Link/Clone Modes, Statistics, Analyze, and Rollback Planning

Build a disposable old/new PostgreSQL pair, run pg_upgrade --check, compare copy/link/clone/swap semantics and rollback boundaries, install new-major extension libraries first, preserve/refresh statistics correctly, and verify before deleting the old cluster.

Intermediate → Advanced180–240 minutesPostgreSQL upgrade and production change engineeringCurrent patched PostgreSQL 18.x target; resolve current minor from official release/version pages at change timeMajor-upgrade labs use two locally installed supported PostgreSQL majors when availableCore PostgreSQL mandatory tooling: pg_upgrade, pg_dump/pg_restore, psql, vacuumdbServiceHub disposable objects: app.ch23_* and servicehub_ch23_* databases/clustersAdmin/OS package access required for binary/cluster upgrade labs; application DDL lab uses normal database privilegesFree local tooling only; no managed service or commercial migrator requiredLast reviewed: August 2026

Learning outcomes

ServiceHub chooses pg_upgrade to move a large local cluster from PostgreSQL 17 to 18 without the time and temporary disk footprint of a full logical reload. The actual conversion can be fast, but the surrounding workflow—new cluster initialization, checksum compatibility, extension libraries, mode choice, statistics, rebuild scripts and rollback point—determines whether the change is safe.

01

Build a disposable old/new pair and run the new server's pg_upgrade --check before downtime.

02

Compare default copy, link, clone and PostgreSQL 18 swap modes by data ownership and rollback consequences.

03

Match initdb/checksum/extension/library prerequisites before running the real upgrade.

04

Understand PostgreSQL 18 optimizer-statistics transfer and the remaining post-upgrade ANALYZE work.

05

Write an explicit rollback matrix that never promises restart of a linked/swapped old cluster after it becomes unsafe.

Lab assumption

Use any two locally installed supported majors where the newer one is PostgreSQL 18. The examples show 17→18 and disposable ports 55471/55472. Set OLD_BIN and NEW_BIN to your actual versioned bin directories. The lab is free/open-source but cannot run unless both server versions are installed.

1. Prepare isolated directories and record tool ownership

shell · Unix-like lab variables
export OLD_BIN=/path/to/postgresql-17/binexport NEW_BIN=/path/to/postgresql-18/binexport OLD_DATA="$PWD/ch23_pg17"export NEW_DATA="$PWD/ch23_pg18"export OLD_PORT=55471export NEW_PORT=55472"$OLD_BIN/postgres" --version"$NEW_BIN/postgres" --version"$NEW_BIN/pg_upgrade" --version
powershell · Windows equivalent variables
$env:OLD_BIN = 'C:\Program Files\PostgreSQL\17\bin'$env:NEW_BIN = 'C:\Program Files\PostgreSQL\18\bin'$env:OLD_DATA = "$PWD\ch23_pg17"$env:NEW_DATA = "$PWD\ch23_pg18"$env:OLD_PORT = '55471'$env:NEW_PORT = '55472'& "$env:OLD_BIN\postgres.exe" --version& "$env:NEW_BIN\postgres.exe" --version& "$env:NEW_BIN\pg_upgrade.exe" --version

Always run the pg_upgrade binary from the new PostgreSQL installation. Keep versioned binaries side-by-side until validation and rollback windows have closed.

2. Initialize the old cluster and create representative data

shell · Unix-like old cluster
rm -rf "$OLD_DATA" "$NEW_DATA""$OLD_BIN/initdb" -D "$OLD_DATA""$OLD_BIN/pg_ctl" -D "$OLD_DATA" -o "-p $OLD_PORT" -l ch23-old.log start"$OLD_BIN/createdb" -p "$OLD_PORT" servicehub_ch23"$OLD_BIN/psql" -X -p "$OLD_PORT" -d servicehub_ch23 <<'SQL'CREATE TABLE work_order (  work_order_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,  status text NOT NULL,  amount numeric(12,2) NOT NULL);INSERT INTO work_order(status,amount)SELECT CASE WHEN g%2=0 THEN 'open' ELSE 'closed' END,       g/10.0FROM generate_series(1,50000) AS g;CREATE INDEX work_order_status_idx ON work_order(status);ANALYZE work_order;SQL

For a real cluster, add every extension/library, tablespace, full-text dictionary, collation and replication dependency to the inventory. This minimal lab focuses on the core upgrade mechanism.

3. Check checksum compatibility before initializing the new cluster

PostgreSQL 18 enables data checksums by default for newly initialized clusters. PostgreSQL 17 did not. pg_upgrade requires the old and new clusters' checksum settings to match.

shell · inspect old checksum state
"$OLD_BIN/pg_controldata" "$OLD_DATA" |  grep -i "Data page checksum version" || true

If the old lab says checksum version 0, initialize the PostgreSQL 18 target with --no-data-checksums. If the old cluster has checksums enabled, initialize the target compatibly instead of disabling them.

shell · initialize a compatible PG18 target
# For an old cluster WITHOUT checksums:"$NEW_BIN/initdb" --no-data-checksums -D "$NEW_DATA"# For an old cluster WITH checksums, omit --no-data-checksums# and verify pg_controldata after initdb.

4. Install new-major extension binaries before pg_upgrade

Before the real upgrade, install every native/contrib extension shared object for PostgreSQL 18. pg_upgrade will move catalog/schema state; you should not manually create duplicate extension definitions in the new cluster.

sql · old-cluster extension inventory
SELECT e.extname, e.extversion,       a.default_version AS packaged_defaultFROM pg_extension AS eLEFT JOIN pg_available_extensions AS a ON a.name=e.extnameORDER BY e.extname;

The lab's core objects need no optional extension. A production change must prove that every native library referenced by the old cluster has a compatible target-major package before the downtime window.

5. Run mode-specific pg_upgrade --check

shell · stop old cluster then check default copy mode
"$OLD_BIN/pg_ctl" -D "$OLD_DATA" stop -m fast"$NEW_BIN/pg_upgrade"   --check   --old-bindir="$OLD_BIN"   --new-bindir="$NEW_BIN"   --old-datadir="$OLD_DATA"   --new-datadir="$NEW_DATA"   --old-port="$OLD_PORT"   --new-port="$NEW_PORT" 
powershell · Windows equivalent compatibility check
& "$env:OLD_BIN\pg_ctl.exe" -D $env:OLD_DATA stop -m fast& "$env:NEW_BIN\pg_upgrade.exe" `  "--check" `  "--old-bindir=$env:OLD_BIN" `  "--new-bindir=$env:NEW_BIN" `  "--old-datadir=$env:OLD_DATA" `  "--new-datadir=$env:NEW_DATA" `  "--old-port=$env:OLD_PORT" `  "--new-port=$env:NEW_PORT"

On Windows, run pg_upgrade from the new installation under an account with the required administrative/file permissions, and keep quoted paths when PostgreSQL is installed under Program Files. The check still validates database compatibility; it does not substitute for a tested backup and rollback plan.

--check performs compatibility checks without upgrading. It can also be run while the old server is still running if old/new ports differ. If you intend to use --link, --clone, --copy-file-range, or --swap, repeat --check with that mode so filesystem-specific constraints are validated.

6. Choose the file-transfer mode by rollback semantics

Mode Mechanism Disk/filesystem Rollback boundary
copy (default) Copies user data files Needs new copy space Old cluster remains unchanged by pg_upgrade
link Hard-links old/new user data files Same filesystem for corresponding data After new cluster is started, old cluster is not safe to use
clone Filesystem reflink clone Supported filesystems only; same filesystem Old cluster stays independent after copy-on-write clone
swap (PG18) Moves/swaps data directories/files and replaces catalogs Same filesystem Can destructively modify old cluster once transfer begins

Clone availability is platform/filesystem-specific. Swap can be fastest for clusters with many relations, but once pg_upgrade reports the old cluster is unsafe, restoring the old cluster requires a backup—not optimism.

7. Perform the disposable upgrade in copy mode

shell · safe teaching-mode upgrade
"$NEW_BIN/pg_upgrade"   --old-bindir="$OLD_BIN"   --new-bindir="$NEW_BIN"   --old-datadir="$OLD_DATA"   --new-datadir="$NEW_DATA"   --old-port="$OLD_PORT"   --new-port="$NEW_PORT"   --jobs=2"$NEW_BIN/pg_ctl" -D "$NEW_DATA" -o "-p $NEW_PORT" -l ch23-new.log start

Expected final output includes compatibility success plus post-upgrade instructions/scripts. Do not invent the exact timing; object count, storage and mode dominate it.

8. PostgreSQL 18 preserves most optimizer statistics—but not all statistics

Unless --no-statistics is used, PostgreSQL 18 pg_upgrade transfers most ordinary optimizer statistics. It does not transfer everything: extended statistics created with CREATE STATISTICS, extension-defined statistics, and cumulative statistics still need regeneration/recollection.

shell · post-upgrade statistics sequence from current pg_upgrade guidance
"$NEW_BIN/vacuumdb" -p "$NEW_PORT"   --all --analyze-in-stages --missing-stats-only --jobs=2"$NEW_BIN/vacuumdb" -p "$NEW_PORT"   --all --analyze-only --jobs=2

Also run any rebuild/fixup scripts that pg_upgrade emits before using affected tables. PostgreSQL explicitly warns that tables named in rebuild scripts can produce wrong results or poor performance until those scripts finish.

9. Verify data, plans, extensions, collations and generated scripts

sql · new-cluster acceptance snapshot
SELECT version();SELECT count(*) AS rows,       sum(amount) AS amount_sum,       min(work_order_id) AS min_id,       max(work_order_id) AS max_idFROM work_order;SELECT extname, extversionFROM pg_extensionORDER BY extname;SELECT c.oid::regcollation,       c.collversion,       pg_collation_actual_version(c.oid) AS actual_versionFROM pg_collation AS cWHERE c.collversion IS DISTINCT FROM pg_collation_actual_version(c.oid)ORDER BY 1;EXPLAIN (ANALYZE, BUFFERS, SETTINGS, TIMING OFF)SELECT count(*) FROM work_order WHERE status='open';

Correct row counts do not prove application correctness; a fast plan does not prove all workload classes are fast. Keep the old cluster intact in copy/clone mode until the Chapter 23 validation gate is satisfied.

10. Wrong rollback promise: link mode after starting the new cluster

Deliberately wrong approach

“We used --link, so if PG18 is slow we can just stop it and start PG17.” Once the new cluster is started in --link mode, shared hard-linked data files can be modified in a way the old major does not understand: the old cluster is no longer safe to start. With --swap, the old cluster becomes destructively modified once file transfer begins and likewise is no longer a safe restart target. Roll back from a pre-upgrade backup/snapshot, or choose copy/clone when an independent old cluster is an explicit rollback requirement.

Observed point Old cluster restart expectation
--check only Old cluster unmodified
copy/clone completed Old cluster remains independent (subject to app-write cutover decisions)
link before links/new-cluster modification Follow pg_upgrade's explicit status; do not guess
link after new cluster starts Do not restart old cluster as rollback
swap after “old cluster no longer safe” notice Restore old from backup if rollback required

11. Cleanup only after acceptance

shell · disposable lab teardown
"$NEW_BIN/pg_ctl" -D "$NEW_DATA" stop -m fast# Only because this is disposable and validation is complete:rm -rf "$OLD_DATA" "$NEW_DATA"rm -f ch23-old.log ch23-new.log

In production, do not run the old-cluster deletion script until backups/restores, application checks, replication, scheduled jobs, performance canaries and rollback policy all say the old physical cluster is no longer needed.

Production judgment

pg_upgrade can make the physical conversion short; it does not make the whole change short. Budget time for extension packaging, checks, rebuild scripts, statistics, replica strategy, validation and rollback evidence. Select copy/link/clone/swap from recovery requirements, not from benchmark speed alone.

Check your understanding

  1. Why can PG17→PG18 initdb checksum defaults cause pg_upgrade --check to fail?
  2. Which pg_upgrade binary should run the upgrade?
  3. What is the key rollback difference between link and clone modes?
  4. What optimizer statistics does PostgreSQL 18 still not fully transfer?
  5. When is it safe to delete the old cluster?
Review the answers

Old/new checksum settings must match, while PG18 initdb defaults changed. Always run the new server's pg_upgrade. Link shares data files and makes the old cluster unsafe after new-cluster modification; clone creates copy-on-write independent files when supported. Extended/custom/cumulative statistics are not fully transferred. Delete the old cluster only after generated fixups, statistics, correctness, performance, backup/restore and rollback acceptance all pass.

Authoritative references

Upgrade, compatibility, locking, and migration behavior is version-sensitive. These PostgreSQL primary sources define the mechanisms used here; always read the exact source/target release notes during a real change.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.