Upgrading to a new major version
Choose an upgrade path:
- In-place major version upgrade (IPMVU): Upgrade the existing deployment and retain its connection strings.
- Upgrade a read-only replica: Replicate data to a separate deployment, then promote and upgrade it.
- Restore from backup: Create a deployment on a supported target version from an existing backup.
For the last two paths, the new deployment does not receive changes that reach the source after the promotion or after the backup. Rehearse and validate the new version first. Before the final switch, stop writes to the source. Then, promote the replica or take the backup, and switch your application's connection details to the new deployment.
Find versions available for new deployments in the catalog, with ibmcloud cdb deployables-show,
or through the /deployables API.
In API examples, replace {region} with the deployment's region, {id} with the deployment's URL-encoded Cloud Resource Name (CRN), and <IAM_TOKEN> with your Identity and Access Management (IAM) access
token.
Requirements for upgrading
Application compatibility
Test your chosen upgrade path on a representative staging deployment before production. For an IPMVU rehearsal, restore a recent backup to the source PostgreSQL version. A deployment has two or three data members: the primary and one or two high-availability (HA) members. Scale the restored deployment to the same number of members as production, because the conversion phase takes longer with more HA members. Make sure that the restored deployment has a completed backup, and then upgrade it to your intended target. Check application queries, background jobs, drivers, extensions, and connection pools. Record performance, and how long your applications see read-only errors and connection failures, for comparison afterward.
Review the release notes for every major version after your source version, up to and including your target version. Also review the role privilege changes.
The upgrade does not keep commit timestamps. After the upgrade, pg_xact_commit_timestamp() returns NULL for every transaction that committed before the upgrade. If your applications read commit timestamps, copy the
values that you need into a table column before you upgrade.
Extensions and other objects
Complete the applicable preparation in the following sections before upgrading. For an IPMVU, prepare the deployment that you upgrade. For the read-only replica and restore paths, prepare the source deployment before you create the replica or take the backup. You cannot change a read-only replica, and a restore upgrades the data in the same operation. Plan for any application dependencies on objects that you remove.
Extensions
pg_repack
pg_repack does not block an upgrade. You do not need to drop or re-create it before or after the upgrade.
old_snapshot
The old_snapshot module is not available in PostgreSQL 17 and later. When you upgrade from PostgreSQL 16 or earlier to PostgreSQL
17 or later, IPMVU removes old_snapshot from each database before read-only mode begins. If any object depends on old_snapshot, the upgrade fails. The removal is not reversed if the upgrade fails later.
If old_snapshot is installed in any database, contact support to identify dependent objects before you upgrade. For the other upgrade paths, contact support to arrange its removal.
anon
If anon version 2 is installed, complete these steps in each database where it is installed. Run them as the owner of the masked tables, such as admin. For an IPMVU, anon version 2 makes the upgrade
fail during the conversion phase, while the database is unavailable, and every retry fails the same way. anon version 3 does not need these steps. Self-check query 10 shows the installed version in extversion.
Before you remove the masking rules, block database access for the masked roles, which must see only masked data. Keep their access blocked until you restore and verify the masking rules and role labels after the upgrade.
-
Record the masking rules and masked roles so that you can restore them after the upgrade.
SELECT objtype, objname, label FROM pg_catalog.pg_seclabels WHERE provider = 'anon'; -
Remove all masking rules.
SELECT anon.remove_masks_for_all_columns(); -
Remove the masked label from each role listed in step 1. On PostgreSQL 16 and later, this action requires the
ADMINoption on the role. For more information, see Role privilege issues during version upgrades.SECURITY LABEL FOR anon ON ROLE <role_name> IS NULL; -
Drop the
anonextension with the cascade option. The cascade option also drops objects that depend onanon, such as masking views and functions. Record their definitions so that you can recreate them after the upgrade.DROP EXTENSION anon CASCADE; -
After the upgrade, re-enable
anon, reapply the recorded masking rules and role labels, and verify masking before you restore access for the masked roles.
PostGIS
If you use PostGIS, upgrade it in each database that uses it before you upgrade PostgreSQL:
SELECT postgis_extensions_upgrade();
Verify the extension version:
SELECT postgis_full_version();
earthdistance
An IPMVU fails during the conversion phase if an index or a stored generated column uses a function of earthdistance version 1.1, such as ll_to_earth(). This also applies when the object uses it through your own
SQL function. PostgreSQL 14 and 15 provide only version 1.1. Check with self-check query 13 in each database, and then handle each listed object:
-
On PostgreSQL 16 or 17, update the extension before you upgrade:
ALTER EXTENSION earthdistance UPDATE; -
On PostgreSQL 14 or 15, if
used_functionis one of your own functions, add asearch_pathsetting to it:ALTER FUNCTION <used_function> SET search_path = ibm_extension, pg_catalog; -
On PostgreSQL 14 or 15, if
used_functionis anearthdistancefunction, drop the listed index before you upgrade. To keep the values of a listed generated column, convert it to a regular column withALTER TABLE <table_name> ALTER COLUMN <column_name> DROP EXPRESSION;. After the upgrade, runALTER EXTENSION earthdistance UPDATE;, and then re-create what you removed. If your target is PostgreSQL 15, the update has no effect, and the objects that you re-create block your next upgrade in the same way. To avoid this, upgrade to PostgreSQL 16 or later.
pgRouting
If your deployment runs PostgreSQL 16 or 17 and you upgrade to PostgreSQL 18, check each database where pgRouting is installed. PostgreSQL 18 deployments do not include the pgRouting 3.5 library. An IPMVU to PostgreSQL
18 fails its compatibility checks, before read-only mode begins, while any function uses that library. Expect no rows:
SELECT DISTINCT p.probin
FROM pg_catalog.pg_proc AS p
WHERE p.probin = '$libdir/libpgrouting-3.5';
If the query returns a row, update pgRouting to version 4.0.1 before you upgrade, and then run the query again:
ALTER EXTENSION pgrouting UPDATE TO '4.0.1';
pgRouting 4.0 removes several functions, such as pgr_createTopology, pgr_analyzeGraph, and pgr_nodeNetwork, and changes others. Test your applications with version 4.0.1 on a staging
deployment first. If they need the removed functions, contact support before you upgrade to PostgreSQL 18. From PostgreSQL 16, you can also upgrade to PostgreSQL 17, which includes the 3.5 library.
SQL functions in indexes, partition keys, and generated columns
During the conversion phase, an IPMVU re-creates the definition of every index, partition key, and stored generated column with an empty search path. This step fails if one of these objects uses a LANGUAGE sql function with
a string body that names an object outside pg_catalog without its schema, directly or through a domain check constraint. Extended statistics have the same effect on a table that has a foreign key. In an index, a call that
names the ibm_extension schema, where extensions are installed, also fails. The failure happens while the database is unavailable, and every retry fails the same way.
Run self-check query 12 in each database. For each listed function, add a search_path setting that names every schema that its body uses. Alternatively, replace its string body with
an SQL-standard body. Run these statements as the function's owner. For example:
ALTER FUNCTION f_unaccent(text) SET search_path = ibm_extension, pg_catalog;
CREATE OR REPLACE FUNCTION f_unaccent(text) RETURNS text
LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT
RETURN ibm_extension.unaccent('ibm_extension.unaccent', $1);
Keep each function's results and other attributes unchanged, because indexes and generated columns store its results, and partitioned tables route rows by them. With a search_path setting, PostgreSQL no longer expands the function
into the queries that call it, so those queries can be slower. An SQL-standard body does not protect the string-body functions that it calls, so fix those functions too. Run the query again until it returns no rows. If it lists a function
that you do not own, contact support before you upgrade.
If you upgrade from PostgreSQL 16 or earlier to PostgreSQL 17 or later, also run self-check query 17. PostgreSQL 17 and later run maintenance commands with a restricted search path. A function
in any language that an index, a materialized view, or extended statistics use then needs its own search_path setting. Otherwise, after the upgrade, ANALYZE, REINDEX, CLUSTER, VACUUM FULL,
and REFRESH MATERIALIZED VIEW fail for the affected objects. Set the search path of each listed function that names objects without their schema, as in the previous example. For more information, see the PostgreSQL 17 migration notes.
Objects that use removed system names
Each major version removes or renames some system catalog columns, functions, and configuration parameters. For example, PostgreSQL 17 removed the checkpoint columns from pg_stat_bgwriter, and PostgreSQL 16 renamed the force_parallel_mode parameter to debug_parallel_query. The prechecks do not detect objects that use these names:
- If a view, a materialized view, or an aggregate uses a removed name, an IPMVU fails during the conversion phase. A function with an SQL-standard body and a function, role, or database setting of a removed parameter have the same effect.
- Other functions and procedures,
pg_cronjobs, and application queries that use a removed name fail when they first run on the new version. This can occur before writes resume.
Run self-check query 14 in each database. Change or drop each listed object whose removed_in version is at or below your target version. Check role and database settings with self-check
query 7. Test your application and monitoring queries on a staging deployment of the target version.
Logical replication slots
IPMVU fails its prechecks while any logical replication slot exists, including inactive slots and slots that wal2json uses. You cannot recover a deleted slot, and a new slot does not include the changes that the old slot retained.
Before you upgrade:
-
Record each slot's name, plug-in, and database with self-check query 2.
-
Pause source writes until you restore consumers after the upgrade, or plan to resynchronize downstream data.
-
Let consumers process pending changes, and then stop them. Keep them stopped, with automatic reconnection turned off, until the task shows Completed.
-
Delete each slot with the
ibmcloud cdb postgresql replication-slot-deletecommand or the Delete a logical replication slot API. Theadminuser cannot drop replication slots with SQL.ibmcloud cdb postgresql replication-slot-delete <NAME|CRN> <SLOT_NAME> -
After the upgrade, recreate the slots as described in Configuring
wal2json.
wal2json is a logical decoding output plug-in, not an extension that you create with CREATE EXTENSION.
Logical subscriptions
A logical subscription receives changes from a publisher. Its replication slot is on the publisher, so the slot query does not list it.
If your deployment runs PostgreSQL 16 or earlier, IPMVU fails its prechecks while any logical subscription exists, including disabled subscriptions. Upgrades from these versions do not preserve a subscription's synchronization state. Before
you upgrade, agree with the publisher's owner how to remove and recreate each subscription, and how to resynchronize the data. Then, delete the subscriptions with the subscriber functions.
The delete_subscription function also drops the subscription's slot on the publisher, and it fails if the publisher is unreachable or the slot no longer exists. In that case, run disable_subscription and then
subscription_slot_none before delete_subscription. Then, ask the publisher's owner to drop the slot, because it keeps retaining transaction logs on the publisher.
If your deployment runs PostgreSQL 17 or later, subscriptions can remain, but they must meet the target version's logical replication upgrade requirements.
The upgrade supports at most 10 subscriptions. With more, it fails its compatibility checks before read-only mode begins. Disable the subscriptions with disable_subscription before you upgrade, and enable them with enable_subscription after the upgrade completes.
UNLOGGED tables and sequences
PostgreSQL does not write changes to UNLOGGED tables and sequences to the transaction log. It does not replicate them to other members, and it resets them after a failover or a restore. None of the upgrade paths keeps their
contents:
- An IPMVU resets them when it restarts PostgreSQL on the new version, and again when it switches the primary role in the completion phase. A failed upgrade can also reset them.
- A promoted read-only replica and a restored backup start with empty
UNLOGGEDtables and resetUNLOGGEDsequences.
After the upgrade, UNLOGGED tables are empty and UNLOGGED sequences are reset. Until the task shows Completed, any changes that you make to these objects can also be lost. Before you upgrade, use
self-check query 11 to identify these objects in each database and plan how to handle each one.
- To keep the contents of a table, convert it with
ALTER TABLE <table_name> SET LOGGED;.SET LOGGEDalso converts the table's indexes and the sequences that it owns. Convert a referenced table before converting any tables that reference it through foreign keys. Alternatively, export the contents and reload them after the task shows Completed. - If a sequence generates values for a logged table, convert it with
ALTER SEQUENCE <sequence_name> SET LOGGED;. Otherwise, stop the applications that use it until the task shows Completed. Then, set it past the highest value in use with thesetvalfunction.
SET LOGGED rewrites the table and blocks reads and writes until the operation finishes. It also generates approximately as much transaction log data as the table and its indexes occupy. Convert large tables well before you submit
the upgrade, and use self-check query 5 to verify that the transaction logs were archived. After the upgrade, you can convert a table back with ALTER TABLE <table_name> SET UNLOGGED;.
For an IPMVU, an UNLOGGED table, even an empty one, also makes the service rebuild every HA member, which extends the time that writes are blocked. For more information, see Availability during an upgrade.
To avoid this rebuild, convert every UNLOGGED table, or drop the ones that you no longer need. TRUNCATE does not avoid the rebuild. UNLOGGED sequences do not cause a rebuild. On PostgreSQL 16 and
17, SET LOGGED is itself a bulk operation that can still cause a rebuild, so plan for one.
In-place major version upgrade
Complete preparation, then use the UI, API, CLI, or Terraform procedure below.
In an IPMVU, the service upgrades the primary in place and upgrades each HA member from its existing data files. When all HA members can reuse their data files, writes are blocked only briefly. The duration depends primarily on the number of database objects and the level of database activity, rather than on the amount of data stored.
Some conditions make the service copy the whole database to the HA members instead, which keeps writes blocked much longer. Other conditions make the upgrade fail after writes are blocked. The prechecks do not detect these conditions, so resolve them before you submit the upgrade. For the list, see Conditions that the prechecks do not detect.
You cannot cancel an IPMVU after you submit it, and you cannot downgrade the deployment in place. The service does not take a data backup before the upgrade, so create and verify an on-demand backup first.
Availability during an upgrade
An IPMVU runs in the following phases. Writes stop when the read-only phase begins and return when the service clears read-only mode. Reads are also unavailable during the conversion phase.
| Phase | What happens | Reads | Writes |
|---|---|---|---|
| Checks | The service waits for pending deployment changes to finish and runs the prechecks. It checks upgrade compatibility on the primary and the HA members, and suspends automatic failover. | Available | Available |
| Read-only | The service makes new transactions read-only in every database and ends all client sessions. It then waits for the HA members and transaction-log archiving to catch up. | Available | Blocked |
| Conversion | The service stops PostgreSQL on all members and upgrades the primary. Where possible, it upgrades each HA member from that member's existing data files. | Unavailable | Unavailable |
| Restart | The upgraded primary starts in read-only mode, and the HA members start. The service rebuilds any member that cannot reuse its data files from the upgraded primary. | Available | Blocked |
| Writes resume | The service clears read-only mode and ends all client sessions again. | Available | Available |
| Completion | The service waits for every HA member to catch up. Then, the service restarts the members one at a time to complete moving the deployment to the new version. Before restarting the primary, the service switches the primary role to another member, which ends client sessions. The service then removes the upgrade files, queues the post-upgrade backup, and marks the tasks as Completed. | Available, except during the switchover | Available, except during the switchover |
Writes resume after at least one HA member has caught up, and every other HA member has caught up or is rebuilding. The service rebuilds an HA member that cannot reuse its data files by copying the whole database to it. If every HA member must be rebuilt, writes stay blocked until the first rebuild finishes. For a large database, this can take hours. A deployment with two data members, which is the default configuration, has only one HA member. As a result, any rebuild blocks writes. HA members cannot reuse their data files in the following situations:
- Any database contains an
UNLOGGEDtable, even an empty one. The service then rebuilds every HA member. Check with self-check query 11, and seeUNLOGGEDtables and sequences. - The deployment runs PostgreSQL 16 or 17, and tables grew through bulk operations or many concurrent inserts or updates. Bulk operations include
COPY,pg_restore,CREATE TABLE AS,CREATE MATERIALIZED VIEW,REFRESH MATERIALIZED VIEW, andALTER TABLEcommands that rewrite a table, such asSET LOGGED. You cannot check for this in advance, so plan for a rebuild.
The service can also rebuild HA members for other reasons that you cannot check in advance. A rehearsal on a staging deployment that you restore from a backup might not show such a rebuild.
A rebuilding HA member keeps its previous data files until the task completes. As a result, it requires free disk space at least equal to the amount of space that its data uses. Before you submit the upgrade, ensure that each data member uses significantly less than half of its available disk space. Check this value by using the used disk space metric. Although the disk space precheck allows up to 90% disk utilization, a rebuilding HA member requires substantially more free space. Otherwise, the rebuild can run out of disk space and fail repeatedly. Writes can then stay blocked for hours before the task fails. Automatic and manual disk scaling wait until the upgrade finishes, so scale the disk before you submit the upgrade. A long rebuild can also make the task fail after writes resume. In both cases, the deployment runs the target version until support completes the upgrade. Allow for a rebuild in your maintenance window.
Writes resume before the task completes. To check whether writes have resumed, run SHOW default_transaction_read_only; in a new session. It returns off after writes resume. Client sessions end when read-only mode
begins, when PostgreSQL stops for the conversion, when writes resume, during the switchover in the completion phase, and when the service restores write access after a failure.
Until the task shows Completed, avoid sustained bulk writes, such as large data loads or batch jobs. The completion phase waits until every HA member has replayed all changes. Under a continuous heavy write load, this wait can time out, and the task then fails after writes resume. The risk is higher while an HA member is still being rebuilt. The deployment then runs the target version without a post-upgrade backup, and support must complete the upgrade.
To check whether your write load lets the HA members catch up, run the following query at your planned upgrade time. Repeat it every second for a few minutes, for example with the psql command \watch 1. The service
needs every HA member to show 0 in replay_lag_bytes at the same time. If this never occurs, reduce the write load before you submit the upgrade.
SELECT application_name, state,
pg_catalog.pg_wal_lsn_diff(pg_catalog.pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes
FROM pg_catalog.pg_stat_replication
ORDER BY application_name;
The upgrade does not refresh optimizer statistics, so queries can choose slow plans until you run ANALYZE. Upgrades to PostgreSQL 15, 16, or 17 do not transfer statistics. Upgrades to PostgreSQL 18 transfer most statistics, but
not extended statistics that you created with CREATE STATISTICS, or statistics that extensions such as PostGIS collect. The autovacuum process analyzes a table only after enough writes, and it never analyzes partitioned
tables. Do not wait for the task to complete. As soon as SHOW default_transaction_read_only; returns off, refresh statistics as described in After the upgrade.
Read-only mode sets a default for new transactions. It does not lock the database. A session that sets default_transaction_read_only to off, or explicitly starts a read-write transaction, can still write. So can a
role whose own default_transaction_read_only setting is off. Maintenance commands such as VACUUM, ANALYZE, REINDEX, and CLUSTER also still run. Read-only mode
does not stop a pg_cron job that is already running, or an enabled subscription on PostgreSQL 17. Such writes can make the upgrade fail. Before you submit the upgrade, stop processes that override read-only mode. Pause scheduled
jobs that write or run maintenance commands, such as pg_cron jobs, and wait for running jobs to finish. Resume them as described in After the upgrade.
Reads can also write transaction logs during read-only mode, for example the first time that queries read rows after a bulk load or a large update. This activity delays the catch-up that the read-only and restart phases wait for, so writes
stay blocked longer, and the upgrade can fail. Before you submit the upgrade, run VACUUM on tables that you recently loaded or modified significantly.
When the service ends a session, a client that connects directly to the deployment receives SQLSTATE 57P01 (admin_shutdown). A write in a read-only transaction fails with SQLSTATE 25006 (read_only_sql_transaction).
During the conversion phase, a new connection can fail with an authentication error, such as SQLSTATE 28P01, although your credentials are unchanged. Try the connection again after the conversion. If these errors continue after
the task shows Failed, contact support. Through a connection pooler, applications can see other connection errors.
Clients that accept only read-write sessions cannot connect while read-only mode is set, so reads are also unavailable to them until writes resume. This applies to libpq clients that set target_session_attrs=read-write,
and to JDBC clients that set targetServerType=primary or master. To keep reads available, use target_session_attrs=any, or primary with libpq 14 or later. For JDBC, use targetServerType=any.
Applications must handle connection failures and read-only errors, reconnect, and retry interrupted transactions only when safe. Connection pooling does not remove these requirements. Application access is not proof that the upgrade task completed.
Backups and recovery
The service does not take a data backup before the upgrade. Create and verify a recent on-demand backup before IPMVU. To return to the source version after an upgrade, restore a pre-upgrade backup or recovery point into a new deployment that runs the source version. Set the version explicitly, because a point-in-time restore without a version creates a deployment that runs the current version. This option requires that the source version is still available for new deployments. Account for changes made since that point.
After a successful upgrade, the service queues a backup of the upgraded deployment. The backup runs separately after the upgrade task completes and might not start immediately. This first backup on the new version copies the whole database, so it can take several hours for a large database. Check its status in Backups and restore. If it fails, take an on-demand backup, and contact support if failures continue. A backup failure does not roll back the upgrade.
Point-in-time recovery (PITR) cannot replay transactions across the major version upgrade. You can restore to a point in time before the upgrade task starts, or after the first backup of the upgraded deployment completes. Restores to recovery points between the upgrade and the completion of the post-upgrade backup are not supported. The service rejects some of these recovery points, while restores to others might be accepted but fail during the restore process. This unsupported period includes the time after writes resume. Until the post-upgrade backup completes, restores to the latest available recovery point can also fail even if the service accepts the restore request.
To keep PITR coverage for changes after the upgrade, keep application writes paused until the post-upgrade backup completes. Pause them in your applications. When writes resume, the upgrade resets default_transaction_read_only on every database, so a database-level read-only setting does not keep writes paused.
A failed upgrade does not queue a backup. PITR can be unavailable for points between the start of the attempt and the next completed backup. After a failed upgrade, if SHOW server_version; reports the source version and the deployment
accepts writes, take an on-demand backup. Otherwise, contact support before you take a backup or make other changes.
Pre-upgrade backups and recovery points belong to the earlier version and remain subject to retention limits. For more information, see PITR.
The skip-backup options are not supported for PostgreSQL: skip_backup in the API, --skip-backup in the CLI, and version_upgrade_skip_backup in Terraform. Requests that enable them fail. The API and the
CLI return an error, and Terraform plans fail.
Before you begin
Apply these requirements to both staging and production:
-
Complete the application, extension, and replication preparation and backup preparation.
-
Resolve the conditions that the prechecks do not detect, and run the self-check queries.
-
Ensure that each data member uses significantly less than half of its available disk space, so that a rebuild of the HA members does not run out of space. For more information, see Availability during an upgrade.
-
Ensure that the deployment has at least one completed backup. Until then, the console disables the Upgrade major version button, and the API rejects the request.
-
IPMVU is not available on a read-only replica deployment. To upgrade a replica, promote it with a version upgrade.
-
Promote or delete every read-only replica of the deployment, including replicas in other regions, and wait for each operation to finish. A deleted replica stays associated with the deployment until it is permanently deleted. The upgrade fails its prechecks while any replica is associated with the deployment. Plan for applications that read from these replicas. For more information, see Read-only replicas and IPMVU.
-
Run one upgrade at a time. The service rejects a new upgrade request while another upgrade task is queued or running.
-
If you set the deployment's
synchronous_commitconfiguration toon, change it tolocalbefore you submit the upgrade. Change it back after the task shows Completed. Withon, a failed upgrade can leave every database read-only, or make commits wait, until support recovers the deployment. The deployment's setting isonif the following query returnsonwith the sourceconfiguration file:SELECT setting, source FROM pg_catalog.pg_settings WHERE name = 'synchronous_commit'; -
Choose a target from your deployment's capabilities. You can upgrade directly to any later version that appears as an upgrade transition, without intermediate versions. A version that you can provision is not necessarily an in-place upgrade target:
ibmcloud cdb deployment-capability-show <NAME|CRN> versionsIn the output, use the
to_versionvalue of an Upgrade Transition entry.
Prechecks and preparation
The service runs the following checks before read-only mode begins. If a check fails, the task shows Failed, and the deployment stays on its current version without blocking writes. If any database has default_transaction_read_only set to on, a failed check also resets this setting on every database and ends all client sessions. The task does not report which check failed, so verify each item before you submit the upgrade.
| Check | Passes when | Your action |
|---|---|---|
| Deployment health | Every data member is running, and each HA member is streaming from the primary. | Let scaling, maintenance, and HA member rebuilds finish. Check with self-check query 1. Other tasks do not fail this check, but they delay the start of the upgrade. Check Recent tasks, or list tasks with ibmcloud cdb deployment-tasks-list.
If this check keeps failing, contact support. |
| Disk space | Each data member uses at most 90% of its allocated disk space. | Scale disk or remove data that you no longer need. Unused logical replication slots can also retain transaction logs. This check allows up to 90%, but a rebuild of the HA members needs each data member to use well under half of its disk. See Availability during an upgrade. |
| CPU, memory, and disk I/O | On each data member, the host's 1-minute load average is at most 90% of its CPU count, and host memory use is at most 90%. Disk I/O utilization is also at most 90%. | Upgrade during low activity, or submit the upgrade again later. The service measures load and memory for the whole host, including other workloads on it, and samples disk I/O over a few seconds. The load average also counts processes that wait for disk I/O. Its values can therefore differ from your monitoring metrics, and scaling your deployment might not lower them. If this check keeps failing, contact support. |
| Transaction-log archiving | Archiving is not failing, and no more than 4 GB of transaction logs are waiting for archiving. | Reduce the write rate until the backlog clears. Check with self-check query 5. The check also fails if an HA member has transaction logs waiting, for example soon after a failover. If this check keeps failing, contact support. |
| Logical replication | No logical replication slot exists. On PostgreSQL 16 and earlier, no logical subscription exists, including disabled subscriptions. | Remove slots. On PostgreSQL 16 and earlier, remove subscriptions. On PostgreSQL 17, disable them. Check with self-check queries 2 and 3. |
| Read-only replicas and replication clients | The deployment has no associated read-only replica, and no replication client other than the HA members connects to it. Replicas count in any region and state, including replicas that are provisioning or disconnected. | List the associated replicas in the console or with ibmcloud cdb deployment-read-replicas. Promote or delete each one, and wait for each operation
to finish. A deleted replica still counts until it is permanently deleted, although the list no longer shows it. See Read-only replicas and IPMVU.
Disconnect other replication clients. Self-check query 1 shows only connected clients. |
| Prepared transactions | No prepared transaction exists. | Have the owning application or transaction manager resolve prepared transactions, and prevent new ones until the upgrade completes. Do not commit or roll them back only to clear the check. Check with self-check query 4. |
| Schema export | The schema of each database exports without an error. An export that times out or runs out of lock space does not fail this check. | Rehearse the upgrade on a staging deployment that you restore from a recent backup. This check does not test whether the target version can re-create the schema, so also resolve the conditions that the prechecks do not detect. If this check keeps failing, contact support. |
| Upgrade compatibility | PostgreSQL upgrade compatibility checks pass on the primary and every HA member. | Complete the extension preparation, including the pgRouting check. On PostgreSQL 17, keep at most 10 subscriptions. Check with
self-check queries 6, 8, 9, and 10. |
Conditions that the prechecks do not detect
The prechecks do not detect the following conditions. Some of them make the upgrade fail after writes are blocked. If the cause is in the schema, such as a function or a database name, every retry fails the same way until you resolve it. Other conditions make the service copy the whole database to the HA members, which keeps writes blocked much longer, or make the upgrade fail after writes resume. A failure in the conversion or restart phase can also leave the HA members stopped until support restarts them. Resolve or plan for each condition before you submit the upgrade.
| Condition | Effect | What to do |
|---|---|---|
A database contains an UNLOGGED table. |
Every HA member is rebuilt, and UNLOGGED contents are reset. |
Check with self-check query 11. See UNLOGGED tables and sequences. |
| The deployment runs PostgreSQL 16 or 17, and tables grew through bulk operations or many concurrent inserts or updates. | HA members are rebuilt. | Plan the maintenance window and disk space for a rebuild. See Availability during an upgrade. |
| A data member uses about half of its disk or more when HA members are rebuilt. | The rebuild runs out of space. Writes can stay blocked for hours before the task fails. | Before you submit the upgrade, scale disk until each data member uses significantly less than 50% of its available disk space. |
An index, a partition key, a stored generated column, or extended statistics use a LANGUAGE sql function with a string body that names objects outside pg_catalog without their schema. In an index, naming the
ibm_extension schema has the same effect. |
The conversion fails. | Check with self-check query 12. See SQL functions in indexes, partition keys, and generated columns. |
An index or a stored generated column uses earthdistance version 1.1. |
The conversion fails. | Check with self-check query 13. See earthdistance. |
| A view, an aggregate, or a function with an SQL-standard body uses a system name that the target version removed. Or a function, role, or database setting uses a removed parameter. | The conversion fails. | Check with self-check queries 7 and 14. See Objects that use removed system names. |
anon version 2 is installed. |
The conversion fails. | Complete the anon preparation. |
A database name contains a single quotation mark ('), a backslash (``), an equals sign (=), a newline, or a carriage return. |
The upgrade fails, in some cases after writes are blocked. | Check with self-check query 15. Rename the database, and update your connection strings. |
| Databases contain many thousands of tables and sequences or, on PostgreSQL 14 and 15, views. | The conversion runs out of lock space and fails. | Check with self-check query 16, and increase max_locks_per_transaction if the query shows that you need to. |
| The deployment has very many databases, tables, or large objects. | The conversion exceeds its time limit. The task fails, or stays in progress while the database is unavailable. | Count the tables and large objects with self-check query 16. Rehearse on a staging deployment that you restore from a recent production backup. If the rehearsal fails, contact support. |
| Write activity is high when the upgrade starts, or many transaction logs wait for archiving. | After read-only mode begins, the HA members and transaction-log archiving get only a few minutes to catch up. A backlog that passes the precheck can take longer to archive, and the upgrade then fails. | Keep the load low from the time that the upgrade starts until writes resume, and run VACUUM on recently loaded tables before the upgrade. Check replication lag by using the query in Availability during an upgrade,
and check archiving by using self-check query 5. |
Writes continue after read-only mode begins, for example from read-only overrides, maintenance commands, running pg_cron jobs, or enabled subscriptions on PostgreSQL 17. |
The HA members and archiving might not catch up in time, causing the upgrade to fail. | Stop these writers before you submit the upgrade, as described in Availability during an upgrade. |
A client prepares a transaction after the prechecks. PREPARE TRANSACTION still works in read-only mode. |
The upgrade fails while writes are blocked. A transaction that is prepared shortly before PostgreSQL stops can cause the conversion to fail. | Stop clients that use two-phase commit until the task shows Completed. Check with self-check query 4. |
| A replication client connects during the upgrade. | A slot that it creates is not kept, and the upgrade can fail or stall while the database is unavailable. | Keep replication clients disconnected until the task shows Completed. Check with self-check queries 1 and 2. |
| A continuous bulk load runs after writes resume. | The task fails after writes resume, and the deployment runs the target version until support completes the upgrade. | Avoid bulk loads until the task shows Completed. |
The deployment's synchronous_commit setting is on. |
A failed upgrade can leave every database read-only until support recovers the deployment. | Change it to local, as described in Before you begin. |
You upgrade from PostgreSQL 16 or earlier to 17 or later, and an index, a materialized view, or extended statistics use a function without a search_path setting. |
After the upgrade, ANALYZE, REINDEX, CLUSTER, VACUUM FULL, and REFRESH MATERIALIZED VIEW can fail for these objects. |
Check with self-check query 17. See SQL functions in indexes, partition keys, and generated columns. |
Do not change schemas, extensions, databases, logical replication slots, or subscriptions between submitting the upgrade and task completion.
Checking readiness yourself
Run the following queries as the admin user with psql before you submit the upgrade. They work on PostgreSQL 14 through 18. Queries 1 through 7 and query 15 cover the whole deployment, so run them once in any database.
Run the other queries in each database.
-
Replication connections. Expect one row for each HA member, which is the number of data members minus one, each with the state
streaming. Any other row is a read-only replica or another replication client. A row namedpg_basebackupmeans that the service is copying the database to an HA member or to a read-only replica. Wait until the copy finishes, and then run the query again.SELECT application_name, client_addr, state FROM pg_catalog.pg_stat_replication ORDER BY application_name; -
Logical replication slots. Expect no rows.
SELECT slot_name, plugin, database, active FROM pg_catalog.pg_replication_slots WHERE slot_type = 'logical'; -
Logical subscriptions. On PostgreSQL 16 and earlier, expect no rows. On PostgreSQL 17, expect
fin thesubenabledcolumn of every row.SELECT d.datname AS database_name, s.subname, s.subenabled FROM pg_catalog.pg_subscription AS s JOIN pg_catalog.pg_database AS d ON d.oid = s.subdbid; -
Prepared transactions. Expect no rows.
SELECT gid, owner, database, prepared FROM pg_catalog.pg_prepared_xacts; -
Transaction-log archiving. Expect
last_failed_timeto be empty or earlier thanlast_archived_time, andwaiting_filesto be 0 or close to 0. A backlog that passes the precheck can still be too large to archive after read-only mode begins.SELECT failed_count, last_failed_time, last_archived_time FROM pg_catalog.pg_stat_archiver;SELECT count(*) AS waiting_files, pg_catalog.pg_size_pretty(count(*) * pg_catalog.pg_size_bytes(pg_catalog.current_setting('wal_segment_size'))) AS waiting_size FROM pg_catalog.pg_ls_archive_statusdir() WHERE name LIKE '%.ready'; -
Database connection settings. Expect no rows. Drop a listed database whose
datconnlimitis-2, because it is invalid. For any other listed database, allow connections or drop it. If the query liststemplate0, contact support.SELECT datname, datallowconn, datconnlimit FROM pg_catalog.pg_database WHERE (datname <> 'template0' AND (NOT datallowconn OR datconnlimit = -2)) OR (datname = 'template0' AND datallowconn); -
Role and database settings. Make sure that every parameter in
setconfigexists in the target version. Reset a parameter that the target version removed or renamed, and set its replacement after the upgrade. This also applies to extension-defined parameters that extensions, which contain a period in their names. Make sure that no role setsdefault_transaction_read_onlyto a false value, such asoff,false,no, or0. Record each database that setsdefault_transaction_read_only, because the upgrade resets this setting. Self-check query 14 checks settings on functions.SELECT r.rolname AS role_name, d.datname AS database_name, s.setconfig FROM pg_catalog.pg_db_role_setting AS s LEFT JOIN pg_catalog.pg_roles AS r ON r.oid = s.setrole LEFT JOIN pg_catalog.pg_database AS d ON d.oid = s.setdatabase; -
Column data types that PostgreSQL cannot upgrade. Expect no rows. The query also lists columns of system composite types, and of domains, arrays, composite types, and ranges that are built on these types. The
base_typecolumn shows the type that blocks the upgrade. Theaclitemtype blocks only upgrades from PostgreSQL 14 or 15 to 16 or later.WITH RECURSIVE problem_types (type_oid, base_type) AS ( SELECT t.oid, t.typname::text FROM pg_catalog.pg_type AS t WHERE t.typnamespace = 'pg_catalog'::pg_catalog.regnamespace AND t.typname IN ('regcollation', 'regconfig', 'regdictionary', 'regnamespace', 'regoper', 'regoperator', 'regproc', 'regprocedure', 'aclitem') UNION ALL SELECT t.oid, 'system composite type ' || t.oid::pg_catalog.regtype::text FROM pg_catalog.pg_type AS t LEFT JOIN pg_catalog.pg_namespace AS n ON n.oid = t.typnamespace WHERE t.typtype = 'c' AND (t.oid < 16384 OR n.nspname = 'information_schema') UNION ALL SELECT derived.type_oid, derived.base_type FROM ( WITH found AS (SELECT type_oid, base_type FROM problem_types) SELECT t.oid AS type_oid, f.base_type FROM pg_catalog.pg_type AS t JOIN found AS f ON t.typtype = 'd' AND t.typbasetype = f.type_oid UNION ALL SELECT t.oid, f.base_type FROM pg_catalog.pg_type AS t JOIN found AS f ON t.typtype = 'b' AND t.typelem = f.type_oid UNION ALL SELECT t.oid, f.base_type FROM pg_catalog.pg_type AS t JOIN pg_catalog.pg_class AS c ON c.reltype = t.oid JOIN pg_catalog.pg_attribute AS a ON a.attrelid = c.oid AND NOT a.attisdropped JOIN found AS f ON a.atttypid = f.type_oid WHERE t.typtype = 'c' UNION ALL SELECT t.oid, f.base_type FROM pg_catalog.pg_type AS t JOIN pg_catalog.pg_range AS r ON r.rngtypid = t.oid JOIN found AS f ON r.rngsubtype = f.type_oid WHERE t.typtype = 'r' ) AS derived ) SELECT DISTINCT n.nspname AS schema_name, c.relname AS relation_name, a.attname AS column_name, a.atttypid::pg_catalog.regtype AS data_type, p.base_type FROM pg_catalog.pg_attribute AS a JOIN pg_catalog.pg_class AS c ON c.oid = a.attrelid JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace JOIN problem_types AS p ON p.type_oid = a.atttypid WHERE a.attnum > 0 AND NOT a.attisdropped AND c.relkind IN ('r', 'm', 'i') AND n.nspname NOT IN ('pg_catalog', 'information_schema') AND n.nspname !~ '^pg_toast_temp_|^pg_temp_' ORDER BY 1, 2, 3; -
Inherited columns without a
NOT NULLconstraint that their parent table has. Expect no rows. To fix a listed column, the table owner runsALTER TABLE <table_name> ALTER COLUMN <column_name> SET NOT NULL;.SELECT n.nspname AS schema_name, c.relname AS table_name, child.attname AS column_name FROM pg_catalog.pg_inherits AS i JOIN pg_catalog.pg_attribute AS child ON child.attrelid = i.inhrelid JOIN pg_catalog.pg_attribute AS parent ON parent.attrelid = i.inhparent AND parent.attname = child.attname JOIN pg_catalog.pg_class AS c ON c.oid = i.inhrelid JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace WHERE parent.attnum > 0 AND parent.attnotnull AND NOT child.attnotnull AND NOT child.attisdropped; -
Installed extensions. Complete the extension preparation. Then, confirm that every remaining extension is available in the target version, for example on a staging deployment of that version.
SELECT extname, extversion FROM pg_catalog.pg_extension ORDER BY extname; -
UNLOGGEDtables and sequences. Their contents do not survive the upgrade, and a listed table makes the service rebuild every HA member. Before you upgrade, seeUNLOGGEDtables and sequences.SELECT n.nspname AS schema_name, c.relname AS relation_name, c.relkind FROM pg_catalog.pg_class AS c JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace WHERE c.relpersistence = 'u' AND c.relkind IN ('r', 'S'); -
SQL functions that can make the conversion fail. Expect no rows. The query lists
LANGUAGE sqlfunctions with a string body that an index, a partial-index predicate, a partition key, a stored generated column, or extended statistics use. It follows calls through operators, domain check constraints, and functions with an SQL-standard body. For each listed function, see SQL functions in indexes, partition keys, and generated columns.WITH RECURSIVE uses (refclassid, refobjid, used_by) AS ( SELECT d.refclassid, d.refobjid, CASE WHEN c.relkind = 'p' THEN 'partition key of ' ELSE 'index ' END || c.oid::pg_catalog.regclass::text FROM pg_catalog.pg_depend AS d JOIN pg_catalog.pg_class AS c ON d.classid = 'pg_catalog.pg_class'::pg_catalog.regclass AND c.oid = d.objid WHERE c.relkind IN ('i', 'I') OR (c.relkind = 'p' AND d.objsubid = 0) UNION SELECT d.refclassid, d.refobjid, 'generated column ' || a.attname || ' of ' || a.attrelid::pg_catalog.regclass::text FROM pg_catalog.pg_depend AS d JOIN pg_catalog.pg_attrdef AS ad ON d.classid = 'pg_catalog.pg_attrdef'::pg_catalog.regclass AND ad.oid = d.objid JOIN pg_catalog.pg_attribute AS a ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum WHERE a.attgenerated = 's' UNION SELECT d.refclassid, d.refobjid, 'generated column ' || a.attname || ' of ' || a.attrelid::pg_catalog.regclass::text FROM pg_catalog.pg_depend AS d JOIN pg_catalog.pg_attribute AS a ON d.classid = 'pg_catalog.pg_class'::pg_catalog.regclass AND a.attrelid = d.objid AND a.attnum = d.objsubid WHERE a.attgenerated = 's' UNION SELECT d.refclassid, d.refobjid, 'statistics ' || s.stxname || ' on ' || s.stxrelid::pg_catalog.regclass::text FROM pg_catalog.pg_depend AS d JOIN pg_catalog.pg_statistic_ext AS s ON d.classid = 'pg_catalog.pg_statistic_ext'::pg_catalog.regclass AND s.oid = d.objid ), used (fn, used_by) AS ( SELECT x.fn, u.used_by FROM uses AS u CROSS JOIN LATERAL ( SELECT u.refobjid AS fn WHERE u.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass UNION ALL SELECT o.oprcode::pg_catalog.oid FROM pg_catalog.pg_operator AS o WHERE u.refclassid = 'pg_catalog.pg_operator'::pg_catalog.regclass AND o.oid = u.refobjid UNION ALL SELECT dc.refobjid FROM pg_catalog.pg_constraint AS con JOIN pg_catalog.pg_depend AS dc ON dc.classid = 'pg_catalog.pg_constraint'::pg_catalog.regclass AND dc.objid = con.oid AND dc.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass WHERE u.refclassid = 'pg_catalog.pg_type'::pg_catalog.regclass AND con.contypid = u.refobjid ) AS x UNION SELECT x.fn, u.used_by FROM used AS u JOIN pg_catalog.pg_proc AS p ON p.oid = u.fn AND p.prosqlbody IS NOT NULL AND p.proconfig IS NULL AND NOT p.prosecdef JOIN pg_catalog.pg_depend AS d ON d.classid = 'pg_catalog.pg_proc'::pg_catalog.regclass AND d.objid = u.fn CROSS JOIN LATERAL ( SELECT d.refobjid AS fn WHERE d.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass UNION ALL SELECT o.oprcode::pg_catalog.oid FROM pg_catalog.pg_operator AS o WHERE d.refclassid = 'pg_catalog.pg_operator'::pg_catalog.regclass AND o.oid = d.refobjid ) AS x ) SELECT DISTINCT u.fn::pg_catalog.regprocedure AS function_name, u.used_by FROM used AS u JOIN pg_catalog.pg_proc AS p ON p.oid = u.fn JOIN pg_catalog.pg_language AS l ON l.oid = p.prolang WHERE l.lanname = 'sql' AND p.prosqlbody IS NULL AND p.proconfig IS NULL AND NOT p.prosecdef AND p.pronamespace <> 'pg_catalog'::pg_catalog.regnamespace AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend AS e WHERE e.classid = 'pg_catalog.pg_proc'::pg_catalog.regclass AND e.objid = p.oid AND e.deptype = 'e') ORDER BY 1, 2; -
Indexes and stored generated columns that use
earthdistanceversion 1.1, directly or through functions with an SQL-standard body. Expect no rows. For each listed object, seeearthdistance.WITH RECURSIVE used (fn, via, used_by) AS ( SELECT d.refobjid, d.refobjid, 'index ' || c.oid::pg_catalog.regclass::text FROM pg_catalog.pg_depend AS d JOIN pg_catalog.pg_class AS c ON d.classid = 'pg_catalog.pg_class'::pg_catalog.regclass AND c.oid = d.objid AND c.relkind IN ('i', 'I') WHERE d.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass UNION SELECT d.refobjid, d.refobjid, 'generated column ' || a.attname || ' of ' || a.attrelid::pg_catalog.regclass::text FROM pg_catalog.pg_depend AS d LEFT JOIN pg_catalog.pg_attrdef AS ad ON d.classid = 'pg_catalog.pg_attrdef'::pg_catalog.regclass AND ad.oid = d.objid JOIN pg_catalog.pg_attribute AS a ON a.attgenerated = 's' AND ((a.attrelid = ad.adrelid AND a.attnum = ad.adnum) OR (d.classid = 'pg_catalog.pg_class'::pg_catalog.regclass AND a.attrelid = d.objid AND a.attnum = d.objsubid)) WHERE d.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass UNION SELECT d.refobjid, u.via, u.used_by FROM used AS u JOIN pg_catalog.pg_proc AS p ON p.oid = u.fn AND p.prosqlbody IS NOT NULL AND p.proconfig IS NULL JOIN pg_catalog.pg_depend AS d ON d.classid = 'pg_catalog.pg_proc'::pg_catalog.regclass AND d.objid = u.fn AND d.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass ) SELECT e.extversion, u.via::pg_catalog.regprocedure AS used_function, u.used_by FROM used AS u JOIN pg_catalog.pg_depend AS m ON m.classid = 'pg_catalog.pg_proc'::pg_catalog.regclass AND m.objid = u.fn AND m.refclassid = 'pg_catalog.pg_extension'::pg_catalog.regclass AND m.deptype = 'e' JOIN pg_catalog.pg_extension AS e ON e.oid = m.refobjid JOIN pg_catalog.pg_proc AS p ON p.oid = u.fn JOIN pg_catalog.pg_language AS l ON l.oid = p.prolang WHERE e.extname = 'earthdistance' AND e.extversion IN ('1.0', '1.1') AND l.lanname = 'sql' AND p.proconfig IS NULL ORDER BY 2, 3; -
Views, materialized views, aggregates, and functions that use system catalog names or configuration parameters that a later PostgreSQL version removed. The
removed_incolumn shows the first version without the name. Expect no rows with aremoved_inversion at or below your target version. A name can match text that is not a catalog reference, so review each listed object. See Objects that use removed system names.WITH removed (relation_name, identifier, removed_in) AS ( VALUES (NULL, 'pg_is_in_backup', 15), (NULL, 'pg_backup_start_time', 15), (NULL, 'pg_start_backup', 15), (NULL, 'pg_stop_backup', 15), ('pg_database', 'datlastsysoid', 15), (NULL, 'force_parallel_mode', 16), ('pg_stat_bgwriter', 'checkpoints_timed', 17), ('pg_stat_bgwriter', 'checkpoints_req', 17), ('pg_stat_bgwriter', 'checkpoint_write_time', 17), ('pg_stat_bgwriter', 'checkpoint_sync_time', 17), ('pg_stat_bgwriter', 'buffers_checkpoint', 17), ('pg_stat_bgwriter', 'buffers_backend', 17), ('pg_stat_bgwriter', 'buffers_backend_fsync', 17), (NULL, 'pg_stat_get_bgwriter_timed_checkpoints', 17), (NULL, 'pg_stat_get_bgwriter_requested_checkpoints', 17), (NULL, 'pg_stat_get_checkpoint_write_time', 17), (NULL, 'pg_stat_get_checkpoint_sync_time', 17), (NULL, 'pg_stat_get_bgwriter_buf_written_checkpoints', 17), (NULL, 'pg_stat_get_buf_written_backend', 17), (NULL, 'pg_stat_get_buf_fsync_backend', 17), ('pg_stat_progress_vacuum', 'max_dead_tuples', 17), ('pg_stat_progress_vacuum', 'num_dead_tuples', 17), ('pg_database', 'daticulocale', 17), ('pg_collation', 'colliculocale', 17), ('element_types', 'domain_default', 17), (NULL, 'interval_accum', 17), (NULL, 'interval_accum_inv', 17), (NULL, 'interval_combine', 17), ('pg_stat_wal', 'wal_write', 18), ('pg_stat_wal', 'wal_sync', 18), ('pg_stat_wal', 'wal_write_time', 18), ('pg_stat_wal', 'wal_sync_time', 18), ('pg_stat_io', 'op_bytes', 18), ('pg_backend_memory_contexts', 'parent', 18), ('pg_attribute', 'attcacheoff', 18) ), definitions (object_name, definition) AS ( SELECT 'view ' || c.oid::pg_catalog.regclass::text, pg_catalog.pg_get_viewdef(c.oid) FROM pg_catalog.pg_class AS c WHERE c.relkind IN ('v', 'm') AND c.relnamespace NOT IN ('pg_catalog'::pg_catalog.regnamespace, 'information_schema'::pg_catalog.regnamespace) UNION ALL SELECT 'function ' || p.oid::pg_catalog.regprocedure::text, pg_catalog.concat_ws(' ', pg_catalog.pg_get_function_sqlbody(p.oid), pg_catalog.array_to_string(p.proconfig, ' ')) FROM pg_catalog.pg_proc AS p WHERE (p.prosqlbody IS NOT NULL OR p.proconfig IS NOT NULL) AND p.pronamespace NOT IN ('pg_catalog'::pg_catalog.regnamespace, 'information_schema'::pg_catalog.regnamespace) UNION ALL SELECT 'aggregate ' || a.aggfnoid::pg_catalog.regprocedure::text, pg_catalog.concat_ws(' ', a.aggtransfn, a.aggfinalfn, a.aggcombinefn, a.aggmtransfn, a.aggminvtransfn, a.aggmfinalfn) FROM pg_catalog.pg_aggregate AS a JOIN pg_catalog.pg_proc AS p ON p.oid = a.aggfnoid WHERE p.pronamespace NOT IN ('pg_catalog'::pg_catalog.regnamespace, 'information_schema'::pg_catalog.regnamespace) ) SELECT d.object_name, r.identifier, r.removed_in FROM definitions AS d JOIN removed AS r ON d.definition ~ ('\m' || r.identifier || '\M') AND (r.relation_name IS NULL OR d.definition ~ ('\m' || r.relation_name || '\M')) ORDER BY 3, 1, 2; -
Database names that the upgrade cannot use. Expect no rows. To fix a listed database, its owner must connect to another database and run
ALTER DATABASE "<database_name>" RENAME TO <new_name>;while no sessions are connected to to the database being renamed. The owner must also have theCREATEDBattribute. Then, update any applications that connect to the database.SELECT datname FROM pg_catalog.pg_database WHERE pg_catalog.strpos(datname, '''') > 0 OR pg_catalog.strpos(datname, pg_catalog.chr(92)) > 0 OR pg_catalog.strpos(datname, '=') > 0 OR pg_catalog.strpos(datname, pg_catalog.chr(10)) > 0 OR pg_catalog.strpos(datname, pg_catalog.chr(13)) > 0 ORDER BY datname; -
Relations that the upgrade locks, large objects, and the deployment's lock capacity. Add up
locked_relationsfor the four databases with the highest values. On PostgreSQL 14 and 15,locked_relationsincludes views and materialized views, because the upgrade also locks them. If the total exceedslock_capacity, increasemax_locks_per_transactionuntillock_capacityexceeds the total. The change restarts the database, so make it before you submit the upgrade. Many tables or large objects also lengthen the conversion, so rehearse the upgrade with your real schema and data.SELECT pg_catalog.current_database() AS database_name, (SELECT pg_catalog.count(*) FROM pg_catalog.pg_class AS c WHERE (c.relkind IN ('r', 'p', 'S') OR (c.relkind IN ('v', 'm') AND pg_catalog.current_setting('server_version_num')::integer < 160000)) AND c.relnamespace NOT IN ('pg_catalog'::pg_catalog.regnamespace, 'information_schema'::pg_catalog.regnamespace)) AS locked_relations, (SELECT pg_catalog.count(*) FROM pg_catalog.pg_largeobject_metadata) AS large_objects, pg_catalog.current_setting('max_locks_per_transaction')::integer * (pg_catalog.current_setting('max_connections')::integer + pg_catalog.current_setting('max_prepared_transactions')::integer) AS lock_capacity; -
Functions without a
search_pathsetting that indexes, materialized views, and extended statistics use. The query follows calls through operators, domain check constraints, and functions with an SQL-standard body. Run this query if you upgrade from PostgreSQL 16 or earlier to PostgreSQL 17 or later. A listed function whose body names objects outsidepg_catalogwithout their schema makes maintenance commands fail after the upgrade. A function is unaffected only if its body, and every function that it calls, names every object outsidepg_catalogwith its schema. For each other listed function, its owner adds asearch_pathsetting, as described in SQL functions in indexes, partition keys, and generated columns.WITH RECURSIVE refreshed (relid, used_by) AS ( SELECT c.oid, c.oid::pg_catalog.regclass FROM pg_catalog.pg_class AS c WHERE c.relkind = 'm' UNION SELECT v.oid, f.used_by FROM refreshed AS f JOIN pg_catalog.pg_rewrite AS r ON r.ev_class = f.relid JOIN pg_catalog.pg_depend AS d ON d.classid = 'pg_catalog.pg_rewrite'::pg_catalog.regclass AND d.objid = r.oid AND d.refclassid = 'pg_catalog.pg_class'::pg_catalog.regclass JOIN pg_catalog.pg_class AS v ON v.oid = d.refobjid AND v.relkind = 'v' ), refs (refclassid, refobjid, used_by) AS ( SELECT d.refclassid, d.refobjid, c.oid::pg_catalog.regclass FROM pg_catalog.pg_depend AS d JOIN pg_catalog.pg_class AS c ON c.oid = d.objid AND c.relkind IN ('i', 'I') WHERE d.classid = 'pg_catalog.pg_class'::pg_catalog.regclass UNION SELECT d.refclassid, d.refobjid, f.used_by FROM refreshed AS f JOIN pg_catalog.pg_rewrite AS r ON r.ev_class = f.relid JOIN pg_catalog.pg_depend AS d ON d.classid = 'pg_catalog.pg_rewrite'::pg_catalog.regclass AND d.objid = r.oid UNION SELECT d.refclassid, d.refobjid, s.stxrelid::pg_catalog.regclass FROM pg_catalog.pg_depend AS d JOIN pg_catalog.pg_statistic_ext AS s ON s.oid = d.objid WHERE d.classid = 'pg_catalog.pg_statistic_ext'::pg_catalog.regclass ), used (fn, used_by) AS ( SELECT x.fn, r.used_by FROM refs AS r CROSS JOIN LATERAL ( SELECT r.refobjid AS fn WHERE r.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass UNION ALL SELECT o.oprcode::pg_catalog.oid FROM pg_catalog.pg_operator AS o WHERE r.refclassid = 'pg_catalog.pg_operator'::pg_catalog.regclass AND o.oid = r.refobjid UNION ALL SELECT dc.refobjid FROM pg_catalog.pg_constraint AS con JOIN pg_catalog.pg_depend AS dc ON dc.classid = 'pg_catalog.pg_constraint'::pg_catalog.regclass AND dc.objid = con.oid AND dc.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass WHERE r.refclassid = 'pg_catalog.pg_type'::pg_catalog.regclass AND con.contypid = r.refobjid ) AS x UNION SELECT x.fn, u.used_by FROM used AS u JOIN pg_catalog.pg_proc AS p ON p.oid = u.fn AND p.prosqlbody IS NOT NULL AND NOT EXISTS (SELECT 1 FROM pg_catalog.unnest(p.proconfig) AS setting WHERE setting LIKE 'search\_path=%') JOIN pg_catalog.pg_depend AS d ON d.classid = 'pg_catalog.pg_proc'::pg_catalog.regclass AND d.objid = u.fn CROSS JOIN LATERAL ( SELECT d.refobjid AS fn WHERE d.refclassid = 'pg_catalog.pg_proc'::pg_catalog.regclass UNION ALL SELECT o.oprcode::pg_catalog.oid FROM pg_catalog.pg_operator AS o WHERE d.refclassid = 'pg_catalog.pg_operator'::pg_catalog.regclass AND o.oid = d.refobjid ) AS x ) SELECT DISTINCT p.oid::pg_catalog.regprocedure AS function_name, u.used_by FROM used AS u JOIN pg_catalog.pg_proc AS p ON p.oid = u.fn JOIN pg_catalog.pg_language AS l ON l.oid = p.prolang WHERE l.lanname NOT IN ('c', 'internal') AND p.prosqlbody IS NULL AND p.pronamespace NOT IN ('pg_catalog'::pg_catalog.regnamespace, 'information_schema'::pg_catalog.regnamespace) AND NOT EXISTS (SELECT 1 FROM pg_catalog.unnest(p.proconfig) AS setting WHERE setting LIKE 'search\_path=%') AND NOT EXISTS (SELECT 1 FROM pg_catalog.pg_depend AS e WHERE e.classid = 'pg_catalog.pg_proc'::pg_catalog.regclass AND e.objid = p.oid AND e.deptype = 'e') ORDER BY 1, 2;
Passing these checks does not guarantee that the upgrade succeeds or that your applications are compatible with the target version. Only a rehearsal on a staging deployment runs the whole conversion.
Planning the upgrade window
Use your staging rehearsal to estimate a maintenance window. The conversion time depends mainly on the number of databases and objects. Rebuilding an HA member depends on the size of the database. Workload and transaction-log volume affect the checks and the catch-up. Allow time for the whole upgrade task, not only the time that writes are blocked.
Start expiration sets the latest time that a queued upgrade can start. It defaults to 5 minutes after the request, and you can set it up to 24 hours ahead. Running operations, such as a backup, and earlier queued requests delay the start. If the upgrade does not start before its expiration time, it does not run, and its task shows Expired. Expiration does not stop an upgrade that has started. For example, a queued request expires at 22:30 UTC. A task that starts at 22:25 UTC can continue past that time.
While the upgrade runs, other requests for the deployment wait until it finishes. These include manual and automatic scaling, configuration changes, changes to users and passwords, changes to allowed IP addresses, and backups. Do not delete the deployment while the upgrade task is queued or running.
Do not create read-only replicas until the task completes. The service does not block such requests. If you create one, the upgrade can fail its prechecks, or the new replica cannot replicate from the upgraded deployment. Delete such a replica, and create a new one after the task completes.
Upgrading in the UI
- Create and verify an on-demand backup. The upgrade does not take one.
- On the deployment's Overview page, click Upgrade major version.
- In the Guidance step, review the guidance, select I have read the guidance and am ready to proceed with the upgrade, and click Next. To learn when reads and writes are unavailable, see Availability during an upgrade.
- In Upgrade to version, select the target version. If more than one target is available, the list selects the newest one by default. The Read-replicas notice in this step is out of date. The upgrade fails its prechecks while any read-only replica is associated with the deployment, so promote or delete every replica before you upgrade.
- In Expiration for starting upgrade, select how long the request can wait to start. The default is 5 minutes.
- Click Upgrade. While the request waits or runs, the Version row on the Overview page shows Upgrade queued or Upgrade in progress.
- Monitor the task in Recent tasks. As soon as writes resume, refresh statistics as described in After the upgrade. When the task shows Completed, complete the remaining steps in that section. If it shows Failed, see Troubleshooting. If it shows Expired, the upgrade did not start, and you can submit it again with a later expiration.
Upgrading through the API
Send a PATCH request to the /deployments/{id}/version endpoint. Set version to the target major version, and set the start expiration with the expiration_datetime query parameter:
curl -X PATCH \
'https://api.{region}.databases.cloud.ibm.com/v5/ibm/deployments/{id}/version?expiration_datetime=<EXPIRATION_TIME>' \
-H 'Authorization: Bearer <IAM_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{"version": "18"}'
Replace <EXPIRATION_TIME> with a UTC time in ISO 8601 format, such as 2026-11-03T22:30:00Z, from 5 minutes to 24 hours after the request. If you omit expiration_datetime, the request expires if
it does not start within 5 minutes.
A successful request returns a task. An accepted request is not a completed upgrade. Use the task ID with Get information about a task to monitor the upgrade. To list
the available targets, use Discover capability information from a deployment with the versions capability. For all parameters, see the
API reference.
Upgrading through the CLI
Use version 0.20.0 or later of the Cloud Databases CLI plug-in. Set the start expiration with --expire-in or --expire-at, from 5 minutes to 24 hours after the request. The default is 5 minutes.
ibmcloud cdb deployment-version-upgrade <NAME|CRN> <TARGET_VERSION> --expire-in 1h --nowait
Without --nowait, the command waits until the task completes or fails. It does not return if the upgrade expires before it starts, so use --nowait in scripts. With --nowait, the command returns after
the service accepts the request. Monitor the task with deployment-tasks-list:
ibmcloud cdb deployment-tasks-list <NAME|CRN>
For all parameters, run ibmcloud cdb deployment-version-upgrade --help.
Upgrading through Terraform
Use IBM Cloud® Terraform provider version 1.79.2 or later. Set version to an available target, review the plan, and apply it. The plan fails if the target is not available for an in-place upgrade.
Set the resource's update timeout long enough for the whole upgrade, and to at least 5 minutes. The provider also uses this timeout, up to 24 hours, as the start expiration. A queued upgrade can therefore start at any time until the timeout
elapses. Apply the change when no backup or other operation is running or queued, so that the upgrade starts immediately. If the apply times out after the upgrade starts, the upgrade continues. If the upgrade is still queued when the timeout
elapses, it expires and does not run. Wait for the task to finish before you run terraform plan or terraform apply again.
If the upgrade task fails, complete Troubleshooting before you run terraform apply again. Terraform submits the upgrade again whenever the deployment still reports the source version.
After an upgrade outside Terraform, set version to the new version, or remove version from the configuration. Upgrades outside Terraform include upgrades in the UI, CLI, or API, and a forced upgrade.
Until you update the configuration, every terraform plan and terraform apply that includes the deployment fails, preventing changes to other resources in the same configuration. The error is similar to Version 14 is not a valid upgrade version or No available upgrade versions for version 18.
The version_upgrade_skip_backup argument is not supported for PostgreSQL. For more information, see the database resource reference.
After the upgrade
As soon as writes resume, make sure that optimizer statistics are complete. Do not wait for the task to complete. Writes have resumed when SHOW default_transaction_read_only; returns off in a new session. In each
database, the following query lists your tables and materialized views that have no statistics. Run ANALYZE <table_name>; for each listed relation. If your user does not have permission to analyze a relation, run the command
as the relation owner. Analyzing a partitioned table also analyzes its partitions.
SELECT c.oid::pg_catalog.regclass AS relation_name
FROM pg_catalog.pg_class AS c
WHERE c.relkind IN ('r', 'm', 'p')
AND c.reltuples < 0
AND NOT c.relispartition
AND c.relpersistence <> 't'
AND c.relnamespace NOT IN ('pg_catalog'::pg_catalog.regnamespace,
'information_schema'::pg_catalog.regnamespace)
AND NOT EXISTS (SELECT 1
FROM pg_catalog.pg_depend AS e
WHERE e.classid = 'pg_catalog.pg_class'::pg_catalog.regclass
AND e.objid = c.oid
AND e.deptype = 'e');
On PostgreSQL 17 and later, ANALYZE can fail with an error such as function ... does not exist. In that case, fix the function that self-check query 17 lists, and run ANALYZE again. A database-wide ANALYZE that stops with an error does not analyze the remaining tables. On PostgreSQL 18, also analyze each table that has statistics objects, as listed by the psql command \dX, and each table that contains PostGIS columns,
because the upgrade does not transfer these statistics.
After the task shows Completed:
-
Confirm that the deployment's version in the console or API shows the target version, and that
SHOW server_version;returns it. Run self-check query 1, and confirm that every HA member is streaming. Also confirm that writes resumed in every database: the following query returns no rows. If an HA member is missing, a row namedpg_basebackupremains, or the query lists a database, contact support.SELECT d.datname AS database_name, s.setconfig FROM pg_catalog.pg_db_role_setting AS s JOIN pg_catalog.pg_database AS d ON d.oid = s.setdatabase WHERE s.setrole = 0 AND d.datallowconn AND NOT d.datistemplate AND EXISTS (SELECT 1 FROM pg_catalog.unnest(s.setconfig) AS c(setting) WHERE c.setting LIKE 'default_transaction_read_only=%'); -
Update extensions. The upgrade keeps each extension at its installed version. In each database, list the extensions that have a newer version, and update each one:
SELECT name, installed_version, default_version FROM pg_catalog.pg_available_extensions WHERE installed_version <> default_version;ALTER EXTENSION <extension_name> UPDATE;If you upgraded from PostgreSQL 14, 15, or 16 to PostgreSQL 17 or later, updating
pg_stat_statementsrenames itsblk_read_timeandblk_write_timecolumns toshared_blk_read_timeandshared_blk_write_time. Update the queries that read these columns. -
Restore extensions and masking as described in preparation. Reload the
UNLOGGEDtables from the contents that you exported. Set eachUNLOGGEDsequence past the values in use, for example withSELECT setval('<sequence_name>', (SELECT max(<column_name>) FROM <table_name>));. Recreate the logical replication slots, subscriptions, and read-only replicas that you removed, and enable the subscriptions that you disabled. Reconnect consumers, and resynchronize data before you resume dependent applications. -
If you set
default_transaction_read_onlyfor a database, set it again. The upgrade resets this setting on every database, so those databases accept writes again. -
Confirm that the post-upgrade backup completed. It copies the whole database, so it can take several hours. If you need point-in-time recovery for changes made after the upgrade, keep application writes paused until then.
-
If you upgraded from PostgreSQL 14 or 15 to PostgreSQL 16 or later, run the role query in Role privilege issues during version upgrades. Grant the listed roles before you resume jobs that manage roles.
-
If Terraform manages the deployment and you upgraded outside Terraform, set
versionto the new version or remove it, as described in Upgrading through Terraform. -
Resume the application writes and scheduled jobs that you paused. Restore the settings that you changed for the upgrade, such as
synchronous_commit. -
Rebuild objects that depend on Unicode data, because newer PostgreSQL versions use newer Unicode data:
- Unless you upgraded from PostgreSQL 16 to PostgreSQL 17, check indexes, materialized views, check constraints, and partition keys whose expressions use
normalize()oris_normalized(). Only text that contains characters that are new in the later Unicode version is affected. - If you upgraded from PostgreSQL 17 to PostgreSQL 18, also check objects that use
unicode_assigned(). Also check objects that apply regular expressions,ILIKE,lower(),upper(), orinitcap()to text in the built-inC.UTF-8locale. Thepg_c_utf8collation and databases that use thebuiltinlocale provider withC.UTF-8use this locale. - If you upgraded to PostgreSQL 18 and a database uses the
icuorbuiltinlocale provider, rebuild its full-text search andpg_trgmindexes.
Run
REINDEX INDEX <index_name>;for each affected index andREFRESH MATERIALIZED VIEW <view_name>;for each affected materialized view. Make sure that rows still satisfy affected check constraints and are in the correct partitions. - Unless you upgraded from PostgreSQL 16 to PostgreSQL 17, check indexes, materialized views, check constraints, and partition keys whose expressions use
-
Compare application behavior and performance with your staging results.
Troubleshooting
If the service rejects a request, the error appears immediately. The UI usually shows Upgrade failed with the reason, the API returns the error message, and the CLI prints the error with a hint. Correct the request, and submit it again.
If the task shows Expired, the upgrade did not start, and the deployment is unchanged. Submit the upgrade again when no other operation is running or queued, or set a later start expiration.
A task that later shows Failed does not identify the failed check or step, and the UI does not show the cause. Depending on the failed step, the deployment can be in one of these states:
- On the source version: The deployment runs the source version and accepts writes. A failed precheck leaves the deployment in this state, and the service restores write access after most failures during read-only mode. A
failed attempt can still have lasting effects. It can remove
old_snapshot, emptyUNLOGGEDtables, resetUNLOGGEDsequences, and leave a gap in PITR coverage. It can also reset database-leveldefault_transaction_read_onlysettings, so those databases accept writes again. If you setdefault_transaction_read_onlytoonfor any database, a failed attempt also ends all client sessions, even when a precheck fails. - On the target version: The deployment runs the target version, but the upgrade is incomplete until support completes it. The console and API can still show the source version. Until then, the HA members might not run, and backups and transaction-log archiving can fail. If self-check query 1 or 5 shows a problem, pause application writes until support completes the upgrade.
- Impaired: The deployment remains read-only or unavailable, or runs without its HA members, until support recovers it. A failure in the conversion or restart phase can leave the HA members stopped until support restarts them, even after writes resume.
To identify the state, connect as admin and run SHOW server_version;. In each database that you use, run SHOW default_transaction_read_only;. Run self-check query 1 to confirm that every HA member is streaming. If admin cannot connect after the task fails, for example because of an authentication error, contact support. The upgrade does not change your credentials.
If the deployment accepts writes, runs the source version, and every HA member is streaming, review the prechecks and run the self-check queries. Resolve every unmet item, take an on-demand backup, and then submit the upgrade again. In every other case, or if the next attempt also fails, contact support before you retry or make other changes. Include:
- The deployment CRN and the failed task ID. Record the task ID when the task fails, because task lists show only recent tasks.
- The source and requested target PostgreSQL versions, and the output of
SHOW server_version;. - The approximate failure time and time zone.
- Whether applications can connect, read, and write.
- The output of the self-check queries.
- The CRN of each related read-only replica, including any recently promoted or deleted replica.
If the request fails with an internal error, contact support. In the UI, this error shows Upgrade failed with the message Failed to create the in-place upgrade task. The UI also displays this message for other
rejected requests when it cannot display the actual reason. To see the reason, submit the request by using the CLI or API. Also contact support if the task stays in progress much longer than in your rehearsal. After a failure, the task can
stay in progress (status running in the CLI and API) while the service attempts recovery.
Remove only the objects that the preparation steps and the self-check queries list. Do not drop other objects to get past a check. Contact support instead.
Upgrading from a read-only replica
Create a read-only replica from the source deployment and wait for the replica to synchronize. Then, promote and upgrade the replica by using the
/remotes/promotion endpoint. Set version to a target that the replica lists in its capabilities:
curl -X POST \
https://api.{region}.databases.cloud.ibm.com/v5/ibm/deployments/{id}/remotes/promotion \
-H 'Authorization: Bearer <IAM_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"promotion": {
"version": "18",
"skip_initial_backup": false
}
}'
Set skip_initial_backup to true only if you want to skip the initial backup that promotion creates. This setting can shorten the promotion task, but recovery from the promoted deployment then requires a later successful
scheduled or on-demand backup.
If a promotion with a version upgrade fails, the service disables the promoted deployment. Contact support. Rehearse a promotion with a version upgrade on a replica of a staging deployment before you rely on it.
Upgrading by restoring a backup
Restore a backup to a deployment that uses a supported target version. If you restore a backup by using the CLI or API and do not specify the resource allocation parameters, the new deployment uses the resource allocations that the source deployment had when the backup was created. In the UI, the restore page starts with the source deployment's current allocations, which you can change.
Restoring a backup in the UI
From Backups and restore in the deployment dashboard, click Restore backup for the backup that you want to use. On the page that opens, select a supported target version in Database version, and configure the options for the new deployment. Then, click Restore backup.
Restoring a backup through the CLI
Create the deployment with the target version and backup ID in the -p JSON argument:
ibmcloud resource service-instance-create example-upgrade databases-for-postgresql standard us-south \
-p '{
"backup_id": "crn:v1:bluemix:public:databases-for-postgresql:us-south:a/54e8ffe85dcedf470db5b5ee6ac4a8d8:1b8f53db-fc2d-4e24-8470-f82b15c71717:backup:06392e97-df90-46d8-98e8-cb67e9e0a8e6",
"version": "18"
}' \
--service-endpoints "public"
Restoring a backup through the API
Use the Resource controller API to restore a backup into a new deployment that runs the target version. Specify the deployment name, location, resource group, plan, backup ID, and target version:
curl -X POST \
https://resource-controller.cloud.ibm.com/v2/resource_instances \
-H 'Authorization: Bearer <IAM_TOKEN>' \
-H 'Content-Type: application/json' \
-d '{
"name": "my-instance",
"target": "bluemix-us-south",
"resource_group": "5g9f447903254bb58972a2f3f5a4c711",
"resource_plan_id": "databases-for-postgresql-standard",
"parameters": {
"backup_id": "crn:v1:bluemix:public:databases-for-postgresql:us-south:a/54e8ffe85dcedf470db5b5ee6ac4a8d8:1b8f53db-fc2d-4e24-8470-f82b15c71717:backup:06392e97-df90-46d8-98e8-cb67e9e0a8e6",
"version": "18"
}
}'
Forced upgrade
After the end-of-life date, all active Databases for PostgreSQL deployments that run the deprecated version are automatically upgraded to the next supported version.
A forced upgrade is an IPMVU, so complete the IPMVU preparation before the end-of-life date.
Upgrade before the end-of-life date to avoid the following risks:
- No SLAs are provided for this type of forced upgrade.
- You might experience some data loss.
- Your application might experience prolonged downtime.
- Your application might stop working if it is incompatible with the new version.
- You cannot control the timing of when this upgrade will happen for your deployment.
- There is no rollback process for this forced upgrade.
- If Terraform manages the deployment and sets
version, everyterraform planfails after the forced upgrade until you updateversion.
For the end-of-life dates, see the version policy page.
Role privilege issues during version upgrades
In PostgreSQL 16 and later, a role with the CREATEROLE attribute, such as admin, needs the ADMIN OPTION on another role to manage it. Managing a role includes changing its password or attributes, dropping
it, and granting its membership. An upgrade from PostgreSQL 14 or 15 to 16 or later does not give admin this option on roles that existed before the upgrade. Jobs that manage these roles as admin, such as password
rotation, then fail. For more information, see the PostgreSQL 16 release notes, role attributes and role grants.
For example, a password change fails with:
ERROR: permission denied to alter role
DETAIL: To change another role's password, the current user must have the CREATEROLE attribute and the ADMIN option on the role.
Before you upgrade, run the following query as admin. It lists the roles that admin cannot manage after the upgrade, except users that you create in the UI or with the CLI or API. On PostgreSQL 16 and later, admin cannot change those users either, so manage them in the UI or with the CLI or API.
SELECT r.rolname AS role_name,
pg_catalog.pg_has_role(r.oid, 'admin'::name, 'MEMBER') AS member_of_admin
FROM pg_catalog.pg_roles AS r
WHERE NOT r.rolsuper
AND NOT r.rolreplication
AND r.rolname <> 'admin'
AND r.rolname !~ '^(pg_|ibm-)'
AND NOT EXISTS (
SELECT 1
FROM pg_catalog.pg_auth_members AS m
JOIN pg_catalog.pg_roles AS b ON b.oid = m.roleid
WHERE m.member = r.oid
AND b.rolname IN ('ibm-cloud-base-user', 'ibm-cloud-base-user-ro'))
AND NOT pg_catalog.pg_has_role(r.oid, 'MEMBER WITH ADMIN OPTION')
ORDER BY 1;
To keep managing a listed role, grant it to admin before you upgrade:
GRANT <role_name> TO admin WITH ADMIN OPTION;
This grant fails for a role whose member_of_admin value is t, because that role is a member of admin. After the upgrade, admin cannot manage such a role.
After an upgrade to PostgreSQL 16 or later, run the query again. For each listed role whose member_of_admin value is f, run the following helper while connected to ibmclouddb or any database except postgres.
Do not include other roles, because one failed grant cancels the whole call. You can run the helper safely more than once:
SELECT grant_admin_option_to_roles('role1', 'role2', 'role3');
Both grants also make admin a member of the role, so admin can use the role's privileges.