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
- Las cuatro garantías de un vistazo
- Atomicidad: todo o nada
- Consistencia: de un estado válido a otro estado válido
- Isolation (aislamiento): como si fueran secuenciales
- Durabilidad: el WAL,
fsyncysynchronous_commit - MVCC: aislamiento sin bloquear las lecturas
- De MVCC al bloat: por qué existe
VACUUM - Cómo implementa cada motor el aislamiento
- Lo que ACID no te garantiza
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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).
- 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
ROLLBACKno restaura nada: se limita a anotar enpg_xactque 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.
- 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:
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:
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
CHECKes la red que garantiza el invariante pase lo que pase; elWHEREcondicional 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.
- 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.
- Durabilidad: el WAL,
fsync y synchronous_commit
fsync y synchronous_commitPromesa: 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. Consynchronous_commit = offpuedes perder las últimas transacciones, pero la base queda íntegra. Confsync = offpuedes 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 aquelDELETE FROM pedidos;sinWHERE. 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.
- 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:
| 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:
- El
xmines otro: esta es una fila nueva, creada por la transacción 812. - El
ctides otro: está en otro sitio físico de la página. La fila vieja sigue ahí, en(0,15), con suxmaxahora puesto a 812. - 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
xminestá confirmado y es anterior a mi instantánea, y suxmaxestá 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
xminde la versión nueva es su propia transacción. - B no lo ve porque ese
xmincorresponde 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
ROLLBACKno necesita borrar nada: basta con que la transacción quede marcada como abortada enpg_xactpara que suxminno 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.
- De MVCC al bloat: por qué existe
VACUUM
VACUUMY 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:
- Por qué hay filas muertas. Porque son las versiones antiguas que MVCC necesitó para no bloquear a nadie.
- Por qué una transacción abierta impide limpiar. Porque
VACUUMsolo puede eliminar una versión si ninguna transacción viva puede necesitarla. Una sesiónidle in transactiondesde 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. - 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.
- 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.
- Lo que ACID no te garantiza
Cuatro límites honestos, porque ACID se cita mucho más de lo que se entiende:
- 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.
- No garantiza las reglas que no declaraste. La C solo firma lo que hay en el esquema (apartado 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. - No sobrevive intacto al reparto entre varias bases de datos. Coordinar un
COMMITentre 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
psqlo una segunda aplicación. ElCHECKestá siempre; tuifsolo 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 elWHERE stock >= 1y lee las filas afectadas. - Confundir
synchronous_commit = offconfsync = off. El primero arriesga las últimas transacciones; el segundo arriesga la base entera. - Suponer que un
ROLLBACKgrande 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
DELETElibera espacio, o queUPDATEreescribe la fila. Ninguna de las dos: dejan versiones muertas, y el espacio solo se reutiliza trasVACUUM. - 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,xmaxyctiduna vez en tu vida. Ver físicamente cómo unUPDATEcrea 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.
- Consulta
xmin,xmax,ctidystockdel producto 15 y anótalos. - Ejecuta
UPDATE productos SET stock = stock - 1 WHERE id = 15;tres veces seguidas (en autocommit), consultando las mismas columnas después de cada una. - Consulta
n_live_tupyn_dead_tupenpg_stat_user_tablesparaproductos. Explica los dos números. - Ejecuta
VACUUM productos;y vuelve a mirarlos. ¿Ha bajado el tamaño del fichero de la tabla (pg_relation_size('productos'))? ¿Por qué? - Ahora repite el paso 2 dentro de
BEGIN…ROLLBACK. ¿Cuántas filas muertas quedan tras elROLLBACK? ¿Y por qué elROLLBACKfue 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);- ¿Se inserta? ¿Qué letra de ACID está en juego y por qué el motor no protesta?
- ¿Se puede expresar esa regla con un
CHECK? Razónalo mirando qué información necesita la comprobación. - Enumera tres formas de hacerla cumplir, indicando en cada una quién la garantiza y qué agujero deja.
- 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":
Y añade: "total, tenemos una réplica y hacemos copia de seguridad cada noche".
- Explica, parámetro a parámetro, qué se pierde exactamente en un corte de corriente.
- ¿Por qué la réplica no resuelve el problema de
fsync = off? - ¿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. - ¿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
ROLLBACKno 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,UNIQUEyNOT NULLson la mitad que firma la base; la otra mitad es tuya. Y el caso del stock enseña a usar las dos vías: elCHECK (stock >= 0)como red inviolable y elUPDATE ... WHERE stock >= 1como 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
COMMITsobrevive a un corte de corriente.synchronous_commites la palanca legítima;fsync = offno 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
xminyxmax, 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 unUPDATEcree una fila nueva con otroctidy deje la vieja muerta. - Y de ahí el bloat: las versiones muertas se acumulan,
VACUUMlas 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 deCREATE 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
- ¿Qué es SQL?
- Configurando tu entorno SQL
- Sintaxis básica de SQL
- Entendiendo bases de datos y tablas
- El modelo relacional: claves primarias y foráneas
- La base de datos del curso: TiendaVerde
Módulo 2: Consultas básicas de SQL
- Instrucción SELECT
- Alias, expresiones y columnas calculadas
- Filtrando datos con WHERE
- DISTINCT y eliminación de duplicados
- Ordenando datos con ORDER BY
- Limitando resultados con LIMIT
Módulo 3: Trabajando con múltiples tablas
- Operaciones JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN y CROSS JOIN
- Uniones de conjuntos: UNION, INTERSECT y EXCEPT
Módulo 4: Filtrado avanzado de datos
- Usando LIKE para coincidencia de patrones
- Operadores IN y BETWEEN
- Valores NULL y IS NULL
- Funciones de agregación: COUNT, SUM, AVG, MIN y MAX
- Agregando datos con GROUP BY
- Cláusula HAVING
Módulo 5: Manipulación de datos
- Creando tablas y restricciones con CREATE TABLE
- Instrucción INSERT
- Instrucción UPDATE
- Instrucción DELETE
- Instrucción UPSERT (MERGE)
- Modificando el esquema: ALTER TABLE y migraciones seguras
Módulo 6: Funciones avanzadas de SQL
- Funciones de cadena
- Funciones numéricas
- Funciones de fecha y hora
- Conversión de tipos y manejo de NULL: CAST y COALESCE
- Expresiones condicionales
Módulo 7: Subconsultas y consultas anidadas
- Introducción a subconsultas
- Subconsultas correlacionadas
- EXISTS y NOT EXISTS
- Usando subconsultas en cláusulas SELECT, FROM y WHERE
- Subconsultas o JOIN: cuál elegir
Módulo 8: Índices y optimización de rendimiento
- Entendiendo los índices
- Creación y gestión de índices
- Tipos de índice y cuándo no indexar
- Técnicas de optimización de consultas
- Análisis del rendimiento de consultas
Módulo 9: Transacciones y concurrencia
- Introducción a las transacciones
- Propiedades ACID
- Instrucciones de control de transacciones
- Niveles de aislamiento y anomalías de concurrencia
- Manejo de concurrencia: bloqueos e interbloqueos
Módulo 10: Temas avanzados
- Vistas
- Expresiones de tabla comunes (CTE)
- Funciones de ventana
- Procedimientos almacenados
- Triggers
- JSON y datos semiestructurados
Módulo 11: SQL en la práctica
- Casos de uso en el mundo real
- Mejores prácticas
- Seguridad: inyección SQL, permisos y roles
- SQL para análisis de datos
- SQL en desarrollo web
