PG18 Hacktober: 31 Days of New Features Unveiling pg_upgrade –swap option

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 --swappg_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.

Author

+ posts

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top