Error de PostgreSQL: violates foreign key constraint
ERROR: insert or update on table "orders" violates foreign key constraint "orders_customer_id_fkey"
DETAIL: Key (customer_id)=(42) is not present in table "customers".
-- direzione opposta:
ERROR: update or delete on table "customers" violates foreign key constraint "orders_customer_id_fkey" on table "orders"
DETAIL: Key (id)=(42) is still referenced from table "orders".
Una clave foránea falló en una de sus dos direcciones: una fila hija apunta a un padre que no está ahí, o una fila padre se está eliminando mientras los hijos aún la referencian. La redacción te dice cuál — "is not present in" frente a "is still referenced from" — y el DETAIL nombra el valor de clave exacto.
Qué significa este error
Una clave foránea es una promesa comprobada en ambos extremos:
- "insert or update on child": escribiste una fila hija cuyo valor referenciador no tiene una fila padre correspondiente. La escritura se rechazó.
- "update or delete on parent": eliminar (o recodificar) el padre dejaría huérfanos a hijos existentes. Por defecto (
NO ACTION) PostgreSQL se niega; la restricción puede en su lugar declararON DELETE CASCADE,SET NULL, etc. — una decisión de schema tomada al crear la restricción, no en tiempo de consulta.
Causas comunes
- ID incorrecto o desfasado desde la aplicación — el padre se borró, o el ID vino de otro entorno.
- Orden entre transacciones: el INSERT del padre se ejecutó en una transacción distinta que aún no ha hecho commit; el insert del hijo no puede verlo.
- Cargas masivas en el orden incorrecto (hijos antes que padres), o cargas parciales.
- Borrar un padre sin una estrategia para sus hijos — el segundo mensaje.
Cómo diagnosticarlo
El DETAIL te da el nombre de la restricción, el valor de clave, y ambos nombres de tabla. A partir de ahí:
-- La restricción, al completo:
\d orders
-- Huérfanos ya presentes (para entender la magnitud del problema en las cargas):
SELECT o.*
FROM orders o
LEFT JOIN customers c ON c.id = o.customer_id
WHERE c.id IS NULL;
Cómo solucionarlo
- Dirección hijo: arregla el valor o el orden — crea el padre primero, en la misma transacción o en una anterior ya confirmada.
- Cargas masivas: carga los padres antes que los hijos; si los datos genuinamente llegan intercalados, haz la restricción deferrable y compruébala en el commit:
ALTER TABLE orders ALTER CONSTRAINT orders_customer_id_fkey DEFERRABLE INITIALLY DEFERRED; - Dirección padre: borra los hijos primero, o declara la intención en el schema (
ON DELETE CASCADEoSET NULL). Trata CASCADE con respeto: un DELETE puede propagarse silenciosamente a un montón de filas — hazlo una elección de diseño consciente, no un apaño rápido. - Muchos sistemas esquivan la dirección padre por completo con borrados lógicos (una columna
deleted_at) para entidades de las que cuelgan otros datos.
🔍
Lectura relacionada: Elegir el índice adecuado — PostgreSQL no indexa automáticamente el lado referenciador de una clave foránea; los borrados en el padre escanean al hijo sin uno.
