Oracle Cursor - Return result and rowcount - oracle

I'm trying to create a procedure in Oracle that would return the result of the query and at the same time count the number of returned rows. Is it possible to do that in one procedure?
This one returns the result of the query (this is an example query, our production query is more complicated):
CREATE OR REPLACE PROCEDURE GETCUR(PARAM1 VARCHAR2)
AS
cur SYS_REFCURSOR;
cSql NUMBER;
cnt INTEGER;
BEGIN
OPEN CUR FOR
SELECT T1.F3, T2.F3 FROM T1 JOIN T2 ON T1.F1 = T2.F2 WHERE T2.F9 = PARAM1;
DBMS_SQL.RETURN_RESULT(CUR);
END;
And this one can count the number of returned rows. Here I print it using PUT_LINE but want to be able to assign it to some variable and insert that into other table - something like a logging mechanism.
CREATE OR REPLACE PROCEDURE GETCUR(PARAM1 VARCHAR2)
AS
cur SYS_REFCURSOR;
cSql NUMBER;
cnt INTEGER;
BEGIN
OPEN CUR FOR
SELECT T1.F3, T2.F3 FROM T1 JOIN T2 ON T1.F1 = T2.F2 WHERE T2.F9 = PARAM1;
--DBMS_SQL.RETURN_RESULT(CUR);
cSql := DBMS_SQL.TO_CURSOR_NUMBER(CUR);
cnt := 0;
LOOP
EXIT WHEN DBMS_SQL.FETCH_ROWS(cSql) = 0;
cnt := cnt + 1;
END LOOP;
DBMS_SQL.CLOSE_CURSOR(cSql);
DBMS_OUTPUT.PUT_LINE(cnt||' rows returned');
END;
But I can't marry these two solutions into one procedure. Is it possible?

If you want to pass back two outputs - a cursor and a count, there is no way to do that without running two queries. That's inefficient and possibly inaccurate for reasons Justin Cave pointed out.
One thing you might do is something like this:
CREATE OR REPLACE FUNCTION GETCUR(PARAM1 VARCHAR2) RETURN SYS_REFCURSOR
AS
cur SYS_REFCURSOR;
BEGIN
OPEN CUR FOR
SELECT T1.F3,
T2.F3,
COUNT(*) OVER ( PARTITION BY NULL ) ROW_COUNT
FROM T1 JOIN T2 ON T1.F1 = T2.F2
WHERE T2.F9 = PARAM1;
RETURN cur;
END;
Your consuming application will know the total row count after fetching the 1st record. And, thanks to statement-level read consistency in Oracle, it does not require you to set the isolation level to guarantee accurate results. It's also more efficient than running two separate queries.

You could do something like
CREATE OR REPLACE PROCEDURE GETCUR(p_PARAM1 OUT VARCHAR2,
p_rc OUT sys_refcursor,
p_cnt OUT integer )
AS
BEGIN
OPEN p_rc
FOR SELECT T1.F3, T2.F3
FROM T1 JOIN T2 ON T1.F1 = T2.F2
WHERE T2.F9 = p_PARAM1;
select count(*)
into p_cnt
from (SELECT T1.F3, T2.F3
FROM T1 JOIN T2 ON T1.F1 = T2.F2
WHERE T2.F9 = p_PARAM1);
end;
However,
This is going to be twice as expensive because you are running the query twice.
If the underlying tables are changing, it is possible that the count you get in the second step won't match the count you got from fetching from the cursor you opened in the first step. You could potentially address that by using a serializable transaction isolation level but that is going to have a number of impacts on your application so it's not something that should be done lightly.

Related

How to append rows to a existing SYS_REFCURSOR?

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 ;

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.

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.

SELECT DISTINCT CLOB_COLUMN FROM TABLE;

I would like to find the distinct CLOB values that can assume the column called CLOB_COLUMN (of type CLOB) contained in the table called COPIA.
I have selected a PROCEDURAL WAY to solve this problem, but I would prefer to give a simple SELECT as the following: SELECT DISTINCT CLOB_COLUMN FROM TABLE avoiding the error "ORA-00932: inconsistent datatypes: expected - got CLOB"
How can I achieve this?
Thank you in advance for your kind cooperation. This is the procedural way I've thought:
-- Find the distinct CLOB values that can assume the column called CLOB_COLUMN (of type CLOB)
-- contained in the table called COPIA
-- Before the execution of the following PL/SQL script, the CLOB values (including duplicates)
-- are contained in the source table, called S1
-- At the end of the excecution of the PL/SQL script, the distinct values of the column called CLOB_COLUMN
-- can be find in the target table called S2
BEGIN
EXECUTE IMMEDIATE 'TRUNCATE TABLE S1 DROP STORAGE';
EXECUTE IMMEDIATE 'DROP TABLE S1 CASCADE CONSTRAINTS PURGE';
EXCEPTION
WHEN OTHERS
THEN
BEGIN
NULL;
END;
END;
BEGIN
EXECUTE IMMEDIATE 'TRUNCATE TABLE S2 DROP STORAGE';
EXECUTE IMMEDIATE 'DROP TABLE S2 CASCADE CONSTRAINTS PURGE';
EXCEPTION
WHEN OTHERS
THEN
BEGIN
NULL;
END;
END;
CREATE GLOBAL TEMPORARY TABLE S1
ON COMMIT PRESERVE ROWS
AS
SELECT CLOB_COLUMN FROM COPIA;
CREATE GLOBAL TEMPORARY TABLE S2
ON COMMIT PRESERVE ROWS
AS
SELECT *
FROM S1
WHERE 3 = 9;
BEGIN
DECLARE
CONTEGGIO NUMBER;
CURSOR C1
IS
SELECT CLOB_COLUMN FROM S1;
C1_REC C1%ROWTYPE;
BEGIN
FOR C1_REC IN C1
LOOP
-- How many records, in S2 table, are equal to c1_rec.clob_column?
SELECT COUNT (*)
INTO CONTEGGIO
FROM S2 BETA
WHERE DBMS_LOB.
COMPARE (BETA.CLOB_COLUMN,
C1_REC.CLOB_COLUMN) = 0;
-- If it does not exist, in S2, a record equal to c1_rec.clob_column,
-- insert c1_rec.clob_column in the table called S2
IF CONTEGGIO = 0
THEN
BEGIN
INSERT INTO S2
VALUES (C1_REC.CLOB_COLUMN);
COMMIT;
END;
END IF;
END LOOP;
END;
END;
If it is acceptable to truncate your field to 32767 characters this works:
select distinct dbms_lob.substr(FIELD_CLOB,32767) from Table1
You could compare the hashes of the CLOB to determine if they are different:
SELECT your_clob
FROM your_table
WHERE ROWID IN (SELECT MIN(ROWID)
FROM your_table
GROUP BY dbms_crypto.HASH(your_clob, dbms_crypto.HASH_SH1))
Edit:
The HASH function doesn't guarantee that there will be no collision. By design however, it is really unlikely that you will get any collision. Still, if the collision risk (<2^80?) is not acceptable, you could improve the query by comparing (with dbms_lob.compare) the subset of rows that have the same hashes.
add TO_CHAR after distinct keyword to convert CLOB to CHAR
SELECT DISTINCT TO_CHAR(CLOB_FIELD) from table1; //This will return distinct values in CLOB_FIELD
Use this approach. In table profile column content is NCLOB. I added the where clause to reduce the time it takes to run which is high,
with
r as (select rownum i, content from profile where package = 'intl'),
s as (select distinct (select min(i) from r where dbms_lob.compare(r.content, t.content) = 0) min_i from profile t where t.package = 'intl')
select (select content from r where r.i = s.min_i) content from s
;
It is not about to win any prizes for efficiency but should work.
select distinct DBMS_LOB.substr(column_name, 3000) from table_name;
If truncating the clob to the size of a varchar2 won't work, and you're worried about hash collisions, you can:
Add a row number to every row;
Use DBMS_lob.compare in a not exists subquery. Exclude duplicates (this means: compare = 0) with a higher rownum.
For example:
create table t (
c1 clob
);
insert into t values ( 'xxx' );
insert into t values ( 'xxx' );
insert into t values ( 'yyy' );
commit;
with rws as (
select row_number () over ( order by rowid ) rn,
t.*
from t
)
select c1 from rws r1
where not exists (
select * from rws r2
where dbms_lob.compare ( r1.c1, r2.c1 ) = 0
and r1.rn > r2.rn
);
C1
xxx
yyy
To bypass the oracle error, you have to do something like this :
SELECT CLOB_COLUMN FROM TABLE COPIA C1
WHERE C1.ID IN (SELECT DISTINCT C2.ID FROM COPIA C2 WHERE ....)
I know this is an old question but I believe I've figure out a better way to do what you are asking.
It is kind of like a cheat really...The idea behind it is that You can't do a DISTINCT of a Clob column but you can do a DISTINCT on a Listagg function of a Clob_Column...you just need to play with the partition clause of the Listagg function to make sure it will only return one value.
With that in mind...here is my solution.
SELECT DISTINCT listagg(clob_column,'| ') within GROUP (ORDER BY unique_id) over (PARTITION BY unique_id) clob_column
FROM copia;

Resources