How can I get the total number of records selected by a sysref cursor? - oracle

BACKGROUND
I am working on a web application that calls PLSQL stored procedures to retrieve and manipulate information. In one such case, the database has two stored procedures; one that selects the total number of records for a given set of parameters and one that returns the actual records with the same parameters, plus pagination parameters (min and max rownum).
EX (not the actual code):
PROCEDURE get_records_count(
p_first_name IN record_table.first_name%TYPE,
p_last_name IN record_table.last_name%TYPE,
p_resultCursor OUT sys_refcursor
)
IS BEGIN
OPEN p_resultCursor FOR
SELECT count(*) from record_table
WHERE first_name LIKE (p_first_name || '%')
OR last_name LIKE (p_last_name || '%');
END;
PROCEDURE get_records(
p_first_name IN record_table.first_name%TYPE,
p_last_name IN record_table.last_name%TYPE,
p_min IN NUMBER,
p_max IN NUMBER,
p_resultCursor OUT sys_refcursor
)
IS BEGIN
OPEN p_resultCursor FOR
SELECT * from record_table
WHERE first_name LIKE (p_first_name || '%')
OR last_name LIKE (p_last_name || '%')
AND rownum >= p_min AND rownum <= p_max;
END;
Whether or not one thinks that this is a good idea is beyond the scope of my position. The problem is that whenever either stored procedure is changed, the results don't match up. The short term fix is to look through both stored procedures, determine which selection criteria from which stored procedure is appropriate and then edit the other stored procedure so that they both match up.
As a long term fix, I would like to change the get_records_count procedure to call the get_records procedure and then return the total number of records that would be returned from the resulting sys_refcursor.
EX:
PROCEDURE get_records_count(
p_first_name IN record_table.first_name%TYPE,
p_last_name IN record_table.last_name%TYPE,
p_resultCursor OUT sys_refcursor
)
AS
v_recordsSelectedCursor sys_refcursor;
BEGIN
/*I understand that I will need to change some logic in get_records to
handle the case in which p_max is set to zero.
It should take this case to not apply an upper limit.*/
get_records(p_first_name,p_last_name,0,0,v_recordsSelectedCursor);
/*This is where I really have NO idea what I'm doing.
Hopefully, you can infer what I'm trying to do. */
OPEN p_resultCursor FOR
SELECT count(*) FROM v_recordsSelectedCursor;
END;
ACTUAL QUESTION
How can I select the number of records that would be returned for a sys_refcursor? The resulting number needs to be returned inside a sys_refcursor

A cursor is just a spec for fetching rows - it doesn't know how many rows are going to be returned until it has fetched them all.
Any method involving calling it twice risks getting inconsistent results unless you use dbms_flashback.enable_at_time at the start of the procedure (and disable it at the end). There is also the performance overhead, of course.
The only way to get a cursor to include its total rowcount is to include an analytic count(*) over () expression in the select list.

How can I select the number of records that would be returned for a
sys_refcursor? The resulting number needs to be returned inside a
sys_refcursor.
You can modify your procedure as below:
PROCEDURE get_records_count(
p_first_name IN record_table.first_name%TYPE,
p_last_name IN record_table.last_name%TYPE,
p_resultCursor OUT sys_refcursor
)
AS
v_recordsSelectedCursor sys_refcursor;
type y is table of record_table%rowtype;
z y;
num number:=0;
BEGIN
get_records(p_first_name,p_last_name,0,0,v_recordsSelectedCursor);
fetch v_recordsSelectedCursor bulk collect into z;
---taking the count of refcursor
num:=z.count;
OPEN p_resultCursor FOR
select num from dual;
dbms_output.put_line(num);
END;
See demo:
SQL> SELECT count(*)
FROM emp;
COUNT(*)
----------
14
SQL> CREATE OR REPLACE PROCEDURE sys_ref_rec_cnt (var OUT sys_refcursor)
2 AS
3 BEGIN
4 OPEN var FOR
5 SELECT *
6 FROM emp;
7 END;
8 /
Procedure created.
SQL> DECLARE
2 x sys_refcursor;
3 k sys_refcursor;
4 TYPE y IS TABLE OF emp%ROWTYPE;
5 z y;
6 num NUMBER := 0;
7 BEGIN
8 sys_ref_rec_cnt (x);
9
10 FETCH x
11 BULK COLLECT INTO z;
12
13 num :=z.count;
14
15 open k for
16 select num from dual;
17
18 DBMS_OUTPUT.put_line ('No. Of records-->'||num);
19 END;
20 /
No. Of records-->14
PL/SQL procedure successfully completed.

Related

why this simple programm creates an end-of-file on communicatoin channel error

CREATE OR REPLACE TYPE a IS OBJECT
(
b integer,
c varchar2(10)
);
/
declare
cursor ca return a is select 1,'e' from dual;
va a;
begin
null;
for cur in ca
loop
DBMS_OUTPUT.PUT_LINE('do nothing');
end loop;
end;
ORA-03113: end-of-file on communication channel
Process ID: 803778
Session ID: 64 Serial number: 4181
the loop as only one element and fast nothing is done in the loop.
But I get the error end-of-file communication channel
As #littlefoot said it works fine if I use a record defined in a package or no record at all. I don't know why it doesn't work with an object
code
Simply put, you can't do that.
Oracle does not support returning object types from a cursor. Cursors *always* return a rowtype. So to do something similar to what you want, you can define a table which has a column of the object type you're interested in, and then have the cursor return the rowtype of that table - i.e. something like:
CREATE OR REPLACE TYPE a IS OBJECT
(
b integer,
c varchar2(10)
);
CREATE TABLE tt
(t_a a);
INSERT INTO tt VALUES (a(1, 'e'));
declare
cursor ca return tt%ROWTYPE is select t_a from tt;
an_a a;
begin
FOR aRow IN ca LOOP
an_a := aRow.t_a;
dbms_output.put_line('an_a.b=' || an_a.b);
dbms_output.put_line('an_a.c=''' || an_a.c || '''');
end loop;
end;
db<>fiddle here
You never said what a is.
Documentation says that - if you use return clause, then it returns rowtype.
RETURN: Specifies the datatype of a cursor return value. You can use the %ROWTYPE attribute in the RETURN clause to provide a record type that represents a row in a database table or a row returned by a previously declared cursor. Also, you can use the %TYPE attribute to provide the datatype of a previously declared record.
A cursor body must have a SELECT statement and the same RETURN clause as its corresponding cursor spec. Also, the number, order, and datatypes of select items in the SELECT clause must match the RETURN clause.
ROWTYPE: A record type that represents a row in a database table or a row fetched from a previously declared cursor or cursor variable. Fields in the record and corresponding columns in the row have the same names and datatypes.
So, if your code were like this, it would work:
SQL> set serveroutput on
SQL>
SQL> declare
2 type a is record(val1 number, val2 varchar2(10));
3
4 cursor ca return a is select 1 ,'e' from dual;
5 va a;
6 begin
7 for cur in ca
8 loop
9 DBMS_OUTPUT.PUT_LINE(cur.val1 ||', '|| cur.val2 ||', do nothing');
10 end loop;
11 end;
12 /
1, e, do nothing
PL/SQL procedure successfully completed.
SQL>
Or, simpler, if you remove the return clause from your own code, it would also work:
SQL> declare
2 cursor ca --return a
3 is select 1,'e' from dual;
4 va a;
5 begin
6 null;
7 for cur in ca
8 loop
9 DBMS_OUTPUT.PUT_LINE('do nothing');
10 end loop;
11 end;
12 /
do nothing
PL/SQL procedure successfully completed.
SQL>
If you're asking what's the reason of end-of-file on communication channel, I wouldn't know.

Stored Procedure output result set

Apologies for the newbie question, I am writing an Oracle stored procedure that opens a cursor for a specific SQL, calculates some variables for each row returned by the cursor but the stored procedure should return as a result set these variables that have been calculated for each row returned by the cursor. I am a bit confused on how to do this - can anyone help?!
I did read some of it so far I have something like this (just a trimmed down example and not exact code) but just need to return v_calc and v_calc_res in a result set:-
CREATE OR REPLACE procedure sp_test
(
in_input in number,
out_return out sys_refcursor
)
as
v_calc number;
v_calc_res number;
CURSOR C_test IS
select blah from test where blah = in_input;
begin
open c_test
loop
fetch c_test into v_calc;
v_calc_res := v_calc*5;
end loop;
end;
If you want a procedure to return a reference cursor for the calling routine to consume the procedure itself cannot then consume it. Cursors, including reference cursors are 1 way, 1 time consumables. As for as the desired calculations, they can be added to select defined for the cursor. So:
-- setup
create table test (blah integer, blah_stuff varchar2(50) );
-- build sp
create or replace procedure sp_blah_text(
in_input in number
, out_cur out sys_refcursor
)
is
begin
open out_cur for
select blah, blah_stuff, blah*5 as blah_x_5
from test
where blah = in_input;
end sp_blah_text;
-- test data
insert into test(blah, blah_stuff)
select 1,'a' from dual union all
select 2,'b' from dual union all
select 2,'x' from dual union all
select 2,'z' from dual union all
select 3,'c' from dual;
-- test
declare
ref_cur sys_refcursor;
l_blah test.blah%type;
l_stuff test.blah_stuff%type;
l_blah_5 test.blah%type;
begin
dbms_output.enable(null);
sp_blah_text(2,ref_cur);
loop
fetch ref_cur
into l_blah
, l_stuff
, l_blah_5;
exit when ref_cur%notfound;
dbms_output.put_line('blah=' || l_blah || ',stuff=' || l_stuff || ',blah*5=' || l_blah_5);
end loop;
end;
This works a treat thank you very much. I now have a performance issue that maybe you could help with. When I open the cursor, I then run several other SELECT statements to retrieve values using the variables from the cursor (see below). I assume this is because the switch between PL/SQL and SQL engine. Would using table collections help? But as I see since I need different columns from different tables I would need to have several different collections, how could I output everything in one record?
CREATE OR REPLACE procedure sp_test
(
in_input in number
)
as
v_calc number;
v_calc_res number;
v_blah_blah number;
v_blah_blah_blah number;
v_blah_blah_blah number;
CURSOR C_test IS
select blah from test where blah = in_input;
begin
open c_test
loop
fetch c_test into v_calc;
select blah_blah into v_blah_blah from t_blah_blah;
select blah_blah_blah into v_blah_blah_blah from t_blah_blah_blah;
select blah_blah_blah_blah into v_blah_blah_blah_blah from t_blah_blah_blah_blah;
v_calc_res := v_calc*5*v_blah_blah*v_blah_blah_blah*v_blah_blah_blah_blah
end loop;
end;

Using cursor with where in condition

I am passing arguments `EBN,BGE' into a procedure , then I am passing this argument to a cursor.
create or replace procedure TEXT_MD (AS_IDS VARCHAR2)
is
CURSOR C_A (AS_ID VARCHAR2) IS
SELECT
name
FROM S_US
WHERE US_ID IN (AS_ID);
BEGIN
FOR A IN C_A (AS_IDS) LOOP
DBMS_OUTPUT.PUT_LINE('I got here: '||AS_IDS);
end loop;
END;
But while debuging the count of the cursor is still null
So my question , why the cursor not returning values with in condition
You are passing a string parameter, so it will be used as a string, not as a list of strings; so, your cursor will be something like
SELECT name
FROM S_US
WHERE US_ID IN ('EBN,BGE')
This will, of course, not do what you need.
You may need to change your procedure and the way to pass parameters; if you want to keep a string parameter , one way could be the following:
setup:
SQL> CREATE TABLE S_US
2 (
3 US_ID,
4 NAME
5 ) AS
6 SELECT 'EBN', 'EBN name' FROM DUAL
7 UNION ALL
8 SELECT 'BGE', 'BGE name' FROM DUAL;
Table created.
procedure:
SQL> CREATE OR REPLACE PROCEDURE TEXT_MD_2(AS_IDS VARCHAR2) IS
2 vSQL varchar2(1000);
3 c sys_refcursor;
4 vName varchar2(16);
5 BEGIN
6 vSQL := 'SELECT name
7 FROM S_US
8 WHERE US_ID IN (' || AS_IDS || ')';
9 open c for vSQL;
10 loop
11 fetch c into vName;
12 if c%NOTFOUND then
13 exit;
14 end if;
15 DBMS_OUTPUT.PUT_LINE(vName);
16 END LOOP;
17 END;
18 /
Procedure created.
You need to call it with a string already formatted to be a parameter list for IN:
SQL> EXEC TEXT_MD_2('''EBN'',''BGE''');
EBN name
BGE name
PL/SQL procedure successfully completed.
This is only an example of a possible way, and not the way I would do this.
Among the reasons to avoud this kind of approach, consider what Justin Cave says:
"that would be a security risk due to SQL injection and would have a potentially significant performance penalty due to constant hard parsing".
I believe you should better check how to pass a list of values to your procedure, rather then using a string to represent a list of strings.
Here is a possible way to do the same thing with a collection:
SQL> CREATE OR REPLACE TYPE tabVarchar2 AS TABLE OF VARCHAR2(16)
2 /
Type created.
SQL>
SQL> CREATE OR REPLACE PROCEDURE TEXT_MD_3(AS_IDS tabVarchar2) IS
2 vSQL VARCHAR2(1000);
3 c SYS_REFCURSOR;
4 vName VARCHAR2(16);
5 BEGIN
6 FOR i IN (SELECT name
7 FROM S_US INNER JOIN TABLE(AS_IDS) tab ON (tab.COLUMN_VALUE = US_ID))
8 LOOP
9 DBMS_OUTPUT.PUT_LINE(i.name);
10 END LOOP;
11 END;
12 /
Procedure created.
SQL>
SQL> DECLARE
2 vList tabVarchar2 := NEW tabVarchar2();
3 BEGIN
4 vList.EXTEND(2);
5 vList(1) := 'BGE';
6 vList(2) := 'EBN';
7 TEXT_MD_3(vList);
8 END;
9 /
BGE name
EBN name
PL/SQL procedure successfully completed.
SQL>
Again, you can define collections in different ways, within a stored procedure or not, indexed or not, and so on; this is only one of the possible ways, not necessarily the best, depending on your environment, needs.

Pagination in Oracle/PLSQL error

Hey I am trying to add paging to my dynamic sql block in PLSQL but for some reason when I run the test script it errors out:
ORA-00932: inconsistent datatypes: expected - got -
Here is my procedure:
create or replace
procedure spm_search_patientmedrecs (
p_columnsort_in in varchar2,
p_column1_in in varchar2,
p_column2_in in varchar2,
p_column3_in in varchar2,
p_column4_in in varchar2,
p_ascdesc_in in varchar2,
p_return_cur_out out sys_refcursor
is
lv_sql varchar2(32767);
lv_startnum number:= 1;
lv_incrementby number:= 20;
begin
lv_sql := '';
lv_sql := 'select * from (
select /*+ first_rows(20) */
'||p_column1_in||',
'||p_column2_in||',
'||p_column3_in||',
'||p_column4_in||',
row_number() over
(order by '||p_columnsort_in||' '||p_ascdesc_in||') rn
from membermedicalreconcilationhdr h,
membermedicalreconcilationdet d
where h.membermedreconciliationhdrskey =
d.membermedreconciliationhdrskey)
where rn between :lv_startnum and :lv_incrementby
order by rn';
open p_return_cur_out for lv_sql;
end spm_search_patientmedrecs;
Here is my test script:
set serveroutput on
declare
type tempcursor is ref cursor;
v_cur_result tempcursor;
p_columnsort_in varchar2(50);
p_column1_in varchar2(50);
p_column2_in varchar2(50);
p_column3_in varchar2(50);
p_column4_in varchar2(50);
p_ascdesc_in varchar2(50);
begin
spm_search_patientmedrecs
('h.PRIMARYMEMBERPLANID',
'h.PRIMARYMEMBERPLANID',
'h.ASSIGNEDUSERID',
'd.MEMBERMEDRECONCILIATIONDETSKEY',
'd.GENERICNM',
'ASC',
v_cur_result
);
loop
fetch v_cur_result into
p_column1_in,p_column2_in,p_column3_in,p_column4_in;
dbms_output.put_line('column 1: '||p_column1_in||' column 2: '||p_column2_in||
' column 3: '||p_column3_in||' column 4: '||p_column4_in);
exit when v_cur_result%notfound;
end loop;
end;
The error I posted above doesnt make sense to me, but I've been looking for the cause for awhile. If anyone can point me in the right direction it would be much appreciated, thanks in advance.
A couple of issues jump out at me.
The query that you are using to return the cursor returns 5 columns (the 4 you pass in plus the computed rn) while your fetch fetches the data into only 4 variables. You would either need to modify your query to return only 4 columns or modify your test script to fetch the data into 5 variables.
In your procedure, you have bind variables in your SQL statement but you don't pass in any bind variables when you open the cursor. My guess is that you want something like this
Passing the bind variables with the USING clause
open p_return_cur_out
for lv_sql
using lv_startnum, lv_incrementby;
There may well be more errors-- if there are, it would be helpful to post the full stack trace including the line number of the error.
A couple of other things to be aware of.
Unless p_columnsort_in happens to specify a column that is unique, your paging code may well miss rows and/or show rows in multiple pages because the sort order isn't fully specified. If rows 20 and 21 have the same p_columnsort_in value, it would be perfectly legal to sort them one way on the first query and another way on the second query so row 20 might show up on the first and second page and row 21 might not show up anywhere.
If efficiency is a concern, using rownum will probably end up being more efficient than using the analytic function like this because the optimizer can generally do a better job of optimizing a rownum predicate.
create or replace
procedure spm_search_patientmedrecs (
p_columnsort_in in varchar2,
p_column1_in in varchar2,
p_column2_in in varchar2,
p_column3_in in varchar2,
p_column4_in in varchar2,
p_ascdesc_in in varchar2,
p_return_cur_out out sys_refcursor
is
lv_sql varchar2(32767);
lv_startnum number:= 1;
lv_incrementby number:= 20;
begin
lv_sql := 'select * from (
select /*+ first_rows(20) */
'||p_column1_in||',
'||p_column2_in||',
'||p_column3_in||',
'||p_column4_in||',
row_number() over
(order by '||p_columnsort_in||' '||p_ascdesc_in||') rn
from membermedicalreconcilationhdr h,
membermedicalreconcilationdet d
where h.membermedreconciliationhdrskey =
d.membermedreconciliationhdrskey)
where rn between :1 and :2
order by rn';
open p_return_cur_out for lv_sql using lv_startnum, lv_incrementby;
end spm_search_patientmedrecs;

Selecting Values from Oracle Table Variable / Array?

Following on from my last question (Table Variables in Oracle PL/SQL?)...
Once you have values in an array/table, how do you get them back out again? Preferably using a select statement or something of the like?
Here's what I've got so far:
declare
type array is table of number index by binary_integer;
pidms array;
begin
for i in (
select distinct sgbstdn_pidm
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH'
)
loop
pidms(pidms.count+1) := i.sgbstdn_pidm;
end loop;
select *
from pidms; --ORACLE DOESN'T LIKE THIS BIT!!!
end;
I know I can output them using dbms_output.putline(), but I'm hoping to get a result set like I would from selecting from any other table.
Thanks in advance,
Matt
You might need a GLOBAL TEMPORARY TABLE.
In Oracle these are created once and then when invoked the data is private to your session.
Oracle Documentation Link
Try something like this...
CREATE GLOBAL TEMPORARY TABLE temp_number
( number_column NUMBER( 10, 0 )
)
ON COMMIT DELETE ROWS;
BEGIN
INSERT INTO temp_number
( number_column )
( select distinct sgbstdn_pidm
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH'
);
FOR pidms_rec IN ( SELECT number_column FROM temp_number )
LOOP
-- Do something here
NULL;
END LOOP;
END;
/
In Oracle, the PL/SQL and SQL engines maintain some separation. When you execute a SQL statement within PL/SQL, it is handed off to the SQL engine, which has no knowledge of PL/SQL-specific structures like INDEX BY tables.
So, instead of declaring the type in the PL/SQL block, you need to create an equivalent collection type within the database schema:
CREATE OR REPLACE TYPE array is table of number;
/
Then you can use it as in these two examples within PL/SQL:
SQL> l
1 declare
2 p array := array();
3 begin
4 for i in (select level from dual connect by level < 10) loop
5 p.extend;
6 p(p.count) := i.level;
7 end loop;
8 for x in (select column_value from table(cast(p as array))) loop
9 dbms_output.put_line(x.column_value);
10 end loop;
11* end;
SQL> /
1
2
3
4
5
6
7
8
9
PL/SQL procedure successfully completed.
SQL> l
1 declare
2 p array := array();
3 begin
4 select level bulk collect into p from dual connect by level < 10;
5 for x in (select column_value from table(cast(p as array))) loop
6 dbms_output.put_line(x.column_value);
7 end loop;
8* end;
SQL> /
1
2
3
4
5
6
7
8
9
PL/SQL procedure successfully completed.
Additional example based on comments
Based on your comment on my answer and on the question itself, I think this is how I would implement it. Use a package so the records can be fetched from the actual table once and stored in a private package global; and have a function that returns an open ref cursor.
CREATE OR REPLACE PACKAGE p_cache AS
FUNCTION get_p_cursor RETURN sys_refcursor;
END p_cache;
/
CREATE OR REPLACE PACKAGE BODY p_cache AS
cache_array array;
FUNCTION get_p_cursor RETURN sys_refcursor IS
pCursor sys_refcursor;
BEGIN
OPEN pCursor FOR SELECT * from TABLE(CAST(cache_array AS array));
RETURN pCursor;
END get_p_cursor;
-- Package initialization runs once in each session that references the package
BEGIN
SELECT level BULK COLLECT INTO cache_array FROM dual CONNECT BY LEVEL < 10;
END p_cache;
/
The sql array type is not neccessary. Not if the element type is a primitive one. (Varchar, number, date,...)
Very basic sample:
declare
type TPidmList is table of sgbstdn.sgbstdn_pidm%type;
pidms TPidmList;
begin
select distinct sgbstdn_pidm
bulk collect into pidms
from sgbstdn
where sgbstdn_majr_code_1 = 'HS04'
and sgbstdn_program_1 = 'HSCOMPH';
-- do something with pidms
open :someCursor for
select value(t) pidm
from table(pidms) t;
end;
When you want to reuse it, then it might be interesting to know how that would look like.
If you issue several commands than those could be grouped in a package.
The private package variable trick from above has its downsides.
When you add variables to a package, you give it state and now it doesn't act as a stateless bunch of functions but as some weird sort of singleton object instance instead.
e.g. When you recompile the body, it will raise exceptions in sessions that already used it before. (because the variable values got invalided)
However, you could declare the type in a package (or globally in sql), and use it as a paramter in methods that should use it.
create package Abc as
type TPidmList is table of sgbstdn.sgbstdn_pidm%type;
function CreateList(majorCode in Varchar,
program in Varchar) return TPidmList;
function Test1(list in TPidmList) return PLS_Integer;
-- "in" to make it immutable so that PL/SQL can pass a pointer instead of a copy
procedure Test2(list in TPidmList);
end;
create package body Abc as
function CreateList(majorCode in Varchar,
program in Varchar) return TPidmList is
result TPidmList;
begin
select distinct sgbstdn_pidm
bulk collect into result
from sgbstdn
where sgbstdn_majr_code_1 = majorCode
and sgbstdn_program_1 = program;
return result;
end;
function Test1(list in TPidmList) return PLS_Integer is
result PLS_Integer := 0;
begin
if list is null or list.Count = 0 then
return result;
end if;
for i in list.First .. list.Last loop
if ... then
result := result + list(i);
end if;
end loop;
end;
procedure Test2(list in TPidmList) as
begin
...
end;
return result;
end;
How to call it:
declare
pidms constant Abc.TPidmList := Abc.CreateList('HS04', 'HSCOMPH');
xyz PLS_Integer;
begin
Abc.Test2(pidms);
xyz := Abc.Test1(pidms);
...
open :someCursor for
select value(t) as Pidm,
xyz as SomeValue
from table(pidms) t;
end;

Resources