Módulo 38: Inserción, modificación y borrado

Objetivos del módulo

  • Comprender qué hacen las sentencias de modificación de datos (INSERT, UPDATE, DELETE y MERGE) y en qué se diferencian de SELECT
  • Insertar filas con y sin lista de columnas, usando valores por defecto (DEFAULT), NULL explícito, secuencias y columnas identidad
  • Insertar varias filas a la vez: INSERT multifila de Oracle 23ai, INSERT ALL, INSERT FIRST e INSERT ... SELECT
  • Modificar filas con UPDATE: una o varias columnas, expresiones, CASE, subconsultas en SET y en WHERE, actualizaciones correlacionadas y de varias columnas a la vez
  • Conocer el UPDATE y el DELETE con join directo (FROM) de Oracle 23ai y sus alternativas clásicas (vistas en línea y MERGE)
  • Borrar filas con DELETE y entender su interacción con las claves ajenas (ORA-02292, ON DELETE CASCADE y ON DELETE SET NULL)
  • Sincronizar tablas con MERGE (upsert), incluida la cláusula DELETE WHERE
  • Recuperar valores modificados con RETURNING ... INTO desde SQL*Plus y conocer sus límites
  • Reconocer los errores más habituales del DML (con su código ORA- real) y aplicar buenas prácticas profesionales

1. El DML de modificación

Hasta ahora (módulos 27 a 33) has usado el DML (Data Manipulation Language, lenguaje de manipulación de datos) solo para leer: la sentencia SELECT. En este módulo aprenderás la otra mitad: las sentencias que cambian el contenido de las tablas.

📘 Concepto: Las sentencias de modificación de datos alteran las filas almacenadas en las tablas, pero no su estructura (eso es DDL). Son cuatro:

  • INSERT: añade filas nuevas.
  • UPDATE: cambia el valor de columnas en filas existentes.
  • DELETE: elimina filas existentes.
  • MERGE: combina inserción y actualización (y opcionalmente borrado) en una sola sentencia.
Sentencia Opera sobre Resultado típico en SQL*Plus ¿Necesita COMMIT?
INSERT Filas nuevas 1 fila creada. / 3 filas creadas. Sí
UPDATE Filas que cumplen el WHERE 6 filas actualizadas. Sí
DELETE Filas que cumplen el WHERE 2 filas suprimidas. Sí
MERGE Filas de destino emparejadas o no con un origen 5 filas fusionadas. Sí
TRUNCATE (DDL) Todas las filas Tabla truncada. No (confirma solo)

Una analogía: si una tabla es un archivador, SELECT es consultar las fichas, INSERT es meter fichas nuevas, UPDATE es tachar y corregir datos en fichas existentes, DELETE es sacar fichas y tirarlas, y MERGE es repasar una lista de cambios y, para cada línea, corregir la ficha si ya existe o crear una nueva si no.

1.1. Las modificaciones son transaccionales

En Oracle todas las sentencias DML forman parte de una transacción (lo estudiarás a fondo en el módulo 35). Los cambios no son definitivos hasta que ejecutas COMMIT, y puedes deshacerlos con ROLLBACK. Por eso en este módulo verás que casi todos los ejemplos terminan con:

ROLLBACK;
Rollback terminado.

Así puedes probar sin miedo: el esquema EMPRESA vuelve a quedar como estaba.

⚠️ Importante: Cualquier sentencia DDL (CREATE, ALTER, DROP, TRUNCATE…) hace un COMMIT implícito antes y después de ejecutarse. Si haces un UPDATE y luego un CREATE TABLE, el UPDATE ya no se puede deshacer. En los ejemplos de este módulo, cuando se crean tablas auxiliares, se hace siempre antes de modificar los datos de EMPRESA o después del ROLLBACK.

1.2. Sobre las salidas de este módulo

Todas las sentencias se han ejecutado en Oracle AI Database 26ai Free con SQL*Plus sobre el esquema EMPRESA recién cargado (scripts empresa_crear.sql y empresa_datos.sql), con la configuración de sesión habitual del curso:

SET NULL '(nulo)'
ALTER SESSION SET NLS_LANGUAGE = 'SPANISH';
ALTER SESSION SET NLS_TERRITORY = 'SPAIN';
ALTER SESSION SET NLS_DATE_FORMAT = 'DD/MM/YYYY';
Sesión modificada.

Sesión modificada.

Sesión modificada.

Los nulos se muestran como (nulo) y los decimales con coma. Los mensajes de SQL*Plus aparecen en español (1 fila creada., 3 filas actualizadas., Rollback terminado.), pero el texto de los errores ORA- se deja en inglés, porque la edición Free solo incluye ese idioma (ORA-02292: integrity constraint ... violated). Los códigos ORA- son los mismos en todos los idiomas.

💡 Consejo: Si quieres que tus resultados coincidan exactamente con los del curso, recarga el esquema EMPRESA antes de empezar con empresa_borrar.sql, empresa_crear.sql y empresa_datos.sql, y no hagas COMMIT de los ejemplos.


2. INSERT: añadir filas

2.1. Sintaxis general

-- Forma 1: una fila con valores literales
INSERT INTO tabla [(columna1, columna2, ...)]
VALUES (valor1, valor2, ...);

-- Forma 2: las filas que devuelve una consulta
INSERT INTO tabla [(columna1, columna2, ...)]
SELECT ...;

Reglas básicas:

  • Debe haber tantos valores como columnas (las de la lista o, si no hay lista, todas las de la tabla).
  • Cada valor debe ser compatible con el tipo de su columna (o convertible implícitamente).
  • La fila resultante debe cumplir todas las restricciones de la tabla (NOT NULL, PRIMARY KEY, UNIQUE, FOREIGN KEY, CHECK). Si incumple una sola, la sentencia falla entera.

2.2. INSERT con lista de columnas (la forma recomendada)

💻 Ejemplo: Damos de alta el departamento de calidad indicando explícitamente las columnas.

INSERT INTO departamentos (dept_id, nombre, localidad, presupuesto)
VALUES (80, 'CALIDAD', 'Málaga', 120000);

SELECT * FROM departamentos WHERE dept_id = 80;
1 fila creada.

   DEPT_ID NOMBRE                                   LOCALIDAD                                PRESUPUESTO
---------- ---------------------------------------- ---------------------------------------- -----------
        80 CALIDAD                                  Málaga                                        120000

1 fila seleccionada.

Escribir la lista de columnas tiene tres ventajas:

  1. Legibilidad: quien lea la sentencia sabe qué valor va a cada columna sin consultar la estructura de la tabla.
  2. Robustez: si mañana alguien añade una columna a la tabla (ALTER TABLE ... ADD), la sentencia sigue funcionando.
  3. Flexibilidad: puedes omitir columnas opcionales y escribirlas en el orden que quieras.

2.3. INSERT sin lista de columnas

Si omites la lista, Oracle espera un valor para cada columna de la tabla y en el orden en que se definieron (el que muestra DESC departamentos).

INSERT INTO departamentos
VALUES (90, 'FORMACIÓN', 'Granada', 60000);
1 fila creada.

Funciona, pero es frágil. Fíjate en lo que ocurre cuando te equivocas en el orden o en el número de valores:

-- Orden equivocado: localidad y nombre intercambiados y texto en presupuesto
INSERT INTO departamentos
VALUES (91, 'Murcia', 'JURÍDICO', 'mucho');
VALUES (91, 'Murcia', 'JURÍDICO', 'mucho')
                                  *
ERROR en línea 2:
ORA-01722: unable to convert string value containing 'm' to a number:
ORA-03302: (ORA-01722 details) invalid string value: mucho
-- Faltan valores (la tabla tiene 4 columnas)
INSERT INTO departamentos VALUES (92, 'JURÍDICO', 'Murcia');

-- Sobran valores respecto a la lista
INSERT INTO departamentos (dept_id, nombre) VALUES (92, 'JURÍDICO', 'Murcia');
INSERT INTO departamentos VALUES (92, 'JURÍDICO', 'Murcia')
            *
ERROR en línea 1:
ORA-00947: not enough values

INSERT INTO departamentos (dept_id, nombre) VALUES (92, 'JURÍDICO', 'Murcia')
            *
ERROR en línea 1:
ORA-00913: too many values

⚠️ Importante: Lo más peligroso no es el error, sino el caso en que no hay error: si intercambias dos columnas del mismo tipo (por ejemplo nombre y localidad, ambas VARCHAR2(40)), Oracle inserta la fila sin protestar y los datos quedan mal. Usa siempre lista de columnas en código que vaya a durar.

2.4. Valores nulos: implícitos y explícitos

Una columna que no aparece en la lista recibe su valor por defecto; si no tiene, recibe NULL. También puedes escribir NULL explícitamente.

-- NULL implícito: localidad y presupuesto no aparecen en la lista
INSERT INTO departamentos (dept_id, nombre) VALUES (93, 'COMPRAS');

-- NULL explícito
INSERT INTO departamentos VALUES (94, 'SEGURIDAD', NULL, NULL);

SELECT * FROM departamentos WHERE dept_id >= 80 ORDER BY dept_id;
1 fila creada.

1 fila creada.

   DEPT_ID NOMBRE                                   LOCALIDAD                                PRESUPUESTO
---------- ---------------------------------------- ---------------------------------------- -----------
        80 CALIDAD                                  Málaga                                        120000
        90 FORMACIÓN                                Granada                                        60000
        93 COMPRAS                                  (nulo)                                   (nulo)
        94 SEGURIDAD                                (nulo)                                   (nulo)

4 filas seleccionadas.

Las dos formas producen lo mismo cuando la columna no tiene DEFAULT. La diferencia aparece cuando sí lo tiene, como verás enseguida.

💡 Consejo: En Oracle, la cadena vacía '' se trata como NULL. INSERT ... VALUES (95, 'X', '', NULL) deja localidad a NULL. Es una peculiaridad de Oracle que no comparten otros SGBD (en PostgreSQL o SQL Server '' y NULL son distintos).

2.5. Valores por defecto: la palabra clave DEFAULT

La tabla CLIENTES define fecha_alta DATE DEFAULT SYSDATE y limite_credito NUMBER(10,2) DEFAULT 3000. El valor por defecto se aplica:

  • cuando la columna no aparece en la lista de columnas, o
  • cuando escribes la palabra clave DEFAULT en lugar del valor.

Pero no se aplica si escribes NULL: un NULL explícito es un valor, y Oracle lo respeta.

-- 1015: fecha_alta y limite_credito omitidas -> toman su DEFAULT
INSERT INTO clientes (cliente_id, nombre, nif, ciudad, provincia)
VALUES (1015, 'Librería Cervantes', 'B11223344', 'Cuenca', 'Cuenca');

-- 1016: DEFAULT explícito
INSERT INTO clientes (cliente_id, nombre, nif, fecha_alta, limite_credito)
VALUES (1016, 'Óptica Visión', 'B22334455', DEFAULT, DEFAULT);

-- 1017: NULL explícito en una columna con DEFAULT
INSERT INTO clientes (cliente_id, nombre, nif, limite_credito)
VALUES (1017, 'Bar Tito', 'B33445566', NULL);

SELECT cliente_id, nombre, fecha_alta, limite_credito, comercial_id
FROM   clientes
WHERE  cliente_id >= 1015
ORDER  BY cliente_id;
1 fila creada.

1 fila creada.

1 fila creada.

CLIENTE_ID NOMBRE                                                       FECHA_ALTA LIMITE_CREDITO COMERCIAL_ID
---------- ------------------------------------------------------------ ---------- -------------- ------------
      1015 Librería Cervantes                                           03/10/2026           3000 (nulo)
      1016 Óptica Visión                                                03/10/2026           3000 (nulo)
      1017 Bar Tito                                                     03/10/2026 (nulo)         (nulo)

3 filas seleccionadas.

El cliente 1017 tiene limite_credito a NULL porque lo pedimos explícitamente. Y si la columna con DEFAULT es además NOT NULL, el NULL explícito provoca un error:

INSERT INTO clientes (cliente_id, nombre, nif, fecha_alta)
VALUES (1018, 'Kiosco Luna', 'B44556677', NULL);
VALUES (1018, 'Kiosco Luna', 'B44556677', NULL)
                                          *
ERROR en línea 2:
ORA-01400: cannot insert NULL into ("CURSO"."CLIENTES"."FECHA_ALTA")

DEFAULT también se puede usar en UPDATE para devolver una columna a su valor por defecto:

UPDATE clientes SET limite_credito = DEFAULT WHERE cliente_id = 1017;
SELECT cliente_id, limite_credito FROM clientes WHERE cliente_id = 1017;
ROLLBACK;
1 fila actualizada.

CLIENTE_ID LIMITE_CREDITO
---------- --------------
      1017           3000

1 fila seleccionada.

Rollback terminado.

📘 Concepto: Desde Oracle 12c existe DEFAULT ON NULL en la definición de la columna (lo viste en el módulo 23): con él, el valor por defecto se aplica también cuando se inserta un NULL explícito. Las columnas de EMPRESA usan el DEFAULT clásico.

2.6. Fechas y conversiones al insertar

Para insertar fechas usa siempre un literal DATE 'AAAA-MM-DD' o TO_DATE con formato explícito. Nunca dependas del formato por defecto de la sesión:

-- Literal ANSI (sin hora: las 00:00)
INSERT INTO empleados (emp_id, nombre, apellidos, email, fecha_alta, oficio, salario, jefe_id, dept_id)
VALUES (170, 'Olga', 'Prieto Sanz', 'olga.prieto@empresa.es', DATE '2026-10-01', 'PROGRAMADOR', 2300, 120, 30);

-- TO_DATE con hora
INSERT INTO empleados (emp_id, nombre, apellidos, email, fecha_alta, oficio, salario, jefe_id, dept_id)
VALUES (171, 'Félix', 'Mora Gil', 'felix.mora@empresa.es',
        TO_DATE('15/10/2026 09:30', 'DD/MM/YYYY HH24:MI'), 'ANALISTA', 2900, 120, 30);

-- Cadena sin formato: depende de NLS_DATE_FORMAT (aquí DD/MM/YYYY) y falla
INSERT INTO empleados (emp_id, nombre, apellidos, email, fecha_alta, oficio, salario, jefe_id, dept_id)
VALUES (172, 'Noa', 'Vera Cid', 'noa.vera@empresa.es', '2026-10-20', 'ANALISTA', 2900, 120, 30);
1 fila creada.

1 fila creada.

VALUES (172, 'Noa', 'Vera Cid', 'noa.vera@empresa.es', '2026-10-20', 'ANALISTA', 2900, 120, 30)
                                                       *
ERROR en línea 2:
ORA-01861: literal does not match format string

SYSDATE inserta la fecha y la hora actuales del servidor:

INSERT INTO empleados (emp_id, nombre, apellidos, email, fecha_alta, oficio, salario, jefe_id, dept_id)
VALUES (172, 'Noa', 'Vera Cid', 'noa.vera@empresa.es', SYSDATE, 'ANALISTA', 2900, 120, 30);

SELECT emp_id, nombre, TO_CHAR(fecha_alta, 'DD/MM/YYYY HH24:MI') AS alta
FROM   empleados
WHERE  emp_id >= 170;

ROLLBACK;
1 fila creada.

    EMP_ID NOMBRE                         ALTA
---------- ------------------------------ ----------------
       170 Olga                           01/10/2026 00:00
       171 Félix                          15/10/2026 09:30
       172 Noa                            03/10/2026 22:44

3 filas seleccionadas.

Rollback terminado.

⚠️ Importante: Como SYSDATE lleva hora, una consulta posterior del tipo WHERE fecha_alta = DATE '2026-10-03' no encontrará a Noa. Si solo te interesa el día, inserta TRUNC(SYSDATE).

2.7. Insertar varias filas con una sentencia

INSERT multifila con VALUES (Oracle 23ai)

⚠️ Importante: Disponible desde Oracle 23ai.

Desde Oracle 23ai, la cláusula VALUES admite varias filas separadas por comas, como en MySQL, PostgreSQL o SQL Server (es la sintaxis del estándar SQL):

INSERT INTO departamentos (dept_id, nombre, localidad, presupuesto)
VALUES (80, 'CALIDAD',   'Málaga',  120000),
       (90, 'FORMACIÓN', 'Granada',  60000),
       (95, 'JURÍDICO',  'Murcia',   NULL);

SELECT * FROM departamentos WHERE dept_id >= 80;
ROLLBACK;
3 filas creadas.

   DEPT_ID NOMBRE                                   LOCALIDAD                                PRESUPUESTO
---------- ---------------------------------------- ---------------------------------------- -----------
        80 CALIDAD                                  Málaga                                        120000
        90 FORMACIÓN                                Granada                                        60000
        95 JURÍDICO                                 Murcia                                   (nulo)

3 filas seleccionadas.

Rollback terminado.

Es una sola sentencia, así que es atómica: si una fila incumple una restricción, no se inserta ninguna.

-- La segunda fila repite el nombre 'VENTAS' (restricción UNIQUE uq_dep_nombre)
INSERT INTO departamentos (dept_id, nombre, localidad)
VALUES (80, 'CALIDAD', 'Málaga'),
       (90, 'VENTAS',  'Granada');

SELECT COUNT(*) FROM departamentos WHERE dept_id = 80;
INSERT INTO departamentos (dept_id, nombre, localidad)
*
ERROR en línea 1:
ORA-00001: unique constraint (CURSO.UQ_DEP_NOMBRE) violated on table CURSO.DEPARTAMENTOS columns (NOMBRE)
ORA-03301: (ORA-00001 details) row with column values (NOMBRE:'VENTAS') already exists

  COUNT(*)
----------
         0

1 fila seleccionada.

El departamento 80, que era correcto, tampoco se ha insertado.

💡 Consejo: En 23ai la misma construcción VALUES sirve como “tabla de valores” dentro de una consulta: SELECT * FROM (VALUES (1, 'uno'), (2, 'dos')) t (n, texto); devuelve dos filas. Es útil para pruebas rápidas.

Alternativas clásicas (Oracle 19c y 21c)

En versiones anteriores a 23ai tienes tres opciones:

  1. Varios INSERT seguidos (lo más sencillo y lo que hacen los scripts de EMPRESA).
  2. INSERT ALL incondicional con una consulta de una fila sobre DUAL:
INSERT ALL
    INTO departamentos (dept_id, nombre, localidad) VALUES (80, 'CALIDAD',   'Málaga')
    INTO departamentos (dept_id, nombre, localidad) VALUES (90, 'FORMACIÓN', 'Granada')
SELECT * FROM dual;
2 filas creadas.
  1. INSERT ... SELECT con UNION ALL de varias filas sobre DUAL:
INSERT INTO departamentos (dept_id, nombre, localidad)
SELECT 95, 'JURÍDICO', 'Murcia' FROM dual UNION ALL
SELECT 96, 'COMPRAS',  'Vigo'   FROM dual;

SELECT * FROM departamentos WHERE dept_id >= 80 ORDER BY dept_id;
ROLLBACK;
2 filas creadas.

   DEPT_ID NOMBRE                                   LOCALIDAD                                PRESUPUESTO
---------- ---------------------------------------- ---------------------------------------- -----------
        80 CALIDAD                                  Málaga                                   (nulo)
        90 FORMACIÓN                                Granada                                  (nulo)
        95 JURÍDICO                                 Murcia                                   (nulo)
        96 COMPRAS                                  Vigo                                     (nulo)

4 filas seleccionadas.

Rollback terminado.
Técnica Versión Ventajas Inconvenientes
Varios INSERT ... VALUES Todas Sencillo, portable Muchas sentencias (más viajes al servidor)
VALUES (...), (...) 23ai+ Estándar, compacto, atómico No funciona en 19c/21c
INSERT ALL ... SELECT * FROM dual 9i+ Puede insertar en varias tablas Sintaxis verbosa
INSERT ... SELECT ... UNION ALL Todas Atómico, portable Hay que repetir FROM dual

2.8. INSERT … SELECT: copiar datos

La forma más potente de INSERT toma las filas de una consulta. Es la herramienta habitual para copiar datos entre tablas, guardar históricos o cargar tablas de resumen.

💻 Ejemplo: Guardamos una “foto” de los empleados de Informática en una tabla histórica.

CREATE TABLE empleados_hist (
    emp_id      NUMBER(6),
    nombre      VARCHAR2(30),
    apellidos   VARCHAR2(50),
    oficio      VARCHAR2(30),
    salario     NUMBER(8,2),
    dept_id     NUMBER(4),
    fecha_copia DATE DEFAULT SYSDATE
);

INSERT INTO empleados_hist (emp_id, nombre, apellidos, oficio, salario, dept_id)
SELECT emp_id, nombre, apellidos, oficio, salario, dept_id
FROM   empleados
WHERE  dept_id = 30;

SELECT emp_id, nombre, oficio, salario, dept_id FROM empleados_hist ORDER BY emp_id;
Tabla creada.

6 filas creadas.

    EMP_ID NOMBRE                         OFICIO                            SALARIO    DEPT_ID
---------- ------------------------------ ------------------------------ ---------- ----------
       102 Marta                          DIRECTOR                             5000         30
       120 Pablo                          ANALISTA                             3500         30
       121 Raquel                         PROGRAMADOR                          2800         30
       122 Iván                           PROGRAMADOR                          2600         30
       123 Sara                           PROGRAMADOR                          2400         30
       124 Hugo                           TÉCNICO SISTEMAS                     2500         30

6 filas seleccionadas.

Observa que:

  • No se escribe VALUES: la consulta ocupa su lugar. Tampoco van paréntesis alrededor del SELECT.
  • Las columnas de la consulta se asignan por posición a las de la lista, no por nombre.
  • fecha_copia no está en la lista, así que toma su DEFAULT (SYSDATE) en las seis filas.
  • El número de columnas debe coincidir; si no, el error es el mismo que con VALUES:
INSERT INTO empleados_hist (emp_id, nombre)
SELECT emp_id, nombre, apellidos FROM empleados WHERE dept_id = 40;
INSERT INTO empleados_hist (emp_id, nombre)
            *
ERROR en línea 1:
ORA-00913: too many values

Cargar una tabla de resumen

INSERT ... SELECT admite cualquier consulta: con joins, GROUP BY, funciones analíticas, subconsultas… Un uso típico es precalcular datos para informes.

💻 Ejemplo: Tabla de ventas mensuales (sin pedidos cancelados) calculada a partir de PEDIDOS y LINEAS_PEDIDO.

CREATE TABLE ventas_mensuales (
    anio        NUMBER(4),
    mes         NUMBER(2),
    num_pedidos NUMBER(5),
    importe     NUMBER(12,2),
    CONSTRAINT pk_ventas_mensuales PRIMARY KEY (anio, mes)
);

INSERT INTO ventas_mensuales (anio, mes, num_pedidos, importe)
SELECT EXTRACT(YEAR FROM p.fecha_pedido),
       EXTRACT(MONTH FROM p.fecha_pedido),
       COUNT(DISTINCT p.pedido_id),
       ROUND(SUM(l.cantidad * l.precio_unitario * (1 - l.descuento / 100)), 2)
FROM   pedidos p
       JOIN lineas_pedido l ON l.pedido_id = p.pedido_id
WHERE  p.estado <> 'CANCELADO'
GROUP  BY EXTRACT(YEAR FROM p.fecha_pedido), EXTRACT(MONTH FROM p.fecha_pedido);

SELECT * FROM ventas_mensuales ORDER BY anio, mes;
Tabla creada.

16 filas creadas.

      ANIO        MES NUM_PEDIDOS    IMPORTE
---------- ---------- ----------- ----------
      2025          1           2     9205,4
      2025          2           2     6791,4
      2025          3           2     1375,2
      2025          4           1    2539,13
      2025          5           1    4772,44
      2025          6           2    12656,5
      2025          9           1     1658,8
      2025         10           1       1555
...

16 filas seleccionadas.

📘 Concepto: CREATE TABLE ... AS SELECT (CTAS, módulo 23) crea la tabla y la rellena en un solo paso, pero copia solo las restricciones NOT NULL. INSERT ... SELECT rellena una tabla que ya existe, con las restricciones, valores por defecto y tipos que tú definiste. En producción se suele preferir crear la tabla con su DDL completo y luego cargarla con INSERT ... SELECT.

2.9. INSERT ALL e INSERT FIRST: inserción multitabla

Oracle permite que una sola consulta alimente varias tablas a la vez. Cada fila del SELECT se evalúa contra una o varias cláusulas INTO.

INSERT ALL incondicional

Cada fila de la consulta se inserta en todas las tablas de las cláusulas INTO:

CREATE TABLE contactos_email (cliente_id NUMBER(6), email VARCHAR2(60));
CREATE TABLE contactos_tel   (cliente_id NUMBER(6), telefono VARCHAR2(15));

INSERT ALL
    INTO contactos_email (cliente_id, email)    VALUES (cliente_id, email)
    INTO contactos_tel   (cliente_id, telefono) VALUES (cliente_id, telefono)
SELECT cliente_id, email, telefono
FROM   clientes
WHERE  provincia = 'Madrid';
Tabla creada.

Tabla creada.

4 filas creadas.

Hay dos clientes de Madrid (1003 y 1004) y dos cláusulas INTO: 2 × 2 = 4 filas. En los VALUES se usan los nombres de columna (o alias) de la consulta.

INSERT ALL condicional

Con WHEN condición THEN INTO ... cada fila va a las tablas cuyas condiciones cumple. Con INSERT ALL se evalúan todas las condiciones, así que una fila puede ir a varias tablas:

INSERT ALL
    WHEN email IS NOT NULL THEN
        INTO contactos_email (cliente_id, email)    VALUES (cliente_id, email)
    WHEN telefono IS NOT NULL THEN
        INTO contactos_tel   (cliente_id, telefono) VALUES (cliente_id, telefono)
SELECT cliente_id, email, telefono
FROM   clientes;

SELECT COUNT(*) FROM contactos_email;
SELECT COUNT(*) FROM contactos_tel;
ROLLBACK;
25 filas creadas.

  COUNT(*)
----------
        12

1 fila seleccionada.

  COUNT(*)
----------
        13

1 fila seleccionada.

Rollback terminado.

De los 14 clientes, 12 tienen email y 13 tienen teléfono: 25 filas en total, y los clientes que tienen ambos datos aparecen en las dos tablas.

INSERT FIRST

Con INSERT FIRST cada fila se inserta solo en la primera cláusula WHEN que cumple (como un CASE). Admite una rama ELSE final.

💻 Ejemplo: Clasificamos a los empleados por tramo salarial en tres tablas.

CREATE TABLE emp_salario_alto  (emp_id NUMBER(6), nombre VARCHAR2(30), salario NUMBER(8,2));
CREATE TABLE emp_salario_medio (emp_id NUMBER(6), nombre VARCHAR2(30), salario NUMBER(8,2));
CREATE TABLE emp_salario_bajo  (emp_id NUMBER(6), nombre VARCHAR2(30), salario NUMBER(8,2));

INSERT FIRST
    WHEN salario >= 4000 THEN
        INTO emp_salario_alto  VALUES (emp_id, nombre, salario)
    WHEN salario >= 2000 THEN
        INTO emp_salario_medio VALUES (emp_id, nombre, salario)
    ELSE
        INTO emp_salario_bajo  VALUES (emp_id, nombre, salario)
SELECT emp_id, nombre, salario
FROM   empleados;

SELECT 'ALTO' AS tramo, COUNT(*) AS n FROM emp_salario_alto
UNION ALL
SELECT 'MEDIO', COUNT(*) FROM emp_salario_medio
UNION ALL
SELECT 'BAJO', COUNT(*) FROM emp_salario_bajo;
ROLLBACK;
24 filas creadas.

TRAMO          N
----- ----------
ALTO           6
MEDIO         11
BAJO           7

3 filas seleccionadas.

Rollback terminado.

Cada uno de los 24 empleados ha ido a una tabla. Si cambias INSERT FIRST por INSERT ALL con las mismas condiciones, los que ganan 4000 o más cumplen también salario >= 2000 y se insertan dos veces:

30 filas creadas.

TRAMO          N
----- ----------
ALTO           6
MEDIO         17
BAJO           7

3 filas seleccionadas.
Variante Cada fila de origen va a… Uso típico
INSERT ALL sin WHEN Todas las tablas INTO Repartir columnas de una fila en varias tablas
INSERT ALL con WHEN Todas las tablas cuyas condiciones cumple Clasificar con categorías solapadas
INSERT FIRST Solo la primera tabla cuya condición cumple (o ELSE) Clasificar en categorías excluyentes

⚠️ Importante: En un INSERT multitabla no se puede usar una secuencia (NEXTVAL) dentro de la consulta de origen (error ORA-02287: sequence number not allowed here; sí se admite en las cláusulas VALUES), y una tabla destino no puede ser una vista ni estar en otra base de datos remota. Hay un máximo de 999 columnas destino en total entre todas las cláusulas INTO.

2.10. Claves generadas: secuencias y columnas identidad

Las tablas de EMPRESA usan claves “naturales” numéricas que escribimos a mano (100, 1001, 5001…). En una aplicación real, la clave primaria la genera la base de datos. Oracle ofrece dos mecanismos (los creaste en los módulos 23 y 26).

Secuencias

Una secuencia es un objeto independiente que entrega números consecutivos. NEXTVAL obtiene el siguiente; CURRVAL devuelve el último que obtuvo tu sesión.

CREATE SEQUENCE seq_clientes START WITH 1015 INCREMENT BY 1 NOCACHE;

INSERT INTO clientes (cliente_id, nombre, nif, ciudad)
VALUES (seq_clientes.NEXTVAL, 'Librería Cervantes', 'B11223344', 'Cuenca');

INSERT INTO clientes (cliente_id, nombre, nif, ciudad)
VALUES (seq_clientes.NEXTVAL, 'Óptica Visión', 'B22334455', 'Toledo');

SELECT seq_clientes.CURRVAL FROM dual;
SELECT cliente_id, nombre, ciudad FROM clientes WHERE cliente_id >= 1015;
ROLLBACK;
Secuencia creada.

1 fila creada.

1 fila creada.

   CURRVAL
----------
      1016

1 fila seleccionada.

CLIENTE_ID NOMBRE                                                       CIUDAD
---------- ------------------------------------------------------------ ----------------------------------------
      1015 Librería Cervantes                                           Cuenca
      1016 Óptica Visión                                                Toledo

2 filas seleccionadas.

Rollback terminado.

Tras el ROLLBACK los dos clientes desaparecen, pero la secuencia no retrocede:

SELECT seq_clientes.NEXTVAL FROM dual;
   NEXTVAL
----------
      1017

1 fila seleccionada.

⚠️ Importante: Las secuencias no son transaccionales: un número entregado no se devuelve aunque hagas ROLLBACK. Por eso las claves generadas pueden tener huecos (1015, 1016, 1019…). Es normal y no debe preocuparte: una clave primaria debe ser única, no consecutiva. Si un requisito legal exige numeración sin huecos (por ejemplo, facturas), no se resuelve con una secuencia.

CURRVAL es muy útil para insertar un padre y sus hijos en la misma sesión: el pedido obtiene su número con NEXTVAL y las líneas lo reutilizan con CURRVAL.

CREATE SEQUENCE seq_pedidos START WITH 6000 NOCACHE;

INSERT INTO pedidos (pedido_id, cliente_id, empleado_id, fecha_pedido)
VALUES (seq_pedidos.NEXTVAL, 1004, 111, DATE '2026-10-02');

INSERT INTO lineas_pedido (pedido_id, num_linea, producto_id, cantidad, precio_unitario)
VALUES (seq_pedidos.CURRVAL, 1, 7, 3, 79.90);

INSERT INTO lineas_pedido (pedido_id, num_linea, producto_id, cantidad, precio_unitario)
VALUES (seq_pedidos.CURRVAL, 2, 6, 3, 19.90);

SELECT p.pedido_id, p.estado, l.num_linea, l.producto_id, l.cantidad, l.descuento
FROM   pedidos p
       JOIN lineas_pedido l ON l.pedido_id = p.pedido_id
WHERE  p.pedido_id = 6000;
ROLLBACK;
Secuencia creada.

1 fila creada.

1 fila creada.

1 fila creada.

 PEDIDO_ID ESTADO        NUM_LINEA PRODUCTO_ID   CANTIDAD  DESCUENTO
---------- ------------ ---------- ----------- ---------- ----------
      6000 PENDIENTE             1           7          3          0
      6000 PENDIENTE             2           6          3          0

2 filas seleccionadas.

Rollback terminado.

El pedido toma estado = 'PENDIENTE' por defecto y las líneas descuento = 0, también por defecto.

Una secuencia también puede usarse en un INSERT ... SELECT (un número distinto por cada fila):

INSERT INTO contactos_email (cliente_id, email)
SELECT seq_clientes.NEXTVAL, email
FROM   clientes
WHERE  email IS NOT NULL AND ROWNUM <= 3;

SELECT * FROM contactos_email;
ROLLBACK;
3 filas creadas.

CLIENTE_ID EMAIL
---------- ------------------------------------------------------------
      1018 compras@talleresmartin.es
      1019 info@clevante.es
      1020 mjherrera@correo.es

3 filas seleccionadas.

Rollback terminado.

Columnas identidad

Una columna identidad (GENERATED ... AS IDENTITY, desde Oracle 12c) lleva una secuencia interna asociada. Hay tres variantes:

Definición ¿Se puede dar un valor? ¿Y NULL?
GENERATED ALWAYS AS IDENTITY No (error ORA-32795) No
GENERATED BY DEFAULT AS IDENTITY Sí No (error ORA-01400)
GENERATED BY DEFAULT ON NULL AS IDENTITY Sí Sí: genera el valor
CREATE TABLE incidencias (
    incidencia_id NUMBER GENERATED ALWAYS AS IDENTITY CONSTRAINT pk_incidencias PRIMARY KEY,
    descripcion   VARCHAR2(100) NOT NULL,
    fecha         DATE DEFAULT SYSDATE NOT NULL
);

INSERT INTO incidencias (descripcion) VALUES ('Impresora sin tóner');
INSERT INTO incidencias (descripcion) VALUES ('Portátil no arranca');
INSERT INTO incidencias (incidencia_id, descripcion) VALUES (10, 'Forzar id');
INSERT INTO incidencias (incidencia_id, descripcion) VALUES (DEFAULT, 'Con DEFAULT');

SELECT incidencia_id, descripcion FROM incidencias;
Tabla creada.

1 fila creada.

1 fila creada.

INSERT INTO incidencias (incidencia_id, descripcion) VALUES (10, 'Forzar id')
                         *
ERROR en línea 1:
ORA-32795: cannot insert into a generated always identity column

1 fila creada.

INCIDENCIA_ID DESCRIPCION
------------- ----------------------------------------------------------------------------------------------------
            1 Impresora sin tóner
            2 Portátil no arranca
            3 Con DEFAULT

3 filas seleccionadas.

Y con BY DEFAULT ON NULL:

CREATE TABLE tareas (
    tarea_id  NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY CONSTRAINT pk_tareas PRIMARY KEY,
    titulo    VARCHAR2(60) NOT NULL
);

INSERT INTO tareas (titulo) VALUES ('Automática');
INSERT INTO tareas (tarea_id, titulo) VALUES (100, 'Manual');
INSERT INTO tareas (tarea_id, titulo) VALUES (NULL, 'Nula');

SELECT * FROM tareas ORDER BY tarea_id;
Tabla creada.

1 fila creada.

1 fila creada.

1 fila creada.

  TAREA_ID TITULO
---------- ------------------------------------------------------------
         1 Automática
         2 Nula
       100 Manual

3 filas seleccionadas.

⚠️ Importante: Con BY DEFAULT, si insertas valores a mano puedes provocar colisiones futuras: cuando la secuencia interna llegue a 100, el siguiente INSERT automático fallará con ORA-00001. Si das valores manuales, que sea solo en cargas iniciales y luego reajusta la identidad con ALTER TABLE ... MODIFY tarea_id GENERATED BY DEFAULT ON NULL AS IDENTITY (START WITH LIMIT VALUE).

-- Limpieza de las tablas auxiliares de esta sección
DROP TABLE tareas PURGE;
DROP TABLE incidencias PURGE;
DROP SEQUENCE seq_clientes;
DROP SEQUENCE seq_pedidos;
DROP TABLE emp_salario_alto PURGE;
DROP TABLE emp_salario_medio PURGE;
DROP TABLE emp_salario_bajo PURGE;
DROP TABLE contactos_email PURGE;
DROP TABLE contactos_tel PURGE;
DROP TABLE empleados_hist PURGE;
DROP TABLE ventas_mensuales PURGE;

2.11. Errores típicos al insertar

Estos son los errores que más verás en clase y en el trabajo. Todos se han provocado sobre EMPRESA:

-- 1) Clave primaria duplicada (ya existe el empleado 100)
INSERT INTO empleados (emp_id, nombre, apellidos, email, fecha_alta, oficio, salario, dept_id)
VALUES (100, 'Otra', 'Ana', 'otra.ana@empresa.es', DATE '2026-10-01', 'BECARIO', 900, 30);
ERROR en línea 1:
ORA-00001: unique constraint (CURSO.PK_EMPLEADOS) violated on table CURSO.EMPLEADOS columns (EMP_ID)
ORA-03301: (ORA-00001 details) row with column values (EMP_ID:100) already exists
-- 2) Clave ajena sin padre (no existe el departamento 99)
INSERT INTO empleados (emp_id, nombre, apellidos, email, fecha_alta, oficio, salario, dept_id)
VALUES (173, 'Iker', 'Sanz Mar', 'iker.sanz@empresa.es', DATE '2026-10-01', 'BECARIO', 900, 99);
ERROR en línea 1:
ORA-02291: integrity constraint (CURSO.FK_EMP_DEPT) violated - parent key not found
-- 3) Restricción CHECK (el salario debe ser > 0)
INSERT INTO empleados (emp_id, nombre, apellidos, email, fecha_alta, oficio, salario, dept_id)
VALUES (173, 'Iker', 'Sanz Mar', 'iker.sanz@empresa.es', DATE '2026-10-01', 'BECARIO', 0, 30);
ERROR en línea 1:
ORA-02290: check constraint (CURSO.CK_EMP_SALARIO) violated
-- 4) Texto demasiado largo para VARCHAR2(40)
INSERT INTO departamentos (dept_id, nombre) VALUES (85, 'DEPARTAMENTO DE RELACIONES INSTITUCIONALES');

-- 5) Número con más dígitos que NUMBER(4)
INSERT INTO departamentos (dept_id, nombre) VALUES (12345, 'PRUEBA');

-- 6) Texto sin comillas: Oracle cree que Murcia es una columna
INSERT INTO departamentos (dept_id, nombre, localidad) VALUES (86, 'PRUEBA', Murcia);
ERROR en línea 1:
ORA-12899: value too large for column "CURSO"."DEPARTAMENTOS"."NOMBRE" (actual: 42, maximum: 40)

ERROR en línea 1:
ORA-01438: value 12345 greater than specified precision (4, 0) for column

ERROR en línea 1:
ORA-00984: column not allowed here
Código Significado Causa habitual Solución
ORA-00001 Restricción única violada PK o UNIQUE repetida Usa otro valor, una secuencia o MERGE
ORA-01400 No se puede insertar NULL Falta una columna NOT NULL Añádela a la lista con un valor
ORA-02290 CHECK violada Valor fuera del rango permitido Corrige el valor
ORA-02291 Clave padre no encontrada FK que apunta a algo inexistente Inserta antes el padre
ORA-12899 Valor demasiado grande Texto más largo que la columna Acorta el texto o amplía la columna
ORA-01438 Precisión numérica superada Número con demasiados dígitos Revisa el valor o el tipo
ORA-01722 Número no válido Texto no numérico en columna NUMBER Revisa el orden de los valores
ORA-01861 / ORA-01843 Fecha no válida Cadena sin formato explícito Usa DATE '...' o TO_DATE
ORA-00947 / ORA-00913 Pocos / demasiados valores Lista de columnas y valores no cuadran Cuenta columnas y valores
ORA-00984 Columna no permitida aquí Literal de texto sin comillas Pon las comillas simples
ORA-32795 Identidad ALWAYS Se intenta dar valor a la identidad Omite la columna

💡 Consejo: Desde 23ai los mensajes de ORA-00001 incluyen una segunda línea ORA-03301 con el valor exacto que se ha repetido. Léelos con atención: ahorran mucho tiempo de depuración.


3. UPDATE: modificar filas

3.1. Sintaxis

UPDATE tabla [alias]
SET    columna1 = expresión1
       [, columna2 = expresión2 ...]
[WHERE condición];
  • SET indica qué columnas cambian y su nuevo valor (puede ser un literal, una expresión con otras columnas, una función o una subconsulta).
  • WHERE selecciona qué filas se modifican, con las mismas condiciones que conoces de SELECT. Si lo omites, se modifican todas las filas.

3.2. Una o varias columnas

SELECT emp_id, nombre, salario FROM empleados WHERE emp_id = 113;

UPDATE empleados
SET    salario = 2000
WHERE  emp_id = 113;

SELECT emp_id, nombre, salario FROM empleados WHERE emp_id = 113;
    EMP_ID NOMBRE                            SALARIO
---------- ------------------------------ ----------
       113 Sergio                               1900

1 fila seleccionada.

1 fila actualizada.

    EMP_ID NOMBRE                            SALARIO
---------- ------------------------------ ----------
       113 Sergio                               2000

1 fila seleccionada.

Para cambiar varias columnas, sepáralas con comas en un único SET (no se repite la palabra SET):

UPDATE empleados
SET    oficio   = 'ANALISTA',
       salario  = 3000,
       telefono = '600100123'
WHERE  emp_id = 123;

SELECT emp_id, nombre, oficio, salario, telefono FROM empleados WHERE emp_id = 123;
ROLLBACK;
1 fila actualizada.

    EMP_ID NOMBRE                         OFICIO                            SALARIO TELEFONO
---------- ------------------------------ ------------------------------ ---------- ---------------
       123 Sara                           ANALISTA                             3000 600100123

1 fila seleccionada.

Rollback terminado.

3.3. Expresiones y funciones

El nuevo valor puede calcularse a partir del valor actual de la propia fila.

💻 Ejemplo: Subida del 3 % del salario y 100 € más de comisión a todo el departamento de Ventas. Como la comisión puede ser NULL, y NULL + 100 es NULL, usamos NVL.

SELECT emp_id, nombre, salario, comision FROM empleados WHERE dept_id = 20 ORDER BY emp_id;

UPDATE empleados
SET    salario  = ROUND(salario * 1.03, 2),
       comision = NVL(comision, 0) + 100
WHERE  dept_id = 20;

SELECT emp_id, nombre, salario, comision FROM empleados WHERE dept_id = 20 ORDER BY emp_id;
ROLLBACK;
    EMP_ID NOMBRE                            SALARIO   COMISION
---------- ------------------------------ ---------- ----------
       101 Luis                                 4800 (nulo)
       110 Carmen                               2200        500
       111 David                                2100        800
       112 Elena                                2300        300
       113 Sergio                               1900          0
       114 Lucía                                1700 (nulo)

6 filas seleccionadas.

6 filas actualizadas.

    EMP_ID NOMBRE                            SALARIO   COMISION
---------- ------------------------------ ---------- ----------
       101 Luis                                 4944        100
       110 Carmen                               2266        600
       111 David                                2163        900
       112 Elena                                2369        400
       113 Sergio                               1957        100
       114 Lucía                                1751        100

6 filas seleccionadas.

Rollback terminado.

Sin el NVL, Luis y Lucía seguirían con la comisión a NULL.

Todas las asignaciones ven los valores “antiguos”

Las expresiones del SET se evalúan con los valores de la fila antes de la actualización, como si se calcularan todas a la vez:

UPDATE empleados
SET    salario  = salario * 2,
       comision = salario          -- ¿el salario antiguo o el nuevo?
WHERE  emp_id = 110;

SELECT emp_id, salario, comision FROM empleados WHERE emp_id = 110;
ROLLBACK;
1 fila actualizada.

    EMP_ID    SALARIO   COMISION
---------- ---------- ----------
       110       4400       2200

1 fila seleccionada.

Rollback terminado.

La comisión ha tomado el salario antiguo (2200), no el nuevo (4400). El orden de las asignaciones no importa.

UPDATE con CASE

CASE permite aplicar reglas distintas a cada fila en una sola pasada.

💻 Ejemplo: Ajuste de precios según el stock: los productos con poco stock suben un 10 %, los que tienen mucho bajan un 10 %.

UPDATE productos
SET    precio = CASE
                    WHEN stock = 0   THEN precio
                    WHEN stock < 10  THEN ROUND(precio * 1.10, 2)
                    WHEN stock > 100 THEN ROUND(precio * 0.90, 2)
                    ELSE precio
                END
WHERE  descatalogado = 'N';

SELECT producto_id, nombre, stock, precio
FROM   productos
WHERE  stock < 10 OR stock > 100
ORDER  BY producto_id;
ROLLBACK;
19 filas actualizadas.

PRODUCTO_ID NOMBRE                                                            STOCK     PRECIO
----------- ------------------------------------------------------------ ---------- ----------
          5 Torre Workstation                                                     4     2088,9
          6 Ratón inalámbrico                                                   200      17,91
          8 Pack teclado y ratón                                                  0       34,5
         12 Smartphone Gama Alta                                                  8     1098,9
         14 Altavoz Inteligente                                                   0         49
         16 Mesa Elevable                                                         6      438,9
         18 Impresora Láser                                                       9      207,9

7 filas seleccionadas.

Rollback terminado.

Observa que dice 19 filas actualizadas. aunque solo cambiaron de precio 5 productos: Oracle cuenta las filas que cumplen el WHERE, aunque el nuevo valor sea igual al antiguo. Si quieres tocar solo las filas que cambian, mueve la lógica al WHERE (WHERE stock < 10 OR stock > 100); es más eficiente y genera menos redo y menos bloqueos.

3.4. Subconsultas en el WHERE

El WHERE de un UPDATE admite cualquier subconsulta de las del módulo 31 (IN, EXISTS, comparaciones con subconsultas escalares…).

💻 Ejemplo (el clásico): Subir un 5 % el salario a los empleados del departamento con más ventas. Primero calculamos las ventas de cada departamento (a través del empleado que gestionó cada pedido):

SELECT e.dept_id,
       ROUND(SUM(l.cantidad * l.precio_unitario * (1 - l.descuento / 100)), 2) AS ventas
FROM   pedidos p
       JOIN empleados e      ON e.emp_id = p.empleado_id
       JOIN lineas_pedido l  ON l.pedido_id = p.pedido_id
WHERE  p.estado <> 'CANCELADO'
GROUP  BY e.dept_id;
   DEPT_ID     VENTAS
---------- ----------
        20   61118,86

1 fila seleccionada.

Solo Ventas gestiona pedidos, pero la sentencia debe ser general. Usamos la consulta, ordenada y limitada a una fila, como subconsulta:

UPDATE empleados
SET    salario = salario * 1.05
WHERE  dept_id = (SELECT e.dept_id
                  FROM   pedidos p
                         JOIN empleados e     ON e.emp_id = p.empleado_id
                         JOIN lineas_pedido l ON l.pedido_id = p.pedido_id
                  WHERE  p.estado <> 'CANCELADO'
                  GROUP  BY e.dept_id
                  ORDER  BY SUM(l.cantidad * l.precio_unitario * (1 - l.descuento / 100)) DESC
                  FETCH FIRST 1 ROW ONLY);
ROLLBACK;
6 filas actualizadas.

Rollback terminado.

⚠️ Importante: FETCH FIRST 1 ROW ONLY elige un departamento aunque haya empate. Si quieres incluir todos los empatados, usa FETCH FIRST 1 ROW WITH TIES y cambia = por IN.

Otro ejemplo con IN y HAVING: 200 € más de comisión a los empleados que han vendido más de 15 000 €.

UPDATE empleados
SET    comision = NVL(comision, 0) + 200
WHERE  emp_id IN (SELECT p.empleado_id
                  FROM   pedidos p
                         JOIN lineas_pedido l ON l.pedido_id = p.pedido_id
                  WHERE  p.estado <> 'CANCELADO'
                  GROUP  BY p.empleado_id
                  HAVING SUM(l.cantidad * l.precio_unitario * (1 - l.descuento / 100)) > 15000);

SELECT emp_id, nombre, comision
FROM   empleados
WHERE  dept_id = 20 AND oficio = 'VENDEDOR'
ORDER  BY emp_id;
ROLLBACK;
2 filas actualizadas.

    EMP_ID NOMBRE                           COMISION
---------- ------------------------------ ----------
       110 Carmen                                700
       111 David                                 800
       112 Elena                                 500
       113 Sergio                                  0

4 filas seleccionadas.

Rollback terminado.

Carmen (22 018 €) y Elena (23 834 €) superan el umbral; David (12 932 €) y Sergio no.

La subconsulta puede leer la misma tabla que se está actualizando. Oracle evalúa la subconsulta con los datos anteriores a la sentencia (lectura consistente), así que el resultado no depende del orden en que se procesen las filas:

UPDATE empleados
SET    salario = salario + 100
WHERE  salario < (SELECT AVG(salario) FROM empleados);
ROLLBACK;
17 filas actualizadas.

Rollback terminado.

3.5. Subconsultas en el SET

Una subconsulta escalar (una fila y una columna) puede proporcionar el nuevo valor:

-- Sara pasa a cobrar el salario medio de los programadores
UPDATE empleados
SET    salario = (SELECT ROUND(AVG(salario), 2) FROM empleados WHERE oficio = 'PROGRAMADOR')
WHERE  emp_id = 123;

SELECT emp_id, nombre, salario FROM empleados WHERE emp_id = 123;
ROLLBACK;
1 fila actualizada.

    EMP_ID NOMBRE                            SALARIO
---------- ------------------------------ ----------
       123 Sara                                 2600

1 fila seleccionada.

Rollback terminado.

Si la subconsulta devuelve más de una fila, el error es el de siempre:

UPDATE empleados
SET    salario = (SELECT salario FROM empleados WHERE oficio = 'PROGRAMADOR')
WHERE  emp_id = 123;
SET    salario = (SELECT salario FROM empleados WHERE oficio = 'PROGRAMADOR')
                  *
ERROR en línea 2:
ORA-01427: single-row subquery returns more than one row

Y si no devuelve ninguna fila, la columna recibe NULL (lo que puede chocar con un NOT NULL o, peor, borrar silenciosamente un dato).

3.6. UPDATE correlacionado

En un UPDATE correlacionado, la subconsulta del SET hace referencia a la fila que se está actualizando (mediante el alias de la tabla exterior), y se evalúa una vez por cada fila.

💻 Ejemplo: Recalcular el presupuesto de cada departamento como 14 pagas de la suma de los salarios de sus empleados.

SELECT dept_id, presupuesto FROM departamentos ORDER BY dept_id;

UPDATE departamentos d
SET    d.presupuesto = (SELECT SUM(e.salario) * 14
                        FROM   empleados e
                        WHERE  e.dept_id = d.dept_id);

SELECT dept_id, presupuesto FROM departamentos ORDER BY dept_id;
ROLLBACK;
   DEPT_ID PRESUPUESTO
---------- -----------
        10      500000
        20      350000
        30      420000
        40      150000
        50      200000
        60      180000
        70 (nulo)

7 filas seleccionadas.

7 filas actualizadas.

   DEPT_ID PRESUPUESTO
---------- -----------
        10       91000
        20      210000
        30      263200
        40      118300
        50      130200
        60      125300
        70 (nulo)

7 filas seleccionadas.

Rollback terminado.

Fíjate en dos cosas:

  1. Se han actualizado 7 filas: también el departamento 70, que no tiene empleados. Su subconsulta devuelve SUM de nada, es decir, NULL. Aquí ya era NULL, pero si hubiera tenido un presupuesto lo habríamos borrado sin querer.
  2. La solución es limitar el UPDATE a las filas que tienen correspondencia, con EXISTS:
UPDATE departamentos d
SET    d.presupuesto = (SELECT SUM(e.salario) * 14
                        FROM   empleados e
                        WHERE  e.dept_id = d.dept_id)
WHERE  EXISTS (SELECT 1 FROM empleados e WHERE e.dept_id = d.dept_id);
ROLLBACK;
6 filas actualizadas.

Rollback terminado.

⚠️ Importante: Este es el error más frecuente en los UPDATE correlacionados: olvidar el WHERE EXISTS y poner a NULL las filas sin correspondencia. Otra opción es proteger el valor con NVL((SELECT ...), columna) para dejar el valor antiguo cuando no hay datos.

Otro ejemplo correlacionado: aumentar el límite de crédito de cada cliente en un 10 % de lo que ha comprado (pedidos entregados). Aquí sí queremos actualizar a todos y usamos NVL(..., 0) para que los que no han comprado sumen 0:

UPDATE clientes c
SET    c.limite_credito = c.limite_credito + 0.10 *
           (SELECT NVL(SUM(l.cantidad * l.precio_unitario * (1 - l.descuento / 100)), 0)
            FROM   pedidos p JOIN lineas_pedido l ON l.pedido_id = p.pedido_id
            WHERE  p.cliente_id = c.cliente_id AND p.estado = 'ENTREGADO');

SELECT cliente_id, nombre, limite_credito FROM clientes ORDER BY cliente_id;
ROLLBACK;
14 filas actualizadas.

CLIENTE_ID NOMBRE                                                       LIMITE_CREDITO
---------- ------------------------------------------------------------ --------------
      1001 Talleres Martín S.L.                                               10224,21
      1002 Construcciones Levante S.A.                                        26426,02
      1003 María José Herrera                                                  2083,79
      1004 Academia Nova                                                       8789,13
      1005 Clínica Dental Sonrisa                                               6223,2
      1006 Antonio Ruiz Pérez                                                   1599,9
      1007 Hotel Mar Azul S.L.                                                15253,91
      1008 Gestoría Delta                                                         5000
...

14 filas seleccionadas.

Rollback terminado.

3.7. Varias columnas desde una subconsulta

Cuando varias columnas salen de la misma subconsulta, no hace falta repetirla: Oracle permite asignar una lista de columnas a una subconsulta que devuelve una fila con ese mismo número de columnas.

UPDATE tabla
SET    (col1, col2, ...) = (SELECT expr1, expr2, ... FROM ... )
WHERE  ...;

💻 Ejemplo: El becario Tomás (160) pasa a tener el mismo oficio, salario y jefe que Raquel (121).

UPDATE empleados
SET    (oficio, salario, jefe_id) = (SELECT oficio, salario, jefe_id
                                     FROM   empleados
                                     WHERE  emp_id = 121)
WHERE  emp_id = 160;

SELECT emp_id, nombre, oficio, salario, jefe_id, dept_id
FROM   empleados
WHERE  emp_id IN (121, 160);
ROLLBACK;
1 fila actualizada.

    EMP_ID NOMBRE                         OFICIO                            SALARIO    JEFE_ID    DEPT_ID
---------- ------------------------------ ------------------------------ ---------- ---------- ----------
       121 Raquel                         PROGRAMADOR                          2800        120         30
       160 Tomás                          PROGRAMADOR                          2800        120 (nulo)

2 filas seleccionadas.

Rollback terminado.

También puede ser correlacionada. Por ejemplo, igualar el salario y la comisión de cada vendedor al máximo de su oficio:

UPDATE empleados e
SET    (e.salario, e.comision) = (SELECT MAX(x.salario), MAX(x.comision)
                                  FROM   empleados x
                                  WHERE  x.oficio = e.oficio)
WHERE  e.oficio = 'VENDEDOR';

SELECT emp_id, nombre, salario, comision FROM empleados WHERE oficio = 'VENDEDOR';
ROLLBACK;
4 filas actualizadas.

    EMP_ID NOMBRE                            SALARIO   COMISION
---------- ------------------------------ ---------- ----------
       110 Carmen                               2300        800
       111 David                                2300        800
       112 Elena                                2300        800
       113 Sergio                               2300        800

4 filas seleccionadas.

Rollback terminado.

3.8. UPDATE con join directo: UPDATE … SET … FROM (Oracle 23ai)

⚠️ Importante: Disponible desde Oracle 23ai.

Muchas veces la condición o el nuevo valor dependen de otra tabla. Desde 23ai puedes escribir el join directamente en una cláusula FROM del UPDATE, y usar las columnas de esa tabla tanto en el SET como en el WHERE:

UPDATE tabla_destino alias
SET    alias.columna = expresión   -- puede usar columnas de otra_tabla
FROM   otra_tabla o
WHERE  condición_de_join AND otras_condiciones;

💻 Ejemplo: Subida del 2 % a los empleados de los departamentos de Madrid.

UPDATE empleados e
SET    e.salario = e.salario * 1.02
FROM   departamentos d
WHERE  d.dept_id = e.dept_id
AND    d.localidad = 'Madrid';

SELECT e.emp_id, e.salario, d.localidad
FROM   empleados e JOIN departamentos d ON d.dept_id = e.dept_id
WHERE  d.localidad = 'Madrid'
ORDER  BY 1;
ROLLBACK;
7 filas actualizadas.

    EMP_ID    SALARIO LOCALIDAD
---------- ---------- ----------------------------------------
       100       6630 Madrid
       101       4896 Madrid
       110       2244 Madrid
       111       2142 Madrid
       112       2346 Madrid
       113       1938 Madrid
       114       1734 Madrid

7 filas seleccionadas.

Rollback terminado.

La tabla del FROM puede ser también una subconsulta. Así descontamos del stock las unidades de los pedidos pendientes:

UPDATE productos pr
SET    pr.stock = pr.stock - v.unidades
FROM   (SELECT l.producto_id, SUM(l.cantidad) AS unidades
        FROM   lineas_pedido l JOIN pedidos p ON p.pedido_id = l.pedido_id
        WHERE  p.estado = 'PENDIENTE'
        GROUP  BY l.producto_id) v
WHERE  v.producto_id = pr.producto_id;

SELECT producto_id, nombre, stock FROM productos WHERE producto_id IN (2, 3, 11, 13);
ROLLBACK;
4 filas actualizadas.

PRODUCTO_ID NOMBRE                                                            STOCK
----------- ------------------------------------------------------------ ----------
          2 Portátil Gaming 16                                                    9
          3 Portátil Básico 15                                                   37
         11 Smartphone Gama Media                                                49
         13 Auriculares Bluetooth                                                78

4 filas seleccionadas.

Rollback terminado.

⚠️ Importante: Cada fila de destino debe emparejarse como mucho con una fila del FROM. Si un mismo producto aparece en varias líneas, Oracle no sabe qué valor aplicar y aborta la sentencia:

-- Los pedidos 5013 y 5019 contienen ambos los productos 15 y 16
UPDATE productos pr
SET    pr.stock = pr.stock - l.cantidad
FROM   lineas_pedido l
WHERE  l.producto_id = pr.producto_id
AND    l.pedido_id IN (5013, 5019);
FROM   lineas_pedido l
       *
ERROR en línea 3:
ORA-30926: The operation attempted to update the same row (rowid: 'AAAR5oAAAAAAAcEAAO') twice.

La solución es la del ejemplo anterior: agregar primero en una subconsulta (GROUP BY producto_id) para que haya una fila por producto.

3.9. Alternativas clásicas al UPDATE con join (19c y 21c)

En versiones anteriores tienes tres caminos para actualizar “a partir de otra tabla”:

a) Subconsultas en WHERE y SET (lo visto en 3.4 a 3.7). Es la forma más portable, aunque obliga a repetir la subconsulta en SET y en WHERE EXISTS.

b) Actualizar una vista en línea (inline view) con el join:

UPDATE (SELECT e.salario, d.localidad
        FROM   empleados e
               JOIN departamentos d ON d.dept_id = e.dept_id
        WHERE  d.localidad = 'Madrid')
SET    salario = salario * 1.02;
ROLLBACK;
7 filas actualizadas.

Rollback terminado.

Para que funcione, la columna modificada debe pertenecer a una tabla con clave preservada (key-preserved table): una tabla cuya clave primaria sigue siendo única en el resultado del join. Aquí EMPLEADOS lo es (cada empleado aparece una vez) y DEPARTAMENTOS no (el departamento 20 aparece seis veces). Si intentas modificar una columna de DEPARTAMENTOS a través de este join:

UPDATE (SELECT d.presupuesto, e.salario
        FROM   empleados e
               JOIN departamentos d ON d.dept_id = e.dept_id
        WHERE  d.localidad = 'Madrid')
SET    presupuesto = presupuesto + 1000;
        FROM   empleados e
               *
ERROR en línea 2:
ORA-30926: The operation attempted to update the same row (rowid: 'AAAR5cAAAAAAAakAAB') twice.

📘 Concepto: En Oracle 19c y 21c la comprobación es estática: cualquier intento de modificar una columna de una tabla sin clave preservada falla al analizar la sentencia con ORA-01779: cannot modify a column which maps to a non key-preserved table, aunque el join devuelva una sola fila. En 26ai, como ves en la salida, Oracle hace la comprobación en tiempo de ejecución y solo falla (con ORA-30926) si una fila llega a modificarse dos veces. Para escribir código portable, modifica solo columnas de la tabla con clave preservada.

c) MERGE con solo la rama WHEN MATCHED (sección 5):

MERGE INTO empleados e
USING (SELECT dept_id FROM departamentos WHERE localidad = 'Madrid') d
ON (e.dept_id = d.dept_id)
WHEN MATCHED THEN
    UPDATE SET e.salario = e.salario * 1.02;
ROLLBACK;
7 filas fusionadas.

Rollback terminado.
Técnica Versión Comentario
UPDATE ... SET ... FROM 23ai+ La más legible; agrega antes si hay varias filas por destino
Subconsultas en SET + WHERE EXISTS Todas Portable (estándar), pero repite la subconsulta
UPDATE (vista en línea) Todas Solo columnas de la tabla con clave preservada
MERGE ... WHEN MATCHED 9i+ Muy usada en Oracle; error ORA-30926 si el origen tiene duplicados

3.10. El peligro del UPDATE sin WHERE

Un UPDATE sin WHERE modifica todas las filas de la tabla. Es la causa de innumerables desastres en producción.

UPDATE empleados SET salario = 2000;   -- ¡Sin WHERE!

SELECT MIN(salario), MAX(salario) FROM empleados;
24 filas actualizadas.

MIN(SALARIO) MAX(SALARIO)
------------ ------------
        2000         2000

1 fila seleccionada.

Todos los empleados, desde la directora general hasta el becario, cobran ahora 2000 €. Por suerte, no hemos hecho COMMIT:

ROLLBACK;
SELECT MIN(salario), MAX(salario) FROM empleados;
Rollback terminado.

MIN(SALARIO) MAX(SALARIO)
------------ ------------
         900         6500

1 fila seleccionada.

⚠️ Importante: Mira siempre el número de filas que devuelve Oracle (24 filas actualizadas.). Si esperabas una y te dice 24, haz ROLLBACK inmediatamente. Y nunca trabajes con autocommit activado cuando modifiques datos a mano (módulo 35).

3.11. Restricciones durante el UPDATE

Un UPDATE debe dejar las filas cumpliendo todas las restricciones. Además, como la sentencia es atómica, si una sola fila incumple una restricción, ninguna fila se modifica:

-- Rosa y Manuel cobran 1600: 1600 - 1700 = -100, que incumple salario > 0
UPDATE empleados SET salario = salario - 1700 WHERE dept_id = 60;

SELECT emp_id, salario FROM empleados WHERE dept_id = 60;
UPDATE empleados SET salario = salario - 1700 WHERE dept_id = 60
*
ERROR en línea 1:
ORA-02290: check constraint (CURSO.CK_EMP_SALARIO) violated

    EMP_ID    SALARIO
---------- ----------
       105       4000
       150       1600
       151       1600
       152       1750

4 filas seleccionadas.

Javier (105), que podía bajar a 2300 sin problemas, tampoco ha cambiado: la sentencia se ha deshecho entera. Es la atomicidad a nivel de sentencia, que verás en detalle en el módulo 35.

Otros errores típicos al actualizar:

UPDATE empleados SET dept_id = 99 WHERE emp_id = 160;          -- FK hacia un padre inexistente
UPDATE departamentos SET dept_id = 15 WHERE dept_id = 10;      -- PK con hijos
UPDATE empleados SET nombre = NULL WHERE emp_id = 160;         -- NOT NULL
UPDATE empleados SET email = 'ana.garcia@empresa.es' WHERE emp_id = 160;  -- UNIQUE
ERROR en línea 1:
ORA-02291: integrity constraint (CURSO.FK_EMP_DEPT) violated - parent key not found

ERROR en línea 1:
ORA-02292: integrity constraint (CURSO.FK_EMP_DEPT) violated - child record found

ERROR en línea 1:
ORA-01407: cannot update ("CURSO"."EMPLEADOS"."NOMBRE") to NULL

ERROR en línea 1:
ORA-00001: unique constraint (CURSO.UQ_EMP_EMAIL) violated on table CURSO.EMPLEADOS columns (EMAIL)
ORA-03301: (ORA-00001 details) row with column values (EMAIL:'ana.garcia@empresa.es') already exists

En cambio, cambiar la clave primaria de un departamento sin hijos (el 70) sí es posible:

UPDATE departamentos SET dept_id = 75 WHERE dept_id = 70;
ROLLBACK;
1 fila actualizada.

Rollback terminado.

💡 Consejo: Que se pueda no significa que se deba. Las claves primarias deberían ser inmutables. Oracle no tiene ON UPDATE CASCADE: si una clave cambia de verdad, es señal de que quizá debería ser una clave subrogada (un número sin significado) y el dato cambiante una columna normal con restricción UNIQUE.

Fíjate en que el error ORA-01407 es distinto del ORA-01400 del INSERT: el primero aparece al actualizar a NULL una columna NOT NULL; el segundo, al insertar NULL.


4. DELETE: borrar filas

4.1. Sintaxis

DELETE [FROM] tabla [alias]
[WHERE condición];

La palabra FROM es opcional en Oracle (DELETE asignaciones WHERE ... también funciona), pero escríbela siempre: es estándar y más legible. Igual que con UPDATE, sin WHERE se borran todas las filas.

DELETE borra filas completas; no se puede “borrar una columna” de una fila. Para vaciar un dato concreto se usa UPDATE ... SET columna = NULL.

4.2. DELETE con WHERE

💡 Consejo: Antes de borrar, ejecuta un SELECT con el mismo WHERE. Si devuelve lo que esperas, cambia SELECT * por DELETE.

SELECT * FROM asignaciones WHERE proyecto_id = 3 AND horas < 100;

DELETE FROM asignaciones
WHERE  proyecto_id = 3
AND    horas < 100;

SELECT emp_id, proyecto_id, horas FROM asignaciones WHERE proyecto_id = 3;
ROLLBACK;
    EMP_ID PROYECTO_ID FECHA_ASIG      HORAS FUNCION
---------- ----------- ---------- ---------- ------------------------------
       104           3 01/10/2025         80 Responsable
       110           3 01/11/2025         40 Apoyo comercial

2 filas seleccionadas.

2 filas suprimidas.

    EMP_ID PROYECTO_ID      HORAS
---------- ----------- ----------
       140           3        200
       141           3        160

2 filas seleccionadas.

Rollback terminado.

4.3. DELETE con subconsultas

-- Pedidos cancelados de clientes de Zaragoza
DELETE FROM pedidos
WHERE  cliente_id IN (SELECT cliente_id FROM clientes WHERE provincia = 'Zaragoza')
AND    estado = 'CANCELADO';
ROLLBACK;
1 fila suprimida.

Rollback terminado.

Con NOT EXISTS borramos productos descatalogados que nunca se han vendido:

SELECT producto_id, nombre
FROM   productos p
WHERE  descatalogado = 'S'
AND    NOT EXISTS (SELECT 1 FROM lineas_pedido l WHERE l.producto_id = p.producto_id);

DELETE FROM productos p
WHERE  p.descatalogado = 'S'
AND    NOT EXISTS (SELECT 1 FROM lineas_pedido l WHERE l.producto_id = p.producto_id);
ROLLBACK;
PRODUCTO_ID NOMBRE
----------- ------------------------------------------------------------
         14 Altavoz Inteligente

1 fila seleccionada.

1 fila suprimida.

Rollback terminado.

⚠️ Importante: Cuidado con NOT IN si la subconsulta puede devolver NULL: la condición nunca es verdadera y no se borra nada (lo verás en el ejercicio 25). Con NOT EXISTS no existe ese problema.

4.4. DELETE correlacionado

💻 Ejemplo: Eliminar, de cada proyecto, la asignación con menos horas.

SELECT a.emp_id, a.proyecto_id, a.horas
FROM   asignaciones a
WHERE  a.horas = (SELECT MIN(x.horas) FROM asignaciones x WHERE x.proyecto_id = a.proyecto_id)
ORDER  BY a.proyecto_id;

DELETE FROM asignaciones a
WHERE  a.horas = (SELECT MIN(x.horas) FROM asignaciones x WHERE x.proyecto_id = a.proyecto_id);
ROLLBACK;
    EMP_ID PROYECTO_ID      HORAS
---------- ----------- ----------
       124           1        380
       140           2        120
       110           3         40
       105           4         60

4 filas seleccionadas.

4 filas suprimidas.

Rollback terminado.

Igual que en el UPDATE, la subconsulta se evalúa sobre los datos anteriores al borrado: no ocurre que, al borrar la de 40 horas, la de 80 pase a ser la mínima y se borre también.

4.5. DELETE con join directo (Oracle 23ai)

⚠️ Importante: Disponible desde Oracle 23ai.

Igual que UPDATE, desde 23ai DELETE admite una cláusula FROM adicional con otras tablas:

-- Borrar las asignaciones de los proyectos terminados antes de 2026
DELETE FROM asignaciones a
FROM   proyectos p
WHERE  p.proyecto_id = a.proyecto_id
AND    p.fecha_fin < DATE '2026-01-01';

SELECT COUNT(*) FROM asignaciones;
ROLLBACK;
7 filas suprimidas.

  COUNT(*)
----------
         7

1 fila seleccionada.

Rollback terminado.

El primer FROM indica la tabla de la que se borra; el segundo, las tablas con las que se combina. El equivalente clásico usa una subconsulta:

DELETE FROM asignaciones a
WHERE  a.proyecto_id IN (SELECT p.proyecto_id
                         FROM   proyectos p
                         WHERE  p.fecha_fin < DATE '2026-01-01');
ROLLBACK;
7 filas suprimidas.

Rollback terminado.

4.6. DELETE y claves ajenas

Al borrar una fila padre, Oracle comprueba qué hacer con sus hijas según la regla de borrado de cada clave ajena (módulo 24). Puedes consultarlas en el diccionario:

COLUMN constraint_name   FORMAT A18
COLUMN table_name        FORMAT A15
COLUMN r_constraint_name FORMAT A18

SELECT constraint_name, table_name, r_constraint_name, delete_rule
FROM   user_constraints
WHERE  constraint_type = 'R'
ORDER  BY table_name, constraint_name;
CONSTRAINT_NAME    TABLE_NAME      R_CONSTRAINT_NAME  DELETE_RU
------------------ --------------- ------------------ ---------
FK_ASI_EMP         ASIGNACIONES    PK_EMPLEADOS       NO ACTION
FK_ASI_PROYECTO    ASIGNACIONES    PK_PROYECTOS       CASCADE
FK_CAT_PADRE       CATEGORIAS      PK_CATEGORIAS      NO ACTION
FK_CLI_COMERCIAL   CLIENTES        PK_EMPLEADOS       NO ACTION
FK_EMP_DEPT        EMPLEADOS       PK_DEPARTAMENTOS   NO ACTION
FK_EMP_JEFE        EMPLEADOS       PK_EMPLEADOS       NO ACTION
FK_LIN_PEDIDO      LINEAS_PEDIDO   PK_PEDIDOS         CASCADE
FK_LIN_PRODUCTO    LINEAS_PEDIDO   PK_PRODUCTOS       NO ACTION
FK_PED_CLIENTE     PEDIDOS         PK_CLIENTES        NO ACTION
FK_PED_EMPLEADO    PEDIDOS         PK_EMPLEADOS       NO ACTION
FK_PRO_CATEGORIA   PRODUCTOS       PK_CATEGORIAS      NO ACTION
FK_PRY_DEPT        PROYECTOS       PK_DEPARTAMENTOS   NO ACTION

12 filas seleccionadas.

NO ACTION (por defecto): ORA-02292

Si existe alguna fila hija, el borrado del padre se rechaza:

DELETE FROM clientes WHERE cliente_id = 1001;   -- tiene pedidos
DELETE FROM clientes WHERE cliente_id = 1013;   -- no tiene pedidos
ROLLBACK;
DELETE FROM clientes WHERE cliente_id = 1001
*
ERROR en línea 1:
ORA-02292: integrity constraint (CURSO.FK_PED_CLIENTE) violated - child record found

1 fila suprimida.

Rollback terminado.

El mensaje dice qué restricción lo impide, lo que te indica en qué tabla están las hijas. Un empleado puede tener hijas en varias tablas (asignaciones, clientes, pedidos, subordinados) y Oracle informa de la primera que encuentra:

DELETE FROM empleados WHERE emp_id = 110;
DELETE FROM empleados WHERE emp_id = 110
*
ERROR en línea 1:
ORA-02292: integrity constraint (CURSO.FK_ASI_EMP) violated - child record found

ON DELETE CASCADE: se borran también las hijas

LINEAS_PEDIDO y ASIGNACIONES se crearon con ON DELETE CASCADE hacia PEDIDOS y PROYECTOS. Al borrar el padre, Oracle borra automáticamente sus hijas en la misma sentencia:

SELECT COUNT(*) AS lineas_5002 FROM lineas_pedido WHERE pedido_id = 5002;
DELETE FROM pedidos WHERE pedido_id = 5002;
SELECT COUNT(*) AS lineas_5002 FROM lineas_pedido WHERE pedido_id = 5002;
ROLLBACK;
LINEAS_5002
-----------
          3

1 fila seleccionada.

1 fila suprimida.

LINEAS_5002
-----------
          0

1 fila seleccionada.

Rollback terminado.

El mensaje dice 1 fila suprimida. aunque en realidad han desaparecido cuatro filas (el pedido y sus tres líneas): el contador solo incluye la tabla nombrada en el DELETE. Lo mismo con proyectos y asignaciones:

SELECT COUNT(*) FROM asignaciones WHERE proyecto_id = 2;
DELETE FROM proyectos WHERE proyecto_id = 2;
SELECT COUNT(*) FROM asignaciones WHERE proyecto_id = 2;
ROLLBACK;
  COUNT(*)
----------
         4

1 fila seleccionada.

1 fila suprimida.

  COUNT(*)
----------
         0

1 fila seleccionada.

Rollback terminado.

⚠️ Importante: ON DELETE CASCADE es cómodo y peligroso a la vez. Un DELETE FROM pedidos; sin WHERE vacía también LINEAS_PEDIDO entera (lo hemos probado: 24 filas suprimidas. y después SELECT COUNT(*) FROM lineas_pedido devuelve 0). Úsalo solo para entidades débiles o dependientes en existencia, como las líneas de un pedido.

ON DELETE SET NULL: las hijas quedan “huérfanas” con NULL

Ninguna FK de EMPRESA usa esta regla, así que la probamos con copias:

CREATE TABLE dept_demo AS SELECT dept_id, nombre FROM departamentos;
ALTER TABLE dept_demo ADD CONSTRAINT pk_dept_demo PRIMARY KEY (dept_id);

CREATE TABLE emp_demo AS SELECT emp_id, nombre, dept_id FROM empleados;
ALTER TABLE emp_demo ADD CONSTRAINT pk_emp_demo PRIMARY KEY (emp_id);
ALTER TABLE emp_demo ADD CONSTRAINT fk_emp_demo_dept
    FOREIGN KEY (dept_id) REFERENCES dept_demo (dept_id) ON DELETE SET NULL;

DELETE FROM dept_demo WHERE dept_id = 40;

SELECT emp_id, nombre, dept_id FROM emp_demo WHERE emp_id IN (103, 130, 131);
ROLLBACK;
Tabla creada.

Tabla modificada.

Tabla creada.

Tabla modificada.

Tabla modificada.

1 fila suprimida.

    EMP_ID NOMBRE                            DEPT_ID
---------- ------------------------------ ----------
       103 Pedro                          (nulo)
       130 Isabel                         (nulo)
       131 Jorge                          (nulo)

3 filas seleccionadas.

Rollback terminado.

Los tres empleados de Contabilidad siguen existiendo, pero con dept_id a NULL. Para que esta regla funcione, la columna FK debe admitir nulos.

Regla Al borrar el padre… Cuándo usarla
(ninguna) = NO ACTION Error ORA-02292 si hay hijas Por defecto; protege los datos
ON DELETE CASCADE Se borran las hijas Entidades débiles (líneas de pedido)
ON DELETE SET NULL Las hijas quedan con FK a NULL Relaciones opcionales (empleado sin departamento)

4.7. DELETE frente a TRUNCATE (repaso)

En el módulo 25 viste TRUNCATE TABLE. Recuerda las diferencias:

Aspecto DELETE FROM t TRUNCATE TABLE t
Tipo DML DDL
Filtra con WHERE Sí No: siempre todas las filas
Se puede deshacer con ROLLBACK Sí No (hace COMMIT implícito)
Genera undo por fila Sí (lento con muchas filas) No (muy rápido)
Respeta ON DELETE CASCADE Sí No (salvo TRUNCATE ... CASCADE, 12c+)
Libera espacio No (el segmento conserva su tamaño) Sí (por defecto, DROP STORAGE)
Reinicia la marca de agua alta (HWM) No Sí
Si la tabla es padre de FK habilitadas Comprueba fila a fila Error ORA-02266
TRUNCATE TABLE dept_demo;   -- es padre de emp_demo
TRUNCATE TABLE emp_demo;
SELECT COUNT(*) FROM emp_demo;
TRUNCATE TABLE dept_demo;
TRUNCATE TABLE dept_demo
               *
ERROR en línea 1:
ORA-02266: unique/primary keys in table referenced by enabled foreign keys

Tabla truncada.

  COUNT(*)
----------
         0

1 fila seleccionada.

Tabla truncada.

En esta versión, una vez vaciada la tabla hija, el TRUNCATE del padre se acepta. En versiones anteriores la documentación exige deshabilitar la clave ajena (ALTER TABLE emp_demo DISABLE CONSTRAINT fk_emp_demo_dept) antes de truncar el padre, aunque la hija esté vacía; si tu código tiene que funcionar en 19c, hazlo así.

DROP TABLE emp_demo PURGE;
DROP TABLE dept_demo PURGE;

5. MERGE: insertar o actualizar (upsert)

5.1. El problema

Un caso muy común: recibes un fichero o una tabla con datos (por ejemplo, la tarifa de un proveedor) y debes actualizar los productos que ya tienes e insertar los nuevos. Con INSERT y UPDATE necesitarías dos sentencias, cada una con su subconsulta, y cuidado con el orden. MERGE lo hace en una sola sentencia, leyendo el origen una sola vez.

📘 Concepto: MERGE (también llamado upsert, de update + insert) combina una tabla destino con un origen (tabla, vista o subconsulta) según una condición de emparejamiento. Para cada fila del origen: si tiene pareja en el destino se ejecuta la rama WHEN MATCHED (actualizar y, opcionalmente, borrar); si no la tiene, la rama WHEN NOT MATCHED (insertar). Forma parte del estándar SQL desde SQL:2003.

5.2. Sintaxis

MERGE INTO destino d
USING origen o
ON (condición_de_emparejamiento)
WHEN MATCHED THEN
    UPDATE SET d.col1 = o.col1 [, ...]
    [WHERE condición_update]
    [DELETE WHERE condición_delete]
WHEN NOT MATCHED THEN
    INSERT (d.col1, d.col2, ...)
    VALUES (o.col1, o.col2, ...)
    [WHERE condición_insert];
  • La condición de ON va entre paréntesis (obligatorio en Oracle).
  • Las dos ramas son opcionales (pero al menos una debe estar) y pueden ir en cualquier orden.
  • En la rama INSERT no se escribe el nombre de la tabla.
flowchart TD
    A[Fila del origen] --> B{"¿Cumple ON con<br/>alguna fila del destino?"}
    B -- "Sí" --> C["WHEN MATCHED<br/>UPDATE SET ... WHERE ..."]
    C --> D{"¿Cumple DELETE WHERE<br/>tras actualizar?"}
    D -- "Sí" --> E[Se borra la fila del destino]
    D -- "No" --> F[Queda actualizada]
    B -- "No" --> G["WHEN NOT MATCHED<br/>INSERT ... WHERE ..."]
    G --> H[Nueva fila en el destino]

5.3. Ejemplo completo: tarifa de un proveedor

CREATE TABLE tarifa_proveedor (
    producto_id  NUMBER(6) CONSTRAINT pk_tarifa_proveedor PRIMARY KEY,
    nombre       VARCHAR2(60),
    categoria_id NUMBER(4),
    precio       NUMBER(8,2)
);

INSERT INTO tarifa_proveedor VALUES (1,  'Portátil UltraBook 14', 2, 949.00);
INSERT INTO tarifa_proveedor VALUES (6,  'Ratón inalámbrico',     5,  17.50);
INSERT INTO tarifa_proveedor VALUES (9,  'Monitor 24 FHD',        6, 139.00);
INSERT INTO tarifa_proveedor VALUES (21, 'Hub USB-C 7 puertos',   4,  39.90);
INSERT INTO tarifa_proveedor VALUES (22, 'Altavoces 2.1',         9,  64.00);
COMMIT;

Los productos 1, 6 y 9 ya existen (dos cambian de precio y el 9 no); el 21 y el 22 son nuevos.

MERGE INTO productos p
USING tarifa_proveedor t
ON (p.producto_id = t.producto_id)
WHEN MATCHED THEN
    UPDATE SET p.precio = t.precio
WHEN NOT MATCHED THEN
    INSERT (producto_id, nombre, categoria_id, precio, stock, fecha_alta)
    VALUES (t.producto_id, t.nombre, t.categoria_id, t.precio, 0, DATE '2026-10-01');

COLUMN nombre FORMAT A25
SELECT producto_id, nombre, precio, stock, descatalogado
FROM   productos
WHERE  producto_id IN (1, 6, 9, 21, 22)
ORDER  BY producto_id;
ROLLBACK;
5 filas fusionadas.

PRODUCTO_ID NOMBRE                        PRECIO      STOCK D
----------- ------------------------- ---------- ---------- -
          1 Portátil UltraBook 14            949         25 N
          6 Ratón inalámbrico               17,5        200 N
          9 Monitor 24 FHD                   139         35 N
         21 Hub USB-C 7 puertos             39,9          0 N
         22 Altavoces 2.1                     64          0 N

5 filas seleccionadas.

Rollback terminado.

Tres filas actualizadas y dos insertadas: 5 filas fusionadas.. Los productos nuevos toman descatalogado = 'N' por defecto.

5.4. Condiciones en las ramas

Cada rama admite un WHERE propio:

  • En WHEN MATCHED ... UPDATE ... WHERE, solo se actualizan las filas emparejadas que lo cumplen (aquí, solo si el precio cambia de verdad).
  • En WHEN NOT MATCHED ... INSERT ... WHERE, solo se insertan las filas del origen que lo cumplen (aquí, solo los productos baratos).
MERGE INTO productos p
USING tarifa_proveedor t
ON (p.producto_id = t.producto_id)
WHEN MATCHED THEN
    UPDATE SET p.precio = t.precio
    WHERE  p.precio <> t.precio
WHEN NOT MATCHED THEN
    INSERT (producto_id, nombre, categoria_id, precio)
    VALUES (t.producto_id, t.nombre, t.categoria_id, t.precio)
    WHERE  t.precio < 50;

SELECT producto_id, nombre, precio
FROM   productos
WHERE  producto_id IN (1, 6, 9, 21, 22)
ORDER  BY 1;
ROLLBACK;
3 filas fusionadas.

PRODUCTO_ID NOMBRE                        PRECIO
----------- ------------------------- ----------
          1 Portátil UltraBook 14            949
          6 Ratón inalámbrico               17,5
          9 Monitor 24 FHD                   139
         21 Hub USB-C 7 puertos             39,9

4 filas seleccionadas.

Rollback terminado.

Ahora solo hay 3 operaciones: dos actualizaciones (1 y 6; el 9 se salta porque su precio no cambia) y una inserción (21; el 22 cuesta 64 € y no cumple t.precio < 50).

Si solo necesitas una de las ramas, omite la otra:

MERGE INTO productos p
USING tarifa_proveedor t
ON (p.producto_id = t.producto_id)
WHEN MATCHED THEN
    UPDATE SET p.precio = t.precio;
ROLLBACK;
3 filas fusionadas.

Rollback terminado.

5.5. La cláusula DELETE WHERE

Dentro de la rama WHEN MATCHED, DELETE WHERE borra las filas del destino que, después de actualizarse, cumplen la condición. Solo afecta a filas que han pasado por el UPDATE.

💻 Ejemplo: Mantenemos el stock de una tienda con los movimientos del día; si un producto se queda a 0, desaparece de la tienda, y si llega un producto nuevo, se da de alta.

CREATE TABLE stock_tienda AS
SELECT producto_id, nombre, stock FROM productos WHERE producto_id <= 8;

CREATE TABLE movimientos_hoy (producto_id NUMBER(6), unidades NUMBER(6));
INSERT INTO movimientos_hoy VALUES (3, -40);   -- se vende todo
INSERT INTO movimientos_hoy VALUES (5, -1);
INSERT INTO movimientos_hoy VALUES (8, 25);    -- reposición
INSERT INTO movimientos_hoy VALUES (9, 12);    -- producto nuevo en la tienda
COMMIT;

SELECT * FROM stock_tienda ORDER BY producto_id;
PRODUCTO_ID NOMBRE                         STOCK
----------- ------------------------- ----------
          1 Portátil UltraBook 14             25
          2 Portátil Gaming 16                10
          3 Portátil Básico 15                40
          4 Torre Oficina i5                  15
          5 Torre Workstation                  4
          6 Ratón inalámbrico                200
          7 Teclado mecánico                  60
          8 Pack teclado y ratón               0

8 filas seleccionadas.
MERGE INTO stock_tienda s
USING (SELECT m.producto_id, p.nombre, m.unidades
       FROM   movimientos_hoy m JOIN productos p ON p.producto_id = m.producto_id) m
ON (s.producto_id = m.producto_id)
WHEN MATCHED THEN
    UPDATE SET s.stock = s.stock + m.unidades
    DELETE WHERE s.stock = 0
WHEN NOT MATCHED THEN
    INSERT (producto_id, nombre, stock)
    VALUES (m.producto_id, m.nombre, m.unidades);

SELECT * FROM stock_tienda ORDER BY producto_id;
ROLLBACK;
4 filas fusionadas.

PRODUCTO_ID NOMBRE                         STOCK
----------- ------------------------- ----------
          1 Portátil UltraBook 14             25
          2 Portátil Gaming 16                10
          4 Torre Oficina i5                  15
          5 Torre Workstation                  3
          6 Ratón inalámbrico                200
          7 Teclado mecánico                  60
          8 Pack teclado y ratón              25
          9 Monitor 24 FHD                    12

8 filas seleccionadas.

Rollback terminado.
  • El producto 3 pasó de 40 a 0 y se borró (DELETE WHERE s.stock = 0 se evalúa con el valor ya actualizado).
  • El 8 tenía stock 0 antes, pero tras sumar 25 ya no cumple la condición: se queda.
  • El 9 no existía y se insertó.

⚠️ Importante: DELETE WHERE no borra las filas del destino que no aparecen en el origen. Un MERGE nunca toca las filas del destino sin pareja. Si necesitas eliminar “lo que ya no viene en el origen”, hazlo con un DELETE ... WHERE NOT EXISTS aparte (ejercicio 29).

5.6. Sincronizar una tabla de resumen

MERGE es la herramienta natural para refrescar tablas de resumen: el origen es una consulta agregada.

CREATE TABLE resumen_clientes (
    cliente_id     NUMBER(6) CONSTRAINT pk_resumen_clientes PRIMARY KEY,
    num_pedidos    NUMBER(5),
    importe_total  NUMBER(12,2),
    ultimo_pedido  DATE
);
INSERT INTO resumen_clientes VALUES (1001, 1, 100, DATE '2025-01-15');   -- datos desfasados
INSERT INTO resumen_clientes VALUES (1013, 0, 0, NULL);
COMMIT;

MERGE INTO resumen_clientes r
USING (SELECT p.cliente_id,
              COUNT(DISTINCT p.pedido_id) AS num_pedidos,
              ROUND(SUM(l.cantidad * l.precio_unitario * (1 - l.descuento / 100)), 2) AS importe,
              MAX(p.fecha_pedido) AS ultimo
       FROM   pedidos p JOIN lineas_pedido l ON l.pedido_id = p.pedido_id
       WHERE  p.estado <> 'CANCELADO'
       GROUP  BY p.cliente_id) v
ON (r.cliente_id = v.cliente_id)
WHEN MATCHED THEN
    UPDATE SET r.num_pedidos   = v.num_pedidos,
               r.importe_total = v.importe,
               r.ultimo_pedido = v.ultimo
WHEN NOT MATCHED THEN
    INSERT (cliente_id, num_pedidos, importe_total, ultimo_pedido)
    VALUES (v.cliente_id, v.num_pedidos, v.importe, v.ultimo);

SELECT * FROM resumen_clientes ORDER BY cliente_id;
ROLLBACK;
12 filas fusionadas.

CLIENTE_ID NUM_PEDIDOS IMPORTE_TOTAL ULTIMO_PED
---------- ----------- ------------- ----------
      1001           3       4704,15 15/06/2026
      1002           3      14260,19 03/03/2026
      1003           2         837,9 27/11/2025
      1004           2        7891,3 08/09/2025
      1005           2          2232 21/04/2026
      1006           1           999 25/06/2025
      1007           2       4202,77 30/05/2026
      1008           1          1497 10/07/2026
...

13 filas seleccionadas.

Rollback terminado.

El 1001 se ha corregido, se han insertado los otros 11 clientes con pedidos, y el 1013 (sin pedidos, no aparece en el origen) se ha quedado como estaba.

5.7. Upsert de una sola fila

Para “insertar o actualizar” un único registro, el origen es una consulta sobre DUAL:

MERGE INTO departamentos d
USING (SELECT 80 AS dept_id, 'CALIDAD' AS nombre, 'Málaga' AS localidad FROM dual) s
ON (d.dept_id = s.dept_id)
WHEN MATCHED THEN
    UPDATE SET d.nombre = s.nombre, d.localidad = s.localidad
WHEN NOT MATCHED THEN
    INSERT (dept_id, nombre, localidad) VALUES (s.dept_id, s.nombre, s.localidad);

-- Segunda ejecución con otra localidad: ahora ya existe y se actualiza
MERGE INTO departamentos d
USING (SELECT 80 AS dept_id, 'CALIDAD' AS nombre, 'Córdoba' AS localidad FROM dual) s
ON (d.dept_id = s.dept_id)
WHEN MATCHED THEN
    UPDATE SET d.nombre = s.nombre, d.localidad = s.localidad
WHEN NOT MATCHED THEN
    INSERT (dept_id, nombre, localidad) VALUES (s.dept_id, s.nombre, s.localidad);

SELECT * FROM departamentos WHERE dept_id = 80;
ROLLBACK;
1 fila fusionada.

1 fila fusionada.

   DEPT_ID NOMBRE                    LOCALIDAD                                PRESUPUESTO
---------- ------------------------- ---------------------------------------- -----------
        80 CALIDAD                   Córdoba                                  (nulo)

1 fila seleccionada.

Rollback terminado.

5.8. Errores típicos con MERGE

a) Origen con duplicados (ORA-30926). Si una fila del destino se empareja con varias del origen, Oracle no sabe con cuál actualizarla:

-- Añadimos al origen otro 'Ratón inalámbrico' y emparejamos por nombre
INSERT INTO tarifa_proveedor VALUES (99, 'Ratón inalámbrico', 5, 18.00);

MERGE INTO productos p
USING tarifa_proveedor t
ON (p.nombre = t.nombre)
WHEN MATCHED THEN
    UPDATE SET p.precio = t.precio;
ROLLBACK;
1 fila creada.

USING tarifa_proveedor t
      *
ERROR en línea 2:
ORA-30926: The operation attempted to update the same row (rowid: 'AAAR5oAAAAAAAcEAAF') twice.

Rollback terminado.

Empareja siempre por una clave del origen o elimina antes los duplicados (con GROUP BY o ROW_NUMBER() en la subconsulta USING).

b) Actualizar una columna del ON (ORA-38104):

MERGE INTO productos p
USING tarifa_proveedor t
ON (p.producto_id = t.producto_id)
WHEN MATCHED THEN
    UPDATE SET p.producto_id = t.producto_id, p.precio = t.precio;
ON (p.producto_id = t.producto_id)
    *
ERROR en línea 3:
ORA-38104: Columns referenced in the ON Clause cannot be updated: "P"."PRODUCTO_ID"

Tiene lógica: si cambiaras la columna por la que se empareja, la fila podría dejar de estar emparejada a mitad de la operación.

DROP TABLE resumen_clientes PURGE;
DROP TABLE movimientos_hoy PURGE;
DROP TABLE stock_tienda PURGE;
DROP TABLE tarifa_proveedor PURGE;

6. La cláusula RETURNING

6.1. Concepto

Tras un INSERT, UPDATE o DELETE, a menudo la aplicación necesita conocer algún valor de la fila afectada: la clave que ha generado una identidad, el nuevo salario tras aplicar una fórmula… En lugar de lanzar un SELECT posterior, Oracle permite devolverlo en la propia sentencia con RETURNING ... INTO.

⚠️ Importante: RETURNING ... INTO deposita los valores en variables, y las variables son cosa de un lenguaje anfitrión: PL/SQL, Java (JDBC), Python, PHP… Su uso completo (sobre todo con varias filas, mediante BULK COLLECT) pertenece al Curso de Oracle PL/SQL. Aquí solo veremos lo que se puede hacer desde *SQLPlus** (o SQLcl) con variables de enlace (bind variables) declaradas con VARIABLE, que no son PL/SQL sino órdenes de la herramienta.

6.2. RETURNING en SQL*Plus con una fila

VARIABLE v_salario NUMBER
VARIABLE v_nombre  VARCHAR2(30)

UPDATE empleados
SET    salario = salario * 1.10
WHERE  emp_id = 121
RETURNING nombre, salario INTO :v_nombre, :v_salario;

PRINT v_nombre v_salario
1 fila actualizada.

V_NOMBRE
--------------------------------
Raquel

 V_SALARIO
----------
      3080

Con INSERT sirve para recuperar la clave insertada (muy útil con identidades y secuencias):

VARIABLE v_id NUMBER
INSERT INTO departamentos (dept_id, nombre) VALUES (81, 'AUDITORÍA')
RETURNING dept_id INTO :v_id;
PRINT v_id
1 fila creada.

      V_ID
----------
        81

6.3. Con varias filas: agregados o error

Una variable escalar solo puede recibir un valor. Si la sentencia afecta a varias filas, puedes devolver un agregado (SUM, COUNT, MAX…):

VARIABLE v_total NUMBER
UPDATE empleados
SET    salario = salario * 1.10
WHERE  dept_id = 30
RETURNING SUM(salario) INTO :v_total;
PRINT v_total
ROLLBACK;

VARIABLE v_borrados NUMBER
DELETE FROM asignaciones WHERE proyecto_id = 1
RETURNING COUNT(*) INTO :v_borrados;
PRINT v_borrados
ROLLBACK;
6 filas actualizadas.

   V_TOTAL
----------
     20680

Rollback terminado.

3 filas suprimidas.

V_BORRADOS
----------
         3

Rollback terminado.

Si intentas devolver una columna sin agregar desde varias filas a una variable escalar, SQL*Plus da un error del cliente:

SELECT emp_id, salario FROM empleados WHERE dept_id = 30 ORDER BY emp_id;

UPDATE empleados
SET    salario = salario * 1.10
WHERE  dept_id = 30
RETURNING salario INTO :v_salario;

SELECT emp_id, salario FROM empleados WHERE dept_id = 30 ORDER BY emp_id;
ROLLBACK;
    EMP_ID    SALARIO
---------- ----------
       102       5000
       120       3500
       121       2800
       122       2600
       123       2400
       124       2500

6 filas seleccionadas.

UPDATE empleados
*
ERROR en línea 1:
ORA-24369: required callbacks not registered for one or more  bind handles

    EMP_ID    SALARIO
---------- ----------
       102       5500
       120       3850
       121       3080
       122       2860
       123       2640
       124       2750

6 filas seleccionadas.

Rollback terminado.

⚠️ Importante: Fíjate en el detalle: aunque aparece un ERROR, los salarios sí se han actualizado. El servidor ejecutó el UPDATE correctamente; el error lo produce la capa cliente (OCI) al no poder recoger varios valores en una variable escalar. No asumas que “ha dado error, luego no ha hecho nada”: comprueba siempre y, si no estás seguro, ROLLBACK. Para recoger varios valores se necesita una colección de PL/SQL (RETURNING ... BULK COLLECT INTO).

6.4. RETURNING OLD y NEW (Oracle 23ai)

⚠️ Importante: Disponible desde Oracle 23ai.

En 23ai puedes pedir el valor anterior (OLD) y el nuevo (NEW) de una columna en un UPDATE:

VARIABLE v_antes   NUMBER
VARIABLE v_despues NUMBER

UPDATE empleados
SET    salario = salario + 250
WHERE  emp_id = 122
RETURNING OLD salario, NEW salario INTO :v_antes, :v_despues;

PRINT v_antes v_despues
ROLLBACK;
1 fila actualizada.

   V_ANTES
----------
      2600

 V_DESPUES
----------
      2850

Rollback terminado.

Sin OLD/NEW, RETURNING devuelve siempre el valor nuevo en UPDATE e INSERT, y el valor que tenía la fila borrada en DELETE.


7. Integridad y DML

7.1. Cuándo se comprueban las restricciones

Por defecto, Oracle comprueba las restricciones al terminar cada sentencia (no fila a fila, ni al final de la transacción). Por eso:

  • Una sentencia que viola una restricción en una sola fila se deshace entera (atomicidad de sentencia), pero las sentencias anteriores de la transacción siguen vigentes.
  • Dentro de una misma sentencia pueden existir estados intermedios inválidos siempre que el resultado final sea válido. Por ejemplo, UPDATE t SET id = id + 1 sobre una PK con los valores 1, 2, 3 funciona, aunque al procesar el 1 pase transitoriamente a coincidir con el 2.
Momento Qué ocurre si se viola una restricción
Restricción inmediata (por defecto) Falla la sentencia y se deshace; la transacción sigue abierta
Restricción diferida (DEFERRABLE INITIALLY DEFERRED) Se comprueba en el COMMIT; si falla, se deshace toda la transacción (ORA-02091)

7.2. Orden de carga: primero los padres

Las claves ajenas obligan a insertar en orden: primero DEPARTAMENTOS, luego EMPLEADOS (y dentro de ellos, primero los jefes), luego CLIENTES, PEDIDOS y LINEAS_PEDIDO. Para borrar, el orden es el inverso (salvo que haya ON DELETE CASCADE). Es exactamente lo que hacen empresa_datos.sql y empresa_borrar.sql.

¿Y si hay un ciclo? Por ejemplo, un equipo tiene un capitán (que es un jugador) y cada jugador pertenece a un equipo. Ninguna de las dos filas puede insertarse primero. La solución son las restricciones diferibles, que se comprueban en el COMMIT:

CREATE TABLE equipos_demo (
    equipo_id   NUMBER CONSTRAINT pk_equipos_demo PRIMARY KEY,
    nombre      VARCHAR2(30) NOT NULL,
    capitan_id  NUMBER
);
CREATE TABLE jugadores_demo (
    jugador_id  NUMBER CONSTRAINT pk_jugadores_demo PRIMARY KEY,
    nombre      VARCHAR2(30) NOT NULL,
    equipo_id   NUMBER NOT NULL
        CONSTRAINT fk_jug_equipo REFERENCES equipos_demo (equipo_id)
        DEFERRABLE INITIALLY DEFERRED
);
ALTER TABLE equipos_demo ADD CONSTRAINT fk_equ_capitan
    FOREIGN KEY (capitan_id) REFERENCES jugadores_demo (jugador_id)
    DEFERRABLE INITIALLY DEFERRED;

INSERT INTO jugadores_demo VALUES (1, 'Lola', 10);         -- el equipo 10 aún no existe
INSERT INTO equipos_demo VALUES (10, 'Las Águilas', 1);
COMMIT;                                                   -- aquí se comprueba: todo cuadra
1 fila creada.

1 fila creada.

Confirmación terminada.

Si al llegar al COMMIT algo no cuadra, se deshace toda la transacción:

INSERT INTO jugadores_demo VALUES (2, 'Paco', 20);         -- el equipo 20 no existe
COMMIT;
SELECT COUNT(*) FROM jugadores_demo;
1 fila creada.

COMMIT
*
ERROR en línea 1:
ORA-02091: transaction rolled back
ORA-02291: integrity constraint (CURSO.FK_JUG_EQUIPO) violated - parent key not found

  COUNT(*)
----------
         1

1 fila seleccionada.

Con SET CONSTRAINT nombre IMMEDIATE (o SET CONSTRAINTS ALL IMMEDIATE) puedes volver a la comprobación inmediata dentro de la transacción actual:

SET CONSTRAINT fk_jug_equipo IMMEDIATE;
INSERT INTO jugadores_demo VALUES (3, 'Rosa', 30);
ROLLBACK;

DROP TABLE equipos_demo CASCADE CONSTRAINTS PURGE;
DROP TABLE jugadores_demo CASCADE CONSTRAINTS PURGE;
Restricción definida.

INSERT INTO jugadores_demo VALUES (3, 'Rosa', 30)
*
ERROR en línea 1:
ORA-02291: integrity constraint (CURSO.FK_JUG_EQUIPO) violated - parent key not found

Rollback terminado.

7.3. DML a través de vistas

Puedes ejecutar INSERT, UPDATE y DELETE sobre una vista (módulo 26) si es actualizable (una tabla base con clave preservada, sin DISTINCT, GROUP BY, funciones de agregado, etc.). Con WITH CHECK OPTION, Oracle impide que el cambio deje la fila fuera de la vista:

CREATE OR REPLACE VIEW v_emp_ventas AS
SELECT emp_id, nombre, apellidos, email, fecha_alta, oficio, salario, dept_id
FROM   empleados
WHERE  dept_id = 20
WITH CHECK OPTION CONSTRAINT ck_v_emp_ventas;

UPDATE v_emp_ventas SET salario = salario + 50 WHERE emp_id = 113;    -- correcto
UPDATE v_emp_ventas SET dept_id = 30 WHERE emp_id = 113;              -- sacaría la fila de la vista
INSERT INTO v_emp_ventas
VALUES (175, 'Rita', 'Luna Paz', 'rita.luna@empresa.es', DATE '2026-10-01', 'VENDEDOR', 1900, 50);
ROLLBACK;
DROP VIEW v_emp_ventas;
Vista creada.

1 fila actualizada.

UPDATE v_emp_ventas SET dept_id = 30 WHERE emp_id = 113
       *
ERROR en línea 1:
ORA-01402: view WITH CHECK OPTION where-clause violation

INSERT INTO v_emp_ventas VALUES (175, 'Rita', 'Luna Paz', 'rita.luna@empresa.es', DATE '2026-10-01', 'VENDEDOR', 1900, 50)
            *
ERROR en línea 1:
ORA-01402: view WITH CHECK OPTION where-clause violation

Rollback terminado.

Vista borrada.

💡 Consejo: Algunas reglas de negocio no se pueden expresar con restricciones declarativas (por ejemplo, “un empleado no puede cobrar más que su jefe”, que implica comparar dos filas). En Oracle se implementan con triggers, que pertenecen a PL/SQL y se estudian en el Curso de Oracle PL/SQL.


8. Buenas prácticas

8.1. El método “SELECT primero”

Antes de cualquier UPDATE o DELETE sobre datos reales:

  1. Escribe un SELECT con exactamente el mismo WHERE (y, para un UPDATE, muestra la columna actual y la expresión nueva).
  2. Comprueba el número de filas y su contenido.
  3. Convierte el SELECT en UPDATE/DELETE sin tocar el WHERE.
  4. Compara el número de filas afectadas con el del SELECT.
  5. Verifica el resultado y solo entonces haz COMMIT.
-- Paso 1: previsualizar el cambio
SELECT emp_id, nombre, salario, ROUND(salario * 1.04, 2) AS salario_nuevo
FROM   empleados
WHERE  oficio = 'PROGRAMADOR';

-- Paso 3: mismo WHERE, ahora UPDATE
UPDATE empleados
SET    salario = ROUND(salario * 1.04, 2)
WHERE  oficio = 'PROGRAMADOR';

8.2. Lista de comprobación

Práctica Por qué
Escribe siempre la lista de columnas en INSERT El código sobrevive a cambios en la tabla y se lee mejor
Usa literales DATE '...' o TO_DATE con formato No dependes de la configuración NLS de la sesión
Filtra por clave primaria cuando modifiques una fila concreta Garantiza que solo se toca esa fila
Lee el contador de filas afectadas Detecta al instante un WHERE mal escrito
Trabaja con autocommit desactivado y confirma explícitamente Siempre tendrás la red de seguridad del ROLLBACK
Agrupa en una transacción las sentencias que van juntas Un pedido y sus líneas se confirman a la vez o no se confirman
Haz copia antes de cambios masivos (CREATE TABLE copia AS SELECT ...) Permite recuperar datos aunque ya hayas confirmado
En UPDATE correlacionados, añade WHERE EXISTS Evita poner a NULL filas sin correspondencia
Prefiere NOT EXISTS a NOT IN con columnas que admiten nulos Evita el resultado vacío inesperado
En MERGE y UPDATE ... FROM, empareja por una clave del origen Evita ORA-30926
Usa MERGE para sincronizaciones en lugar de DELETE + INSERT Una sola pasada, menos bloqueos, conserva las filas que no cambian
No uses ON DELETE CASCADE “por comodidad” Un borrado accidental se propaga en cascada
No modifiques claves primarias Oracle no propaga los cambios a las hijas

💡 Consejo: En SQL Developer, el botón Commit (o F11) y Rollback (F12) están en la barra de la hoja de trabajo, y el registro de la parte inferior muestra cuántas filas ha afectado cada sentencia. Con autocommit desactivado (por defecto en SQL Developer), al cerrar la conexión con cambios pendientes te pregunta si quieres confirmarlos.


9. Ejercicios resueltos

Todos los ejercicios parten del esquema EMPRESA original y terminan con ROLLBACK (o borrando las tablas auxiliares) para dejarlo intacto. Las salidas son las reales de Oracle 26ai.

Ejercicio 1: Alta de un departamento

Da de alta el departamento 80, CALIDAD, con sede en Málaga y sin presupuesto asignado. Usa lista de columnas.

Ver solución
INSERT INTO departamentos (dept_id, nombre, localidad)
VALUES (80, 'CALIDAD', 'Málaga');

SELECT * FROM departamentos WHERE dept_id = 80;
ROLLBACK;
1 fila creada.

   DEPT_ID NOMBRE                    LOCALIDAD                                PRESUPUESTO
---------- ------------------------- ---------------------------------------- -----------
        80 CALIDAD                   Málaga                                   (nulo)

1 fila seleccionada.

Rollback terminado.

Al omitir presupuesto en la lista, toma NULL (no tiene DEFAULT). También valdría escribirla con NULL explícito.

Ejercicio 2: Alta de un empleado

Da de alta a Olga Prieto Sanz (empleado 170, olga.prieto@empresa.es), programadora del departamento 30 a las órdenes de Pablo (120), con fecha de alta 1 de octubre de 2026 y salario de 2300 €. No tiene teléfono ni comisión.

Ver solución
INSERT INTO empleados (emp_id, nombre, apellidos, email, fecha_alta,
                       oficio, salario, jefe_id, dept_id)
VALUES (170, 'Olga', 'Prieto Sanz', 'olga.prieto@empresa.es', DATE '2026-10-01',
        'PROGRAMADOR', 2300, 120, 30);

SELECT emp_id, nombre, telefono, comision, jefe_id, dept_id
FROM   empleados
WHERE  emp_id = 170;
ROLLBACK;
1 fila creada.

    EMP_ID NOMBRE                    TELEFONO          COMISION    JEFE_ID    DEPT_ID
---------- ------------------------- --------------- ---------- ---------- ----------
       170 Olga                      (nulo)          (nulo)            120         30

1 fila seleccionada.

Rollback terminado.

Errores típicos: olvidar una columna NOT NULL (email, fecha_alta, oficio…) da ORA-01400; escribir '01/10/2026' sin TO_DATE funciona solo si coincide con el NLS_DATE_FORMAT de la sesión.

Ejercicio 3: Valores por defecto

Da de alta al cliente 1015, Librería Cervantes (NIF B11223344, Cuenca), asignado al comercial 113, dejando que la fecha de alta y el límite de crédito tomen sus valores por defecto. Comprueba qué valores se han guardado.

Ver solución
INSERT INTO clientes (cliente_id, nombre, nif, ciudad, provincia, comercial_id)
VALUES (1015, 'Librería Cervantes', 'B11223344', 'Cuenca', 'Cuenca', 113);

SELECT cliente_id, nombre, fecha_alta, limite_credito, comercial_id
FROM   clientes
WHERE  cliente_id = 1015;
ROLLBACK;
1 fila creada.

CLIENTE_ID NOMBRE                    FECHA_ALTA LIMITE_CREDITO COMERCIAL_ID
---------- ------------------------- ---------- -------------- ------------
      1015 Librería Cervantes        03/10/2026           3000          113

1 fila seleccionada.

Rollback terminado.

fecha_alta recibe SYSDATE (con hora) y limite_credito 3000. Se obtiene lo mismo escribiendo DEFAULT en VALUES; en cambio, escribir NULL dejaría el límite a NULL y la fecha daría ORA-01400.

Ejercicio 4: Inserción multifila

Inserta con una sola sentencia tres categorías nuevas: 12 Gaming (hija de 1), 13 Accesorios móvil (hija de 8) y 14 Papelería (hija de 10). Da la versión para 23ai y una alternativa válida en 19c.

Ver solución

Versión 23ai/26ai:

INSERT INTO categorias (categoria_id, nombre, categoria_padre_id)
VALUES (12, 'Gaming',           1),
       (13, 'Accesorios móvil',  8),
       (14, 'Papelería',        10);

SELECT * FROM categorias WHERE categoria_id >= 12;
ROLLBACK;
3 filas creadas.

CATEGORIA_ID NOMBRE                    CATEGORIA_PADRE_ID
------------ ------------------------- ------------------
          12 Gaming                                     1
          13 Accesorios móvil                           8
          14 Papelería                                 10

3 filas seleccionadas.

Rollback terminado.

Alternativa clásica (19c):

INSERT ALL
    INTO categorias VALUES (12, 'Gaming',           1)
    INTO categorias VALUES (13, 'Accesorios móvil',  8)
    INTO categorias VALUES (14, 'Papelería',        10)
SELECT * FROM dual;
ROLLBACK;
3 filas creadas.

Rollback terminado.

INSERT ALL exige una consulta al final; SELECT * FROM dual devuelve una fila, así que cada INTO se ejecuta una vez.

Registra el pedido 5025 del cliente 1013, gestionado por Carmen (110) el 2 de octubre de 2026, con dos líneas: 2 unidades del producto 10 y 1 del producto 17. El precio unitario de cada línea debe tomarse del catálogo actual, no escribirse a mano.

Ver solución
INSERT INTO pedidos (pedido_id, cliente_id, empleado_id, fecha_pedido)
VALUES (5025, 1013, 110, DATE '2026-10-02');

INSERT INTO lineas_pedido (pedido_id, num_linea, producto_id, cantidad, precio_unitario)
SELECT 5025, 1, producto_id, 2, precio FROM productos WHERE producto_id = 10;

INSERT INTO lineas_pedido (pedido_id, num_linea, producto_id, cantidad, precio_unitario)
SELECT 5025, 2, producto_id, 1, precio FROM productos WHERE producto_id = 17;

SELECT p.pedido_id, p.estado, l.num_linea, l.producto_id, l.cantidad,
       l.precio_unitario, l.descuento
FROM   pedidos p JOIN lineas_pedido l ON l.pedido_id = p.pedido_id
WHERE  p.pedido_id = 5025;
ROLLBACK;
1 fila creada.

1 fila creada.

1 fila creada.

 PEDIDO_ID ESTADO        NUM_LINEA PRODUCTO_ID   CANTIDAD PRECIO_UNITARIO  DESCUENTO
---------- ------------ ---------- ----------- ---------- --------------- ----------
      5025 PENDIENTE             1          10          2             329          0
      5025 PENDIENTE             2          17          1              45          0

2 filas seleccionadas.

Rollback terminado.

INSERT ... SELECT permite mezclar literales (5025, 1, 2) con columnas de la consulta (precio). El precio se guarda en la línea porque es un dato histórico: si el catálogo cambia mañana, el pedido debe conservar el precio al que se vendió.

Ejercicio 6: Copia de clientes de una provincia

Crea una tabla clientes_valencia con la misma estructura que clientes pero vacía, y cárgala después con los clientes de la provincia de Valencia.

Ver solución
CREATE TABLE clientes_valencia AS
SELECT * FROM clientes WHERE 1 = 0;

SELECT COUNT(*) FROM clientes_valencia;

INSERT INTO clientes_valencia
SELECT * FROM clientes WHERE provincia = 'Valencia';

SELECT cliente_id, nombre, ciudad FROM clientes_valencia;
ROLLBACK;
DROP TABLE clientes_valencia PURGE;
Tabla creada.

  COUNT(*)
----------
         0

1 fila seleccionada.

2 filas creadas.

CLIENTE_ID NOMBRE                    CIUDAD
---------- ------------------------- ----------------------------------------
      1001 Talleres Martín S.L.      Valencia
      1013 Carlos Ibáñez Rey         Valencia

2 filas seleccionadas.

Rollback terminado.

Tabla borrada.

WHERE 1 = 0 es el truco clásico para copiar la estructura sin datos. Aquí INSERT ... SELECT * sin lista de columnas es aceptable porque ambas tablas tienen exactamente las mismas columnas en el mismo orden. Fíjate en que el ROLLBACK deshace el INSERT, pero no el CREATE TABLE (DDL).

Ejercicio 7: Tabla de ventas por producto

Crea la tabla ventas_producto (producto_id PK, nombre, unidades, importe) y cárgala con las unidades vendidas y el importe neto (con descuentos) de todos los productos, también los que nunca se han vendido (con 0). Muestra los cinco productos con más importe y los que no se han vendido.

Ver solución
CREATE TABLE ventas_producto (
    producto_id  NUMBER(6) CONSTRAINT pk_ventas_producto PRIMARY KEY,
    nombre       VARCHAR2(60),
    unidades     NUMBER(8),
    importe      NUMBER(12,2)
);

INSERT INTO ventas_producto (producto_id, nombre, unidades, importe)
SELECT pr.producto_id,
       pr.nombre,
       NVL(SUM(l.cantidad), 0),
       NVL(ROUND(SUM(l.cantidad * l.precio_unitario * (1 - l.descuento / 100)), 2), 0)
FROM   productos pr
       LEFT JOIN lineas_pedido l ON l.producto_id = pr.producto_id
GROUP  BY pr.producto_id, pr.nombre;

SELECT * FROM ventas_producto ORDER BY importe DESC FETCH FIRST 5 ROWS ONLY;
SELECT * FROM ventas_producto WHERE unidades = 0;
ROLLBACK;
DROP TABLE ventas_producto PURGE;
Tabla creada.

20 filas creadas.

PRODUCTO_ID NOMBRE                      UNIDADES    IMPORTE
----------- ------------------------- ---------- ----------
          1 Portátil UltraBook 14             15   12361,25
          3 Portátil Básico 15                24      10479
          4 Torre Oficina i5                  11       6490
          5 Torre Workstation                  3    5146,29
          9 Monitor 24 FHD                    40     4934,5

5 filas seleccionadas.

PRODUCTO_ID NOMBRE                      UNIDADES    IMPORTE
----------- ------------------------- ---------- ----------
         14 Altavoz Inteligente                0          0

1 fila seleccionada.

Rollback terminado.

Tabla borrada.

El LEFT JOIN conserva el producto 14 y NVL convierte en 0 sus sumas nulas. Con un JOIN interno se habrían cargado solo 19 filas.

Ejercicio 8: Reparto de pedidos con INSERT ALL

Crea dos tablas vacías pedidos_abiertos y pedidos_cerrados con las columnas pedido_id, cliente_id y estado, y repártelos con una sola sentencia: los PENDIENTE y ENVIADO a la primera y los ENTREGADO y CANCELADO a la segunda.

Ver solución
CREATE TABLE pedidos_abiertos AS SELECT pedido_id, cliente_id, estado FROM pedidos WHERE 1 = 0;
CREATE TABLE pedidos_cerrados AS SELECT pedido_id, cliente_id, estado FROM pedidos WHERE 1 = 0;

INSERT ALL
    WHEN estado IN ('PENDIENTE', 'ENVIADO') THEN
        INTO pedidos_abiertos (pedido_id, cliente_id, estado) VALUES (pedido_id, cliente_id, estado)
    WHEN estado IN ('ENTREGADO', 'CANCELADO') THEN
        INTO pedidos_cerrados (pedido_id, cliente_id, estado) VALUES (pedido_id, cliente_id, estado)
SELECT pedido_id, cliente_id, estado
FROM   pedidos;

SELECT * FROM pedidos_abiertos ORDER BY pedido_id;
SELECT COUNT(*) FROM pedidos_cerrados;
ROLLBACK;
DROP TABLE pedidos_abiertos PURGE;
DROP TABLE pedidos_cerrados PURGE;
24 filas creadas.

 PEDIDO_ID CLIENTE_ID ESTADO
---------- ---------- ------------
      5020       1007 ENVIADO
      5021       1001 ENVIADO
      5022       1012 PENDIENTE
      5023       1009 PENDIENTE
      5024       1008 PENDIENTE

5 filas seleccionadas.

  COUNT(*)
----------
        19

1 fila seleccionada.

Como las condiciones son excluyentes, INSERT ALL e INSERT FIRST dan el mismo resultado. Si una condición pudiera solaparse con otra, habría que pensar cuál de los dos se necesita.

Ejercicio 9: Clasificación de clientes con INSERT FIRST

Reparte los clientes en tres tablas según su límite de crédito: clientes_vip (15 000 € o más), clientes_normal (5000 € o más) y clientes_basico (el resto, incluidos los que tengan límite nulo).

Ver solución
CREATE TABLE clientes_vip     (cliente_id NUMBER(6), nombre VARCHAR2(60), limite_credito NUMBER(10,2));
CREATE TABLE clientes_normal  (cliente_id NUMBER(6), nombre VARCHAR2(60), limite_credito NUMBER(10,2));
CREATE TABLE clientes_basico  (cliente_id NUMBER(6), nombre VARCHAR2(60), limite_credito NUMBER(10,2));

INSERT FIRST
    WHEN limite_credito >= 15000 THEN
        INTO clientes_vip    VALUES (cliente_id, nombre, limite_credito)
    WHEN limite_credito >= 5000 THEN
        INTO clientes_normal VALUES (cliente_id, nombre, limite_credito)
    ELSE
        INTO clientes_basico VALUES (cliente_id, nombre, limite_credito)
SELECT cliente_id, nombre, limite_credito
FROM   clientes;

SELECT * FROM clientes_vip ORDER BY limite_credito DESC;
SELECT 'NORMAL' AS grupo, COUNT(*) AS n FROM clientes_normal
UNION ALL
SELECT 'BASICO', COUNT(*) FROM clientes_basico;
ROLLBACK;
DROP TABLE clientes_vip PURGE;
DROP TABLE clientes_normal PURGE;
DROP TABLE clientes_basico PURGE;
14 filas creadas.

CLIENTE_ID NOMBRE                         LIMITE_CREDITO
---------- ------------------------------ --------------
      1002 Construcciones Levante S.A.             25000
      1010 Instituto Tecnológico Sur               20000
      1007 Hotel Mar Azul S.L.                     15000

3 filas seleccionadas.

GRUPO           N
------ ----------
NORMAL          5
BASICO          6

2 filas seleccionadas.

Con INSERT FIRST, un cliente de 25 000 € entra solo en clientes_vip aunque también cumpla >= 5000. Un límite NULL no cumple ninguna condición (NULL >= 15000 es desconocido) y cae en el ELSE, como pide el enunciado.

Ejercicio 10: Proyectos con secuencia

Crea la secuencia seq_proyectos empezando en 6 y úsala para dar de alta dos proyectos: “Renovación web corporativa” (dept. 50, inicio 1/11/2026, 25 000 €) y “Auditoría ISO 27001” (dept. 30, inicio 10/1/2027, 18 000 €). Haz ROLLBACK, vuelve a insertar el primero y explica el número que recibe.

Ver solución
CREATE SEQUENCE seq_proyectos START WITH 6 INCREMENT BY 1 NOCACHE;

INSERT INTO proyectos (proyecto_id, nombre, dept_id, fecha_inicio, presupuesto)
VALUES (seq_proyectos.NEXTVAL, 'Renovación web corporativa', 50, DATE '2026-11-01', 25000);

INSERT INTO proyectos (proyecto_id, nombre, dept_id, fecha_inicio, presupuesto)
VALUES (seq_proyectos.NEXTVAL, 'Auditoría ISO 27001', 30, DATE '2027-01-10', 18000);

SELECT proyecto_id, nombre, dept_id, fecha_inicio FROM proyectos WHERE proyecto_id >= 6;
ROLLBACK;

INSERT INTO proyectos (proyecto_id, nombre, dept_id, fecha_inicio)
VALUES (seq_proyectos.NEXTVAL, 'Renovación web corporativa', 50, DATE '2026-11-01');
SELECT proyecto_id, nombre FROM proyectos WHERE proyecto_id >= 6;
ROLLBACK;
DROP SEQUENCE seq_proyectos;
Secuencia creada.

1 fila creada.

1 fila creada.

PROYECTO_ID NOMBRE                            DEPT_ID FECHA_INIC
----------- ------------------------------ ---------- ----------
          6 Renovación web corporativa             50 01/11/2026
          7 Auditoría ISO 27001                    30 10/01/2027

2 filas seleccionadas.

Rollback terminado.

1 fila creada.

PROYECTO_ID NOMBRE
----------- ------------------------------
          8 Renovación web corporativa

1 fila seleccionada.

Rollback terminado.

Secuencia borrada.

El proyecto recibe el 8, no el 6: el ROLLBACK deshace las filas, pero los números 6 y 7 ya se entregaron y la secuencia no retrocede.

Ejercicio 11: Tabla con identidad

Crea la tabla notas_cliente con una clave nota_id generada siempre por identidad, una FK a clientes, un texto obligatorio y una fecha por defecto. Inserta tres notas sin indicar la clave y comprueba la numeración.

Ver solución
CREATE TABLE notas_cliente (
    nota_id     NUMBER GENERATED ALWAYS AS IDENTITY CONSTRAINT pk_notas_cliente PRIMARY KEY,
    cliente_id  NUMBER(6) NOT NULL CONSTRAINT fk_not_cliente REFERENCES clientes (cliente_id),
    texto       VARCHAR2(200) NOT NULL,
    fecha       DATE DEFAULT SYSDATE NOT NULL
);

INSERT INTO notas_cliente (cliente_id, texto) VALUES (1001, 'Solicita presupuesto de 5 portátiles');
INSERT INTO notas_cliente (cliente_id, texto) VALUES (1007, 'Reclamación por retraso en la entrega');
INSERT INTO notas_cliente (cliente_id, texto) VALUES (1001, 'Presupuesto aceptado');

COLUMN texto FORMAT A40
SELECT nota_id, cliente_id, texto FROM notas_cliente ORDER BY nota_id;
ROLLBACK;
DROP TABLE notas_cliente PURGE;
Tabla creada.

1 fila creada.

1 fila creada.

1 fila creada.

   NOTA_ID CLIENTE_ID TEXTO
---------- ---------- ----------------------------------------
         1       1001 Solicita presupuesto de 5 portátiles
         2       1007 Reclamación por retraso en la entrega
         3       1001 Presupuesto aceptado

3 filas seleccionadas.

Rollback terminado.

Tabla borrada.

Si intentas insertar una nota de un cliente inexistente (2000) obtienes ORA-02291: integrity constraint (CURSO.FK_NOT_CLIENTE) violated - parent key not found.

Ejercicio 12: Corregir un INSERT erróneo

Este INSERT falla. Explica por qué y corrígelo.

INSERT INTO empleados (emp_id, nombre, apellidos, email, fecha_alta, oficio, salario, dept_id)
VALUES (171, 'Ana', 'García Pons', 'ana.garcia@empresa.es', DATE '2026-10-01', 'CONTABLE', 2100, 40);
Ver solución
INSERT INTO empleados (emp_id, nombre, apellidos, email, fecha_alta, oficio, salario, dept_id)
*
ERROR en línea 1:
ORA-00001: unique constraint (CURSO.UQ_EMP_EMAIL) violated on table CURSO.EMPLEADOS columns (EMAIL)
ORA-03301: (ORA-00001 details) row with column values (EMAIL:'ana.garcia@empresa.es') already exists

La clave primaria 171 está libre, pero el email ya lo usa la directora general (restricción uq_emp_email). El mensaje indica la restricción y el valor repetido. Corrección:

INSERT INTO empleados (emp_id, nombre, apellidos, email, fecha_alta, oficio, salario, dept_id)
VALUES (171, 'Ana', 'García Pons', 'ana.garcia2@empresa.es', DATE '2026-10-01', 'CONTABLE', 2100, 40);
ROLLBACK;
1 fila creada.

Rollback terminado.

Ejercicio 13: Subida por oficio

Sube un 4 % el salario de todos los programadores, redondeando a dos decimales. Muestra los salarios antes y después.

Ver solución
SELECT emp_id, nombre, salario FROM empleados WHERE oficio = 'PROGRAMADOR';

UPDATE empleados
SET    salario = ROUND(salario * 1.04, 2)
WHERE  oficio = 'PROGRAMADOR';

SELECT emp_id, nombre, salario FROM empleados WHERE oficio = 'PROGRAMADOR';
ROLLBACK;
    EMP_ID NOMBRE                       SALARIO
---------- ------------------------- ----------
       121 Raquel                          2800
       122 Iván                            2600
       123 Sara                            2400

3 filas seleccionadas.

3 filas actualizadas.

    EMP_ID NOMBRE                       SALARIO
---------- ------------------------- ----------
       121 Raquel                          2912
       122 Iván                            2704
       123 Sara                            2496

3 filas seleccionadas.

Rollback terminado.

Ejercicio 14: Cambio de departamento

Sergio (113) pasa al departamento de Marketing (50) como analista, a las órdenes de Laura (104). Hazlo con una sola sentencia.

Ver solución
UPDATE empleados
SET    dept_id = 50,
       jefe_id = 104,
       oficio  = 'ANALISTA'
WHERE  emp_id = 113;

SELECT emp_id, nombre, oficio, jefe_id, dept_id FROM empleados WHERE emp_id = 113;
ROLLBACK;
1 fila actualizada.

    EMP_ID NOMBRE                    OFICIO                            JEFE_ID    DEPT_ID
---------- ------------------------- ------------------------------ ---------- ----------
       113 Sergio                    ANALISTA                              104         50

1 fila seleccionada.

Rollback terminado.

Un error típico es separar las asignaciones con AND en lugar de comas. En versiones anteriores a 23ai es un error de sintaxis, pero en 26ai, que tiene tipo BOOLEAN, la sentencia es válida y significa algo muy distinto: dept_id = (50 AND jefe_id = 104). La expresión booleana se evalúa (aquí es falsa, porque el jefe de Sergio es el 101), se convierte a número y se intenta asignar a dept_id:

UPDATE empleados SET dept_id = 50 AND jefe_id = 104 WHERE emp_id = 113;
UPDATE empleados SET dept_id = 50 AND jefe_id = 104 WHERE emp_id = 113
*
ERROR en línea 1:
ORA-02291: integrity constraint (CURSO.FK_EMP_DEPT) violated - parent key not found

Nos ha salvado la clave ajena (no existe el departamento 0). Si la columna no tuviera FK, el UPDATE habría guardado un valor absurdo sin ningún aviso. Las asignaciones se separan siempre con comas.

Ejercicio 15: Comisiones nulas

Pon la comisión a 0 a los empleados del departamento 20 que la tengan a NULL. ¿Qué ocurre si escribes comision = NULL en el WHERE?

Ver solución
SELECT emp_id, nombre, comision FROM empleados WHERE dept_id = 20 AND comision IS NULL;

UPDATE empleados
SET    comision = 0
WHERE  dept_id = 20
AND    comision IS NULL;

-- Versión errónea
UPDATE empleados
SET    comision = 0
WHERE  dept_id = 20
AND    comision = NULL;
ROLLBACK;
    EMP_ID NOMBRE                      COMISION
---------- ------------------------- ----------
       101 Luis                      (nulo)
       114 Lucía                     (nulo)

2 filas seleccionadas.

2 filas actualizadas.

0 filas actualizadas.

Rollback terminado.

comision = NULL nunca es verdadero (es desconocido), así que la versión errónea no modifica nada y no da error. Fíjate en que en el SET sí se usa = para asignar (SET comision = NULL es correcto para vaciar una columna); en el WHERE hay que usar IS NULL.

Ejercicio 16: Rebaja de productos poco vendidos

Rebaja un 15 % el precio de los productos no descatalogados de los que se han vendido menos de 5 unidades en total.

Ver solución

Primero miramos qué productos cumplen la condición:

SELECT pr.producto_id, pr.nombre, pr.precio, SUM(l.cantidad) AS unidades
FROM   productos pr JOIN lineas_pedido l ON l.producto_id = pr.producto_id
GROUP  BY pr.producto_id, pr.nombre, pr.precio
HAVING SUM(l.cantidad) < 5
ORDER  BY 1;
PRODUCTO_ID NOMBRE                        PRECIO   UNIDADES
----------- ------------------------- ---------- ----------
          2 Portátil Gaming 16              1499          3
          5 Torre Workstation               1899          3
         12 Smartphone Gama Alta             999          2
         18 Impresora Láser                  189          2
         20 Tablet 10                        279          3

5 filas seleccionadas.
UPDATE productos pr
SET    pr.precio = ROUND(pr.precio * 0.85, 2)
WHERE  pr.descatalogado = 'N'
AND    (SELECT NVL(SUM(l.cantidad), 0)
        FROM   lineas_pedido l
        WHERE  l.producto_id = pr.producto_id) < 5;

SELECT producto_id, nombre, precio FROM productos WHERE producto_id IN (2, 5, 12, 18, 20);
ROLLBACK;
5 filas actualizadas.

PRODUCTO_ID NOMBRE                             PRECIO
----------- ------------------------------ ----------
          2 Portátil Gaming 16                1274,15
          5 Torre Workstation                 1614,15
         12 Smartphone Gama Alta               849,15
         18 Impresora Láser                    160,65
         20 Tablet 10                          237,15

5 filas seleccionadas.

Rollback terminado.

La subconsulta correlacionada con NVL(..., 0) incluiría también productos nunca vendidos (0 < 5); el único es el 14, que queda fuera por estar descatalogado.

Ejercicio 17: Premio al mejor vendedor

Suma 300 € a la comisión del empleado que más importe ha vendido (sin contar pedidos cancelados).

Ver solución
UPDATE empleados
SET    comision = NVL(comision, 0) + 300
WHERE  emp_id = (SELECT p.empleado_id
                 FROM   pedidos p
                        JOIN lineas_pedido l ON l.pedido_id = p.pedido_id
                 WHERE  p.estado <> 'CANCELADO'
                 AND    p.empleado_id IS NOT NULL
                 GROUP  BY p.empleado_id
                 ORDER  BY SUM(l.cantidad * l.precio_unitario * (1 - l.descuento / 100)) DESC
                 FETCH FIRST 1 ROW ONLY);

SELECT emp_id, nombre, comision FROM empleados WHERE emp_id = 112;
ROLLBACK;
1 fila actualizada.

    EMP_ID NOMBRE                      COMISION
---------- ------------------------- ----------
       112 Elena                            600

1 fila seleccionada.

Rollback terminado.

Elena (23 833,55 €) tenía 300 € de comisión y pasa a 600 €. El filtro empleado_id IS NOT NULL evita que los pedidos online sin empleado formen un grupo NULL que podría ganar. Alternativa compatible con versiones antiguas: HAVING SUM(...) = (SELECT MAX(SUM(...)) ... GROUP BY ...).

Ejercicio 18: Presupuesto según horas

Recalcula el presupuesto de cada proyecto como 50 € por cada hora asignada. Los proyectos sin asignaciones deben conservar su presupuesto.

Ver solución
UPDATE proyectos pr
SET    pr.presupuesto = (SELECT SUM(a.horas) * 50
                         FROM   asignaciones a
                         WHERE  a.proyecto_id = pr.proyecto_id)
WHERE  EXISTS (SELECT 1 FROM asignaciones a WHERE a.proyecto_id = pr.proyecto_id);

SELECT proyecto_id, nombre, presupuesto FROM proyectos ORDER BY proyecto_id;
ROLLBACK;
4 filas actualizadas.

PROYECTO_ID NOMBRE                    PRESUPUESTO
----------- ------------------------- -----------
          1 Migración a la nube             71500
          2 Nueva tienda online             67500
          3 Campaña Navidad 2025            24000
          4 Optimización de almacén         15000
          5 Plan estratégico 2027     (nulo)

5 filas seleccionadas.

Rollback terminado.

Sin el WHERE EXISTS se actualizarían 5 filas y el proyecto 5 recibiría NULL (aquí ya lo era, pero en general se perdería el dato).

Ejercicio 19: Igualar salarios a la media

Pon a todos los programadores el salario medio actual de los programadores.

Ver solución
SELECT ROUND(AVG(salario), 2) FROM empleados WHERE oficio = 'PROGRAMADOR';

UPDATE empleados
SET    salario = (SELECT ROUND(AVG(salario), 2)
                  FROM   empleados
                  WHERE  oficio = 'PROGRAMADOR')
WHERE  oficio = 'PROGRAMADOR';

SELECT emp_id, nombre, salario FROM empleados WHERE oficio = 'PROGRAMADOR';
ROLLBACK;
ROUND(AVG(SALARIO),2)
---------------------
                 2600

1 fila seleccionada.

3 filas actualizadas.

    EMP_ID NOMBRE                       SALARIO
---------- ------------------------- ----------
       121 Raquel                          2600
       122 Iván                            2600
       123 Sara                            2600

3 filas seleccionadas.

Rollback terminado.

Aunque la subconsulta lee la misma tabla que se modifica, Oracle la evalúa con los datos de antes del UPDATE (lectura consistente): la media es 2600 para las tres filas y no va cambiando a medida que se actualizan.

Ejercicio 20: Envío de pedidos pendientes

Marca como ENVIADO, con fecha de envío 3 de julio de 2026, los pedidos pendientes del cliente 1012. Después intenta marcar como enviado el pedido 5024 con fecha 30 de junio de 2026 y explica el resultado.

Ver solución
UPDATE pedidos
SET    estado      = 'ENVIADO',
       fecha_envio = DATE '2026-07-03'
WHERE  cliente_id = 1012
AND    estado = 'PENDIENTE';

SELECT pedido_id, fecha_pedido, fecha_envio, estado FROM pedidos WHERE cliente_id = 1012;

UPDATE pedidos
SET    estado = 'ENVIADO', fecha_envio = DATE '2026-06-30'
WHERE  pedido_id = 5024;
ROLLBACK;
1 fila actualizada.

 PEDIDO_ID FECHA_PEDI FECHA_ENVI ESTADO
---------- ---------- ---------- ------------
      5013 14/10/2025 16/10/2025 ENTREGADO
      5022 01/07/2026 03/07/2026 ENVIADO

2 filas seleccionadas.

UPDATE pedidos
*
ERROR en línea 1:
ORA-02290: check constraint (CURSO.CK_PED_FECHAS) violated

Rollback terminado.

El pedido 5024 es del 10/07/2026 y la restricción ck_ped_fechas exige fecha_envio >= fecha_pedido.

Ejercicio 21: Copiar datos de otro departamento

El departamento 70 se traslada a la misma sede que Informática (30) y recibe su mismo presupuesto. Hazlo con una única subconsulta.

Ver solución
UPDATE departamentos
SET    (localidad, presupuesto) = (SELECT localidad, presupuesto
                                   FROM   departamentos
                                   WHERE  dept_id = 30)
WHERE  dept_id = 70;

SELECT * FROM departamentos WHERE dept_id IN (30, 70);
ROLLBACK;
1 fila actualizada.

   DEPT_ID NOMBRE                    LOCALIDAD                                PRESUPUESTO
---------- ------------------------- ---------------------------------------- -----------
        30 INFORMÁTICA               Valencia                                      420000
        70 INVESTIGACIÓN             Valencia                                      420000

2 filas seleccionadas.

Rollback terminado.

Ejercicio 22: UPDATE con join (23ai) y equivalente clásico

Pon como fecha de fin el 31/12/2026 a los proyectos sin fecha de fin de los departamentos situados en Valencia. Escribe la versión con UPDATE ... FROM y la clásica.

Ver solución
SELECT p.proyecto_id, p.nombre, p.fecha_fin, d.localidad
FROM   proyectos p JOIN departamentos d ON d.dept_id = p.dept_id
ORDER  BY 1;
PROYECTO_ID NOMBRE                    FECHA_FIN  LOCALIDAD
----------- ------------------------- ---------- ----------------------------------------
          1 Migración a la nube       30/09/2025 Valencia
          2 Nueva tienda online       (nulo)     Valencia
          3 Campaña Navidad 2025      31/12/2025 Barcelona
          4 Optimización de almacén   (nulo)     Zaragoza
          5 Plan estratégico 2027     (nulo)     Madrid

5 filas seleccionadas.

Versión 23ai:

UPDATE proyectos p
SET    p.fecha_fin = DATE '2026-12-31'
FROM   departamentos d
WHERE  d.dept_id = p.dept_id
AND    d.localidad = 'Valencia'
AND    p.fecha_fin IS NULL;

SELECT proyecto_id, nombre, fecha_fin FROM proyectos ORDER BY 1;
ROLLBACK;
1 fila actualizada.

PROYECTO_ID NOMBRE                    FECHA_FIN
----------- ------------------------- ----------
          1 Migración a la nube       30/09/2025
          2 Nueva tienda online       31/12/2026
          3 Campaña Navidad 2025      31/12/2025
          4 Optimización de almacén   (nulo)
          5 Plan estratégico 2027     (nulo)

5 filas seleccionadas.

Rollback terminado.

Versión clásica (cualquier versión):

UPDATE proyectos
SET    fecha_fin = DATE '2026-12-31'
WHERE  fecha_fin IS NULL
AND    dept_id IN (SELECT dept_id FROM departamentos WHERE localidad = 'Valencia');
ROLLBACK;
1 fila actualizada.

Rollback terminado.

Ejercicio 23: Descontar stock de pedidos pendientes (versión clásica)

Resta del stock de cada producto las unidades que figuran en pedidos PENDIENTE, sin usar la sintaxis UPDATE ... FROM de 23ai.

Ver solución
UPDATE productos pr
SET    pr.stock = pr.stock - (SELECT SUM(l.cantidad)
                              FROM   lineas_pedido l
                                     JOIN pedidos p ON p.pedido_id = l.pedido_id
                              WHERE  p.estado = 'PENDIENTE'
                              AND    l.producto_id = pr.producto_id)
WHERE  EXISTS (SELECT 1
               FROM   lineas_pedido l
                      JOIN pedidos p ON p.pedido_id = l.pedido_id
               WHERE  p.estado = 'PENDIENTE'
               AND    l.producto_id = pr.producto_id);

SELECT producto_id, nombre, stock FROM productos WHERE producto_id IN (2, 3, 11, 13);
ROLLBACK;
4 filas actualizadas.

PRODUCTO_ID NOMBRE                         STOCK
----------- ------------------------- ----------
          2 Portátil Gaming 16                 9
          3 Portátil Básico 15                37
         11 Smartphone Gama Media             49
         13 Auriculares Bluetooth             78

4 filas seleccionadas.

Rollback terminado.

El resultado coincide con el de la versión UPDATE ... FROM de la sección 3.8. Sin el WHERE EXISTS, los 16 productos sin pedidos pendientes recibirían stock - NULL, es decir, NULL; como stock es NOT NULL, la sentencia falla entera:

SET    pr.stock = pr.stock - (SELECT SUM(l.cantidad)
       *
ERROR en línea 2:
ORA-01407: cannot update ("CURSO"."PRODUCTOS"."STOCK") to NULL

Si la columna admitiera nulos, el error no aparecería y habríamos borrado el stock de 16 productos sin darnos cuenta.

Ejercicio 24: Borrar asignaciones de un departamento

Elimina las asignaciones a proyectos de todos los empleados del departamento de Marketing (50).

Ver solución
SELECT a.emp_id, a.proyecto_id, a.horas
FROM   asignaciones a
WHERE  a.emp_id IN (SELECT emp_id FROM empleados WHERE dept_id = 50);

DELETE FROM asignaciones
WHERE  emp_id IN (SELECT emp_id FROM empleados WHERE dept_id = 50);
ROLLBACK;
    EMP_ID PROYECTO_ID      HORAS
---------- ----------- ----------
       140           2        120
       104           3         80
       140           3        200
       141           3        160

4 filas seleccionadas.

4 filas suprimidas.

Rollback terminado.

Primero el SELECT con el mismo WHERE; el DELETE borra exactamente esas 4 filas. En 23ai también: DELETE FROM asignaciones a FROM empleados e WHERE e.emp_id = a.emp_id AND e.dept_id = 50;.

Ejercicio 25: Borrar clientes sin pedidos (y la trampa de NOT IN)

Elimina los clientes que no han hecho ningún pedido. Después, explica por qué esta consulta devuelve 0 aunque hay 20 empleados que no son comerciales de ningún cliente:

SELECT COUNT(*) FROM empleados WHERE emp_id NOT IN (SELECT comercial_id FROM clientes);
Ver solución
SELECT cliente_id, nombre
FROM   clientes c
WHERE  NOT EXISTS (SELECT 1 FROM pedidos p WHERE p.cliente_id = c.cliente_id);

DELETE FROM clientes c
WHERE  NOT EXISTS (SELECT 1 FROM pedidos p WHERE p.cliente_id = c.cliente_id);
ROLLBACK;
CLIENTE_ID NOMBRE
---------- -------------------------
      1013 Carlos Ibáñez Rey
      1014 Farmacia Central

2 filas seleccionadas.

2 filas suprimidas.

Rollback terminado.

Aquí NOT IN también funcionaría (DELETE FROM clientes WHERE cliente_id NOT IN (SELECT cliente_id FROM pedidos); borra las mismas 2 filas) porque pedidos.cliente_id es NOT NULL. Pero con una columna que admite nulos:

SELECT COUNT(*) FROM empleados WHERE emp_id NOT IN (SELECT comercial_id FROM clientes);
SELECT COUNT(*) FROM empleados e
WHERE  NOT EXISTS (SELECT 1 FROM clientes c WHERE c.comercial_id = e.emp_id);
  COUNT(*)
----------
         0

1 fila seleccionada.

  COUNT(*)
----------
        20

1 fila seleccionada.

Los clientes 1009 y 1013 tienen comercial_id nulo; emp_id NOT IN (110, 111, ..., NULL) equivale a emp_id <> 110 AND ... AND emp_id <> NULL, y la última comparación es desconocida, así que la condición nunca es verdadera. Un DELETE ... WHERE emp_id NOT IN (...) no borraría nada. Con NOT EXISTS no hay problema.

Ejercicio 26: Borrar pedidos cancelados

Elimina los pedidos cancelados y comprueba cuántas líneas de pedido quedan antes y después. ¿Por qué no hay que borrar antes las líneas?

Ver solución
SELECT COUNT(*) AS lineas FROM lineas_pedido;
DELETE FROM pedidos WHERE estado = 'CANCELADO';
SELECT COUNT(*) AS lineas FROM lineas_pedido;
ROLLBACK;
    LINEAS
----------
        51

1 fila seleccionada.

1 fila suprimida.

    LINEAS
----------
        50

1 fila seleccionada.

Rollback terminado.

La FK fk_lin_pedido tiene ON DELETE CASCADE: al borrar el pedido 5008, Oracle borra su línea automáticamente. Si la FK fuera NO ACTION, habría que borrar antes las líneas (DELETE FROM lineas_pedido WHERE pedido_id IN (SELECT pedido_id FROM pedidos WHERE estado = 'CANCELADO')).

Ejercicio 27: Eliminar un departamento con dependencias

Hay que eliminar el departamento de Logística (60). Sus empleados quedarán sin departamento y su proyecto también. Averigua qué lo impide y resuélvelo paso a paso.

Ver solución
DELETE FROM departamentos WHERE dept_id = 60;
ERROR en línea 1:
ORA-02292: integrity constraint (CURSO.FK_PRY_DEPT) violated - child record found

El error indica la primera restricción que encuentra Oracle, pero puede haber más. Desvinculamos los empleados y volvemos a probar:

UPDATE empleados SET dept_id = NULL WHERE dept_id = 60;
DELETE FROM departamentos WHERE dept_id = 60;
4 filas actualizadas.

ERROR en línea 1:
ORA-02292: integrity constraint (CURSO.FK_PRY_DEPT) violated - child record found

Falta desvincular el proyecto 4:

UPDATE proyectos SET dept_id = NULL WHERE dept_id = 60;
DELETE FROM departamentos WHERE dept_id = 60;
ROLLBACK;
1 fila actualizada.

1 fila suprimida.

Rollback terminado.

Para saber de antemano qué tablas dependen de DEPARTAMENTOS, consulta USER_CONSTRAINTS buscando las FK cuyo r_constraint_name sea PK_DEPARTAMENTOS (sección 4.6). Las tres sentencias deberían ir en la misma transacción: si alguna falla, se deshacen todas.

Ejercicio 28: Borrado correlacionado por la media

Elimina las asignaciones cuyas horas sean inferiores a la media de horas de su proyecto.

Ver solución
SELECT a.emp_id, a.proyecto_id, a.horas,
       (SELECT ROUND(AVG(x.horas)) FROM asignaciones x WHERE x.proyecto_id = a.proyecto_id) AS media
FROM   asignaciones a
ORDER  BY a.proyecto_id, a.emp_id;
    EMP_ID PROYECTO_ID      HORAS      MEDIA
---------- ----------- ---------- ----------
       120           1        450        477
       121           1        600        477
       124           1        380        477
       120           2        300        338
       122           2        520        338
       123           2        410        338
       140           2        120        338
       104           3         80        120
...

14 filas seleccionadas.
DELETE FROM asignaciones a
WHERE  a.horas < (SELECT AVG(x.horas)
                  FROM   asignaciones x
                  WHERE  x.proyecto_id = a.proyecto_id);

SELECT emp_id, proyecto_id, horas FROM asignaciones ORDER BY proyecto_id, emp_id;
ROLLBACK;
8 filas suprimidas.

    EMP_ID PROYECTO_ID      HORAS
---------- ----------- ----------
       121           1        600
       122           2        520
       123           2        410
       140           3        200
       141           3        160
       150           4        150

6 filas seleccionadas.

Rollback terminado.

La media de cada proyecto se calcula con los datos originales, no se recalcula a medida que se borran filas.

Ejercicio 29: Sincronizar la tabla de la web

La tienda web mantiene su propia tabla precios_web (producto_id, nombre, precio) con los productos 1, 2, 14 y 99. Sincronízala con PRODUCTOS en una sola sentencia: actualiza nombre y precio de los existentes, elimina los descatalogados e inserta los productos activos que falten. Antes, sube el precio del producto 2 a 1549 € en el catálogo. ¿Qué pasa con el 99, que ya no existe en el catálogo?

Ver solución
CREATE TABLE precios_web (
    producto_id  NUMBER(6) CONSTRAINT pk_precios_web PRIMARY KEY,
    nombre       VARCHAR2(60),
    precio       NUMBER(8,2)
);
INSERT INTO precios_web VALUES (1,  'Portátil UltraBook 14', 899.00);
INSERT INTO precios_web VALUES (2,  'Portátil Gaming 16',    1399.00);
INSERT INTO precios_web VALUES (14, 'Altavoz Inteligente',   49.00);
INSERT INTO precios_web VALUES (99, 'Producto retirado',     10.00);
COMMIT;

UPDATE productos SET precio = 1549 WHERE producto_id = 2;

MERGE INTO precios_web w
USING (SELECT producto_id, nombre, precio, descatalogado FROM productos) p
ON (w.producto_id = p.producto_id)
WHEN MATCHED THEN
    UPDATE SET w.precio = p.precio, w.nombre = p.nombre
    DELETE WHERE p.descatalogado = 'S'
WHEN NOT MATCHED THEN
    INSERT (producto_id, nombre, precio)
    VALUES (p.producto_id, p.nombre, p.precio)
    WHERE  p.descatalogado = 'N';

SELECT COUNT(*) FROM precios_web;
SELECT * FROM precios_web WHERE producto_id IN (1, 2, 14, 99);
ROLLBACK;
DROP TABLE precios_web PURGE;
1 fila actualizada.

20 filas fusionadas.

  COUNT(*)
----------
        20

1 fila seleccionada.

PRODUCTO_ID NOMBRE                        PRECIO
----------- ------------------------- ----------
          1 Portátil UltraBook 14            899
          2 Portátil Gaming 16              1549
         99 Producto retirado                 10

3 filas seleccionadas.

Rollback terminado.
  • El 2 se actualiza a 1549; el 1 se “actualiza” con el mismo valor.
  • El 14 (descatalogado) se actualiza y después se borra por el DELETE WHERE.
  • Se insertan los 17 productos activos que faltaban. Total: 1, 2, 99 y 17 nuevos = 20 filas.
  • El 99 sigue ahí: no está en el origen, así que MERGE no lo toca. Para eliminarlo hace falta otra sentencia:
DELETE FROM precios_web w
WHERE  NOT EXISTS (SELECT 1 FROM productos p WHERE p.producto_id = w.producto_id);

Ejercicio 30: Upsert de una asignación

Asigna 20 horas del empleado 122 al proyecto 4 como “Desarrollador” con fecha 5/10/2026; si ya estuviera asignado, suma las horas a las que tenga. Ejecuta la sentencia dos veces y observa el resultado.

Ver solución
MERGE INTO asignaciones a
USING (SELECT 122 AS emp_id, 4 AS proyecto_id, 20 AS horas FROM dual) s
ON (a.emp_id = s.emp_id AND a.proyecto_id = s.proyecto_id)
WHEN MATCHED THEN
    UPDATE SET a.horas = a.horas + s.horas
WHEN NOT MATCHED THEN
    INSERT (emp_id, proyecto_id, fecha_asignacion, horas, funcion)
    VALUES (s.emp_id, s.proyecto_id, DATE '2026-10-05', s.horas, 'Desarrollador');

SELECT emp_id, proyecto_id, fecha_asignacion, horas, funcion
FROM   asignaciones WHERE emp_id = 122 AND proyecto_id = 4;
1 fila fusionada.

    EMP_ID PROYECTO_ID FECHA_ASIG      HORAS FUNCION
---------- ----------- ---------- ---------- ------------------------------
       122           4 05/10/2026         20 Desarrollador

1 fila seleccionada.

Segunda ejecución (mismo MERGE):

1 fila fusionada.

    EMP_ID PROYECTO_ID FECHA_ASIG      HORAS FUNCION
---------- ----------- ---------- ---------- ------------------------------
       122           4 05/10/2026         40 Desarrollador

1 fila seleccionada.
ROLLBACK;

La primera vez entra por WHEN NOT MATCHED (inserta) y la segunda por WHEN MATCHED (suma). Es la forma idiomática en Oracle de “insertar o actualizar” sin consultar antes.

Ejercicio 31: Refrescar una tabla de resumen por departamento

La tabla resumen_dept (dept_id, num_empleados, masa_salarial) tiene datos desfasados de los departamentos 10, 20 y 70. Refréscala con un MERGE para que contenga todos los departamentos (incluidos los que no tienen empleados) con sus datos actuales.

Ver solución
CREATE TABLE resumen_dept (
    dept_id         NUMBER(4) CONSTRAINT pk_resumen_dept PRIMARY KEY,
    num_empleados   NUMBER(4),
    masa_salarial   NUMBER(12,2)
);
INSERT INTO resumen_dept VALUES (10, 1, 6500);
INSERT INTO resumen_dept VALUES (20, 99, 1);
INSERT INTO resumen_dept VALUES (70, 3, 5000);
COMMIT;

MERGE INTO resumen_dept r
USING (SELECT d.dept_id, COUNT(e.emp_id) AS n, NVL(SUM(e.salario), 0) AS masa
       FROM   departamentos d LEFT JOIN empleados e ON e.dept_id = d.dept_id
       GROUP  BY d.dept_id) v
ON (r.dept_id = v.dept_id)
WHEN MATCHED THEN
    UPDATE SET r.num_empleados = v.n, r.masa_salarial = v.masa
WHEN NOT MATCHED THEN
    INSERT VALUES (v.dept_id, v.n, v.masa);

SELECT * FROM resumen_dept ORDER BY dept_id;
ROLLBACK;
DROP TABLE resumen_dept PURGE;
7 filas fusionadas.

   DEPT_ID NUM_EMPLEADOS MASA_SALARIAL
---------- ------------- -------------
        10             1          6500
        20             6         15000
        30             6         18800
        40             3          8450
        50             3          9300
        60             4          8950
        70             0             0

7 filas seleccionadas.

Rollback terminado.

El LEFT JOIN desde DEPARTAMENTOS hace que el 70 aparezca con COUNT(e.emp_id) = 0; NVL convierte su suma nula en 0. El becario sin departamento no cuenta en ninguno.

Ejercicio 32: Recuperar el nuevo salario con RETURNING

Sube 150 € el salario de Álvaro (141) y recupera el salario resultante en una variable de SQL*Plus sin hacer un SELECT posterior.

Ver solución
VARIABLE v_nuevo NUMBER

UPDATE empleados
SET    salario = salario + 150
WHERE  emp_id = 141
RETURNING salario INTO :v_nuevo;

PRINT v_nuevo
ROLLBACK;
1 fila actualizada.

   V_NUEVO
----------
      2850

Rollback terminado.

VARIABLE y PRINT son órdenes de SQL*Plus/SQLcl (también funcionan en SQL Developer ejecutando como script con F5). En una aplicación, el valor se recogería en una variable del lenguaje anfitrión.

Ejercicio 33: Rebaja con suelo

La dirección quiere bajar 1700 € el salario de todo el departamento 60. Explica por qué falla la sentencia directa y escribe una versión que aplique la rebaja sin dejar a nadie por debajo de 1000 €.

Ver solución
UPDATE empleados SET salario = salario - 1700 WHERE dept_id = 60;
ERROR en línea 1:
ORA-02290: check constraint (CURSO.CK_EMP_SALARIO) violated

Rosa y Manuel (1600 €) quedarían en negativo, lo que incumple ck_emp_salario (salario > 0). Al ser la sentencia atómica, no cambia ningún salario, tampoco el de Javier. Con GREATEST fijamos un suelo:

UPDATE empleados SET salario = GREATEST(salario - 1700, 1000) WHERE dept_id = 60;
SELECT emp_id, nombre, salario FROM empleados WHERE dept_id = 60;
ROLLBACK;
4 filas actualizadas.

    EMP_ID NOMBRE                       SALARIO
---------- ------------------------- ----------
       105 Javier                          2300
       150 Rosa                            1000
       151 Manuel                          1000
       152 Cristina                        1000

4 filas seleccionadas.

Rollback terminado.

Ejercicio 34: Cambiar la clave de un departamento

Intenta renumerar el departamento 40 como 45, y el 70 como 75. Explica la diferencia.

Ver solución
UPDATE departamentos SET dept_id = 45 WHERE dept_id = 40;
UPDATE departamentos SET dept_id = 75 WHERE dept_id = 70;
ROLLBACK;
UPDATE departamentos SET dept_id = 45 WHERE dept_id = 40
*
ERROR en línea 1:
ORA-02292: integrity constraint (CURSO.FK_EMP_DEPT) violated - child record found

1 fila actualizada.

Rollback terminado.

El 40 tiene empleados que apuntan a él; cambiar su clave los dejaría huérfanos y Oracle no ofrece ON UPDATE CASCADE. El 70 no tiene hijas y se puede renumerar. Para renumerar el 40 habría que insertar el 45 (con otro nombre, por la UNIQUE de nombre, o cambiando antes el del 40), mover los hijos con UPDATE y borrar el 40: un buen motivo para no usar claves con significado que puedan cambiar.


10. Ejercicios propuestos

Ejercicio 1: Alta completa de un cliente con pedido

Da de alta un cliente nuevo de Teruel asignado a Elena (112) y, en la misma transacción, un pedido suyo con tres líneas cuyos precios se tomen del catálogo. Usa una secuencia para el pedido y CURRVAL para las líneas. Comprueba con una consulta el importe total del pedido y deshaz todo.

Ejercicio 2: Histórico de salarios

Crea la tabla historico_salarios (emp_id, fecha, salario_anterior, salario_nuevo) y escribe un INSERT ... SELECT que registre la subida del 3 % de los empleados de Contabilidad antes de aplicarla con un UPDATE. ¿En qué orden deben ejecutarse las dos sentencias?

Ejercicio 3: Reasignar la cartera de un comercial

Sergio (113) deja la empresa. Reasigna sus clientes a la vendedora con menos clientes asignados (calcúlala con una subconsulta) y después intenta borrar a Sergio. ¿Qué otras tablas te lo impiden? Resuélvelo sin perder información de pedidos.

Ejercicio 4: Ajuste de límites de crédito

Fija el límite de crédito de cada cliente al doble de su mayor pedido entregado, pero sin bajar nunca de 1500 € y dejando sin tocar a los clientes que no tienen pedidos entregados. Hazlo en una versión con subconsultas y otra con MERGE.

Ejercicio 5: Clasificación de productos

Con un INSERT FIRST, reparte los productos en productos_estrella (más de 10 000 € vendidos), productos_normales (algo vendido) y productos_sin_ventas. Las tres tablas deben incluir el importe vendido.

Ejercicio 6: Limpieza de proyectos

Elimina los proyectos que terminaron antes de 2026 junto con sus asignaciones. Antes, guarda en una tabla proyectos_archivados los proyectos y el total de horas que tuvieron. ¿Qué regla de borrado de fk_asi_proyecto hace que no necesites borrar antes las asignaciones?


Resumen

Concepto Sintaxis / idea clave
Insertar una fila INSERT INTO t (c1, c2) VALUES (v1, v2);
Insertar varias filas (23ai) INSERT INTO t (c1, c2) VALUES (a, b), (c, d);
Insertar desde consulta INSERT INTO t (c1, c2) SELECT x, y FROM ...;
Valor por defecto Omitir la columna o escribir DEFAULT (NULL explícito no lo activa)
Multitabla INSERT ALL WHEN ... THEN INTO t1 ... SELECT ...; / INSERT FIRST ... ELSE INTO ...
Secuencia seq.NEXTVAL (siguiente), seq.CURRVAL (último de tu sesión); no retrocede con ROLLBACK
Identidad GENERATED ALWAYS / BY DEFAULT [ON NULL] AS IDENTITY
Modificar UPDATE t SET c1 = expr, c2 = expr WHERE ...;
Subconsulta en SET SET c = (SELECT ...) (una fila; si no hay filas, NULL)
Varias columnas SET (c1, c2) = (SELECT x, y FROM ...)
Correlacionado UPDATE t a SET c = (SELECT ... WHERE x.k = a.k) WHERE EXISTS (...)
UPDATE con join (23ai) UPDATE t a SET a.c = b.c FROM t2 b WHERE b.k = a.k;
Vista en línea UPDATE (SELECT ... JOIN ...) SET ... (tabla con clave preservada)
Borrar DELETE FROM t WHERE ...;
DELETE con join (23ai) DELETE FROM t a FROM t2 b WHERE b.k = a.k AND ...;
FK al borrar NO ACTION (ORA-02292), ON DELETE CASCADE, ON DELETE SET NULL
Vaciar tabla TRUNCATE TABLE t; (DDL, sin ROLLBACK)
Upsert MERGE INTO d USING o ON (...) WHEN MATCHED THEN UPDATE ... [DELETE WHERE ...] WHEN NOT MATCHED THEN INSERT ...;
Devolver valores ... RETURNING col INTO :var; (SQL*Plus VARIABLE); varias filas: agregados o PL/SQL
OLD / NEW (23ai) RETURNING OLD c, NEW c INTO :a, :b
Atomicidad de sentencia Si una fila falla, la sentencia entera se deshace
Errores frecuentes ORA-00001, ORA-01400, ORA-01407, ORA-02290, ORA-02291, ORA-02292, ORA-01427, ORA-30926
Regla de oro SELECT con el mismo WHERE antes de UPDATE/DELETE, y COMMIT solo tras comprobar