Erreur PostgreSQL : duplicate key value violates unique constraint
ERROR: duplicate key value violates unique constraint "users_email_key"
DETAIL: Key (email)=([email protected]) already exists.
Un INSERT ou un UPDATE a tenté de créer une deuxième ligne avec la même valeur dans une colonne (ou un ensemble de colonnes) couverte par une contrainte d'unicité ou un index unique. La ligne DETAIL vous indique exactement quelle contrainte et quelle valeur — le diagnostic part de là.
Ce que signifie cette erreur
La contrainte a fonctionné : PostgreSQL garantit l'unicité même sous concurrence, et cette erreur est cette garantie qui se déclenche. Deux choses à savoir au-delà de l'évidence :
- Le nom entre guillemets est le nom de la contrainte ou de l'index (
users_email_key,users_pkey, …).\d usersdans psql montre quelles colonnes il couvre — y compris les index sur expression commelower(email), où le doublon peut ne pas être littéralement identique. - Comme toute erreur, elle annule la transaction en cours : toute instruction ultérieure dans la même transaction échouera avec « current transaction is aborted » jusqu'à ce que vous fassiez un rollback.
Causes fréquentes
- Courses vérifier-puis-insérer : deux requêtes exécutent toutes deux un
SELECT, ne voient toutes deux aucune ligne, font toutes deux unINSERT. Sous concurrence, l'une d'elles doit obtenir 23505 — la vérification ne peut pas résoudre cela, seule l'insertion elle-même le peut (voir la solution upsert ci-dessous). - Une séquence désynchronisée par rapport à la table — le cas classique après un import ou une restauration de données qui a inséré des ID explicites sans faire avancer la séquence. Les nouvelles insertions tirent des ID déjà utilisés et échouent sur la clé primaire, une fois par ID obsolète.
- Réessais du client rejouant un INSERT qui avait effectivement réussi la première fois (timeout après le commit, files d'attente « au moins une fois »).
- Chargements en masse contenant de véritables doublons.
Comment la diagnostiquer
-- Quelle contrainte, quelles colonnes :
\d users
-- Suspicion « séquence restée en arrière » (erreur sur la _pkey avec des id seriaux) :
SELECT max(id) FROM users;
SELECT last_value FROM users_id_seq;
-- si last_value <= max(id), c'est la cause
Si la contrainte porte sur une clé métier (email, SKU, …) plutôt que sur la clé primaire, regardez d'où vient la valeur du DETAIL : le même utilisateur qui soumet deux fois, une logique de réessai, ou deux chemins de code créant la même entité.
Comment la corriger
- Courses à l'insertion → upsert. Faites en sorte que l'insertion elle-même décide, de façon atomique :
INSERT INTO users (email, name) VALUES ('[email protected]', 'Alice') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name; -- ou bien DO NOTHING si le doublon doit simplement être ignoré - Séquence obsolète → resynchronisez-la :
SELECT setval(pg_get_serial_sequence('users', 'id'), (SELECT max(id) FROM users)); - Réessais → idempotence : donnez aux opérations une clé unique générée par le client et utilisez
ON CONFLICT DO NOTHINGdessus, afin que les rejeux deviennent des no-ops. - Chargements en masse : chargez dans une table de staging, dédupliquez (
SELECT DISTINCT ON (key) ...), puis insérez. - Évitez le catch-and-ignore à l'intérieur d'une transaction plus large : l'instruction en échec l'a déjà annulée. Préférez
ON CONFLICT; si vous devez vraiment tenter-et-récupérer, entourez l'instruction d'unSAVEPOINT.
🔍
À lire aussi : Choisir le bon index — les index uniques, partiels et sur expression, et ce qu'ils coûtent aux écritures.
