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, COMMIT implícito por DDL o al salir y ROLLBACK implícito ante una caída
  • Usar SAVEPOINT y ROLLBACK TO SAVEPOINT para 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, SERIALIZABLE y READ ONLY) con SET TRANSACTION
  • Manejar los bloqueos de Oracle: bloqueos de fila (TX) y de tabla (TM), SELECT ... FOR UPDATE con NOWAIT, WAIT n y SKIP LOCKED, y LOCK 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 errores ORA- 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 ROLLBACK y luego haces COMMIT, 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_saldo impidió 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: BEGIN en Oracle abre un bloque PL/SQL, no una transacción. Si vienes de otro SGBD y escribes BEGIN 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 (COMMIT o ROLLBACK) 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 nombre deshace 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 TO no termina la transacción ni libera los bloqueos adquiridos antes del savepoint (sí los adquiridos después).
  • COMMIT y ROLLBACK completos 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:

  1. Nadie ve los cambios no confirmados de otros (no existen las lecturas sucias).
  2. Los lectores no bloquean a los escritores: un SELECT no pone bloqueos, así que nunca impide un UPDATE.
  3. Los escritores no bloquean a los lectores: un UPDATE pendiente 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 COMMIT de 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ámetro UNDO_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 SERIALIZABLE de 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 UPDATE no se puede combinar con DISTINCT, 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 DEPARTAMENTOS en modo EXCLUSIVE durante 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-00054 ya 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 hacer ROLLBACK (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 DROP que 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 b deshace el DELETE y el UPDATE del presupuesto, y elimina el savepoint c, que era posterior a b.
  • Por eso ROLLBACK TO c falla con ORA-01086 y no deshace nada: la inserción de la asignación del becario se mantiene.
  • El COMMIT confirma: 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 hecho ROLLBACK, el INSERT de 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 SELECT de B dice que no existe (la fila de A no está confirmada): comprobar antes de insertar no sirve para evitar duplicados en concurrencia.
  • El INSERT de 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:

  1. Escribe EXIT en SQL*Plus con la configuración por defecto.
  2. Escribe SET EXITCOMMIT OFF y después EXIT.
  3. Su proceso se cuelga y el administrador lo mata.
  4. Ejecuta CREATE VIEW v AS SELECT * FROM cuentas_demo; y luego ROLLBACK.
Ver solución
  1. 999: EXITCOMMIT está a ON por defecto y SQL*Plus confirma al salir (comprobado en 3.4).
  2. 200: con EXITCOMMIT OFF, el EXIT deshace (comprobado en 3.4).
  3. 200: la sesión muere de forma anormal y PMON deshace su transacción y libera el bloqueo (comprobado en 3.5).
  4. 999: CREATE VIEW es DDL, confirma lo pendiente antes de ejecutarse; el ROLLBACK no 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