Módulo 08: Introducción al PL/SQL Dinámico
Objetivos del módulo
- Comprender qué es el SQL dinámico y cuándo usarlo
- Dominar
EXECUTE IMMEDIATEpara sentencias dinámicas - Usar variables de enlace (bind variables) para seguridad y rendimiento
- Ejecutar DDL dinámico (CREATE, ALTER, DROP)
- Ejecutar DML dinámico (INSERT, UPDATE, DELETE)
- Ejecutar consultas dinámicas con
INTOy cursores - Conocer el paquete
DBMS_SQLpara casos avanzados - Proteger el código contra SQL Injection
1. ¿Qué es el SQL dinámico?
El SQL dinámico permite construir y ejecutar sentencias SQL en tiempo de ejecución, a diferencia del SQL estático que se escribe directamente en el código PL/SQL.
| Característica | SQL Estático | SQL Dinámico |
|---|---|---|
| Sentencia conocida en compilación | ✅ | ❌ |
| Verificación de sintaxis en compilación | ✅ | ❌ |
| Permite DDL (CREATE, DROP, ALTER) | ❌ | ✅ |
| Nombre de tabla/columna variable | ❌ | ✅ |
| Rendimiento | Mejor | Ligeramente inferior |
📘 Concepto: El SQL estático se compila y verifica cuando creas el procedimiento. El SQL dinámico se compila y verifica cada vez que se ejecuta, lo que aporta flexibilidad pero reduce algo el rendimiento y la seguridad si no se usa correctamente.
¿Cuándo usar SQL dinámico?
- Cuando necesitas ejecutar DDL (CREATE TABLE, DROP INDEX, ALTER…)
- Cuando el nombre de la tabla o columna se determina en tiempo de ejecución
- Cuando la estructura de la consulta cambia según condiciones (cláusulas WHERE opcionales)
- Cuando necesitas ejecutar SQL almacenado en una variable o tabla
2. EXECUTE IMMEDIATE
EXECUTE IMMEDIATE es la forma más simple y recomendada de ejecutar SQL dinámico:
2.1. Sintaxis general
EXECUTE IMMEDIATE cadena_sql
[INTO variable1, variable2, ...]
[USING [IN | OUT | IN OUT] bind1, bind2, ...];
2.2. DDL dinámico
PL/SQL no permite sentencias DDL de forma estática. Con SQL dinámico, sí:
-- Crear una tabla
DECLARE
v_tabla VARCHAR2(30) := 'LOG_AUDITORIA';
BEGIN
EXECUTE IMMEDIATE
'CREATE TABLE ' || v_tabla || ' (' ||
' id NUMBER GENERATED ALWAYS AS IDENTITY, ' ||
' fecha DATE DEFAULT SYSDATE, ' ||
' usuario VARCHAR2(30) DEFAULT USER, ' ||
' accion VARCHAR2(200)' ||
')';
DBMS_OUTPUT.PUT_LINE('Tabla ' || v_tabla || ' creada correctamente');
END;
/
Eliminar y truncar tablas:
-- Procedimiento genérico para eliminar una tabla si existe
CREATE OR REPLACE PROCEDURE pr_drop_tabla_si_existe
(p_tabla IN VARCHAR2)
IS
v_cuenta NUMBER;
BEGIN
SELECT COUNT(*) INTO v_cuenta
FROM user_tables
WHERE table_name = UPPER(p_tabla);
IF v_cuenta > 0 THEN
EXECUTE IMMEDIATE 'DROP TABLE ' || DBMS_ASSERT.SIMPLE_SQL_NAME(p_tabla) || ' CASCADE CONSTRAINTS';
DBMS_OUTPUT.PUT_LINE('Tabla ' || p_tabla || ' eliminada');
ELSE
DBMS_OUTPUT.PUT_LINE('La tabla ' || p_tabla || ' no existe');
END IF;
END pr_drop_tabla_si_existe;
/
⚠️ Importante: Las sentencias DDL realizan un COMMIT implícito antes y después de ejecutarse. Si tienes una transacción DML pendiente, se confirmará automáticamente al ejecutar DDL dinámico.
2.3. DML dinámico con bind variables
Siempre usa variables de enlace (bind variables con :nombre) para pasar valores:
-- INSERT dinámico con bind variables (SEGURO)
CREATE OR REPLACE PROCEDURE pr_insertar_log
(p_accion IN VARCHAR2)
IS
v_sql VARCHAR2(200);
BEGIN
v_sql := 'INSERT INTO log_auditoria (accion) VALUES (:accion)';
EXECUTE IMMEDIATE v_sql USING p_accion;
COMMIT;
END pr_insertar_log;
/
UPDATE dinámico:
-- Procedimiento que actualiza cualquier columna de texto de una tabla
CREATE OR REPLACE PROCEDURE pr_actualizar_campo
(p_tabla IN VARCHAR2,
p_columna IN VARCHAR2,
p_valor IN VARCHAR2,
p_id_col IN VARCHAR2,
p_id_valor IN NUMBER)
IS
v_sql VARCHAR2(500);
v_filas NUMBER;
BEGIN
-- Validar nombres de tabla y columna contra inyección SQL
v_sql := 'UPDATE ' || DBMS_ASSERT.SIMPLE_SQL_NAME(p_tabla) ||
' SET ' || DBMS_ASSERT.SIMPLE_SQL_NAME(p_columna) || ' = :valor' ||
' WHERE ' || DBMS_ASSERT.SIMPLE_SQL_NAME(p_id_col) || ' = :id';
EXECUTE IMMEDIATE v_sql USING p_valor, p_id_valor;
v_filas := SQL%ROWCOUNT;
DBMS_OUTPUT.PUT_LINE('Filas actualizadas: ' || v_filas);
END pr_actualizar_campo;
/
DELETE dinámico:
DECLARE
v_sql VARCHAR2(200);
v_filas NUMBER;
BEGIN
v_sql := 'DELETE FROM log_auditoria WHERE fecha < :fecha_limite';
EXECUTE IMMEDIATE v_sql USING SYSDATE - 30;
v_filas := SQL%ROWCOUNT;
DBMS_OUTPUT.PUT_LINE('Registros eliminados: ' || v_filas);
COMMIT;
END;
/
💡 Truco: Las variables de enlace (
:nombre+USING) no solo previenen inyección SQL, también mejoran el rendimiento: Oracle puede reutilizar el plan de ejecución (soft parse) en lugar de recompilarlo cada vez.
2.4. Consultas dinámicas con INTO
Para consultas que devuelven una sola fila:
-- Contar registros de cualquier tabla
CREATE OR REPLACE FUNCTION fn_contar_registros
(p_tabla IN VARCHAR2)
RETURN NUMBER
IS
v_cuenta NUMBER;
BEGIN
EXECUTE IMMEDIATE
'SELECT COUNT(*) FROM ' || DBMS_ASSERT.SIMPLE_SQL_NAME(p_tabla)
INTO v_cuenta;
RETURN v_cuenta;
END fn_contar_registros;
/
-- Uso
SELECT fn_contar_registros('CONDUCTOR') AS total FROM DUAL;
Con condiciones y bind variables:
DECLARE
v_nombre VARCHAR2(100);
v_sql VARCHAR2(500);
BEGIN
v_sql := 'SELECT nombre FROM conductor WHERE ROWNUM = 1 ORDER BY fec_nacimiento';
EXECUTE IMMEDIATE v_sql INTO v_nombre;
DBMS_OUTPUT.PUT_LINE('Primer conductor: ' || v_nombre);
END;
/
3. Cursores dinámicos
Para consultas que devuelven múltiples filas, se usan cursores dinámicos:
3.1. Cursor REF con OPEN FOR
CREATE OR REPLACE PROCEDURE pr_listar_tabla
(p_tabla IN VARCHAR2)
IS
TYPE t_cursor IS REF CURSOR;
v_cursor t_cursor;
v_sql VARCHAR2(500);
v_nombre VARCHAR2(100);
BEGIN
v_sql := 'SELECT nombre FROM ' || DBMS_ASSERT.SIMPLE_SQL_NAME(p_tabla) ||
' ORDER BY nombre';
OPEN v_cursor FOR v_sql;
LOOP
FETCH v_cursor INTO v_nombre;
EXIT WHEN v_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_nombre);
END LOOP;
CLOSE v_cursor;
END pr_listar_tabla;
/
EXEC pr_listar_tabla('CONDUCTOR');
3.2. SYS_REFCURSOR con bind variables
CREATE OR REPLACE PROCEDURE pr_buscar_multas_periodo
(p_fecha_ini IN DATE,
p_fecha_fin IN DATE)
IS
v_cursor SYS_REFCURSOR;
v_sql VARCHAR2(500);
v_fecha DATE;
v_hora VARCHAR2(5);
v_importe NUMBER;
BEGIN
v_sql := 'SELECT fecha_multa, hora_multa, importe ' ||
'FROM multa ' ||
'WHERE fecha_multa BETWEEN :fini AND :ffin ' ||
'ORDER BY fecha_multa';
OPEN v_cursor FOR v_sql USING p_fecha_ini, p_fecha_fin;
DBMS_OUTPUT.PUT_LINE('--- Multas del periodo ---');
LOOP
FETCH v_cursor INTO v_fecha, v_hora, v_importe;
EXIT WHEN v_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_fecha || ' ' || v_hora || ' - ' || v_importe || ' €');
END LOOP;
CLOSE v_cursor;
END pr_buscar_multas_periodo;
/
4. DBMS_SQL: SQL dinámico avanzado
El paquete DBMS_SQL ofrece un control más granular para casos complejos donde EXECUTE IMMEDIATE no es suficiente:
- Número variable de columnas en el
SELECT - Estructura de la consulta completamente desconocida
- Procesamientos por lotes masivos
4.1. Flujo básico de DBMS_SQL
DECLARE
v_cursor_id INTEGER;
v_sql VARCHAR2(500);
v_filas INTEGER;
v_nombre VARCHAR2(100);
BEGIN
-- 1. Abrir cursor
v_cursor_id := DBMS_SQL.OPEN_CURSOR;
-- 2. Parsear la sentencia
v_sql := 'SELECT nombre FROM conductor WHERE ROWNUM <= :limite';
DBMS_SQL.PARSE(v_cursor_id, v_sql, DBMS_SQL.NATIVE);
-- 3. Bind variables
DBMS_SQL.BIND_VARIABLE(v_cursor_id, ':limite', 5);
-- 4. Definir columnas de salida
DBMS_SQL.DEFINE_COLUMN(v_cursor_id, 1, v_nombre, 100);
-- 5. Ejecutar
v_filas := DBMS_SQL.EXECUTE(v_cursor_id);
-- 6. Fetch en bucle
WHILE DBMS_SQL.FETCH_ROWS(v_cursor_id) > 0 LOOP
DBMS_SQL.COLUMN_VALUE(v_cursor_id, 1, v_nombre);
DBMS_OUTPUT.PUT_LINE('Conductor: ' || v_nombre);
END LOOP;
-- 7. Cerrar cursor
DBMS_SQL.CLOSE_CURSOR(v_cursor_id);
EXCEPTION
WHEN OTHERS THEN
IF DBMS_SQL.IS_OPEN(v_cursor_id) THEN
DBMS_SQL.CLOSE_CURSOR(v_cursor_id);
END IF;
RAISE;
END;
/
📘 Concepto:
DBMS_SQLes más verboso queEXECUTE IMMEDIATE, pero permite construir consultas donde ni siquiera conoces el número de columnas en tiempo de compilación. Para la mayoría de casos,EXECUTE IMMEDIATEes suficiente y recomendado. UsaDBMS_SQLsolo cuando EXECUTE IMMEDIATE no cubra tu necesidad.
4.2. DML masivo con DBMS_SQL
DECLARE
v_cursor_id INTEGER;
v_sql VARCHAR2(200);
v_filas INTEGER;
BEGIN
v_cursor_id := DBMS_SQL.OPEN_CURSOR;
v_sql := 'UPDATE infraccion SET valor_economico = valor_economico * :factor WHERE vigente = :vig';
DBMS_SQL.PARSE(v_cursor_id, v_sql, DBMS_SQL.NATIVE);
DBMS_SQL.BIND_VARIABLE(v_cursor_id, ':factor', 1.05);
DBMS_SQL.BIND_VARIABLE(v_cursor_id, ':vig', 1);
v_filas := DBMS_SQL.EXECUTE(v_cursor_id);
DBMS_OUTPUT.PUT_LINE('Infracciones actualizadas: ' || v_filas);
DBMS_SQL.CLOSE_CURSOR(v_cursor_id);
ROLLBACK; -- Deshacemos para no alterar datos de prueba
EXCEPTION
WHEN OTHERS THEN
IF DBMS_SQL.IS_OPEN(v_cursor_id) THEN
DBMS_SQL.CLOSE_CURSOR(v_cursor_id);
END IF;
RAISE;
END;
/
5. Seguridad: protección contra SQL Injection
⚠️ Importante: La inyección SQL es uno de los riesgos más graves en el desarrollo de software. En SQL dinámico, NUNCA concatenes directamente valores proporcionados por el usuario en la cadena SQL. Usa siempre bind variables o las funciones del paquete
DBMS_ASSERT.
5.1. Ejemplo vulnerable (NUNCA hacer esto)
-- ❌ VULNERABLE a SQL Injection
CREATE OR REPLACE PROCEDURE pr_buscar_INSEGURO
(p_nombre IN VARCHAR2)
IS
v_sql VARCHAR2(500);
BEGIN
-- ¡¡¡PELIGRO!!! Concatenación directa de entrada del usuario
v_sql := 'SELECT nombre FROM conductor WHERE nombre = ''' || p_nombre || '''';
-- Un atacante podría enviar: ' OR '1'='1
-- Resultado: SELECT nombre FROM conductor WHERE nombre = '' OR '1'='1'
EXECUTE IMMEDIATE v_sql;
END;
/
5.2. Ejemplo seguro con bind variables
-- ✅ SEGURO con bind variables
CREATE OR REPLACE PROCEDURE pr_buscar_SEGURO
(p_nombre IN VARCHAR2)
IS
v_sql VARCHAR2(500);
v_nombre VARCHAR2(100);
BEGIN
v_sql := 'SELECT nombre FROM conductor WHERE nombre = :nombre AND ROWNUM = 1';
EXECUTE IMMEDIATE v_sql INTO v_nombre USING p_nombre;
DBMS_OUTPUT.PUT_LINE('Encontrado: ' || v_nombre);
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('No encontrado');
END pr_buscar_SEGURO;
/
5.3. DBMS_ASSERT para nombres de objetos
Cuando necesitas incluir nombres de tablas o columnas dinámicamente (no se pueden usar con bind variables), utiliza DBMS_ASSERT:
-- Validar nombre de tabla/columna
v_tabla_segura := DBMS_ASSERT.SIMPLE_SQL_NAME(p_tabla);
-- Lanza error si el nombre contiene caracteres no permitidos
-- Validar que un nombre incluye el esquema
v_tabla_segura := DBMS_ASSERT.QUALIFIED_SQL_NAME(p_esquema || '.' || p_tabla);
-- Validar que es un nombre de objeto existente
v_tabla_segura := DBMS_ASSERT.SQL_OBJECT_NAME(p_tabla);
Ejemplo completo seguro:
CREATE OR REPLACE FUNCTION fn_contar_seguro
(p_tabla IN VARCHAR2,
p_where IN VARCHAR2 DEFAULT NULL)
RETURN NUMBER
IS
v_sql VARCHAR2(500);
v_cuenta NUMBER;
v_tabla VARCHAR2(128);
BEGIN
-- Validar el nombre de la tabla
v_tabla := DBMS_ASSERT.SIMPLE_SQL_NAME(p_tabla);
v_sql := 'SELECT COUNT(*) FROM ' || v_tabla;
-- Solo añadir WHERE si se proporciona
IF p_where IS NOT NULL THEN
v_sql := v_sql || ' WHERE ' || p_where;
END IF;
EXECUTE IMMEDIATE v_sql INTO v_cuenta;
RETURN v_cuenta;
END fn_contar_seguro;
/
-- Uso seguro
SELECT fn_contar_seguro('CONDUCTOR') FROM DUAL;
SELECT fn_contar_seguro('INFRACCION', 'vigente = 1') FROM DUAL;
5.4. Reglas de oro de seguridad
| Regla | Detalle |
|---|---|
| Bind variables para valores | Siempre usar :nombre + USING para datos |
| DBMS_ASSERT para identificadores | Validar nombres de tablas/columnas |
| Nunca concatenar entrada del usuario | Ni directa ni indirectamente |
| Principio de mínimo privilegio | Solo dar permisos estrictamente necesarios |
| Validar entrada | Comprobar tipo, longitud y rango de los parámetros |
6. Patrones útiles de SQL dinámico
6.1. Consulta con WHERE dinámico
CREATE OR REPLACE PROCEDURE pr_busqueda_avanzada
(p_nombre IN VARCHAR2 DEFAULT NULL,
p_marca IN VARCHAR2 DEFAULT NULL,
p_color IN VARCHAR2 DEFAULT NULL)
IS
v_sql VARCHAR2(1000);
v_cursor SYS_REFCURSOR;
v_mat VARCHAR2(20);
v_marca VARCHAR2(50);
v_color VARCHAR2(50);
BEGIN
v_sql := 'SELECT v.matricula, v.marca, v.color ' ||
'FROM vehiculo v ' ||
'JOIN conductor c ON v.DNI = c.DNI ' ||
'WHERE 1=1';
IF p_nombre IS NOT NULL THEN
v_sql := v_sql || ' AND UPPER(c.nombre) LIKE ''%'' || UPPER(:nombre) || ''%''';
ELSE
v_sql := v_sql || ' AND (1=1 OR :nombre IS NULL)';
END IF;
IF p_marca IS NOT NULL THEN
v_sql := v_sql || ' AND UPPER(v.marca) = UPPER(:marca)';
ELSE
v_sql := v_sql || ' AND (1=1 OR :marca IS NULL)';
END IF;
IF p_color IS NOT NULL THEN
v_sql := v_sql || ' AND UPPER(v.color) = UPPER(:color)';
ELSE
v_sql := v_sql || ' AND (1=1 OR :color IS NULL)';
END IF;
v_sql := v_sql || ' ORDER BY v.marca, v.matricula';
OPEN v_cursor FOR v_sql USING p_nombre, p_marca, p_color;
DBMS_OUTPUT.PUT_LINE('--- Resultados de búsqueda ---');
LOOP
FETCH v_cursor INTO v_mat, v_marca, v_color;
EXIT WHEN v_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_mat || ' | ' || v_marca || ' | ' || NVL(v_color, 'N/A'));
END LOOP;
CLOSE v_cursor;
END pr_busqueda_avanzada;
/
-- Búsquedas de ejemplo
EXEC pr_busqueda_avanzada(p_marca => 'Seat');
EXEC pr_busqueda_avanzada(p_color => 'Rojo');
EXEC pr_busqueda_avanzada(p_nombre => 'García', p_marca => 'Seat');
6.2. Creación dinámica de tabla de backup
CREATE OR REPLACE PROCEDURE pr_backup_tabla
(p_tabla IN VARCHAR2)
IS
v_tabla_orig VARCHAR2(128);
v_tabla_backup VARCHAR2(128);
v_existe NUMBER;
BEGIN
v_tabla_orig := DBMS_ASSERT.SIMPLE_SQL_NAME(p_tabla);
v_tabla_backup := v_tabla_orig || '_BKP_' || TO_CHAR(SYSDATE, 'YYYYMMDD');
-- Verificar si ya existe el backup
SELECT COUNT(*) INTO v_existe
FROM user_tables
WHERE table_name = UPPER(v_tabla_backup);
IF v_existe > 0 THEN
EXECUTE IMMEDIATE 'DROP TABLE ' || v_tabla_backup;
END IF;
-- Crear backup como copia de la tabla original
EXECUTE IMMEDIATE 'CREATE TABLE ' || v_tabla_backup ||
' AS SELECT * FROM ' || v_tabla_orig;
DBMS_OUTPUT.PUT_LINE('Backup creado: ' || v_tabla_backup);
-- Mostrar filas copiadas
DECLARE
v_filas NUMBER;
BEGIN
EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM ' || v_tabla_backup INTO v_filas;
DBMS_OUTPUT.PUT_LINE('Filas copiadas: ' || v_filas);
END;
END pr_backup_tabla;
/
-- Uso
EXEC pr_backup_tabla('CONDUCTOR');
7. Ejercicios prácticos
Ejercicio 1: DDL dinámico
Crea un procedimiento que genere una tabla de auditoría para cualquier tabla existente:
CREATE OR REPLACE PROCEDURE pr_crear_tabla_auditoria
(p_tabla IN VARCHAR2)
IS
v_tabla VARCHAR2(128);
v_audit VARCHAR2(128);
v_existe NUMBER;
BEGIN
v_tabla := DBMS_ASSERT.SIMPLE_SQL_NAME(p_tabla);
v_audit := 'AUD_' || v_tabla;
-- Eliminar si ya existe
SELECT COUNT(*) INTO v_existe
FROM user_tables WHERE table_name = UPPER(v_audit);
IF v_existe > 0 THEN
EXECUTE IMMEDIATE 'DROP TABLE ' || v_audit;
END IF;
EXECUTE IMMEDIATE
'CREATE TABLE ' || v_audit || ' (' ||
' aud_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, ' ||
' aud_fecha TIMESTAMP DEFAULT SYSTIMESTAMP, ' ||
' aud_user VARCHAR2(30) DEFAULT USER, ' ||
' aud_accion VARCHAR2(10), ' ||
' aud_detalle VARCHAR2(4000)' ||
')';
DBMS_OUTPUT.PUT_LINE('Tabla de auditoría creada: ' || v_audit);
END pr_crear_tabla_auditoria;
/
-- Prueba
EXEC pr_crear_tabla_auditoria('CONDUCTOR');
Ejercicio 2: Consulta genérica con paginación
Crea una función que devuelva el número de páginas necesarias para mostrar una tabla completa:
CREATE OR REPLACE FUNCTION fn_paginas_necesarias
(p_tabla IN VARCHAR2,
p_filas_pagina IN NUMBER DEFAULT 10)
RETURN NUMBER
IS
v_total NUMBER;
BEGIN
EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM ' || DBMS_ASSERT.SIMPLE_SQL_NAME(p_tabla)
INTO v_total;
RETURN CEIL(v_total / p_filas_pagina);
END fn_paginas_necesarias;
/
-- Prueba
SELECT fn_paginas_necesarias('CONDUCTOR', 5) AS paginas FROM DUAL;
SELECT fn_paginas_necesarias('INFRACCION') AS paginas FROM DUAL;
Ejercicio 3: Reporte dinámico
Crea un procedimiento que genere un reporte configurable de multas:
CREATE OR REPLACE PROCEDURE pr_reporte_multas
(p_orden IN VARCHAR2 DEFAULT 'fecha_multa',
p_direccion IN VARCHAR2 DEFAULT 'DESC',
p_limite IN NUMBER DEFAULT 10)
IS
v_sql VARCHAR2(1000);
v_cursor SYS_REFCURSOR;
v_fecha DATE;
v_importe NUMBER;
v_matricula VARCHAR2(20);
v_dir VARCHAR2(4);
BEGIN
-- Validar dirección
v_dir := CASE UPPER(p_direccion)
WHEN 'ASC' THEN 'ASC'
ELSE 'DESC'
END;
v_sql := 'SELECT fecha_multa, importe, matricula FROM multa ' ||
'ORDER BY ' || DBMS_ASSERT.SIMPLE_SQL_NAME(p_orden) || ' ' || v_dir ||
' FETCH FIRST :limite ROWS ONLY';
OPEN v_cursor FOR v_sql USING p_limite;
DBMS_OUTPUT.PUT_LINE(RPAD('FECHA', 12) || RPAD('IMPORTE', 12) || 'MATRÍCULA');
DBMS_OUTPUT.PUT_LINE(RPAD('-', 36, '-'));
LOOP
FETCH v_cursor INTO v_fecha, v_importe, v_matricula;
EXIT WHEN v_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(RPAD(v_fecha, 12) ||
RPAD(v_importe || ' €', 12) ||
v_matricula);
END LOOP;
CLOSE v_cursor;
END pr_reporte_multas;
/
-- Pruebas
EXEC pr_reporte_multas;
EXEC pr_reporte_multas('importe', 'DESC', 5);
EXEC pr_reporte_multas(p_orden => 'matricula', p_direccion => 'ASC');
Resumen
| Concepto | Detalle |
|---|---|
| SQL dinámico | Sentencias SQL construidas y ejecutadas en tiempo de ejecución |
| EXECUTE IMMEDIATE | Forma principal de ejecutar SQL dinámico en PL/SQL |
| INTO | Captura el resultado de un SELECT dinámico de una fila |
| USING | Pasa valores a las bind variables (:nombre) de la sentencia |
| OPEN cursor FOR | Abre un cursor dinámico para consultas de múltiples filas |
| SYS_REFCURSOR | Tipo predefinido para cursores dinámicos |
| DBMS_SQL | Paquete para SQL dinámico avanzado (estructura desconocida) |
| Bind variables | :nombre en la sentencia + USING para pasar valores de forma segura |
| DBMS_ASSERT | Paquete para validar nombres de objetos SQL (previene inyección) |
| SQL Injection | Vulnerabilidad por concatenar entrada del usuario en SQL; prevenida con bind variables |
| DDL dinámico | EXECUTE IMMEDIATE 'CREATE/ALTER/DROP...'; realiza COMMIT implícito |
| WHERE 1=1 | Patrón útil para construir cláusulas WHERE dinámicas incrementalmente |