El módulo 8 terminó confesando una mentira: durante ochenta y tantas lecciones hemos supuesto que somos el único usuario de la base de datos. Este módulo desmonta esa suposición, y empieza por la pieza que la hace manejable: la transacción, una unidad de trabajo que el motor trata como indivisible. O pasa entera, o no pasa nada. Aquí verás por qué confirmar un pedido en TiendaVerde —cuatro operaciones sobre tres tablas— es un desastre esperando a ocurrir si no es atómico; entenderás el autocommit, la fuente número uno de malentendidos con las transacciones; aprenderás BEGIN, COMMIT y ROLLBACK con su ciclo de vida completo; y conocerás el formato de dos sesiones con el que se demuestra todo lo demás del módulo. Al final tendrás dos terminales psql abiertas a la vez y habrás visto, por primera vez en el curso, que dos sesiones no ven lo mismo al mismo tiempo.

Contenido

  1. Qué es una transacción
  2. El caso de TiendaVerde: confirmar un pedido son cuatro operaciones
  3. Qué queda si falla el tercer paso
  4. Autocommit: cada sentencia ya es una transacción
  5. BEGIN, COMMIT y ROLLBACK: el ciclo de vida
  6. Cómo saber si estás dentro de una transacción
  7. El formato de dos sesiones
  8. Cerrar la sesión sin confirmar, y qué pasa si el servidor cae
  9. El estado abortado
  10. Transacciones de solo lectura
  11. Duración: transacciones cortas y el problema de idle in transaction
  12. Errores Comunes y Consejos
  13. Ejercicios
  14. Conclusión

  1. Qué es una transacción

Una transacción es un conjunto de sentencias SQL que el motor ejecuta como una sola operación indivisible: o se aplican todas o no se aplica ninguna.

El ejemplo de manual es la transferencia bancaria. Mover 100 € de una cuenta a otra son dos operaciones:

UPDATE cuentas SET saldo = saldo - 100 WHERE id = 1;   -- restar del origen
UPDATE cuentas SET saldo = saldo + 100 WHERE id = 2;   -- sumar al destino

Si el sistema se cae entre las dos, los 100 € han dejado de existir. Y no hay ninguna consulta que pueda detectarlo después: las dos filas son individualmente válidas; solo la relación entre ambas está rota, y esa relación no vive en ninguna columna.

La transacción resuelve exactamente eso: convierte las dos sentencias en una. El sublenguaje que la controla es el TCL (Transaction Control Language), el cuarto de los que nombramos en 01-01 y el único que aún no habías usado a fondo.

  1. El caso de TiendaVerde: confirmar un pedido son cuatro operaciones

Olvidemos los bancos: en TiendaVerde el caso es más rico y lo tienes en la base de datos. Un cliente confirma su carrito y el sistema tiene que hacer cuatro cosas sobre tres tablas:

flowchart LR
    A["1 · INSERT<br/>cabecera en <b>pedidos</b><br/>estado 'pendiente'"] --> B["2 · INSERT<br/>una fila por artículo<br/>en <b>lineas_pedido</b>"]
    B --> C["3 · UPDATE<br/>descontar el stock<br/>en <b>productos</b>"]
    C --> D["4 · UPDATE<br/>estado = 'pagado'<br/>en <b>pedidos</b>"]

Pau Llorens Vidal (cliente 6) compra por web dos aceites de oliva, un té matcha y —sin saber que está agotado— un pack de velas de cera de soja. Los cuatro pasos, tal y como los ejecuta la aplicación:

-- Paso 1: la cabecera  →  RETURNING devuelve id = 21
INSERT INTO pedidos (cliente_id, empleado_id, fecha_pedido, estado, metodo_pago, gastos_envio)
VALUES (6, NULL, DATE '2026-03-05', 'pendiente', 'tarjeta', 4.95)
RETURNING id;

-- Paso 2: las líneas
INSERT INTO lineas_pedido (pedido_id, producto_id, cantidad, precio_unitario, descuento) VALUES
(21,  1, 2, 12.50, 0.00),
(21, 15, 1, 22.00, 0.00),
(21, 13, 1, 13.75, 0.00)
RETURNING id, producto_id, cantidad, precio_unitario;
id producto_id cantidad precio_unitario
48 1 2 12.50
49 15 1 22.00
50 13 1 13.75

Sesenta euros con setenta y cinco de producto más 4,95 € de portes: 65,70 €. Ahora el paso que falla:

-- Paso 3: descontar stock, línea a línea (es lo que hace el ORM al recorrer el carrito)
UPDATE productos SET stock = stock - 2 WHERE id =  1;
UPDATE productos SET stock = stock - 1 WHERE id = 15;
UPDATE productos SET stock = stock - 1 WHERE id = 13;
UPDATE 1
UPDATE 1
ERROR:  new row for relation "productos" violates check constraint "productos_stock_check"
DETAIL:  Failing row contains (13, Velas de cera de soja (pack 2), 3, 5, 13.75, 6.90, -1, t, 2025-03-01).

El producto 13 tiene stock = 0 y el CHECK (stock >= 0) de 05-01 impide bajarlo a −1. El paso 3 ha fallado a la mitad. Y el paso 4 —marcar el pedido como pagado— no llega a ejecutarse nunca.

  1. Qué queda si falla el tercer paso

Esta es la pregunta que hay que mirar de frente. Sin transacción, cada sentencia se confirmó sola y esto es lo que hay ahora en la base de datos:

Tabla Estado real tras el fallo ¿Es correcto?
pedidos El pedido 21 existe, en estado pendiente A medias: existe pero nadie lo va a cobrar
lineas_pedido Tres líneas (48, 49, 50) por valor de 60,75 € Sí, pero de un pedido que no se completó
productos.stock (id 1) 120 → 118 No: se han reservado 2 unidades de un pedido inexistente
productos.stock (id 15) 40 → 39 No: lo mismo con el matcha
productos.stock (id 13) 0 → 0 Sí… pero el cliente cree que lo ha comprado
SELECT id, nombre, stock FROM productos WHERE id IN (1, 13, 15) ORDER BY id;
id nombre stock
1 Aceite de oliva virgen extra 500 ml 118
13 Velas de cera de soja (pack 2) 0
15 Té verde matcha ceremonial 30 g 39

Tres unidades de inventario han desaparecido del sistema sin que nadie las haya comprado. Multiplícalo por cien pedidos al día y en un mes el inventario del ERP no se parece al del almacén. Y no hay ningún error registrado en ninguna parte: la aplicación devolvió un mensaje al cliente, el cliente cerró la pestaña, y las filas se quedaron ahí. Lo mismo, dentro de una transacción:

BEGIN;
  -- los cuatro pasos, exactamente iguales
ROLLBACK;   -- el motor deshace TODO: la cabecera, las tres líneas y los dos descuentos

SELECT (SELECT COUNT(*) FROM pedidos)              AS pedidos,
       (SELECT COUNT(*) FROM lineas_pedido)        AS lineas,
       (SELECT stock FROM productos WHERE id = 1)  AS stock_aceite,
       (SELECT stock FROM productos WHERE id = 15) AS stock_matcha;
pedidos lineas stock_aceite stock_matcha
20 47 120 40

Como si nunca hubiera ocurrido. 20 pedidos, 47 líneas, los stocks intactos. Esa es toda la idea.

  1. Autocommit: cada sentencia ya es una transacción

Aquí está el malentendido número uno, y conviene decirlo sin rodeos:

En SQL no existe "estar fuera de una transacción". Toda sentencia se ejecuta dentro de una. Si no abres una explícitamente, el motor abre una implícita que dura exactamente lo que dura esa sentencia y se confirma sola al terminar. Eso es el autocommit.

Dos consecuencias que hay que interiorizar:

  1. Una sola sentencia siempre es atómica. El UPDATE productos SET precio = precio * 1.05; de 05-03 afecta a 20 filas: o cambian las 20, o no cambia ninguna. Por eso INSERT ... ON CONFLICT (05-05) es seguro y SELECT + INSERT no lo es: una sentencia es atómica y dos no.
  2. El ROLLBACK no existe para lo ya confirmado. Cuando ves UPDATE 20 en autocommit, ya está en disco. No hay marcha atrás.

Y el problema es que el autocommit no se comporta igual en todas partes:

Entorno Estado por omisión Cómo se abre una transacción explícita Cómo se desactiva el autocommit
psql (PostgreSQL) Autocommit activado BEGIN; \set AUTOCOMMIT off — a partir de ahí, psql abre un BEGIN implícito antes de la primera sentencia y hay que hacer COMMIT a mano
MySQL / MariaDB (cliente mysql) Autocommit activado START TRANSACTION; o BEGIN; SET autocommit = 0;
SQL Server (SSMS) Autocommit activado BEGIN TRANSACTION; SET IMPLICIT_TRANSACTIONS ON;
Oracle (SQL*Plus) Desactivado: toda sentencia abre transacción y hay que hacer COMMIT Implícita, con la primera sentencia DML Es el comportamiento por omisión
SQLite (CLI) Autocommit activado BEGIN; No hay opción: se abre explícitamente
psycopg 3 (Python) Autocommit desactivado: abre transacción sola Automática con la primera sentencia conn.autocommit = True para lo contrario
JDBC (Java) Autocommit activado conn.setAutoCommit(false) y después conn.commit()
SQLAlchemy Autocommit desactivado: la Session mantiene transacción abierta Automática session.commit() / session.rollback()

Nota de dialecto: Oracle es el caso que más sorprende: allí un UPDATE sin COMMIT posterior no ha ocurrido para nadie más que para ti, y si cierras la sesión se pierde. Y en el extremo opuesto, psycopg y SQLAlchemy hacen lo contrario de lo que la mayoría espera: tu aplicación Python ya está dentro de una transacción abierta desde la primera consulta, aunque tú no hayas escrito BEGIN en ninguna parte. Volveremos sobre esto en 09-03, porque explica la mitad de los bloqueos misteriosos en producción.

  1. BEGIN, COMMIT y ROLLBACK: el ciclo de vida

Tres palabras y ya lo sabes todo:

Sentencia Qué hace Sinónimos aceptados en PostgreSQL
BEGIN; Abre una transacción explícita START TRANSACTION;, BEGIN WORK;, BEGIN TRANSACTION;
COMMIT; Confirma: todo lo hecho se vuelve permanente y visible para los demás END;, COMMIT WORK;
ROLLBACK; Deshace: la base vuelve al estado que tenía antes del BEGIN ABORT;, ROLLBACK WORK;

START TRANSACTION es la forma del estándar SQL y funciona en PostgreSQL, MySQL y SQL Server; BEGIN es la más corta y la que usa todo el mundo en PostgreSQL. Ojo: en MySQL, BEGIN es además el arranque de un bloque de código en procedimientos, así que allí conviene escribir START TRANSACTION para que no haya ambigüedad.

El ciclo de vida completo, con el estado abortado que veremos en el apartado 9:

stateDiagram-v2
    [*] --> Autocommit: sesión conectada
    Autocommit --> Activa: BEGIN
    Activa --> Activa: INSERT / UPDATE / DELETE / SELECT
    Activa --> Abortada: ERROR en una sentencia
    Abortada --> Abortada: cualquier sentencia<br/>→ 25P02
    Activa --> Confirmada: COMMIT
    Activa --> Deshecha: ROLLBACK
    Abortada --> Deshecha: ROLLBACK
    Abortada --> Deshecha: COMMIT<br/>(¡se comporta como ROLLBACK!)
    Confirmada --> Autocommit
    Deshecha --> Autocommit

Fíjate en la transición más peligrosa del diagrama: un COMMIT sobre una transacción abortada no confirma nada, hace un ROLLBACK. PostgreSQL responde ROLLBACK en lugar de COMMIT, y si tu script no mira esa respuesta, creerá que guardó los datos.

  1. Cómo saber si estás dentro de una transacción

Es la pregunta práctica más frecuente, y psql te la responde en el propio prompt:

Prompt Significado
tiendaverde=> Fuera de transacción (autocommit)
tiendaverde=*> Dentro de una transacción abierta
tiendaverde=!> Dentro de una transacción abortada
tiendaverde-> Sentencia incompleta: falta el ;

El carácter final es > para un usuario normal y # para un superusuario, así que un administrador dentro de una transacción ve tiendaverde=*#. Lo que importa siempre es el asterisco: aparece justo después del BEGIN y desaparece con el COMMIT o el ROLLBACK. Desde SQL:

SELECT pg_current_xact_id_if_assigned() AS xid,
       (pg_current_xact_id_if_assigned() IS NOT NULL) AS ha_escrito;
xid ha_escrito
(null) false

Devuelve NULL mientras la transacción no haya escrito nada, porque PostgreSQL no gasta un identificador de transacción en quien solo lee; en cuanto haces un INSERT o un UPDATE, aparece un número. Su hermana pg_current_xact_id() (antes txid_current(), que sigue funcionando) fuerza la asignación, así que devuelve siempre un número — y por eso no sirve para diagnosticar: al preguntar, cambia la respuesta.

Y dos comodines de psql: \echo :ROW_COUNT imprime las filas afectadas por la última sentencia —el UPDATE N de 05-03, pero utilizable dentro de un script— y \set ON_ERROR_STOP on hace que psql aborte el fichero al primer error en lugar de seguir lanzando sentencias contra una transacción ya muerta. Es obligatorio en cualquier script de migración.

  1. El formato de dos sesiones

Todo lo que queda del módulo trata de lo que ocurre cuando dos personas trabajan a la vez, y eso no se ve en una salida de psql normal. A partir de aquí lo mostraremos siempre así:

Cómo reproducirlo tú. Abre dos terminales y en cada una lanza psql -h localhost -U curso_sql -d tiendaverde. Llamaremos Sesión A a la primera y Sesión B a la segunda. Ejecuta las sentencias en el orden de los instantes t1, t2, t3… alternando de terminal. Todos los ejemplos del módulo están pensados para hacerse así, y no entenderás ninguno de verdad hasta que los teclees.

La primera demostración: qué ve cada sesión antes y después del COMMIT.

Instante Sesión A Sesión B
t1 BEGIN;
t2 SELECT stock FROM productos WHERE id = 15;40
t3 UPDATE productos SET stock = 39 WHERE id = 15;UPDATE 1
t4 SELECT stock FROM productos WHERE id = 15;39
t5 SELECT stock FROM productos WHERE id = 15;40
t6 COMMIT;
t7 SELECT stock FROM productos WHERE id = 15;39

Léelo despacio, porque en esas siete líneas está el módulo entero:

  • En t4, A ve 39. Es su propio cambio: toda transacción ve siempre lo que ella misma ha hecho.
  • En t5, B ve 40. El cambio de A existe, está escrito, pero no está confirmado, y para B es como si no existiera. B no se bloquea, no espera, no recibe ningún aviso: simplemente lee el valor bueno anterior.
  • En t7, después del COMMIT, B ve 39. El cambio se ha hecho público de golpe, y para B ocurrió entero en el instante del COMMIT, no repartido entre t3 y t6.

Eso que acabas de ver es el aislamiento, la tercera letra de ACID, y lo estudiaremos a fondo en 09-02 y 09-04. Y el mecanismo que lo hace posible sin que B tenga que esperar —dos valores de la misma fila coexistiendo— es MVCC, la respuesta a la pregunta que dejó abierta 08-05 sobre el bloat.

Cuando el ejemplo requiera ver el SQL completo en lugar de resumido, usaremos dos bloques etiquetados con el instante:

-- Sesión A
BEGIN;                                                    -- t1
UPDATE productos SET stock = stock - 1 WHERE id = 15;     -- t3
COMMIT;                                                   -- t6
-- Sesión B
SELECT stock FROM productos WHERE id = 15;                -- t5  → 40
SELECT stock FROM productos WHERE id = 15;                -- t7  → 39

Y para los interbloqueos de 09-05, diagramas mermaid de secuencia. Sea cual sea el formato, la regla no cambia: siempre se indica qué ve cada sesión en cada instante y cuál se queda esperando.

  1. Cerrar la sesión sin confirmar, y qué pasa si el servidor cae

Los dos finales imprevistos tienen la misma respuesta, y es tranquilizadora:

Situación Qué ocurre
Escribes \q o cierras la terminal con una transacción abierta ROLLBACK implícito. PostgreSQL deshace todo lo no confirmado
Se corta la red entre el cliente y el servidor Igual: al detectar la desconexión, el servidor deshace la transacción
El proceso del servidor muere, o se va la luz Al arrancar, PostgreSQL hace recuperación: reaplica desde el WAL lo confirmado y descarta lo que no. Las transacciones a medias desaparecen
Haces COMMIT y un microsegundo después se va la luz Los datos están. Eso es la durabilidad, y el mecanismo que la garantiza (el WAL) es el apartado central de 09-02

La regla mental: lo confirmado sobrevive a todo; lo no confirmado no sobrevive a nada. No hay estado intermedio, ni forma de que una transacción quede "medio aplicada" tras una caída.

  1. El estado abortado

Este mensaje lo vas a ver, seguro, y probablemente hoy mismo:

ERROR:  current transaction is aborted, commands ignored until end of transaction block

Ocurre así:

tiendaverde=> BEGIN;
BEGIN
tiendaverde=*> UPDATE productos SET stock = stock - 1 WHERE id = 1;
UPDATE 1
tiendaverde=*> UPDATE productos SET stock = stock - 1 WHERE id = 13;
ERROR:  new row for relation "productos" violates check constraint "productos_stock_check"
tiendaverde=!> SELECT COUNT(*) FROM productos;
ERROR:  current transaction is aborted, commands ignored until end of transaction block
tiendaverde=!> ROLLBACK;
ROLLBACK
tiendaverde=>

Fíjate en el prompt: pasó de =*> a =!> en cuanto hubo un error. A partir de ahí PostgreSQL rechaza cualquier sentencia, incluso un SELECT COUNT(*) inofensivo, con el código 25P02.

Por qué es así, y por qué es lo correcto. La transacción prometió atomicidad: o todo o nada. En cuanto una sentencia falla, "todo" ya es imposible, así que la única promesa que el motor puede seguir cumpliendo es "nada". Dejarte continuar sería permitir que confirmaras un resultado parcial creyendo que está completo — exactamente el desastre del apartado 3, pero con el sello de calidad de una transacción encima.

Cómo salir: hay dos puertas y las dos terminan la transacción. ROLLBACK; deshace todo, y es la salida honesta. COMMIT; responde ROLLBACK y deshace todo igualmente. Existe una tercera que no cierra la transacción y salva el trabajo ya hecho: volver a un SAVEPOINT anterior al error, una de las razones de ser de los savepoints (09-03).

Nota de dialecto — y es una divergencia enorme. MySQL/InnoDB no aborta la transacción entera. Si una sentencia falla, se deshace solo esa sentencia y la transacción sigue viva y aceptando órdenes; puedes hacer COMMIT y confirmarás todo lo anterior al error. SQL Server queda en medio: depende de la gravedad del error y de SET XACT_ABORT ON (que lo hace comportarse como PostgreSQL, y es lo recomendado). Oracle también deshace solo la sentencia fallida. Consecuencia práctica: un script probado en MySQL que "funciona" puede estar confirmando resultados parciales; el mismo script en PostgreSQL fallará ruidosamente. La versión ruidosa es la buena.

  1. Transacciones de solo lectura

Se declaran así:

BEGIN TRANSACTION READ ONLY;
SELECT COUNT(*) FROM pedidos;
COMMIT;

Y si intentas escribir dentro: ERROR: cannot execute UPDATE in a read-only transaction. Cuatro razones para declararlas:

  1. Es una red de seguridad contra ti mismo. Un informe mensual no debería poder modificar nada; con READ ONLY, un UPDATE pegado por error es un error y no un incidente.
  2. No consume un identificador de transacción, lo que reduce la presión sobre MVCC y sobre el congelamiento de transacciones (09-02).
  3. Es obligatoria en una réplica de solo lectura. Si tu informe apunta a un secundario, mejor que la escritura falle en tu portátil.
  4. Habilita DEFERRABLE, que permite ejecutar un informe largo en modo SERIALIZABLE sin riesgo de que aborte por conflicto (09-03).

  1. Duración: transacciones cortas y el problema de idle in transaction

Esta es la única regla operativa que hay que memorizar de esta lección:

Una transacción debe abrirse lo más tarde posible y cerrarse lo antes posible. Nunca, jamás, se espera dentro de una transacción abierta: ni entrada del usuario, ni una llamada HTTP, ni una lectura de fichero, ni un sleep.

Una transacción abierta y ociosa (el estado idle in transaction) hace tres daños simultáneos:

Daño Detalle
Retiene bloqueos Las filas que tocó siguen bloqueadas para quien quiera modificarlas. Si es la del producto más vendido, has parado la tienda (09-05)
Retiene una instantánea VACUUM no puede limpiar ninguna versión de fila que esa transacción aún pudiera necesitar. Con una transacción abierta desde hace horas, las filas muertas se acumulan en toda la base: es la causa clásica del bloat de 08-05
Ocupa una conexión Y las conexiones son un recurso escaso y caro

El caso real es siempre el mismo: un formulario que abre transacción, muestra una pantalla de confirmación y espera. El usuario se va a comer. La tienda se para. Cómo se detecta:

SELECT pid, state, now() - xact_start AS duracion, left(query, 55) AS ultima_consulta
FROM   pg_stat_activity
WHERE  state = 'idle in transaction'
ORDER BY xact_start;
pid state duracion ultima_consulta
41287 idle in transaction 01:42:19 UPDATE productos SET stock = stock - 1 WHERE id = 15

Una hora y cuarenta y dos minutos con la fila del matcha bloqueada. La defensa automática es un parámetro que todo servidor de producción debería tener puestoSET idle_in_transaction_session_timeout = '5min';— fijable por sesión, por usuario, por base de datos o en postgresql.conf. Junto a statement_timeout y lock_timeout forma el kit de supervivencia que detalla 09-05.

Errores Comunes y Consejos

  • Creer que "no estoy usando transacciones". Sí las usas: cada sentencia suelta es una. La pregunta no es si hay transacción, sino dónde empieza y dónde acaba.
  • Suponer que el autocommit se comporta igual en todas partes. En Oracle está desactivado; en psycopg y SQLAlchemy tu aplicación ya está dentro de una transacción abierta sin que hayas escrito BEGIN.
  • Hacer BEGIN y olvidar cerrar. El error operativo más caro del módulo: bloqueas filas, impides el VACUUM y ocupas una conexión. Mira el asterisco del prompt antes de levantarte.
  • Ignorar el estado abortado, o no comprobar la respuesta del COMMIT. Tras un error todo falla con 25P02 hasta el ROLLBACK; y sobre una transacción abortada, COMMIT devuelve ROLLBACK y no guarda nada. Si tu script no lo mira, creerá que guardó.
  • Ejecutar un fichero .sql sin \set ON_ERROR_STOP on. psql seguirá lanzando cien sentencias contra una transacción muerta y el error real quedará sepultado entre cien mensajes de 25P02.
  • Descontar stock línea a línea sin transacción. Es el apartado 3: inventario roto en silencio y sin traza.
  • Esperar dentro de una transacción abierta. Entrada de usuario, llamada a la pasarela de pago, lectura de un fichero grande. Prepara los datos fuera, abre, escribe, cierra.
  • Consejo: adopta BEGIN … verificar … COMMIT/ROLLBACK como reflejo en cualquier UPDATE o DELETE manual, y BEGIN TRANSACTION READ ONLY para tus informes: cuesta dos palabras y hace imposible el accidente.
  • Consejo: ten siempre dos terminales psql abiertas mientras estudias este módulo. Es la única forma de ver la concurrencia.

Ejercicios

Trabaja sobre la base recién recargada (tiendaverde.sql) y con dos terminales psql abiertas.

Ejercicio 1

Reproduce el desastre del apartado 3 y después su versión correcta.

  1. Sin transacción, ejecuta los cuatro pasos de confirmación del pedido de Pau con las tres líneas (productos 1, 15 y 13). Anota qué falla.
  2. Escribe una consulta que demuestre la incoherencia: pedidos, líneas y stocks de los tres productos implicados.
  3. Recarga el script y repítelo todo dentro de BEGINROLLBACK. Comprueba con la misma consulta que no queda rastro.
  4. ¿Qué habría pasado si el paso 3 se hubiera escrito como un solo UPDATE ... FROM lineas_pedido (05-03) en lugar de tres sentencias? ¿Seguiría haciendo falta la transacción?

Ejercicio 2

Llamando INSERTAR_PEDIDO a la sentencia INSERT INTO pedidos (cliente_id, empleado_id, fecha_pedido, estado, metodo_pago, gastos_envio) VALUES (6, NULL, DATE '2026-03-05', 'pendiente', 'tarjeta', 4.95) RETURNING id;, predice qué verá cada sesión antes de ejecutar nada, y después compruébalo con dos terminales:

Instante Sesión A Sesión B ¿Qué ve B?
t1 BEGIN;
t2 INSERTAR_PEDIDO
t3 SELECT COUNT(*) FROM pedidos; ¿?
t4 ROLLBACK;
t5 SELECT COUNT(*) FROM pedidos; ¿?
t6 INSERTAR_PEDIDO ¿qué id?

La pregunta de t6 es la interesante: ¿qué identificador recibe el pedido de B, sabiendo que el de A se deshizo?

Ejercicio 3

En una sesión, provoca deliberadamente el estado abortado y sal de él de las dos formas posibles.

  1. BEGIN;, un UPDATE válido sobre productos y después un INSERT que viole una clave foránea (por ejemplo, un pedido con cliente_id = 999).
  2. Intenta ejecutar SELECT 1;. Copia el mensaje exacto.
  3. Sal con COMMIT; y anota qué responde el servidor. Comprueba después si el UPDATE válido llegó a aplicarse.
  4. Repite todo saliendo con ROLLBACK; y compara.
  5. Explica en dos frases por qué MySQL se comportaría de forma distinta y cuál de los dos comportamientos prefieres para un script de facturación.

Soluciones

Solución 1

1 y 2. El tercer UPDATE falla con productos_stock_check, porque el producto 13 está a 0. La consulta que revela el estropicio:

SELECT (SELECT COUNT(*) FROM pedidos)                     AS pedidos,
       (SELECT COUNT(*) FROM lineas_pedido)               AS lineas,
       (SELECT estado FROM pedidos WHERE id = 21)         AS estado_21,
       (SELECT stock FROM productos WHERE id = 1)         AS stock_1,
       (SELECT stock FROM productos WHERE id = 15)        AS stock_15,
       (SELECT stock FROM productos WHERE id = 13)        AS stock_13;
pedidos lineas estado_21 stock_1 stock_15 stock_13
21 50 pendiente 118 39 0

Un pedido que nadie va a cobrar, tres líneas huérfanas de propósito y tres unidades de inventario evaporadas.

3. Con BEGIN al principio y ROLLBACK al final, la misma consulta devuelve 20, 47, (null), 120, 40, 0. Estado inicial exacto.

4. Con un solo UPDATE ... FROM, el paso 3 sería atómico por sí mismo: al violar el CHECK en una de las tres filas, no se aplicaría ninguna, y los stocks quedarían en 120 y 40. Pero la transacción sigue haciendo falta, y por dos motivos: el pedido 21 y sus tres líneas ya están insertados y confirmados por los pasos 1 y 2, así que la incoherencia persiste; y el paso 4 tampoco se ejecuta. La atomicidad de una sentencia no da atomicidad al proceso: la unidad de trabajo es el pedido, no el UPDATE.

Solución 2

En t3, B ve 20: el INSERT de A no está confirmado y para B no existe. En t5, B sigue viendo 20, porque A hizo ROLLBACK y el pedido nunca existió para nadie. Y en t6, el id que recibe B es 22, no 21. Ese 22 es la parte importante. Las secuencias no se deshacen con un ROLLBACK, como ya avisaron 05-02 y 05-05: A consumió el valor 21 al insertar y ese valor se perdió al deshacer. Es deliberado — si nextval respetara las transacciones, dos sesiones tendrían que esperarse la una a la otra para obtener un identificador, y eso destruiría el rendimiento de cualquier sistema con inserciones concurrentes. El precio es que los id tienen huecos, y la consecuencia práctica (por qué una PK no debe usarse como número de factura) se desarrolla en 09-05.

Solución 3

2. El mensaje, literal: ERROR: current transaction is aborted, commands ignored until end of transaction block.

3. El servidor responde a COMMIT; con ROLLBACK, y el UPDATE válido no se ha aplicado: la transacción entera se deshizo. Esa respuesta discordante —pides COMMIT y te contestan ROLLBACK— es la señal de que estás confirmando una transacción muerta.

4. Con ROLLBACK; el resultado es idéntico, pero honesto: pediste deshacer y deshizo. La diferencia no está en los datos, sino en que en el primer caso un script que no lea la respuesta creerá que guardó.

5. En MySQL/InnoDB, el INSERT fallido se habría deshecho solo a sí mismo y la transacción habría seguido viva; el COMMIT habría confirmado el UPDATE de productos. Para un script de facturación es preferible el comportamiento de PostgreSQL: una factura a medias es peor que ninguna factura, y el error ruidoso obliga a mirar. El de MySQL es más cómodo en cargas masivas donde se toleran filas rechazadas, pero exige comprobar el resultado de cada sentencia a mano.

Conclusión

Ya tienes la unidad de trabajo que faltaba:

  • Una transacción es un conjunto de sentencias que se aplican todas o ninguna. Se controla con el TCL: BEGIN, COMMIT y ROLLBACK.
  • El caso de TiendaVerde: confirmar un pedido son cuatro operaciones sobre tres tablas, y si la tercera falla a medias quedan un pedido sin cobrar, tres líneas huérfanas y tres unidades de inventario evaporadas — sin ningún error registrado.
  • El autocommit no es la ausencia de transacciones: es una transacción por sentencia. Por eso una sentencia siempre es atómica y dos nunca lo son. Y por eso importa saber que Oracle no lo activa, y que psycopg y SQLAlchemy hacen justo lo contrario de lo que casi todo el mundo supone.
  • El ciclo de vida: activa → confirmada / deshecha, con el desvío al estado abortado en cuanto una sentencia falla. Ahí PostgreSQL rechaza todo con 25P02 y un COMMIT responde ROLLBACK; MySQL, en cambio, solo deshace la sentencia fallida. Y para saber dónde estás: el asterisco del prompt (tiendaverde=*>), la admiración si está abortada (=!>) y pg_current_xact_id_if_assigned().
  • El formato de dos sesiones que usaremos todo el módulo, y su primera lección: antes del COMMIT, B ve el valor viejo sin esperar ni enterarse; después, lo ve entero y de golpe.
  • Cerrar la sesión o caerse el servidor equivalen a un ROLLBACK; lo confirmado sobrevive siempre. Transacciones de solo lectura para los informes, y la regla de oro: cortas, y nunca esperando a nadie. Una transacción idle in transaction bloquea filas, impide el VACUUM —el bloat de 08-05— y ocupa una conexión.

Has visto qué hace una transacción. Falta qué garantiza exactamente, y esa respuesta tiene cuatro letras. En Propiedades ACID desmontaremos una a una la atomicidad (y el mecanismo que permite deshacer), la consistencia (dónde acaba la responsabilidad de la base de datos y empieza la tuya, con el stock que no puede quedar negativo como caso de estudio), el aislamiento (por qué B veía 40 mientras A veía 39) y la durabilidad (el WAL, fsync y por qué tus datos sobreviven a un corte de corriente). Y por fin llegará MVCC: cómo PostgreSQL guarda varias versiones de cada fila, cómo se ven con SELECT xmin, xmax, *, y por qué de ahí salen las filas muertas, el bloat y la necesidad de VACUUM que 08-05 dejó pendiente.

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