Watch at EaseStories. Answers. Perspective.

Technology · PRACTICAL GUIDE

PostgreSQL duplicate key on a generated ID? Prove sequence drift before setval

Read the violated constraint, identify who generated the key, resolve the owned sequence from the catalog, and stop writers before one targeted repair. A business-key duplicate or live race needs a different fix.

Original diagram separating a business-key collision from a generated-ID sequence drift after checking the constraint, column default, owned sequence and state.
Original illustration: identify the constraint and value generator before touching a sequence. Watch at Ease

A PostgreSQL duplicate-key error proves that one constrained value already exists. It does not prove the primary-key sequence is wrong.

Preserve the constraint and rejected value, identify whether the database or application generated it, then compare the catalog-resolved sequence with the table under controlled writer conditions.

Preserve the exact constraint and rejected value

Save the full error, especially the constraint name and DETAIL key/value. A duplicate email or SKU is an application or source-data collision; resetting an ID sequence cannot fix it.

Record the failing statement shape, whether the ID was omitted or supplied explicitly, the schema-qualified table, and what changed before the first failure: import, seed, restore, failover, replication or manual insert.

Prove which mechanism generates the column

Inspect the column default or identity definition. Ask the catalog with pg_get_serial_sequence('schema.table','id') instead of constructing a sequence name; the function supports owned serial and identity sequences and returns NULL when no sequence is associated.

If the application supplies the key, the default is not invoked. If the constraint covers several columns or a business key, stop the sequence branch and investigate retries, idempotency and conflicting source records.

Compare table and sequence state without advancing it

Read MAX(id) from the table and inspect the resolved sequence's last_value and is_called. Do not call nextval merely to inspect it: nextval advances atomically and its value is not reclaimed after an aborted transaction.

Check start, increment, minimum, maximum and cycle settings before using a recipe built for a positive increment of one. An empty table, renamed table, unowned sequence, descending sequence or partitioned design needs its own explicit decision.

Quiesce writers before a targeted repair

Use a maintenance window or otherwise stop application inserts and loaders that can race with MAX(id). PostgreSQL documents that setval becomes visible immediately and is not undone by rollback, so a transaction wrapper is not a substitute for controlling concurrent writers.

For a proven nonempty, increment-by-one owned sequence, set its current value to the table maximum with is_called true. For an empty table, choose the documented start value and is_called false, or use an appropriate identity restart. Never use TRUNCATE RESTART IDENTITY unless deleting the table's rows is actually intended.

Verify one generated insert and close the root cause

Restore one writer and perform a controlled insert that omits the generated column. Confirm the new key is unique, the intended row committed, and the sequence now moves in the expected direction. A gap is not evidence of corruption; PostgreSQL sequences are not gapless.

Then fix the cause: stop seed/import code from supplying generated IDs unless required, add an explicit post-load sequence reconciliation step, verify failover or logical-replication behavior, and alert on a generated value colliding with an existing primary key. If drift returns, do not keep pushing setval forward without finding the writer.

Original diagram showing writer quiescence, a catalog-resolved sequence repair, one generated insert and separate branches for empty or custom sequences and recurring drift.
Original illustration: repair one proven sequence boundary during a controlled maintenance window. Watch at Ease