Post

PostgreSQL Sequence Conflicts After an Upgrade: How to Fix Them

Upgrading PostgreSQL can leave sequences out of sync with table max values, causing duplicate-key errors and blocked sign-ins, fixed here with SQL and bash.

After upgrading from PostgreSQL 15 to 16, some sequences no longer matched the max value in their table: duplicate-key errors on insert, and in one case, blocked sign-ins. Here’s the SQL function and the bash script I used to find and fix every misaligned sequence.

Why sequences drift after an upgrade

A PostgreSQL sequence is a database object that generates integer values, most often used for primary keys. During a major-version upgrade, sequence state doesn’t always carry over in step with the data in the tables it feeds, so a sequence can end up behind the actual max value already in use, and the next insert collides with an existing key.

A function that resets sequences to match table data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE OR REPLACE FUNCTION reset_sequences_to_max(schema varchar default 'public', dry_run bool default true)
RETURNS void AS $$
DECLARE
    r RECORD;
    query TEXT;
BEGIN
    -- Loop through all tables
    FOR r IN SELECT t.table_schema, t.table_name, column_name, sequence_name
             FROM information_schema.tables t
             JOIN information_schema.columns c ON t.table_name = c.table_name
             JOIN information_schema.sequences s ON 'nextval('''||s.sequence_name||'''::regclass)' = c.column_default
             WHERE t.table_schema = schema
    LOOP
        -- Construct the dynamic query to set the sequence value
        query := format('SELECT setval(''%I'', (SELECT MAX(%I) FROM %I.%I) - 1);',
                         r.sequence_name, r.column_name, r.table_schema, r.table_name);

        -- Execute the dynamic query
        if dry_run then
            raise notice'Run query: %', query;
        else
            EXECUTE query;
        end if;

    END LOOP;
END;
$$ LANGUAGE plpgsql;

It loops through every table in the given schema, builds a setval call for each sequence, and either prints the query for review or runs it, depending on dry_run.

1
SELECT reset_sequences_to_max('public', false); -- set dry_run to false to execute

A bash script for resetting sequences across many databases

For multiple databases at once, generate the setval statements directly from the catalog and run them per database:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cat > /tmp/reset.sql << EOL
-- SQL commands to reset sequences
 SELECT
     'SELECT SETVAL(' ||
        quote_literal(quote_ident(sequence_namespace.nspname) || '.' || quote_ident(class_sequence.relname)) ||
        ', COALESCE(MAX(' ||quote_ident(pg_attribute.attname)|| '), 1) ) FROM ' ||
        quote_ident(table_namespace.nspname)|| '.'||quote_ident(class_table.relname)|| ';'
 FROM pg_depend
     INNER JOIN pg_class AS class_sequence
         ON class_sequence.oid = pg_depend.objid
             AND class_sequence.relkind = 'S'
     INNER JOIN pg_class AS class_table
         ON class_table.oid = pg_depend.refobjid
     INNER JOIN pg_attribute
         ON pg_attribute.attrelid = class_table.oid
             AND pg_depend.refobjsubid = pg_attribute.attnum
     INNER JOIN pg_namespace as table_namespace
         ON table_namespace.oid = class_table.relnamespace
     INNER JOIN pg_namespace AS sequence_namespace
         ON sequence_namespace.oid = class_sequence.relnamespace
 ORDER BY sequence_namespace.nspname, class_sequence.relname;
EOL
1
2
3
4
for db in $databases; do
    psql -Atq -f /tmp/reset.sql -d $db -o /tmp/$db.sql
    psql -f /tmp/$db.sql -d $db
done

The principle

Back up before running either of these against anything real, and use the function’s dry_run default to see the generated setval statements before you execute them. Sequence drift after an upgrade is common enough to check for by default, not just when sign-ins start failing.

This post is licensed under CC BY 4.0 by the author.