Wednesday, 14 September 2011

Apply best practices to cursor variables

Question:
I have a stored procedure that uses a REF CURSOR parameter to return a result set as a cursor variable. How can I call that procedure and then insert the rows identified by the cursor variable into a table, using FORALL?
Answer:
This question was answered in the March/April 2006 issue of Oracle Magazine. A cursor variable is a PL/SQL variable that points to a result set. You can fetch the rows of a cursor variable's result set just as you would fetch the rows identified by an explicitly declared cursor. You can in particular use BULK COLLECT against a cursor variable to deposit into a collection all the rows identified by the result set. You can then reference that collection in a FORALL INSERT statement.
Let's take a look at the kind of code you would write to achieve your goal. I will use the jokes and joke_archive tables as my example data structures:
CREATE TABLE jokes (
joke_id INTEGER,
title VARCHAR2(100),
text VARCHAR2(4000)
)
/
CREATE TABLE joke_archive (
archived_on DATE,
old_stuff VARCHAR2(4000)
)
/
I will place two jokes in the jokes table, as shown in Listing 1.
Code Listing 1: Inserting jokes into the jokes table

BEGIN
INSERT INTO jokes
VALUES (100, 'Why does an elephant take a shower?'
,'Why does an elephant take a shower? ' ||
'Because it can't fit into the bathtub!');

INSERT INTO jokes
VALUES (101
,'How can you prevent diseases caused by biting insects?'
,'How can you prevent diseases caused by biting insects?' || 'Don't bite any!');

COMMIT;
END;
I now need to write a procedure that will identify joke text or titles that need to be moved to the joke_archive table. Here is the header of my procedure:
CREATE OR REPLACE
PROCEDURE get_title_or_text (
title_like_in IN VARCHAR2
,return_title_in IN BOOLEAN
,joke_count_out OUT PLS_INTEGER
,jokes_out OUT SYS_REFCURSOR
)
I pass in a string (title_like_in) that acts as a filter to identify the rows in the jokes table to be moved to the archive. I specify whether I want to retrieve titles (return_title_in => TRUE) or text (return_title_in => FALSE). I then return the total number of rows identified by the result set (joke_count_out), as well as the result set itself (joke_out). I use the system-defined weak REF CURSOR type, SYS_REFCURSOR (available in Oracle9i Database Release 2 and later).
Listing 2 contains the implementation of the get_title_or_text procedure.
Code Listing 2: get_title_or_text procedure
1 CREATE OR REPLACE PROCEDURE get_title_or_text (
2 title_like_in IN VARCHAR2
3 ,return_title_in IN BOOLEAN
4 ,joke_count_out OUT PLS_INTEGER
5 ,jokes_out OUT SYS_REFCURSOR
6 )
7 IS
8 c_from_where VARCHAR2 (100) := ' FROM jokes WHERE title LIKE :your_title';
9 l_colname all_tab_columns.column_name%TYPE := 'TEXT';
10 l_query VARCHAR2 (32767);
11 BEGIN
12 IF return_title_in
13 THEN
14 l_colname := 'TITLE';
15 END IF;
16
17 l_query := 'SELECT ' || l_colname || c_from_where;
18
19 OPEN jokes_out FOR l_query USING title_like_in;
20
21 EXECUTE IMMEDIATE 'SELECT COUNT(*)' || c_from_where
22 INTO joke_count_out
23 USING title_like_in;
24 END get_title_or_text;
Here is an explanation of the interesting parts of the get_title_or_text procedure in Listing 2.
Line 8. Because the get_title_or_text procedure is executing two dynamic queries, differing only in the SELECT list, store the rest of those queries (the FROM and WHERE clauses) in a reusable string.
Lines 12-17. Construct the dynamic query to retrieve all titles or text with a matching title.
Line 19. Associate the result set with the cursor variable, for the specified title filter.
Lines 21-23. Compute the number of rows identified by the query.
I can now call this procedure, return a result set, move that data into a collection, and then use the collection in a FORALL statement, as shown in Listing 3.
Code Listing 3: From results to collection to FORALL
1 DECLARE
2 l_count PLS_INTEGER;
3 l_jokes sys_refcursor;
4
5 TYPE jokes_tt IS TABLE OF jokes.text%TYPE;
6
7 l_joke_array jokes_tt := jokes_tt ();
8 BEGIN
9 get_title_or_text (title_like_in => '%insect%'
10 ,return_title_in => FALSE
11 ,joke_count_out => l_count
12 ,jokes_out => l_jokes
13 );
14 DBMS_OUTPUT.put_line ('Number of jokes found = ' || l_count);
15
16 FETCH l_jokes
17 BULK COLLECT INTO l_joke_array;
18
19 CLOSE l_jokes;
20
21 FORALL indx IN l_joke_array.FIRST .. l_joke_array.LAST
22 INSERT INTO joke_archive
23 VALUES (SYSDATE, l_joke_array (indx));
24 END;
Here is an explanation of the interesting parts of the anonymous block in Listing 3.
Lines 2 and 3. Declare variables to retrieve the values returned by the get_title_or_text procedure.
Lines 5-7. Declare a nested table to hold the data identified by the cursor variable.
Lines 8-13. Call the stored procedure to return the cursor variable and the count of rows in the result set.
Lines 16-19. Use BULK COLLECT to fetch all the rows in the result set into the nested table. Then close the cursor variable. Note that I use BULK COLLECT with an implicit SELECT INTO to retrieve all the rows identified by the cursor. If you are querying a large volume of data, this approach can consume an unacceptable amount of memory. Under such circumstances, you may want to switch to using BULK COLLECT with a LIMIT clause.
Lines 21-23. Use a FORALL statement to push the data into the joke archive.
Listing 4 contains a modified version of the previous anonymous block in Listing 3, showing how to use the LIMIT clause to restrict the number of rows fetched with a BULK COLLECT query, thus reducing the amount of memory needed to populate the collection.
Code Listing 4: From results to collection to FORALL plus LIMIT
DECLARE
l_count PLS_INTEGER;
l_jokes sys_refcursor;

TYPE jokes_tt IS TABLE OF jokes.text%TYPE;

l_joke_array jokes_tt := jokes_tt ();
BEGIN
get_title_or_text (title_like_in => '%insect%'
,return_title_in => FALSE
,joke_count_out => l_count
,jokes_out => l_jokes
);
DBMS_OUTPUT.put_line ('Number of jokes found = ' || l_count);

LOOP
-- Fetch the next 100 rows.
FETCH l_jokes
BULK COLLECT INTO l_joke_array LIMIT 100;

EXIT WHEN l_joke_array.COUNT = 0;

-- Push them into the archive.
FORALL indx IN l_joke_array.FIRST .. l_joke_array.LAST
INSERT INTO joke_archive
VALUES (SYSDATE, l_joke_array (indx));
END LOOP;

CLOSE l_jokes;
END;
This answer demonstrates how you can take advantage of the flexibility of cursor variables to construct your queries at runtime without losing the performance benefits of the BULK COLLECT and FORALL features.

No comments:

Post a Comment