Puntos Clave de MVCC
El Control de Concurrencia Multiversión (MVCC, por sus siglas en inglés), es el mecanismo que utiliza PostgreSQL para permitir que muchas transacciones lean y escriban los mismos datos al mismo tiempo sin interferir entre sí.
En lugar de bloquear una fila para cada lector, como hacían los diseños de bases de datos más antiguos, PostgreSQL mantiene múltiples versiones de una fila y permite que cada transacción vea la versión que era correcta en el momento de su propia instantánea.
Esta página construye el modelo mental detrás de ese sistema antes de que llegues a las páginas más aplicadas de esta sección sobre niveles de aislamiento, bloqueos a nivel de fila y transacciones largas.
Comprender MVCC cambia la forma en que razonas sobre los errores de concurrencia, ya que la mayoría del comportamiento sorprendente que encontrarás en producción se remonta a lo que la instantánea de una transacción podía y no podía ver.
Resumen
- PostgreSQL nunca sobrescribe una fila en su lugar; escribe una nueva versión y deja la antigua hasta que ya no se necesite.
- Por Qué Importa: Este diseño permite que las lecturas y escrituras procedan concurrentemente sin que los lectores bloqueen a los escritores o los escritores bloqueen a los lectores.
- Conceptos Clave: instantánea (snapshot), versión de tupla (tuple version), xmin/xmax, visibilidad (visibility), tupla muerta (dead tuple), vacuum.
- Cuándo Usar: Este modelo está siempre activo, pero importa más al razonar sobre transacciones de larga duración, hinchazón (bloat) y elecciones de nivel de aislamiento.
- Limitaciones / Compensaciones: Las versiones de fila antiguas se acumulan como hinchazón hasta que
vacuumlas recupera, y una transacción de larga duración puede retrasar esa recuperación indefinidamente. - Temas Relacionados: conceptos básicos de transacciones, niveles de aislamiento, bloqueos a nivel de fila, transacciones largas, anomalías de serialización.
Fundamentos
Cada fila en una tabla de PostgreSQL contiene dos columnas de sistema ocultas, xmin y xmax, que registran qué transacción creó la versión de la fila y qué transacción, si la hubo, la eliminó o reemplazó.
Cuando ejecutas un UPDATE, PostgreSQL no modifica la fila existente en su lugar; inserta una nueva versión de fila con un xmin fresco y marca el xmax de la versión antigua con el ID de la transacción que actualiza.
Un DELETE funciona de la misma manera: la fila no se elimina físicamente de inmediato; solo se marca con un xmax para que otras transacciones sepan que ya no debe considerarse actual.
Esto significa que una sola fila lógica puede tener muchas versiones de tupla físicas apiladas en la tabla a la vez, y el trabajo de PostgreSQL en el momento de la lectura es averiguar qué versión es la correcta para la transacción que la solicita.
Esa decisión se toma utilizando una instantánea (snapshot), un registro de qué transacciones ya se habían confirmado (committed), estaban en curso (in-progress) o aún no habían comenzado en el momento en que se tomó la instantánea.
Una versión de fila es visible para una transacción solo si su transacción creadora se había confirmado antes de que se tomara la instantánea y su transacción eliminadora, si la hubo, no lo había hecho.
Piensa en ello como un documento compartido con historial de versiones completo: cada edición crea una nueva revisión en lugar de borrar la antigua, y a cada lector se le entrega la revisión específica que coincide con el momento en que abrió el documento.
Puedes observar esto directamente mirando las columnas ocultas de una fila.
SELECT xmin, xmax, ctid FROM app.accounts WHERE id = 1;El ctid que se muestra junto a ellas es la ubicación física de la versión de la fila, que cambia cada vez que una actualización crea una nueva versión en una ubicación diferente.
Mecánica e Interacciones
La recompensa práctica de este diseño es la garantía de concurrencia principal de PostgreSQL: los lectores nunca bloquean a los escritores, y los escritores nunca bloquean a los lectores.
Un SELECT de larga duración simplemente sigue utilizando su propia instantánea y nunca necesita esperar a que finalice una UPDATE concurrente, porque esa actualización está creando una nueva versión en lugar de modificar la que el lector ya tiene.
Los escritores aún pueden bloquear a otros escritores, ya que dos transacciones que intentan actualizar la misma fila deben serializarse a través de un bloqueo a nivel de fila, pero ese es un caso mucho más limitado que bloquear también a todos los lectores.
Aquí es donde entran en juego los niveles de aislamiento, y vale la pena ser preciso sobre lo que realmente controlan.
Un nivel de aislamiento no cambia cómo funciona el bloqueo en la capa de almacenamiento; cambia cómo se comporta la instantánea de una transacción y qué anomalías de concurrencia tolera.
Read Committed (Lectura Confirmada), el predeterminado de PostgreSQL, toma una nueva instantánea para cada sentencia dentro de una transacción, por lo que cada sentencia ve los datos confirmados más recientes en el momento en que se ejecuta.
Repeatable Read (Lectura Repetible) toma una única instantánea para toda la transacción, por lo que cada sentencia dentro de ella ve la misma vista coherente, incluso si otras transacciones confirman cambios entre medias.
Serializable (Serializable) va aún más allá, detectando y rechazando el subconjunto de patrones de ejecución concurrentes que no podrían haber ocurrido si las transacciones se hubieran ejecutado una a la vez, incluso cuando la instantánea de Repeatable Read por sí sola no detecta el conflicto.
El costo del enfoque de MVCC de escribir una nueva versión es que las versiones antiguas no desaparecen por sí solas.
Una tupla muerta, una versión de fila que ninguna transacción puede ver ya, permanece en la tabla ocupando espacio hasta que vacuum la recupera para su reutilización.
SELECT n_dead_tup FROM pg_stat_user_tables WHERE relname = 'accounts';Autovacuum ejecuta esta limpieza automáticamente en segundo plano, pero una transacción que permanece abierta durante mucho tiempo puede impedir que vacuum recupere versiones que están técnicamente muertas, porque la instantánea de esa transacción antigua aún podría necesitarlas.
Consideraciones Avanzadas y Aplicaciones
El modo de fallo de MVCC más consecuente en producción es la sesión idle-in-transaction (inactiva en transacción), una conexión que abrió una transacción, tomó una instantánea y luego permaneció inactiva sin confirmar ni revertir.
Esa instantánea abierta retiene el horizonte de limpieza de autovacuum para todas las tablas de la base de datos, no solo para las que la transacción tocó, lo que significa que las tuplas muertas se acumulan en todas partes hasta que esa única conexión finalmente se cierra.
idle_in_transaction_session_timeout existe específicamente para limitar este riesgo cerrando forzosamente las conexiones que se exceden en su tiempo de permanencia.
La hinchazón (bloat), el espacio acumulado consumido por tuplas muertas que vacuum aún no ha recuperado, tiene consecuencias reales de rendimiento más allá del uso del disco, ya que los escaneos y las búsquedas de índices tienen que saltarse versiones muertas para encontrar las activas.
MVCC también interactúa directamente con el esquema y las decisiones de bloqueo cubiertas en otra parte de esta documentación: un patrón de eliminación suave (soft delete) que utiliza una columna deleted_at produce su propia forma de hinchazón a menos que se combine con archivado periódico, y un SELECT ... FOR UPDATE toma explícitamente un bloqueo a nivel de fila además de la mecánica de instantáneas de MVCC para prevenir una clase específica de carrera de escritura-escritura que una instantánea por sí sola no puede resolver.
Elegir un nivel de aislamiento es, en última instancia, una compensación entre la tolerancia a anomalías y la complejidad de la aplicación, y diferentes niveles son genuinamente más adecuados para diferentes cargas de trabajo.
| Nivel de Aislamiento | Fortaleza | Debilidad | Mejor Ajuste |
|---|---|---|---|
| Read Committed | Bajo sobrecosto, frescura intuitiva por sentencia, predeterminado de PostgreSQL | Vulnerable a lecturas no repetibles dentro de una transacción | La mayoría de las transacciones OLTP cotidianas con lógica corta y simple |
| Repeatable Read | Vista consistente en toda la transacción, inmune a lecturas no repetibles | Puede encontrar fallos de serialización en conflictos de escritura-escritura que deben reintentarse | Informes o lógica multisentencia que debe ver una instantánea coherente |
| Serializable | Garantía más fuerte, se comporta como si las transacciones se ejecutaran una a la vez | Mayor probabilidad de conflictos que requieren reintento, cierto costo de rendimiento | Lógica financiera o crítica de invariantes donde cualquier anomalía es inaceptable |
Las consultas analíticas de larga duración y las escrituras OLTP que compiten por las mismas tablas son una tensión común que MVCC ayuda a aliviar pero no elimina por completo, ya que un informe largo bajo Repeatable Read todavía mantiene su instantánea abierta durante toda la duración del informe, con la misma consecuencia de bloqueo de vacuum que una sesión idle-in-transaction.
Monitorear pg_stat_activity para transacciones en estado active o idle in transaction con un xact_start antiguo es el hábito operativo estándar para detectar esta clase de problema antes de que se convierta en un incidente de hinchazón.
Conceptos Erróneos Comunes
- "Un UPDATE modifica la fila en su lugar." PostgreSQL escribe una nueva versión de tupla y marca la antigua como muerta; nunca reescribe bytes dentro de una versión de fila existente.
- "Un SELECT puede bloquear un UPDATE concurrente, o viceversa." MVCC está diseñado específicamente para que los lectores y escritores nunca se bloqueen mutuamente; solo los escritores que compiten por la misma fila se bloquean entre sí.
- "Los niveles de aislamiento más altos implican más bloqueos." El nivel de aislamiento cambia el comportamiento de la instantánea y la detección de anomalías; no cambia fundamentalmente cómo se adquieren los bloqueos a nivel de fila.
- "Las filas eliminadas desaparecen inmediatamente." Un DELETE solo marca una versión de fila como no actual; el espacio físico no se recupera hasta que se ejecuta
vacuumy ninguna transacción todavía lo necesita. - "Vacuum es una limpieza opcional que puedo posponer indefinidamente." Posponer
vacuumpermite que las tuplas muertas se acumulen en hinchazón que degrada el rendimiento de escaneo e índices, y en casos extremos, arriesga el desbordamiento del ID de transacción (transaction ID wraparound).
Preguntas Frecuentes
¿Qué significa realmente MVCC y qué problema resuelve?
El Control de Concurrencia Multiversión (MVCC) permite a PostgreSQL dar a cada transacción una vista coherente de los datos sin hacer que los lectores y escritores esperen unos a otros, manteniendo múltiples versiones de cada fila en lugar de una copia compartida y bloqueable.
¿Qué son xmin y xmax?
xmines el ID de la transacción que creó una versión de fila dada.xmaxes el ID de la transacción que la eliminó o reemplazó, si la hubo.- PostgreSQL utiliza ambos, comparados contra la instantánea de una transacción, para decidir si una versión es visible.
¿Por qué un SELECT de larga duración no bloquea a otras transacciones para actualizar las mismas filas?
El SELECT está leyendo desde su propia instantánea de versiones ya confirmadas, y un UPDATE crea una nueva versión en lugar de alterar la que el SELECT ya tiene, por lo que los dos nunca compiten por los mismos datos físicos.
¿Qué es una instantánea en términos de MVCC?
Una instantánea es el registro privado de una transacción de qué otras transacciones contaron como confirmadas en el momento en que comenzó a leer, y es lo que determina qué versiones de fila tiene permitido ver esa transacción.
¿El nivel de aislamiento cambia cómo funcionan los bloqueos?
No, el nivel de aislamiento cambia lo que ve la instantánea de una transacción y qué patrones de ejecución concurrentes se rechazan como anomalías, mientras que el comportamiento de bloqueo a nivel de fila para escritores en conflicto permanece en gran medida igual en todos los niveles.
¿Por qué mi tabla sigue creciendo aunque estoy eliminando filas antiguas?
- Las filas eliminadas o reemplazadas por actualizaciones se convierten en tuplas muertas, no en espacio liberado, en el momento de la escritura.
- Autovacuum recupera ese espacio en segundo plano, pero necesita ejecutarse y completarse para que ocurra.
- Una transacción de larga duración en otra parte de la base de datos puede retrasar esa recuperación incluso en tablas que no toca.
¿Qué es peligroso en una sesión idle-in-transaction?
La instantánea de una transacción abierta retiene el punto hasta el cual autovacuum puede recuperar de forma segura las tuplas muertas en toda la base de datos, por lo que una transacción abierta olvidada puede causar hinchazón en todas partes, no solo en las tablas que consultó.
¿Cuál es la diferencia entre Read Committed y Repeatable Read en la práctica?
Read Committed toma una nueva instantánea para cada sentencia, por lo que dos SELECT en la misma transacción pueden ver datos diferentes si algo se confirma entre medias, mientras que Repeatable Read toma una instantánea para toda la transacción, por lo que cada sentencia ve la misma vista coherente.
¿Cuándo debería usar Serializable en lugar de Repeatable Read?
Serializable vale la pena el sobrecosto de reintento adicional cuando la lógica impone un invariante que una instantánea por sí sola no puede proteger, como asegurar que una suma de filas nunca exceda un límite bajo escrituras concurrentes.
¿Puede una versión de fila ser visible para una transacción pero no para otra en el mismo instante?
Sí, ese es el propósito de MVCC: la instantánea de cada transacción determina independientemente la visibilidad, por lo que dos transacciones ejecutándose al mismo tiempo pueden ver legítimamente versiones diferentes de la misma fila lógica.
¿Cómo compruebo si la hinchazón de MVCC se está convirtiendo en un problema?
pg_stat_user_tables expone n_dead_tup por tabla, y un número que aumenta constantemente en relación con las filas vivas, combinado con que autovacuum no sigue el ritmo, es la señal temprana estándar que vale la pena investigar.
¿Se aplica MVCC de la misma manera en una réplica de lectura?
Una réplica aplica el mismo modelo de versión y visibilidad al registro de escritura anticipada (WAL) que transmite desde el primario, pero una consulta de larga duración en una réplica puede entrar en conflicto con los cambios entrantes reproducidos de maneras que se manejan de forma diferente a los conflictos de MVCC en el lado del primario.
Relacionados
- Conceptos Básicos de Transacciones - BEGIN, COMMIT, ROLLBACK y límites de transacción
- Visibilidad MVCC - ejemplos prácticos leyendo xmin, xmax y estadísticas de tuplas muertas
- Niveles de Aislamiento - Read Committed, Repeatable Read y Serializable en profundidad
- Bloqueos a Nivel de Fila - bloqueo explícito sobre el modelo de instantáneas de MVCC
- Transacciones Largas - el riesgo operativo de mantener una instantánea abierta demasiado tiempo
- Anomalías de Serialización - los errores de concurrencia que los niveles de aislamiento están diseñados para prevenir
Versiones de Pila: Esta página fue escrita para PostgreSQL 18.4 (versión principal estable 18, línea de mantenimiento 17 también soportada).