Módulo 06: Subprogramas: Procedimientos y Funciones
Objetivos del módulo
- Entender qué son los subprogramas y por qué son esenciales
- Crear procedimientos almacenados con
CREATE PROCEDURE - Crear funciones con
CREATE FUNCTION - Dominar los modos de parámetros:
IN,OUTeIN OUT - Invocar subprogramas desde bloques anónimos y desde otros subprogramas
- Eliminar y recompilar subprogramas
- Conocer los paquetes (packages) como forma de organizar código
- Aplicar buenas prácticas en el diseño de subprogramas
1. ¿Qué son los subprogramas?
Los subprogramas son bloques PL/SQL con nombre que se almacenan en la base de datos y se pueden reutilizar. Existen dos tipos:
| Tipo | Descripción | Devuelve valor |
|---|---|---|
| Procedimiento | Ejecuta una acción (insertar, actualizar, calcular, etc.) | No (usa parámetros OUT si necesita devolver datos) |
| Función | Calcula y devuelve un único valor | Sí, con RETURN |
📘 Concepto: La diferencia principal es que una función devuelve un valor y se puede usar dentro de expresiones SQL, mientras que un procedimiento realiza acciones y se invoca con
EXECUTEoCALL. Si necesitas devolver un solo valor, usa una función. Si necesitas realizar operaciones complejas, usa un procedimiento.
Ventajas de los subprogramas
- Reutilización: escribe el código una vez, úsalo muchas veces
- Mantenimiento: si la lógica cambia, solo modificas un lugar
- Seguridad: puedes dar permiso de ejecución sin dar acceso directo a las tablas
- Rendimiento: se compilan y almacenan en la BD, listos para ejecutarse
- Modularidad: divide problemas complejos en piezas manejables
2. Procedimientos almacenados
2.1. Sintaxis de creación
CREATE [OR REPLACE] PROCEDURE nombre_procedimiento
(parametro1 [IN | OUT | IN OUT] tipo,
parametro2 [IN | OUT | IN OUT] tipo,
...)
IS | AS
-- Declaraciones locales
BEGIN
-- Código ejecutable
EXCEPTION
-- Manejo de excepciones
END [nombre_procedimiento];
/
💡 Truco: Usa siempre
CREATE OR REPLACE PROCEDUREen lugar de soloCREATE PROCEDURE. Así puedes modificar el procedimiento sin tener que borrarlo primero. Si no usasOR REPLACEy el procedimiento ya existe, Oracle dará un error.
2.2. Procedimiento sin parámetros
CREATE OR REPLACE PROCEDURE pr_listar_conductores
IS
BEGIN
FOR r IN (SELECT nombre, fec_carnet FROM conductor ORDER BY nombre) LOOP
DBMS_OUTPUT.PUT_LINE(r.nombre || ' - Carnet: ' || r.fec_carnet);
END LOOP;
END pr_listar_conductores;
/
Ejecución:
SET SERVEROUTPUT ON;
-- Forma 1: con EXECUTE (abreviado EXEC)
EXEC pr_listar_conductores;
-- Forma 2: con bloque anónimo
BEGIN
pr_listar_conductores;
END;
/
-- Forma 3: con CALL
CALL pr_listar_conductores();
2.3. Procedimiento con parámetros IN
Los parámetros IN son los valores de entrada (es el modo por defecto):
CREATE OR REPLACE PROCEDURE pr_buscar_conductor
(p_nombre IN conductor.nombre%TYPE)
IS
v_fec_nac conductor.fec_nacimiento%TYPE;
v_fec_car conductor.fec_carnet%TYPE;
BEGIN
SELECT fec_nacimiento, fec_carnet
INTO v_fec_nac, v_fec_car
FROM conductor
WHERE UPPER(nombre) LIKE '%' || UPPER(p_nombre) || '%'
AND ROWNUM = 1;
DBMS_OUTPUT.PUT_LINE('Nacimiento: ' || v_fec_nac);
DBMS_OUTPUT.PUT_LINE('Carnet: ' || v_fec_car);
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('No se encontró conductor con nombre: ' || p_nombre);
END pr_buscar_conductor;
/
Ejecución:
EXEC pr_buscar_conductor('García');
2.4. Procedimiento con parámetros OUT
Los parámetros OUT permiten devolver valores al programa que llama:
CREATE OR REPLACE PROCEDURE pr_estadisticas_multas
(p_total OUT NUMBER,
p_promedio OUT NUMBER,
p_maximo OUT NUMBER)
IS
BEGIN
SELECT COUNT(*), AVG(m.importe), MAX(m.importe)
INTO p_total, p_promedio, p_maximo
FROM multa m;
EXCEPTION
WHEN NO_DATA_FOUND THEN
p_total := 0;
p_promedio := 0;
p_maximo := 0;
END pr_estadisticas_multas;
/
Ejecución:
DECLARE
v_total NUMBER;
v_promedio NUMBER;
v_maximo NUMBER;
BEGIN
pr_estadisticas_multas(v_total, v_promedio, v_maximo);
DBMS_OUTPUT.PUT_LINE('Total multas: ' || v_total);
DBMS_OUTPUT.PUT_LINE('Importe promedio: ' || ROUND(v_promedio, 2));
DBMS_OUTPUT.PUT_LINE('Importe máximo: ' || v_maximo);
END;
/
2.5. Procedimiento con parámetros IN OUT
Los parámetros IN OUT sirven tanto de entrada como de salida:
CREATE OR REPLACE PROCEDURE pr_formatear_nombre
(p_nombre IN OUT VARCHAR2)
IS
BEGIN
-- Eliminar espacios y capitalizar
p_nombre := INITCAP(TRIM(p_nombre));
END pr_formatear_nombre;
/
Ejecución:
DECLARE
v_nombre VARCHAR2(100) := ' juan GARCÍA lópez ';
BEGIN
DBMS_OUTPUT.PUT_LINE('Antes: [' || v_nombre || ']');
pr_formatear_nombre(v_nombre);
DBMS_OUTPUT.PUT_LINE('Después: [' || v_nombre || ']');
END;
/
📘 Concepto: Resumen de modos de parámetros:
Modo ¿Se puede leer? ¿Se puede modificar? Valor por defecto IN(default)✅ ❌ Sí OUT❌ (valor indefinido al entrar) ✅ No IN OUT✅ ✅ No
2.6. Parámetros con valores por defecto
CREATE OR REPLACE PROCEDURE pr_informe_infracciones
(p_vigente IN NUMBER DEFAULT 1,
p_min_importe IN NUMBER DEFAULT 0)
IS
BEGIN
DBMS_OUTPUT.PUT_LINE('=== Informe de Infracciones ===');
DBMS_OUTPUT.PUT_LINE('Filtro: vigente=' || p_vigente ||
', importe>=' || p_min_importe);
DBMS_OUTPUT.PUT_LINE('');
FOR r IN (SELECT descripcion, puntos, valor_economico
FROM infraccion
WHERE vigente = p_vigente
AND valor_economico >= p_min_importe
ORDER BY valor_economico DESC) LOOP
DBMS_OUTPUT.PUT_LINE(RPAD(r.descripcion, 40) ||
LPAD(r.puntos || ' pts', 8) ||
LPAD(r.valor_economico || ' €', 10));
END LOOP;
END pr_informe_infracciones;
/
Ejecución con y sin parámetros:
-- Usa valores por defecto (vigente=1, min_importe=0)
EXEC pr_informe_infracciones;
-- Solo cambia el importe mínimo (notación nombrada)
EXEC pr_informe_infracciones(p_min_importe => 200);
-- Cambia ambos
EXEC pr_informe_infracciones(0, 100);
💡 Truco: La notación nombrada (
p_parametro => valor) permite pasar parámetros en cualquier orden y omitir los que tienen valor por defecto. Es muy recomendable cuando hay muchos parámetros.
3. Funciones
3.1. Sintaxis de creación
CREATE [OR REPLACE] FUNCTION nombre_funcion
(parametro1 [IN] tipo,
parametro2 [IN] tipo,
...)
RETURN tipo_retorno
IS | AS
-- Declaraciones locales
BEGIN
-- Código ejecutable
RETURN valor;
EXCEPTION
-- Manejo de excepciones
END [nombre_funcion];
/
3.2. Función básica
CREATE OR REPLACE FUNCTION fn_calcular_edad
(p_fecha_nacimiento IN DATE)
RETURN NUMBER
IS
BEGIN
RETURN TRUNC(MONTHS_BETWEEN(SYSDATE, p_fecha_nacimiento) / 12);
END fn_calcular_edad;
/
Uso:
-- Desde un bloque PL/SQL
DECLARE
v_edad NUMBER;
BEGIN
v_edad := fn_calcular_edad(TO_DATE('15/03/1990', 'DD/MM/YYYY'));
DBMS_OUTPUT.PUT_LINE('Edad: ' || v_edad || ' años');
END;
/
-- Desde una consulta SQL
SELECT nombre, fec_nacimiento, fn_calcular_edad(fec_nacimiento) AS edad
FROM conductor;
3.3. Función que devuelve texto
CREATE OR REPLACE FUNCTION fn_clasificar_infraccion
(p_valor_economico IN NUMBER)
RETURN VARCHAR2
IS
BEGIN
RETURN CASE
WHEN p_valor_economico < 100 THEN 'LEVE'
WHEN p_valor_economico < 300 THEN 'GRAVE'
WHEN p_valor_economico < 600 THEN 'MUY GRAVE'
ELSE 'EXTREMA'
END;
END fn_clasificar_infraccion;
/
Uso en SQL:
SELECT
descripcion,
valor_economico,
fn_clasificar_infraccion(valor_economico) AS clasificacion
FROM infraccion
WHERE vigente = 1
ORDER BY valor_economico DESC;
3.4. Función con cálculos complejos
CREATE OR REPLACE FUNCTION fn_calcular_iva
(p_base IN NUMBER,
p_tipo_iva IN NUMBER DEFAULT 21)
RETURN NUMBER
IS
v_total NUMBER(12,2);
BEGIN
IF p_base IS NULL OR p_base < 0 THEN
RAISE_APPLICATION_ERROR(-20001, 'La base imponible debe ser positiva');
END IF;
IF p_tipo_iva NOT IN (4, 10, 21) THEN
RAISE_APPLICATION_ERROR(-20002, 'Tipo de IVA no válido. Use 4, 10 o 21');
END IF;
v_total := ROUND(p_base * (1 + p_tipo_iva / 100), 2);
RETURN v_total;
END fn_calcular_iva;
/
Uso:
-- IVA general (21%)
SELECT fn_calcular_iva(1000) AS total_general FROM DUAL;
-- Resultado: 1210.00
-- IVA reducido (10%)
SELECT fn_calcular_iva(1000, 10) AS total_reducido FROM DUAL;
-- Resultado: 1100.00
⚠️ Importante: Una función usada dentro de una sentencia SQL no puede modificar datos (INSERT, UPDATE, DELETE) ni usar
COMMIToROLLBACK. Si necesitas modificar datos, usa un procedimiento.
4. Eliminar y gestionar subprogramas
4.1. Eliminar subprogramas
DROP PROCEDURE pr_listar_conductores;
DROP FUNCTION fn_calcular_edad;
4.2. Recompilar manualmente
ALTER PROCEDURE pr_listar_conductores COMPILE;
ALTER FUNCTION fn_calcular_edad COMPILE;
4.3. Ver subprogramas en el diccionario de datos
-- Ver todos tus procedimientos y funciones
SELECT object_name, object_type, status, created, last_ddl_time
FROM user_objects
WHERE object_type IN ('PROCEDURE', 'FUNCTION')
ORDER BY object_type, object_name;
-- Ver el código fuente de un subprograma
SELECT line, text
FROM user_source
WHERE name = 'FN_CALCULAR_EDAD'
ORDER BY line;
-- Ver errores de compilación
SELECT line, position, text
FROM user_errors
WHERE name = 'FN_CALCULAR_EDAD'
ORDER BY line;
💡 Truco: Si un procedimiento o función tiene errores de compilación, Oracle lo marca con estado
INVALID. Puedes ver los errores conSHOW ERRORSen SQL*Plus/SQLcl justo después de compilar, o consultando la vistaUSER_ERRORS.
5. Llamadas entre subprogramas
Los subprogramas pueden llamarse entre sí:
-- Función auxiliar
CREATE OR REPLACE FUNCTION fn_es_mayor_edad(p_fecha_nac IN DATE)
RETURN BOOLEAN
IS
BEGIN
RETURN MONTHS_BETWEEN(SYSDATE, p_fecha_nac) / 12 >= 18;
END fn_es_mayor_edad;
/
-- Procedimiento que usa la función
CREATE OR REPLACE PROCEDURE pr_verificar_conductor
(p_nombre IN conductor.nombre%TYPE)
IS
v_fec_nac conductor.fec_nacimiento%TYPE;
BEGIN
SELECT fec_nacimiento INTO v_fec_nac
FROM conductor
WHERE UPPER(nombre) LIKE '%' || UPPER(p_nombre) || '%'
AND ROWNUM = 1;
IF fn_es_mayor_edad(v_fec_nac) THEN
DBMS_OUTPUT.PUT_LINE(p_nombre || ' es mayor de edad');
ELSE
DBMS_OUTPUT.PUT_LINE(p_nombre || ' es menor de edad');
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('Conductor no encontrado: ' || p_nombre);
END pr_verificar_conductor;
/
6. Introducción a los paquetes (Packages)
Un paquete agrupa procedimientos, funciones, tipos, variables y cursores relacionados en una unidad lógica. Consta de dos partes:
6.1. Especificación (cabecera pública)
Define qué ofrece el paquete (la interfaz pública):
CREATE OR REPLACE PACKAGE pkg_infracciones
IS
-- Constantes públicas
c_iva CONSTANT NUMBER := 21;
-- Procedimientos públicos
PROCEDURE listar_vigentes;
PROCEDURE listar_por_importe(p_min NUMBER DEFAULT 0);
-- Funciones públicas
FUNCTION obtener_total_valor RETURN NUMBER;
FUNCTION clasificar(p_importe NUMBER) RETURN VARCHAR2;
END pkg_infracciones;
/
6.2. Cuerpo (implementación)
Define cómo funcionan los elementos declarados en la especificación:
CREATE OR REPLACE PACKAGE BODY pkg_infracciones
IS
-- Función privada (solo visible dentro del paquete)
FUNCTION formatear_importe(p_importe NUMBER) RETURN VARCHAR2
IS
BEGIN
RETURN TO_CHAR(p_importe, '9G999D00') || ' €';
END formatear_importe;
-- Implementación de procedimientos públicos
PROCEDURE listar_vigentes
IS
BEGIN
FOR r IN (SELECT descripcion, valor_economico FROM infraccion WHERE vigente = 1) LOOP
DBMS_OUTPUT.PUT_LINE(r.descripcion || ': ' || formatear_importe(r.valor_economico));
END LOOP;
END listar_vigentes;
PROCEDURE listar_por_importe(p_min NUMBER DEFAULT 0)
IS
BEGIN
FOR r IN (SELECT descripcion, valor_economico
FROM infraccion
WHERE vigente = 1 AND valor_economico >= p_min
ORDER BY valor_economico DESC) LOOP
DBMS_OUTPUT.PUT_LINE(r.descripcion || ': ' || formatear_importe(r.valor_economico));
END LOOP;
END listar_por_importe;
-- Implementación de funciones públicas
FUNCTION obtener_total_valor RETURN NUMBER
IS
v_total NUMBER;
BEGIN
SELECT SUM(valor_economico) INTO v_total
FROM infraccion
WHERE vigente = 1;
RETURN NVL(v_total, 0);
END obtener_total_valor;
FUNCTION clasificar(p_importe NUMBER) RETURN VARCHAR2
IS
BEGIN
RETURN CASE
WHEN p_importe < 100 THEN 'LEVE'
WHEN p_importe < 300 THEN 'GRAVE'
WHEN p_importe >= 300 THEN 'MUY GRAVE'
ELSE 'DESCONOCIDA'
END;
END clasificar;
END pkg_infracciones;
/
6.3. Uso del paquete
-- Llamar a procedimientos del paquete
EXEC pkg_infracciones.listar_vigentes;
EXEC pkg_infracciones.listar_por_importe(200);
-- Usar funciones del paquete
SELECT
descripcion,
valor_economico,
pkg_infracciones.clasificar(valor_economico) AS tipo
FROM infraccion;
-- Usar constantes del paquete
DECLARE
v_total NUMBER;
BEGIN
v_total := pkg_infracciones.obtener_total_valor;
DBMS_OUTPUT.PUT_LINE('Total: ' || v_total);
DBMS_OUTPUT.PUT_LINE('IVA aplicable: ' || pkg_infracciones.c_iva || '%');
END;
/
📘 Concepto: Los paquetes son la forma recomendada de organizar código PL/SQL en proyectos reales. Proporcionan encapsulación (elementos privados vs públicos), evitan conflictos de nombres y permiten sobrecarga de subprogramas (mismo nombre con diferentes parámetros).
7. Ejercicios prácticos
Ejercicio 1: Procedimiento básico
Crea un procedimiento que muestre los vehículos de una marca determinada:
CREATE OR REPLACE PROCEDURE pr_vehiculos_por_marca
(p_marca IN vehiculo.marca%TYPE)
IS
v_encontrados NUMBER := 0;
BEGIN
DBMS_OUTPUT.PUT_LINE('--- Vehículos marca: ' || UPPER(p_marca) || ' ---');
FOR r IN (SELECT matricula, marca, color
FROM vehiculo
WHERE UPPER(marca) = UPPER(p_marca)) LOOP
v_encontrados := v_encontrados + 1;
DBMS_OUTPUT.PUT_LINE(r.matricula || ' | ' || r.marca || ' | ' ||
NVL(r.color, 'Sin color'));
END LOOP;
DBMS_OUTPUT.PUT_LINE('Total encontrados: ' || v_encontrados);
END pr_vehiculos_por_marca;
/
-- Prueba
EXEC pr_vehiculos_por_marca('Seat');
Ejercicio 2: Función de cálculo
Crea una función que devuelva los años de experiencia de un conductor (diferencia entre la fecha de carnet y hoy):
CREATE OR REPLACE FUNCTION fn_anios_experiencia
(p_fec_carnet IN DATE)
RETURN NUMBER
IS
BEGIN
RETURN TRUNC(MONTHS_BETWEEN(SYSDATE, p_fec_carnet) / 12);
END fn_anios_experiencia;
/
-- Uso en consulta
SELECT nombre, fec_carnet, fn_anios_experiencia(fec_carnet) AS experiencia
FROM conductor
ORDER BY experiencia DESC;
Ejercicio 3: Procedimiento con OUT
Crea un procedimiento que devuelva por parámetros OUT el número de conductores y la fecha del carnet más antiguo:
CREATE OR REPLACE PROCEDURE pr_resumen_conductores
(p_total OUT NUMBER,
p_carnet_viejo OUT DATE)
IS
BEGIN
SELECT COUNT(*), MIN(fec_carnet)
INTO p_total, p_carnet_viejo
FROM conductor;
END pr_resumen_conductores;
/
-- Prueba
DECLARE
v_total NUMBER;
v_fecha DATE;
BEGIN
pr_resumen_conductores(v_total, v_fecha);
DBMS_OUTPUT.PUT_LINE('Conductores: ' || v_total);
DBMS_OUTPUT.PUT_LINE('Carnet más antiguo: ' || v_fecha);
END;
/
Ejercicio 4: Función booleana para validación
Crea una función que valide si una matrícula tiene formato correcto (4 números + 3 letras):
CREATE OR REPLACE FUNCTION fn_validar_matricula
(p_matricula IN VARCHAR2)
RETURN BOOLEAN
IS
BEGIN
RETURN REGEXP_LIKE(p_matricula, '^\d{4}[A-Z]{3}$');
END fn_validar_matricula;
/
-- Prueba
DECLARE
v_mat VARCHAR2(10);
BEGIN
v_mat := '1234ABC';
IF fn_validar_matricula(v_mat) THEN
DBMS_OUTPUT.PUT_LINE(v_mat || ' → Formato correcto');
ELSE
DBMS_OUTPUT.PUT_LINE(v_mat || ' → Formato incorrecto');
END IF;
v_mat := 'ABCD123';
IF fn_validar_matricula(v_mat) THEN
DBMS_OUTPUT.PUT_LINE(v_mat || ' → Formato correcto');
ELSE
DBMS_OUTPUT.PUT_LINE(v_mat || ' → Formato incorrecto');
END IF;
END;
/
Resumen
| Concepto | Detalle |
|---|---|
| Procedimiento | Subprograma que ejecuta una acción; no devuelve valor con RETURN |
| Función | Subprograma que calcula y devuelve un valor; se puede usar en SQL |
| CREATE OR REPLACE | Crea o reemplaza un subprograma existente |
| Parámetro IN | Solo lectura (entrada). Es el modo por defecto |
| Parámetro OUT | Solo escritura (salida). Devuelve un valor al llamador |
| Parámetro IN OUT | Lectura y escritura. Entra con un valor y sale modificado |
| Valores por defecto | DEFAULT permite omitir parámetros en la llamada |
| Notación nombrada | p_param => valor para pasar parámetros por nombre |
| RETURN | Devuelve el valor de una función y termina su ejecución |
| DROP PROCEDURE/FUNCTION | Elimina un subprograma de la BD |
| USER_OBJECTS / USER_SOURCE | Vistas del diccionario para consultar subprogramas |
| SHOW ERRORS | Muestra errores de compilación en SQL*Plus/SQLcl |
| Paquete (Package) | Agrupa subprogramas, tipos y variables en una unidad lógica |
| Especificación del paquete | Interfaz pública (lo que otros pueden usar) |
| Cuerpo del paquete | Implementación (puede incluir elementos privados) |