1 Theory, Practice & Methodology of Relational Database Design and Programming Copyright © Ellis Cohen Cursors These slides are licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License. For more information on how you may use them, please see
2 © Ellis Cohen Overview of Lecture PL/SQL Cursors Cursor-Based Fetch & Scrollability Cursor Sensitivity Cursor-Based Update Cursor Variables & Parameters Nested Cursors
3 © Ellis Cohen PL/SQL Cursors
4 © Ellis Cohen Cursors as Iterators Iterators are programming language objects which iterate over some sort of collection. Cursors are a special kind of iterator built-in to SQL-based embedded languages which iterate over result sets (i.e. the result of a query)
5 © Ellis Cohen Cursors DECLARE CURSOR curs IS SELECT deptno, count(*) AS knt FROM Emps WHERE job = 'ANALYST' GROUP BY deptno; BEGIN FOR aRec IN curs LOOP pl( aRec.deptno || ': ' || aRec.knt ); END LOOP; END; Separates query description from the use of the result set
6 © Ellis Cohen Parameterized Cursors DECLARE CURSOR curs( aJob varchar ) IS SELECT deptno, count(*) AS knt FROM Emps WHERE job = aJob GROUP BY deptno; BEGIN FOR aRec IN curs( 'ANALYST' ) LOOP pl( aRec.deptno || ': ' || aRec.knt ); END LOOP; END;
7 © Ellis Cohen Package Cursors BEGIN FOR aRec IN EmpPkg.DeptsByJobsCurs( :job ) LOOP pl( … ) END LOOP; EXCEPTION WHEN OTHERS THEN plerr(); END; Instantiates a cursor ShowDeptsByJob( :job ) PACKAGE BODY EmpPkg AS CURSOR DeptsByJobsCurs( theJob varchar ) IS SELECT deptno, count(*) AS knt FROM Emps WHERE job = theJob GROUP BY deptno; … Cursors can be defined in packages
8 © Ellis Cohen Cursor-Based Fetch & Scrollability
9 © Ellis Cohen Explicit Fetch Control DECLARE CURSOR curs … BEGIN FOR rec IN curs LOOP statements END LOOP; END; DECLARE CURSOR curs … rec curs%ROWTYPE; BEGIN OPEN curs; FETCH curs INTO rec; WHILE curs%FOUND LOOP statements FETCH curs INTO rec; END LOOP; CLOSE curs; END;
10 © Ellis Cohen Fetching Example DECLARE CURSOR curs( aJob varchar ) IS SELECT ename, deptno FROM Emps WHERE job = aJob; rec curs%ROWTYPE; BEGIN OPEN curs('ANALYST'); LOOP FETCH curs INTO rec; EXIT WHEN curs%NOTFOUND; INSERT INTO CoolEmps VALUES rec; END LOOP; CLOSE curs; END;
11 © Ellis Cohen Cursor Attributes %FOUND Before the first fetch from curs, curs%FOUND is NULL. Afterwards, it is TRUE if the last fetch returned a row, or FALSE if there are no rows left. %NOTFOUND Before the first fetch from curs, curs%FOUND is NULL. Afterwards, it is FALSE if the last fetch returned a row, or TRUE if there are no rows left. %ROWCOUNT curs%ROWCOUNT returns the number of rows fetched (0 if the cursor is not yet open) %ISOPEN curs%ISOPEN return TRUE if the cursor is open, FALSE if it is not
12 © Ellis Cohen More Fetching Example DECLARE CURSOR c1 IS SELECT * FROM Emps1 ORDER BY empno; CURSOR c2 IS SELECT * FROM Emps2 ORDER BY empno; jemps EmpPkg.EmpList; e1 c1%ROWTYPE; e2 c2%ROWTYPE; BEGIN OPEN c1; FETCH c1 INTO e1; OPEN c2; FETCH c2 INTO e2; WHILE (c1%FOUND or c2%FOUND) LOOP IF (c2%NOTFOUND or (c1%FOUND and (e1.empno < e2.empno))) THEN jemps(jemps.count + 1) := e1; FETCH c1 INTO e1; ELSE jemps(jemps.count + 1) := e2; FETCH c2 INTO e2; END IF; END LOOP; CLOSE c1; CLOSE c2; EmpPkg.DoSomethingCoolWith( jemps ); END; What does this do?
13 © Ellis Cohen Answer: More Fetching Example Emps1 and Emps2 are tables of employees, and we use the cursors c1 and c2 to iterate through them in order of their employee number. We compare the employees pointed to by c1 and c2, and take the employee with the smallest employee number, append it to the jemps array, and move that cursor forward to point to the next employee (in order) in the corresponding table. This is a MERGE, and jemps will contain all the employees, in order of their employee number. (SELECT empno FROM Emps1 UNION ALL (SELECT empno FROM Emps2) ORDER BY empno should be implemented internally in essentially the same way
14 © Ellis Cohen Scrollability Can FETCH only move forward through the result set, or can it move backwards or be repositioned to arbitrary rows? Oracle: Forward-Only SQL Server: Based on options available when defining the cursor
15 © Ellis Cohen Cursor Sensitivity
16 © Ellis Cohen Cursor Sensitivity Once we start iterating through the rows selected by a cursor, are the membership or contents of those rows sensitive to database changes made by this user or other users? Oracle: NO SQL Server: Based on options available when defining the cursor
17 © Ellis Cohen No Membership Sensitivity DECLARE CURSOR curs IS SELECT * from Emps WHERE job = 'ANALYST'; rec curs%ROWTYPE; BEGIN OPEN curs; DELETE Emps WHERE job = 'ANALYST'; COMMIT; LOOP FETCH curs INTO rec; EXIT WHEN curs%NOTFOUND; pl( rec.ename || ' ' || rec.deptno ); END LOOP; CLOSE curs; END; DELETE has no effect on output
18 © Ellis Cohen No Content Sensitivity DECLARE CURSOR curs IS SELECT * from Emps WHERE job = 'ANALYST'; rec curs%ROWTYPE; BEGIN OPEN curs; UPDATE Emps SET deptno = 10 WHERE job = 'ANALYST'; COMMIT; LOOP FETCH curs INTO rec; EXIT WHEN curs%NOTFOUND; pl( rec.ename || ' ' || rec.deptno ); END LOOP; CLOSE curs; END; UPDATE has no effect on output
19 © Ellis Cohen Cursor-Based Update
20 © Ellis Cohen Cursor FOR UPDATE CURSOR curs IS SELECT sal FROM Emps WHERE deptno = 30 FOR UPDATE; Specifies that the cursor will be used to update the underlying table Emps
21 © Ellis Cohen Cursored Update Model empno ename deptno sal comm 7499ALLEN MARTIN BLAKE KING TURNER STERN Emps Result Set curs FOR UPDATE maintains the connection (using ROWIDS) between a tuple in the Result Set and the corresponding tuple in the table that was queried CURRENT OF curs
22 © Ellis Cohen Programming with FOR UPDATE DECLARE CURSOR curs IS SELECT sal FROM Emps WHERE deptno = 30 FOR UPDATE; BEGIN FOR rec IN curs LOOP IF (...) THEN UPDATE Emps SET sal = sal * 1.05 WHERE CURRENT OF curs; END IF; END LOOP; END; enables the selected tuples to be updated through the cursor The tuple in Emps last fetched through the cursor Some complex test
23 © Ellis Cohen Update Sensitivity DECLARE CURSOR curs IS SELECT * from Emps WHERE deptno = 30 FOR UPDATE; rec curs%ROWTYPE; BEGIN OPEN curs; UPDATE Emps SET sal = 100 WHERE deptno = 30; LOOP FETCH curs INTO rec; EXIT WHEN curs%NOTFOUND; UPDATE Emps SET sal = sal * 1.1 WHERE CURRENT OF curs; END LOOP; CLOSE curs; END; What will the value of sal be for tuples where deptno = 30? Also, if the cursor was scrollable, what salary value would be obtained when scrolling back to a previously updated tuple?
24 © Ellis Cohen Cursor Variables & Parameters
25 © Ellis Cohen Cursors vs Ref Cursors CURSOR curs IS SELECT * from Emps WHERE deptno = 30 FOR UPDATE; does NOT declare a cursor variable. It is more like a TYPE or PROCEDURE declaration. Although we can open and fetch from it OPEN curs; FETCH curs INTO aRec; There is no way to store an open cursor into a variable pass an open cursor as a parameter return an open cursor from a function That's what REF CURSORs are for!
26 © Ellis Cohen Ref Cursors DECLARE TYPE EmpCursorType IS REF CURSOR RETURN Emps%ROWTYPE; cv EmpCursorType; rec cv%ROWTYPE; BEGIN IF :mtype = 'job' THEN OPEN cv FOR SELECT * FROM Emps WHERE (job LIKE :match); ELSIF :mtype = 'ename' THEN OPEN cv FOR SELECT * FROM Emps WHERE (ename LIKE :match); END IF; LOOP FETCH cv INTO rec; EXIT WHEN cv%NOTFOUND; pl( rec.ename || ' ' || rec.deptno ); END LOOP; CLOSE cv; END; Dynamically choose query to process Type checking
27 © Ellis Cohen Weak Ref Cursors DECLARE TYPE EmpCursorType IS REF CURSOR; cv EmpCursorType; rec Emps%ROWTYPE; BEGIN IF :mtype = 'job' THEN OPEN cv FOR SELECT * FROM Emps WHERE (job LIKE :match); ELSIF :mtype = 'ename' THEN OPEN cv FOR SELECT * FROM Emps WHERE (ename LIKE :match); END IF; LOOP FETCH cv INTO rec; EXIT WHEN cv%NOTFOUND; pl( rec.ename || ' ' || rec.deptno ); END LOOP; CLOSE cv; END; Weak REF CURSORs do not specify a RETURN type. No early type checking Type checking at FETCH-time
28 © Ellis Cohen SYS_REFCURSOR DECLARE cv SYS_REFCURSOR; rec Emps%ROWTYPE; BEGIN IF :mtype = 'job' THEN OPEN cv FOR SELECT * FROM Emps WHERE (job LIKE :match); ELSIF :mtype = 'ename' THEN OPEN cv FOR SELECT * FROM Emps WHERE (ename LIKE :match); END IF; LOOP FETCH cv INTO rec; EXIT WHEN cv%NOTFOUND; pl( rec.ename || ' ' || rec.deptno ); END LOOP; CLOSE cv; END; Built-in Weak REF CURSOR type
29 © Ellis Cohen Returning REF CURSORs SQL> variable rc REFCURSOR; SQL> execute :rc := EmpPkg.GetEmpByJob( 'ANALYST' ); SQL> print :rc Useful hack FUNCTION GetEmpByJob( aJob varchar ) RETURN SYS_REFCURSOR IS cv SYS_REFCURSOR; BEGIN OPEN cv FOR SELECT * FROM Emps WHERE (job = aJob) or (aJob IS NULL); RETURN cv; END; In EmpPkg CURSOR declarations are simpler Functions that return cursors can do error checking and raise exceptions
30 © Ellis Cohen Returning Result Sets SQL> variable rc REFCURSOR; SQL> execute :rc := EmpPkg.GetEmpByJob( 'ANALYST' ); SQL> print :rc EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO SCOTT ANALYST APR FORD ANALYST DEC SQL> execute :rc := EmpPkg.GetEmpByJob( null ); SQL> print :rc EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO SMITH CLERK DEC ALLEN SALESMAN FEB WARD SALESMAN FEB JONES MANAGER APR MARTIN SALESMAN SEP BLAKE MANAGER MAY CLARK MANAGER JUN SCOTT ANALYST APR KING PRESIDENT 17-NOV TURNER SALESMAN SEP ADAMS CLERK MAY JAMES CLERK DEC FORD ANALYST DEC MILLER CLERK JAN
31 © Ellis Cohen Using a REF Cursor DECLARE cv SYS_REFCURSOR; rec EmpPkg.DeptKntRec; BEGIN cv := EmpPkg.GetDeptsByJobsCurs( :job ); LOOP FETCH cv INTO aRec; EXIT WHEN cv%NOTFOUND; -- use rec to build part of result page END LOOP; EXCEPTION WHEN OTHERS THEN … -- generate error page … -- return; END; … -- build rest of result page & return END; Returns a REF cursor ShowDeptsByJob( :job )
32 © Ellis Cohen Defining & Opening a REF Cursor TYPE DeptKntRec IS RECORD ( deptno int, knt int ); FUNCTION GetDeptsByJobsCurs( theJob varchar ) RETURN SYS_REFCURSOR IS cv SYS_REFCURSOR; BEGIN OPEN cv FOR SELECT deptno, count(*) AS knt FROM Emps WHERE job = theJob GROUP BY deptno; RETURN cv; END; In EmpPkg
33 © Ellis Cohen Nested Cursors
34 © Ellis Cohen List Employees in Each Dept DECLARE fstr varchar(200); sep varchar(5); BEGIN FOR d IN (SELECT deptno,dname FROM Depts ORDER BY dname) LOOP fstr := d.dname; sep := ': '; FOR e IN (SELECT ename FROM Emps WHERE deptno = d.deptno) LOOP fstr := fstr || sep || e.ename; sep := ', '; END LOOP; pl( fstr ); END LOOP; END; ACCOUNTING: CLARK, KING, MILLER OPERATIONS RESEARCH: SMITH, JONES, SCOTT, ADAMS, FORD SALES: ALLEN, WARD, MARTIN, BLAKE, TURNER, JAMES
35 © Ellis Cohen Nested Cursors ACCOUNTING dname CLARK CURSOR(…) KING MILLER OPERATIONS dname RESEARCH SMITH JONES SCOTT … (EMPTY) … dname CURSOR CURSOR(…)
36 © Ellis Cohen Formatting with Nested Cursors DECLARE curs SYS_REFCURSOR := GetDeptEmpCursNest(); ncurs SYS_REFCURSOR; fstr varchar(200); sep char(2) ; nstr varchar(50); BEGIN LOOP FETCH curs INTO fstr, ncurs; EXIT WHEN curs%notfound; sep := ': '; LOOP FETCH ncurs INTO nstr; EXIT WHEN ncurs%notfound; fstr := fstr || sep || nstr; sep := ', '; END LOOP; pl( fstr ); END LOOP; END; Each tuple consists of a string (dname) plus an [opened] REF CURSOR Lists employees in each department
37 © Ellis Cohen Building Nested Cursors FUNCTION GetDeptEmpCursNest RETURN SYS_REFCURSOR IS cv SYS_REFCURSOR; BEGIN OPEN cv FOR SELECT dname, CURSOR( SELECT ename FROM Emps e WHERE e.deptno = d.deptno) FROM Depts d ORDER BY dname; RETURN cv; END; / Nested CURSOR expression Each tuple consists of a string (dname) plus an [opened] REF CURSOR