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;
Related
i have a scenario where i need to create a dynamic query with some if/else statements, i have prepared the query but unable to use this inside loop below is the snippet of what i am trying to acheive.
Query :
select user_name into username from table1 where user ='user1';
for(query)
Loop
Dbms_output.put_line('user name ' || user_name);
END Loop;
is this possible to use the vaiable in the for loop ?
Not really. Notice in the 19c PL/SQL Reference it says:
cursor
Name of an explicit cursor (not a cursor variable) that is not open when the cursor FOR LOOP is entered.
You have to code this the long way, by explicitly fetching the cursor into a record until you hit cursorname%notfound, e.g.
create or replace procedure cheese_report
( cheese_cur in sys_refcursor )
as
type cheese_detail is record
( name varchar2(15)
, region varchar2(30) );
cheese cheese_detail;
begin
loop
fetch cheese_cur into cheese;
dbms_output.put_line(cheese.name || ', ' || cheese.region);
exit when cheese_cur%notfound;
end loop;
close cheese_cur;
end;
Test:
declare
cheese sys_refcursor;
begin
open cheese for
select 'Cheddar', 'UK' from dual union all
select 'Gruyere', 'France' from dual union all
select 'Ossau Iraty', 'Spain' from dual union all
select 'Yarg', 'UK' from dual;
cheese_report(cheese);
end;
/
Cheddar, UK
Gruyere, France
Ossau Iraty, Spain
Yarg, UK
In Oracle 21c you can simplify this somewhat, though you still have to know the structure of the result set:
create or replace procedure cheese_report
( cheese_cur in sys_refcursor )
as
type cheese_detail is record
( cheese varchar2(15)
, region varchar2(30) );
begin
for r cheese_detail in values of cheese_cur
loop
dbms_output.put_line(r.cheese || ', ' || r.region);
end loop;
end;
You could parse an unknown ref cursor using dbms_sql to find the column names and types, but it's not straightforward as you have to do every processing step yourself. For an example of something similar, see www.williamrobertson.net/documents/refcursor-to-csv.shtml.
I am working on an application and made the decision that all the queries would be procedures. I hope to have gains in performance and ease of maintenance by doing it this way. Our DBA's have also expressed interest in having it done this way.
I have an HR table where operations are performed on it each night, and any changes are recorded in a secondary table. We are not doing auditing, these change records are kept until the next run and show users the changes that have happened.
To keep my question shorter I have reduced the number of columns in HR.
The HR table ID, GROUP_NAME, and GROUP_LEVEL. The Drill table has ID and TYPEVALUE.
CREATE OR REPLACE PROCEDURE DOCSADM.DRILL_RECORD_POSITION (
RECORD_TYPE IN VARCHAR2,
OUT_ID OUT VARCHAR2,
OUT_GROUP_NAME OUT VARCHAR2,
OUT_GROUP_LEVEL OUT VARCHAR2
) AS
BEGIN
SELECT HR.ID, HR.GROUP_NAME, HR.GROUP_LEVEL
INTO OUT_ID, OUT_GROUP_NAME, OUT_GROUP_LEVEL
FROM HR_POSITION HR JOIN DRILL_POSITION DP ON (HR.ID = DP.ID) WHERE DP.TYPEVALUE = RECORD_TYPE;
END DRILL_RECORD_POSITION;
The procedure compiles without issue. Before doing all the work in the application to link to the procedure and extract the values which in this case will eventually be displayed in a view or webpage, I wanted to have a quick little script that would call the procedure and then display the results so I can verify in Oracle.
Loops
BEGIN
for t in (DRILL_RECORD_POSITION('D', V1,V5,V6))
loop
--dbms_output.put_line(t.V1 || t.V5 || t.V6);
dbms_output.put_line(t.OUT_ID);
end loop;
END;
/
CURSORS
DECLARE
V1 HR_POSITION.ID%TYPE;
V5 HR_POSITION.GROUP_NAME%TYPE;
V6 HR_POSITION.GROUP_LEVEL%TYPE;
CURSOR T_CUR IS DRILL_RECORD_POSITION('D', V1,V5,V6);
BEGIN
OPEN T_CUR;
DBMS_OUTPUT.PUTLINE('START');
LOOP
FETCH T_CUR INTO V1,V5,V6;
EXIT WHEN T_CUR%NOTFOUND;
DBMS_OUTPUT.PUTLINE(V1||V5||V6);
END LOOP;
CLOSE T_CUR;
END;
FOR LOOPS
DECLARE
V1 HR_POSITION.POSITION_ID%TYPE;
V5 HR_POSITION.GROUP_NAME%TYPE;
V6 HR_POSITION.GROUP_LEVEL%TYPE;
BEGIN
DBMS_OUTPUT.PUTLINE('START');
FOR INDEX IN (DRILL_RECORD_POSITION('D', V1,V5,V6))
LOOP
--DBMS_OUTPUT.PUTLINE(INDEX.ID);
DBMS_OUTPUT.PUTLINE(INDEX.V1||INDEX.V5||INDEX.V6);
END LOOP;
END;
Note: I edited the column names out and shorted some when transferring here so I might have made a few mistakes.
All the articles I have seen online show me how to display from within the original procedure or by using views, cursors, records. Unless I am wrong, Eclipse wont have any problems using the information in the current form which is why I am passing it that way. So I am not interested in changing the procedure and would like to work with it as is, since thats how the application will be doing it.
As this is the first of the stored procedures I am doing for the application, instead of using adhoc queries from the application, I dont have any existing examples to work from, which is why I believe the results will work fine, because it should be the same format the adhoc ones use.
Update:
In one of the comments, I was pointed to what should have been a solution. This was confirmed by another solution that was under it.
I keep getting the error
ORA-01422: exact fetch returns more than requested number of rows
So Im returning multiple rows, but that is my expectation and what is happening. I just cant seem to figure out how to display the results.
To test the procedure you showed, you would do something like:
declare
l_id hr_position.id%type;
l_group_name hr_position.group_name%type;
l_group_level hr_position.group_level%type;
begin
drill_record_position('D', l_id, l_group_name, l_group_level);
dbms_output.put_line(l_id ||':'|| l_group_name ||':'|| l_group_level);
end;
/
But that - or more specifically, your procedure - only works if there is exactly one row in the query's result set for the passed-in value type. It seems you're expecting multiple rows back (which would get too-many-rows), but there could also be non (which would get no-data-found).
So really it seems like your question should be about how to write your procedure so it works with one of the retrieval/test methods you tried.
If your procedure needs to return multiple rows then it can use a ref cursor, e.g.:
create or replace procedure drill_record_position (
p_record_type in varchar2,
p_ref_cursor out sys_refcursor
)
as
begin
open p_ref_cursor for
select hr.id, hr.group_name, hr.group_level
from hr_position hr
join drill_position dp
on hr.id = dp.id
where dp.typevalue = p_record_type;
end drill_record_position;
/
which you could then test with something like:
declare
l_ref_cursor sys_refcursor;
l_id hr_position.id%type;
l_group_name hr_position.group_name%type;
l_group_level hr_position.group_level%type;
begin
drill_record_position('D', l_ref_cursor);
loop
fetch l_ref_cursor into l_id, l_group_name, l_group_level;
exit when l_ref_cursor%notfound;
dbms_output.put_line(l_id ||':'|| l_group_name ||':'|| l_group_level);
end loop;
close l_ref_cursor;
end;
/
You can also do that as a function, which might be easier to work with from your application:
-- drop procedure drill_record_position;
create or replace function drill_record_position (p_record_type in varchar2)
return sys_refcursor as
l_ref_cursor sys_refcursor;
begin
open l_ref_cursor for
select hr.id, hr.group_name, hr.group_level
from hr_position hr
join drill_position dp
on hr.id = dp.id
where dp.typevalue = p_record_type;
return l_ref_cursor;
end drill_record_position;
/
declare
l_ref_cursor sys_refcursor;
l_id hr_position.id%type;
l_group_name hr_position.group_name%type;
l_group_level hr_position.group_level%type;
begin
l_ref_cursor := drill_record_position('D');
loop
fetch l_ref_cursor into l_id, l_group_name, l_group_level;
exit when l_ref_cursor%notfound;
dbms_output.put_line(l_id ||':'|| l_group_name ||':'|| l_group_level);
end loop;
close l_ref_cursor;
end;
/
You coudl also do this with collections and a pipelined function, which is more work to set up:
create type t_drill_obj as object (
-- use your real data types...
id number,
group_name varchar2(10),
group_level number
)
/
create type t_drill_tab as table of t_drill_obj
/
create or replace function drill_record_position (p_record_type in varchar2)
return t_drill_tab pipelined as
begin
for l_row in (
select t_drill_obj(hr.id, hr.group_name, hr.group_level) as obj
from hr_position hr
join drill_position dp
on hr.id = dp.id
where dp.typevalue = p_record_type
)
loop
pipe row (l_row.obj);
end loop;
return;
end drill_record_position;
/
but you could call it as part of another query, and even join tot he result if you needed to:
select * from table(drill_record_position('D'));
Let's create two test procedures:
CREATE OR REPLACE PROCEDURE Aaaa_Test1(
pDog SYS_REFCURSOR
) IS
TYPE tDogRec is record (objid varchar2(7), lim number, debt number);
TYPE tDog IS TABLE OF tDogRec;
vDog tDog;
BEGIN
IF pDog%ISOPEN THEN
FETCH pDog BULK COLLECT INTO vDog;
IF vDog.count >= 1 THEN
FOR i IN vDog.First..vDog.Last LOOP
Dbms_Output.Put_Line('Aaaa_Test1 = '||vDog(i).Objid);
END LOOP;
END IF;
END IF;
END; -- Aaaa_Test1 Procedure
/
CREATE OR REPLACE PROCEDURE Aaaa_Test2(
pDog SYS_REFCURSOR
) IS
TYPE tDogRec is record (objid varchar2(7), lim number, debt number);
TYPE tDog IS TABLE OF tDogRec;
vDog tDog;
BEGIN
IF pDog%ISOPEN THEN
FETCH pDog BULK COLLECT INTO vDog;
IF vDog.count >= 1 THEN
FOR i IN vDog.First..vDog.Last LOOP
Dbms_Output.Put_Line('Aaaa_Test2 = '||vDog(i).Objid);
END LOOP;
END IF;
END IF;
END; -- Aaaa_Test2 Procedure
Then let's try to open cursor and pass it to these procedures in order:
DECLARE
Vcdogcur SYS_REFCURSOR;
BEGIN
OPEN Vcdogcur FOR
select '6518535' objid, 10000 lim,0 debt
from dual
union all
select '6518536', 0,500
from dual
union all
select '5656058', 0,899
from dual
union all
select '2180965', 5000,0
from dual
union all
select '2462902', 0,100
from dual;
Aaaa_Test1(Vcdogcur);
Aaaa_Test2(Vcdogcur);
CLOSE Vcdogcur;
END;
As you can see, I can't use already fetched cursor in second procedure, because ORACLE cursors are forward-and-read-only. What ways can help to solve this task?
I can't simply bring these procedures into one. Need to keep their logic separate from each other.
You need to open the cursor twice. Using a string variable to hold the query will prevent you from writing the query twice.
DECLARE
Vcdogcur SYS_REFCURSOR;
dyn_query varchar2(500);
BEGIN
dyn_query := 'select ''6518535'' objid, 10000 lim,0 debt
from dual
union all
select ''6518536'', 0,500
from dual
union all
select ''5656058'', 0,899
from dual
union all
select ''2180965'', 5000,0
from dual
union all
select ''2462902'', 0,100
from dual' ;
open Vcdogcur for dyn_query ;
Aaaa_Test1(Vcdogcur);
CLOSE Vcdogcur;
open Vcdogcur for dyn_query ;
Aaaa_Test2(Vcdogcur);
close Vcdogcur;
END;
/
Cursors are NOT designed to be re-used: you read them once, keep moving forward and as you're doing so you're discarding any previously scanned rows. Think of a Java stream... This is a feature, not a bug - cursors are intended to be very much memory/disk efficient.
So the options are:
1) As Nicolas mentioned, close and re-open the same cursor. You'll pay the performance penalty of running the same query twice
2) Store the query results in a temp table (good for very large sets as it would use disk)
3) Store the query results in a collection (nested table - good for small-medium sized tables)
4) If you really can't do any of the easy solutions above you can try to mess around with your code so that you have one "dispatch" procedure that reads the cursor and then passes each row to your 2 "worker" procedures. You'd have to modify your stored procs to be able to process row at a time
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;
I have to write an Oracle procedure which should invoke an Oracle function returning REF_CURSOR. The function is declared like that
FUNCTION "IMPACTNET"."TF_CONVERTPARA" (PARASTRING IN NVARCHAR2) RETURN SYS_REFCURSOR
AS
c SYS_REFCURSOR;
BEGIN
OPEN c FOR
SELECT SUBSTR(element, 1, INSTR(element, '|') - 1) as key,
SUBSTR(element, INSTR(element, '|') + 1, 99999) as val
FROM (
SELECT REGEXP_SUBSTR(PARASTRING, '[^;]+', 1, LEVEL) element
FROM dual
CONNECT BY LEVEL < LENGTH(REGEXP_REPLACE(PARASTRING, '[^;]+')) + 1
);
RETURN c;
END;
Can you tell me what I need to write in order to invoke the function from within my procedure? I'd like to insert all the returned values (shaped a table with two columns) into a rational table.
Thank you in advance!
Something along the lines of this should work (obviously, I'm guessing about table names and column names and the exact logic that you're trying to implement)
CREATE PROCEDURE some_procedure_name
AS
l_rc SYS_REFCURSOR := impactnet.tf_convertpara( <<some string>> );
l_key VARCHAR2(100);
l_val VARCHAR2(100);
BEGIN
LOOP
FETCH l_rc
INTO l_key, l_val;
EXIT WHEN l_rc%notfound;
INSERT INTO some_table( key_column, val_column )
VALUES( l_key, l_val );
END LOOP;
END;
As Ollie points out, it would be more efficient to do a BULK COLLECT and a FORALL. If you're just dealing with a few thousand rows (since your function is just parsing the data in a delimited string, I'm assuming you expect relatively few rows to be returned), the performance difference is probably minimal. But if you're processing more data, the difference can be quite noticeable. Depending on the Oracle version and your specific requirements, you may be able to simplify the INSERT statement in the FORALL to insert a record rather than listing each column from the record individually.
CREATE PROCEDURE some_procedure_name
AS
TYPE key_val_rec
IS RECORD(
key VARCHAR2(100),
val VARCHAR2(100)
);
TYPE key_val_coll
IS TABLE OF key_val_rec;
l_rc SYS_REFCURSOR := impactnet.tf_convertpara( <<some string>> );
l_coll key_val_coll;
BEGIN
LOOP
FETCH l_rc
BULK COLLECT INTO l_coll
LIMIT 100;
EXIT WHEN l_coll.count = 0;
FORALL i IN l_coll.FIRST .. l_coll.LAST
INSERT INTO some_table( key_column, val_column )
VALUES( l_coll(i).key, l_coll(i).val );
END LOOP;
END;