On September 25, 2025, PostgreSQL—the world’s most advanced open-source database—hit a new milestone. Version 18 is here, and it’s packed with powerful upgrades: smarter performance, a more intuitive developer experience, groundbreaking SQL/JSON capabilities, and dozens of enhancements that push the boundaries of what Postgres can do.
At OpenSource DB, we’re celebrating this release with a full month of content. Starting October 1st, we’re launching “PG18 Hacktober: 31 Days of New Features” 31 straight days of daily blog posts exploring the newest features in PostgreSQL 18. Each post will deliver deep dives, hands-on examples, and practical insights for developers, DBAs, architects, and all Postgres enthusiasts.
Follow along and level up your PostgreSQL skills, one day, one feature at a time.
To kick off this series, we’re starting exactly where any PostgreSQL 18 journey begins: with the UPGRADE. After all, before you can take advantage of the new features, you need to get your database to PG18, and thankfully, this version makes that easier (and faster) than ever.
In today’s post, we’ll take a deep dive into the new pg_upgrade
--swap
capability, the enhancement that enables near-instant statistics collection by copying them over. This approach dramatically reduces upgrade time and downtime, especially for large databases.
If you’re planning your move to PostgreSQL 18, this is the upgrade path you’ll want to know about.
pg_upgrade –swap: What does the documentation say
Move the data directories from the old cluster to the new cluster. Then, replace the catalog files with those generated for the new cluster. This mode can outperform –link, –clone, –copy, and –copy-file-range, especially on clusters with many relations.
However, this mode creates many garbage files in the old cluster, which can prolong the file synchronization step if –sync-method=syncfs is used. Therefore, it is recommended to use –sync-method=fsync with –swap.
Additionally, once the file transfer step begins, the old cluster will be destructively modified and therefore will no longer be safe to start.
Let’s run the pg_upgrade
using both the --link
and --swap
and check the time difference.For example, I’m taking a small test by creating a database test_db
with 100 tables and each consists of 1000 rows.
- PG17 Bin directory :
/usr/pgsql-17/bin/
- PG18 Bin directory :
/usr/pgsql-18/bin/
- PG17 Data_directory for –link :
/var/lib/pgsql/17/data_link
- PG18 Data_directory for –link :
/var/lib/pgsql/18/data_link
- PG17 Data_directory for –swap :
/var/lib/pgsql/17/data_swap
- PG18 Data_directory for –swap :
/var/lib/pgsql/18/data_swap
–link
We are checking the time taken.
[postgres@ip-172-31-11-103 ~]$ /usr/pgsql-18/bin/pg_upgrade --old-datadir /var/lib/pgsql/17/data_link --new-datadir /var/lib/pgsql/18/data_link --old-bindir /usr/pgsql-17/bin/ --new-bindir /usr/pgsql-18/bin/ --check
Performing Consistency Checks
-----------------------------
Checking cluster versions ok
Checking database connection settings ok
Checking database user is the install user ok
Checking for prepared transactions ok
Checking for contrib/isn with bigint-passing mismatch ok
Checking for valid logical replication slots ok
Checking for subscription state ok
Checking data type usage ok
Checking 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*
[postgres@ip-172-31-11-103 ~]$ time /usr/pgsql-18/bin/pg_upgrade --old-datadir /var/lib/pgsql/17/data_link --new-datadir /var/lib/pgsql/18/data_link --old-b
indir /usr/pgsql-17/bin/ --new-bindir /usr/pgsql-18/bin/ --link
Performing Consistency Checks
-----------------------------
Checking cluster versions ok
Checking database connection settings ok
Checking database user is the install user ok
Checking for prepared transactions ok
Checking for contrib/isn with bigint-passing mismatch ok
Checking for valid logical replication slots ok
Checking for subscription state ok
Checking data type usage ok
Checking for objects affected by Unicode update ok
Checking for not-null constraint inconsistencies ok
Creating dump of global objects ok
Creating dump of database schemas
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
If pg_upgrade fails after this point, you must re-initdb the
new cluster before continuing.
Performing Upgrade
------------------
Setting locale and encoding for new cluster ok
Analyzing all rows in the new cluster ok
Freezing all rows in the new cluster ok
Deleting files from new pg_xact ok
Copying old pg_xact to new server ok
Setting oldest XID for new cluster ok
Setting next transaction ID and epoch for new cluster ok
Deleting files from new pg_multixact/offsets ok
Copying old pg_multixact/offsets to new server ok
Deleting files from new pg_multixact/members ok
Copying old pg_multixact/members to new server ok
Setting next multixact ID and offset 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
Adding ".old" suffix to old "global/pg_control" ok
If you want to start the old cluster, you will need to remove
the ".old" suffix from "/var/lib/pgsql/17/data_link/global/pg_control.old".
Because "link" mode was used, the old cluster cannot be safely
started once the new cluster has been started.
Linking 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/pgsql-18/bin/vacuumdb --all --analyze-in-stages --missing-stats-only
/usr/pgsql-18/bin/vacuumdb --all --analyze-only
Running this script will delete the old cluster's data files:
./delete_old_cluster.sh
real 0m2.417s
user 0m0.162s
sys 0m0.254s
It took 2.417s
when we use --link
–swap
[postgres@ip-172-31-11-103 ~]$ /usr/pgsql-18/bin/pg_upgrade --old-datadir /var/lib/pgsql/17/data_swap --new-datadir /var/lib/pgsql/18/data_swap --old-bindir /usr/pgsql-17/bin/ --new-bindir /usr/pgsql-18/bin/ --check
Performing Consistency Checks on Old Live Server
------------------------------------------------
Checking cluster versions ok
Checking database connection settings ok
Checking database user is the install user ok
Checking for prepared transactions ok
Checking for contrib/isn with bigint-passing mismatch ok
Checking for valid logical replication slots ok
Checking for subscription state ok
Checking data type usage ok
Checking 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*
[postgres@ip-172-31-11-103 ~]$ time /usr/pgsql-18/bin/pg_upgrade --old-datadir /var/lib/pgsql/17/data_swap --new-datadir /var/lib/pgsql/18/data_swap --old-bindir /usr/pgsql-17/bin/ --new-bindir /usr/pgsql-18/bin/ --swap
Performing Consistency Checks
-----------------------------
Checking cluster versions ok
Checking database connection settings ok
Checking database user is the install user ok
Checking for prepared transactions ok
Checking for contrib/isn with bigint-passing mismatch ok
Checking for valid logical replication slots ok
Checking for subscription state ok
Checking data type usage ok
Checking for objects affected by Unicode update ok
Checking for not-null constraint inconsistencies ok
Creating dump of global objects ok
Creating dump of database schemas
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
If pg_upgrade fails after this point, you must re-initdb the
new cluster before continuing.
Performing Upgrade
------------------
Setting locale and encoding for new cluster ok
Analyzing all rows in the new cluster ok
Freezing all rows in the new cluster ok
Deleting files from new pg_xact ok
Copying old pg_xact to new server ok
Setting oldest XID for new cluster ok
Setting next transaction ID and epoch for new cluster ok
Deleting files from new pg_multixact/offsets ok
Copying old pg_multixact/offsets to new server ok
Deleting files from new pg_multixact/members ok
Copying old pg_multixact/members to new server ok
Setting next multixact ID and offset 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
Adding ".old" suffix to old "global/pg_control" ok
Because "swap" mode was used, the old cluster can no longer be
safely started.
Swapping data directories
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/pgsql-18/bin/vacuumdb --all --analyze-in-stages --missing-stats-only
/usr/pgsql-18/bin/vacuumdb --all --analyze-only
Running this script will delete the old cluster's data files:
./delete_old_cluster.sh
real 0m2.402s
user 0m0.162s
sys 0m0.246s
I took 2.402s
when we use –swap .
Let’s observe the output of pg_upgrade while using both the options
–link
When --link
is used, pg_upgrade
creates hard links from the old cluster’s data files to the new cluster’s directory instead of copying them.To protect the integrity of the upgrade, pg_upgrade
renames the old cluster’s pg_control
file by appending .old
to it during the upgrade.If you decide to roll back or start the old cluster before starting the new cluster, you can restore the old cluster by removing the .old
suffix from the renamed pg_control
file.
If you want to start the old cluster, you will need to remove
the ".old" suffix from "/var/lib/pgsql/17/data_link/global/pg_control.old".
Because "link" mode was used, the old cluster cannot be safely
started once the new cluster has been started.
Let’s check the possibility of restarting the old cluster when we used --link
#Stopping the PG18 instance
[postgres@ip-172-31-11-103 data_link]$ /usr/pgsql-18/bin/pg_ctl -D /var/lib/pgsql/18/data_link stop
waiting for server to shut down.... done
server stopped
#Checknig the pg_control file in the PG17 data_directory
[postgres@ip-172-31-11-103]$ cd /var/lib/pgsql/17/data_link/global/
[postgres@ip-172-31-11-103 global]$ ls -ltr|grep pg_control.old
-rw-------. 1 postgres postgres 8192 Oct 1 08:28 pg_control.old
#Change the .old file back to original file
[postgres@ip-172-31-11-103 global]$ mv pg_control.old pg_control
[postgres@ip-172-31-11-103 global]$ ls -ltr|grep pg_control
-rw-------. 1 postgres postgres 8192 Oct 1 09:41 pg_control
#Restart the PG17 , changed the port to 5435
[postgres@ip-172-31-11-103 global]$ cd /var/lib/pgsql/17/data_link
[postgres@ip-172-31-11-103 data_link]$ vi postgresql.conf
[postgres@ip-172-31-11-103 data_link]$ /usr/pgsql-17/bin/pg_ctl -D /var/lib/pgsql/17/data_link start
waiting for server to start....2025-10-01 09:43:17.720 UTC [8649] LOG: redirecting log output to logging collector process
2025-10-01 09:43:17.720 UTC [8649] HINT: Future log output will appear in directory "log".
done
server started
#Try connecting to the database
[postgres@ip-172-31-11-103 data_link]$ psql -p 5435
psql (18.0, server 17.6)
Type "help" for help.
postgres=#
When we used –link option, we can retrieve the old PG17 instances by renaming the pg_control file.
–swap
With --swap
, pg_upgrade
moves (renames) all files completely from the old cluster directory to the new cluster directory.The upgrade then replaces important catalog files (like pg_control
) in the new cluster with fresh ones generated for the upgraded PostgreSQL version.The old cluster directory is left in a changed state, and the original catalog files are moved or renamed (pg_control.old
), making it irreversibly unusable.
Because "swap" mode was used, the old cluster can no longer be
safely started.
Let’s check the possibility of restarting the old cluster when we used --swap
#Stopping the PG18 instance
[postgres@ip-172-31-11-103 data_link]$ /usr/pgsql-18/bin/pg_ctl -D /var/lib/pgsql/18/data_swap stop
waiting for server to shut down.... done
server stopped
#Checknig the pg_control file in the PG17 data_directory
[postgres@ip-172-31-11-103]$ cd /var/lib/pgsql/17/data_swap/global/
[postgres@ip-172-31-11-103 global]$ ls -ltr|grep pg_control.old
-rw-------. 1 postgres postgres 8192 Oct 1 08:29 pg_control.old
#Change the .old file back to original file
[postgres@ip-172-31-11-103 global]$ mv pg_control.old pg_control
[postgres@ip-172-31-11-103 global]$ ls -ltr|grep pg_control
-rw-------. 1 postgres postgres 8192 Oct 1 09:44 pg_control
#Restart the PG17 , changed the port to 5433
[postgres@ip-172-31-11-103 global]$ cd /var/lib/pgsql/17/data_link
[postgres@ip-172-31-11-103 data_link]$ vi postgresql.conf
[postgres@ip-172-31-11-103 global]$ /usr/pgsql-17/bin/pg_ctl -D /var/lib/pgsql/17/data_swap start
waiting for server to start....2025-10-01 09:45:09.180 UTC [8677] LOG: redirecting log output to logging collector process
2025-10-01 09:45:09.180 UTC [8677] HINT: Future log output will appear in directory "log".
done
server started
#Try connecting to the databas
[postgres@ip-172-31-11-103 global]$ psql -p 5433
psql: error: connection to server on socket "/run/postgresql/.s.PGSQL.5433" failed: FATAL: database "postgres" does not exist
DETAIL: The database subdirectory "base/5" is missing.
[postgres@ip-172-31-11-103 global]$
This error indicates that the system catalog folder for the database with OID 5 (which corresponds to the default “postgres” database) is missing or corrupted in the data directory of the PostgreSQL server running on port 5433 because --swap
option will move the catalog files to the PG18 in pg_upgrade process.
When to Use pg_upgrade --swap
We should consider using the --swap
option with pg_upgrade
in the following situations:
- Very large clusters – especially those containing thousands (or tens of thousands) of tables, indexes, or very large databases.
- Efficient filesystem support – when your operating system can move directories quickly (common in most Linux/UNIX filesystems).
- No rollback required – if you don’t plan to revert to the old data directory and prefer automatic cleanup after the upgrade.
- Minimal downtime is critical – when your goal is to reduce I/O and achieve the fastest possible upgrade window.
Conclusion
PostgreSQL 18’s new --swap
option for pg_upgrade changes the game for major version upgrades. Instead of copying or hard-linking files, it simply moves the data directories, making the whole process significantly faster, especially for large clusters.
That said, it’s not without trade-offs. Once a swap begins, there’s no going back, the old cluster becomes unusable. So, you’ll want to have solid backups in place before pulling the trigger. If you need rollback flexibility, the older --link
option is still a great choice. It’s slightly slower, but it lets you fall back to the old cluster if needed, as long as you’re on the same filesystem.
TL;DR
Pick --swap
when speed is critical and you’re confident in your backup plan. Stick with --link
if you prefer an old school approach.
Stay with us as we continue our deep dive into PostgreSQL 18 all month long. Each day, we’ll unpack new features designed to help DBAs and developers build faster, smarter, and more resilient database systems.
Up next: a powerful enhancement that’s changing the way developers work with Postgres, you won’t want to miss it.