Error de PostgreSQL: deadlock detected
ERROR: deadlock detected
DETAIL: Process 18461 waits for ShareLock on transaction 1064; blocked by process 18463.
Process 18463 waits for ShareLock on transaction 1063; blocked by process 18461.
HINT: See server log for query details.
CONTEXT: while updating tuple (0,3) in relation "accounts"
Dos (o más) transacciones están cada una esperando un bloqueo que la otra sostiene. Ninguna puede avanzar nunca, así que PostgreSQL detecta el ciclo, elige una transacción como víctima y la aborta con este error. La otra transacción continúa con normalidad.
Qué significa este error
Los bloqueos a nivel de fila en PostgreSQL se sostienen hasta que la transacción hace commit o rollback. Si la transacción A bloquea la fila 1 y luego quiere la fila 2, mientras que la transacción B sostiene la fila 2 y quiere la fila 1, forman un ciclo: un deadlock. Cuando un backend ha estado esperando un bloqueo durante más de deadlock_timeout (por defecto: 1 segundo), PostgreSQL ejecuta una comprobación de deadlock; si encuentra un ciclo, aborta a uno de los participantes con el SQLSTATE 40P01, hermano de 40001.
Un matiz importante: la base de datos ya ha resuelto la situación para cuando ves el error. Nada está corrupto y la transacción superviviente completó su espera de bloqueo. Lo que queda es un problema de la aplicación — la transacción abortada se revirtió y su trabajo se perdió, así que tu aplicación debe estar preparada para reintentarla.
Causas comunes
- Actualizar las mismas filas en órdenes distintos. El caso clásico: el trabajo A actualiza la cuenta 1 y luego la cuenta 2, el trabajo B actualiza la cuenta 2 y luego la cuenta 1. Dos escritores multifila cualesquiera sin un orden acordado pueden hacer deadlock.
- Tocar tablas en órdenes distintos entre rutas de código — p. ej. una ruta escribe
ordersy luegoinventory, otra escribeinventoryy luegoorders. - Claves foráneas. Insertar o actualizar una fila hija toma un bloqueo compartido sobre la fila padre referenciada. Mezclado con actualizaciones concurrentes del padre, esto produce deadlocks que parecen misteriosos porque ninguna consulta menciona ambas tablas.
- Ascensos de bloqueo: leer una fila con
SELECT ... FOR SHAREy actualizarla después permite que dos transacciones adquieran el bloqueo compartido y luego se bloqueen mutuamente en el ascenso. - Las transacciones largas no causan deadlocks por sí solas, pero sostienen bloqueos durante más tiempo y amplían la ventana para todos los patrones anteriores.
Cómo diagnosticarlo
El error en sí lleva casi todo lo que necesitas:
DETAILlista los ID de proceso y para qué transacción esperaba cada uno.CONTEXTa menudo nombra la tupla y la relación que se estaba escribiendo.- El log del servidor en ese timestamp registra las consultas implicadas — eso es a lo que apunta el HINT.
Configura log_lock_waits = on para registrar también cualquier espera de bloqueo más larga que deadlock_timeout, lo que te muestra los casos que estuvieron a punto de colisionar, no solo las colisiones. Para investigar en vivo quién bloquea a quién:
SELECT waiting.pid AS waiting_pid, waiting.query AS waiting_query,
blocking.pid AS blocking_pid, blocking.query AS blocking_query
FROM pg_stat_activity waiting
JOIN pg_stat_activity blocking
ON blocking.pid = ANY (pg_blocking_pids(waiting.pid));
Cómo solucionarlo
- Acordad un orden global de bloqueo. Cuando una transacción escribe varias filas, adquiere los bloqueos en un orden determinista primero:
SELECT id FROM accounts WHERE id IN (7, 3, 42) ORDER BY id FOR UPDATE; -- luego los UPDATE, en cualquier orden - Tocad las tablas en el mismo orden en todas partes (documentad el orden; no importa cuál sea, solo que sea consistente).
- Mantén las transacciones cortas. Nunca esperes entrada del usuario, llamadas HTTP o colas mientras sostienes bloqueos de fila.
- Reintenta ante el SQLSTATE
40P01— la transacción completa, no solo la sentencia fallida, con un pequeño backoff y un número acotado de intentos. - Cuando sea posible, colapsa el patrón leer-modificar-escribir en una sola sentencia (
UPDATE ... SET x = x - 1 WHERE ...): una sola sentencia adquiere sus bloqueos en una sola pasada y es mucho más difícil que haga deadlock.
