martes 5 de diciembre de 2006

Cursores en PL/SQL

Programación PL/SQL en el WCLos cursores se utilizan en PL/SQL para manejar las sentencias SELECT. Un cursor esta formado por un conjunto de registros devueltos por una instrucción SQL del tipo SELECT. Desde un punto de visto interno a la base de datos Oracle, los cursores son segmentos de memoria utilizados para realizar operaciones con los registros devueltos tras ejecutar una sentencia SELECT.

Se pueden distinguir dos tipos de cursores:

- Cursores implícitos: Se utilizan cuando la sentencia SELECT devuelve un solo registro y su formato es como sigue:

     DECLARE
       lsalario empleados.salario%TYPE;
       ldni empleados.dni%TYPE;
     BEGIN
       SELECT salario, dni
       INTO lsalario, ldni
       FROM empleados
       WHERE nombre = 'Juan'
       AND apellidos = 'Rodrigo Comas';
       /* Resto de sentencias del bloque */
     END;

Nota: Mucha gente considera que las sentencias UPDATE, dentro de un bloque PLSQL, son también cursores implícitos, no obstante, yo prefiero no incluirlas dentro de este concepto.

- Cursores explícitos: Se utilizan cuando la sentencia SELECT puede devolver varios registros. También se pueden utilizar en consultas que devuelvan un solo registro por razones de eficiencia con respecto a los cursores implícitos, eficiencia que mejorará especialmente si el cursor explícito se tiene que ejecutar varias veces dentro del bloque de código PL/SQL.

Un cursor explícito tiene que ser definido previamente como cualquier otra variable PLSQL y debe serle asignado un nombre. Veamos un ejemplo que muestra el DNI y el salario de los trabajadores incluidos en la tabla empleados:

     DECLARE
       CURSOR cemp IS
       SELECT salario, dni
       FROM empleados;
       cepm_rec cemp%ROWTYPE;
     BEGIN
       FOR cemp_rec IN cemp
       LOOP
         DBMS_OUTPUT.PUT_LINE
           (cemp_rec.dni || ' ' || cemp_rec.salario);
       END LOOP;
     END;

Los cursores explícitos admiten el uso de parámetros. Los parámetros deben declararse junto con el cursor. Por ejemplo:

     DECLARE
       CURSOR cemp(pnombre IN VARCHAR2) IS
       SELECT salario, dni
       FROM empleados
       WHERE nombre = pnombre;
       cepm_rec cemp%ROWTYPE;
       vnombre VARCHAR2(20);
     BEGIN
       vnombre := 'Juan';
       DBMS_OUTPUT.PUT_LINE
         ('Sueldo de los empleados con nombre ' || vnombre);
       FOR cemp_rec IN cemp(vnombre)
       LOOP
         DBMS_OUTPUT.PUT_LINE
           (cemp_rec.dni || ' ' || cemp_rec.salario);
       END LOOP;
     END;

En los ejemplos anteriores los cursores se han controlado con la sentencia FOR pero también pueden controlarse mediante el uso de las sentencias OPEN, FETCH y CLOSE. La sentencia OPEN identifica el cursor que se tiene que utilizar. La sentencia FETCH pone, registro a registro, los valores devueltos por el cursor en las variables correspondientes, variables que pueden estar constituidas por una lista de variables o un registro PLSQL (este es el caso de los ejemplos que incluyo en este artículo). Por último, la sentencia CLOSE cierra el cursor y libera la memoria reservada. Veamos como quedaría nuestro ejemplo utilizando este tipo de sentencias en lugar de utilizar la sentencia FOR:

     DECLARE
       CURSOR cemp(pnombre IN VARCHAR2) IS
       SELECT salario, dni
       FROM empleados
       WHERE nombre = pnombre;
       cepm_rec cemp%ROWTYPE;
       vnombre VARCHAR2(20);
     BEGIN
       vnombre := 'Juan';
       DBMS_OUTPUT.PUT_LINE
         ('Sueldo de los empleados con nombre ' || vnombre);
       OPEN cemp(vnombre);
       LOOP
         FETCH cemp INTO cemp_rec;
         DBMS_OUTPUT.PUT_LINE
           (cemp_rec.dni || ' ' || cemp_rec.salario);
         EXIT WHEN cemp%NOTFOUND; -- Último registro.
       END LOOP;
       DBMS_OUTPUT.PUT_LINE
         ('Número de empleados procesados ' || cemp%ROWCOUNT);
       CLOSE cemp;
     END;

Sólo me queda señalar que existe una tercera opción para manejar cursores que a mí, particularmente, no me gusta utilizar pero que no quiero omitir:

     DECLARE
       TYPE ecursor IS REF CURSOR RETURN empleados%ROWTYPE;
       cemp ecursor;
       cepm_rec empleados%ROWTYPE;
     BEGIN
       OPEN cemp FOR SELECT * FROM empleados;
       FOR cemp_rec IN cemp
       LOOP
         DBMS_OUTPUT.PUT_LINE
           (cemp_rec.dni || ' ' || cemp_rec.salario);
       END LOOP;
     END;

Finalmente sólo mencionar que existen cuatro tipos de atributos que nos permiten controlar la ejecución de un cursor:

- %ISOPEN: Devuelve "true" si el cursor está abierto.
- %FOUND: Devuelve "true" si el registro fue satisfactoriamente procesado.
- %NOTFOUND: Devuelve "true" si el registro no pudo ser procesado. Normalmente esto ocurre cuando ya se han procesado todos los registros devueltos por el cursor.
- %ROWCOUNT: Devuelve el número de registros que han sido procesados hasta ese momento.

11 comentarios:

Lom dijo...

Hola,
Hata hoy siempre había manejado los cursores usando OPEN, FETCH y CLOSE con EXIT WHEN ..%NOTFOUND para salir del LOOP.
Me ha sorprendido la simplicidad de manejo explicada en:
DECLARE
CURSOR cemp IS
SELECT salario, dni
FROM empleados;
cepm_rec cemp%ROWTYPE;
BEGIN
FOR cemp_rec IN cemp
LOOP
DBMS_OUTPUT.PUT_LINE
(cemp_rec.dni || ' ' || cemp_rec.salario);
END LOOP;
END;

Gracias

Carlos Eduardo dijo...

Saludos, muy buena la explicacion, bueno tengo una preg.

Tengo hecho a base de cursores con query's dinamicos un procedimiento en oracle y pues la verdad tarda en los tiempos de respuesta quisiera saber como reducir estos tiempos, si acaso el uso de los cursores reduce estos tiempos o incrementa o cual de las formas que explicas es mas efectiva, bueno bueno esa es una preg :P, la otra es que para el caso de pruebas he corrido mi store y ayer termine el store sin finalizar este (ctrl+alt+delete :P) e intente de nuevo pero con unos cambios en el store (sin modificar parte del cursor o forma de uso) y lo deje asi toda la noche , y hoy revise y no ha terminado :S la pregunta es si el cursor se queda bloqueado o algo asi en la base de datos (ya que lo corri desde una terminal pues no tengo acceso a donde esta montada la base de datos) asi es que tengo esas 2 dudas, me gustaria saber si sabes algo al respecto te agradeceria, bueno si puedes pues postea algo o mandame un correo a char_corp@hot.... gracias.

Pepelu dijo...

Hola Carlos Eduardo,

Los dos motivos más usuales por los que un cursor tarda demasiado tiempo en ejecutarse son:

1) Un problema de rendimiento, es decir, que dentro del cursor tengas alguna consulta dinámica que tarde mucho tiempo en ejecutarse.

En este caso te recomiendo que pruebes las consultas dinámicas una a una para determinar cual es la que te está dando problemas.

2) Que realices alguna operación que necesite bloquear algún objeto de la base de datos y dicho objeto esté ya bloqueado y el cursor se quede experando hasta que el objeto quede liberado.

CRivera dijo...

si se cual cual es la que tarda mas tiempo pero pues es por el tipo de consulta algun consejo para realizar querys mas rapidos, quiero decir mira mi query es algo asi
(SELECT B.ACCOUNT_NO, SUM(B.BALANCE_DUE) FROM CMF_BALANCE@NAMESERVER01, CMF@NAMESERVER01C WHERE B.ACCOUNT_NO = C.ACCOUNT_NO AND C.ACCOUNT_STATUS >= -1 AND B.PPDD_DATE < SYSDATE GROUP BY B.ACCOUNT_NO )

donde NAMESERVER01 es el nombre de uno de los servidores de donde extraigo infomacion, (por eso la necesidad de executar querys dinamicos) y pues no se si al efectuar la operacion de SUM o por el tamannio de la bd (entre 590000 60000 registros) sea el problema, el caso es que si existe la manera de hacer algo como hilos o threads para ejecutar varios de estos querys o algun consejo pepelu.. ???

Pepelu dijo...

No veo nada extraño en esa consulta. Supongo que ambas o al menos una de las tablas tendrá un índice sobre ACCOUNT_NO.

Cuando se accede a bases de datos en otros servidores no se pueden utilizar los índices de las tablas remotas y esto suele generar problemas de rendimiento si se unen con tablas de la base de datos local. Pero en tu caso ambas tablas están en el mismo servidor y entiendo que esto no debería ser un problema.

Prueba a ejecutar la consulta de manera no remota para ver si de esa manera la consulta se ejecuta sin problemas.

DaEnVa dijo...

Hola Pepelu, tengo una pregunta siempre he manejado los cursores con FOR (así como tu lo haces en estos ejemplos), pero por ahí me pasaron un Tip para optimizar el tiempo de respuesta de un cursor, claro siempre teniendo en cuenta de como es que se crea la consulta respetando los indices y de mas, este es el ejemplo del FOR que estoy utilizando y quisieras que me dieras tu opinión al respecto.

DECLARE

BEGIN
FOR cemp_rec IN (SELECT salario sal, dni di
FROM empleados;)
LOOP
DBMS_OUTPUT.PUT_LINE
(cemp_rec.di || ' ' || cemp_rec.sal);
END LOOP;
END;

Saludos.

Pepelu dijo...

Hola DaEnVa,

Yo con las sentencias FOR siempre utilizo cursores, más que nada porque el código es más fácil de leer.

Sinceramente no creo que escribir directamente la consulta SELECT dentro del FOR mejore el rendimiento. Quizá puede que mejore algo el tiempo de parsing, pero la mejora no creo que sea significativa.

DaEnVa dijo...

Hola pepelu, gracias por tu opinión y para ser sincero ahora me quedo mas claro este método, ahora me surge otra duda, que sucede esto con la utilización de la memoria de parte del cliente, cual de todos los métodos expuestos aquí crees tú, que utiliza menos memoria en el computador del cliente, esta duda surge porque en donde trabajo, por motivos como siempre económicos, muchos computadores utilizan RAM de 256mb, se que es poco pero mi intención es optimizar esta parte también, de antemano gracias por tu ayuda.

Salu2.

Pepelu dijo...

DaEnVA,

No creo que haya gran diferencia en cuanto al consumo de memoria en los clientes.

Vania dijo...

Hola a todos:
Tengo una duda sobre cursores y necesito ayuda.
Tengo por ejemplo:
open c1 for
select *
from empleados
Estoy trabajando con REF CURSOR y necesito 2 cosas:
1. recorrer el cursor (c1) para obtener ciertos conjunto de datos
(todos los departementos asociados al conjunto de empleados)
2. enviar c1 de vuelta a un procedimiento almacenado.
Asi las soluciones posibles son:
A: duplicarlo antes de comenzar a recorrer c1:
c2 := c1,
Pero una vez recorrido c1, el cursor C2 queda tambien sin registros y por tanto no lo puedo enviar de vuelta para hacer el paso 2.
B: una vez recorrido c1, volverlo al punto de inicio (se puede hacer eso con el REF CURSOR?)
Estaria muy agradecida con su ayuda,
desde ya muchas gracias
Saludos

Pepelu dijo...

Hola Vania,

No entiendo muy bien lo que quieres hacer. Un cursor no se puede duplicar, una vez recorrido lo puedes volver a abrir.

Si quieres seguir preguntándome puedes hacerlo enviándome un email a oracleyplsql[arroba]gmail[punto]com.