PostgreSQL
MVCC, snapshots y visibilidad
Explica MVCC, snapshots, versiones de filas y reglas de visibilidad que permiten concurrencia sin bloquear todas las lecturas.
- Última actualización
- Actualizada
- Nivel
- Profundización
PostgreSQL
Explica MVCC, snapshots, versiones de filas y reglas de visibilidad que permiten concurrencia sin bloquear todas las lecturas.
Multi-Version Concurrency Control significa que PostgreSQL puede conservar varias versiones físicas de una misma fila durante un tiempo.
versión A creada por T1
↓ UPDATE de T2
versión A queda obsoleta para snapshots futuros
versión B contiene el nuevo valor
↓
cada transacción decide cuál puede verUn UPDATE no suele sobrescribir los bytes de la fila original. Crea una nueva tuple y marca la anterior como reemplazada. Un DELETE marca una versión como eliminada; no necesariamente retira inmediatamente su espacio físico.
Sin versionado, un lector podría necesitar esperar cada escritura para evitar observar datos a medio modificar. MVCC permite:
Esto mejora concurrencia en sistemas OLTP, pero genera versiones muertas que deben limpiarse y reciclarse.
Las tablas ordinarias utilizan almacenamiento heap. Cada versión de fila contiene metadata interna relacionada con:
Los nombres internos clásicos incluyen xmin y xmax, aunque no deben convertirse en una API de negocio sin comprender wraparound, freezing y semántica.
Un snapshot describe qué transacciones estaban confirmadas, activas o futuras desde la perspectiva de una consulta.
Regla conceptual:
¿la transacción creadora es visible?
+
¿la transacción que eliminó la versión todavía no es visible?
→ la tuple puede verseEl algoritmo real considera estados de commit, transaction IDs, subtransactions y hints. El modelo útil es que una tuple no es “visible para todos” de forma absoluta: depende del snapshot.
Un lector puede ver la versión anterior mientras otro writer crea una nueva.
Dos writers sobre la misma fila sí compiten. Uno puede esperar, reevaluar condiciones o fallar según nivel de aislamiento.
El lector solicita un row lock y entra deliberadamente en coordinación con writers.
MVCC reduce bloqueo entre lectura y escritura, no entre escrituras incompatibles.
UPDATE products
SET price = 120
WHERE id = 10;Conceptualmente:
price = 120.Una transacción antigua puede seguir viendo el precio anterior.
DELETE FROM sessions WHERE id = 10;La tuple queda invisible para snapshots nuevos después del commit, pero el espacio no se borra inmediatamente del archivo. Vacuum puede marcarlo reutilizable cuando ningún snapshot necesita esa versión.
Una versión obsoleta se denomina comúnmente dead tuple cuando ya no es visible para transacciones relevantes.
Consecuencias de acumularlas:
DELETE no reduce automáticamente el tamaño del archivo. El espacio suele reutilizarse dentro de la tabla.
Un índice apunta a ubicaciones del heap. Puede contener entradas hacia tuples que ya no son visibles para el snapshot.
Flujo de un index scan:
índice encuentra TID
↓
heap verifica versión y visibilidad
↓
se devuelve o descartaPor eso un índice no evita toda consulta al heap.
Un index-only scan puede devolver columnas almacenadas en el índice sin leer cada tuple del heap, pero necesita saber que la página es all-visible.
La visibility map registra páginas donde todas las tuples son visibles para todos los snapshots relevantes.
Si la página no está marcada all-visible, PostgreSQL consulta el heap para confirmar visibilidad.
Vacuum mantiene esa información; por eso mantenimiento y index-only scans están relacionados.
Heap-Only Tuple updates pueden evitar nuevas entradas en índices cuando:
Cadena conceptual:
índice → tuple original → versión HOT → versión HOT más recienteBeneficios:
Factores que lo dificultan:
ALTER TABLE products SET (fillfactor = 80);Deja espacio libre en páginas para futuras versiones HOT. El trade-off es almacenar menos filas por página y aumentar tamaño inicial.
No reduzcas fillfactor globalmente. Mide tablas con updates frecuentes y columnas indexadas estables.
Cada statement obtiene un snapshot nuevo. Dos SELECT dentro de la misma transacción pueden ver commits diferentes.
La transacción conserva un snapshot estable desde su primera consulta relevante.
Añade detección de dependencias para impedir anomalías no serializables, además del snapshot.
MVCC es la base; el nivel define cómo se obtiene y utiliza el snapshot.
Una transacción larga conserva un snapshot antiguo. Vacuum no puede retirar versiones que todavía podrían ser visibles para ese snapshot.
Incluso una transacción read-only puede causar:
n_dead_tup elevado.idle in transaction es especialmente problemático: no trabaja, pero conserva el snapshot y la conexión.
Replication slots también pueden retener WAL y, según tipo/estado, horizontes de vacuum. Un consumidor lógico detenido puede provocar:
pg_wal creciendo.Monitorea slots, lag y horizontes.
Los transaction IDs tienen espacio finito y se comparan de forma circular. Una tuple extremadamente antigua podría parecer futura si no se congela.
Vacuum freezes tuples antiguas para marcar que su creación es anterior a cualquier transacción relevante.
Autovacuum anti-wraparound es una operación de seguridad. Puede ejecutarse de forma agresiva aunque las configuraciones normales de vacuum sean conservadoras.
Ignorarlo puede llevar a que PostgreSQL bloquee escrituras para proteger integridad.
VACUUM (FREEZE) some_table;No debe ejecutarse rutinariamente sin motivo. Freeze anticipado puede ser útil antes de cargas o plantillas, pero produce trabajo adicional.
Métricas como age(datfrozenxid) ayudan a vigilar riesgo.
MVCC por sí solo no evita duplicados concurrentes.
Patrón inseguro:
T1 SELECT no existe SKU
T2 SELECT no existe SKU
T1 INSERT
T2 INSERTUna unique constraint utiliza índices y locking para serializar el conflicto. MVCC proporciona visibilidad; constraints protegen invariantes.
Una FK puede adquirir row-level locks sobre la fila referenciada para evitar que el parent desaparezca durante la inserción del child.
Aunque un SELECT normal no bloquee, integridad referencial sí necesita coordinación explícita.
Estado inicial:
inventory.quantity = 10T1:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT quantity FROM inventory WHERE id = 1; -- 10T2:
UPDATE inventory SET quantity = 8 WHERE id = 1;
COMMIT;T1 vuelve a leer:
SELECT quantity FROM inventory WHERE id = 1; -- sigue viendo 10Otra transacción nueva ve 8. Existen dos versiones físicas durante un tiempo; cada snapshot elige una.
Se forma una cadena de versiones. HOT puede ayudar; vacuum y pruning limpian cuando es seguro.
Impide limpieza de versiones anteriores aunque no haga writes.
La visibility map no marca suficientes páginas all-visible, quizá por writes recientes o vacuum insuficiente.
Libera lógicamente filas, pero archivo e índices pueden seguir grandes. Vacuum reutiliza espacio; VACUUM FULL reescribe y bloquea fuertemente.
No puede ser HOT respecto a ese índice y crea más write amplification.
Puede tener pocas dead tuples, pero aún necesita vacuum para freeze y visibility map.
Writers, DDL, FKs y explicit locks siguen coordinando.
Normalmente deja espacio interno reutilizable.
Generalmente crea una nueva tuple.
Conserva snapshots y bloquea mantenimiento.
Cada índice amplifica escrituras y reduce oportunidades HOT.
Reescribe la tabla y toma lock fuerte. Primero diagnostica la causa.
Consulta actividad antigua:
SELECT
pid,
usename,
state,
xact_start,
query_start,
backend_xmin,
query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start;Estadísticas de tabla:
SELECT
relname,
n_live_tup,
n_dead_tup,
n_tup_upd,
n_tup_hot_upd,
last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;Estas cifras son estimaciones y deben combinarse con tamaño, workload y logs.
Niveles de aislamiento y anomalías define qué snapshot se utiliza y qué conflictos concurrentes puede tolerar cada transacción.