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
FORcon 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 INTOsolo 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 deSQL%ROWCOUNTpara 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 opensi 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
%ROWTYPEes 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%TYPEni restricciones comoNOT NULLen 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 UPDATEbloquea las filas hasta que se ejecute unCOMMIToROLLBACK. Si otra sesión intenta modificar las mismas filas, quedará en espera. UsaFOR UPDATE NOWAITsi prefieres que lance un error inmediato en lugar de esperar, oFOR UPDATE WAIT npara esperar un máximo densegundos.
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_REFCURSORes 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 |