PostgreSQL-Fehler: duplicate key value violates unique constraint
ERROR: duplicate key value violates unique constraint "users_email_key"
DETAIL: Key (email)=([email protected]) already exists.
Ein INSERT oder UPDATE hat versucht, eine zweite Zeile mit demselben Wert in einer Spalte (oder Spaltenmenge) zu erzeugen, die von einem Unique-Constraint oder Unique-Index abgedeckt ist. Die DETAIL-Zeile sagt Ihnen genau, welcher Constraint und welcher Wert — dort beginnt die Diagnose.
Was dieser Fehler bedeutet
Der Constraint hat funktioniert: PostgreSQL garantiert Eindeutigkeit auch unter Nebenläufigkeit, und dieser Fehler ist diese Garantie in Aktion. Zwei Dinge sind über das Offensichtliche hinaus wissenswert:
- Der Name in Anführungszeichen ist der Constraint- oder Indexname (
users_email_key,users_pkey, …).\d usersin psql zeigt, welche Spalten er abdeckt — einschließlich Ausdrucksindizes wielower(email), bei denen das Duplikat nicht buchstäblich identisch sein muss. - Wie jeder Fehler bricht er die aktuelle Transaktion ab: jede spätere Anweisung in derselben Transaktion schlägt mit "current transaction is aborted" fehl, bis Sie zurückrollen.
Häufige Ursachen
- Check-then-Insert-Races: zwei Anfragen führen beide
SELECTaus, beide sehen keine Zeile, beide führenINSERTaus. Unter Nebenläufigkeit muss eine von ihnen 23505 erhalten — der Check kann das nicht verhindern, nur das Insert selbst (siehe den Upsert-Fix unten). - Eine Sequenz, die nicht mit der Tabelle synchron ist — der Klassiker nach einem Datenimport oder Restore, der explizite IDs eingefügt hat, ohne die Sequenz weiterzustellen. Neue Inserts ziehen bereits verwendete IDs und schlagen am Primärschlüssel fehl, einmal pro veralteter ID.
- Client-Wiederholungen, die ein INSERT erneut abspielen, das beim ersten Mal tatsächlich erfolgreich war (Timeout nach Commit, At-least-once-Queues).
- Massenladevorgänge, die echte Duplikate enthalten.
So diagnostizieren Sie ihn
-- Welcher Constraint, welche Spalten:
\d users
-- Verdacht "Sequenz zurückgeblieben" (Fehler auf der _pkey mit seriellen IDs):
SELECT max(id) FROM users;
SELECT last_value FROM users_id_seq;
-- wenn last_value <= max(id), ist dies die Ursache
Wenn der Constraint auf einem Geschäftsschlüssel (E-Mail, SKU, …) statt auf dem Primärschlüssel liegt, schauen Sie, woher der Wert in DETAIL kommt: derselbe Benutzer, der doppelt absendet, Retry-Logik oder zwei Codepfade, die dieselbe Entität erzeugen.
So beheben Sie ihn
- Insert-Races → Upsert. Lassen Sie das Insert selbst atomar entscheiden:
INSERT INTO users (email, name) VALUES ('[email protected]', 'Alice') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name; -- oder DO NOTHING, wenn das Duplikat einfach ignoriert werden soll - Veraltete Sequenz → neu synchronisieren:
SELECT setval(pg_get_serial_sequence('users', 'id'), (SELECT max(id) FROM users)); - Wiederholungen → Idempotenz: geben Sie Operationen einen clientseitig erzeugten eindeutigen Schlüssel und verwenden Sie darauf
ON CONFLICT DO NOTHING, sodass Wiederholungen zu No-Ops werden. - Massenladevorgänge: in eine Staging-Tabelle laden, deduplizieren (
SELECT DISTINCT ON (key) ...), dann einfügen. - Vermeiden Sie Catch-and-Ignore innerhalb einer größeren Transaktion: die fehlgeschlagene Anweisung hat sie bereits abgebrochen. Bevorzugen Sie
ON CONFLICT; wenn Sie wirklich versuchen-und-wiederherstellen müssen, kapseln Sie die Anweisung in einSAVEPOINT.
🔍
Weiterführend: Den richtigen Index wählen — Unique-, Partial- und Ausdrucksindizes und was sie beim Schreiben kosten.
