Durante cinco módulos hemos mirado el esquema de BiblioRed como se mira un plano: sobre la mesa, quieto, con tiempo para discutir si esa columna sobra o si esa clave ajena falta. El plano ya está bien. Las tablas están normalizadas, las restricciones escritas y las cuatro desnormalizaciones justificadas por escrito.
Esta mañana el plano se ha convertido en un edificio. El sistema ha entrado en producción en las cuatro sucursales de Vallmar y, con él, ha entrado el mundo real: hay dos personas en el mostrador de la sucursal Centro registrando préstamos a la vez, hay un socio pagando una multa con tarjeta mientras el servidor decide apagarse, y hay una consulta que en el portátil de desarrollo tardaba 30 milisegundos y aquí tarda catorce segundos.
Este módulo trata de todo eso. Y empieza por la pieza sobre la que descansa el resto: la transacción.
Una transacción es la respuesta de las bases de datos a una pregunta incómoda: ¿qué pasa si una operación se interrumpe por la mitad? No es una pregunta teórica. El servidor se apaga, el proceso muere, la red se corta, la aplicación lanza una excepción, el operario cierra la ventana. La única pregunta relevante no es si va a ocurrir, sino qué queda en la base de datos cuando ocurre. Y la respuesta que da una base de datos transaccional es tan simple como radical: queda todo, o no queda nada.
En esta lección veremos qué es una transacción y por qué existe, cómo se controla desde SQL, cómo se comporta cuando algo falla, y qué significan de verdad las cuatro letras de ACID —con especial detenimiento en la D de durabilidad y en el mecanismo que la hace posible, el registro de escritura anticipada o WAL, que es el mismo mecanismo que reaparecerá en la lección 06-04 cuando hablemos de copias de seguridad.
Contenido
- El problema: registrar un préstamo son tres operaciones
- Qué es una transacción
- Control de transacciones en SQL:
BEGIN,COMMIT,ROLLBACK - El modo autoconfirmación: cada instrucción suelta ya es una transacción
- Puntos de guardado:
SAVEPOINT,ROLLBACK TOyRELEASE - El ciclo de vida de una transacción
- Atomicidad: todo o nada
- Consistencia: de un estado válido a otro válido
- Aislamiento: enunciado aquí, desarrollado en 06-02
- Durabilidad y el registro de escritura anticipada (WAL)
- Puntos de control y recuperación tras una caída
- El coste de la durabilidad y los parámetros que la relajan
- Errores dentro de una transacción: el comportamiento de PostgreSQL
- Transacciones y DDL
- Buenas prácticas al escribir transacciones
- Fuera de PostgreSQL: SQLite y MongoDB
- El problema: registrar un préstamo son tres operaciones
Empecemos por el caso más común de BiblioRed. Marta Alsina (socia 14) se acerca al mostrador de la sucursal Centro con el ejemplar EJ-3081 de "El mapa del tiempo". Tenía una reserva pendiente sobre ese material. La persona del mostrador pulsa "Prestar".
Lo que la aplicación tiene que hacer en la base de datos son tres operaciones distintas:
-- 1) Registrar el préstamo
INSERT INTO prestamos (socio_id, ejemplar_id, fecha_prestamo, fecha_devolucion_prevista)
VALUES (14, 3081, CURRENT_DATE, CURRENT_DATE + INTERVAL '21 days');
-- 2) Marcar el ejemplar como prestado
UPDATE ejemplares
SET estado = 'prestado'
WHERE ejemplar_id = 3081;
-- 3) Cerrar la reserva que dio origen al préstamo
UPDATE reservas
SET estado = 'atendida'
WHERE socio_id = 14
AND material_id = (SELECT material_id FROM ejemplares WHERE ejemplar_id = 3081)
AND estado = 'activa';Tres instrucciones. Cada una, por separado, es correcta. Y sin embargo el conjunto es una bomba, porque hay dos momentos en los que el mundo puede detenerse:
| Si falla... | Estado que queda en la base de datos | Qué significa en la biblioteca |
|---|---|---|
| Después de (1), antes de (2) | Hay un préstamo registrado, pero el ejemplar figura como disponible |
El catálogo web ofrece un ejemplar que Marta se ha llevado a casa. Otro socio se desplaza a Centro para nada |
| Después de (2), antes de (3) | El ejemplar está prestado, pero la reserva sigue activa |
Marta tiene el libro y sigue en cola por él. El sistema le avisará de que "su reserva está disponible" |
| Después de (1) y (2), antes de (3) | Igual que el anterior, y además la reserva bloquea el siguiente ejemplar que se devuelva | La cola de reservas se corrompe silenciosamente |
Ninguno de estos estados intermedios es un estado válido de la biblioteca. No existe una biblioteca en la que un libro esté prestado y disponible al mismo tiempo. Existe en la base de datos porque hemos escrito tres instrucciones donde el negocio tiene un solo hecho: "Marta se ha llevado el ejemplar EJ-3081".
Fíjate en que ninguna restricción del módulo 4 nos salva de esto. Un CHECK comprueba una fila; una clave ajena comprueba una referencia. Ninguna de las dos puede expresar "estas tres instrucciones van juntas". Necesitamos otra herramienta, de otra naturaleza: una que no hable de datos, sino de tiempo.
- Qué es una transacción
Definición. Una transacción es una secuencia de operaciones sobre la base de datos que el gestor trata como una sola unidad indivisible de trabajo: o se aplican todas sus operaciones, o no se aplica ninguna.
Tres consecuencias que conviene tener claras desde el principio:
- La transacción es una unidad lógica, no técnica. Su tamaño lo decide el negocio, no el motor. "Registrar un préstamo" es una transacción porque en la biblioteca es un acto único. Que sean tres
UPDATEo siete es irrelevante. - La frontera la marca quien escribe el código. El gestor no puede adivinar que esos tres
UPDATEvan juntos. Alguien tiene que decírselo, y ese alguien eres tú. - Una transacción no es solo "un grupo de instrucciones". Es un grupo de instrucciones con cuatro garantías asociadas —las propiedades ACID— que el gestor se compromete a cumplir aunque se corte la luz.
El acrónimo ACID lo acuñaron Theo Härder y Andreas Reuter en 1983, formalizando ideas que Jim Gray venía desarrollando desde los años setenta en IBM. Recordarás del recorrido histórico de 01-03 que este es exactamente el periodo en que las bases de datos relacionales pasaron de prototipo de laboratorio a sistema de producción bancario: sin transacciones fiables, ese salto no habría sido posible.
| Letra | Propiedad | Pregunta que responde |
|---|---|---|
| A | Atomicidad | ¿Puede quedar la operación a medias? |
| C | Consistencia | ¿Puede la base quedar en un estado que viole sus reglas? |
| I | Aislamiento | ¿Puede otra transacción ver mi trabajo a medio hacer o estropearlo? |
| D | Durabilidad | ¿Puede perderse algo que ya me confirmaron? |
La respuesta a las cuatro, en un gestor transaccional, es no. Las veremos una a una a partir del apartado 7. Antes hay que saber escribirlas.
- Control de transacciones en SQL:
BEGIN, COMMIT, ROLLBACK
BEGIN, COMMIT, ROLLBACKEl vocabulario es corto y no ha cambiado en cuarenta años:
| Instrucción | Qué hace | Sinónimos |
|---|---|---|
BEGIN |
Abre una transacción explícita | START TRANSACTION, BEGIN TRANSACTION, BEGIN WORK |
COMMIT |
Confirma: todo lo hecho pasa a ser definitivo y visible | COMMIT WORK, END |
ROLLBACK |
Deshace: todo lo hecho desde el BEGIN desaparece |
ROLLBACK WORK, ABORT |
START TRANSACTION es la forma del estándar SQL; BEGIN es la forma corta que PostgreSQL admite y que se usa en la práctica. Son equivalentes.
El préstamo de Marta, escrito correctamente:
BEGIN;
INSERT INTO prestamos (socio_id, ejemplar_id, fecha_prestamo, fecha_devolucion_prevista)
VALUES (14, 3081, DATE '2026-08-02', DATE '2026-08-23');
UPDATE ejemplares SET estado = 'prestado' WHERE ejemplar_id = 3081;
UPDATE reservas SET estado = 'atendida'
WHERE socio_id = 14 AND material_id = 907 AND estado = 'activa';
COMMIT;Resultado esperado:
Y si algo va mal en medio —la aplicación detecta que el ejemplar ya estaba prestado, o el socio está dado de baja— basta con cambiar la última línea:
Después del ROLLBACK la base de datos está exactamente como estaba antes del BEGIN. No hay préstamo, el ejemplar sigue disponible y la reserva sigue activa. No hay que "deshacer a mano" nada: deshacer es responsabilidad del gestor, y lo hace bien.
Comprobarlo tú mismo con dos terminales
Esta es la primera de varias demostraciones que debes reproducir abriendo dos terminales con psql conectados a la misma base de datos. Llamaremos Sesión A y Sesión B a cada una. Ejecuta las instrucciones en el orden de la columna "momento": el orden importa, y ahí está toda la enseñanza.
| Momento | Sesión A (mostrador Centro) | Sesión B (catálogo web) |
|---|---|---|
| t1 | BEGIN; |
|
| t2 | UPDATE ejemplares SET estado='prestado' WHERE ejemplar_id=3081; → UPDATE 1 |
|
| t3 | SELECT estado FROM ejemplares WHERE ejemplar_id=3081; → prestado |
|
| t4 | SELECT estado FROM ejemplares WHERE ejemplar_id=3081; → disponible |
|
| t5 | ROLLBACK; |
|
| t6 | SELECT estado FROM ejemplares WHERE ejemplar_id=3081; → disponible |
Dos cosas que aprender aquí:
- Dentro de la transacción, A ve sus propios cambios (t3). Es coherente: A está trabajando.
- Fuera de la transacción, B no ve nada (t4) hasta que A confirme. Es la propiedad de aislamiento asomándose. Qué ve exactamente cada sesión, en qué momento y bajo qué reglas, es el contenido íntegro de la lección 06-02; aquí solo nos interesa constatar que el trabajo no confirmado es invisible para los demás.
- El modo autoconfirmación: cada instrucción suelta ya es una transacción
Una pregunta razonable: si las transacciones se abren con BEGIN, ¿qué pasa con los cientos de INSERT sueltos que escribimos en los módulos 2 y 3, sin BEGIN a la vista? ¿Estaban desprotegidos?
No. Estaban dentro de una transacción, solo que implícita.
Autoconfirmación (autocommit). Cuando el cliente no ha abierto una transacción explícita, el gestor envuelve cada instrucción individual en su propia transacción, que se confirma automáticamente si la instrucción tiene éxito y se deshace si falla.
Es decir, esto:
se comporta internamente como esto:
Y tiene una consecuencia muy útil que suele pasar desapercibida: una única instrucción SQL ya es atómica. Si ese UPDATE afecta a 9.400 ejemplares y falla en el 9.399 porque uno viola un CHECK, no quedan 9.398 filas modificadas: no queda ninguna. El estándar exige exactamente esto, y todos los gestores serios lo cumplen.
| Situación | ¿Hace falta BEGIN explícito? |
|---|---|
| Una sola instrucción, sin lógica alrededor | No. La autoconfirmación basta |
| Dos o más instrucciones que deben ir juntas | Sí, siempre |
| Una instrucción, pero con lógica de la aplicación entre la lectura y la escritura | Sí (leer el estado y decidir con él es ya una operación compuesta) |
Un INSERT masivo de 200.000 filas |
Sí, por rendimiento: una transacción por fila obliga a 200.000 confirmaciones a disco |
Ese último punto tiene una medida concreta. Cargar 200.000 filas en prestamos fila a fila en autoconfirmación puede tardar varios minutos; las mismas 200.000 filas dentro de un solo BEGIN ... COMMIT tardan unos segundos. La diferencia no está en el INSERT, está en el COMMIT: cada confirmación obliga a sincronizar el registro con el disco, y eso lo veremos en el apartado 10.
Cuidado con el modo de tu cliente
No todos los clientes se comportan igual, y esta es una fuente inagotable de sorpresas:
| Entorno | Comportamiento por omisión |
|---|---|
psql |
Autoconfirmación activada. BEGIN la desactiva hasta el COMMIT/ROLLBACK |
| Controlador JDBC (Java) | Autoconfirmación activada; se desactiva con setAutoCommit(false) |
psycopg (Python) |
Autoconfirmación desactivada: abre transacción sola y hay que llamar a commit() |
| Muchos ORM | Abren transacción por petición o por "unidad de trabajo"; conviene saber cuál |
SQLite (sqlite3 CLI) |
Autoconfirmación activada |
El error clásico con psycopg es escribir un INSERT, no llamar a commit(), cerrar el programa y no encontrar la fila. No se ha perdido: se ha deshecho, que es justo lo que debe pasar con una transacción que nunca se confirmó.
- Puntos de guardado:
SAVEPOINT, ROLLBACK TO y RELEASE
SAVEPOINT, ROLLBACK TO y RELEASEUn ROLLBACK es un instrumento contundente: deshace la transacción entera. A veces se necesita algo más fino, y para eso están los puntos de guardado.
Punto de guardado (savepoint). Marca con nombre dentro de una transacción abierta que permite deshacer el trabajo posterior a esa marca sin abortar la transacción completa.
Las tres instrucciones:
| Instrucción | Efecto |
|---|---|
SAVEPOINT nombre |
Coloca una marca |
ROLLBACK TO SAVEPOINT nombre |
Deshace todo lo hecho después de la marca. La transacción sigue viva |
RELEASE SAVEPOINT nombre |
Elimina la marca (ya no se podrá volver a ella). No deshace nada |
Para qué sirven de verdad
La documentación suele presentarlos con ejemplos artificiales. Los dos usos reales son estos:
Uso 1: operaciones opcionales dentro de una operación obligatoria.
Al registrar la devolución de un préstamo vencido, BiblioRed intenta emitir la multa correspondiente. Si el cálculo de la multa falla —porque el tipo de multa no está configurado para ese material, por ejemplo—, la devolución debe registrarse igualmente: es intolerable que un socio no pueda devolver un libro porque el sistema de multas está mal configurado.
BEGIN;
-- Obligatorio: registrar la devolución
UPDATE prestamos SET fecha_devolucion = CURRENT_DATE WHERE prestamo_id = 88214;
UPDATE ejemplares SET estado = 'disponible' WHERE ejemplar_id = 3081;
-- Opcional: emitir la multa por retraso
SAVEPOINT antes_multa;
INSERT INTO multas (socio_id, prestamo_id, motivo, importe, fecha_emision, estado)
VALUES (14, 88214, 'retraso', 3.50, CURRENT_DATE, 'pendiente');
-- Si esto falla, la aplicación ejecuta:
-- ROLLBACK TO SAVEPOINT antes_multa;
-- y registra el incidente para revisión manual
RELEASE SAVEPOINT antes_multa;
COMMIT;Resultado esperado en el camino feliz:
Y en el camino con fallo, tras el ROLLBACK TO SAVEPOINT antes_multa, el COMMIT final confirma la devolución sin la multa. Que es exactamente lo que la biblioteca quiere.
Uso 2: recuperarse de un error sin perder el trabajo.
Este es el uso decisivo en PostgreSQL, y se entiende mejor en el apartado 13: cuando una instrucción falla dentro de una transacción, PostgreSQL aborta la transacción entera y rechaza todo lo que venga después. Un punto de guardado es la única forma de sobrevivir a un error y continuar. De hecho, cuando un controlador ofrece "reintentar esta instrucción", casi siempre está poniendo un SAVEPOINT implícito antes de cada instrucción.
El precio
Los puntos de guardado no son gratis: cada uno consume recursos internos del gestor. Poner uno antes de cada instrucción en un bucle de 100.000 iteraciones degrada el rendimiento de forma perceptible. Úsalos donde hay una decisión real que tomar, no por sistema.
- El ciclo de vida de una transacción
El comportamiento que hemos visto responde a un autómata muy sencillo, presente en cualquier libro de texto y en la implementación de cualquier gestor:
stateDiagram-v2
[*] --> Activa: BEGIN
Activa --> Activa: SELECT / INSERT / UPDATE / DELETE
Activa --> ParcialmenteConfirmada: última instrucción ejecutada, COMMIT solicitado
ParcialmenteConfirmada --> Confirmada: registro sincronizado en disco
ParcialmenteConfirmada --> Fallida: fallo al escribir el registro
Activa --> Fallida: error de instrucción / ROLLBACK / caída
Fallida --> Abortada: se deshacen los cambios (rollback)
Confirmada --> [*]
Abortada --> [*]
Los cinco estados, con su significado práctico:
| Estado | Qué significa | ¿Los cambios son visibles para otros? |
|---|---|---|
| Activa | La transacción se está ejecutando | No |
| Parcialmente confirmada | Se ha pedido COMMIT, pero el registro aún no está garantizado en disco |
No |
| Confirmada | El COMMIT ha terminado con éxito |
Sí, y ya no hay marcha atrás |
| Fallida | Algo ha impedido continuar | No |
| Abortada | Los cambios se han deshecho; la base está como antes del BEGIN |
No, y nunca lo serán |
Hay dos detalles que suelen pasarse por alto y que aquí importan mucho.
El primero: "parcialmente confirmada" no es un tecnicismo. Es el instante crítico. La aplicación ha pedido COMMIT, el gestor ha aplicado los cambios en memoria, pero todavía no ha recibido la confirmación del disco de que el registro está a salvo. Si la máquina cae en ese microsegundo, la transacción no se ha confirmado y se deshará al arrancar. Por eso el gestor no le responde "COMMIT" al cliente hasta estar seguro: la respuesta al cliente es la promesa de durabilidad.
El segundo: de "confirmada" no se sale. No existe "des-confirmar". Un ROLLBACK después de un COMMIT no deshace nada —abre una transacción vacía y la deshace—. Si necesitas revertir algo ya confirmado, tienes que escribir la operación inversa, o restaurar de una copia (lección 06-04). Esta irreversibilidad es una característica, no un defecto: es lo que permite construir encima.
- Atomicidad: todo o nada
Atomicidad. Una transacción es indivisible: o se aplican todas sus operaciones o no se aplica ninguna. No existe estado intermedio observable ni persistente.
Qué garantiza. Que las tres instrucciones del préstamo de Marta se comporten como una sola. Que no exista jamás en prestamos una fila sin su correspondiente ejemplares.estado = 'prestado'.
Qué falla si no está. Exactamente la tabla de estados intermedios del apartado 1: préstamos fantasma, ejemplares prestados que figuran disponibles, reservas huérfanas. Y lo peor es que estos fallos son silenciosos. No hay error, no hay traza, no hay excepción. Solo una biblioteca que un día descubre que su inventario no cuadra y no sabe desde cuándo.
Cómo la implementa el gestor. Con la información necesaria para deshacer. Antes de modificar un dato, el gestor registra en algún sitio lo suficiente para volver atrás:
- PostgreSQL no sobrescribe las filas: cada
UPDATEcrea una versión nueva de la fila y marca la vieja como obsoleta a partir de esa transacción. Deshacer es tan simple como marcar la transacción como abortada: las versiones nuevas dejan de ser visibles para todo el mundo y las viejas siguen ahí. Este mecanismo es el MVCC, y su tratamiento completo es de 06-02. La consecuencia interesante es que en PostgreSQL unROLLBACKes más barato que unCOMMIT, al contrario que en otros gestores. - Oracle, MySQL/InnoDB usan un segmento de deshacer (undo): guardan la imagen anterior de cada fila modificada y, al deshacer, la reponen.
Ambos caminos llegan al mismo sitio. La diferencia práctica es que PostgreSQL paga después, limpiando las versiones muertas con VACUUM (06-02), y los otros pagan durante, manteniendo el segmento de deshacer.
Comprobación práctica de la atomicidad
Provoquemos un fallo a propósito en mitad de una transacción. Vamos a intentar prestar un ejemplar a un socio que no existe (el 9999), con la clave ajena de 02-06 haciendo su trabajo:
BEGIN;
UPDATE ejemplares SET estado = 'prestado' WHERE ejemplar_id = 3082;
INSERT INTO prestamos (socio_id, ejemplar_id, fecha_prestamo, fecha_devolucion_prevista)
VALUES (9999, 3082, CURRENT_DATE, CURRENT_DATE + 21);
ROLLBACK;
SELECT estado FROM ejemplares WHERE ejemplar_id = 3082;Resultado esperado:
BEGIN UPDATE 1 ERROR: insert or update on table "prestamos" violates foreign key constraint "prestamos_socio_id_fkey" DETAIL: Key (socio_id)=(9999) is not present in table "socios". ROLLBACK estado ------------ disponible
El UPDATE había funcionado. La atomicidad lo ha borrado del mapa. El ejemplar sigue disponible, como debe ser.
- Consistencia: de un estado válido a otro válido
Consistencia. Una transacción lleva la base de datos de un estado válido a otro estado válido. Si la base cumplía todas sus reglas antes de la transacción, las cumple después.
Qué garantiza. Que al confirmar, ninguna clave ajena apunta a la nada, ningún CHECK está violado, ningún UNIQUE está duplicado y ningún NOT NULL está vacío. Todo el catálogo de restricciones de 04-04 y la integridad referencial de 02-06 siguen en pie al otro lado del COMMIT.
Qué falla si no está. Datos que contradicen las reglas del negocio: multas asignadas a préstamos inexistentes, inscripciones a eventos borrados, ejemplares en sucursales que cerraron.
Cómo la implementa el gestor. Comprobando las restricciones. La mayoría se comprueban al ejecutar cada instrucción; algunas pueden diferirse al COMMIT si se declararon DEFERRABLE, lo que es imprescindible cuando dos tablas se referencian mutuamente:
BEGIN;
SET CONSTRAINTS ALL DEFERRED;
-- Ahora podemos insertar en orden "imposible": las claves ajenas
-- no se comprueban hasta el COMMIT
INSERT INTO ...;
INSERT INTO ...;
COMMIT; -- aquí se verifican todas las restricciones diferidasSi al llegar al COMMIT alguna restricción diferida no se cumple, el COMMIT falla y la transacción se deshace entera. Es la consistencia haciendo su trabajo en el último segundo.
El matiz: la C es la letra más discutida de las cuatro
Conviene decirlo, porque el estudiante que lea sobre el tema se lo encontrará: hay un consenso amplio en que la "C" de ACID no está a la misma altura que las otras tres.
Las razones son estas:
| Propiedad | ¿Quién es responsable de cumplirla? |
|---|---|
| Atomicidad | El gestor, íntegramente |
| Aislamiento | El gestor, íntegramente |
| Durabilidad | El gestor, íntegramente |
| Consistencia | A medias: el gestor comprueba las reglas que le has declarado; el resto es responsabilidad de tu código |
Si multas.importe puede ser negativo porque nadie escribió el CHECK, la base de datos aceptará -50,00 € sin protestar y habrá sido perfectamente "consistente": no ha violado ninguna regla, porque esa regla no existía. La consistencia del gestor es consistencia respecto a las restricciones declaradas, no respecto al sentido común.
Además, la atomicidad y el aislamiento ya implican buena parte de lo que la C promete. Por eso hay quien dice, con razón, que ACID son "tres propiedades y una letra que quedaba bien en el acrónimo". La postura útil para un profesional es intermedia: la C es un recordatorio de que las reglas del negocio deben estar declaradas en el esquema para que la transacción pueda protegerlas. Ese es exactamente el argumento del apartado 21 de la lección 04-04 sobre qué reglas van en la base y cuáles en la aplicación.
- Aislamiento: enunciado aquí, desarrollado en 06-02
Aislamiento. Cada transacción se ejecuta como si fuera la única del sistema. Los resultados de una transacción concurrente no interfieren con los de otra.
Qué garantiza. Que puedas razonar sobre tu transacción sin pensar en las otras once que se están ejecutando a la vez.
Qué falla si no está. Los dos mostradores de Centro prestan el mismo ejemplar EJ-3081 al mismo tiempo. Dos socios ocupan la misma última plaza del club de lectura. Un informe suma cifras de un instante y cifras de otro, y no cuadra con nada.
Cómo la implementa el gestor. Con bloqueos, con control multiversión (MVCC), o con una combinación de ambos.
Y aquí paramos deliberadamente. El aislamiento es, con diferencia, la más compleja de las cuatro propiedades: es la única que admite grados —el estándar SQL define cuatro niveles y cada uno permite unos fenómenos y prohíbe otros—, es la única en la que el gestor te deja elegir cuánta garantía quieres a cambio de cuánto rendimiento, y es la fuente de los errores más difíciles de reproducir de toda esta profesión.
Todo eso es el contenido íntegro de la lección 06-02: los fenómenos de concurrencia uno a uno con dos sesiones reproducibles, los cuatro niveles de aislamiento y su tabla canónica, los bloqueos compartidos y exclusivos, el MVCC de PostgreSQL, los interbloqueos y el bloqueo optimista frente al pesimista. Aquí nos basta con la definición y con saber que el aislamiento existe, que tiene niveles, y que el nivel por omisión de PostgreSQL —READ COMMITTED— no es el más estricto.
- Durabilidad y el registro de escritura anticipada (WAL)
Durabilidad. Una vez que el gestor ha respondido
COMMIT, los cambios sobreviven a cualquier fallo posterior: corte de luz, apagado del proceso, caída del sistema operativo.
Qué garantiza. Que cuando el mostrador ve "Préstamo registrado", el préstamo existe. Aunque el edificio se quede sin luz medio segundo después.
Qué falla si no está. Que la biblioteca crea que ha cobrado una multa que no consta. Y, sobre todo, que nadie sepa cuáles de las operaciones de la última hora sobrevivieron y cuáles no.
Cómo se implementa es la parte interesante, y merece la pena entenderla porque explica muchas cosas del comportamiento de PostgreSQL, incluido el rendimiento.
El problema: escribir en disco es lento y no es instantáneo
Recuerda el gestor de buffers de la lección 01-04. La base de datos no lee ni escribe directamente en disco: mantiene en memoria una caché de páginas (en PostgreSQL, shared_buffers). Cuando un UPDATE modifica una fila, lo que se modifica es la página en memoria. Esa página queda marcada como sucia (modificada y no escrita a disco) y se escribirá "más tarde".
Esto es imprescindible para el rendimiento: la memoria es varios órdenes de magnitud más rápida que el disco, y agrupar escrituras evita miles de operaciones de entrada/salida.
Pero crea un problema evidente: si el COMMIT solo modifica memoria, un corte de luz se lleva por delante todo lo confirmado.
La solución ingenua sería escribir a disco todas las páginas modificadas en cada COMMIT. Es correcta, y es inaceptablemente lenta: las páginas modificadas están dispersas por el fichero de datos, y escribirlas obliga a saltos de cabezal (en disco mecánico) o a reescribir bloques enteros (en SSD). Una transacción que toca tres tablas escribiría en tres sitios lejanos del disco.
La solución: escribir primero el registro
Registro de escritura anticipada (Write-Ahead Log, WAL). Antes de modificar una página de datos, el gestor escribe en un fichero de registro secuencial una anotación que describe el cambio. El registro se sincroniza a disco antes de confirmar la transacción; las páginas de datos pueden esperar.
La regla, enunciada de forma canónica, es de una simplicidad total:
Nunca se escribe un cambio en los ficheros de datos antes de haber escrito en disco el registro que lo describe.
¿Por qué es más rápido? Porque el registro es secuencial. Todas las anotaciones de todas las transacciones se añaden al final del mismo fichero, una detrás de otra. Escribir 4 KB al final de un fichero secuencial es la operación más barata que existe en cualquier sistema de almacenamiento. Escribir 4 KB en ocho sitios distintos del disco, no.
Así queda la secuencia real de un COMMIT:
sequenceDiagram
participant App as Aplicación
participant GT as Gestor de transacciones
participant Buf as Gestor de buffers (memoria)
participant WAL as Registro WAL (disco)
participant Dat as Ficheros de datos (disco)
App->>GT: BEGIN
App->>GT: UPDATE ejemplares ...
GT->>Buf: modifica la página en memoria (queda sucia)
GT->>WAL: anota el cambio (en el buffer del WAL)
App->>GT: COMMIT
GT->>WAL: escribe el registro de COMMIT y hace fsync()
WAL-->>GT: confirmado en disco
GT-->>App: COMMIT (ya es durable)
Note over Buf,Dat: más tarde, sin prisa
Buf->>Dat: el punto de control escribe las páginas sucias
Fíjate en el orden: la aplicación recibe el "COMMIT" en cuanto el registro está a salvo, no cuando los datos están escritos. Los datos pueden tardar minutos en llegar a su sitio definitivo. No importa: la información para reconstruirlos ya está en un sitio seguro.
En PostgreSQL el WAL vive en el directorio pg_wal/, en ficheros de 16 MB por omisión. Puedes verlo:
total 65540 drwx------ 3 postgres postgres 4096 ago 2 09:14 . -rw------- 1 postgres postgres 16777216 ago 2 12:38 000000010000000000000023 -rw------- 1 postgres postgres 16777216 ago 2 11:02 000000010000000000000024 -rw------- 1 postgres postgres 16777216 ago 2 11:02 000000010000000000000025
Y consultar la posición actual del registro (el LSN, Log Sequence Number, que es la dirección de un byte dentro del registro):
Ese número avanza con cada escritura. Es el reloj interno de la durabilidad, y volverá a aparecer en 06-04 cuando hablemos de recuperación a un instante concreto.
- Puntos de control y recuperación tras una caída
Si el registro creciera indefinidamente y hubiera que releerlo entero para recuperarse, arrancar una base de datos de dos años llevaría días. Por eso existen los puntos de control.
Punto de control (checkpoint). Operación periódica en la que el gestor escribe a disco todas las páginas sucias que hay en memoria y anota en el registro que, hasta ese punto, los ficheros de datos están al día.
Consecuencia: para recuperarse de una caída solo hay que leer el registro desde el último punto de control. Todo lo anterior ya está en los ficheros de datos.
Los parámetros que lo gobiernan en PostgreSQL:
| Parámetro | Valor típico | Qué controla |
|---|---|---|
checkpoint_timeout |
5min |
Tiempo máximo entre puntos de control |
max_wal_size |
1GB |
Cuánto WAL puede acumularse antes de forzar uno |
checkpoint_completion_target |
0.9 |
Reparte la escritura a lo largo del intervalo, para no provocar un pico de disco |
Hay un compromiso claro: puntos de control frecuentes hacen la recuperación rápida pero cargan el disco durante el funcionamiento normal; puntos de control espaciados son más suaves en marcha pero alargan el arranque tras una caída.
Qué ocurre exactamente al arrancar después de un corte
Supongamos que a las 12:41 se va la luz en el centro de proceso de datos de Vallmar. El último punto de control fue a las 12:37. Entre las 12:37 y las 12:41 hubo 214 transacciones: 209 confirmadas y 5 abiertas en el momento del corte.
Al arrancar, PostgreSQL detecta que el cierre no fue limpio y ejecuta la recuperación, en dos fases:
Fase 1 — Rehacer (redo). Lee el registro desde el último punto de control y vuelve a aplicar todos los cambios anotados, tanto los de transacciones confirmadas como los de las que no lo estaban. Suena raro, y es deliberado: es más rápido reaplicarlo todo y limpiar después que ir decidiendo caso por caso.
Fase 2 — Deshacer (undo). Se descartan los efectos de las transacciones que no llegaron a confirmarse. En PostgreSQL esta fase es casi gratuita gracias al MVCC: las 5 transacciones abiertas simplemente nunca constan como confirmadas en el mapa de estados de transacción, así que sus versiones de fila son invisibles para todo el mundo y se limpiarán con VACUUM. En un gestor con segmento de deshacer, esta fase sí implica trabajo real de reposición.
El resultado tras la recuperación es exacto: las 209 confirmadas están; las 5 abiertas no dejaron rastro. Ni una a medias.
En el registro del servidor se ve así:
LOG: database system was interrupted; last known up at 2026-08-02 12:37:14 CEST LOG: database system was not properly shut down; automatic recovery in progress LOG: redo starts at 0/23A18420 LOG: invalid record length at 0/23A4F8C0: wanted 24, got 0 LOG: redo done at 0/23A4F890 system usage: CPU: user: 0.31 s, system: 0.08 s, elapsed: 1.42 s LOG: database system is ready to accept connections
Ese "invalid record length" no es un error: es el gestor encontrando el final del registro válido, es decir, el instante exacto del corte. Léelo como "hasta aquí llegó la luz".
Este mecanismo —el registro, los puntos de control, rehacer y deshacer— es lo que en 01-04 llamamos gestor de transacciones y recuperación, trabajando codo con codo con el gestor de buffers. Ahora ya sabes qué hacen exactamente esas dos cajas del diagrama.
- El coste de la durabilidad y los parámetros que la relajan
El fsync() del apartado 10 —la llamada al sistema que obliga al disco a confirmar que ha escrito de verdad— es la operación más cara de todo el ciclo. En un SSD de servidor decente ronda los 0,1-1 ms; en un disco mecánico, entre 5 y 15 ms. Ese tiempo es un techo duro: una base de datos no puede confirmar más transacciones por segundo de las que su disco puede sincronizar.
PostgreSQL ofrece un parámetro para relajarlo, y hay que entender exactamente qué se compra y qué se paga:
| Valor | Qué hace | Qué se puede perder |
|---|---|---|
on (por omisión) |
Espera al fsync() del WAL antes de responder |
Nada |
off |
Responde COMMIT sin esperar al fsync(); el registro se escribe en los siguientes ~200 ms |
Las últimas transacciones confirmadas ante un corte de corriente |
local |
Espera al disco local, no a las réplicas | Las últimas transacciones ante la pérdida del primario |
remote_write |
Espera a que la réplica lo reciba | Menos, con réplicas (03-01) |
La ganancia es real y a veces espectacular: en cargas de muchas transacciones pequeñas, off puede multiplicar el número de transacciones por segundo. Pero la letra pequeña hay que decirla completa:
Con
synchronous_commit = offpuedes perder transacciones que el gestor ya te confirmó. La base de datos no queda corrupta —la atomicidad y la consistencia se mantienen: las transacciones perdidas se pierden enteras—, pero desaparecen operaciones que el usuario vio como completadas.
¿Es aceptable? Depende de la tabla:
| Operación de BiblioRed | ¿synchronous_commit = off? |
|---|---|
Cobro de una multa (pagos) |
Nunca. Es dinero |
| Registro de un préstamo | No. Es el inventario |
| Alta de un socio | No |
| Registro de "material consultado en sala" para estadísticas | Sí, razonablemente |
| Carga masiva nocturna de un histórico, repetible desde el fichero origen | Sí |
Regla práctica: si perder los últimos segundos obliga a llamar a alguien por teléfono, no lo desactives.
Nota adicional: existe también el parámetro fsync = off. Ese sí desactiva la protección de raíz y puede dejar la base corrupta e irrecuperable ante un corte. Su único uso legítimo es una base de datos desechable de pruebas que se puede regenerar con un script. Nunca en producción, bajo ninguna circunstancia y por ningún motivo.
- Errores dentro de una transacción: el comportamiento de PostgreSQL
Este apartado explica uno de los mensajes de error más frecuentes —y peor entendidos— de PostgreSQL.
Cuando una instrucción falla dentro de una transacción explícita, PostgreSQL aborta la transacción entera. No la instrucción: la transacción. A partir de ese momento cualquier instrucción se rechaza hasta que se ejecute ROLLBACK (o ROLLBACK TO SAVEPOINT).
BEGIN;
INSERT INTO socios (nombre, apellidos, email, fecha_alta, sucursal_id, activo)
VALUES ('Nuria', 'Bastos', '[email protected]', CURRENT_DATE, 3, true);
-- Error a propósito: sucursal inexistente
INSERT INTO socios (nombre, apellidos, email, fecha_alta, sucursal_id, activo)
VALUES ('Iván', 'Pereda', '[email protected]', CURRENT_DATE, 77, true);
-- Intentamos seguir como si nada
SELECT count(*) FROM socios;
COMMIT;BEGIN INSERT 0 1 ERROR: insert or update on table "socios" violates foreign key constraint "socios_sucursal_id_fkey" DETAIL: Key (sucursal_id)=(77) is not present in table "sucursales". ERROR: current transaction is aborted, commands ignored until end of transaction block ROLLBACK
Dos cosas notables:
- El
SELECT—que es inofensivo— también se rechaza. La transacción está envenenada. - El
COMMITfinal ha respondidoROLLBACK. PostgreSQL no confirma una transacción abortada: la deshace. Es un comportamiento seguro y a la vez traicionero, porque una aplicación que solo comprueba "¿me han respondido alCOMMIT?" creerá que todo fue bien.
Comparación entre gestores
| Gestor | Comportamiento ante un error dentro de la transacción |
|---|---|
| PostgreSQL | Aborta la transacción entera. Solo ROLLBACK o ROLLBACK TO SAVEPOINT la reviven |
| Oracle | Deshace solo la instrucción fallida; la transacción sigue viva |
| MySQL/InnoDB | Depende del error: la mayoría deshacen solo la instrucción; un interbloqueo deshace la transacción |
| SQL Server | Depende de la gravedad y de XACT_ABORT |
| SQLite | Deshace solo la instrucción (salvo errores graves) |
La postura de PostgreSQL es la más estricta, y es defendible: si una instrucción de tu unidad de trabajo ha fallado, lo más probable es que tu unidad de trabajo ya no tenga sentido. Pero obliga a escribir el código de otra manera.
La solución correcta
Si un error concreto es esperable y quieres sobrevivir a él, envuélvelo en un punto de guardado:
BEGIN;
INSERT INTO socios (nombre, apellidos, email, fecha_alta, sucursal_id, activo)
VALUES ('Nuria', 'Bastos', '[email protected]', CURRENT_DATE, 3, true);
SAVEPOINT sp_ivan;
INSERT INTO socios (nombre, apellidos, email, fecha_alta, sucursal_id, activo)
VALUES ('Iván', 'Pereda', '[email protected]', CURRENT_DATE, 77, true);
-- falla → la aplicación ejecuta:
ROLLBACK TO SAVEPOINT sp_ivan;
SELECT count(*) FROM socios; -- ahora sí funciona
COMMIT;BEGIN INSERT 0 1 SAVEPOINT ERROR: insert or update on table "socios" violates foreign key constraint "socios_sucursal_id_fkey" ROLLBACK count ------- 12001 COMMIT
El alta de Nuria se ha conservado. La de Iván no. La transacción ha llegado viva al COMMIT.
- Transacciones y DDL
Una característica de PostgreSQL que sorprende a quien viene de otros gestores: el DDL es transaccional. CREATE TABLE, ALTER TABLE, DROP TABLE, CREATE INDEX pueden ir dentro de una transacción y deshacerse con ROLLBACK.
BEGIN;
ALTER TABLE socios ADD COLUMN idioma_preferido TEXT DEFAULT 'es';
CREATE TABLE preferencias_socio (
socio_id INTEGER PRIMARY KEY REFERENCES socios(socio_id),
boletin BOOLEAN NOT NULL DEFAULT false
);
-- Nos arrepentimos
ROLLBACK;
SELECT column_name FROM information_schema.columns
WHERE table_name = 'socios' AND column_name = 'idioma_preferido';Ni la columna ni la tabla existen. Esto tiene una consecuencia operativa enorme: una migración de esquema en PostgreSQL puede ser atómica. Si el paso 7 de 9 falla, el esquema vuelve intacto al estado inicial en lugar de quedarse a medio migrar, que es la peor situación posible en una madrugada de despliegue.
| Gestor | ¿DDL transaccional? |
|---|---|
| PostgreSQL | Sí, casi todo el DDL |
| SQL Server | Sí, en gran medida |
| SQLite | Sí |
| Oracle | No: cada DDL confirma implícitamente la transacción en curso |
| MySQL/InnoDB | No hasta la versión 8.0, y aun así con limitaciones |
Las excepciones en PostgreSQL, que conviene conocer:
CREATE DATABASE,DROP DATABASE,CREATE TABLESPACEno pueden ir en una transacción.CREATE INDEX CONCURRENTLY—el que no bloquea la tabla— tampoco: es precisamente su forma de no bloquear. Volverá en 06-03.VACUUMtampoco.
Y una advertencia importante: que el DDL sea transaccional no significa que sea gratis. Un ALTER TABLE toma un bloqueo fuerte sobre la tabla, y mientras la transacción esté abierta nadie más podrá usarla. Los bloqueos son el tema de 06-02.
- Buenas prácticas al escribir transacciones
Cuatro reglas que evitan la mayoría de los problemas de producción. Las tres primeras se resumen en una idea: una transacción abierta es un recurso caro que alguien más está esperando.
- Transacciones cortas
Una transacción abierta retiene bloqueos, impide que VACUUM limpie versiones muertas y consume una ranura de conexión. Cuanto más dure, más molesta.
| Antipatrón | Alternativa |
|---|---|
| Abrir transacción, recorrer 500.000 filas, confirmar al final | Procesar por lotes de 1.000-10.000, confirmando cada lote |
| Meter en la misma transacción el préstamo y la regeneración del informe mensual | Dos transacciones: el préstamo es urgente, el informe no |
| Abrir al principio de la petición web y cerrar al final | Abrir justo antes de la primera escritura |
Un DELETE de nueve millones de filas por lotes:
-- Ejecutar repetidamente hasta que devuelva DELETE 0
DELETE FROM prestamos
WHERE prestamo_id IN (
SELECT prestamo_id FROM prestamos
WHERE fecha_devolucion < DATE '2016-01-01'
LIMIT 10000
);Cada ejecución es su propia transacción (autoconfirmación), corta, interrumpible y que no hincha el registro con nueve millones de anotaciones de golpe.
- Nunca dejes una transacción abierta esperando a un humano
Es el error clásico del sistema de mostrador:
BEGIN; SELECT ... FROM ejemplares WHERE ejemplar_id = 3081 FOR UPDATE; -- ...se muestra un diálogo al operario: "¿Confirmar préstamo? [Sí] [No]" -- ...el operario se va a comer COMMIT;
Ese FOR UPDATE mantiene bloqueada la fila del ejemplar durante cuarenta minutos, y cualquier otro mostrador que intente tocarlo se queda esperando. El patrón correcto es leer sin transacción, mostrar el diálogo, y abrir la transacción después de que el humano decida —comprobando entonces que nada haya cambiado—. Es exactamente el bloqueo optimista, que se ve en 06-02.
Como red de seguridad, PostgreSQL permite cortar a los descuidados:
Cualquier sesión que se quede más de 30 segundos con una transacción abierta sin hacer nada será desconectada. En producción es muy recomendable poner un valor razonable.
- No metas llamadas a servicios externos dentro de una transacción
BEGIN; INSERT INTO pagos ...; -- llamada HTTP a la pasarela de pago (puede tardar 8 segundos o no responder nunca) UPDATE multas SET estado = 'pagada' ...; COMMIT;
Dos problemas de naturaleza distinta:
- De rendimiento: la transacción dura lo que dure la red.
- De corrección, y este es el grave: la llamada externa no se deshace con
ROLLBACK. Si elCOMMITfalla después de haber cobrado, has cobrado y no consta. La transacción de base de datos no puede deshacer el mundo exterior.
El patrón correcto separa las dos cosas: una transacción registra la intención (pagos con estado iniciado), se hace la llamada externa fuera de toda transacción, y una segunda transacción registra el resultado. Con una referencia idempotente para poder reintentar sin cobrar dos veces —para eso está la columna pagos.referencia.
- Que la aplicación sepa reintentar
Una transacción puede fallar por causas transitorias: un interbloqueo, un fallo de serialización, una desconexión momentánea. Estos errores no significan que el código esté mal; significan que hay que volver a intentarlo. Una aplicación seria envuelve sus transacciones en un reintento con espera creciente y un número máximo de intentos. La mecánica concreta se ve en 06-02, donde aparecen los errores que se deben reintentar y los que no.
- Fuera de PostgreSQL: SQLite y MongoDB
SQLite
SQLite es plenamente ACID, lo cual sorprende a quien lo toma por "una base de datos de juguete". No lo es: es la base de datos más desplegada del mundo, y es transaccional de verdad.
Sus diferencias vienen de su naturaleza embebida (01-04):
| Aspecto | SQLite |
|---|---|
| Sintaxis | BEGIN / COMMIT / ROLLBACK y SAVEPOINT, igual |
| Concurrencia de escritura | Una sola transacción de escritura a la vez en todo el fichero |
| Modo por omisión (rollback journal) | Un escritor excluye a todos los lectores durante la escritura |
Modo WAL (PRAGMA journal_mode=WAL) |
Los lectores no se bloquean con el escritor; sigue habiendo un solo escritor |
| Durabilidad | PRAGMA synchronous (FULL, NORMAL, OFF), análogo a synchronous_commit |
Activar el modo WAL, que es lo primero que se hace en cualquier uso serio de SQLite:
El concepto es el mismo que en PostgreSQL —escribir primero el registro—, aplicado a un fichero local. La diferencia decisiva sigue siendo la granularidad del bloqueo: PostgreSQL bloquea filas; SQLite bloquea el fichero entero para escribir. Para BiblioRed, con cuatro mostradores escribiendo a la vez, SQLite sería una mala elección; para la aplicación de inventario que un bibliotecario lleva en una tableta y sincroniza al final del día, sería perfecta.
MongoDB
Retomando lo que vimos en 03-04:
| Aspecto | MongoDB |
|---|---|
| Atomicidad por omisión | A nivel de un solo documento, siempre, sin declarar nada |
| Transacciones multidocumento | Disponibles desde 2018 (v4.0 en conjuntos de réplicas; v4.2 en clústeres fragmentados) |
| Durabilidad | Registro propio (journal) y writeConcern ({w: "majority", j: true}) |
La atomicidad a nivel de documento explica por qué el modelado documental de 03-03 empuja a agrupar en un documento lo que debe cambiar junto. Si el préstamo y el estado del ejemplar viven en el mismo documento, la operación es atómica sin transacción alguna.
Con documentos separados sí hace falta una transacción explícita:
const session = db.getMongo().startSession();
session.startTransaction({ writeConcern: { w: "majority" } });
try {
session.getDatabase("biblioredes").prestamos.insertOne(
{ socio_id: 14, ejemplar_id: 3081, fecha_prestamo: new Date() }, { session });
session.getDatabase("biblioredes").ejemplares.updateOne(
{ _id: 3081 }, { $set: { estado: "prestado" } }, { session });
session.commitTransaction();
} catch (e) {
session.abortTransaction();
}Funciona, y es correcto. Pero en MongoDB una transacción multidocumento tiene un coste notablemente mayor que en PostgreSQL, y la comunidad la trata como la excepción, no como la herramienta habitual. El criterio de 03-04 sigue siendo válido: si tu dominio necesita transacciones multidocumento a todas horas, es una señal fuerte de que el modelo relacional encaja mejor con tu problema.
Errores Comunes y Consejos
Creer que el ROLLBACK deshace lo confirmado. No existe. Una vez respondido el COMMIT, el único camino de vuelta es una operación compensatoria o una restauración desde copia (06-04). Escribe el código sabiendo que COMMIT es un punto de no retorno.
Confiar en el COMMIT sin comprobar la respuesta. Como vimos en el apartado 13, un COMMIT sobre una transacción abortada responde ROLLBACK sin lanzar excepción en algunos clientes. Comprueba siempre el resultado; no supongas.
Dejar la transacción abierta a la espera de un humano o de una red. Es el origen del 80 % de los problemas de bloqueo en producción. Configura idle_in_transaction_session_timeout y no lo dejes al criterio de nadie.
Meter todo el proceso nocturno en una sola transacción. Nueve horas de proceso en una transacción abierta impiden limpiar versiones muertas, hinchan la base y, si falla en la hora octava, se pierde todo. Divide por lotes con confirmación intermedia.
Olvidar commit() en el controlador. Con psycopg y con muchos ORM, si no confirmas, el trabajo se descarta al cerrar la conexión. Sin error, sin aviso, sin fila.
Usar synchronous_commit = off en tablas que representan dinero o inventario. La ganancia de rendimiento es real y la pérdida potencial también. Decídelo tabla por tabla, no globalmente, y déjalo escrito.
Tocar fsync = off alguna vez en producción. No hay ningún caso. Ninguno.
No poner un SAVEPOINT donde hay un error esperable. Si tu lógica tiene un "esto puede fallar y no pasa nada", en PostgreSQL necesita un punto de guardado. Sin él, el fallo se lleva por delante toda la transacción.
Consejo final: nombra las transacciones en el código. Un método registrarPrestamo() que abre y cierra la transacción entera, con el BEGIN y el COMMIT visibles en el mismo bloque de código, se lee y se audita. Un BEGIN en un sitio y un COMMIT tres capas más abajo es una fuente inagotable de transacciones olvidadas.
Ejercicios
Ejercicio 1: Escribir la transacción de la devolución con multa
En BiblioRed, devolver un ejemplar implica: (a) poner fecha_devolucion en prestamos; (b) poner ejemplares.estado = 'disponible'; (c) si hay retraso, emitir una multa de 0,20 € por día en multas. La emisión de la multa es opcional: si falla, la devolución debe registrarse igualmente.
Escribe la transacción completa para el préstamo 88214 del socio 14 sobre el ejemplar 3081, cuya fecha_devolucion_prevista era 2026-07-15 y que se devuelve el 2026-08-02. Usa un punto de guardado donde corresponda y calcula el importe con SQL, no a mano.
Ejercicio 2: Predecir el estado final
Dada la siguiente secuencia, indica qué filas de socios existen al final y por qué. Supón que la sucursal 77 no existe y que las demás sí.
BEGIN;
INSERT INTO socios (nombre, apellidos, email, fecha_alta, sucursal_id, activo)
VALUES ('A', 'Uno', '[email protected]', CURRENT_DATE, 1, true);
SAVEPOINT s1;
INSERT INTO socios (nombre, apellidos, email, fecha_alta, sucursal_id, activo)
VALUES ('B', 'Dos', '[email protected]', CURRENT_DATE, 2, true);
SAVEPOINT s2;
INSERT INTO socios (nombre, apellidos, email, fecha_alta, sucursal_id, activo)
VALUES ('C', 'Tres', '[email protected]', CURRENT_DATE, 77, true);
ROLLBACK TO SAVEPOINT s2;
INSERT INTO socios (nombre, apellidos, email, fecha_alta, sucursal_id, activo)
VALUES ('D', 'Cuatro', '[email protected]', CURRENT_DATE, 4, true);
ROLLBACK TO SAVEPOINT s1;
COMMIT;Ejercicio 3: Diagnosticar una decisión de durabilidad
La concejalía de Vallmar quiere reducir el tiempo de respuesta del mostrador. Un técnico propone poner synchronous_commit = off en el fichero de configuración del servidor, para toda la base de datos. Argumenta con tres puntos concretos por qué esa decisión, tal como está formulada, es inaceptable, y propón una alternativa que conserve parte del beneficio.
Soluciones
Solución 1
BEGIN;
UPDATE prestamos
SET fecha_devolucion = DATE '2026-08-02'
WHERE prestamo_id = 88214;
UPDATE ejemplares
SET estado = 'disponible'
WHERE ejemplar_id = 3081;
SAVEPOINT antes_multa;
INSERT INTO multas (socio_id, prestamo_id, motivo, importe, fecha_emision, estado)
SELECT p.socio_id,
p.prestamo_id,
'retraso',
(p.fecha_devolucion - p.fecha_devolucion_prevista) * 0.20,
p.fecha_devolucion,
'pendiente'
FROM prestamos p
WHERE p.prestamo_id = 88214
AND p.fecha_devolucion > p.fecha_devolucion_prevista;
RELEASE SAVEPOINT antes_multa;
COMMIT;Puntos clave de la solución:
- El
INSERT ... SELECTcon la condiciónAND p.fecha_devolucion > p.fecha_devolucion_previstahace que la multa se emita solo si hay retraso, sin necesidad de lógica en la aplicación. Si no hay retraso, la respuesta esINSERT 0 0y no pasa nada. - El importe se calcula con la resta de fechas (18 días × 0,20 € = 3,60 €), leyendo del propio préstamo ya actualizado dentro de la misma transacción. Es correcto porque la transacción ve sus propios cambios.
- El punto de guardado permite que, si el
INSERTfalla —por ejemplo, porque unCHECKdemultasrechaza un importe superior al máximo de la ordenanza—, la aplicación ejecuteROLLBACK TO SAVEPOINT antes_multay confirme igualmente la devolución. - El orden importa: primero
prestamos, luegoejemplares, luegomultas. Mantener siempre el mismo orden de acceso a las tablas previene interbloqueos (06-02).
Solución 2
Al final no existe ninguna de las cuatro filas. Recorrido paso a paso:
| Paso | Efecto |
|---|---|
INSERT A |
A insertada |
SAVEPOINT s1 |
Marca con A ya insertada |
INSERT B |
B insertada |
SAVEPOINT s2 |
Marca con A y B insertadas |
INSERT C |
Falla (sucursal 77 inexistente). La transacción queda abortada |
ROLLBACK TO s2 |
Revive la transacción y la devuelve al estado de s2: A y B existen |
INSERT D |
D insertada. A, B y D existen |
ROLLBACK TO s1 |
Vuelve al estado de s1: solo A existe. B y D desaparecen |
COMMIT |
Confirma... el estado de s1, es decir, solo A |
La trampa del ejercicio es doble. Primera: ROLLBACK TO s2 no aborta la transacción, la rescata —sin él, todo lo posterior habría fallado con current transaction is aborted—. Segunda: ROLLBACK TO s1 descarta B y D, que muchos dan por confirmadas porque "ya habían pasado". Un punto de guardado deshace todo lo posterior a la marca, incluidas las operaciones que tuvieron éxito.
Solución 3
Punto 1 — El alcance es global cuando el problema no lo es. Ponerlo en el fichero de configuración lo aplica a todas las transacciones, incluidas las de pagos y multas. La biblioteca aceptaría perder cobros confirmados a cambio de que el mostrador vaya más rápido. No es un intercambio que un servicio público pueda hacer, y desde luego no lo puede decidir un técnico en solitario.
Punto 2 — No se ha medido dónde está el problema. No hay ningún dato que diga que el tiempo de respuesta del mostrador se va en el fsync(). Es igual de probable —más, de hecho— que se vaya en una consulta sin índice (06-03), en el tiempo de red o en el propio interfaz. Cambiar un parámetro de durabilidad antes de haber medido es actuar sobre una hipótesis no verificada, y encima con la garantía de seguridad como moneda.
Punto 3 — El riesgo no está acotado ni comunicado. "Se pueden perder las últimas transacciones" es una afirmación que la concejalía debe conocer y aceptar por escrito, porque afecta a datos de ciudadanos y a cobros. Una decisión de este tipo no es técnica: es de riesgo operativo, y se documenta.
Alternativa razonable. Dejar synchronous_commit = on como configuración global y desactivarlo por transacción, solo en las operaciones donde perder unos segundos es tolerable:
BEGIN;
SET LOCAL synchronous_commit = off;
INSERT INTO consultas_sala (material_id, sucursal_id, momento)
VALUES (907, 1, now());
COMMIT;Con SET LOCAL el efecto muere al terminar la transacción, así que no puede escaparse a otra operación por accidente. Y antes de eso: medir con EXPLAIN ANALYZE (06-03) dónde se va realmente el tiempo del mostrador, que casi nunca está donde se cree.
Conclusión
Esta lección ha cambiado el objeto de estudio. Hasta el módulo 5 mirábamos la estructura: qué tablas, qué columnas, qué restricciones. A partir de aquí miramos el comportamiento: qué ocurre cuando el sistema está en marcha y las cosas salen mal.
La transacción es la unidad con la que se razona sobre ese comportamiento. Hemos visto que registrar un préstamo en BiblioRed son tres instrucciones pero un solo hecho, y que quien decide dónde empieza y acaba una unidad de trabajo no es el gestor, sino quien escribe el código. Hemos visto el vocabulario completo —BEGIN, COMMIT, ROLLBACK, SAVEPOINT, ROLLBACK TO, RELEASE—, el modo de autoconfirmación que envuelve cada instrucción suelta en su propia transacción, y el ciclo de cinco estados por el que pasa toda transacción, con ese instante crítico de "parcialmente confirmada" en el que el gestor todavía no ha prometido nada.
De las cuatro propiedades ACID hemos desarrollado tres. La atomicidad, que borra los estados intermedios y hace que un ROLLBACK en PostgreSQL sea más barato que un COMMIT. La consistencia, que es la letra a medias —el gestor protege las reglas que le has declarado, y solo esas, lo que convierte cada CHECK y cada clave ajena del módulo 4 en parte de la garantía transaccional—. Y la durabilidad, que hemos abierto en canal: el registro de escritura anticipada que se sincroniza antes que los datos porque escribir secuencialmente es barato y escribir disperso no lo es; el punto de control que acota cuánto registro hay que releer; la recuperación en dos fases —rehacer todo desde el último punto de control, descartar después lo no confirmado— que devuelve la base exactamente al último COMMIT respondido; y el precio de todo ello, ese fsync() que pone un techo duro al número de transacciones por segundo y que synchronous_commit permite relajar a cambio de aceptar, por escrito, qué se está dispuesto a perder.
También hemos aprendido a convivir con el carácter estricto de PostgreSQL: una instrucción fallida aborta la transacción entera, y el punto de guardado es la única forma de sobrevivir a un error esperable. A cambio, PostgreSQL regala algo que otros gestores no tienen: DDL transaccional, y con él migraciones de esquema que o se aplican enteras o no dejan rastro.
Queda una letra sin desarrollar, y es la más difícil de las cuatro. El aislamiento lo hemos enunciado —cada transacción se comporta como si estuviera sola— y hemos visto asomarse su efecto en la demostración de dos terminales del apartado 3, donde la sesión B no veía nada de lo que la sesión A estaba haciendo. Pero no hemos dicho qué ocurre cuando las dos sesiones tocan la misma fila, ni qué ve exactamente cada una, ni qué pasa si las dos deciden a la vez que el ejemplar EJ-3081 está disponible y las dos lo prestan.
Eso es la lección 06-02, Concurrencia y Niveles de Aislamiento: los cuatro fenómenos clásicos —actualización perdida, lectura sucia, lectura no repetible, lectura fantasma— provocados uno a uno con dos terminales psql sobre los datos de BiblioRed; el sesgo de escritura, que sorprende incluso en niveles altos de aislamiento; los cuatro niveles del estándar SQL con la tabla de qué permite cada uno y qué hace realmente PostgreSQL; los bloqueos compartidos y exclusivos con SELECT ... FOR UPDATE; el control multiversión que hace que en PostgreSQL los lectores nunca bloqueen a los escritores, y el VACUUM que paga esa factura; los interbloqueos, con dos sesiones que se esperan mutuamente para siempre hasta que el gestor mata a una; y la solución final y completa al problema que ya está esperándonos en la agenda del club de lectura de la sucursal Norte: dos socios inscribiéndose en el mismo segundo a la última plaza libre.
Fundamentos de Bases de Datos
Módulo 1: Introducción a las Bases de Datos
- Conceptos Básicos de Bases de Datos
- Tipos de Bases de Datos
- Historia y Evolución de las Bases de Datos
- Sistemas Gestores de Bases de Datos y Arquitectura
Módulo 2: Bases de Datos Relacionales
- Modelo Relacional
- Lenguaje SQL
- Operaciones Básicas en SQL
- Consultas Multitabla: JOIN y Subconsultas
- Agregación y Agrupación de Datos
- Integridad Referencial
Módulo 3: Bases de Datos No Relacionales
- Introducción a NoSQL
- Tipos de Bases de Datos NoSQL
- Modelado de Datos en NoSQL
- Comparación entre Bases de Datos Relacionales y No Relacionales
Módulo 4: Diseño de Esquemas
- Principios de Diseño de Esquemas
- Diagramas Entidad-Relación (ER)
- Transformación de Diagramas ER a Esquemas Relacionales
- Tipos de Datos y Restricciones
Módulo 5: Normalización
Módulo 6: Transacciones, Rendimiento y Seguridad
- Transacciones y Propiedades ACID
- Concurrencia y Niveles de Aislamiento
- Índices y Optimización de Consultas
- Seguridad, Permisos y Copias de Seguridad
Módulo 7: Ejercicios Prácticos
- Ejercicios de SQL
- Ejercicios de Diseño de Esquemas
- Ejercicios de Normalización
- Ejercicios de Consultas Avanzadas y Transacciones
Módulo 8: Casos de Estudio
- Caso de Estudio: Base de Datos Relacional
- Caso de Estudio: Base de Datos No Relacional
- Caso de Estudio: Persistencia Políglota
