How to append rows to a existing SYS_REFCURSOR? - oracle

I want to know if there is a way append to the results of a cursor that is fetched from inside a loop.
Right now out_cursor contains the results pertaining to the last iteration of the outer for loop. I want to know if it will be possible to append the rows from each iteration to this cursor so that the cursor will contain rows from all iterations of the loop.
For context, this out_cursor is consumed by a Java DAO class.
CREATE OR REPLACE NONEDITIONABLE PROCEDURE testproc (out_cursor OUT SYS_REFCURSOR)
IS
CURSOR actor_cursor IS
SELECT actor_id, first_name, last_name
FROM actor
WHERE actor_id <= 100;
row1 actor_cursor%rowtype;
TYPE MyRec IS RECORD (actor_id film_actor.actor_id%TYPE, film_id film_actor.film_id%TYPE);
rec MyRec;
BEGIN
FOR row1 IN actor_cursor
LOOP
dbms_output.put_line('actor_id:'||row1.actor_id ||' ---- '|| row1.first_name || row1.last_name);
OPEN out_cursor FOR
SELECT actor_id, film_id
FROM film_actor
WHERE actor_id = row1.actor_id;
END LOOP;
LOOP -- this loop is here just to print out_cursor for testing
FETCH out_cursor INTO rec;
EXIT WHEN out_cursor%NOTFOUND;
dbms_output.put_line('actor_id:'||rec.actor_id||', film_id:'||rec.film_id);
END LOOP;
END;
The output of the script is something like this
actor_id:98 ---- CHRIS BRIDGES
actor_id:99 ---- JIM MOSTEL
actor_id:100 ---- SPENCER DEPP
actor_id:100, film_id:17
actor_id:100, film_id:118
I understand that only rows from the last iteration of the for loop are there in the out_cursor.
Is there any way to make out_cursor return the results from all the iterations of the outer FOR LOOP, essentially aggregate the results of all the iterations. To produce result like this?
actor_id:98 ---- CHRIS BRIDGES
actor_id:99 ---- JIM MOSTEL
actor_id:100 ---- SPENCER DEPP
actor_id:98, film_id:77 // results from iteration #1
actor_id:98, film_id:43
actor_id:99, film_id:67 // results from iteration #2
actor_id:99, film_id:90
actor_id:100, film_id:17 // results from iteration #3
actor_id:100, film_id:118
I am aware that I can easily archive the same results using JOINS. But I am not allowed to modify the SQL queries (actual SQLs are very complex) - I can only run them and use the results produced from one SQL as parameters (in SELECT or WHERE clause) for the next SQL.
(UPDATE)
The General requirement is - there are a sequence of queries, resultset of each is used in the constraints of the next one. I cannot modify the queries themselves - so using a join or sub-query is out of question.
I was hoping to open a cursor for each query and then iterate through the values in the cursor - using the cursor attributes in the constraint of the next query, for which I will open a cursor inside the for loop. Exactly as shown in the sample. But the problem was the cursor inside the loop only contained the selections made in the last iteration - during each iteration a new cursor was getting created. So, I am getting only a subset of the results I wanted in the out_cursor.
I think i found a way to do this -
CREATE OR REPLACE PACKAGE PKG_BIDS_REPORTS_TEST as
TYPE Q2DATA IS RECORD (
actor_id film_actor.actor_id%TYPE,
film_id film_actor.film_id%TYPE
);
TYPE Q2DATA_TAB IS TABLE OF Q2DATA INDEX BY BINARY_INTEGER;
Q2DATA_REC Q2DATA_TAB;
PROCEDURE pqr_reports_test(l_out_data OUT sys_refcursor);
end PKG_BIDS_REPORTS_TEST;
/
create or replace NONEDITIONABLE PACKAGE BODY PKG_BIDS_REPORTS_TEST as
PROCEDURE pqr_reports_test(l_out_data OUT sys_refcursor) AS
CURSOR actor_cursor IS
SELECT actor_id, first_name, last_name
FROM actor
WHERE actor_id <= 100;
row1 actor_cursor%rowtype;
CURSOR film_actor_cursor(actorid film_actor.actor_id%TYPE) IS
SELECT actor_id, film_id
FROM film_actor
WHERE actor_id = actorid;
row2 film_actor_cursor%rowtype;
V_CNT NUMBER:=0;
BEGIN
V_CNT := q2data_rec.COUNT;
FOR row1 IN actor_cursor
LOOP
--dbms_output.put_line(row1.actor_id ||' ---- '|| row1.first_name ||' ---- '|| row1.last_name);
FOR row2 IN film_actor_cursor(row1.actor_id)
LOOP
V_CNT := V_CNT + 1;
q2data_rec(V_CNT).actor_id := row2.actor_id;
q2data_rec(V_CNT).film_id := row2.film_id;
--dbms_output.put_line(row2.actor_id||','||row2.film_id);
END LOOP;
END LOOP;
OPEN L_OUT_DATA FOR SELECT DISTINCT datarec.actor_id, datarec.film_id from TABLE(q2data_rec) datarec;
END;
End PKG_BIDS_REPORTS_TEST;
Essentially create a PLSQL table and fill it in the loop with a counter like a Java array. It works. Not the most elegant, definitely.
So, I would definitely appreciate any optimizations or alternates.

In order to get the output you are expecting for, you cannot close the first cursor before open the second one. It is the only way to have both iterations combined.
Although it would be inefficient, a way would be
BEGIN
FOR row1 IN actor_cursor
LOOP
dbms_output.put_line(row1.actor_id ||' ---- '|| row1.first_name || row1.last_name);
OPEN out_cursor FOR
SELECT actor_id, film_id
FROM film_actor
WHERE actor_id = row1.actor_id;
END LOOP;
FOR row1 IN actor_cursor
LOOP
OPEN out_cursor FOR
SELECT actor_id, film_id
FROM film_actor
WHERE actor_id = row1.actor_id;
LOOP
FETCH out_cursor INTO rec;
EXIT WHEN out_cursor%NOTFOUND;
dbms_output.put_line(row1.actor_id ||','|| rec.film_id);
END LOOP;
END LOOP;
END;

As I indicated before you cannot combine the result of separate cursors. However, you can (at least in this case) combine them into a single select and return that result. And since you are returning a reference cursor you cannot loop through it unless you close and re-open it. So in this case your procedure should consist of a single open cursor statement.
create or replace
procedure testproc (out_cursor out sys_refcursor)
is
begin
open out_cursor for
select a.actor_id, a.first_name, a.last_name, fa.film_id
from actor a
join film_actor fa
on fa.actor_id = a.actor_id
where a.actor_id <= 100;
end testproc ;
------- test ------
declare
actor_id actor.actor_id%type
fname actor.first_name%type
l_name actor,last_name%type
film_id film.film_id%type;
rec sys_refcursor ;
begin
testproc(rec);
loop
fetch rec
into actor_id, fname, lname, film_id;
exit when rec%notfound;
dbms_output.put_line('actor_id:' || actor_id ||
' name: ' || fname || ' ' || lname
' film_id:' || film_id
);
end loop;
end ;

Related

Loop through plsql code for a number of tables

A master table is to be updated daily with the input data from two sources in two tables. The plsql code for processing the two tables are practically identical except for the table names. We have to separately log possible errors about the data in the input tables therefore have to run the code once each for the two input tables.
The attempted solution is by putting the table names in a variable, and cycle through the code twice:
declare
input_table varchar2(20);
begin
for i in (select column_value as var from table(sys.ODCIvarchar2List('MIDGETS', 'GIANTS'))) loop
if i.var = 'MIDGETS' then
input_table := 'midget_table';
elsif i.var = 'GIANTS' then
input_table := 'giant_table';
end if;
for rec in (select col1, col2 from input_table) loop
<the processing code>
end loop;
end;
/
The problem is that plsql does not seem to be aware that input_table is a variable. It "thinks" that input_table is literally the name of the table, and returns the (ORA-00942: table or view does not exist) error.
Since this is dynamic code, the EXECUTE IMMEDIATE was then tried:
declare
input_table varchar2(20);
begin
for ... 'MIDGETS', 'GIANTS' ... loop
input_table := ...
...
end loop;
for rec in ( EXECUTE IMMEDIATE 'select col1, col2 from ' || input_table ) loop
<processing>
end loop;
end;
/
But EXECUTE IMMEDIATE is not allowed either in this context.
Is there a way at all? Or is making two copies of the .sql file, one for MIDGETS and one for GIANTS, the only way out?
You can use dynamic query as below
declare
type crs_type is ref cursor;
c crs_type;
v_query varchar2(2000);
input_table varchar2(20);
v_col1 midgets.col1%type; -- assuming identical data types for common named columns
v_col2 midgets.col2%type;
begin
for ... 'MIDGETS', 'GIANTS' ... loop
input_table := ...
...
end loop;
v_query := 'select col1, col2 from ' || input_table;
open c for v_query;
loop
fetch c into v_col1, v_col2;
exit when c%notfound;
<processing>
end loop;
close c;
end;

I am using cursor with a dynamic sql but when using further with bulk binding results are not coming

With PL/SQL oracle, I am using cursor with a dynamic sql but when using further with bulk binding results are not coming, but when using without bulk binding it works. Please suggest what am i missing here, sample below is a code snippet for your reference, although it is not the exact code but will give you an overview what i am trying to do.
cur_data SYS_REFCURSOR;
TYPE sample_data IS RECORD
(
col1 VSAMPLEDATA.COL1%TYPE,
col2 VSAMPLEDATA.COL2%TYPE
);
TYPE reclist IS TABLE OF sample_data ;
rec reclist;
Begin
p_query_string:='SELECT * from VSAMPLEDATA where COL2=:pVal';
OPEN cur_data FOR p_query_string USING 'DATA1';
LOOP
FETCH cur_data
BULK COLLECT INTO rec LIMIT 1000;
EXIT WHEN
rec.COUNT = 0;
FOR indx IN 1 .. rec.COUNT
LOOP
doing something;
END LOOP
END LOOP
CLOSE cur_data ;
You can avoid dynamic SQL and build a simpler, safer parametric cursor; for example, reflecting the structure of your code:
DECLARE
TYPE sample_data IS RECORD(col1 VSAMPLEDATA.COL1%TYPE, col2 VSAMPLEDATA.COL2%TYPE);
TYPE reclist IS TABLE OF sample_data;
--
rec reclist;
--
CURSOR cur_data(param IN VARCHAR2) IS
SELECT *
FROM VSAMPLEDATA
WHERE COL2 = param;
BEGIN
OPEN cur_data('DATA1');
LOOP
FETCH cur_data BULK COLLECT INTO rec LIMIT 1000;
EXIT WHEN rec.COUNT = 0;
FOR indx IN 1 .. rec.COUNT
LOOP
DBMS_OUTPUT.put_line(rec(indx).col1 || ' - ' || rec(indx).col2);
END LOOP;
END LOOP;
CLOSE cur_data;
END;
Test case:
create table VSAMPLEDATA(col1, col2) as
select 1, 'DATA1' from dual union all
select 2, 'DATA2' from dual
The result:
1 - DATA1
You are using sys_refcursor in wrong way.Also there is no need to use cursor in your case. sys_refcursor can be used to pass cursors from and to a stored procedure. SYS_REFCURSOR is a pre-declared weak ref cursor.
Also you are not executing the query so no result when using a bind variable. Execute Immediate is missing while using a binding the variable. See below example:
declare
TYPE sample_data IS RECORD
(
col1 EMPLOYEE.EMPLOYEE_ID%type,
col2 EMPLOYEE.FIRST_NAME%type
);
TYPE reclist IS TABLE OF sample_data;
rec reclist;
p_query_string varchar2(1000);
BEGIN
p_query_string := 'SELECT EMPLOYEE_ID,FIRST_NAME from EMPLOYEE where EMPLOYEE_ID=:pVal';
Execute immediate p_query_string BULK COLLECT INTO rec USING 1;
FOR indx IN 1 .. rec.COUNT
LOOP
--doing something;
dbms_output.put_line(rec(indx).col1);
END LOOP;
End;
Hi everyone this one has been resolved. It was always working, only problem was with the underlying query which had some conditional checks due to that results were not coming.

Iterate through all rows in table PL/SQL

From table1 I would like to gather values from certain columns. First of all I have tried to copy one table to another but I stuck when tried to:
for row in row_count
for column in column_count
insert into table2 at (x,y) value from (row,column)
column++
end
row++
end
My first function to count how many rows is:
create or replace FUNCTION func_count_rows(table_name IN varchar2,
debug boolean default false)
RETURN number IS
total number(2) := 0;
BEGIN
IF debug = true THEN
DBMS_OUTPUT.put('Function count rows: ');
DBMS_OUTPUT.PUT_LINE('select count(*) from ' || table_name || ';');
DBMS_OUTPUT.put('Returns: ');
DBMS_OUTPUT.PUT_LINE('');
END IF;
execute immediate 'select count(*) from ' || table_name into total;
RETURN total;
END;
Then my procedure to first print values but I stuck here:
create or replace procedure gather_values (rows_quantity in VARCHAR2,
column_count in VARCHAR2,
debug boolean default false
)
is begin
select
FOR i IN 1..rows_quantity LOOP
DBMS_OUTPUT.PUT_LINE('#### ROW 1 ####');
FOR i IN 1..94 LOOP
END LOOP;
END LOOP;
end;
I don't know how to get column quantity and value from exact (x,y) of table.
Could you kindly help me? Thank you.
I have forget to tell that I'm using oracle SQL enviroment.
First of all, this has nothing in common with PL/SQL:
for row in row_count
for column in column_count
insert into table2 at (x,y) value from (row,column)
column++
end
row++
end
See documentation here.
To copy all rows from one table to another:
insert into table2 (x,y)
select a, b
from table1;
It is a simple SQL query, it can be used as is or inside a PL/SQL procedure.
There is a lot of possibilities to iterate all rows of a table. The most simple:
for i in (select column1, column2, ... from table1) loop
dbms_output.put_line(i.column1);
end loop;
Another ways:
Using cursors
Using collections
Using dynamic SQL and dbms_sql package
To count rows in a table, you can use SQL query:
select count(*)
from table1
or almost the same PL/SQL code (you don't need to use execute immediate):
declare
total number;
begin
select count(*)
into total
from table1;
dbms_output.put_line('count of rows: ' || total);
end;
/
But in any case you don't need to know, how many rows and columns a table contains, to iterate them. You need only to know, how to filter, which of them you want to iterate.

How to declare a cursor after BEGIN?

I want to know if a cursor can be declared after BEGIN.
And how can I export the result of the plsql to an Excel sheet, because I have to run this procedure as a job.
CREATE OR REPLACE PROCEDURE masc(v_amsprogramid VARCHAR2) AS
v_mid VARCHAR2(50);
v_sid VARCHAR2(50);
CURSOR c1 IS
SELECT DISTINCT mid
FROM table_a WHERE aid = v_aid
ORDER BY mid;
BEGIN
OPEN c1;
LOOP
FETCH c1 INTO v_mid;
EXIT WHEN c1%NOTFOUND;
DECLARE
CURSOR c2 IS
SELECT DISTINCT sid INTO v_sid
FROM table_b WHERE mid = v_mid;
BEGIN
OPEN c2;
LOOP
FETCH c1 INTO v_mid;
EXIT WHEN c1%NOTFOUND;
dbms_output.PUT_LINE('MID : ' || v_mid);
dbms_output.PUT_LINE('Sid : ' || v_sid);
END LOOP;
CLOSE c2;
END LOOP;
CLOSE c1;
END masc;
I want to know if i can declare a cursor after begin
Not exactly. But you could use a cursor for loop instead of declaring an explicit cursor.
For example,
FOR i IN (SELECT distinct MID from table_a WHERE AID = V_AID ORDER BY MID)
LOOP
<do something>
END LOOP;
But anyway, this would be slower as row-by-row is slow-by-slow. I don't see a need of procedure at all. If you really need to do it in PL/SQL then consider BULK COLLECT.
And how can i export the result of the plsql to an excel sheet because i ahev to run this procedure as a job.
I don't see a need of PL/SQL in that case. You could simply use SPOOL in SQL*Plus.
For example,
sqlplus user/pass#service_name
<required formatting options>
SPOOL /location/myfile.csv
SELECT distinct MID from table_a WHERE AID = V_AID ORDER BY MID;
SPOOL OFF
Maybe you are looking for this:
create or replace PROCEDURE MASC (V_AMSPROGRAMID VARCHAR2) AS
V_MID VARCHAR2(50);
V_SID VARCHAR2(50);
CURSOR C1 IS
SELECT distinct MID from table_a WHERE AID = V_AID
ORDER BY MID;
CURSOR C2 IS
SELECT DISTINCT SID INTO V_SID FROM table_b WHERE MID = V_MID
ORDER BY MID;
BEGIN
...
or
create or replace PROCEDURE MASC (V_AMSPROGRAMID VARCHAR2) AS
V_MID VARCHAR2(50);
V_SID VARCHAR2(50);
CURSOR C1 IS
SELECT distinct MID from table_a WHERE AID = V_AID
ORDER BY MID;
CURSOR C2(v in NUMBER) IS
SELECT DISTINCT SID INTO V_SID FROM table_b WHERE MID = v
ORDER BY MID;
BEGIN
OPEN C1;
...
OPEN C2(V_MID);
...
U can use reference cursor for this purpose
create or replace PROCEDURE MASC (V_AMSPROGRAMID VARCHAR2) AS
V_MID VARCHAR2(50);
V_SID VARCHAR2(50);
C1 sys_refcursor ;
c2 sys_refcursor ;
BEGIN
OPEN C1 for SELECT distinct MID from table_a WHERE AID = V_AID
ORDER BY MID;
LOOP
FETCH C1 INTO V_MID;
EXIT WHEN C1%NOTFOUND;
open C2 for SELECT DISTINCT SID INTO V_SID FROM table_b WHERE MID = V_MID;
LOOP
FETCH C1 INTO V_MID;
EXIT WHEN C1%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('MID : ' || V_MID);
DBMS_OUTPUT.PUT_LINE('Sid : ' || V_SID);
END LOOP;
CLOSE C2;
CLOSE C1;
END LOOP;
You can declare multiple cursors in the same pl/sql block. There is no need to declare the second cursor after you've opened the first cursor!
You would write something like:
create or replace procedure masc (p_amsprogramid varchar2)
as
v_mid varchar2(50);
v_sid varchar2(50);
cursor c1
is
select distinct mid
from table_a
where aid = p_amsprogramid
order by mid;
cursor c2
is
select distinct sid
from table_b
where mid = v_mid;
begin
open c1;
loop
fetch c1 into v_mid;
exit when c1%notfound;
open c2;
loop
fetch c1 into v_mid;
exit when c1%notfound;
dbms_output.put_line('mid : ' || v_mid);
dbms_output.put_line('sid : ' || v_sid);
end loop;
close c2;
end loop;
close c1;
end masc;
/
However, if you were to replace your open-cursor-loop-fetches as a cursor-for-loop, you could simplify things a bit:
create or replace procedure masc (p_amsprogramid varchar2)
as
cursor c1
is
select distinct mid
from table_a
where aid = p_amsprogramid
order by mid;
cursor c2
is
select distinct sid
from table_b
where mid = v_mid;
begin
for rec1 in c1
loop
for rec2 in c2
loop
dbms_output.put_line('mid : ' || rec1.mid);
dbms_output.put_line('sid : ' || rec2.sid);
end loop;
end loop;
end masc;
/
Looking at that, you've got a nested cursor loop. This screams procedural thinking, rather than set-based thinking, which is pretty much a big no-no when you're working with datasets in the database (ie. it's slow. You're having to constantly switch between the SQL and PL/SQL engines, instead of simply asking the SQL engine to calculate everything before delivering it to the PL/SQL engine).
By doing the nested cursor loop, you're basically reinventing NESTED LOOP joins - something the SQL engine can do far better than you can (not to mention it might not be the most efficient join, and the SQL engine could choose a better way of doing the join!). Any time you see a nested cursor loop, you should IMMEDIATELY stop and look to see if you can combine the queries into a single select statement. (Actually, any time you see a loop you should pause and consider whether you really do need it; sometimes it's necessary, but if you're doing something like selecting a set of results and then going through each row and then doing an update, consider merging the select into the update so that you have a statement that updates all the rows at once. It'll be much faster!)
For example, your original procedure could be rewritten as:
create or replace procedure masc (p_amsprogramid varchar2)
as
cursor c1
is
select distinct a.mid,
b.sid
from table_a a
inner join table_b b on (a.mid = b.mid)
where a.aid = p_amsprogramid
order by mid;
begin
for rec1 in c1
loop
dbms_output.put_line('mid : ' || rec1.mid);
dbms_output.put_line('sid : ' || rec1.sid);
end loop;
end masc;
/
Much simpler to read, understand and maintain, I think you'll agree!
If you're wanting to write the results of the sql query out as a file, you'll need to use UTL_FILE, instead of DBMS_OUTPUT. Bear in mind that the directory the file is written to needs to be something that is mounted/mapped to the server the database sits on. If you write the results as character-delimited, you can then easily import that file into Excel.
You might find this to be of use.

For loop with Table name in Stored Procedures

I am working on Oracle stored procedures.
My requirement is below
IF variable1 := 'true"
THEN
tableName=abr
ELSE
tableName=mvr
END IF;
FOR i IN (select unique(row1) as sc from tableName t where t.row2 = 'name') LOOP
BEGIN
-- required Logic
END
END LOOP;
But here I am not able to pass the table name in tableName parameter. How to do it?
You'll need to use Execute Immediate - it's designed for operations that aren't known until run time.
For normal operations, Oracle must know the tables and columns at compile time. You can't do SELECT * FROM tableName because it has no idea what tableName is and therefore it can't be compiled correctly.
Instead, you can do EXECUTE IMMEDIATE 'SELECT * FROM ' || tableName;
You can select your results INTO a variable, loop the result set, or BULK COLLECT into a structure and then iterate that.
For a simple select into, you can do this:
EXECUTE IMMEDIATE 'SELECT COL1, COL2 FROM ' || tableName INTO V_COL1, V_COL2
V_COL1 & V_COL2 are just local variables, tableName is a string representing your table name, and COL2 and COL2 are columns in the table you're selecting from. You can use the likes of ALL_TAB_COLUMNS to get the structure of a table dynamically.
Here is an example from Oracle docs:
CREATE OR REPLACE PROCEDURE query_invoice(
month VARCHAR2,
year VARCHAR2) IS
TYPE cur_typ IS REF CURSOR;
c cur_typ;
query_str VARCHAR2(200);
inv_num NUMBER;
inv_cust VARCHAR2(20);
inv_amt NUMBER;
BEGIN
query_str := 'SELECT num, cust, amt FROM inv_' || month ||'_'|| year
|| ' WHERE invnum = :id';
OPEN c FOR query_str USING inv_num;
LOOP
FETCH c INTO inv_num, inv_cust, inv_amt;
EXIT WHEN c%NOTFOUND;
-- process row here
END LOOP;
CLOSE c;
END;
/
http://docs.oracle.com/cd/B12037_01/appdev.101/b10795/adfns_dy.htm
You are going to have to build a for loop for each table then use your logic to determine which loop you will execute.

Resources