Error de 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 o UPDATE intentó crear una segunda fila con el mismo valor en una columna (o conjunto de columnas) cubierta por una restricción unique o un índice único. La línea DETAIL te dice exactamente qué restricción y qué valor — el diagnóstico empieza ahí.
Qué significa este error
La restricción funcionó: PostgreSQL garantiza la unicidad incluso bajo concurrencia, y este error es esa garantía disparándose. Dos cosas que conviene saber más allá de lo obvio:
- El nombre entre comillas es el nombre de la restricción o del índice (
users_email_key,users_pkey, …).\d usersen psql muestra qué columnas cubre — incluidos índices de expresión comolower(email), donde el duplicado puede no ser literalmente idéntico. - Como cualquier error, aborta la transacción actual: cada sentencia posterior en la misma transacción fallará con "current transaction is aborted" hasta que hagas rollback.
Causas comunes
- Carreras comprobar-luego-insertar: dos peticiones ejecutan ambas
SELECT, ambas ven que no hay fila, ambas hacenINSERT. Bajo concurrencia una de ellas tiene que obtener 23505 — la comprobación no puede arreglar esto, solo el propio insert puede (ver la solución con upsert más abajo). - Una secuencia desincronizada con la tabla — el clásico tras una importación o restauración de datos que insertó IDs explícitos sin avanzar la secuencia. Los nuevos inserts sacan IDs ya usados y fallan en la clave primaria, una vez por cada ID desfasado.
- Reintentos del cliente reproduciendo un INSERT que en realidad tuvo éxito la primera vez (timeout tras el commit, colas at-least-once).
- Cargas masivas que contienen duplicados genuinos.
Cómo diagnosticarlo
-- Qué restricción, qué columnas:
\d users
-- Sospecha de "secuencia rezagada" (error en la _pkey con id seriales):
SELECT max(id) FROM users;
SELECT last_value FROM users_id_seq;
-- si last_value <= max(id), esta es la causa
Si la restricción está sobre una clave de negocio (email, SKU, …) en vez de la clave primaria, mira de dónde viene el valor del DETAIL: el mismo usuario enviando dos veces, lógica de reintento, o dos rutas de código creando la misma entidad.
Cómo solucionarlo
- Carreras de inserción → upsert. Haz que el propio insert decida, atómicamente:
INSERT INTO users (email, name) VALUES ('[email protected]', 'Alice') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name; -- o DO NOTHING si el duplicado simplemente se ignora - Secuencia desfasada → resincronízala:
SELECT setval(pg_get_serial_sequence('users', 'id'), (SELECT max(id) FROM users)); - Reintentos → idempotencia: da a las operaciones una clave única generada por el cliente y usa
ON CONFLICT DO NOTHINGsobre ella, para que las repeticiones se conviertan en no-ops. - Cargas masivas: carga en una tabla de staging, deduplica (
SELECT DISTINCT ON (key) ...), y luego inserta. - Evita capturar-e-ignorar dentro de una transacción más grande: la sentencia fallida ya la ha abortado. Prefiere
ON CONFLICT; si realmente debes intentar-y-recuperar, envuelve la sentencia en unSAVEPOINT.
🔍
Lectura relacionada: Elegir el índice adecuado — índices únicos, parciales y de expresión, y qué cuestan en las escrituras.
