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
- Qué es una transacción
- El caso de TiendaVerde: confirmar un pedido son cuatro operaciones
- Qué queda si falla el tercer paso
- Autocommit: cada sentencia ya es una transacción
BEGIN,COMMITyROLLBACK: el ciclo de vida- Cómo saber si estás dentro de una transacción
- El formato de dos sesiones
- Cerrar la sesión sin confirmar, y qué pasa si el servidor cae
- El estado abortado
- Transacciones de solo lectura
- Duración: transacciones cortas y el problema de idle in transaction
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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 destinoSi 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.
- 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.
- 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 |
| 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.
- 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:
- 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 esoINSERT ... ON CONFLICT(05-05) es seguro ySELECT+INSERTno lo es: una sentencia es atómica y dos no. - El
ROLLBACKno existe para lo ya confirmado. Cuando vesUPDATE 20en 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
UPDATEsinCOMMITposterior 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 escritoBEGINen ninguna parte. Volveremos sobre esto en 09-03, porque explica la mitad de los bloqueos misteriosos en producción.
BEGIN, COMMIT y ROLLBACK: el ciclo de vida
BEGIN, COMMIT y ROLLBACK: el ciclo de vidaTres 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.
- 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.
- 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 instantest1,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 delCOMMIT, 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 B
SELECT stock FROM productos WHERE id = 15; -- t5 → 40
SELECT stock FROM productos WHERE id = 15; -- t7 → 39Y 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.
- 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.
- El estado abortado
Este mensaje lo vas a ver, seguro, y probablemente hoy mismo:
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
COMMITy confirmarás todo lo anterior al error. SQL Server queda en medio: depende de la gravedad del error y deSET 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.
- Transacciones de solo lectura
Se declaran así:
Y si intentas escribir dentro: ERROR: cannot execute UPDATE in a read-only transaction. Cuatro razones para declararlas:
- Es una red de seguridad contra ti mismo. Un informe mensual no debería poder modificar nada; con
READ ONLY, unUPDATEpegado por error es un error y no un incidente. - No consume un identificador de transacción, lo que reduce la presión sobre MVCC y sobre el congelamiento de transacciones (09-02).
- 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.
- Habilita
DEFERRABLE, que permite ejecutar un informe largo en modoSERIALIZABLEsin riesgo de que aborte por conflicto (09-03).
- 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 puesto —SET 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
BEGINy olvidar cerrar. El error operativo más caro del módulo: bloqueas filas, impides elVACUUMy 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 con25P02hasta elROLLBACK; y sobre una transacción abortada,COMMITdevuelveROLLBACKy no guarda nada. Si tu script no lo mira, creerá que guardó. - Ejecutar un fichero
.sqlsin\set ON_ERROR_STOP on.psqlseguirá lanzando cien sentencias contra una transacción muerta y el error real quedará sepultado entre cien mensajes de25P02. - 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/ROLLBACKcomo reflejo en cualquierUPDATEoDELETEmanual, yBEGIN TRANSACTION READ ONLYpara tus informes: cuesta dos palabras y hace imposible el accidente. - Consejo: ten siempre dos terminales
psqlabiertas 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.
- 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.
- Escribe una consulta que demuestre la incoherencia: pedidos, líneas y stocks de los tres productos implicados.
- Recarga el script y repítelo todo dentro de
BEGIN…ROLLBACK. Comprueba con la misma consulta que no queda rastro. - ¿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.
BEGIN;, unUPDATEválido sobreproductosy después unINSERTque viole una clave foránea (por ejemplo, un pedido concliente_id = 999).- Intenta ejecutar
SELECT 1;. Copia el mensaje exacto. - Sal con
COMMIT;y anota qué responde el servidor. Comprueba después si elUPDATEválido llegó a aplicarse. - Repite todo saliendo con
ROLLBACK;y compara. - 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 sí 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,COMMITyROLLBACK. - 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
25P02y unCOMMITrespondeROLLBACK; 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 (=!>) ypg_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ónidle in transactionbloquea filas, impide elVACUUM—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
- ¿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
