Módulo 03: Uso de Sentencias DML en PL/SQL
Objetivos del módulo
- Ejecutar sentencias
INSERT,UPDATEyDELETEdentro de bloques PL/SQL - Utilizar
SELECT INTOpara recuperar datos en variables - Conocer y usar los atributos implícitos del cursor SQL (
SQL%ROWCOUNT,SQL%FOUND, etc.) - Gestionar transacciones con
COMMIT,ROLLBACKySAVEPOINT - Aplicar buenas prácticas de DML dentro de PL/SQL
1. Sentencias DML dentro de PL/SQL
Una de las grandes ventajas de PL/SQL es que puedes usar sentencias SQL de manipulación de datos (INSERT, UPDATE, DELETE, SELECT) directamente dentro de tus bloques, mezclándolas con lógica procedural.
📘 Concepto: En PL/SQL, las sentencias DML se escriben tal cual como en SQL, pero con la ventaja de poder usar variables PL/SQL en lugar de valores literales. Esto hace que el código sea dinámico y reutilizable.
2. SELECT INTO
SELECT INTO es la forma de obtener datos de la base de datos y almacenarlos en variables PL/SQL:
2.1. Seleccionar un único valor
DECLARE
v_num_multas NUMBER;
BEGIN
SELECT COUNT(*) INTO v_num_multas
FROM multa;
DBMS_OUTPUT.PUT_LINE('Total de multas: ' || v_num_multas);
END;
/
2.2. Seleccionar múltiples columnas
DECLARE
v_nombre conductor.nombre%TYPE;
v_fec_nac conductor.fec_nacimiento%TYPE;
v_fec_car conductor.fec_carnet%TYPE;
BEGIN
SELECT nombre, fec_nacimiento, fec_carnet
INTO v_nombre, v_fec_nac, v_fec_car
FROM conductor
WHERE ROWNUM = 1;
DBMS_OUTPUT.PUT_LINE('Nombre: ' || v_nombre);
DBMS_OUTPUT.PUT_LINE('Nacimiento: ' || v_fec_nac);
DBMS_OUTPUT.PUT_LINE('Carnet: ' || v_fec_car);
END;
/
2.3. Seleccionar en un registro %ROWTYPE
DECLARE
r_vehiculo vehiculo%ROWTYPE;
BEGIN
SELECT * INTO r_vehiculo
FROM vehiculo
WHERE ROWNUM = 1;
DBMS_OUTPUT.PUT_LINE('Matrícula: ' || r_vehiculo.matricula);
DBMS_OUTPUT.PUT_LINE('Marca: ' || r_vehiculo.marca);
DBMS_OUTPUT.PUT_LINE('Color: ' || NVL(r_vehiculo.color, 'Sin color'));
END;
/
⚠️ Importante:
SELECT INTOdebe devolver exactamente una fila:
- Si no devuelve ninguna fila → excepción
NO_DATA_FOUND- Si devuelve más de una fila → excepción
TOO_MANY_ROWSPara procesar múltiples filas, usa cursores (Módulo 04).
3. INSERT en PL/SQL
Las sentencias INSERT se escriben igual que en SQL, pero pueden usar variables:
3.1. INSERT con valores de variables
DECLARE
v_matricula VARCHAR2(10) := '1234ABC';
v_marca VARCHAR2(30) := 'Toyota';
v_color VARCHAR2(20) := 'Rojo';
BEGIN
INSERT INTO vehiculo (matricula, marca, color)
VALUES (v_matricula, v_marca, v_color);
DBMS_OUTPUT.PUT_LINE('Vehículo insertado correctamente');
COMMIT;
END;
/
3.2. INSERT con SELECT
BEGIN
-- Insertar datos que provienen de otra consulta
INSERT INTO vehiculo_backup (matricula, marca, color)
SELECT matricula, marca, color
FROM vehiculo
WHERE color = 'Rojo';
DBMS_OUTPUT.PUT_LINE('Registros copiados: ' || SQL%ROWCOUNT);
COMMIT;
END;
/
3.3. INSERT con RETURNING INTO
RETURNING INTO permite obtener valores generados por la inserción (por ejemplo, IDs de secuencias):
DECLARE
v_nuevo_id NUMBER;
BEGIN
INSERT INTO infraccion (id, codigo, descripcion, puntos, valor_economico, vigente)
VALUES (seq_infraccion.NEXTVAL, 'INF-NEW', 'Infracción de prueba', 2, 100, 1)
RETURNING id INTO v_nuevo_id;
DBMS_OUTPUT.PUT_LINE('Nueva infracción creada con ID: ' || v_nuevo_id);
ROLLBACK; -- Deshacemos para no ensuciar los datos
END;
/
💡 Truco:
RETURNING INTOes muy útil para obtener el valor de columnas con valores por defecto, secuencias oSYSDATEsin necesidad de hacer una consulta adicional.
4. UPDATE en PL/SQL
4.1. UPDATE básico con variables
DECLARE
v_nuevo_color VARCHAR2(20) := 'Azul';
v_matricula VARCHAR2(10) := '1234ABC';
BEGIN
UPDATE vehiculo
SET color = v_nuevo_color
WHERE matricula = v_matricula;
IF SQL%FOUND THEN
DBMS_OUTPUT.PUT_LINE('Vehículo actualizado. Filas afectadas: ' || SQL%ROWCOUNT);
ELSE
DBMS_OUTPUT.PUT_LINE('No se encontró el vehículo con matrícula: ' || v_matricula);
END IF;
COMMIT;
END;
/
4.2. UPDATE con subconsulta
BEGIN
-- Actualizar el valor económico de infracciones incrementándolo un 10%
UPDATE infraccion
SET valor_economico = valor_economico * 1.10
WHERE vigente = 1;
DBMS_OUTPUT.PUT_LINE('Infracciones actualizadas: ' || SQL%ROWCOUNT);
ROLLBACK; -- Deshacemos la subida de precios de prueba
END;
/
4.3. UPDATE con RETURNING INTO
DECLARE
v_importe_anterior infraccion.valor_economico%TYPE;
v_importe_nuevo infraccion.valor_economico%TYPE;
BEGIN
UPDATE infraccion
SET valor_economico = valor_economico * 1.05
WHERE ROWNUM = 1
RETURNING valor_economico INTO v_importe_nuevo;
DBMS_OUTPUT.PUT_LINE('Nuevo importe: ' || v_importe_nuevo);
ROLLBACK;
END;
/
5. DELETE en PL/SQL
5.1. DELETE básico
DECLARE
v_fecha_limite DATE := ADD_MONTHS(SYSDATE, -60); -- Hace 5 años
BEGIN
DELETE FROM multa
WHERE fecha < v_fecha_limite;
DBMS_OUTPUT.PUT_LINE('Multas antiguas eliminadas: ' || SQL%ROWCOUNT);
ROLLBACK; -- Deshacemos para no perder datos
END;
/
5.2. DELETE con condición compuesta
BEGIN
DELETE FROM infraccion
WHERE vigente = 0
AND valor_economico < 50;
IF SQL%ROWCOUNT > 0 THEN
DBMS_OUTPUT.PUT_LINE('Infracciones obsoletas eliminadas: ' || SQL%ROWCOUNT);
COMMIT;
ELSE
DBMS_OUTPUT.PUT_LINE('No se encontraron infracciones que cumplan los criterios');
END IF;
END;
/
6. Atributos implícitos del cursor SQL
Después de cada sentencia DML, Oracle actualiza automáticamente unos atributos implícitos que proporcionan información sobre el resultado de la operación:
| Atributo | Tipo | Descripción |
|---|---|---|
SQL%ROWCOUNT | NUMBER | Número de filas afectadas por la última sentencia DML |
SQL%FOUND | BOOLEAN | TRUE si la última sentencia DML afectó al menos a una fila |
SQL%NOTFOUND | BOOLEAN | TRUE si la última sentencia DML no afectó a ninguna fila |
SQL%ISOPEN | BOOLEAN | Siempre FALSE para cursores implícitos (se cierran automáticamente) |
📘 Concepto: Estos atributos se refieren siempre a la última sentencia DML ejecutada. Si ejecutas un
UPDATEseguido de unINSERT, los atributos reflejarán el resultado delINSERT, no delUPDATE.
Ejemplo completo con atributos
BEGIN
-- Intentar actualizar infracciones con código inexistente
UPDATE infraccion
SET valor_economico = 999
WHERE codigo = 'INEXISTENTE';
DBMS_OUTPUT.PUT_LINE('SQL%ROWCOUNT: ' || SQL%ROWCOUNT);
IF SQL%FOUND THEN
DBMS_OUTPUT.PUT_LINE('SQL%FOUND: TRUE - Se actualizaron filas');
ELSE
DBMS_OUTPUT.PUT_LINE('SQL%FOUND: FALSE - No se actualizó ninguna fila');
END IF;
IF SQL%NOTFOUND THEN
DBMS_OUTPUT.PUT_LINE('SQL%NOTFOUND: TRUE - No se encontraron coincidencias');
END IF;
DBMS_OUTPUT.PUT_LINE('SQL%ISOPEN: ' ||
CASE WHEN SQL%ISOPEN THEN 'TRUE' ELSE 'FALSE' END);
ROLLBACK;
END;
/
7. Gestión de transacciones
7.1. COMMIT (confirmar cambios)
COMMIT hace permanentes todos los cambios realizados desde el último COMMIT o ROLLBACK:
BEGIN
INSERT INTO vehiculo (matricula, marca, color)
VALUES ('9999ZZZ', 'Ford', 'Negro');
UPDATE vehiculo
SET color = 'Blanco'
WHERE matricula = '9999ZZZ';
-- Ambas operaciones se confirman de forma atómica
COMMIT;
DBMS_OUTPUT.PUT_LINE('Transacción confirmada');
END;
/
7.2. ROLLBACK (deshacer cambios)
ROLLBACK deshace todos los cambios no confirmados:
BEGIN
DELETE FROM multa WHERE ROWNUM <= 5;
DBMS_OUTPUT.PUT_LINE('Filas eliminadas: ' || SQL%ROWCOUNT);
-- Deshacemos los cambios
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('Cambios deshechos con ROLLBACK');
END;
/
7.3. SAVEPOINT (puntos de guardado)
SAVEPOINT permite hacer ROLLBACK parcial, deshaciendo solo hasta un punto concreto:
BEGIN
INSERT INTO vehiculo (matricula, marca, color)
VALUES ('1111AAA', 'Seat', 'Rojo');
SAVEPOINT sp_despues_insert;
UPDATE vehiculo
SET color = 'Verde'
WHERE matricula = '1111AAA';
-- Solo deshacemos el UPDATE, el INSERT se mantiene
ROLLBACK TO sp_despues_insert;
-- Confirmamos el INSERT
COMMIT;
DBMS_OUTPUT.PUT_LINE('Solo el INSERT fue confirmado');
END;
/
⚠️ Importante: Un
COMMIToROLLBACKcompleto elimina todos losSAVEPOINTdefinidos. Además, las sentencias DDL (CREATE,ALTER,DROP) ejecutan un COMMIT implícito, por lo que no se pueden deshacer conROLLBACK.
8. DML con lógica procedural: ejemplo completo
Un ejemplo que combina todas las operaciones DML con lógica de control:
DECLARE
v_num_conductores NUMBER;
v_num_multas NUMBER;
v_ratio NUMBER(10,2);
BEGIN
-- Obtener estadísticas
SELECT COUNT(*) INTO v_num_conductores FROM conductor;
SELECT COUNT(*) INTO v_num_multas FROM multa;
DBMS_OUTPUT.PUT_LINE('=== Informe de Multas ===');
DBMS_OUTPUT.PUT_LINE('Conductores: ' || v_num_conductores);
DBMS_OUTPUT.PUT_LINE('Multas: ' || v_num_multas);
-- Calcular ratio
IF v_num_conductores > 0 THEN
v_ratio := v_num_multas / v_num_conductores;
DBMS_OUTPUT.PUT_LINE('Ratio multas/conductor: ' || TO_CHAR(v_ratio, '990.00'));
-- Evaluar la situación
CASE
WHEN v_ratio < 1 THEN DBMS_OUTPUT.PUT_LINE('Situación: BUENA');
WHEN v_ratio < 3 THEN DBMS_OUTPUT.PUT_LINE('Situación: REGULAR');
WHEN v_ratio >= 3 THEN DBMS_OUTPUT.PUT_LINE('Situación: PREOCUPANTE');
END CASE;
ELSE
DBMS_OUTPUT.PUT_LINE('No hay conductores registrados');
END IF;
END;
/
9. Buenas prácticas con DML en PL/SQL
💡 Truco: Sigue estas recomendaciones para escribir código DML robusto:
- Siempre comprueba
SQL%ROWCOUNTdespués de unUPDATEoDELETEpara verificar que afectó a las filas esperadas. - Usa
COMMITde forma consciente: no hagasCOMMITdentro de un bucle (puede generar incoherencias si falla a mitad). Confirma al final de la unidad lógica de trabajo. - Usa
SAVEPOINTcuando necesites deshacer parcialmente operaciones complejas. - Usa
RETURNING INTOen lugar de hacer unSELECTadicional después deINSERToUPDATE. - Usa
%TYPEen las variables que reciben datos de columnas para evitar inconsistencias de tipos.
10. Ejercicios prácticos
Ejercicio 1: SELECT INTO con cálculos
Obtén el valor medio de las infracciones vigentes y muéstralo formateado:
DECLARE
v_media infraccion.valor_economico%TYPE;
BEGIN
SELECT AVG(valor_economico) INTO v_media
FROM infraccion
WHERE vigente = 1;
DBMS_OUTPUT.PUT_LINE('Valor medio de infracciones vigentes: ' ||
TO_CHAR(v_media, '999G999D00') || ' €');
END;
/
Ejercicio 2: INSERT condicional
Inserta un vehículo solo si su matrícula no existe ya en la tabla:
DECLARE
v_matricula VARCHAR2(10) := '5555BBB';
v_existe NUMBER;
BEGIN
SELECT COUNT(*) INTO v_existe
FROM vehiculo
WHERE matricula = v_matricula;
IF v_existe = 0 THEN
INSERT INTO vehiculo (matricula, marca, color)
VALUES (v_matricula, 'Renault', 'Gris');
DBMS_OUTPUT.PUT_LINE('Vehículo insertado correctamente');
COMMIT;
ELSE
DBMS_OUTPUT.PUT_LINE('El vehículo con matrícula ' || v_matricula || ' ya existe');
END IF;
END;
/
Ejercicio 3: UPDATE con verificación
Incrementa el valor económico de las infracciones en un 5% solo si hay más de 10 infracciones vigentes:
DECLARE
v_num_vigentes NUMBER;
BEGIN
SELECT COUNT(*) INTO v_num_vigentes
FROM infraccion
WHERE vigente = 1;
IF v_num_vigentes > 10 THEN
UPDATE infraccion
SET valor_economico = valor_economico * 1.05
WHERE vigente = 1;
DBMS_OUTPUT.PUT_LINE('Actualizadas ' || SQL%ROWCOUNT || ' infracciones');
COMMIT;
ELSE
DBMS_OUTPUT.PUT_LINE('Solo hay ' || v_num_vigentes ||
' infracciones vigentes. No se actualiza.');
END IF;
END;
/
Ejercicio 4: Transacción con SAVEPOINT
Realiza una operación compuesta: inserta un vehículo, crea un savepoint, inserta una multa. Si la multa falla (cantidad no válida), deshaz solo la multa pero mantén el vehículo:
DECLARE
v_importe NUMBER := -50; -- Importe inválido: negativo
BEGIN
-- Paso 1: Insertar vehículo
INSERT INTO vehiculo (matricula, marca, color)
VALUES ('7777XXX', 'BMW', 'Azul');
DBMS_OUTPUT.PUT_LINE('Vehículo insertado');
SAVEPOINT sp_vehiculo_ok;
-- Paso 2: Validar e insertar multa
IF v_importe > 0 THEN
DBMS_OUTPUT.PUT_LINE('Multa registrada');
COMMIT;
ELSE
DBMS_OUTPUT.PUT_LINE('Importe de multa no válido. Deshaciendo multa...');
ROLLBACK TO sp_vehiculo_ok;
COMMIT; -- El vehículo sigue insertado
END IF;
END;
/
Resumen
| Concepto | Detalle |
|---|---|
| SELECT INTO | Asigna el resultado de una consulta (una sola fila) a variables PL/SQL |
| INSERT | Inserta filas usando variables PL/SQL como valores |
| UPDATE | Modifica filas usando variables PL/SQL en SET y WHERE |
| DELETE | Elimina filas usando variables PL/SQL en la condición WHERE |
| RETURNING INTO | Obtiene valores generados tras INSERT o UPDATE sin consulta adicional |
| SQL%ROWCOUNT | Número de filas afectadas por la última sentencia DML |
| SQL%FOUND | TRUE si la última DML afectó al menos una fila |
| SQL%NOTFOUND | TRUE si la última DML no afectó a ninguna fila |
| SQL%ISOPEN | Siempre FALSE para cursores implícitos |
| COMMIT | Confirma todos los cambios pendientes de forma permanente |
| ROLLBACK | Deshace todos los cambios no confirmados |
| SAVEPOINT | Marca un punto para poder hacer ROLLBACK TO parcial |