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,DELETEyMERGE) y en qué se diferencian deSELECT - Insertar filas con y sin lista de columnas, usando valores por defecto (
DEFAULT),NULLexplícito, secuencias y columnas identidad - Insertar varias filas a la vez:
INSERTmultifila de Oracle 23ai,INSERT ALL,INSERT FIRSTeINSERT ... SELECT - Modificar filas con
UPDATE: una o varias columnas, expresiones,CASE, subconsultas enSETy enWHERE, actualizaciones correlacionadas y de varias columnas a la vez - Conocer el
UPDATEy elDELETEcon join directo (FROM) de Oracle 23ai y sus alternativas clásicas (vistas en línea yMERGE) - Borrar filas con
DELETEy entender su interacción con las claves ajenas (ORA-02292,ON DELETE CASCADEyON DELETE SET NULL) - Sincronizar tablas con
MERGE(upsert), incluida la cláusulaDELETE WHERE - Recuperar valores modificados con
RETURNING ... INTOdesde 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 unCOMMITimplícito antes y después de ejecutarse. Si haces unUPDATEy luego unCREATE TABLE, elUPDATEya 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 delROLLBACK.
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
COMMITde 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:
- Legibilidad: quien lea la sentencia sabe qué valor va a cada columna sin consultar la estructura de la tabla.
- Robustez: si mañana alguien añade una columna a la tabla (
ALTER TABLE ... ADD), la sentencia sigue funcionando. - 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
nombreylocalidad, ambasVARCHAR2(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 comoNULL.INSERT ... VALUES (95, 'X', '', NULL)dejalocalidadaNULL. Es una peculiaridad de Oracle que no comparten otros SGBD (en PostgreSQL o SQL Server''yNULLson 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
DEFAULTen 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 NULLen 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 unNULLexplícito. Las columnas de EMPRESA usan elDEFAULTclá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
SYSDATElleva hora, una consulta posterior del tipoWHERE fecha_alta = DATE '2026-10-03'no encontrará a Noa. Si solo te interesa el día, insertaTRUNC(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
VALUESsirve 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:
- Varios
INSERTseguidos (lo más sencillo y lo que hacen los scripts de EMPRESA). INSERT ALLincondicional con una consulta de una fila sobreDUAL:
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.
INSERT ... SELECTconUNION ALLde varias filas sobreDUAL:
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 delSELECT. - Las columnas de la consulta se asignan por posición a las de la lista, no por nombre.
fecha_copiano está en la lista, así que toma suDEFAULT(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
PEDIDOSyLINEAS_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 restriccionesNOT NULL.INSERT ... SELECTrellena 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 conINSERT ... 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
INSERTmultitabla no se puede usar una secuencia (NEXTVAL) dentro de la consulta de origen (errorORA-02287: sequence number not allowed here; sí se admite en las cláusulasVALUES), 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áusulasINTO.
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 siguienteINSERTautomático fallará conORA-00001. Si das valores manuales, que sea solo en cargas iniciales y luego reajusta la identidad conALTER 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-00001incluyen una segunda líneaORA-03301con 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];
SETindica qué columnas cambian y su nuevo valor (puede ser un literal, una expresión con otras columnas, una función o una subconsulta).WHEREselecciona qué filas se modifican, con las mismas condiciones que conoces deSELECT. 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, yNULL + 100esNULL, usamosNVL.
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 ONLYelige un departamento aunque haya empate. Si quieres incluir todos los empatados, usaFETCH FIRST 1 ROW WITH TIESy cambia=porIN.
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:
- Se han actualizado 7 filas: también el departamento 70, que no tiene empleados. Su subconsulta devuelve
SUMde nada, es decir,NULL. Aquí ya eraNULL, pero si hubiera tenido un presupuesto lo habríamos borrado sin querer. - La solución es limitar el
UPDATEa las filas que tienen correspondencia, conEXISTS:
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 EXISTSy poner aNULLlas filas sin correspondencia. Otra opción es proteger el valor conNVL((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 (conORA-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, hazROLLBACKinmediatamente. 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ónUNIQUE.
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
SELECTcon el mismoWHERE. Si devuelve lo que esperas, cambiaSELECT *porDELETE.
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 INsi la subconsulta puede devolverNULL: la condición nunca es verdadera y no se borra nada (lo verás en el ejercicio 25). ConNOT EXISTSno 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 CASCADEes cómodo y peligroso a la vez. UnDELETE FROM pedidos;sinWHEREvacía tambiénLINEAS_PEDIDOentera (lo hemos probado:24 filas suprimidas.y despuésSELECT COUNT(*) FROM lineas_pedidodevuelve 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 ramaWHEN MATCHED(actualizar y, opcionalmente, borrar); si no la tiene, la ramaWHEN 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
ONva 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
INSERTno 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 = 0se 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 WHEREno borra las filas del destino que no aparecen en el origen. UnMERGEnunca toca las filas del destino sin pareja. Si necesitas eliminar “lo que ya no viene en el origen”, hazlo con unDELETE ... WHERE NOT EXISTSaparte (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 ... INTOdeposita 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, medianteBULK 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 conVARIABLE, 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ó elUPDATEcorrectamente; 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 + 1sobre 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:
- Escribe un
SELECTcon exactamente el mismoWHERE(y, para unUPDATE, muestra la columna actual y la expresión nueva). - Comprueba el número de filas y su contenido.
- Convierte el
SELECTenUPDATE/DELETEsin tocar elWHERE. - Compara el número de filas afectadas con el del
SELECT. - 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.
Ejercicio 5: Pedido con precios del catálogo
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
MERGEno 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 |