Reset PostgreSQL sequences after importing data with explicit IDs
You finished a MySQL to PostgreSQL migration, the data looks right, and then the first insert your app makes blows up:
ERROR: duplicate key value violates unique constraint "users_pkey"
DETAIL: Key (id)=(1) already exists. The rows are fine. The problem is the sequence behind the primary key. This page explains why it happens and gives you a one-liner per table plus a script that fixes every table at once.
Why it happens
A PostgreSQL SERIAL or IDENTITY column draws new values
from a sequence, and the sequence is only advanced when Postgres itself supplies
the value. When you load a dump that already contains explicit id
values, the rows go in but the sequence counter never moves — it is still sitting
at 1. The next insert that omits the id asks the sequence for a
number, gets a low one that a migrated row already uses, and violates the primary
key. The fix is to fast-forward the sequence past the largest ID currently in the
table.
Fix one table
Use setval() together with pg_get_serial_sequence() so
you never have to know the sequence's real name — it works for both
SERIAL and IDENTITY columns:
SELECT setval(
pg_get_serial_sequence('users', 'id'),
(SELECT MAX(id) FROM users)
);
After this, MAX(id) is stored as "already used", so the next value
handed out is MAX(id) + 1. One caveat: on an empty table
MAX(id) is NULL and setval() rejects it.
This version is safe whether the table has rows or not:
SELECT setval(
pg_get_serial_sequence('users', 'id'),
GREATEST(COALESCE((SELECT MAX(id) FROM users), 0), 1),
(SELECT COUNT(*) > 0 FROM users)
);
The third argument is is_called: true means "the value
is used, hand out the next one", false means "hand out this value
first". An empty table therefore restarts cleanly at 1.
Fix an identity column the native way
If the column is a true identity column (GENERATED ... AS IDENTITY,
Postgres 10+), you can restart it directly. Compute one past the current maximum
and run:
ALTER TABLE users ALTER COLUMN id RESTART WITH 1043;
This RESTART WITH form only works on identity columns; for a
SERIAL column stick with the setval() approach above.
Not sure which you have? In psql run \d users: an
identity column reads generated ... as identity, while a serial
column shows default nextval('users_id_seq').
Fix every table at once
After a full-database import you want to reset all of them. This block walks every serial and identity column in the current schema and fixes each sequence, empty tables included:
DO $$
DECLARE
r RECORD;
seq TEXT;
maxid BIGINT;
BEGIN
FOR r IN
SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
AND (column_default LIKE 'nextval(%' OR is_identity = 'YES')
LOOP
seq := pg_get_serial_sequence(
quote_ident(r.table_schema) || '.' || quote_ident(r.table_name),
r.column_name);
CONTINUE WHEN seq IS NULL;
EXECUTE format('SELECT COALESCE(MAX(%I), 0) FROM %I.%I',
r.column_name, r.table_schema, r.table_name)
INTO maxid;
EXECUTE format('SELECT setval(%L, %s, %s)',
seq, GREATEST(maxid, 1), maxid > 0);
END LOOP;
END $$;
It skips columns whose sequence is not owned by the table (a manually created
sequence used as a default), which pg_get_serial_sequence reports as
NULL. Run it once and every auto-numbered table is back in sync.
Avoid the problem entirely
This whole class of error only exists because a raw dump import leaves the sequences untouched. SwapSQL advances every sequence past your imported data as part of the conversion, so the first insert after you switch over just works.
Migrating a database? Convert MySQL to PostgreSQL with sequences set for you →