Módulo 39: Transacciones y concurrencia
Objetivos del módulo
- Comprender qué es una transacción y por qué es la unidad básica de trabajo de un SGBD
- Conocer en detalle las propiedades ACID y cómo las garantiza Oracle (undo, redo, restricciones y multiversión)
- Saber cuándo empieza y cuándo termina una transacción en Oracle:
COMMIT,ROLLBACK,COMMITimplícito por DDL o al salir yROLLBACKimplícito ante una caída - Usar
SAVEPOINTyROLLBACK TO SAVEPOINTpara deshacer parcialmente una transacción - Configurar el autocommit en SQL*Plus y SQL Developer y conocer sus peligros
- Entender la atomicidad a nivel de sentencia y la lectura consistente multiversión de Oracle
- Identificar los problemas clásicos de concurrencia: actualización perdida, lectura sucia, lectura no repetible y lectura fantasma
- Conocer los niveles de aislamiento del estándar SQL y los que ofrece Oracle (
READ COMMITTED,SERIALIZABLEyREAD ONLY) conSET TRANSACTION - Manejar los bloqueos de Oracle: bloqueos de fila (TX) y de tabla (TM),
SELECT ... FOR UPDATEconNOWAIT,WAIT nySKIP LOCKED, yLOCK TABLE - Reconocer y resolver interbloqueos (
ORA-00060) - Consultar datos del pasado con Flashback Query (
AS OF TIMESTAMP) - Analizar escenarios con dos sesiones simultáneas mediante tablas de cronología
1. ¿Qué es una transacción?
1.1. Concepto
Imagina que vas al banco a transferir 300 € de tu cuenta a la de un amigo. Para el banco son dos operaciones: restar 300 € de tu cuenta y sumar 300 € a la de tu amigo. ¿Qué pasaría si el sistema se cayera justo entre las dos? Tus 300 € habrían desaparecido sin llegar a ningún sitio. Para el banco, y para ti, esas dos operaciones deben ser una sola unidad: o se hacen las dos, o no se hace ninguna.
📘 Concepto: Una transacción (transaction) es una secuencia de una o más operaciones sobre la base de datos que se ejecuta como una unidad lógica de trabajo: o todas sus operaciones tienen efecto (se confirman, commit) o ninguna lo tiene (se deshacen, rollback). Lleva la base de datos de un estado consistente a otro estado consistente.
Ya en el módulo 03 viste que el gestor de transacciones es uno de los componentes fundamentales de un SGBD. En este módulo lo verás funcionar en Oracle.
stateDiagram-v2
[*] --> Activa: primera sentencia DML
Activa --> Activa: más sentencias DML
Activa --> Confirmada: COMMIT
Activa --> Abortada: ROLLBACK o fallo
Confirmada --> [*]: cambios permanentes y visibles
Abortada --> [*]: base de datos como al principio
1.2. Preparamos un ejemplo: cuentas bancarias
Para los ejemplos que necesitan confirmar cambios (y no queremos estropear EMPRESA) usamos una pequeña tabla de cuentas:
CREATE TABLE cuentas_demo (
cuenta_id NUMBER(4) CONSTRAINT pk_cuentas_demo PRIMARY KEY,
titular VARCHAR2(30) NOT NULL,
saldo NUMBER(10,2) NOT NULL,
CONSTRAINT ck_cue_saldo CHECK (saldo >= 0)
);
INSERT INTO cuentas_demo VALUES (1, 'Ana García', 1000);
INSERT INTO cuentas_demo VALUES (2, 'Luis Martínez', 500);
INSERT INTO cuentas_demo VALUES (3, 'Marta Sánchez', 200);
COMMIT;
SELECT * FROM cuentas_demo ORDER BY cuenta_id;
Tabla creada.
1 fila creada.
1 fila creada.
1 fila creada.
Confirmación terminada.
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
1 Ana García 1000
2 Luis Martínez 500
3 Marta Sánchez 200
3 filas seleccionadas.
Una transferencia de 300 € de Ana a Luis es una transacción con dos UPDATE:
UPDATE cuentas_demo SET saldo = saldo - 300 WHERE cuenta_id = 1;
UPDATE cuentas_demo SET saldo = saldo + 300 WHERE cuenta_id = 2;
SELECT * FROM cuentas_demo ORDER BY cuenta_id;
COMMIT;
1 fila actualizada.
1 fila actualizada.
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
1 Ana García 700
2 Luis Martínez 800
3 Marta Sánchez 200
3 filas seleccionadas.
Confirmación terminada.
💡 Consejo: Las salidas de este módulo se han obtenido en Oracle AI Database 26ai Free con SQLPlus, con
SET NULL '(nulo)'y la sesión en español (NLS_LANGUAGE = 'SPANISH',NLS_TERRITORY = 'SPAIN',NLS_DATE_FORMAT = 'DD/MM/YYYY'). Los mensajes de SQLPlus se muestran en español (Confirmación terminada.,Rollback terminado.) y el texto de los erroresORA-en inglés, el único idioma que incluye la edición Free. En las demostraciones con dos sesiones se muestra la hora de cada paso (HH24:MI:SS) para que veas las esperas reales.
2. Las propiedades ACID
Toda transacción debe cumplir cuatro propiedades, conocidas por sus iniciales en inglés: ACID (término acuñado por Härder y Reuter en 1983).
| Propiedad | Significado | Quién la garantiza en Oracle |
|---|---|---|
| Atomicidad (Atomicity) | Todo o nada | Segmentos de deshacer (undo) |
| Consistencia (Consistency) | De un estado válido a otro estado válido | Restricciones de integridad + lógica de la aplicación |
| aIslamiento (Isolation) | Las transacciones concurrentes no se interfieren | Multiversión (lectura consistente) y bloqueos |
| Durabilidad (Durability) | Lo confirmado no se pierde | Registro de rehacer (redo log) escrito en el COMMIT |
2.1. Atomicidad
📘 Concepto: Atomicidad: una transacción es indivisible. Si cualquiera de sus operaciones no puede completarse (error, caída,
ROLLBACK), se deshacen todas las que ya se hubieran hecho.
Oracle lo consigue guardando, por cada cambio, la información necesaria para deshacerlo (la “imagen anterior” de los datos) en los segmentos de deshacer (undo segments). Un ROLLBACK aplica esa información en sentido inverso.
Volvamos al banco. Luis quiere enviar 500 € a Marta… pero se equivoca y ordena la operación al revés (Marta, que solo tiene 200 €, enviaría 500 € a Luis):
UPDATE cuentas_demo SET saldo = saldo + 500 WHERE cuenta_id = 2; -- abono a Luis
UPDATE cuentas_demo SET saldo = saldo - 500 WHERE cuenta_id = 3; -- cargo a Marta: saldo negativo
SELECT * FROM cuentas_demo ORDER BY cuenta_id;
1 fila actualizada.
UPDATE cuentas_demo SET saldo = saldo - 500 WHERE cuenta_id = 3
*
ERROR en línea 1:
ORA-02290: check constraint (CURSO.CK_CUE_SALDO) violated
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
1 Ana García 700
2 Luis Martínez 1300
3 Marta Sánchez 200
3 filas seleccionadas.
¡Cuidado! El segundo UPDATE ha fallado, pero el primero sigue aplicado: Luis tiene 500 € de más. Oracle deshace la sentencia que falla, no la transacción entera. Es la aplicación (o tú) quien debe decidir qué hacer; aquí lo correcto es deshacer toda la transacción:
ROLLBACK;
SELECT * FROM cuentas_demo ORDER BY cuenta_id;
Rollback terminado.
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
1 Ana García 700
2 Luis Martínez 800
3 Marta Sánchez 200
3 filas seleccionadas.
⚠️ Importante: En Oracle, un error en una sentencia no aborta automáticamente la transacción (a diferencia de PostgreSQL, que la marca como abortada). La transacción sigue abierta con todos los cambios anteriores. Si no haces
ROLLBACKy luego hacesCOMMIT, confirmarás una transferencia a medias.
2.2. Consistencia
📘 Concepto: Consistencia: si la base de datos cumple todas las reglas de integridad antes de la transacción, debe cumplirlas también después. Durante la transacción puede haber estados intermedios inconsistentes, pero nunca deben quedar confirmados.
La consistencia se apoya en dos pilares:
- Las restricciones declarativas (
PRIMARY KEY,FOREIGN KEY,CHECK,NOT NULL,UNIQUE), que Oracle comprueba siempre. En el ejemplo anterior,ck_cue_saldoimpidió un saldo negativo. - Las reglas de negocio que no son restricciones y que debe respetar la aplicación. Por ejemplo, “una transferencia no cambia el dinero total del banco”:
SELECT SUM(saldo) AS total FROM cuentas_demo;
TOTAL
----------
1700
1 fila seleccionada.
Antes de la transferencia de Ana a Luis el total era 1000 + 500 + 200 = 1700, y después sigue siendo 1700. Esa invariante solo se mantiene si los dos UPDATE van en la misma transacción.
2.3. Aislamiento
📘 Concepto: Aislamiento: cada transacción debe ejecutarse como si fuera la única en el sistema. Los cambios no confirmados de una transacción no deben ser visibles para las demás, y el resultado de ejecutar transacciones concurrentes debería ser equivalente a ejecutarlas una detrás de otra (seriabilidad).
El aislamiento total es caro (obligaría a esperar mucho), así que los SGBD ofrecen niveles de aislamiento que relajan esta propiedad a cambio de rendimiento (sección 7). Oracle lo implementa con un mecanismo propio, la lectura consistente multiversión (sección 5), más los bloqueos de fila (sección 8).
2.4. Durabilidad
📘 Concepto: Durabilidad: una vez que el SGBD confirma una transacción (
Confirmación terminada.), sus cambios persisten aunque inmediatamente después se vaya la luz o se caiga el servidor.
En Oracle, cuando haces COMMIT, el proceso LGWR (Log Writer) escribe en los ficheros de redo log (en disco) la descripción de todos los cambios de la transacción. Solo cuando esa escritura termina, Oracle te responde Confirmación terminada.. Los bloques de datos modificados pueden seguir en memoria y escribirse en los ficheros de datos más tarde (proceso DBWn): si el servidor cae, al arrancar Oracle hace la recuperación de instancia (instance recovery): reaplica el redo de lo confirmado y deshace con el undo lo que no lo estaba.
flowchart LR
S["Tu sesión<br/>UPDATE ..."] --> BC["Caché de datos<br/>(bloques en memoria)"]
S --> UN["Undo<br/>(imagen anterior)"]
S --> RB["Buffer de redo<br/>(en memoria)"]
C["COMMIT"] --> LGWR["LGWR escribe<br/>el redo en disco"]
RB --> LGWR
LGWR --> OK["Confirmación terminada."]
BC -. "más tarde (DBWn)" .-> DF[("Ficheros de datos")]
| Elemento | Para qué sirve |
|---|---|
| Undo | Deshacer (ROLLBACK), lectura consistente (ver datos anteriores) y Flashback Query |
| Redo | Rehacer lo confirmado tras una caída (durabilidad) |
| SCN (System Change Number) | Número que crece con cada confirmación; marca el “momento” de cada cambio y de cada lectura |
3. Inicio y fin de una transacción en Oracle
3.1. Comienzo implícito
Oracle no tiene una sentencia BEGIN TRANSACTION (como SQL Server o MySQL con START TRANSACTION). Una transacción empieza implícitamente con la primera sentencia DML (INSERT, UPDATE, DELETE, MERGE, SELECT ... FOR UPDATE) que ejecutas tras conectarte o tras terminar la transacción anterior. Las consultas SELECT normales no inician transacción.
Opcionalmente puedes empezarla con SET TRANSACTION, para fijar sus características (nivel de aislamiento, solo lectura, nombre). Debe ser la primera sentencia de la transacción:
UPDATE cuentas_demo SET saldo = saldo WHERE cuenta_id = 1; -- ya hay transacción en curso
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
ROLLBACK;
1 fila actualizada.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
*
ERROR en línea 1:
ORA-01453: SET TRANSACTION must be first statement of transaction
Rollback terminado.
💡 Consejo:
BEGINen Oracle abre un bloque PL/SQL, no una transacción. Si vienes de otro SGBD y escribesBEGIN TRANSACTION;, obtendrás un error de compilación de PL/SQL.
3.2. Fin explícito: COMMIT y ROLLBACK
| Sentencia | Efecto |
|---|---|
COMMIT [WORK] | Hace permanentes todos los cambios de la transacción, los hace visibles a las demás sesiones, libera los bloqueos y borra los savepoints |
ROLLBACK [WORK] | Deshace todos los cambios de la transacción, libera los bloqueos y borra los savepoints |
ROLLBACK TO [SAVEPOINT] nombre | Deshace solo los cambios posteriores al savepoint; la transacción sigue abierta |
La palabra WORK es opcional y existe por compatibilidad con el estándar.
3.3. COMMIT implícito por DDL
Toda sentencia DDL (CREATE, ALTER, DROP, TRUNCATE, RENAME, COMMENT, GRANT, REVOKE…) ejecuta un COMMIT antes de empezar (y otro al terminar). Lo que tuvieras pendiente queda confirmado:
UPDATE cuentas_demo SET saldo = saldo + 1000 WHERE cuenta_id = 3;
CREATE TABLE tmp_ddl_demo (x NUMBER); -- COMMIT implícito
ROLLBACK; -- ya no hay nada que deshacer
SELECT * FROM cuentas_demo WHERE cuenta_id = 3;
1 fila actualizada.
Tabla creada.
Rollback terminado.
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
3 Marta Sánchez 1200
1 fila seleccionada.
Y lo más sorprendente: el COMMIT se produce aunque la sentencia DDL falle (si el error no es de sintaxis, sino de ejecución):
UPDATE cuentas_demo SET saldo = saldo + 1000 WHERE cuenta_id = 3;
CREATE TABLE cuentas_demo (x NUMBER); -- falla: la tabla ya existe
ROLLBACK;
SELECT * FROM cuentas_demo WHERE cuenta_id = 3;
1 fila actualizada.
CREATE TABLE cuentas_demo (x NUMBER)
*
ERROR en línea 1:
ORA-00955: name is already used by an existing object
Rollback terminado.
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
3 Marta Sánchez 1200
1 fila seleccionada.
El saldo de Marta (que se había restaurado a 200 entre ambos ejemplos) vuelve a ser 1200: el UPDATE quedó confirmado por el intento fallido de CREATE TABLE. Restauramos el saldo antes de seguir:
UPDATE cuentas_demo SET saldo = 200 WHERE cuenta_id = 3;
COMMIT;
1 fila actualizada.
Confirmación terminada.
⚠️ Importante: No mezcles DDL y DML en la misma “transacción”. En un script, termina explícitamente las transacciones (
COMMIToROLLBACK) antes de cualquier DDL.
3.4. COMMIT implícito al salir de SQL*Plus
Por defecto, SQL*Plus (y SQLcl) hacen COMMIT al salir con EXIT o QUIT. Lo controla la variable EXITCOMMIT:
SHOW EXITCOMMIT
exitcommit ON
💻 Ejemplo: Una sesión modifica un saldo y sale sin
COMMIT; después, una sesión nueva consulta el saldo.
-- Sesión 1
UPDATE cuentas_demo SET saldo = 999 WHERE cuenta_id = 3;
EXIT
1 fila actualizada.
-- Sesión 2 (nueva conexión)
SELECT * FROM cuentas_demo WHERE cuenta_id = 3;
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
3 Marta Sánchez 999
1 fila seleccionada.
El cambio se confirmó al salir. Con SET EXITCOMMIT OFF, el EXIT hace ROLLBACK:
-- Sesión 1 (tras restaurar el saldo a 200)
SET EXITCOMMIT OFF
UPDATE cuentas_demo SET saldo = 999 WHERE cuenta_id = 3;
EXIT
-- Sesión 2
SELECT * FROM cuentas_demo WHERE cuenta_id = 3;
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
3 Marta Sánchez 200
1 fila seleccionada.
⚠️ Importante: El comportamiento al cerrar depende de la herramienta, no del servidor. SQL Developer, al desconectar con cambios pendientes, pregunta si quieres confirmarlos o deshacerlos; muchos controladores de programación (JDBC, por ejemplo) trabajan por defecto en modo autocommit. Nunca confíes en el cierre: termina tú las transacciones.
3.5. ROLLBACK implícito ante una caída
Si la sesión termina de forma anormal (se cuelga el cliente, se corta la red, se mata el proceso), Oracle detecta que la sesión ha muerto y el proceso de fondo PMON deshace su transacción y libera sus bloqueos. Lo hemos comprobado matando (kill -9) un SQL*Plus con un cambio pendiente:
-- Sesión 1: hace el cambio y se queda esperando... y la matamos
UPDATE cuentas_demo SET saldo = 999 WHERE cuenta_id = 3;
1 fila actualizada.
-- Sesión 2, tras matar el proceso de la sesión 1
SELECT * FROM cuentas_demo WHERE cuenta_id = 3;
UPDATE cuentas_demo SET saldo = saldo WHERE cuenta_id = 3; -- ¿sigue bloqueada la fila?
ROLLBACK;
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
3 Marta Sánchez 200
1 fila seleccionada.
1 fila actualizada.
Rollback terminado.
El cambio se ha deshecho y la fila ya no está bloqueada. Lo mismo ocurre si se cae el servidor entero: en la recuperación de instancia se deshacen las transacciones que no habían confirmado.
| Cómo termina | Resultado |
|---|---|
COMMIT | Se confirma |
ROLLBACK | Se deshace |
| Sentencia DDL (aunque falle en ejecución) | Se confirma lo anterior |
EXIT en SQL*Plus con EXITCOMMIT ON (por defecto) | Se confirma |
EXIT en SQL*Plus con EXITCOMMIT OFF | Se deshace |
| Caída del cliente, de la red o del servidor | Se deshace (PMON / recuperación de instancia) |
4. SAVEPOINT: puntos de retorno
4.1. Concepto
📘 Concepto: Un punto de salvaguarda (savepoint) es una marca con nombre dentro de una transacción.
ROLLBACK TO SAVEPOINT nombredeshace solo los cambios hechos después de esa marca y deja la transacción abierta, con los cambios anteriores intactos.
Es como el “guardar partida” de un videojuego: si algo sale mal, vuelves al último punto guardado sin empezar desde el principio.
4.2. Demostración
UPDATE cuentas_demo SET saldo = saldo - 100 WHERE cuenta_id = 1;
SAVEPOINT tras_cargo;
UPDATE cuentas_demo SET saldo = saldo + 100 WHERE cuenta_id = 2;
SAVEPOINT tras_abono;
DELETE FROM cuentas_demo WHERE cuenta_id = 3;
SELECT * FROM cuentas_demo ORDER BY cuenta_id;
1 fila actualizada.
Punto de grabación creado.
1 fila actualizada.
Punto de grabación creado.
1 fila suprimida.
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
1 Ana García 600
2 Luis Martínez 900
2 filas seleccionadas.
Deshacemos solo el borrado:
ROLLBACK TO SAVEPOINT tras_abono;
SELECT * FROM cuentas_demo ORDER BY cuenta_id;
Rollback terminado.
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
1 Ana García 600
2 Luis Martínez 900
3 Marta Sánchez 200
3 filas seleccionadas.
Retrocedemos un poco más (la palabra SAVEPOINT es opcional):
ROLLBACK TO tras_cargo;
SELECT * FROM cuentas_demo ORDER BY cuenta_id;
Rollback terminado.
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
1 Ana García 600
2 Luis Martínez 800
3 Marta Sánchez 200
3 filas seleccionadas.
Al volver a tras_cargo, el savepoint posterior tras_abono deja de existir:
ROLLBACK TO tras_abono;
ROLLBACK TO tras_abono
*
ERROR en línea 1:
ORA-01086: savepoint 'TRAS_ABONO' never established in this session or is invalid
Un ROLLBACK completo deshace todo (también el cargo a Ana) y borra todos los savepoints:
ROLLBACK;
SELECT * FROM cuentas_demo ORDER BY cuenta_id;
ROLLBACK TO tras_cargo;
Rollback terminado.
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
1 Ana García 700
2 Luis Martínez 800
3 Marta Sánchez 200
3 filas seleccionadas.
ROLLBACK TO tras_cargo
*
ERROR en línea 1:
ORA-01086: savepoint 'TRAS_CARGO' never established in this session or is invalid
4.3. Reglas de los savepoints
- Si creas dos savepoints con el mismo nombre, el segundo sustituye al primero:
UPDATE cuentas_demo SET saldo = 1 WHERE cuenta_id = 1;
SAVEPOINT sp;
UPDATE cuentas_demo SET saldo = 2 WHERE cuenta_id = 1;
SAVEPOINT sp; -- mueve "sp" aquí
UPDATE cuentas_demo SET saldo = 3 WHERE cuenta_id = 1;
ROLLBACK TO sp;
SELECT saldo FROM cuentas_demo WHERE cuenta_id = 1;
ROLLBACK;
SALDO
----------
2
1 fila seleccionada.
ROLLBACK TOno termina la transacción ni libera los bloqueos adquiridos antes del savepoint (sí los adquiridos después).COMMITyROLLBACKcompletos eliminan todos los savepoints.- Uso típico: en un proceso largo (por ejemplo, cargar un lote de pedidos), marcar un savepoint antes de cada pedido y, si uno falla, volver a él y seguir con el siguiente sin perder los anteriores.
5. Atomicidad de sentencia y lectura consistente
5.1. Atomicidad a nivel de sentencia
Además de la atomicidad de la transacción, Oracle garantiza que cada sentencia es atómica: si falla en cualquier fila, se deshace la sentencia completa (Oracle crea un savepoint implícito antes de cada sentencia). Pero, como viste en 2.1, la transacción sigue viva con las sentencias anteriores:
UPDATE cuentas_demo SET saldo = saldo + 50 WHERE cuenta_id = 3; -- correcta
UPDATE cuentas_demo SET saldo = saldo - 600; -- falla en Marta (250 - 600 < 0)
SELECT * FROM cuentas_demo ORDER BY cuenta_id;
ROLLBACK;
1 fila actualizada.
UPDATE cuentas_demo SET saldo = saldo - 600
*
ERROR en línea 1:
ORA-02290: check constraint (CURSO.CK_CUE_SALDO) violated
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
1 Ana García 700
2 Luis Martínez 800
3 Marta Sánchez 250
3 filas seleccionadas.
Rollback terminado.
El segundo UPDATE podía restar 600 a Ana (700) y a Luis (800), pero como falló con Marta no se ha aplicado a ninguna cuenta. El primer UPDATE (+50 a Marta) sigue pendiente.
flowchart LR
T["Transacción"] --> S1["Sentencia 1<br/>correcta"]
S1 --> S2["Sentencia 2<br/>falla en una fila"]
S2 -- "se deshace entera" --> X["Estado tras la sentencia 1"]
X --> D{"¿Qué hace la aplicación?"}
D -- "COMMIT" --> C["Se confirma solo la sentencia 1"]
D -- "ROLLBACK" --> R["No queda nada"]
5.2. Lectura consistente multiversión
Oracle aplica un principio fundamental:
📘 Concepto: Lectura consistente (read consistency): cada consulta ve los datos tal como estaban confirmados en el instante en que empezó (un SCN concreto), aunque otras sesiones los modifiquen mientras la consulta se ejecuta. Si un bloque ha cambiado después, Oracle reconstruye la versión antigua con el undo. Es un modelo multiversión (MVCC, Multi-Version Concurrency Control).
De este principio se derivan tres consecuencias que distinguen a Oracle:
- Nadie ve los cambios no confirmados de otros (no existen las lecturas sucias).
- Los lectores no bloquean a los escritores: un
SELECTno pone bloqueos, así que nunca impide unUPDATE. - Los escritores no bloquean a los lectores: un
UPDATEpendiente no impide que otros lean la fila (leen la versión anterior).
Solo los escritores bloquean a los escritores que quieren modificar las mismas filas.
💻 Ejemplo (dos sesiones): A modifica el salario de Carmen sin confirmar; B lo consulta antes y después del
COMMITde A.
| Instante | Sesión A | Sesión B |
|---|---|---|
| 23:03:00 | UPDATE empleados SET salario = 2500 WHERE emp_id = 110; | |
| 23:03:02 | SELECT ... WHERE emp_id = 110; → 2200 | |
| 23:03:06 | COMMIT; | |
| 23:03:08 | SELECT ... WHERE emp_id = 110; → 2500 |
Salida real de la sesión A:
[A] t0 UPDATE sin confirmar
HORA
--------
23:03:00
1 fila actualizada.
EMP_ID SALARIO
---------- ----------
110 2500
[A] t6 COMMIT
HORA
--------
23:03:06
Confirmación terminada.
Salida real de la sesión B:
[B] t2 leer mientras A no ha confirmado
HORA
--------
23:03:02
EMP_ID SALARIO
---------- ----------
110 2200
[B] t8 leer tras COMMIT de A
HORA
--------
23:03:08
EMP_ID SALARIO
---------- ----------
110 2500
A ve su propio cambio (2500) desde el primer momento; B no lo ve hasta que A confirma. Y el SELECT de B a las 23:03:02 no ha esperado: ha leído la versión anterior al instante.
⚠️ Importante: Si una consulta muy larga necesita versiones antiguas que el undo ya ha sobrescrito, falla con
ORA-01555: snapshot too old. Se evita dimensionando el undo (parámetroUNDO_RETENTION) y no dejando consultas eternas; es tarea del administrador.
6. Problemas de concurrencia
Cuando varias transacciones acceden a los mismos datos a la vez, sin un control adecuado pueden aparecer cuatro problemas clásicos (descritos por el estándar SQL-92 y por la teoría de Gray y Bernstein).
| Problema | Inglés | Qué ocurre | ¿Puede pasar en Oracle? |
|---|---|---|---|
| Actualización perdida | Lost update | Dos transacciones leen un dato y lo sobrescriben; una de las escrituras se pierde | Sí, si la aplicación lee, calcula fuera y escribe un valor absoluto |
| Lectura sucia | Dirty read | Se lee un dato modificado por otra transacción no confirmada (que luego podría deshacerse) | No, nunca |
| Lectura no repetible | Non-repeatable read | Leer dos veces la misma fila en la misma transacción da valores distintos porque otra la modificó y confirmó entre medias | Sí, en READ COMMITTED |
| Lectura fantasma | Phantom read | Repetir una consulta con condición devuelve filas nuevas (o desaparecidas) insertadas/borradas por otra transacción confirmada | Sí, en READ COMMITTED |
6.1. Actualización perdida (demostrado)
El patrón peligroso es “leer → calcular en la aplicación → escribir el valor calculado”. Dos vendedores venden a la vez el mismo portátil (stock 25): A vende 5 y B vende 3. El stock final debería ser 17.
| Instante | Sesión A | Sesión B |
|---|---|---|
| 23:06:50 | SELECT stock → 25 | |
| 23:06:51 | SELECT stock → 25 | |
| 23:06:53 | UPDATE ... SET stock = 20 (25 − 5) | |
| 23:06:54 | UPDATE ... SET stock = 22 (25 − 3) → espera (A tiene la fila) | |
| 23:06:56 | COMMIT; | el UPDATE termina |
| 23:06:56 | COMMIT; → stock final 22 |
Salida real de B:
[B] t1 lee stock
HORA
--------
23:06:51
PRODUCTO_ID NOMBRE STOCK
----------- ---------------------- ----------
1 Portátil UltraBook 14 25
[B] t4 vende 3: escribe 25 - 3 = 22
HORA
--------
23:06:54
1 fila actualizada.
HORA
--------
23:06:56
Confirmación terminada.
[B] resultado final
PRODUCTO_ID NOMBRE STOCK
----------- ---------------------- ----------
1 Portátil UltraBook 14 22
El stock es 22: la venta de A se ha perdido. Oracle hizo esperar a B (bloqueo de fila), pero B escribió un valor calculado con un dato obsoleto. Hay tres soluciones:
Solución 1: actualización relativa. Que sea la base de datos quien haga el cálculo con el valor actual:
UPDATE productos SET stock = stock - 3 WHERE producto_id = 1;
Repetido el escenario con stock = stock - 5 en A y stock = stock - 3 en B, B espera a A (de 23:07:19 a 23:07:22) y después resta 3 sobre el valor ya confirmado:
[B] t1 vende 3 con stock = stock - 3 (espera)
HORA
--------
23:07:19
1 fila actualizada.
HORA
--------
23:07:22
Confirmación terminada.
PRODUCTO_ID NOMBRE STOCK
----------- ---------------------- ----------
1 Portátil UltraBook 14 17
Solución 2: bloqueo pesimista con SELECT ... FOR UPDATE. Cada sesión bloquea la fila al leerla, de modo que la segunda no puede ni leerla para modificar hasta que la primera termine (sección 8.3):
| Instante | Sesión A | Sesión B |
|---|---|---|
| 23:07:05 | SELECT ... FOR UPDATE → 25 | |
| 23:07:06 | SELECT ... FOR UPDATE → espera | |
| 23:07:11 | UPDATE ... SET stock = 20; COMMIT; | recibe 20 |
| 23:07:11 | UPDATE ... SET stock = 17; COMMIT; |
[B] t1 lee stock con FOR UPDATE (espera)
HORA
--------
23:07:06
PRODUCTO_ID NOMBRE STOCK
----------- ---------------------- ----------
1 Portátil UltraBook 14 20
HORA
--------
23:07:11
[B] vende 3 sobre el valor leido: 20 - 3 = 17
1 fila actualizada.
Confirmación terminada.
PRODUCTO_ID NOMBRE STOCK
----------- ---------------------- ----------
1 Portátil UltraBook 14 17
Solución 3: bloqueo optimista. No se bloquea al leer; al escribir se comprueba que el dato no ha cambiado (WHERE stock = valor_leído). Si el UPDATE afecta a 0 filas, alguien se adelantó: se vuelve a leer y se reintenta. Lo verás en el ejercicio 7.
| Técnica | Ventaja | Inconveniente |
|---|---|---|
Actualización relativa (x = x - n) | Sencilla, sin esperas largas | Solo vale si el cálculo cabe en el UPDATE |
Pesimista (FOR UPDATE) | Garantiza que nadie toca la fila mientras decides | Los demás esperan; peligroso si el usuario tarda |
Optimista (WHERE col = valor_leído o columna de versión) | No bloquea mientras el usuario piensa | Hay que gestionar el reintento |
6.2. Lectura sucia: imposible en Oracle
La demostración de la sección 5.2 es precisamente el caso de lectura sucia: B consulta mientras A tiene un cambio sin confirmar y no lo ve. Oracle no ofrece ningún nivel de aislamiento que permita lecturas sucias:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
*
ERROR en línea 1:
ORA-02179: valid options: ISOLATION LEVEL { SERIALIZABLE | READ COMMITTED }
6.3. Lectura no repetible (demostrado)
B lee dos veces el salario de Carmen dentro de la misma transacción; entre ambas lecturas, A lo cambia y confirma.
| Instante | Sesión A | Sesión B (READ COMMITTED) |
|---|---|---|
| 23:08:05 | SELECT salario → 2200 | |
| 23:08:07 | UPDATE ... = 2500; COMMIT; | |
| 23:08:09 | SELECT salario → 2500 |
[B] t0 primera lectura
HORA
--------
23:08:05
EMP_ID SALARIO
---------- ----------
110 2200
[B] t4 segunda lectura (misma transaccion)
HORA
--------
23:08:09
EMP_ID SALARIO
---------- ----------
110 2500
En READ COMMITTED cada sentencia ve lo confirmado al empezar esa sentencia, así que la segunda lectura ve el cambio de A. No es un error de Oracle: es lo que define ese nivel. Pero si B estuviera calculando un informe con ambas lecturas, sus cifras no cuadrarían (ejercicio 8).
6.4. Lectura fantasma (demostrado)
B cuenta los empleados de Informática dos veces; entre medias, A da de alta a uno nuevo y confirma.
| Instante | Sesión A | Sesión B (READ COMMITTED) |
|---|---|---|
| 23:08:49 | COUNT(*), SUM(salario) → 6, 18800 | |
| 23:08:51 | INSERT ... dept_id = 30; COMMIT; | |
| 23:08:53 | COUNT(*), SUM(salario) → 7, 21100 |
[B] t0 primer recuento
EMPLEADOS_30 MASA
------------ ----------
6 18800
[B] t4 segundo recuento
EMPLEADOS_30 MASA
------------ ----------
7 21100
Ha aparecido una fila “fantasma”. La diferencia con la lectura no repetible es sutil: allí cambiaba el valor de una fila ya leída; aquí cambia el conjunto de filas que cumplen la condición.
flowchart TB
subgraph NR["Lectura no repetible"]
nr1["Leo la fila 110: 2200"] --> nr2["Otro hace UPDATE + COMMIT"] --> nr3["Leo la fila 110: 2500"]
end
subgraph FA["Lectura fantasma"]
fa1["Cuento filas dept 30: 6"] --> fa2["Otro hace INSERT + COMMIT"] --> fa3["Cuento filas dept 30: 7"]
end
7. Niveles de aislamiento
7.1. Los niveles del estándar SQL
El estándar SQL-92 define cuatro niveles de aislamiento según qué problemas permiten:
| Nivel | Lectura sucia | Lectura no repetible | Lectura fantasma |
|---|---|---|---|
READ UNCOMMITTED | Posible | Posible | Posible |
READ COMMITTED | No | Posible | Posible |
REPEATABLE READ | No | No | Posible |
SERIALIZABLE | No | No | No |
La actualización perdida no aparece en la tabla del estándar porque cualquier nivel con bloqueos de escritura la evita si las actualizaciones son relativas; con lecturas en la aplicación hay que protegerse como en 6.1.
7.2. Los niveles de Oracle
| Nivel en Oracle | ¿Estándar? | Qué ve cada consulta | Problemas posibles |
|---|---|---|---|
READ COMMITTED (por defecto) | Sí | Lo confirmado al empezar la sentencia | No repetible, fantasma |
SERIALIZABLE | Sí | Lo confirmado al empezar la transacción (más sus propios cambios) | Ninguno de los tres; puede dar ORA-08177 al escribir |
READ ONLY | No (extensión de Oracle) | Lo confirmado al empezar la transacción | Ninguno; no permite DML |
Oracle no admite READ UNCOMMITTED ni REPEATABLE READ:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
*
ERROR en línea 1:
ORA-02179: valid options: ISOLATION LEVEL { SERIALIZABLE | READ COMMITTED }
7.3. Cómo se elige el nivel
Para una transacción se usa SET TRANSACTION como primera sentencia (las variantes son ISOLATION LEVEL READ COMMITTED, ISOLATION LEVEL SERIALIZABLE y READ ONLY, y opcionalmente NAME 'texto' para identificarla en las vistas de administración). Para todas las transacciones de la sesión, ALTER SESSION:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
COMMIT;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE NAME 'cierre_mensual';
ROLLBACK;
ALTER SESSION SET ISOLATION_LEVEL = SERIALIZABLE;
ALTER SESSION SET ISOLATION_LEVEL = READ COMMITTED;
Transacción definida.
Confirmación terminada.
Transacción definida.
Rollback terminado.
Sesión modificada.
Sesión modificada.
SET TRANSACTION solo afecta a la transacción actual; al hacer COMMIT o ROLLBACK se vuelve al nivel de la sesión.
7.4. SERIALIZABLE: lecturas repetibles y sin fantasmas (demostrado)
Repetimos los escenarios 6.3 y 6.4 con B en SERIALIZABLE:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
| Instante | Sesión A | Sesión B (SERIALIZABLE) |
|---|---|---|
| 23:08:21 | SELECT salario (110) → 2200 | |
| 23:08:23 | UPDATE ... = 2500; COMMIT; | |
| 23:08:25 | SELECT salario (110) → 2200 | |
COMMIT; | ||
SELECT salario (110) → 2500 |
Transacción definida.
[B] t0 primera lectura
EMP_ID SALARIO
---------- ----------
110 2200
[B] t4 segunda lectura (misma transaccion)
EMP_ID SALARIO
---------- ----------
110 2200
Confirmación terminada.
[B] t4 tras terminar la transaccion
EMP_ID SALARIO
---------- ----------
110 2500
Y el recuento de Informática:
Transacción definida.
[B] t0 primer recuento
EMPLEADOS_30 MASA
------------ ----------
6 18800
[B] t4 segundo recuento
EMPLEADOS_30 MASA
------------ ----------
6 18800
B ve una “foto” fija de la base de datos tomada al comenzar su transacción. Al terminarla, ve los cambios de A.
7.5. El precio de SERIALIZABLE: ORA-08177
¿Qué pasa si una transacción serializable intenta modificar una fila que otra transacción cambió y confirmó después de que ella empezara? Oracle no puede hacer como si la otra no existiera, así que rechaza la sentencia:
| Instante | Sesión A | Sesión B (SERIALIZABLE) |
|---|---|---|
| 23:09:18 | SELECT salario (110) → 2200 | |
| 23:09:20 | UPDATE ... (110); COMMIT; | |
| 23:09:22 | UPDATE ... (110) → ORA-08177 |
[B] t4 intenta modificar la fila que A ha cambiado y confirmado
HORA
--------
23:09:22
UPDATE empleados SET salario = salario + 100 WHERE emp_id = 110
*
ERROR en línea 1:
ORA-08177: can't serialize access for this transaction
[B] t4 modificar otra fila
UPDATE empleados SET salario = salario + 100 WHERE emp_id = 111
*
ERROR en línea 1:
ORA-08177: can't serialize access for this transaction
Fíjate en el segundo error: la fila 111 no la había tocado A, pero está en el mismo bloque de datos que la 110. Oracle registra las transacciones que han modificado un bloque en la cabecera del bloque y, en algunos casos, no puede distinguir qué fila concreta cambió, así que también rechaza la modificación. La documentación de Oracle lo advierte: con SERIALIZABLE, la aplicación debe estar preparada para recibir ORA-08177, hacer ROLLBACK y reintentar la transacción completa (ejercicio 9).
📘 Concepto: Técnicamente, el
SERIALIZABLEde Oracle (como el de PostgreSQL antes de la versión 9.1) es aislamiento de instantánea (snapshot isolation): evita las tres anomalías del estándar, pero en teoría admite una anomalía más rebuscada llamada sesgo de escritura (write skew): dos transacciones leen el mismo conjunto de filas, cada una modifica filas distintas basándose en lo leído, y el resultado conjunto viola una regla que ninguna de las dos violaba por separado (por ejemplo, dos médicos de guardia que se van a la vez porque cada uno vio que quedaba el otro). Las soluciones son bloquear explícitamente lo leído (SELECT ... FOR UPDATE) o modelar la regla con una restricción.
7.6. READ ONLY
Una transacción READ ONLY ve la base de datos como estaba al empezar (igual que SERIALIZABLE) y no permite modificaciones. Es ideal para informes que lanzan varias consultas que deben cuadrar entre sí:
SET TRANSACTION READ ONLY;
SELECT SUM(salario) FROM empleados;
UPDATE empleados SET salario = 0 WHERE emp_id = 160;
COMMIT;
Transacción definida.
SUM(SALARIO)
------------
67900
1 fila seleccionada.
UPDATE empleados SET salario = 0 WHERE emp_id = 160
*
ERROR en línea 1:
ORA-01456: may not perform insert, delete, update operation inside a READ ONLY transaction
Confirmación terminada.
Como nunca escribe, nunca recibe ORA-08177 ni bloquea a nadie. Y tampoco bloquea a los escritores: en la repetición del escenario 6.3 con B en READ ONLY, A modificó y confirmó la fila que B había leído sin ninguna espera (los lectores no bloquean a los escritores), mientras B seguía viendo 2200.
7.7. ¿Qué nivel elijo?
| Situación | Nivel recomendado |
|---|---|
| Aplicaciones de gestión normales (OLTP) | READ COMMITTED (por defecto) con actualizaciones relativas o FOR UPDATE donde haga falta |
| Informe con varias consultas que deben cuadrar | READ ONLY |
| Proceso que lee y escribe y necesita una foto fija (cierre contable) con pocas colisiones | SERIALIZABLE, con reintento ante ORA-08177 |
| Muchas transacciones concurrentes sobre las mismas filas | READ COMMITTED + bloqueos explícitos; SERIALIZABLE provocaría muchos ORA-08177 |
8. Bloqueos
8.1. Tipos de bloqueo en Oracle
Oracle bloquea automáticamente lo mínimo necesario. Los dos tipos que te encontrarás con el DML son:
| Tipo | Nombre | Qué protege | Quién lo pone |
|---|---|---|---|
| TX | Bloqueo de transacción (de fila) | Las filas modificadas: nadie más puede modificarlas hasta que termines | INSERT, UPDATE, DELETE, MERGE, SELECT ... FOR UPDATE |
| TM | Bloqueo DML (de tabla) | La estructura de la tabla: impide, por ejemplo, un DROP o ALTER mientras la modificas | Las mismas sentencias (en modo compatible con otros DML) y LOCK TABLE |
📘 Concepto: Oracle nunca escala bloqueos: aunque modifiques un millón de filas, no convierte los bloqueos de fila en un bloqueo de tabla (otros SGBD sí lo hacen). Además, el bloqueo de fila se guarda en el propio bloque de datos, no en una lista en memoria, así que no hay límite de filas bloqueadas.
Los bloqueos se liberan solo al terminar la transacción (COMMIT o ROLLBACK), o al hacer ROLLBACK TO un savepoint anterior a ellos.
8.2. Bloqueo implícito en DML: el escritor espera al escritor
| Instante | Sesión A | Sesión B |
|---|---|---|
| 23:04:33 | UPDATE empleados ... WHERE emp_id = 110; | |
| 23:04:35 | SELECT ... WHERE emp_id = 110; → inmediato, 2200 | |
| 23:04:35 | UPDATE ... WHERE emp_id = 111; → inmediato | |
| 23:04:35 | UPDATE ... WHERE emp_id = 110; → espera | |
| 23:04:40 | ROLLBACK; | el UPDATE termina |
Salida real de B:
[B] SELECT de la fila bloqueada
HORA
--------
23:04:35
EMP_ID SALARIO
---------- ----------
110 2200
[B] UPDATE de otra fila (111)
1 fila actualizada.
[B] UPDATE de la fila 110 (espera)
HORA
--------
23:04:35
1 fila actualizada.
HORA
--------
23:04:40
EMP_ID SALARIO
---------- ----------
110 2300
111 2200
- La lectura de la fila bloqueada no espera (lee la versión confirmada).
- Modificar otra fila de la misma tabla no espera (el bloqueo es de fila, no de tabla).
- Modificar la misma fila espera exactamente hasta que A termina (5 segundos). Como A hizo
ROLLBACK, B parte del salario original: 2200 + 100 = 2300.
⚠️ Importante: Por defecto la espera es indefinida. Si A se va a comer con una transacción abierta, B esperará hasta que A vuelva. Por eso las transacciones deben ser cortas y nunca esperar al usuario (sección 11).
8.3. SELECT … FOR UPDATE
SELECT ... FOR UPDATE lee las filas y las bloquea como si fuera a modificarlas. Es la base del bloqueo pesimista. Sus variantes controlan qué hacer si alguna fila ya está bloqueada:
| Cláusula | Si la fila está bloqueada por otro… |
|---|---|
FOR UPDATE | Espera indefinidamente |
FOR UPDATE NOWAIT | Falla al instante con ORA-00054 |
FOR UPDATE WAIT n | Espera hasta n segundos y, si no lo consigue, falla |
FOR UPDATE SKIP LOCKED | Se salta las filas bloqueadas y devuelve (y bloquea) las demás |
FOR UPDATE OF columna | En un join, bloquea solo las filas de la tabla de esa columna |
💻 Ejemplo (dos sesiones): A bloquea a Carmen (110) y David (111) durante 12 segundos; B prueba todas las variantes.
-- Sesión A
SELECT emp_id, nombre, salario FROM empleados WHERE emp_id IN (110, 111) FOR UPDATE;
EMP_ID NOMBRE SALARIO
---------- ---------- ----------
110 Carmen 2200
111 David 2100
2 filas seleccionadas.
Sesión B, NOWAIT (23:05:20):
SELECT emp_id, nombre, salario FROM empleados WHERE emp_id = 110 FOR UPDATE NOWAIT;
SELECT emp_id, nombre, salario FROM empleados WHERE emp_id = 110 FOR UPDATE NOWAIT
*
ERROR en línea 1:
ORA-00054: Failed to acquire a lock (Type: "TX", Name: "Transaction", Description: "Lock held by a transaction to allow other transactions to wait for it") because it is currently held by another
session. The resource being locked can be identified by 524293 ("usn<<16 | slot") and 562 ("sequence")
ORA-40097: Failure to acquire row lock, currently held by transaction ID 8.5.562
En versiones anteriores el mensaje era más escueto (ORA-00054: resource busy and acquire with NOWAIT specified or timeout expired); en 26ai indica incluso el identificador de la transacción que tiene el bloqueo.
Sesión B, WAIT 3 (de 23:05:20 a 23:05:23):
SELECT emp_id, nombre, salario FROM empleados WHERE emp_id = 110 FOR UPDATE WAIT 3;
ERROR en línea 1:
ORA-00054: Failed to acquire a lock (Type: "TX", Name: "Transaction", ...) because it is currently held by another session. ...
ORA-40097: Failure to acquire row lock, currently held by transaction ID 8.5.562
HORA
--------
23:05:23
Ha esperado 3 segundos y ha desistido. En Oracle 26ai el error es también ORA-00054; en versiones anteriores, al agotarse el WAIT n se obtenía ORA-30006: resource busy; acquire with WAIT timeout expired.
Sesión B, SKIP LOCKED:
SELECT emp_id, nombre, salario FROM empleados WHERE oficio = 'VENDEDOR' FOR UPDATE SKIP LOCKED;
EMP_ID NOMBRE SALARIO
---------- ---------- ----------
112 Elena 2300
113 Sergio 1900
2 filas seleccionadas.
De los cuatro vendedores, devuelve solo los dos libres. Es la técnica ideal para colas de trabajo con varios procesos (ejercicio 11).
Sesión B, FOR UPDATE OF en un join:
SELECT e.emp_id, e.nombre, d.nombre AS departamento
FROM empleados e JOIN departamentos d ON d.dept_id = e.dept_id
WHERE e.emp_id IN (112, 113)
FOR UPDATE OF e.salario NOWAIT;
ROLLBACK;
EMP_ID NOMBRE DEPARTAMENTO
---------- ---------- ----------------------------------------
112 Elena VENTAS
113 Sergio VENTAS
2 filas seleccionadas.
Rollback terminado.
Solo se bloquean las filas de EMPLEADOS; la fila del departamento 20 queda libre. Sin OF, se bloquearían las filas de ambas tablas.
Por último, B pide un FOR UPDATE normal sobre David (111) a las 23:05:23 y espera hasta que A hace ROLLBACK a las 23:05:30:
[B] FOR UPDATE espera
HORA
--------
23:05:23
EMP_ID NOMBRE SALARIO
---------- ---------- ----------
111 David 2100
HORA
--------
23:05:30
⚠️ Importante:
FOR UPDATEno se puede combinar conDISTINCT,GROUP BY, funciones de agregado ni operadores de conjuntos: Oracle necesita saber qué filas concretas bloquear.
8.4. LOCK TABLE
Con LOCK TABLE bloqueas una tabla entera de forma explícita. Rara vez es necesario, pero conviene conocer sus modos:
LOCK TABLE tabla IN modo MODE [NOWAIT | WAIT n];
| Modo | Abreviatura | Permite a otros | Uso |
|---|---|---|---|
ROW SHARE | RS | Todo salvo EXCLUSIVE | Indicar intención de bloquear filas |
ROW EXCLUSIVE | RX | DML concurrente; impide SHARE y superiores | El que toman automáticamente INSERT, UPDATE, DELETE |
SHARE | S | Leer y otros SHARE; impide DML | Foto estable de una tabla para varias sesiones |
SHARE ROW EXCLUSIVE | SRX | Leer; impide DML y SHARE | Como SHARE, pero solo una sesión |
EXCLUSIVE | X | Solo leer | Uso exclusivo de la tabla |
Compatibilidad (¿puede B obtener el modo de la columna si A tiene el de la fila?):
| A tiene \ B pide | RS | RX | S | SRX | X |
|---|---|---|---|---|---|
| RS | Sí | Sí | Sí | Sí | No |
| RX | Sí | Sí | No | No | No |
| S | Sí | No | Sí | No | No |
| SRX | Sí | No | No | No | No |
| X | No | No | No | No | No |
💻 Ejemplo (dos sesiones): A bloquea
DEPARTAMENTOSen modoEXCLUSIVEdurante 8 segundos.
| Instante | Sesión A | Sesión B |
|---|---|---|
| 23:05:40 | LOCK TABLE departamentos IN EXCLUSIVE MODE; | |
| 23:05:42 | SELECT COUNT(*) → inmediato, 7 | |
| 23:05:42 | LOCK TABLE ... SHARE MODE NOWAIT → ORA-00054 | |
| 23:05:42 | LOCK TABLE ... SHARE MODE WAIT 2 → ORA-00054 a las 23:05:44 | |
| 23:05:44 | UPDATE departamentos ... → espera | |
| 23:05:48 | ROLLBACK; | el UPDATE termina |
[B] SELECT
COUNT(*)
----------
7
[B] LOCK SHARE NOWAIT
LOCK TABLE departamentos IN SHARE MODE NOWAIT
*
ERROR en línea 1:
ORA-00054: Failed to acquire a lock (Type: "TM", Name: "DML", Description: "Synchronizes accesses to an object") because it is currently held by another session. The resource being locked can be
identified by 73308 ("Table") and 0 ("operation")
[B] LOCK SHARE WAIT 2
HORA
--------
23:05:42
LOCK TABLE departamentos IN SHARE MODE WAIT 2
*
ERROR en línea 1:
ORA-00054: Failed to acquire a lock (Type: "TM", Name: "DML", ...) because it is currently held by another session. ...
HORA
--------
23:05:44
[B] UPDATE (espera)
1 fila actualizada.
HORA
--------
23:05:48
Fíjate en que ahora el error menciona un bloqueo de tipo TM (de tabla), no TX. Incluso con la tabla en modo EXCLUSIVE, la lectura no espera.
8.5. Ver los bloqueos: V$LOCK y V$SESSION
Los administradores consultan los bloqueos en las vistas dinámicas V$LOCK (bloqueos actuales: tipo TX/TM, modo que se tiene LMODE, modo que se pide REQUEST, y si bloquea a otros BLOCK = 1) y V$SESSION (columna BLOCKING_SESSION: qué sesión bloquea a cada una). Un usuario normal no tiene acceso:
SELECT sid, type, lmode, request, block FROM v$lock WHERE type IN ('TX', 'TM');
SELECT sid, type, lmode, request, block FROM v$lock WHERE type IN ('TX', 'TM')
*
ERROR en línea 1:
ORA-00942: table or view "SYS"."V_$LOCK" does not exist
Con el privilegio adecuado (por ejemplo el rol SELECT_CATALOG_ROLE, que se concede con DCL, módulo 37), durante la espera de la sección 8.2 verías una fila TX con LMODE = 6 y BLOCK = 1 para la sesión A, otra TX con REQUEST = 6 para la sesión B, y una TM en modo 3 (RX) sobre EMPLEADOS para cada una. En SQL Developer tienes la misma información en Herramientas → Supervisar sesiones.
💡 Consejo: Afortunadamente, desde 23ai el propio mensaje
ORA-00054ya te dice el tipo de bloqueo y la transacción que lo tiene, lo que facilita mucho el diagnóstico sin privilegios de administrador.
9. Interbloqueos (deadlocks)
9.1. Concepto
📘 Concepto: Un interbloqueo (deadlock) es una espera circular: la transacción A espera un recurso que tiene B, y B espera un recurso que tiene A. Ninguna puede avanzar por sí sola.
flowchart LR
A["Transacción A<br/>tiene la fila 110"] -- "espera la fila 111" --> B["Transacción B<br/>tiene la fila 111"]
B -- "espera la fila 110" --> A
Oracle detecta los interbloqueos automáticamente (en pocos segundos; en nuestras pruebas, entre 4 y 5) y los resuelve eligiendo a una de las sesiones: deshace la sentencia que estaba esperando (no la transacción entera) y le devuelve ORA-00060. La otra sesión sigue esperando hasta que la “víctima” termine su transacción.
9.2. Demostración con dos sesiones
| Instante | Sesión A | Sesión B |
|---|---|---|
| 23:06:00 | UPDATE ... emp_id = 110; (bloquea 110) | |
| 23:06:01 | UPDATE ... emp_id = 111; (bloquea 111) | |
| 23:06:03 | UPDATE ... emp_id = 111; → espera a B | |
| 23:06:04 | UPDATE ... emp_id = 110; → espera a A: ciclo | |
| 23:06:08 | recibe ORA-00060 | sigue esperando |
| 23:06:11 | ROLLBACK; | su UPDATE termina |
Salida real de A:
[A] UPDATE 110
HORA
--------
23:06:00
1 fila actualizada.
[A] UPDATE 111
HORA
--------
23:06:03
UPDATE empleados SET salario = salario + 10 WHERE emp_id = 111
*
ERROR en línea 1:
ORA-00060: deadlock detected while waiting for resource
HORA
--------
23:06:08
[A] ROLLBACK
HORA
--------
23:06:11
Rollback terminado.
Salida real de B:
[B] UPDATE 111
HORA
--------
23:06:01
1 fila actualizada.
[B] UPDATE 110
HORA
--------
23:06:04
1 fila actualizada.
HORA
--------
23:06:11
EMP_ID SALARIO
---------- ----------
110 2220
111 2120
A, que fue la primera en quedarse esperando, recibe ORA-00060 unos segundos después. Pero atención: tras el error, A sigue teniendo bloqueada la fila 110 (solo se deshizo su segunda sentencia), así que B no avanza hasta que A hace ROLLBACK a las 23:06:11. Entonces B completa su trabajo (110: 2200 + 20; 111: 2100 + 20).
⚠️ Importante: Al recibir
ORA-00060, la aplicación debe hacerROLLBACK(liberando sus bloqueos) y, normalmente, reintentar la transacción. Si en vez de eso se queda esperando o sigue trabajando, la otra sesión seguirá bloqueada. Oracle además escribe un fichero de traza con el detalle del interbloqueo para el administrador.
9.3. Cómo evitarlos
| Medida | Explicación |
|---|---|
| Acceder a los recursos siempre en el mismo orden | Si todas las transacciones actualizan primero la fila de menor clave, no puede formarse un ciclo |
| Transacciones cortas | Menos tiempo con bloqueos, menos probabilidad de cruce |
| Bloquear todo lo necesario al principio | Un SELECT ... FOR UPDATE de todas las filas implicadas antes de modificar |
Usar NOWAIT o WAIT n donde sea razonable | Convierte una espera potencialmente infinita en un error controlable |
| Indexar las claves ajenas | En Oracle, una FK sin índice obliga a bloquear la tabla hija entera al borrar o modificar la clave del padre, lo que multiplica los conflictos |
10. Flashback Query: consultar el pasado
10.1. Concepto
Gracias al undo, Oracle puede reconstruir cómo estaban los datos hace un rato, aunque ya se hayan confirmado cambios posteriores. Es la consulta retrospectiva (Flashback Query):
SELECT ... FROM tabla AS OF TIMESTAMP instante [WHERE ...];
SELECT ... FROM tabla AS OF SCN número [WHERE ...];
Es la herramienta perfecta para el “¡he borrado lo que no debía y ya he hecho COMMIT!”. No necesita privilegios especiales sobre tus propias tablas.
10.2. Demostración
Primero dejamos a Ana con un saldo redondo de 1000 € y confirmamos:
UPDATE cuentas_demo SET saldo = 1000 WHERE cuenta_id = 1;
COMMIT;
1 fila actualizada.
Confirmación terminada.
Poco después, consultamos la tabla y, unos segundos más tarde, alguien comete un error grave y lo confirma:
SELECT TO_CHAR(SYSTIMESTAMP, 'HH24:MI:SS') AS hora FROM dual;
SELECT * FROM cuentas_demo ORDER BY cuenta_id;
-- ...unos segundos después, un error grave y confirmado:
UPDATE cuentas_demo SET saldo = saldo * 2;
DELETE FROM cuentas_demo WHERE cuenta_id = 3;
COMMIT;
SELECT * FROM cuentas_demo ORDER BY cuenta_id;
HORA
--------
23:11:03
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
1 Ana García 1000
2 Luis Martínez 800
3 Marta Sánchez 200
3 filas seleccionadas.
3 filas actualizadas.
1 fila suprimida.
Confirmación terminada.
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
1 Ana García 2000
2 Luis Martínez 1600
2 filas seleccionadas.
¿Cómo estaba la tabla hace 4 segundos?
SELECT *
FROM cuentas_demo AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '4' SECOND)
ORDER BY cuenta_id;
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
1 Ana García 1000
2 Luis Martínez 800
3 Marta Sánchez 200
3 filas seleccionadas.
Una tabla “del pasado” se puede combinar con la actual, por ejemplo para ver qué ha cambiado:
SELECT a.cuenta_id, a.saldo AS saldo_antes, d.saldo AS saldo_ahora
FROM cuentas_demo AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '4' SECOND) a
LEFT JOIN cuentas_demo d ON d.cuenta_id = a.cuenta_id
ORDER BY a.cuenta_id;
CUENTA_ID SALDO_ANTES SALDO_AHORA
---------- ----------- -----------
1 1000 2000
2 800 1600
3 200 (nulo)
3 filas seleccionadas.
Y para reparar el daño con DML normal:
-- Recuperar la fila borrada
INSERT INTO cuentas_demo
SELECT * FROM cuentas_demo AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '4' SECOND)
WHERE cuenta_id = 3;
-- Deshacer el UPDATE masivo
UPDATE cuentas_demo c
SET c.saldo = (SELECT a.saldo
FROM cuentas_demo AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '4' SECOND) a
WHERE a.cuenta_id = c.cuenta_id)
WHERE c.cuenta_id IN (1, 2);
COMMIT;
SELECT * FROM cuentas_demo ORDER BY cuenta_id;
1 fila creada.
2 filas actualizadas.
Confirmación terminada.
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
1 Ana García 1000
2 Luis Martínez 800
3 Marta Sánchez 200
3 filas seleccionadas.
10.3. Límites
- Solo llega hasta donde llegue el undo disponible (normalmente minutos u horas, según
UNDO_RETENTION). Si pides un momento demasiado antiguo:
SELECT * FROM cuentas_demo AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '2' DAY);
SELECT * FROM cuentas_demo AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '2' DAY)
*
ERROR en línea 1:
ORA-08180: no snapshot found based on specified time
- No atraviesa cambios de estructura (DDL) de la tabla: si se modificó su definición después del instante pedido, da
ORA-01466: unable to read data - table definition has changed. - La correspondencia entre hora y SCN tiene una granularidad de unos segundos: para precisión absoluta, usa
AS OF SCN. - Oracle ofrece más tecnologías Flashback (consulta de versiones
VERSIONS BETWEEN,FLASHBACK TABLE ... TO TIMESTAMP,FLASHBACK TABLE ... TO BEFORE DROPque viste con la papelera en el módulo 25), pero exigen privilegios o configuración que corresponden al administrador.
11. Autocommit en las herramientas
11.1. SQL*Plus y SQLcl
Por defecto está desactivado. Con SET AUTOCOMMIT ON (o SET AUTOCOMMIT n para confirmar cada n sentencias), cada sentencia DML se confirma inmediatamente:
SELECT saldo FROM cuentas_demo WHERE cuenta_id = 1;
SHOW AUTOCOMMIT
SET AUTOCOMMIT ON
SHOW AUTOCOMMIT
UPDATE cuentas_demo SET saldo = saldo + 1 WHERE cuenta_id = 1;
ROLLBACK;
SELECT saldo FROM cuentas_demo WHERE cuenta_id = 1;
SET AUTOCOMMIT OFF
SALDO
----------
1000
1 fila seleccionada.
autocommit OFF
autocommit IMMEDIATE
1 fila actualizada.
Confirmación terminada.
Rollback terminado.
SALDO
----------
1001
1 fila seleccionada.
Observa el Confirmación terminada. que aparece solo tras el UPDATE: el ROLLBACK posterior ya no tiene nada que deshacer. Y otra sesión ve el cambio al instante: lo comprobamos con una sesión A en autocommit que sumó 50 € a Ana y siguió conectada, mientras una sesión B leía:
[B] lee
CUENTA_ID TITULAR SALDO
---------- -------------------- ----------
1 Ana García 1050
1 fila seleccionada.
Terminamos con la tabla de cuentas:
DROP TABLE cuentas_demo PURGE;
Tabla borrada.
11.2. SQL Developer
En SQL Developer el autocommit también está desactivado por defecto. Se configura en Herramientas → Preferencias → Base de datos → Avanzada → Confirmación automática. Con él desactivado:
- Los botones Confirmar (F11) y Rollback (F12) de la hoja de trabajo terminan la transacción.
- Al cerrar la conexión con cambios pendientes, te pregunta qué hacer.
- Las ventanas de edición de tablas (pestaña Datos) también trabajan en transacción: los cambios no se guardan hasta que pulsas Confirmar.
11.3. ¿Autocommit sí o no?
| Con autocommit | Sin autocommit |
|---|---|
| Cada sentencia es una transacción | Tú decides dónde empieza y acaba cada unidad de trabajo |
| No se pueden agrupar operaciones (la transferencia bancaria quedaría a medias) | Puedes agrupar y deshacer |
Un UPDATE sin WHERE es irreversible (salvo Flashback) | Un UPDATE sin WHERE se arregla con ROLLBACK |
| Cómodo para scripts de carga sencillos | Lo recomendado para trabajar a mano y en aplicaciones |
⚠️ Importante: Muchos entornos de programación (JDBC, ODBC, la mayoría de librerías de Python y PHP) arrancan con autocommit activado. En una aplicación seria, desactívalo y gestiona las transacciones explícitamente.
12. Buenas prácticas con transacciones
| Práctica | Motivo |
|---|---|
| Haz las transacciones cortas | Menos tiempo bloqueando filas, menos esperas y menos interbloqueos |
| Nunca esperes al usuario dentro de una transacción | Si el usuario se va a por un café con una fila bloqueada, todos los demás esperan. Lee los datos, deja que el usuario piense sin transacción abierta, y luego modifica con bloqueo optimista |
| Agrupa en una transacción lo que es una unidad de negocio | El pedido y sus líneas, la transferencia completa |
No hagas COMMIT dentro de un bucle fila a fila | Es lento (cada COMMIT espera a disco) y rompe la atomicidad del proceso |
Tras un error, decide explícitamente ROLLBACK o reintento | Oracle no aborta la transacción por ti |
Usa actualizaciones relativas (x = x + n) | Evitan la actualización perdida sin bloqueos explícitos |
| Accede a las filas siempre en el mismo orden | Previene interbloqueos |
Usa NOWAIT/WAIT n en procesos interactivos | El usuario recibe un mensaje en lugar de quedarse colgado |
Usa SKIP LOCKED en colas de trabajo | Varios procesos trabajan en paralelo sin pisarse |
Usa READ ONLY para informes | Cifras coherentes sin bloquear a nadie |
| No mezcles DDL y DML | El DDL confirma lo pendiente |
| Indexa las claves ajenas | Evita bloqueos de tabla en la hija |
💡 Consejo: El patrón profesional para formularios de edición es: (1) leer la fila y su “versión” (una columna numérica que se incrementa en cada cambio, o la marca
ORA_ROWSCN); (2) el usuario edita sin transacción abierta; (3)UPDATE ... SET ..., version = version + 1 WHERE id = :id AND version = :version_leida; (4) si se actualizan 0 filas, otro usuario modificó el registro: avisar y recargar.
13. Ejercicios resueltos
En los ejercicios con dos sesiones, las horas son las reales de la ejecución. En todos los casos el esquema EMPRESA se ha restaurado al terminar.
Ejercicio 1: Laberinto de savepoints
Partiendo de EMPRESA original, se ejecuta esta secuencia. ¿Qué queda en la base de datos? Razónalo antes de ver la solución.
INSERT INTO departamentos (dept_id, nombre, localidad) VALUES (80, 'CALIDAD', 'Málaga');
SAVEPOINT a;
UPDATE empleados SET dept_id = 80 WHERE emp_id = 160;
SAVEPOINT b;
DELETE FROM asignaciones WHERE proyecto_id = 4;
SAVEPOINT c;
UPDATE departamentos SET presupuesto = 90000 WHERE dept_id = 80;
ROLLBACK TO b;
INSERT INTO asignaciones (emp_id, proyecto_id, horas, funcion) VALUES (160, 5, 10, 'Becario');
ROLLBACK TO c;
COMMIT;
Ver solución
1 fila creada.
Punto de grabación creado.
1 fila actualizada.
Punto de grabación creado.
3 filas suprimidas.
Punto de grabación creado.
1 fila actualizada.
Rollback terminado.
1 fila creada.
ROLLBACK TO c
*
ERROR en línea 1:
ORA-01086: savepoint 'C' never established in this session or is invalid
Confirmación terminada.
SELECT dept_id, nombre, presupuesto FROM departamentos WHERE dept_id = 80;
SELECT emp_id, dept_id FROM empleados WHERE emp_id = 160;
SELECT COUNT(*) AS asig_p4 FROM asignaciones WHERE proyecto_id = 4;
SELECT COUNT(*) AS asig_160 FROM asignaciones WHERE emp_id = 160;
DEPT_ID NOMBRE PRESUPUESTO
---------- ---------------------- -----------
80 CALIDAD (nulo)
EMP_ID DEPT_ID
---------- ----------
160 80
ASIG_P4
----------
3
ASIG_160
----------
1
ROLLBACK TO bdeshace elDELETEy elUPDATEdel presupuesto, y elimina el savepointc, que era posterior ab.- Por eso
ROLLBACK TO cfalla conORA-01086y no deshace nada: la inserción de la asignación del becario se mantiene. - El
COMMITconfirma: el departamento 80 (sin presupuesto), Tomás en el 80, las 3 asignaciones del proyecto 4 intactas y la nueva asignación de Tomás.
(Después se restauró el esquema borrando esos cambios.)
Ejercicio 2: El índice que confirmó un error
Un compañero ejecuta esto. ¿Cuál es el presupuesto final del departamento 40 y por qué?
UPDATE departamentos SET presupuesto = presupuesto * 2 WHERE dept_id = 40;
CREATE INDEX ix_emp_oficio_demo ON empleados (oficio);
ROLLBACK;
Ver solución
1 fila actualizada.
Índice creado.
Rollback terminado.
SELECT dept_id, presupuesto FROM departamentos WHERE dept_id = 40;
DEPT_ID PRESUPUESTO
---------- -----------
40 300000
1 fila seleccionada.
El presupuesto se ha duplicado (de 150 000 a 300 000) de forma permanente: CREATE INDEX es DDL y hace un COMMIT implícito antes de ejecutarse, así que el ROLLBACK ya no tenía nada que deshacer. Habría que corregirlo con otro UPDATE (o con Flashback Query). Limpieza: DROP INDEX ix_emp_oficio_demo; y UPDATE ... SET presupuesto = 150000 ...; COMMIT;.
Ejercicio 3: Transacción con errores intermedios
¿Qué queda confirmado tras esta secuencia?
INSERT INTO categorias VALUES (12, 'Gaming', 1);
INSERT INTO categorias VALUES (13, 'Gaming', 2);
UPDATE productos SET categoria_id = 12 WHERE producto_id = 2;
UPDATE productos SET categoria_id = 13 WHERE producto_id = 5;
COMMIT;
Ver solución
1 fila creada.
INSERT INTO categorias VALUES (13, 'Gaming', 2)
*
ERROR en línea 1:
ORA-00001: unique constraint (CURSO.UQ_CAT_NOMBRE) violated on table CURSO.CATEGORIAS columns (NOMBRE)
ORA-03301: (ORA-00001 details) row with column values (NOMBRE:'Gaming') already exists
1 fila actualizada.
UPDATE productos SET categoria_id = 13 WHERE producto_id = 5
*
ERROR en línea 1:
ORA-02291: integrity constraint (CURSO.FK_PRO_CATEGORIA) violated - parent key not found
Confirmación terminada.
SELECT categoria_id, nombre FROM categorias WHERE categoria_id >= 12;
SELECT producto_id, categoria_id FROM productos WHERE producto_id IN (2, 5);
CATEGORIA_ID NOMBRE
------------ ----------------------
12 Gaming
PRODUCTO_ID CATEGORIA_ID
----------- ------------
2 12
5 3
Cada sentencia fallida se deshace sola (atomicidad de sentencia), pero las correctas permanecen y el COMMIT las confirma: existe la categoría 12 y el producto 2 está en ella; la categoría 13 no existe y el producto 5 sigue en la 3. Si las cuatro sentencias formaban una unidad, lo correcto tras el primer error era ROLLBACK.
Ejercicio 4: La trampa del autocommit
En SQL*Plus se ejecuta lo siguiente. ¿Cuál es el salario final de Tomás (160)?
SET AUTOCOMMIT ON
UPDATE empleados SET salario = 9999 WHERE emp_id = 160;
ROLLBACK;
Ver solución
1 fila actualizada.
Confirmación terminada.
Rollback terminado.
SELECT emp_id, salario FROM empleados WHERE emp_id = 160;
EMP_ID SALARIO
---------- ----------
160 9999
1 fila seleccionada.
Con autocommit activado, el UPDATE se confirma en el mismo momento (lo delata el Confirmación terminada.); el ROLLBACK no deshace nada. El becario cobra 9999 € hasta que alguien lo corrija (se restauró a 900 y se volvió a SET AUTOCOMMIT OFF).
Ejercicio 5: Borrar un pedido mientras otro le añade líneas
La sesión A borra el pedido 5008 (y, por la cascada, su línea) sin confirmar. Mientras tanto, la sesión B consulta sus líneas e intenta añadirle una nueva. Describe qué ve B, si espera, y cómo termina.
Ver solución
| Instante | Sesión A | Sesión B |
|---|---|---|
| 23:14:10 | DELETE FROM pedidos WHERE pedido_id = 5008; | |
| 23:14:12 | SELECT COUNT(*) ... pedido_id = 5008 → 1 | |
| 23:14:12 | INSERT INTO lineas_pedido (... 5008 ...) → espera | |
| 23:14:16 | COMMIT; | el INSERT falla: ORA-02291 |
Sesión A:
[A] t0 borra el pedido 5008 (y en cascada su linea)
HORA
--------
23:14:10
1 fila suprimida.
LINEAS_5008
-----------
0
[A] t6 COMMIT
HORA
--------
23:14:16
Confirmación terminada.
Sesión B:
[B] t2 consulta las lineas del pedido 5008
HORA
--------
23:14:12
LINEAS_5008
-----------
1
[B] t2 intenta anadir una linea al pedido 5008
INSERT INTO lineas_pedido (pedido_id, num_linea, producto_id, cantidad, precio_unitario)
*
ERROR en línea 1:
ORA-02291: integrity constraint (CURSO.FK_LIN_PEDIDO) violated - parent key not found
HORA
--------
23:14:16
- B sigue viendo la línea (1) porque el borrado de A no está confirmado.
- Para insertar una hija, Oracle debe comprobar que el padre existe; como la fila padre está bloqueada por un borrado pendiente, no puede decidir y espera a que A termine.
- A confirma: el padre ya no existe y B recibe
ORA-02291. Si A hubiera hechoROLLBACK, elINSERTde B habría tenido éxito.
Ejercicio 6: Dos altas con la misma clave
A y B dan de alta a la vez un departamento con el número 80. B comprueba antes con un SELECT que no existe. ¿Qué ocurre?
Ver solución
| Instante | Sesión A | Sesión B |
|---|---|---|
| 23:14:23 | INSERT ... (80, 'CALIDAD', 'Málaga'); | |
| 23:14:24 | SELECT COUNT(*) ... dept_id = 80 → 0 | |
| 23:14:24 | INSERT ... (80, 'AUDITORÍA', 'Cádiz'); → espera | |
| 23:14:28 | COMMIT; | ORA-00001 |
[B] t1 comprueba si existe el 80
EXISTE_80
----------
0
[B] t1 inserta tambien el 80
INSERT INTO departamentos (dept_id, nombre, localidad) VALUES (80, 'AUDITORÍA', 'Cádiz')
*
ERROR en línea 1:
ORA-00001: unique constraint (CURSO.PK_DEPARTAMENTOS) violated on table CURSO.DEPARTAMENTOS columns (DEPT_ID)
ORA-03301: (ORA-00001 details) row with column values (DEPT_ID:80) already exists
HORA
--------
23:14:28
- El
SELECTde B dice que no existe (la fila de A no está confirmada): comprobar antes de insertar no sirve para evitar duplicados en concurrencia. - El
INSERTde B espera porque el índice de la clave primaria ya tiene una entrada pendiente para 80; Oracle no sabe aún si A confirmará. - A confirma y B recibe
ORA-00001. Si A hubiera deshecho, B habría insertado sin problemas. - Conclusión: la integridad la garantizan las restricciones (y las secuencias o identidades para generar claves), no las comprobaciones previas de la aplicación.
Ejercicio 7: Bloqueo optimista
Dos sesiones venden unidades del monitor 4K (producto 10, stock 12). Cada una lee el stock, calcula y escribe con la condición AND stock = valor_leído. A vende 2 y B vende 5. ¿Cómo detecta B el conflicto y cuál es el stock final?
Ver solución
-- Sesión A
SELECT producto_id, stock FROM productos WHERE producto_id = 10; -- 12
UPDATE productos SET stock = 10 WHERE producto_id = 10 AND stock = 12; -- 12 - 2
COMMIT;
-- Sesión B
SELECT producto_id, stock FROM productos WHERE producto_id = 10; -- 12
UPDATE productos SET stock = 7 WHERE producto_id = 10 AND stock = 12; -- 12 - 5
| Instante | Sesión A | Sesión B |
|---|---|---|
| 23:14:36 | lee 12 | |
| 23:14:38 | UPDATE ... AND stock = 12 → 1 fila | lee 12 |
| 23:14:40 | COMMIT; | UPDATE ... AND stock = 12 → 0 filas |
| 23:14:40 | vuelve a leer: 10; UPDATE ... SET stock = 5 ... AND stock = 10 → 1 fila; COMMIT |
Salida de B:
[B] t1 lee stock
PRODUCTO_ID STOCK
----------- ----------
10 12
[B] t3 vende 5 (bloqueo optimista)
HORA
--------
23:14:40
0 filas actualizadas.
[B] vuelve a leer y reintenta
PRODUCTO_ID STOCK
----------- ----------
10 10
1 fila actualizada.
Confirmación terminada.
PRODUCTO_ID STOCK
----------- ----------
10 5
Cuando el UPDATE de B se ejecuta, la fila ya tiene stock 10 confirmado (si A no hubiera confirmado aún, B habría esperado y luego Oracle habría reevaluado la condición con el valor confirmado). La condición stock = 12 ya no se cumple: 0 filas actualizadas. es la señal de conflicto. B relee y reintenta: stock final 5 (12 − 2 − 5), sin actualizaciones perdidas y sin haber bloqueado nada mientras “pensaba”. Al terminar se restauró el stock a 12.
Ejercicio 8: Un informe que no cuadra
La sesión B genera un informe en dos pasos: primero el total de salarios de la empresa y, cuatro segundos después, el desglose por departamentos. Entre ambos pasos, A sube un 10 % el salario de Informática y confirma. Compara lo que obtiene B en READ COMMITTED y en READ ONLY.
Ver solución
En READ COMMITTED (por defecto):
[B] t0 informe parte 1: total de la empresa
TOTAL_EMPRESA
-------------
67900
[B] t4 informe parte 2: desglose por departamento
DEPT_ID TOTAL
---------- ----------
10 6500
20 15000
30 20680
40 8450
50 9300
60 8950
(nulo) 900
7 filas seleccionadas.
SUMA_DESGLOSE
-------------
69780
El desglose suma 69 780 € pero el total decía 67 900 €: el informe es incoherente porque la segunda consulta ya ve la subida de A (18 800 → 20 680 en el departamento 30). Es una lectura no repetible a nivel de informe.
Con SET TRANSACTION READ ONLY; como primera sentencia de B:
Transacción definida.
[B] t0 informe parte 1: total de la empresa
TOTAL_EMPRESA
-------------
67900
[B] t4 informe parte 2: desglose por departamento
SUMA_DESGLOSE
-------------
67900
Ambas consultas ven la misma foto (la del inicio de la transacción) y el informe cuadra. A no tuvo que esperar en ningún caso. (La fila con DEPT_ID vacío es el becario sin departamento.)
Ejercicio 9: ORA-08177 y reintento
B trabaja en SERIALIZABLE: lee la comisión de Carmen y, cuatro segundos después, intenta sumarle 100 €. Entre medias, A modifica el salario de Carmen y confirma. Muestra qué pasa y cómo debe reaccionar B.
Ver solución
-- Sesión B
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT emp_id, salario, comision FROM empleados WHERE emp_id = 110;
-- (A: UPDATE empleados SET salario = 2500 WHERE emp_id = 110; COMMIT;)
UPDATE empleados SET comision = comision + 100 WHERE emp_id = 110;
Transacción definida.
[B] t0 lee la comision de 110
HORA
--------
23:16:46
EMP_ID SALARIO COMISION
---------- ---------- ----------
110 2200 500
[B] t4 intenta modificar 110
UPDATE empleados SET comision = comision + 100 WHERE emp_id = 110
*
ERROR en línea 1:
ORA-08177: can't serialize access for this transaction
Aunque B modifica una columna distinta (comision), la fila cambió después de que empezara su transacción. La reacción correcta es deshacer y repetir la transacción completa:
ROLLBACK;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT emp_id, salario, comision FROM empleados WHERE emp_id = 110;
UPDATE empleados SET comision = comision + 100 WHERE emp_id = 110;
SELECT emp_id, salario, comision FROM empleados WHERE emp_id = 110;
ROLLBACK;
Rollback terminado.
Transacción definida.
EMP_ID SALARIO COMISION
---------- ---------- ----------
110 2500 500
1 fila actualizada.
EMP_ID SALARIO COMISION
---------- ---------- ----------
110 2500 600
Rollback terminado.
En el reintento, B ya parte de una foto que incluye el cambio de A (salario 2500) y su modificación se acepta.
Ejercicio 10: Interbloqueo entre departamentos
A traspasa 1000 € de presupuesto del departamento 20 al 10 actualizando primero el 10 y luego el 20. B traspasa 500 € del 10 al 20 actualizando primero el 20 y luego el 10. Ambas empiezan casi a la vez. ¿Qué ocurre? ¿Cómo lo evitarías?
Ver solución
| Instante | Sesión A | Sesión B |
|---|---|---|
| 23:16:57 | UPDATE ... dept_id = 10 (bloquea 10) | |
| 23:16:58 | UPDATE ... dept_id = 20 (bloquea 20) | |
| 23:17:00 | UPDATE ... dept_id = 20 → espera | |
| 23:17:01 | UPDATE ... dept_id = 10 → espera: ciclo | |
| 23:17:04 | ORA-00060 | sigue esperando |
| 23:17:06 | ROLLBACK; | su UPDATE termina |
Sesión A:
[A] t3 UPDATE departamento 20
HORA
--------
23:17:00
UPDATE departamentos SET presupuesto = presupuesto - 1000 WHERE dept_id = 20
*
ERROR en línea 1:
ORA-00060: deadlock detected while waiting for resource
HORA
--------
23:17:04
[A] estado de su transaccion
DEPT_ID PRESUPUESTO
---------- -----------
10 501000
20 350000
Tras el ORA-00060, A conserva su primer cambio (10 → 501 000) y su bloqueo. Al hacer ROLLBACK, B termina:
[B] t4 UPDATE departamento 10
HORA
--------
23:17:01
1 fila actualizada.
HORA
--------
23:17:06
DEPT_ID PRESUPUESTO
---------- -----------
10 499500
20 350500
Prevención: que todas las transacciones actualicen los departamentos en el mismo orden (por ejemplo, siempre de menor a mayor dept_id) o que bloqueen ambos al principio con SELECT ... WHERE dept_id IN (10, 20) FOR UPDATE. Así la segunda sesión espera al principio y nunca se forma el ciclo.
Ejercicio 11: Cola de trabajo con SKIP LOCKED
Tenemos una tabla cola_tareas con 5 tareas PENDIENTE. Dos procesos trabajadores toman tareas a la vez: el primero bloquea las tareas 1 a 3. Muestra qué le pasa al segundo con NOWAIT y con SKIP LOCKED.
Ver solución
CREATE TABLE cola_tareas (
tarea_id NUMBER(4) CONSTRAINT pk_cola_tareas PRIMARY KEY,
descripcion VARCHAR2(60) NOT NULL,
estado VARCHAR2(10) DEFAULT 'PENDIENTE' NOT NULL,
trabajador VARCHAR2(10)
);
INSERT INTO cola_tareas (tarea_id, descripcion) VALUES (1, 'Enviar factura pedido 5020');
INSERT INTO cola_tareas (tarea_id, descripcion) VALUES (2, 'Enviar factura pedido 5021');
INSERT INTO cola_tareas (tarea_id, descripcion) VALUES (3, 'Preparar envío pedido 5022');
INSERT INTO cola_tareas (tarea_id, descripcion) VALUES (4, 'Preparar envío pedido 5023');
INSERT INTO cola_tareas (tarea_id, descripcion) VALUES (5, 'Preparar envío pedido 5024');
COMMIT;
Trabajador A:
SELECT tarea_id, descripcion
FROM cola_tareas
WHERE estado = 'PENDIENTE' AND tarea_id <= 3
FOR UPDATE SKIP LOCKED;
UPDATE cola_tareas SET estado = 'EN CURSO', trabajador = 'A'
WHERE tarea_id <= 3 AND estado = 'PENDIENTE';
-- ...procesa durante 5 segundos y COMMIT
TAREA_ID DESCRIPCION
---------- ------------------------------
1 Enviar factura pedido 5020
2 Enviar factura pedido 5021
3 Preparar envío pedido 5022
3 filas seleccionadas.
3 filas actualizadas.
Trabajador B, mientras A procesa:
SELECT tarea_id, descripcion FROM cola_tareas WHERE estado = 'PENDIENTE' FOR UPDATE NOWAIT;
SELECT tarea_id, descripcion FROM cola_tareas WHERE estado = 'PENDIENTE' FOR UPDATE SKIP LOCKED;
UPDATE cola_tareas SET estado = 'EN CURSO', trabajador = 'B' WHERE tarea_id > 3 AND estado = 'PENDIENTE';
COMMIT;
SELECT tarea_id, descripcion FROM cola_tareas WHERE estado = 'PENDIENTE' FOR UPDATE NOWAIT
*
ERROR en línea 1:
ORA-00054: Failed to acquire a lock (Type: "TX", Name: "Transaction", ...) because it is currently held by another session. ...
ORA-40097: Failure to acquire row lock, currently held by transaction ID 1.13.572
TAREA_ID DESCRIPCION
---------- ------------------------------
4 Preparar envío pedido 5023
5 Preparar envío pedido 5024
2 filas seleccionadas.
2 filas actualizadas.
Confirmación terminada.
Estado final, cuando ambos han confirmado:
TAREA_ID DESCRIPCION ESTADO TRABAJADOR
---------- ------------------------------ ---------- ----------
1 Enviar factura pedido 5020 EN CURSO A
2 Enviar factura pedido 5021 EN CURSO A
3 Preparar envío pedido 5022 EN CURSO A
4 Preparar envío pedido 5023 EN CURSO B
5 Preparar envío pedido 5024 EN CURSO B
Con NOWAIT el trabajador B falla porque alguna de las filas pedidas estaba bloqueada; con SKIP LOCKED toma las libres y los dos procesos trabajan en paralelo sin repartirse la misma tarea. (Al final: DROP TABLE cola_tareas PURGE;.)
Ejercicio 12: Inventario con LOCK TABLE … SHARE
La sesión A quiere calcular el inventario (unidades y valor) con la garantía de que nadie vende nada mientras tanto, pero sin impedir que otros consulten. ¿Qué modo de LOCK TABLE usarías? Comprueba qué pueden hacer otras sesiones.
Ver solución
El modo SHARE: permite leer y que otras sesiones también obtengan SHARE, pero impide cualquier DML.
-- Sesión A
LOCK TABLE productos IN SHARE MODE;
SELECT SUM(stock) AS unidades, SUM(stock * precio) AS valor FROM productos;
-- ...5 segundos de cálculos...
SELECT SUM(stock) AS unidades, SUM(stock * precio) AS valor FROM productos;
COMMIT;
Tabla(s) bloqueada(s).
UNIDADES VALOR
---------- ----------
688 140701
UNIDADES VALOR
---------- ----------
688 140701
HORA
--------
23:17:26
Confirmación terminada.
Sesión B, mientras tanto:
SELECT COUNT(*) FROM productos;
LOCK TABLE productos IN SHARE MODE NOWAIT;
COMMIT;
UPDATE productos SET stock = stock - 1 WHERE producto_id = 6;
ROLLBACK;
COUNT(*)
----------
20
Tabla(s) bloqueada(s).
Confirmación terminada.
[B] t1 intenta vender (UPDATE)
HORA
--------
23:17:23
1 fila actualizada.
HORA
--------
23:17:26
Rollback terminado.
B lee sin problemas e incluso obtiene su propio bloqueo SHARE (son compatibles), pero su UPDATE espera de 23:17:23 a 23:17:26, cuando A termina. Si solo se necesitara que las lecturas de A cuadraran, bastaría con SET TRANSACTION READ ONLY, sin bloquear a nadie; LOCK TABLE ... SHARE tiene sentido cuando A, además, va a escribir basándose en esos datos.
Ejercicio 13: Recuperar empleados borrados con Flashback Query
Sobre una copia de EMPLEADOS (emp_copia_demo), alguien borra por error a todo el departamento de Ventas y confirma. Recupera las filas con Flashback Query y comprueba que la copia vuelve a coincidir con el original.
Ver solución
CREATE TABLE emp_copia_demo AS SELECT * FROM empleados;
-- ...un rato después...
DELETE FROM emp_copia_demo WHERE dept_id = 20;
COMMIT;
SELECT COUNT(*) FROM emp_copia_demo;
Tabla creada.
6 filas suprimidas.
Confirmación terminada.
COUNT(*)
----------
18
SELECT emp_id, nombre, oficio
FROM emp_copia_demo AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '5' SECOND)
WHERE dept_id = 20
ORDER BY emp_id;
EMP_ID NOMBRE OFICIO
---------- ------------ ------------------------------
101 Luis DIRECTOR
110 Carmen VENDEDOR
111 David VENDEDOR
112 Elena VENDEDOR
113 Sergio VENDEDOR
114 Lucía ADMINISTRATIVO
6 filas seleccionadas.
INSERT INTO emp_copia_demo
SELECT *
FROM emp_copia_demo AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '5' SECOND)
WHERE dept_id = 20;
COMMIT;
SELECT COUNT(*) FROM emp_copia_demo;
SELECT COUNT(*) AS diferencias FROM (
SELECT * FROM empleados
MINUS
SELECT * FROM emp_copia_demo
);
DROP TABLE emp_copia_demo PURGE;
6 filas creadas.
Confirmación terminada.
COUNT(*)
----------
24
DIFERENCIAS
-----------
0
Tabla borrada.
MINUS confirma que no falta ninguna fila. En la práctica, en lugar de “hace 5 segundos” se usa una hora concreta: AS OF TIMESTAMP TO_TIMESTAMP('2026-10-03 23:17:50', 'YYYY-MM-DD HH24:MI:SS').
Ejercicio 14: Identifica la anomalía
Para cada planificación (las transacciones T1 y T2 se ejecutan en ese orden de pasos), indica qué problema de concurrencia aparece y si podría ocurrir en Oracle con el nivel por defecto.
| Paso | a) | b) | c) | d) |
|---|---|---|---|---|
| 1 | T1 lee x = 10 | T1: UPDATE x = 50 (sin confirmar) | T1 cuenta filas con estado = 'P': 4 | T1 lee x = 10 |
| 2 | T2 lee x = 10 | T2 lee x | T2 inserta una fila 'P' y confirma | T2: UPDATE x = 20, confirma |
| 3 | T1 escribe x = 10 + 5, confirma | T1: ROLLBACK | T1 vuelve a contar: 5 | T1 lee x |
| 4 | T2 escribe x = 10 + 3, confirma |
Ver solución
| Caso | Problema | ¿En Oracle READ COMMITTED? |
|---|---|---|
| a) | Actualización perdida: el resultado final es 13 y se pierde el +5 de T1 | Sí, porque los cálculos se hacen fuera con valores leídos antes (demostrado en 6.1). Se evita con x = x + n, FOR UPDATE o bloqueo optimista |
| b) | Lectura sucia: si T2 leyera 50, habría leído un valor que nunca existió | No: T2 lee 10 (la versión confirmada); demostrado en 5.2 |
| c) | Lectura fantasma: aparece una fila nueva que cumple la condición | Sí (demostrado en 6.4). No ocurre en SERIALIZABLE ni READ ONLY |
| d) | Lectura no repetible: T1 lee 10 y luego 20 | Sí (demostrado en 6.3). No ocurre en SERIALIZABLE ni READ ONLY |
Ejercicio 15: Comparativa de niveles
Completa la tabla indicando, para cada nivel de aislamiento de Oracle, qué ve la segunda lectura de B en los escenarios de las secciones 6.3 y 6.4, y si B puede modificar la fila 110 tras el cambio confirmado de A.
| Nivel de B | 2.ª lectura del salario | 2.º recuento del dept. 30 | UPDATE de la fila 110 |
|---|---|---|---|
READ COMMITTED | ? | ? | ? |
SERIALIZABLE | ? | ? | ? |
READ ONLY | ? | ? | ? |
Ver solución
| Nivel de B | 2.ª lectura del salario | 2.º recuento del dept. 30 | UPDATE de la fila 110 |
|---|---|---|---|
READ COMMITTED | 2500 (ve el cambio) | 7 (ve el fantasma) | Sí (espera si A no ha confirmado, y se aplica sobre el valor confirmado) |
SERIALIZABLE | 2200 (foto inicial) | 6 (foto inicial) | No: ORA-08177 (hay que reintentar) |
READ ONLY | 2200 (foto inicial) | 6 (foto inicial) | No: ORA-01456, no se permite DML |
Todos los valores proceden de las ejecuciones reales de las secciones 6 y 7.
Ejercicio 16: ¿Se guardó o no?
Explica qué valor de saldo verá una sesión nueva en cada caso, sabiendo que la sesión 1 ejecuta UPDATE cuentas_demo SET saldo = 999 WHERE cuenta_id = 3; sin COMMIT y luego:
- Escribe
EXITen SQL*Plus con la configuración por defecto. - Escribe
SET EXITCOMMIT OFFy despuésEXIT. - Su proceso se cuelga y el administrador lo mata.
- Ejecuta
CREATE VIEW v AS SELECT * FROM cuentas_demo;y luegoROLLBACK.
Ver solución
- 999:
EXITCOMMITestá aONpor defecto y SQL*Plus confirma al salir (comprobado en 3.4). - 200: con
EXITCOMMIT OFF, elEXITdeshace (comprobado en 3.4). - 200: la sesión muere de forma anormal y PMON deshace su transacción y libera el bloqueo (comprobado en 3.5).
- 999:
CREATE VIEWes DDL, confirma lo pendiente antes de ejecutarse; elROLLBACKno tiene nada que deshacer (mismo mecanismo que en 3.3 y en el ejercicio 2).
14. Ejercicios propuestos
Ejercicio 1: Transferencia robusta
Escribe la secuencia de sentencias de una transferencia entre dos cuentas de cuentas_demo que: bloquee primero ambas cuentas en orden de cuenta_id con FOR UPDATE NOWAIT, compruebe el saldo, haga los dos UPDATE y confirme. ¿Qué debe hacer el programa si recibe ORA-00054 o ORA-02290?
Ejercicio 2: Cronología con tres sesiones
Diseña un escenario con tres sesiones (A, B y C) sobre la fila del empleado 113 en el que C espere a B y B espere a A. Ejecútalo con dos sesiones en segundo plano y una en primer plano, anota las horas y dibuja la tabla de cronología. ¿En qué orden se desbloquean?
Ejercicio 3: Informe mensual coherente
Escribe un informe de ventas (total por cliente y total general, a partir de PEDIDOS y LINEAS_PEDIDO) que deba cuadrar aunque se registren pedidos mientras se ejecuta. ¿Qué sentencia pondrías al principio? ¿Por qué no usarías LOCK TABLE?
Ejercicio 4: Sincronización con savepoints
Diseña un proceso que inserte tres pedidos nuevos con sus líneas, marcando un savepoint antes de cada pedido. Provoca un error en las líneas del segundo pedido (por ejemplo, un producto inexistente), vuelve a su savepoint y confirma los otros dos. Comprueba el resultado.
Ejercicio 5: Versión de fila
Añade a una copia de PRODUCTOS una columna version NUMBER DEFAULT 0 NOT NULL y escribe el UPDATE de bloqueo optimista que modifica el precio solo si la versión no ha cambiado, incrementándola. Pruébalo con dos sesiones.
Ejercicio 6: Investiga
Busca en la documentación de Oracle (Database Concepts, capítulo Data Concurrency and Consistency) qué es un bloqueo de tabla hija por clave ajena sin índice y explica con un ejemplo de EMPRESA en qué situación aparecería.
Resumen
| Concepto | Sintaxis / idea clave |
|---|---|
| Transacción | Unidad lógica de trabajo: todo o nada |
| ACID | Atomicidad (undo), Consistencia (restricciones), Aislamiento (multiversión + bloqueos), Durabilidad (redo en el COMMIT) |
| Inicio | Implícito con el primer DML; opcional SET TRANSACTION como primera sentencia |
| Confirmar / deshacer | COMMIT; / ROLLBACK; |
| Savepoint | SAVEPOINT s; … ROLLBACK TO [SAVEPOINT] s; (borra los posteriores) |
COMMIT implícito | Cualquier DDL (incluso si falla en ejecución); EXIT en SQL*Plus con EXITCOMMIT ON |
ROLLBACK implícito | Caída de la sesión o del servidor; EXIT con EXITCOMMIT OFF |
| Atomicidad de sentencia | Una sentencia que falla se deshace entera; la transacción sigue abierta |
| Lectura consistente | Cada consulta ve lo confirmado al empezar; lectores y escritores no se bloquean |
| Anomalías | Actualización perdida, lectura sucia (nunca en Oracle), no repetible, fantasma |
| Niveles de Oracle | READ COMMITTED (defecto), SERIALIZABLE (ORA-08177), READ ONLY (ORA-01456 si escribes) |
| Elegir nivel | SET TRANSACTION ISOLATION LEVEL ...; / ALTER SESSION SET ISOLATION_LEVEL = ...; |
| Bloqueos | TX (fila), TM (tabla); se liberan al terminar la transacción; nunca escalan |
| Bloqueo pesimista | SELECT ... FOR UPDATE [OF col] [NOWAIT \| WAIT n \| SKIP LOCKED]; |
| Bloqueo de tabla | LOCK TABLE t IN {ROW SHARE \| ROW EXCLUSIVE \| SHARE \| SHARE ROW EXCLUSIVE \| EXCLUSIVE} MODE [NOWAIT]; |
| Errores de bloqueo | ORA-00054 (ocupado), ORA-00060 (interbloqueo), ORA-08177 (serialización) |
| Bloqueo optimista | UPDATE ... WHERE id = :id AND col = :valor_leido → 0 filas = conflicto |
| Flashback Query | SELECT ... FROM t AS OF TIMESTAMP (SYSTIMESTAMP - INTERVAL '5' MINUTE); |
| Autocommit | SET AUTOCOMMIT ON/OFF (SQL*Plus); Preferencias en SQL Developer; mejor desactivado |
| Regla de oro | Transacciones cortas, nunca esperar al usuario dentro de una transacción |