Niveles de aislamiento de transacciones en PostgreSQL en la práctica

PostgreSQL implementa tres niveles de aislamiento distintos: Read Committed, Repeatable Read y Serializable. (READ UNCOMMITTED existe por compatibilidad con el estándar SQL, pero se comporta exactamente igual que Read Committed: PostgreSQL nunca te muestra datos no confirmados.) Las diferencias solo importan bajo concurrencia, que es precisamente cuando son más difíciles de depurar, así que conviene conocerlas antes del incidente.

Read Committed: el nivel por defecto y su punto ciego

Cada sentencia ve una instantánea (snapshot) de la base de datos correspondiente al momento en que esa sentencia comenzó. Dos consecuencias:

Las soluciones idiomáticas en este nivel:

-- make the read-modify-write atomic in one statement:
UPDATE accounts SET balance = balance - 10 WHERE id = 1;

-- or lock the row at read time:
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;

Un detalle útil: cuando un UPDATE en Read Committed encuentra una fila ya modificada por una transacción concurrente que ya hizo commit, reevalúa su cláusula WHERE sobre la nueva versión y continúa. No obtienes ningún error, lo cual es cómodo y, en ocasiones, fuente de errores muy sutiles.

Repeatable Read: una única instantánea para toda la transacción

La instantánea se toma en la primera consulta y se mantiene hasta el commit: cada sentencia ve los mismos datos. En PostgreSQL esto también previene las lecturas fantasma (phantom reads), siendo más estricto de lo que exige el estándar SQL. El precio aparece en las escrituras:

ERROR:  could not serialize access due to concurrent update

Si intentas modificar una fila que una transacción concurrente cambió y confirmó después de que se tomara tu instantánea, PostgreSQL no puede fingir que ambos órdenes ocurrieron: aborta tu transacción. Tu aplicación debe estar preparada para reintentar la transacción completa. Repeatable Read es el nivel natural para lecturas consistentes de varias consultas: informes, exportaciones, copias de seguridad (pg_dump se ejecuta en este nivel).

Serializable: como si las transacciones se ejecutaran una a una

Serializable en PostgreSQL es Repeatable Read más SSI (serializable snapshot isolation): el motor rastrea las dependencias de lectura/escritura entre transacciones concurrentes y aborta una siempre que un ciclo haría que el resultado fuera imposible bajo cualquier orden serial. Detecta anomalías que Repeatable Read no puede: el caso de manual es el write skew (sesgo de escritura):

-- Two doctors, rule: at least one must stay on call.
-- T1: SELECT count(*) FROM oncall;  -- sees 2, so it's safe to leave
-- T2: SELECT count(*) FROM oncall;  -- sees 2, so it's safe to leave
-- T1: DELETE FROM oncall WHERE doctor = 'alice'; COMMIT;
-- T2: DELETE FROM oncall WHERE doctor = 'bob';   COMMIT;
-- Repeatable Read: both commit, zero doctors on call.
-- Serializable: one of them gets error 40001 and must retry.

Costes: algo de CPU y memoria para el rastreo de dependencias y, lo que es importante, falsos positivos: se pueden abortar transacciones "por si acaso". Todas las transacciones de la carga de trabajo deben ejecutarse en Serializable para que sus garantías se cumplan, y cada una de ellas necesita lógica de reintento.

Reintentar correctamente

Cómo elegir en la práctica

SituaciónElección razonable
OLTP típico con actualizaciones de filas concretasRead Committed + actualizaciones atómicas / SELECT FOR UPDATE donde importe
Lecturas de varias sentencias que deben ser mutuamente consistentesRepeatable Read
Invariantes que abarcan varias filas ("al menos uno", "la suma no debe superar", doble reserva)Serializable con reintentos, o bloqueo explícito si los puntos calientes son pocos y conocidos
Colas de trabajosRead Committed + FOR UPDATE SKIP LOCKED

Establece el nivel por transacción (BEGIN ISOLATION LEVEL SERIALIZABLE) o por sesión; mezclar niveles no supone problema, salvo que, como se ha indicado, las garantías de Serializable requieren que todas las transacciones relacionadas lo usen.

🔍 Lectura relacionada: VACUUM & bloat — las transacciones largas en cualquier nivel de aislamiento retrasan la limpieza de toda la base de datos.
🧯 Errores relacionados: could not serialize access, deadlock detected, lock timeout y current transaction is aborted — el que ves cuando falta la lógica de reintento.