-- 22 --

Retrieving Data with Cursors

When you're using SQL*Plus, you can submit a query without being concerned with the number of rows that are returned. It doesn't matter whether the query retrieves zero, one, or a thousand rows. However, in PL/SQL subprograms, you cannot use an ordinary SELECT statement to retrieve more than one row. If a SELECT statement in a PL/SQL subprogram--;be it an anonymous block, a stored procedure, or a trigger--;retrieves more than one row, Oracle returns an error message. Obviously, the capability to retrieve more than one row is essential, and the resource that Oracle provides to accomplish this job is the cursor. Oracle utilities such as SQL*Plus automatically create and use cursors.

Here's an example of how Oracle deals with a SELECT statement that returns more than one row:

SQL> declare

  2

  2  Employee_ID     Employee.Employee_ID%type;

  3  Last_Name       Employee.Last_Name%type;

  4  First_Name      Employee.First_Name%type;

  5

  5  begin

  6

  6  select Employee_ID, Last_Name, First_Name

  7  into   Employee_ID, Last_Name, First_Name

  8  from Employee

  9  order by Employee_ID;

 10

 10  end;

 11  /

declare

 *

ERROR at line 1:

ORA-01422: exact fetch returns more than requested number of rows

ORA-06512: at line 6

You can think of a cursor as a window into the result set of a query. (See Figure 22.1.) You will generally perform four steps when you use a cursor:

Figure 22.1. Illustration of a cursor.



Note

In PL/SQL, a SELECT statement that returns more than one row raises the predefined exception TOO_MANY_ROWS.


This chapter focuses on the practical use of cursors in PL/SQL.

Here is a stored procedure that returns information about repair items that probably shouldn't be repaired because the market value of the item is less than the estimated repair cost. This procedure--;Get_Uneconomical_Repairs--;makes use of a cursor.

SQL> create or replace procedure Get_Uneconomical_Repairs is

  2

  2  Rep_ID       Repair_Item.Repair_ID%type;

  3  Item_No      Repair_Item.Item_Number%type;

  4  Est_Cost     Repair_Item.Estimated_Cost%type;

  5  Used_Val     Product.Current_Used_Value%type;

  6

  6  cursor get_est_cost_used_val is

  7         select Repair_ID, Item_Number, Estimated_Cost, Current_Used_Value

  8         from Repair_Item I, Product P

  9         where

 10         I.Product_ID = P.Product_ID and

 11         I.Manufacturer_ID = P.Manufacturer_ID and

 12         Estimated_Cost > Current_Used_Value

 13         order by Repair_ID, Item_Number;

 14

 14  begin

 15

 15  dbms_output.enable;

 16

 16  open get_est_cost_used_val;

 17

 17  loop

 18

 18     fetch get_est_cost_used_val

 19           into Rep_ID, Item_No, Est_Cost, Used_Val;

 20     exit when get_est_cost_used_val%notfound;

 21

 21     dbms_output.put_line(Rep_ID || '  ' || to_char(Item_No) ||  '  '

 22        || '  ' || to_char(Est_Cost) || '  '

 23        || to_char(Used_Val));

 24

 24

 24  end loop;

 25

 25  end;

 26  /

Procedure created.

SQL> set serveroutput on

SQL> execute Get_Uneconomical_Repairs;

501  1    231  85

501  1    231  85

PL/SQL procedure successfully completed.

Here's a look at each step of this cursor in detail. First, the cursor--;Get_Est_Cost_Used_Val--;is declared as a join between two tables--;Repair_Item and Product. Second, the cursor is opened within the executable section of the stored procedure. Third, a loop statement fetches rows from the cursor until no more rows are retrieved.

Declaring a Cursor

Every cursor must be declared before it can be used. Declaring a cursor means giving it a name and specifying the SELECT statement with which the cursor is associated. The basic syntax used in PL/SQL to declare a cursor is

CURSOR cursor-name

[(parameter1 parameter1-datatype [:= default1],

...

 parameterN parameterN-datatype [:= defaultN])]

IS select-stmt;

where cursor-name is the name of the cursor and subject to Oracle object-naming requirements.

parameter1 is the name of the first parameter to be supplied to the cursor.

parameter1-datatype is the datatype for parameter1.

default1 is an optional default value for parameter1.

parameterN is the name of the last parameter to be supplied to the cursor.

parameterN-datatype is the datatype for parameterN.

defaultN is an optional default value for parameterN.

select-stmt is a valid SELECT statement that will be associated with the declared cursor.

For instance, here is a simple cursor declaration that doesn't specify any parameters:

cursor get_employees is

     select Employee_ID, Last_Name, First_Name

     from Employee

     order by Employee_ID;

The following example shows a cursor declaration with three parameters:

  2  cursor patients_with_hypertension

  3         (patient_age number,

  4          normal_dyastolic,

  5          normal_systolic) is

  6          select patient_id, age, dyastolic, systolic

  7          from patient

  8          where

  9          dyastolic > normal_dyastolic * (age+200)/200 and

 10          systolic > normal_systolic * (age+200)/200;

Here is the same cursor with default values specified for each of the cursor parameters:

  2  cursor patients_with_hypertension

  3         (patient_age number default 55,

  4          normal_dyastolic number default 70,

  5          normal_systolic  number default 130) is

  6          select patient_id, age, dyastolic, systolic

  7          from patient

  8          where

  9          dyastolic > normal_dyastolic * (age+200)/200 and

 10          systolic > normal_systolic * (age+200)/200;



Note

If you use any of the Oracle precompilers--;for example, Pro*C--;you'll need to use a cursor to retrieve more than one row via a SELECT statement.


Opening a Cursor

Before you can fetch rows from a cursor, you must open the cursor. When the cursor is opened, its SELECT statement is executed and Oracle constructs a list of the qualified rows. These rows are referred to as the active set. If the cursor was declared without any parameters, the syntax is very simple.

open my_cursor;

If the cursor was declared with parameters, you must supply a PL/SQL variable or a literal value for each parameter when you open the cursor.

SQL> declare

  2

  2  cursor patients_with_hypertension

  3         (patient_age number,

  4          normal_dyastolic number) is

  5          select patient_id

  6          from patient

  7          where

  8          dyastolic > normal_dyastolic * (age+200)/200 and

  9          systolic > 180;

 10

 10  Patient_ID Patient.Patient_ID%type;

 11

 11  begin

 12

 12  open patients_with_hypertension (45, 80);

 13

 13  end;

 14  /

PL/SQL procedure successfully completed.

If the cursor was declared with parameters--;and default values were specified for those parameters--;you do not have to furnish a PL/SQL variable or a literal value for each parameter.

SQL> declare

  2

  2  cursor patients_with_hypertension

  3         (patient_age number default 55,

  4          normal_dyastolic number default 70,

  5          normal_systolic  number default 130) is

  6          select patient_id

  7          from patient

  8          where

  9          dyastolic > normal_dyastolic * (age+200)/200 and

 10          systolic > normal_systolic * (age+200)/200;

 11

 11  Patient_ID Patient.Patient_ID%type;

 12

 12  begin

 13

 13  dbms_output.enable;

 14

 14  open patients_with_hypertension;

 15

 15  loop

 16

 16     fetch patients_with_hypertension

 17           into Patient_ID;

 18     exit when patients_with_hypertension%notfound;

 19

 19     dbms_output.put_line(patient_record.patient_id);

 20

 20  end loop;

 21

 21  end;

 22  /

N3393

PL/SQL procedure successfully completed.

If the cursor was declared with parameters--;but no default values were specified for those parameters--;you must supply a PL/SQL variable or a literal value for each parameter. Otherwise, Oracle will reject the open cursor statement.

SQL> declare

  2

  2  cursor patients_with_hypertension

  3         (patient_age number,

  4          normal_dyastolic number,

  5          normal_systolic number) is

  6          select patient_id

  7          from patient

  8          where

  9          dyastolic > normal_dyastolic * (age+200)/200 and

 10          systolic > 180;

 11

 11  Patient_ID Patient.Patient_ID%type;

 12

 12  begin

 13

 13  open patients_with_hypertension;

 14

 14  end;

 15  /

declare

 *

ERROR at line 1:

ORA-06550: line 13, column 1:

PLS-00306: wrong number or types of arguments

           in call to 'PATIENTS_WITH_HYPERTENSION'

ORA-06550: line 13, column 1:

PL/SQL: SQL Statement ignored

Fetching Rows from a Cursor

Once the cursor has been opened, the query has been executed and the qualified rows have been identified. To retrieve the rows, you must execute the FETCH statement, which retrieves the value of each column specified in the cursor's SELECT statement and places it in a PL/SQL variable. In general, you'll want to fetch rows within a loop. To illustrate, here is an anonymous PL/SQL block that fetches rows from the Employee table:

SQL> declare

  2

  2  Employee_ID     Employee.Employee_ID%type;

  3  Last_Name       Employee.Last_Name%type;

  4  First_Name      Employee.First_Name%type;

  5

  5  cursor get_employees is

  6         select Employee_ID, Last_Name, First_Name

  7         from Employee

  8         order by Employee_ID;

  9

  9  begin

 10

 10  dbms_output.enable;

 11

 11  open get_employees;

 12

 12  loop

 13

 13     fetch get_employees

 14        into

 15        Employee_ID, Last_Name, First_Name;

 16

 16     exit when get_employees%notfound;

 17

 17     dbms_output.put_line(to_char(Employee_ID));

 18

 18  end loop;

 19

 19  end;

 20  /

1001

1002

1003

1004

1005

1006

1007

1008

1009

PL/SQL procedure successfully completed.

Never make any assumptions about how many rows will be fetched from a cursor. Instead, you should use the EXIT statement to exit the loop when all rows have been fetched from the cursor. The syntax to use is

EXIT [label] [WHEN condition]

where label is an optional label name that specifies which loop should be exited and condition is a PL/SQL condition that returns a Boolean value.

Four specific attributes are associated with declared cursors: %ROWCOUNT, %FOUND, %NOTFOUND, and %ISOPEN. These attributes are referenced by placing them after a cursor's name. To terminate a loop with the EXIT statement, reference a cursor's %NOTFOUND attribute in this way:

exit when get_employees%notfound;

Closing a Cursor

You must close a cursor for two reasons:

If a PL/SQL program doesn't close a cursor, Oracle closes the cursor when the subprogram disconnects from the Oracle database, either by terminating or by performing a DISCONNECT. Closing a cursor is straightforward:

close get_employees;

The following example shows you how you to supply a different set of parameter values to a cursor by closing the cursor, changing the parameter values, and reopening the cursor. First, the patients_with_hypertension cursor is opened with age set to 50 and normal_dyastolic set to 80. The rows are fetched in a loop, and the cursor is closed. Next, the cursor is reopened with age equal to 40 and normal_dyastolic set at 70. The rows are fetched in a loop with different results.

SQL> declare  2

  2  Patient_ID   Patient.Patient_ID%type;

  3  Age          Patient.Age%type;

  4  Dyastolic    Patient.Dyastolic%type;

  5

  5  cursor patients_with_hypertension

  6         (patient_age number,

  7          normal_dyastolic number) is

  8          select patient_id, age, dyastolic

  9          from patient

 10          where

 11          dyastolic > normal_dyastolic * (age+200)/200;

 12

 12  begin

 13

 13  dbms_output.enable;

 14

 14  open patients_with_hypertension (50, 80);

 15

 15  loop

 16

 16     fetch patients_with_hypertension

 17           into Patient_ID, Age, Dyastolic;

 18     exit when patients_with_hypertension%notfound;

 19

 19     dbms_output.put_line('With age=50, dyas=80: ' || Patient_ID);

 20

 20  end loop;

 21

 21  close patients_with_hypertension;

 22

 22  open patients_with_hypertension (40, 70);

 23

 23  loop

 24

 24     fetch patients_with_hypertension

 25           into Patient_ID, Age, Dyastolic;

 26     exit when patients_with_hypertension%notfound;

 27

 27     dbms_output.put_line('With age=40, dyas=70: ' || Patient_ID);

 28

 28  end loop;

 29

 29  close patients_with_hypertension;

 30

 30  end;

 31  /

With age=50, dyas=80: N3393

With age=40, dyas=70: A2002

With age=40, dyas=70: N3393

With age=40, dyas=70: E3893

PL/SQL procedure successfully completed.

Working with Cursor FOR Loops

As an alternative to opening, fetching, and closing a cursor, Oracle furnishes another approach--;the cursor FOR loop. With the cursor FOR loop, Oracle implicitly declares a variable--;the loop index--;that is of the same record type as the cursor's record.

SQL> declare

  2

  2  cursor Get_Unecon_Repairs is

  3         select Repair_ID, Item_Number, Estimated_Cost, Current_Used_Value

  4         from Repair_Item I, Product P

  5         where

  6         I.Product_ID = P.Product_ID and

  7         I.Manufacturer_ID = P.Manufacturer_ID and

  8         Estimated_Cost > Current_Used_Value

  9         order by Repair_ID, Item_Number;

 10

 10  begin

 11

 11  dbms_output.enable;

 12

 12  for Get_Unecon_Repairs_Rec in Get_Unecon_Repairs loop

 13

 13     dbms_output.put_line ('Repair ID: ' ||

 14                           Get_Unecon_Repairs_Rec.Repair_ID);

 15

 15  end loop;

 16

 16  end;

 17  /

Repair ID: 501

PL/SQL procedure successfully completed.

The name that follows FOR is the loop index that is implicitly declared. However, you can't reference the loop index outside of the loop statement.

SQL> declare

  2

  2  cursor Get_Unecon_Repairs is

  3         select Repair_ID, Item_Number, Estimated_Cost, Current_Used_Value

  4         from Repair_Item I, Product P

  5         where

  6         I.Product_ID = P.Product_ID and

  7         I.Manufacturer_ID = P.Manufacturer_ID and

  8         Estimated_Cost > Current_Used_Value

  9         order by Repair_ID, Item_Number;

 10

 10  begin

 11

 11  dbms_output.enable;

 12

 12  for Get_Unecon_Repairs_Rec in Get_Unecon_Repairs loop

 13

 13     dbms_output.put_line ('Repair ID: ' ||

 14                           Get_Unecon_Repairs_Rec.Repair_ID);

 15

 15  end loop;

 16

 16  Get_Unecon_Repairs_Rec.Manufacturer_ID := 'XYZ';

 17

 17  end;

 18  /

declare

 *

ERROR at line 1:

ORA-06550: line 16, column 24:

PLS-00201: identifier 'GET_UNECON_REPAIRS_REC.MANUFACTURER_ID' must be declared

ORA-06550: line 16, column 1:

PL/SQL: Statement ignored

Was It %FOUND or %NOTFOUND?

The previous examples used the %NOTFOUND attribute to determine whether a FETCH statement retrieved a row. When all of the rows in the active set have been fetched and the last FETCH statement fails to retrieve a row, %NOTFOUND evaluates to TRUE.



Note

Before the FETCH statement is invoked, %NOTFOUND returns a NULL. If your PL/SQL program has a loop in which the FETCH statement might not be called, you should consider testing for the condition of %NOTFOUND evaluating to NULL.


Getting the Number of Rows with %ROWCOUNT

You don't need a counter to keep track of the number of rows that are fetched from a cursor. Instead, reference the cursor's %ROWCOUNT attribute. %ROWCOUNT returns the running count of the rows that have been fetched. Here's an example:

SQL> declare

  2

  2  Employee_ID     Employee.Employee_ID%type;

  3  Last_Name       Employee.Last_Name%type;

  4  First_Name      Employee.First_Name%type;

  5

  5  cursor get_employees is

  6         select Employee_ID, Last_Name, First_Name

  7         from Employee

  8         order by Employee_ID;

  9

  9  begin

 10

 10  dbms_output.enable;

 11

 11  open get_employees;

 12

 12  loop

 13

 13     fetch get_employees

 14        into

 15        Employee_ID, Last_Name, First_Name;

 16

 16     exit when get_employees%notfound;

 17

 17     dbms_output.put_line ('Rowcount: ' || get_employees%rowcount);

 18

 18  end loop;

 19

 19  end;

 20  /

Rowcount: 1

Rowcount: 2

Rowcount: 3

Rowcount: 4

Rowcount: 5

Rowcount: 6

Rowcount: 7

Rowcount: 8

Rowcount: 9

PL/SQL procedure successfully completed.

Instead of exiting a loop when there are no more rows in the cursor, you can specify an exit condition when a specified %ROWCOUNT is achieved.

SQL> declare

  2

  2  Employee_ID     Employee.Employee_ID%type;

  3  Last_Name       Employee.Last_Name%type;

  4  First_Name      Employee.First_Name%type;

  5

  5  cursor get_employees is

  6         select Employee_ID, Last_Name, First_Name

  7         from Employee

  8         order by Employee_ID;

  9

  9  begin

 10

 10  dbms_output.enable;

 11

 11  open get_employees;

 12

 12  loop

 13

 13     fetch get_employees

 14        into

 15        Employee_ID, Last_Name, First_Name;

 16

 16     exit when get_employees%rowcount >= 5;

 17

 17     dbms_output.put_line ('Rowcount: ' || get_employees%rowcount);

 18

 18  end loop;

 19

 19  end;

 20  /

Rowcount: 1

Rowcount: 2

Rowcount: 3

Rowcount: 4

PL/SQL procedure successfully completed.

The Cursor %ISOPEN

A PL/SQL subprogram can have multiple cursors open simultaneously. The program logic of a PL/SQL subprogram might be quite complex; during the subprogram's execution, you might need to determine whether a particular cursor is open. The %ISOPEN attribute can be associated with any cursor; it returns TRUE if the cursor is open and FALSE if the cursor is not open.

SQL> declare

  2

  2  Employee_ID     Employee.Employee_ID%type;

  3  Last_Name       Employee.Last_Name%type;

  4  First_Name      Employee.First_Name%type;

  5

  5  cursor get_employees is

  6         select Employee_ID, Last_Name, First_Name

  7         from Employee

  8         order by Employee_ID;

  9

  9  begin

 10

 10  dbms_output.enable;

 11

 11  open get_employees;

 12

 12  loop

 13

 13     fetch get_employees

 14        into

 15        Employee_ID, Last_Name, First_Name;

 16

 16     exit when get_employees%rowcount >= 5;

 17

 17     if get_employees%isopen then

 18       dbms_output.put_line ('The get_employees cursor is open.');

 19     else

 20       dbms_output.put_line ('The get_employees cursor is closed.');

 21     end if;

 22

 22  end loop;

 23

 23  close get_employees;

 24

 24  if get_employees%isopen then

 25    dbms_output.put_line ('The get_employees cursor is open.');

 26  else

 27    dbms_output.put_line ('The get_employees cursor is closed.');

 28  end if;

 29

 29  end;

 30  /

The get_employees cursor is open.

The get_employees cursor is open.

The get_employees cursor is open.

The get_employees cursor is open.

The get_employees cursor is closed.

PL/SQL procedure successfully completed.

Using Multiple Cursors

You can declare cursors based on very complex SELECT statements--;for example, multiple-table joins or UNIONs. However, some calculations involve multiple steps that simply can't be performed in a single SELECT. In such cases, you can implement the processing logic by nesting one cursor inside another cursor.

I'll illustrate this concept with an example from the repair store application. Using the Repair_Header and Repair_Item tables, suppose that you wanted to retrieve each repair header, analyze the related customer information, and--;depending on the results of the customer analysis--;retrieve information on each repair item for that repair. You can achieve this result by defining two cursors: get_repair_header_info and get_repair_item_info. The second cursor is defined with the argument Repair_ID. Here's what the block looks like: ***Production: Please make sure the instances of double hyphens in the code below do not become em dashes. Thanks.***

SQL> declare

  2

  2  Rep_ID           Repair_Header.Repair_ID%type;

  3  Customer_ID      Repair_Header.Customer_ID%type;

  4  Item_Number      Repair_Item.Item_Number%type;

  5  Product_ID       Repair_Item.Product_ID%type;

  6  Manufacturer_ID  Repair_Item.Manufacturer_ID%type;

  7

  7  cursor get_repair_header_info is

  8          select Repair_ID, Customer_ID

  9          from Repair_Header

 10          order by Repair_ID;

 11

 11  cursor get_repair_item_info (This_Repair_ID number) is

 12         select Item_Number, Product_ID, Manufacturer_ID

 13         from Repair_Item

 14         where

 15         Repair_ID = This_Repair_ID

 16         order by Item_Number;

 17

 17  begin

 18

 18  dbms_output.enable;

 19

 19  open get_repair_header_info;

 20

 20  loop

 21

 21     fetch get_repair_header_info

 22           into Rep_ID, Customer_ID;

 23     exit when get_repair_header_info%notfound;

 24

 24     dbms_output.put_line('Repair_ID: ' || Rep_ID);

 25

 25     open get_repair_item_info (Rep_ID);

 26

 26  -- Additional statements for customer analysis

 27  -- appear here.

 28

 28     loop

 29

 29        fetch get_repair_item_info

 30              into Item_Number, Product_ID, Manufacturer_ID;

 31        exit when get_repair_item_info%notfound;

 32

 32        dbms_output.put_line('   Item_Number:    ' || to_char(Item_Number));

 33        dbms_output.put_line('   Product_ID:     ' || Product_ID);

 34        dbms_output.put_line('   Manufacturer_ID:' || Manufacturer_ID);

 35

 35     end loop;

 36

 36     close get_repair_item_info;

 37

 37  end loop;

 38

 38  close get_repair_header_info;

 39

 39  end;

 40  /

Repair_ID: 501

Item_Number:     1

Product_ID:      C2002

Manufacturer_ID: MIT501

Repair_ID: 502

Item_Number:     1

Product_ID:      D301

Manufacturer_ID: SEN101

Repair_ID: 503

Item_Number:     1

Product_ID:      B311

Manufacturer_ID: TES801

Repair_ID: 504

Item_Number:     1

Product_ID:      B801

Manufacturer_ID: SEN101

Repair_ID: 505

Item_Number:     1

Product_ID:      A903

Manufacturer_ID: TES801

Repair_ID: 506

Item_Number:     1

Product_ID:      TR901

Manufacturer_ID: TES801

PL/SQL procedure successfully completed.

Summary

This chapter covers the following concepts: