There are nothing wrong with general idea of migrating to bulk collect
. Bulk operations just minimizes number of context switches and database round trips.
There are only one generic thing which wrong with your code. LIMIT
clause prevents overusing memory by bulk operations, so it's proper decision to use it with bulk collect. But v_reports
grows uncontrolled. Therefore move bulk insert inside a loop and clear v_reports
afterwards.
There are some inaccuracies in altered code. Please review code fragment below, comments in /**/
style are mine.
CREATE OR REPLACE FUNCTION bar(...)
IS
CURSOR cur IS SELECT a,b FROM source_table ;
TYPE t_source IS TABLE OF cur%ROWTYPE INDEX BY PLS_INTEGER;
TYPE t_report IS TABLE OF destination_table%ROWTYPE INDEX BY PLS_INTEGER;
v_sources t_source;
v_reports t_report
/* 1. correct type is same as type of index
2. There are nothing wrong with sparse collections, but a separate
counter which incremented continuously needed for t_report.
*/
v_report_inx PLS_INTEGER := 0; -- To Prevent Sparse Collection
BEGIN
OPEN cur;
<<l_next>> --10G doesn't have the continue statement.
LOOP
FETCH cur BULK COLLECT INTO v_sources LIMIT 100 ;
/* On last step v_sources.count < 100, not exactly 0.
Also if there are no elements then no processing done,
so check at the end of loop.
EXIT WHEN v_sources.count = 0;
*/
/* correct way is to loop from 1 to count
(.last and .first not usable because both is null for empty array)
*/
FOR i IN 1 .. v_sources.count LOOP
v_report_inx := v_report_inx + 1;
--Ignore Record Case: ignore the record entirely
IF v_sources(i).a = -1 THEN
-- do something
GOTO l_next ; --10g doesn't have the continue statement.
END IF;
/* No need for ELSE here, just execution continues */
-- do something else
v_reports(v_report_inx).first := 'SUCCESS' ;
-- Transform Case:
IF v_sources(i).b = 'z' THEN
-- do something
v_reports(v_report_inx).second := 'something';
ELSE
-- do something
v_reports(v_report_inx).second := 'something else';
END IF;
END LOOP;
/* Use "indicies of" construct to deal with sparsed collections */
FORALL i in indices of v_reports
/* t_report already declared with %ROWTYPE
so just insert entire row, it works faster */
INSERT INTO report_table VALUES v_reports(i);
/* Cleanup after insert */
v_reports.delete;
/* If number of selected records less than LIMIT then last row reached. */
EXIT WHEN v_sources.count < 100;
END LOOP;
CLOSE cur;
EXCEPTION
...
END;
Update
Thanks to @jonearles. He encouraged me to test performance for different approaches to handle cursors in PL/SQL.
Below is results for test with 3 000 000 records. It's clearly that migration from plain explicit cursor to bulk collect approach give a real performance gain.
At the same time explicit cursor with bulk collect option and properly choosed LIMIT always outperform implicit cursor, but difference between them lies in acceptable bounds.
Variant name | Time (sec)
-------------------------------------
bulk_cursor_limit_500 | 1.26
bulk_cursor_limit_100 | 1.52
bulk_unlimited | 1.75
implicit_cursor | 1.83
plain_cursor | 27.20
Below is code for test (limited SQLFiddle example here)
Scheme setup
drop table t
/
drop table log_run
/
create table t(a number, b number)
/
insert into t select level, level from dual connect by level <= 3000000
/
create table log_run(id varchar2(30), seconds number);
/
delete log_run
/
Single test run
declare
cursor test_cur is
select a, b from t;
test_rec test_cur%rowtype;
counter number;
vStart timestamp;
vEnd timestamp;
vTimeFormat varchar2(30) := 'SSSSS.FF9';
begin
vStart := systimestamp;
open test_cur;
loop
fetch test_cur into test_rec;
exit when test_cur%notfound;
counter := counter + 1;
end loop;
close test_cur;
vEnd := systimestamp;
insert into log_run(id, seconds)
values('plain_cursor',
to_number(to_char(vEnd,vTimeFormat))
-
to_number(to_char(vStart,vTimeFormat))
)
;
end;
/
--Implicit cursor
--0.2 seconds
declare
test_rec t%rowtype;
counter number;
vStart timestamp;
vEnd timestamp;
vTimeFormat varchar2(30) := 'SSSSS.FF9';
begin
vStart := systimestamp;
for c_test_rec in (select a, b from t) loop
test_rec.a := c_test_rec.a;
test_rec.b := c_test_rec.b;
counter := counter + 1;
end loop;
vEnd := systimestamp;
insert into log_run(id, seconds)
values('implicit_cursor',
to_number(to_char(vEnd,vTimeFormat))
-
to_number(to_char(vStart,vTimeFormat))
)
;
end;
/
declare
cursor test_cur is
select a, b from t;
type t_test_table is table of t%rowtype;
test_tab t_test_table;
counter number;
vStart timestamp;
vEnd timestamp;
vTimeFormat varchar2(30) := 'SSSSS.FF9';
begin
vStart := systimestamp;
open test_cur;
loop
fetch test_cur bulk collect into test_tab limit 100;
for i in 1 .. test_tab.count loop
counter := counter + 1;
end loop;
exit when test_tab.count < 100;
end loop;
close test_cur;
vEnd := systimestamp;
insert into log_run(id, seconds)
values('bulk_cursor_limit_100',
to_number(to_char(vEnd,vTimeFormat))
-
to_number(to_char(vStart,vTimeFormat))
)
;
end;
/
declare
cursor test_cur is
select a, b from t;
type t_test_table is table of t%rowtype;
test_tab t_test_table;
counter number;
vStart timestamp;
vEnd timestamp;
vTimeFormat varchar2(30) := 'SSSSS.FF9';
begin
vStart := systimestamp;
open test_cur;
loop
fetch test_cur bulk collect into test_tab limit 500;
for i in 1 .. test_tab.count loop
counter := counter + 1;
end loop;
exit when test_tab.count < 500;
end loop;
close test_cur;
vEnd := systimestamp;
insert into log_run(id, seconds)
values('bulk_cursor_limit_500',
to_number(to_char(vEnd,vTimeFormat))
-
to_number(to_char(vStart,vTimeFormat))
)
;
end;
/
declare
type t_test_table is table of t%rowtype;
test_tab t_test_table;
counter number;
vStart timestamp;
vEnd timestamp;
vTimeFormat varchar2(30) := 'SSSSS.FF9';
begin
vStart := systimestamp;
select * bulk collect into test_tab from t;
for i in 1 .. test_tab.count loop
counter := counter + 1;
end loop;
vEnd := systimestamp;
insert into log_run(id, seconds)
values('bulk_unlimited',
to_number(to_char(vEnd,vTimeFormat))
-
to_number(to_char(vStart,vTimeFormat))
)
;
end;
/
Select average results
select * from (
select lr.id, trunc(avg(seconds),2) seconds
from log_run lr group by lr.id)
order by seconds
)