Módulo 04: Cursores en PL/SQL

Objetivos del módulo

  • Entender qué es un cursor y por qué es necesario
  • Diferenciar entre cursores implícitos y explícitos
  • Declarar, abrir, recorrer y cerrar cursores explícitos
  • Usar el bucle FOR con cursores (cursor FOR LOOP)
  • Crear cursores con parámetros
  • Trabajar con cursores de actualización (FOR UPDATE / WHERE CURRENT OF)
  • Dominar los atributos de cursor: %FOUND, %NOTFOUND, %ISOPEN, %ROWCOUNT

1. ¿Qué es un cursor?

Un cursor es un puntero a un área de memoria privada (denominada context area) que Oracle utiliza para procesar una sentencia SQL. Cada vez que ejecutas una sentencia SQL, Oracle crea internamente un cursor.

📘 Concepto: Imagina que tienes una tabla con 1000 filas y quieres procesarlas una a una. SELECT INTO solo permite una fila. La solución: un cursor, que funciona como un “dedo” que va señalando cada fila del resultado mientras tú la procesas.

Tipos de cursores

Tipo Descripción ¿Quién lo gestiona?
Implícito Oracle lo crea automáticamente para cada sentencia SQL Oracle
Explícito Lo declaras y gestionas tú manualmente El programador

2. Cursores implícitos

Oracle crea un cursor implícito cada vez que ejecutas una sentencia DML (INSERT, UPDATE, DELETE) o un SELECT INTO. No necesitas declarar nada.

Atributos de los cursores implícitos

Ya los vimos en el módulo anterior con el prefijo SQL%:

BEGIN
    UPDATE infraccion
    SET valor_economico = valor_economico * 1.01
    WHERE vigente = 1;
    
    DBMS_OUTPUT.PUT_LINE('Filas actualizadas: ' || SQL%ROWCOUNT);
    
    IF SQL%FOUND THEN
        DBMS_OUTPUT.PUT_LINE('Se encontraron registros');
    END IF;
    
    ROLLBACK;
END;
/

⚠️ Importante: Los atributos SQL% se actualizan con cada sentencia SQL que ejecutes. Si necesitas guardar el valor de SQL%ROWCOUNT para usarlo después, asígnalo a una variable inmediatamente tras la sentencia DML.


3. Cursores explícitos

Con los cursores explícitos tienes control total del proceso. El ciclo de vida tiene 4 pasos:

1. DECLARE  →  Declarar el cursor (definir la consulta)
2. OPEN     →  Abrir el cursor (ejecutar la consulta)
3. FETCH    →  Recuperar filas una a una
4. CLOSE    →  Cerrar el cursor (liberar recursos)

3.1. Sintaxis básica

DECLARE
    -- 1. DECLARAR el cursor
    CURSOR c_conductores IS
        SELECT nombre, fec_nacimiento, fec_carnet
        FROM conductor;
    
    -- Variables para almacenar cada fila
    v_nombre   conductor.nombre%TYPE;
    v_fec_nac  conductor.fec_nacimiento%TYPE;
    v_fec_car  conductor.fec_carnet%TYPE;
BEGIN
    -- 2. ABRIR el cursor
    OPEN c_conductores;
    
    -- 3. RECORRER (FETCH) fila a fila
    LOOP
        FETCH c_conductores INTO v_nombre, v_fec_nac, v_fec_car;
        
        -- Salir cuando no hay más filas
        EXIT WHEN c_conductores%NOTFOUND;
        
        DBMS_OUTPUT.PUT_LINE(v_nombre || ' - Carnet: ' || v_fec_car);
    END LOOP;
    
    -- 4. CERRAR el cursor
    CLOSE c_conductores;
END;
/

3.2. Atributos de cursores explícitos

Atributo Tipo Descripción
cursor%FOUND BOOLEAN TRUE si el último FETCH devolvió una fila
cursor%NOTFOUND BOOLEAN TRUE si el último FETCH no devolvió fila
cursor%ISOPEN BOOLEAN TRUE si el cursor está abierto
cursor%ROWCOUNT NUMBER Número de filas recuperadas hasta el momento
DECLARE
    CURSOR c_infracciones IS
        SELECT descripcion, valor_economico
        FROM infraccion
        WHERE vigente = 1;
    
    v_desc    infraccion.descripcion%TYPE;
    v_importe infraccion.valor_economico%TYPE;
BEGIN
    OPEN c_infracciones;
    
    LOOP
        FETCH c_infracciones INTO v_desc, v_importe;
        EXIT WHEN c_infracciones%NOTFOUND;
        
        DBMS_OUTPUT.PUT_LINE(c_infracciones%ROWCOUNT || '. ' || 
                             v_desc || ' → ' || v_importe || ' €');
    END LOOP;
    
    DBMS_OUTPUT.PUT_LINE('Total infracciones procesadas: ' || c_infracciones%ROWCOUNT);
    
    CLOSE c_infracciones;
END;
/

⚠️ Importante: Siempre cierra los cursores cuando termines. Los cursores abiertos consumen recursos del servidor. Si te olvidas de cerrar un cursor, puede producirse un error ORA-06511: PL/SQL: cursor already open si intentas abrirlo de nuevo.


4. FETCH con %ROWTYPE

En lugar de declarar una variable por cada columna, puedes usar %ROWTYPE:

DECLARE
    CURSOR c_vehiculos IS
        SELECT * FROM vehiculo;
    
    r_vehiculo vehiculo%ROWTYPE;
BEGIN
    OPEN c_vehiculos;
    
    LOOP
        FETCH c_vehiculos INTO r_vehiculo;
        EXIT WHEN c_vehiculos%NOTFOUND;
        
        DBMS_OUTPUT.PUT_LINE(r_vehiculo.matricula || ' | ' || 
                             r_vehiculo.marca || ' | ' || 
                             NVL(r_vehiculo.color, 'Sin color'));
    END LOOP;
    
    CLOSE c_vehiculos;
END;
/

💡 Truco: Usar %ROWTYPE es especialmente útil cuando el cursor selecciona muchas columnas. Además, si la estructura de la tabla cambia, tu código se adapta automáticamente.


5. Cursor FOR LOOP (forma simplificada)

El cursor FOR LOOP es la forma más concisa y recomendada de recorrer un cursor. Oracle se encarga automáticamente de:

  • Abrir el cursor
  • Declarar la variable de registro
  • Hacer el FETCH en cada iteración
  • Cerrar el cursor al terminar
DECLARE
    CURSOR c_conductores IS
        SELECT nombre, fec_carnet
        FROM conductor
        ORDER BY nombre;
BEGIN
    FOR r IN c_conductores LOOP
        DBMS_OUTPUT.PUT_LINE(r.nombre || ' - Carnet: ' || r.fec_carnet);
    END LOOP;
    -- No necesitas OPEN, FETCH ni CLOSE
END;
/

Cursor FOR LOOP sin declaración previa (cursor inline)

Se puede definir la consulta directamente en el FOR, sin declarar el cursor:

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;
/

📘 Concepto: El cursor FOR LOOP es la forma más usada y recomendada en PL/SQL moderno. Solo necesitas el ciclo manual (OPEN/FETCH/CLOSE) cuando requieres un control más fino, como procesar filas en lotes o usar condicionales complejas entre fetches.


6. Cursores con parámetros

Los cursores parametrizados permiten reutilizar la misma consulta con diferentes valores:

DECLARE
    CURSOR c_multas_por_lugar(p_lugar VARCHAR2) IS
        SELECT fecha, lugar
        FROM multa
        WHERE UPPER(lugar) LIKE '%' || UPPER(p_lugar) || '%'
        ORDER BY fecha;
BEGIN
    DBMS_OUTPUT.PUT_LINE('=== Multas en Madrid ===');
    FOR r IN c_multas_por_lugar('MADRID') LOOP
        DBMS_OUTPUT.PUT_LINE(r.fecha || ' - ' || r.lugar);
    END LOOP;
    
    DBMS_OUTPUT.PUT_LINE('');
    DBMS_OUTPUT.PUT_LINE('=== Multas en Barcelona ===');
    FOR r IN c_multas_por_lugar('BARCELONA') LOOP
        DBMS_OUTPUT.PUT_LINE(r.fecha || ' - ' || r.lugar);
    END LOOP;
END;
/

Cursores con múltiples parámetros

DECLARE
    CURSOR c_infracciones(p_min_importe NUMBER, p_vigente NUMBER) IS
        SELECT descripcion, puntos, valor_economico
        FROM infraccion
        WHERE valor_economico >= p_min_importe
        AND vigente = p_vigente
        ORDER BY valor_economico DESC;
BEGIN
    DBMS_OUTPUT.PUT_LINE('--- Infracciones vigentes >= 200€ ---');
    FOR r IN c_infracciones(200, 1) LOOP
        DBMS_OUTPUT.PUT_LINE(r.descripcion || ' | ' || 
                             r.puntos || ' pts | ' || 
                             r.valor_economico || ' €');
    END LOOP;
END;
/

💡 Truco: Los parámetros del cursor son de solo lectura (modo IN) y solo aceptan tipos de datos. No puedes usar %TYPE ni restricciones como NOT NULL en la declaración de parámetros del cursor.


7. Cursores de actualización (FOR UPDATE)

Cuando necesitas modificar o borrar las filas que estás procesando con un cursor, debes usar FOR UPDATE al declararlo y WHERE CURRENT OF en las sentencias DML.

7.1. FOR UPDATE (bloqueo de filas)

FOR UPDATE bloquea las filas seleccionadas para que ninguna otra sesión las modifique mientras las procesas:

DECLARE
    CURSOR c_infracciones_update IS
        SELECT id, descripcion, valor_economico
        FROM infraccion
        WHERE vigente = 1
        FOR UPDATE OF valor_economico;  -- Bloqueamos la columna que vamos a modificar
    
    v_incremento NUMBER := 1.10;  -- 10% de incremento
BEGIN
    FOR r IN c_infracciones_update LOOP
        UPDATE infraccion
        SET valor_economico = r.valor_economico * v_incremento
        WHERE CURRENT OF c_infracciones_update;
        
        DBMS_OUTPUT.PUT_LINE('Actualizada: ' || r.descripcion || 
                             ' → ' || ROUND(r.valor_economico * v_incremento, 2) || ' €');
    END LOOP;
    
    DBMS_OUTPUT.PUT_LINE('Infracciones actualizadas: ' || SQL%ROWCOUNT);
    
    ROLLBACK;  -- Deshacemos para no modificar datos reales
END;
/

7.2. WHERE CURRENT OF (actualizar la fila actual)

WHERE CURRENT OF cursor permite actualizar o borrar exactamente la fila en la que el cursor está posicionado, sin necesidad de repetir la condición WHERE:

DECLARE
    CURSOR c_vehiculos_sin_color IS
        SELECT matricula, marca, color
        FROM vehiculo
        WHERE color IS NULL
        FOR UPDATE OF color;
BEGIN
    FOR r IN c_vehiculos_sin_color LOOP
        UPDATE vehiculo
        SET color = 'Sin especificar'
        WHERE CURRENT OF c_vehiculos_sin_color;
    END LOOP;
    
    DBMS_OUTPUT.PUT_LINE('Vehículos actualizados: ' || SQL%ROWCOUNT);
    
    ROLLBACK;
END;
/

7.3. DELETE con WHERE CURRENT OF

DECLARE
    CURSOR c_infracciones_obsoletas IS
        SELECT id, descripcion
        FROM infraccion
        WHERE vigente = 0
        FOR UPDATE;
BEGIN
    FOR r IN c_infracciones_obsoletas LOOP
        DBMS_OUTPUT.PUT_LINE('Eliminando: ' || r.descripcion);
        
        DELETE FROM infraccion
        WHERE CURRENT OF c_infracciones_obsoletas;
    END LOOP;
    
    DBMS_OUTPUT.PUT_LINE('Total eliminadas: ' || SQL%ROWCOUNT);
    
    ROLLBACK;
END;
/

⚠️ Importante: FOR UPDATE bloquea las filas hasta que se ejecute un COMMIT o ROLLBACK. Si otra sesión intenta modificar las mismas filas, quedará en espera. Usa FOR UPDATE NOWAIT si prefieres que lance un error inmediato en lugar de esperar, o FOR UPDATE WAIT n para esperar un máximo de n segundos.


8. Variables de cursor (REF CURSOR)

Las variables de cursor o REF CURSOR permiten que un cursor apunte a diferentes consultas en tiempo de ejecución:

DECLARE
    TYPE t_ref_cursor IS REF CURSOR;
    v_cursor t_ref_cursor;
    
    v_opcion NUMBER := 1;
    v_nombre VARCHAR2(100);
BEGIN
    -- Abrir el cursor con diferentes consultas según la opción
    IF v_opcion = 1 THEN
        OPEN v_cursor FOR 
            SELECT nombre FROM conductor ORDER BY nombre;
    ELSE
        OPEN v_cursor FOR 
            SELECT marca FROM vehiculo ORDER BY marca;
    END IF;
    
    LOOP
        FETCH v_cursor INTO v_nombre;
        EXIT WHEN v_cursor%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(v_nombre);
    END LOOP;
    
    CLOSE v_cursor;
END;
/

SYS_REFCURSOR (tipo predefinido)

Oracle proporciona SYS_REFCURSOR como tipo predefinido equivalente a un REF CURSOR sin tipo:

DECLARE
    v_cursor SYS_REFCURSOR;
    v_descripcion infraccion.descripcion%TYPE;
    v_importe     infraccion.valor_economico%TYPE;
BEGIN
    OPEN v_cursor FOR
        SELECT descripcion, valor_economico FROM infraccion WHERE vigente = 1;
    
    LOOP
        FETCH v_cursor INTO v_descripcion, v_importe;
        EXIT WHEN v_cursor%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(v_descripcion || ': ' || v_importe || ' €');
    END LOOP;
    
    CLOSE v_cursor;
END;
/

📘 Concepto: SYS_REFCURSOR es muy útil cuando quieres pasar un cursor como parámetro de un procedimiento o función, permitiendo que el procedimiento que llama decida qué hacer con los resultados.


9. Ejercicios prácticos

Ejercicio 1: Cursor explícito básico

Recorre todos los conductores y muestra su nombre y la edad al sacarse el carnet:

DECLARE
    CURSOR c_conductores IS
        SELECT nombre, fec_nacimiento, fec_carnet
        FROM conductor;
    
    r conductor%ROWTYPE;
    v_edad NUMBER;
BEGIN
    OPEN c_conductores;
    LOOP
        FETCH c_conductores INTO r.nombre, r.fec_nacimiento, r.fec_carnet;
        EXIT WHEN c_conductores%NOTFOUND;
        
        v_edad := TRUNC(MONTHS_BETWEEN(r.fec_carnet, r.fec_nacimiento) / 12);
        DBMS_OUTPUT.PUT_LINE(r.nombre || ' - Se sacó el carnet con ' || v_edad || ' años');
    END LOOP;
    CLOSE c_conductores;
END;
/

Ejercicio 2: Cursor FOR LOOP con estadísticas

Recorre las infracciones vigentes y calcula el total acumulado:

DECLARE
    CURSOR c_infracciones IS
        SELECT descripcion, valor_economico
        FROM infraccion
        WHERE vigente = 1
        ORDER BY valor_economico DESC;
    
    v_total NUMBER := 0;
BEGIN
    DBMS_OUTPUT.PUT_LINE('--- Infracciones Vigentes ---');
    
    FOR r IN c_infracciones LOOP
        v_total := v_total + r.valor_economico;
        DBMS_OUTPUT.PUT_LINE(RPAD(r.descripcion, 50) || TO_CHAR(r.valor_economico, '999G999') || ' €');
    END LOOP;
    
    DBMS_OUTPUT.PUT_LINE(RPAD(' ', 50, '-') || '----------');
    DBMS_OUTPUT.PUT_LINE(RPAD('TOTAL', 50) || TO_CHAR(v_total, '999G999') || ' €');
END;
/

Ejercicio 3: Cursor con parámetros

Crea un cursor parametrizado que muestre las multas entre dos fechas:

DECLARE
    CURSOR c_multas_entre_fechas(p_desde DATE, p_hasta DATE) IS
        SELECT fecha, lugar
        FROM multa
        WHERE fecha BETWEEN p_desde AND p_hasta
        ORDER BY fecha;
    
    v_count NUMBER := 0;
BEGIN
    DBMS_OUTPUT.PUT_LINE('--- Multas del año 2020 ---');
    
    FOR r IN c_multas_entre_fechas(TO_DATE('01/01/2020', 'DD/MM/YYYY'),
                                    TO_DATE('31/12/2020', 'DD/MM/YYYY')) LOOP
        v_count := v_count + 1;
        DBMS_OUTPUT.PUT_LINE(r.fecha || ' - ' || r.lugar);
    END LOOP;
    
    DBMS_OUTPUT.PUT_LINE('Total: ' || v_count || ' multas');
END;
/

Ejercicio 4: Cursor de actualización

Incrementa en un 15% el valor de las infracciones que tengan más de 3 puntos y estén vigentes:

DECLARE
    CURSOR c_infr IS
        SELECT id, descripcion, puntos, valor_economico
        FROM infraccion
        WHERE puntos > 3 AND vigente = 1
        FOR UPDATE OF valor_economico;
    
    v_contador NUMBER := 0;
BEGIN
    FOR r IN c_infr LOOP
        UPDATE infraccion
        SET valor_economico = ROUND(r.valor_economico * 1.15, 2)
        WHERE CURRENT OF c_infr;
        
        v_contador := v_contador + 1;
        DBMS_OUTPUT.PUT_LINE('Actualizada: ' || r.descripcion || 
                             ' | Antes: ' || r.valor_economico || 
                             ' | Después: ' || ROUND(r.valor_economico * 1.15, 2));
    END LOOP;
    
    DBMS_OUTPUT.PUT_LINE('Total actualizadas: ' || v_contador);
    
    ROLLBACK;
END;
/

Resumen

Concepto Detalle
Cursor implícito Oracle lo crea automáticamente para cada sentencia SQL; se consulta con SQL%
Cursor explícito Declarado manualmente; ciclo DECLARE → OPEN → FETCH → CLOSE
%FOUND / %NOTFOUND Indican si el último FETCH devolvió una fila o no
%ISOPEN Indica si el cursor está abierto
%ROWCOUNT Número de filas recuperadas hasta el momento
Cursor FOR LOOP Forma simplificada: OPEN, FETCH y CLOSE automáticos
Cursor inline Consulta definida directamente en el FOR sin declarar cursor
Cursores con parámetros Permiten reutilizar la consulta con diferentes valores
FOR UPDATE Bloquea las filas seleccionadas para modificarlas
WHERE CURRENT OF Actualiza o borra la fila actual del cursor sin repetir la condición
NOWAIT / WAIT n Controlan el comportamiento al encontrar filas bloqueadas
REF CURSOR / SYS_REFCURSOR Variables de cursor dinámicas que pueden apuntar a diferentes consultas