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:
- Dos
SELECTidénticos dentro de una misma transacción pueden devolver resultados distintos si alguien confirma cambios entre medias (lectura no repetible, non-repeatable read). - La clásica actualización perdida (lost update): dos transacciones leen ambas un saldo de 100, ambas calculan 100 − 10, ambas escriben 90. Una de las retiradas desaparece. Read Committed no previene este patrón cuando la lectura y la escritura son sentencias separadas.
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
- Reintenta ante los SQLSTATE
40001(serialization_failure) y40P01(deadlock_detected). - Reintenta la transacción entera, incluidas sus lecturas: los valores que leíste la primera vez son precisamente lo que puede haber dejado de ser cierto. Reejecutar solo la sentencia que falló es incorrecto.
- Mantén las transacciones reintentadas cortas y acotadas (intentos limitados, backoff pequeño). Una transacción que lee media base de datos seguirá colisionando indefinidamente.
- No envuelvas efectos secundarios (correos, llamadas HTTP) dentro de una transacción que pretendes reintentar.
Cómo elegir en la práctica
| Situación | Elección razonable |
|---|---|
| OLTP típico con actualizaciones de filas concretas | Read Committed + actualizaciones atómicas / SELECT FOR UPDATE donde importe |
| Lecturas de varias sentencias que deben ser mutuamente consistentes | Repeatable 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 trabajos | Read 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.
