En 09-01 viste qué hace una transacción. Ahora toca qué garantiza exactamente, y la respuesta cabe en cuatro letras: ACID — atomicidad, consistencia, aislamiento y durabilidad. No son una etiqueta de marketing: son cuatro promesas concretas, cada una con un mecanismo detrás, cada una con una forma conocida de romperse y cada una con un límite que conviene saber dónde está.

Y hay una recompensa al final de la lección. La cuarta letra nos llevará al WAL, el registro que hace que tus datos sobrevivan a un corte de corriente y que cierra la promesa que 05-04 dejó abierta sobre recuperar un borrado. Y la tercera nos llevará por fin a MVCC, el modelo con el que PostgreSQL consigue que una sesión lea sin bloquear a nadie mientras otra escribe la misma fila — el mecanismo que explica, exactamente, las filas muertas, el bloat y la necesidad de VACUUM que 08-05 dejó pendiente.

Contenido

  1. Las cuatro garantías de un vistazo
  2. Atomicidad: todo o nada
  3. Consistencia: de un estado válido a otro estado válido
  4. Isolation (aislamiento): como si fueran secuenciales
  5. Durabilidad: el WAL, fsync y synchronous_commit
  6. MVCC: aislamiento sin bloquear las lecturas
  7. De MVCC al bloat: por qué existe VACUUM
  8. Cómo implementa cada motor el aislamiento
  9. Lo que ACID no te garantiza
  10. Errores Comunes y Consejos
  11. Ejercicios
  12. Conclusión

  1. Las cuatro garantías de un vistazo

Letra Promete La rompe Mecanismo en PostgreSQL
Atomicidad La transacción se aplica entera o nada Un fallo a mitad sin marcha atrás Estado de la transacción en pg_xact + versiones de fila (MVCC)
Consistencia La base pasa de un estado válido a otro válido Una regla que nadie declaró ni comprobó CHECK, NOT NULL, UNIQUE, FOREIGN KEY… y tu código
Isolation Las transacciones concurrentes no se pisan Anomalías de concurrencia (09-04) MVCC + instantáneas + bloqueos
Durabilidad Lo confirmado sobrevive a un corte de corriente Un COMMIT que solo llegó a la memoria WAL escrito y sincronizado antes del COMMIT

Una forma útil de recordarlas: A y D protegen frente a los fallos (errores, caídas, cortes de luz); C e I protegen frente a los demás (tus propias reglas y las otras sesiones).

  1. Atomicidad: todo o nada

Es la que ya has visto: los cuatro pasos de confirmar un pedido son uno solo. Pero la pregunta interesante es cómo lo hace el motor, porque la respuesta de PostgreSQL sorprende a quien viene de otros sistemas.

En un motor con registro de deshacer (undo log) —InnoDB, Oracle— el UPDATE sobrescribe la fila en su sitio y guarda aparte una copia del valor anterior. El ROLLBACK consiste en releer ese registro y restaurar físicamente los valores viejos: cuanto más grande la transacción, más caro deshacerla.

PostgreSQL no hace eso. Un UPDATE no sobrescribe nada: escribe una versión nueva de la fila y deja la vieja donde estaba, marcada con el identificador de la transacción que la sustituyó. Deshacer es entonces trivial:

En PostgreSQL, un ROLLBACK no restaura nada: se limita a anotar en pg_xact que esa transacción abortó. A partir de ese instante, todas las versiones de fila que ella escribió pasan a ser invisibles para todo el mundo, y las viejas vuelven a ser las buenas.

Las consecuencias prácticas de esta decisión de diseño son grandes y las notarás en el trabajo:

PostgreSQL (MVCC puro) InnoDB / Oracle (undo log)
Coste del COMMIT Proporcional a lo escrito Muy barato
Coste del ROLLBACK Prácticamente cero, sea cual sea el tamaño Proporcional al tamaño de la transacción
Deshacer un DELETE de 10 millones de filas Instantáneo Puede tardar más que el propio DELETE
Precio a pagar Las versiones viejas se acumulan: hace falta VACUUM El undo crece y hay que purgarlo

Por eso el BEGIN ... ROLLBACK que 08-05 recomendaba para analizar un DELETE con EXPLAIN ANALYZE es tan barato en PostgreSQL: deshacer no cuesta nada. Y por eso, a cambio, PostgreSQL necesita un VACUUM que los otros no necesitan — apartado 7.

  1. Consistencia: de un estado válido a otro estado válido

La C es la letra peor explicada de las cuatro, porque suena a "los datos son correctos" y no es eso. Lo que promete es más modesto y más útil:

Si la base cumplía todas las reglas declaradas antes de la transacción, las seguirá cumpliendo después. La transacción puede violarlas durante su ejecución; lo que no puede es dejarlas violadas al confirmar.

Y ahí está la letra pequeña: "reglas declaradas". La base de datos garantiza exactamente lo que le has dicho, ni una regla más. Todo lo de 05-01 cuenta:

Regla declarada Qué impide en TiendaVerde
CHECK (stock >= 0) Un stock negativo
CHECK (puntuacion BETWEEN 1 AND 5) Una reseña de 7 estrellas
CHECK (estado IN ('pendiente','pagado',…)) Un pedido en estado devuelto
FOREIGN KEY (cliente_id) REFERENCES clientes(id) Un pedido de un cliente inexistente
UNIQUE (email) Dos clientes con el mismo correo
NOT NULL Un pedido sin fecha

Y esto no lo impide nada del esquema: que un cliente reseñe un producto que nunca compró; que un pedido entregado no tenga ninguna línea; que la suma de las líneas no cuadre con el importe cobrado; que se venda un producto activo = FALSE. Son reglas de negocio no declaradas, y la base de datos no sabe que existen.

El caso del stock: dos formas de garantizar el invariante

"El stock nunca puede quedar negativo" se puede hacer cumplir de dos maneras, y conviene entender que no son equivalentes.

Forma 1 — declarativa. Es la que TiendaVerde ya tiene:

-- Ya está en el esquema (05-01)
stock INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0)
UPDATE productos SET stock = stock - 1 WHERE id = 13;   -- el 13 está a 0
ERROR:  new row for relation "productos" violates check constraint "productos_stock_check"

Ventaja: es inviolable. Da igual quién escriba, desde qué aplicación, con qué lenguaje o a las tres de la mañana. Inconveniente: la única respuesta posible es un error, que además aborta la transacción entera (09-01) y obliga a tu código a interpretar un mensaje.

Forma 2 — en la sentencia. Un UPDATE condicional que no compra si no hay existencias:

UPDATE productos
SET    stock = stock - 1
WHERE  id = 13
  AND  stock >= 1;
UPDATE 0

Cero filas, sin error y sin abortar nada. Tu aplicación lee el contador de filas afectadas —el UPDATE N de 05-03, el :ROW_COUNT de 09-01— y si es 0 responde "producto agotado" al cliente. Y hay una propiedad enorme escondida ahí: ese UPDATE también es correcto frente a la concurrencia, porque la comprobación (stock >= 1) y la escritura ocurren en la misma sentencia atómica. Es la misma lección de 05-05 con el upsert: comprobar y luego actuar en dos sentencias nunca es seguro.

La regla de oro de la C: usa las dos. El CHECK es la red que garantiza el invariante pase lo que pase; el WHERE condicional es el que convierte "excepción" en "flujo normal de negocio". La primera protege los datos, la segunda protege la experiencia del usuario.

Lo que la base no puede hacer por ti es la parte que no le has contado. Reglas como "un pedido pagado debe tener al menos una línea" o "no se puede reseñar lo que no se ha comprado" necesitan una restricción declarada que hoy no existe, un trigger (10-05) o código de aplicación dentro de la misma transacción. La consistencia es una responsabilidad compartida, y la base de datos solo firma su mitad.

  1. Isolation (aislamiento): como si fueran secuenciales

La promesa, en una frase:

Varias transacciones ejecutándose a la vez deben producir el mismo resultado que si se hubieran ejecutado una detrás de otra, en algún orden.

Ya la viste funcionando en 09-01: mientras A tenía el stock del matcha en 39 sin confirmar, B seguía leyendo 40. Para B, la transacción de A aún no había empezado; después del COMMIT, había ocurrido entera. Nunca a medias.

Y aquí está el matiz que hace de esta la letra más interesante de las cuatro: el aislamiento es negociable. Las otras tres son todo o nada, pero de esta puedes pedir más o menos:

Quieres… Pagas…
Aislamiento perfecto (SERIALIZABLE) Menos concurrencia, y transacciones que abortan y hay que reintentar
Aislamiento relajado (READ COMMITTED) Más rendimiento, y ciertas anomalías que tu código debe tener en cuenta

Esa negociación son los niveles de aislamiento, con sus cuatro escalones y sus cuatro anomalías, y es el contenido íntegro de 09-04. Aquí basta con saber que existe la palanca y que PostgreSQL la trae puesta por omisión en READ COMMITTED.

  1. Durabilidad: el WAL, fsync y synchronous_commit

Promesa: si el servidor te dijo COMMIT, los datos están, aunque le arranques el cable un microsegundo después.

El problema es que escribir en disco es lento y las páginas de datos están dispersas por el fichero. Si cada COMMIT tuviera que escribir en disco todas las páginas modificadas, en sus posiciones aleatorias, y esperar a que llegaran, el rendimiento sería inaceptable. La solución, universal en todos los motores serios, es el registro de escritura anticipada: WAL (Write-Ahead Log).

flowchart TD
    A["UPDATE productos<br/>SET stock = 39 WHERE id = 15"] --> B["1 · Modificar la página<br/>en <b>shared_buffers</b> (RAM)"]
    A --> C["2 · Escribir el cambio en el<br/><b>búfer del WAL</b> (RAM)"]
    C --> D["3 · <b>COMMIT</b>: volcar el WAL a disco<br/>y <b>fsync</b> — se espera aquí"]
    D --> E["✅ El servidor responde COMMIT"]
    B -.->|"más tarde, en un<br/><b>checkpoint</b>"| F["4 · Las páginas sucias<br/>bajan a los ficheros de datos"]
    G["💥 corte de corriente"] -.-> H["Al arrancar: <b>recuperación</b><br/>se relee el WAL y se reaplica<br/>lo confirmado que no llegó al paso 4"]

La idea clave es el orden: primero el registro, después los datos (de ahí "escritura anticipada"). El WAL es un fichero secuencial, y escribir secuencialmente es órdenes de magnitud más rápido que escribir en posiciones aleatorias. En el instante del COMMIT los ficheros de datos pueden estar completamente desactualizados; da igual, porque el WAL ya contiene la receta para reconstruirlos.

Y fsync es la palabra crítica: escribir no basta, porque el sistema operativo y el propio disco tienen sus cachés. fsync es la llamada que obliga a que el dato esté físicamente en el medio persistente antes de continuar.

El compromiso: synchronous_commit

Ese fsync es lo único que hay entre tu COMMIT y una respuesta instantánea. PostgreSQL te deja negociarlo:

Valor Qué espera antes de responder COMMIT Qué se pierde si cae la máquina
on (por omisión) Que el WAL esté sincronizado en disco Nada
local Igual, pero sin esperar a las réplicas Nada en local; posible desfase de la réplica
off No espera: responde y sincroniza después Las últimas transacciones confirmadas (por omisión, hasta 3× wal_writer_delay, unos 600 ms)
remote_apply Que una réplica lo haya aplicado y sea visible allí Nada, a costa de bastante latencia
-- Solo para esta transacción: una carga masiva de datos que se puede repetir
SET LOCAL synchronous_commit = off;

Es un ajuste legítimo —y muy rentable— en una carga de datos reejecutable o en una tabla de métricas. Es inaceptable en un pedido o un cobro.

⚠️ Lo que sí es una barbaridad: fsync = off. No confundas los dos parámetros. Con synchronous_commit = off puedes perder las últimas transacciones, pero la base queda íntegra. Con fsync = off puedes perder la base entera: las escrituras llegan a disco en cualquier orden y la recuperación deja un fichero de datos corrupto e irreparable. Está pensado para bancos de pruebas desechables y para nada más.

Replicación y PITR, brevemente

El WAL no sirve solo para recuperarse de una caída. Como es un registro completo y ordenado de todo lo que ha cambiado, sirve para otras dos cosas de las que aquí solo diremos el nombre, porque son materia de administración:

  • Replicación. Si envías el WAL a un segundo servidor y este lo va aplicando, tienes una copia viva de la base. Es el fundamento de la alta disponibilidad y de las réplicas de solo lectura para informes.
  • PITR (Point-In-Time Recovery). Con una copia base más todo el WAL posterior archivado, puedes restaurar la base en cualquier instante concreto, por ejemplo 2026-03-05 11:59:58, dos segundos antes de aquel DELETE FROM pedidos; sin WHERE. Esta es la promesa que 05-04 dejó abierta: no hay "deshacer" para algo ya confirmado, pero sí hay una máquina del tiempo, siempre que alguien hubiera configurado el archivado antes del accidente.

  1. MVCC: aislamiento sin bloquear las lecturas

Ya lo hemos mencionado tres veces; toca abrirlo. MVCC significa Multi-Version Concurrency Control: control de concurrencia por multiversión.

La idea, en una frase: cada fila puede existir en varias versiones a la vez, y cada transacción ve la versión que le corresponde según cuándo empezó.

De ahí sale la propiedad más valiosa de PostgreSQL en concurrencia:

Los lectores nunca bloquean a los escritores, y los escritores nunca bloquean a los lectores.

Por eso en 09-01 la sesión B pudo leer el stock del matcha sin esperar ni un milisegundo mientras A lo estaba modificando: A escribió una versión nueva, y B siguió leyendo la vieja, que para ella era la buena.

Las columnas ocultas xmin y xmax

Cada versión de fila lleva dos columnas de sistema que puedes consultar aunque no aparezcan en SELECT *:

Columna Significado
xmin Identificador de la transacción que creó esta versión
xmax Identificador de la transacción que la borró o sustituyó. Vale 0 si la versión sigue vigente
ctid Posición física de la versión: (página, índice dentro de la página)

Míralo en vivo. Recarga la base y ejecuta:

SELECT xmin, xmax, ctid, id, nombre, stock FROM productos WHERE id = 15;
xmin xmax ctid id nombre stock
748 0 (0,15) 15 Té verde matcha ceremonial 30 g 40

(Los números de transacción dependen de tu instalación; lo que importa son las relaciones entre ellos.) La fila fue creada por la transacción 748 —el INSERT de carga— y nadie la ha tocado desde entonces (xmax = 0). Ahora modifícala:

UPDATE productos SET stock = stock - 1 WHERE id = 15;
SELECT xmin, xmax, ctid, id, stock FROM productos WHERE id = 15;
xmin xmax ctid id stock
812 0 (0,21) 15 39

Tres cosas han cambiado a la vez, y son toda la explicación de MVCC:

  1. El xmin es otro: esta es una fila nueva, creada por la transacción 812.
  2. El ctid es otro: está en otro sitio físico de la página. La fila vieja sigue ahí, en (0,15), con su xmax ahora puesto a 812.
  3. La tabla ocupa una fila más en disco, aunque SELECT COUNT(*) siga devolviendo 20.

Y eso es literalmente lo que significaba aquella frase de 08-05: "un UPDATE no modifica la fila: escribe una versión nueva y marca la vieja como muerta". Ahora ya sabes con qué la marca: con su xmax.

La instantánea

Cuando una transacción necesita decidir qué versiones ve, toma una instantánea (snapshot): la lista de qué transacciones estaban confirmadas en ese momento. Con ella, la regla de visibilidad de cada versión es sencilla:

Una versión es visible si su xmin está confirmado y es anterior a mi instantánea, y su xmax está vacío, abortado, o es posterior a mi instantánea.

Ahí está todo lo que viste en 09-01 sin explicación:

  • A ve su propio cambio porque el xmin de la versión nueva es su propia transacción.
  • B no lo ve porque ese xmin corresponde a una transacción aún no confirmada, y para B eso equivale a inexistente.
  • Tras el COMMIT, la instantánea siguiente de B ya incluye a A, y la versión nueva pasa a ser visible.
  • Y un ROLLBACK no necesita borrar nada: basta con que la transacción quede marcada como abortada en pg_xact para que su xmin no valide ninguna de sus versiones.

Cuándo se toma la instantánea es exactamente la diferencia entre los niveles de aislamiento: en READ COMMITTED se toma una nueva en cada sentencia; en REPEATABLE READ y SERIALIZABLE, una sola para toda la transacción. Esa única frase explica el 90 % de 09-04.

  1. De MVCC al bloat: por qué existe VACUUM

Y ahora la factura. Si cada UPDATE deja una versión muerta y cada DELETE se limita a poner un xmax, la tabla solo crece. Las versiones que ya no son visibles para ninguna transacción son las filas muertas (dead tuples), y el espacio que ocupan es el bloat de 08-05.

UPDATE productos SET stock = stock + 1 WHERE id = 15;   -- repetido 5 veces
SELECT relname, n_live_tup, n_dead_tup
FROM   pg_stat_user_tables WHERE relname = 'productos';
relname n_live_tup n_dead_tup
productos 20 5

Veinte filas vivas y cinco cadáveres, tras cinco actualizaciones de una sola fila. Ahí está el círculo completo, y esta es la cadena entera que 08-05 dejó a medias:

flowchart LR
    A["Necesito <b>aislamiento</b><br/>sin bloquear lecturas"] --> B["<b>MVCC</b>: varias versiones<br/>de cada fila"]
    B --> C["Cada UPDATE/DELETE deja<br/><b>versiones muertas</b>"]
    C --> D["Tablas e índices <b>engordan</b>:<br/>el <i>bloat</i>"]
    D --> E["<b>VACUUM</b> marca ese espacio<br/>como reutilizable"]
    E -.->|"no puede limpiar lo que<br/>una transacción vieja<br/>aún podría necesitar"| F["Transacción abierta<br/>= bloat que no se limpia"]

Y así quedan explicadas las tres cosas que 08-05 anunció y no pudo justificar:

  1. Por qué hay filas muertas. Porque son las versiones antiguas que MVCC necesitó para no bloquear a nadie.
  2. Por qué una transacción abierta impide limpiar. Porque VACUUM solo puede eliminar una versión si ninguna transacción viva puede necesitarla. Una sesión idle in transaction desde hace tres horas sostiene una instantánea de hace tres horas, y con ella todas las versiones muertas de toda la base. Es el daño del apartado 11 de 09-01, ahora con su mecanismo.
  3. Por qué existe CREATE INDEX CONCURRENTLY. Porque construir un índice normal necesita bloquear las escrituras de la tabla, y MVCC permite hacerlo sin ese bloqueo a cambio de recorrer la tabla dos veces. El detalle es de 09-05.

  1. Cómo implementa cada motor el aislamiento

Motor Modelo Detalle
PostgreSQL MVCC puro, versiones en la propia tabla Lectores y escritores no se bloquean. Precio: VACUUM y el bloat
Oracle MVCC con undo segments La versión vieja se reconstruye desde el undo. Sin bloat, pero con el error clásico ORA-01555: snapshot too old si el undo se recicla antes de que acabe una consulta larga
MySQL / InnoDB MVCC + bloqueos Undo log para las lecturas consistentes, más bloqueos de intervalo (gap locks) que bloquean rangos de claves y evitan fantasmas en REPEATABLE READ
SQL Server Bloqueos por omisión, MVCC opcional Por omisión, un lector bloquea a un escritor y viceversa. Con READ_COMMITTED_SNAPSHOT ON pasa a un modelo tipo MVCC usando tempdb como almacén de versiones
SQLite Bloqueo de fichero Un escritor a la vez en toda la base. Con WAL activado, los lectores no se bloquean con el escritor, pero sigue habiendo un único escritor

Nota de dialecto: el vocabulario engaña. "REPEATABLE READ" significa cosas distintas en PostgreSQL y en InnoDB, y "READ COMMITTED" no se comporta igual en PostgreSQL que en SQL Server sin snapshot. El nivel de aislamiento no es portable: es lo primero que hay que verificar al migrar una aplicación entre motores, y el detalle está en 09-04.

  1. Lo que ACID no te garantiza

Cuatro límites honestos, porque ACID se cita mucho más de lo que se entiende:

  1. No garantiza que tu lógica sea correcta. Una transacción perfectamente atómica, consistente, aislada y duradera puede descontar el stock del producto equivocado. ACID garantiza que se hará entera y para siempre; que sea lo que había que hacer es cosa tuya.
  2. No garantiza las reglas que no declaraste. La C solo firma lo que hay en el esquema (apartado 3).
  3. No se extiende más allá de la base de datos. Este es el importante en el mundo real: si tu transacción descuenta el stock y además llama a la pasarela de pago, la pasarela no participa en tu ROLLBACK. Puedes deshacer el pedido; no puedes deshacer el cargo. Por eso 09-03 insistirá en que ninguna llamada externa debe vivir dentro de una transacción abierta, y por eso existen patrones como la outbox o las sagas.
  4. No sobrevive intacto al reparto entre varias bases de datos. Coordinar un COMMIT entre dos servidores exige un protocolo de dos fases, que es lento y frágil.

Ese último punto es la razón de que en sistemas distribuidos aparezca el acrónimo BASE (Basically Available, Soft state, Eventually consistent): en lugar de garantizar coherencia en todo momento, se garantiza que el sistema converge a un estado coherente. Es un compromiso deliberado, no una versión defectuosa de ACID — pero es un compromiso que solo tiene sentido cuando el reparto es inevitable. En una base de datos relacional de un solo servidor, ACID es completo, gratis y no hay ninguna razón para renunciar a él.

Errores Comunes y Consejos

  • Creer que la C significa "los datos son correctos". Significa "se respetan las reglas declaradas". Lo que no está en el esquema, no está garantizado.
  • Confiar la validación solo a la aplicación. Mañana habrá un script de migración, un becario con psql o una segunda aplicación. El CHECK está siempre; tu if solo está en tu código.
  • Confiar solo en el CHECK. Aborta la transacción entera y convierte un caso de negocio normal —"agotado"— en una excepción. Añade el WHERE stock >= 1 y lee las filas afectadas.
  • Confundir synchronous_commit = off con fsync = off. El primero arriesga las últimas transacciones; el segundo arriesga la base entera.
  • Suponer que un ROLLBACK grande es caro. En PostgreSQL es prácticamente gratis; en InnoDB y Oracle puede tardar más que la propia operación. Es al revés de lo que casi todo el mundo espera.
  • Creer que DELETE libera espacio, o que UPDATE reescribe la fila. Ninguna de las dos: dejan versiones muertas, y el espacio solo se reutiliza tras VACUUM.
  • Dejar una transacción abierta y luego quejarse del bloat. Son la misma cosa: la instantánea vieja impide limpiar.
  • Meter una llamada HTTP dentro de una transacción. ACID acaba en el borde de la base de datos; el cargo a la tarjeta no se deshace con un ROLLBACK.
  • Consejo: declara las reglas dos veces, en el esquema y en la sentencia. La primera protege los datos; la segunda, la experiencia del usuario.
  • Consejo: mira xmin, xmax y ctid una vez en tu vida. Ver físicamente cómo un UPDATE crea una fila nueva vale por diez explicaciones de MVCC.
  • Consejo: si te importan tus datos, comprueba hoy que tienes copias y archivado de WAL. El PITR no se improvisa después del accidente.

Ejercicios

Ejercicio 1

Sobre la base recién recargada, demuestra experimentalmente el modelo MVCC.

  1. Consulta xmin, xmax, ctid y stock del producto 15 y anótalos.
  2. Ejecuta UPDATE productos SET stock = stock - 1 WHERE id = 15; tres veces seguidas (en autocommit), consultando las mismas columnas después de cada una.
  3. Consulta n_live_tup y n_dead_tup en pg_stat_user_tables para productos. Explica los dos números.
  4. Ejecuta VACUUM productos; y vuelve a mirarlos. ¿Ha bajado el tamaño del fichero de la tabla (pg_relation_size('productos'))? ¿Por qué?
  5. Ahora repite el paso 2 dentro de BEGINROLLBACK. ¿Cuántas filas muertas quedan tras el ROLLBACK? ¿Y por qué el ROLLBACK fue instantáneo?

Ejercicio 2

TiendaVerde quiere una regla nueva: el importe de una devolución no puede superar el total facturado de su pedido. Hoy nada lo impide, y de hecho puedes comprobarlo:

INSERT INTO devoluciones (pedido_id, motivo, fecha, importe)
VALUES (1, 'Prueba de coherencia', DATE '2026-03-05', 9999.00);
  1. ¿Se inserta? ¿Qué letra de ACID está en juego y por qué el motor no protesta?
  2. ¿Se puede expresar esa regla con un CHECK? Razónalo mirando qué información necesita la comprobación.
  3. Enumera tres formas de hacerla cumplir, indicando en cada una quién la garantiza y qué agujero deja.
  4. Escribe la consulta que detecta hoy si alguna devolución de TiendaVerde viola la regla.

Ejercicio 3

Un compañero propone esta configuración para el servidor de producción de TiendaVerde, "porque va más rápido":

fsync = off
synchronous_commit = off
full_page_writes = off

Y añade: "total, tenemos una réplica y hacemos copia de seguridad cada noche".

  1. Explica, parámetro a parámetro, qué se pierde exactamente en un corte de corriente.
  2. ¿Por qué la réplica no resuelve el problema de fsync = off?
  3. ¿En qué escenario concreto sería razonable poner synchronous_commit = off, y en cuál sería inaceptable? Pon un ejemplo de cada uno con tablas de TiendaVerde.
  4. ¿Qué mecanismo permitiría recuperar la base al instante anterior al fallo, y qué hay que haber configurado antes para poder usarlo?

Soluciones

Solución 1

1 y 2. El xmin cambia en cada UPDATE (cada uno es una transacción distinta que crea una versión nueva) y el ctid también, porque cada versión ocupa una posición física nueva. El xmax de la versión visible es siempre 0; el que se pone al valor de la transacción nueva es el de la versión anterior, que ya no ves.

3. n_live_tup = 20 y n_dead_tup = 3: veinte filas vivas y tres versiones muertas, una por UPDATE. La tabla tiene ahora 23 versiones almacenadas para 20 filas lógicas.

4. Tras VACUUM productos;, n_dead_tup baja a 0, pero pg_relation_size no cambia. Y es la parte importante del ejercicio: VACUUM marca el espacio como reutilizable por la propia tabla, no lo devuelve al sistema operativo. Para eso hace falta VACUUM FULL, que reescribe la tabla entera con un bloqueo exclusivo (08-05). Por eso el bloat se previene y no se cura.

5. Quedan igualmente tres filas muertas: las versiones que la transacción escribió antes de deshacerse siguen físicamente ahí, solo que ahora son invisibles porque su xmin corresponde a una transacción abortada. Y el ROLLBACK fue instantáneo precisamente por eso: no restauró nada, solo anotó "abortada" en pg_xact (apartado 2). Deshacer no borra el trabajo hecho: lo deja invisible y se lo encarga al VACUUM.

Solución 2

1. Sí se inserta. INSERT 0 1, sin ninguna queja. La letra en juego es la C: la base garantiza las reglas declaradas, y esa regla no está declarada en ninguna parte. Las restricciones de devoluciones solo exigen que el pedido_id exista y que el importe sea >= 0. Nueve mil euros cumplen ambas.

2. No, no se puede con un CHECK. Un CHECK solo puede mirar las columnas de la propia fila que se está insertando. Para saber si 9999,00 € es demasiado hay que sumar las líneas del pedido 1, es decir, consultar otra tabla, y eso un CHECK no lo permite (PostgreSQL rechaza subconsultas en un CHECK, precisamente porque el resultado podría cambiar después y la restricción dejaría de cumplirse sin que nadie tocara la fila).

3. Tres formas:

Forma Quién la garantiza Agujero que deja
Comprobación en la aplicación antes del INSERT, dentro de la transacción Tu código Cualquier otra vía de escritura (script, psql, segunda aplicación) la salta. Y entre la comprobación y el INSERT hay una ventana de carrera si las líneas del pedido pueden cambiar
INSERT ... SELECT condicional, que solo inserta si la suma lo permite La propia sentencia, atómicamente Sigue sin proteger contra otras vías de escritura, pero elimina la condición de carrera: comprobación y escritura son una sola sentencia
Trigger BEFORE INSERT OR UPDATE sobre devoluciones que consulta lineas_pedido y lanza un error La base de datos, para todo el mundo Es la más robusta; su coste es que la lógica de negocio pasa a vivir en la base y hay que mantenerla ahí (10-05)

4. La consulta que audita el estado actual:

SELECT d.id, d.pedido_id, d.importe,
       ROUND(SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento)), 2)
         + MAX(pe.gastos_envio)                                             AS total_pedido
FROM   devoluciones  AS d
JOIN   pedidos       AS pe ON pe.id = d.pedido_id
JOIN   lineas_pedido AS lp ON lp.pedido_id = pe.id
GROUP  BY d.id, d.pedido_id, d.importe
HAVING d.importe > ROUND(SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento)), 2)
                   + MAX(pe.gastos_envio);

Sobre la base recién recargada devuelve 0 filas: las tres devoluciones (26,75 €, 34,02 € y 19,80 € sobre los pedidos 6, 10 y 13) están todas por debajo del total de su pedido. Con la fila de 9999,00 € insertada, devuelve una.

Solución 3

1. Parámetro a parámetro:

Parámetro Qué se pierde en un corte
synchronous_commit = off Las últimas transacciones confirmadas (fracciones de segundo). La base queda íntegra y coherente: simplemente no llegó a existir lo último
fsync = off Potencialmente la base entera. Sin sincronización, las escrituras llegan al disco en cualquier orden y la recuperación puede encontrarse el WAL y los datos en estados incompatibles. El resultado es corrupción silenciosa
full_page_writes = off Protección frente a escrituras parciales de página. Si el sistema cae escribiendo una página de 8 KB y solo se graban 4, sin esta opción el WAL no puede reconstruirla y esa página queda rota

2. La réplica no salva nada porque replica lo que el primario dice que ocurrió. Si el primario corrompe sus datos, propaga datos corruptos; y si el fallo se detecta días después, la corrupción ya está en la réplica y en las copias de las últimas noches. Una réplica protege del fallo de hardware de un servidor, no de la corrupción lógica.

3. Razonable en una carga masiva y reejecutable: rellenar una tabla de métricas o de análisis con SET LOCAL synchronous_commit = off, donde perder los últimos segundos solo significa relanzar el proceso. Inaceptable en pedidos, lineas_pedido y devoluciones: perder un pedido confirmado significa que el cliente pagó y en el sistema no hay nada, y ningún ahorro de latencia compensa eso.

4. PITR. Con una copia base y el archivado continuo del WAL se puede restaurar la base a cualquier instante anterior al fallo. Lo que hay que tener configurado antes —y esta es toda la moraleja— es wal_level = replica (o superior), archive_mode = on con su archive_command, un destino de archivo fiable y copias base periódicas y probadas. Una copia de seguridad que nunca se ha restaurado no es una copia de seguridad: es una esperanza.

Conclusión

Ya sabes qué prometen exactamente las cuatro letras y qué hay debajo de cada una:

  • Atomicidad: todo o nada. Y en PostgreSQL el ROLLBACK no restaura nada: marca la transacción como abortada y sus versiones de fila dejan de ser visibles. Por eso deshacer es gratis aquí y caro en InnoDB u Oracle.
  • Consistencia: de un estado válido a otro según las reglas declaradas. CHECK, FK, UNIQUE y NOT NULL son la mitad que firma la base; la otra mitad es tuya. Y el caso del stock enseña a usar las dos vías: el CHECK (stock >= 0) como red inviolable y el UPDATE ... WHERE stock >= 1 como flujo de negocio, que además es seguro frente a la concurrencia por ser una sola sentencia.
  • Aislamiento: las transacciones concurrentes se comportan como si fueran secuenciales. Es la única letra negociable, y esa negociación son los niveles de aislamiento de 09-04.
  • Durabilidad: el WAL se escribe y se sincroniza antes que los datos, por eso un COMMIT sobrevive a un corte de corriente. synchronous_commit es la palanca legítima; fsync = off no lo es. Y del WAL salen también la replicación y el PITR, que es la respuesta que 05-04 dejó pendiente sobre cómo recuperar un borrado.
  • MVCC: cada fila vive en varias versiones, marcadas con xmin y xmax, y cada transacción ve las que su instantánea permite. De ahí que los lectores no bloqueen a los escritores, y de ahí que un UPDATE cree una fila nueva con otro ctid y deje la vieja muerta.
  • Y de ahí el bloat: las versiones muertas se acumulan, VACUUM las marca como reutilizables, y una transacción abierta se lo impide. La cadena que 08-05 dejó a medias queda cerrada, incluida la razón de ser de CREATE INDEX CONCURRENTLY.
  • Los límites de ACID: no valida tu lógica, no adivina tus reglas, no se extiende a la pasarela de pago y no cruza gratis la frontera de un servidor — de ahí BASE y los sistemas distribuidos.

Con la teoría en su sitio, toca el repertorio completo de mandos. En Instrucciones de control de transacciones verás todas las opciones de BEGIN (ISOLATION LEVEL, READ ONLY, DEFERRABLE), los SAVEPOINT —que permiten deshacer solo una parte de una transacción y, de paso, rescatarte del estado abortado de 09-01—, cómo fijar el nivel por omisión, cómo se comporta el autocommit de psycopg, JDBC y SQLAlchemy, por qué en PostgreSQL puedes hacer ROLLBACK de un CREATE TABLE y en MySQL no, y el patrón de aplicación completo: try / except / rollback, reintentos idempotentes y la prohibición de llamar a servicios externos con una transacción abierta. Todo ello construyendo, de principio a fin, la confirmación de un pedido de TiendaVerde.

Curso de SQL

Módulo 1: Introducción a SQL

Módulo 2: Consultas básicas de SQL

Módulo 3: Trabajando con múltiples tablas

Módulo 4: Filtrado avanzado de datos

Módulo 5: Manipulación de datos

Módulo 6: Funciones avanzadas de SQL

Módulo 7: Subconsultas y consultas anidadas

Módulo 8: Índices y optimización de rendimiento

Módulo 9: Transacciones y concurrencia

Módulo 10: Temas avanzados

Módulo 11: SQL en la práctica

Módulo 12: Proyecto final

© Copyright 2026. Todos los derechos reservados