Skip to main content

Postgres Sequence Out Of Sync

Symptom

Insert operations start failing with duplicate-key or unique-constraint errors even though the application is not intentionally reusing the same id values. The original January 25, 2026 incident surfaced this pattern during OMR result writes, but the database failure mode is generic.

Likely Causes

  • a past script inserted ids manually and never advanced the sequence;
  • a bulk import or restore loaded rows without resetting sequence state;
  • one table switched from manual ids to auto-increment while older rows still held larger values.

Diagnosis

Compare the sequence state with the actual maximum row id.

Representative SQL:

SELECT pg_get_serial_sequence('exam_results', 'examResultId');

SELECT last_value
FROM public."exam_results_examResultId_seq";

SELECT MAX("examResultId")
FROM exam_results;

If last_value is below the table's real maximum id, the next generated values will eventually collide with existing rows.

Fix

Reset the sequence to the current maximum row id.

Representative SQL:

SELECT setval(
'public."exam_results_examResultId_seq"',
(SELECT MAX("examResultId") FROM exam_results)
);

Apply the same check to related child tables that use their own sequences.

Prevention

  • Treat setval(..., max(id)) as mandatory follow-up after manual id inserts, data loads, or restore jobs.
  • Keep one id-generation strategy per table. Mixing manual ids with sequence-backed ids creates delayed failures.
  • When the same unique-id error appears across many rows at once, inspect the sequence before debugging application payloads.

Source Incident Detail

The January 25, 2026 source incident happened in the OMR grading result path. A previous Python-script workflow inserted examResultId manually into JSON/imported rows. Later, the FastAPI/Prisma path stopped specifying ids and relied on the Postgres sequence.

DetailValue
Affected tableexam_results
User-visible symptomOMR grading could not finish because result inserts failed
Errorunique constraint failed on examResultId
Historical causemanual ids inserted through the earlier Python/JSON upload path
Immediate repairsetval() sequence reset to the table maximum

The useful diagnosis signal was that the same unique-id error appeared for many students, not one malformed row. That pointed to the id generator rather than the payload.

The source record compared old and new id ranges:

ObservationMeaning
older rows had ids around 2909-2920manual inserts had advanced row ids
newer sequence-backed rows were around 159-173, then 635-661sequence had not caught up
collision began as the sequence reached existing historical idsdelayed failure from mixed id strategies

Apply the same reasoning after data restores, manual imports, table copies, or any migration that writes ids explicitly.