Situation: Production began throwing intermittent "duplicate key value violates unique constraint" errors after a migration. I would investigate three hypotheses: application race conditions, migration script bug, or pre-existing inconsistent data.Investigation steps (high level)1. Reproduce & scope: collect error traces, timestamps, app/node IDs, SQL statements, and transaction IDs from logs. Correlate with migration run time.2. Query the DB to find duplicates and the offending constraint.3. Review migration scripts and run history (idempotency, transaction boundaries, concurrent runs).4. Mitigate impact in prod quickly, then fix root cause and harden.SQL to locate duplicates and offending constraint- Find which constraint name is failing in application logs; or list unique constraints:sql
SELECT conname, conrelid::regclass, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE contype = 'u';
- Find duplicate key values for a specific unique index/columns (replace table, cols):sql
SELECT col1, col2, COUNT(*) AS cnt
FROM my_table
GROUP BY col1, col2
HAVING COUNT(*) > 1;
- Inspect full duplicate rows (choose the alive row and duplicates):sql
SELECT *
FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY col1, col2 ORDER BY created_at DESC, id) rn
FROM my_table
) t
WHERE rn > 1;
- If the error gives index name, find columns:sql
SELECT indexdef FROM pg_indexes WHERE indexname = 'my_unique_idx';
Checks on migration scripts- Idempotency: ensure INSERTs use INSERT ... ON CONFLICT ... DO NOTHING/UPDATE when appropriate.- Transaction scope: verify scripts wrap related steps in a transaction if needed (and avoid long-running transactions that block).- Concurrency: confirm migration runners cannot run concurrently (check job scheduler, CI/CD hooks); look for missing advisory locks.- Data checks: did script assume uniqueness that wasn't enforced yet? Did it create a non-deferrable constraint before cleaning duplicates?- Run history: check migration logs and timestamps to see partial failures or retries.Mitigation steps in production (fast, safe)1. Short-term: implement application retry/backoff for uniqueness errors if operations are idempotent.2. Prevent concurrent writers during cleanup: acquire a global advisory lock from app or quiesce writers (maintenance mode) for cleanup.3. Cleanup duplicates during low traffic: - Back up affected rows first. - Use the ROW_NUMBER query to delete duplicates safely:sql
WITH duplicates AS (
SELECT ctid
FROM (
SELECT ctid, ROW_NUMBER() OVER (PARTITION BY col1, col2 ORDER BY created_at DESC, id) rn
FROM my_table
) t WHERE rn > 1
)
DELETE FROM my_table WHERE ctid IN (SELECT ctid FROM duplicates);
4. Add the unique constraint/index after cleanup. To avoid locks, create the index concurrently:sql
CREATE UNIQUE INDEX CONCURRENTLY my_table_col1_col2_key ON my_table(col1, col2);
Note: CONCURRENTLY cannot run inside a transaction and still needs exclusivity for brief moments; ensure maintenance window if heavy write load.Longer term fixes- Make migrations idempotent and use advisory locks in migrations:sql
SELECT pg_try_advisory_lock(hashtext('migration_name'));
-- run migration
SELECT pg_advisory_unlock(hashtext('migration_name'));
- Add tests that simulate parallel runs and validate idempotency.- Add monitoring/alerts on constraint-violation error rates and surge of 400/500 errors.- Consider making constraint DEFERRABLE if transactional ordering requires it (requires careful schema planning).Why these steps- The duplicate-row queries identify whether data is already inconsistent (pre-existing).- Reviewing migration concurrency and idempotency distinguishes script bugs from race conditions.- Short-term mitigations minimize customer impact; adding indexes/constrains after cleanup prevents recurrence.Edge cases & cautions- Deleting duplicates must preserve business rules (merge vs drop).- CREATE INDEX CONCURRENTLY still can fail if new duplicates are inserted during the run — ensure writers are paused or use a two-step quiesce.- If constraints already exist but errors still happen, investigate application transaction isolation and retries causing lost updates.This approach quickly isolates root cause, resolves immediate production impact safely, and prevents recurrence via migration hardening and operational safeguards.