How to check whether a dynamic cursor will retrieve no records? - oracle

I have a procedure which has a structure as given below:
PROCEDURE broker(prm_qgent in varchar2, prm_cursor out sys_refcursor)
IS
mmy_query varchar(200);
BEGIN
OPEN prm_cursor FOR SELECT * FROM DUAL;
mmy_query :='SELECT *some dynamic query* where 1=1';
if prm_agent is not null then
mmy_query := mmy_query ||'AND agent_code = ''' ||prm_agent || '''';
end if;
OPEN prm_cursor FOR mmy_query;
END broker;
mmy_query is the search criteria. So if a broker is not in the table, it should retrieve zero records.
mmy_query is the dynamic query to retrieve records. If dynamic query retrieves record, it will only retrieve 1 record. So, I want to check if mmy_query retrieves no records.
tried prm_cursor%ROWTYPE, shows zero record in both cases.
tried SQL%ROWTYPE, always shows 1 record.

You need to open the cursor, fetch the first record from it and check FOUND or NOT_FOUND attributtes.
Note: if the first row is fetched, then there is no other way to "rewind the cursor to the beginning" except that closing that cursor and opening it again (and executing the same query again).
According to the documentation (from the above link), each named cursor has 4 attributtes:
%ISOPEN
named_cursor%ISOPEN has the value TRUE if the cursor is open, and
FALSE if it is not open.
%FOUND
named_cursor%FOUND has one of these values:
If the cursor is not open, INVALID_CURSOR
If cursor is open but no fetch was tried, NULL.
If the most recent fetch returned a row, TRUE.
If the most recent fetch did not return a row, FALSE.
%NOTFOUND
named_cursor%NOTFOUND has one of these values:
If cursor is not open, INVALID_CURSOR.
If cursor is open but no fetch was tried, NULL.
If the most recent fetch returned a row, FALSE.
If the most recent fetch did not return a row, TRUE.
%ROWCOUNT
named_cursor%ROWCOUNT has one of these values:
If cursor is not open, INVALID_CURSOR.
If cursor is open, the number of rows fetched so far.
As you see, there is no way to check whether the query returned either no rows or some rows whithout opening the cursor and fetching from it.
Simple examples:
A query that returns some rows:
DECLARE
my_cursor SYS_REFCURSOR;
y VARCHAR(10);
BEGIN
OPEN my_cursor FOR 'select * FROM dual WHERE 1=1';
FETCH my_cursor INTO y;
IF my_cursor%found THEN
DBMS_OUTPUT.PUT_LINE('FOUND');
ELSE
DBMS_OUTPUT.PUT_LINE('NOT FOUND');
END IF;
CLOSE my_cursor;
END;
/
Result: FOUND
A query that returns empty resultset:
DECLARE
my_cursor SYS_REFCURSOR;
y VARCHAR(10);
BEGIN
OPEN my_cursor FOR 'select * FROM dual WHERE 1=0';
FETCH my_cursor INTO y;
IF my_cursor%found THEN
DBMS_OUTPUT.PUT_LINE('FOUND');
ELSE
DBMS_OUTPUT.PUT_LINE('NOT FOUND');
END IF;
CLOSE my_cursor;
END;
/
Result: NOT FOUND

It is unknown whether or not rows will be found until the caller attempts to fetch from the cursor. At the time when the cursor is opened, this has not happened yet. A procedure that simply opens and returns a cursor has no way of knowing what will happen when the caller tries to fetch from it after the procedure has completed.
Perhaps it would be theoretically possible to parse the execution plan from v$sql_plan to get the estimated cardinality, if you could find its sql_id (perhaps from querying v$sql), but this would not be straightforward and at best the result would be a guess.
All you can really do is execute the query twice (or execute a simplified version of it, if that's possible), bearing in mind that the results could change between executions if other sessions are modifying the data.

Related

ORA-01002: fetch out of sequence in procedure

I created a procedure in oracle database that returns data in a ref-cursor, and I want it to return the rowcount of this cursor also as an output variable. After testing, the P_count variable is filled correctly, but when I tried to open it an
ORA-01002: fetch out of sequence
error is fired. I have read before about it and I found that the problem is because I am using a fetch statement in. But till now I did not discover how to resolve it. Any helps are appreciated, thank you.
Below is my Procedure:
PROCEDURE IS_CLIENT_LOGGED_IN (P_CLIENT_NUM Varchar2,P_CURSOR out SYS_REFCURSOR ,P_COUNT OUT NUMBER,P_ERROR out Varchar2) AS
TYPE MyRec IS RECORD (ID VARCHAR2(100));
cur_rec MyRec;
lv_cur SYS_REFCURSOR;
BEGIN
BEGIN
Open lv_cur FOR
SELECT ID
FROM tbl_registration
WHERE tbl_client_id = P_CLIENT_NUM
AND tbl_logout_date is null;
LOOP
FETCH lv_cur INTO cur_rec;
EXIT WHEN P_CURSOR%notfound;
P_COUNT := P_CURSOR%rowcount;--will return row number beginning with 1
END LOOP;
P_CURSOR := lv_cur;
EXCEPTION WHEN OTHERS THEN
P_ERROR := 'Unable to select Data from tbl_registration' ||SQLERRM;
END;
Searching online I found reasons for the issue as below:
Fetching from a cursor after the last row has been retrieved and the ORA-1403 error returned.
If the cursor has been opened with the FOR UPDATE clause, fetching after a COMMIT has been issued will return the error.
Rebinding any placeholders in the SQL statement, then issuing a fetch before reexecuting the statement.
But I cannot find a proper solution.
You can only use a cursor once, so if you want both the row count and the actual result set, you will need two separate queries. Then the problem is that the data might have been updated by other sessions between the two queries, giving inconsistent results. (For example, another session inserted some rows earlier, and then commits while your procedure is running.)
One approach would be to use flashback query to ensure that the count and the fetch refer to the same point in time:
create or replace procedure is_client_logged_in
( p_client_num varchar2
, p_cursor out sys_refcursor
, p_count out number
, p_error out varchar2 )
as
k_starttime constant timestamp := systimestamp;
begin
select count(*) into p_count
from tbl_registration as of timestamp k_starttime
where tbl_client_id = p_client_num
and tbl_logout_date is null;
open p_cursor for
select id
from tbl_registration as of timestamp k_starttime
where tbl_client_id = p_client_num
and tbl_logout_date is null;
exception
when others then p_error := 'Unable to retrieve registration data.'||chr(10)||sqlerrm;
end;
What you want sounds very simple, but isn't. A cursor is for one-use only. So if you read through it, you are done with it. Returning the unused cursor plus its row count requires special handling hence.
One way to come to mind is two separate queries: One to count the rows, one to open the cursor. The problem, though: In the nanoseconds between the queries the row count may change because of other sessions inserting or deleting or updating data. Okay, you can lock the table exclusively, but then you will have to release it after the two statements. Unfortunately, there is no UNLOCK TABLE command in Oracle. Releasing a table is only possible via a COMMIT or ROLLBACK. So, in order not to interfere with the caller's transaction you need a SAFEPOINT, too.
Here is your procedure with the above method applied:
CREATE OR REPLACE PROCEDURE is_client_logged_in (p_client_num VARCHAR2, p_cursor OUT SYS_REFCURSOR, p_count OUT NUMBER, p_error OUT VARCHAR2) AS
BEGIN
SAVEPOINT for_unlock;
LOCK TABLE tbl_registration IN EXCLUSIVE MODE;
SELECT COUNT(*) INTO p_count
FROM tbl_registration
WHERE tbl_client_id = p_client_num
AND tbl_logout_date IS NULL;
Open p_cursor FOR
SELECT id
FROM tbl_registration
WHERE tbl_client_id = p_client_num
AND tbl_logout_date IS NULL;
ROLLBACK TO SAVEPOINT for_unlock;
EXCEPTION WHEN OTHERS THEN
ROLLBACK TO SAVEPOINT for_unlock;
p_error := 'Unable to select Data from tbl_registration' || SQLERRM;
END is_client_logged_in;
Demo: https://dbfiddle.uk/?rdbms=oracle_18&fiddle=552d7f76d03a9a221c17f05c6663d2c9
Here is an alternative. It's really muddling through, but well...
I've shown a solution with locking the table. But imagine a query that runs for an hour. You'd lock that table for twice this time.
William has shown a great solution with flashback queries. But with a query running for an hour, the flashback data may time out.
So, here is a weird solution getting count and results in one query. This is easy in your case, because I expect the ID you are selcting to be an integer, just as the count. So I select the count into the first row, fetch that and give back the cursor that is now at the position for the first actual data row (the first ID).
CREATE OR REPLACE PROCEDURE is_client_logged_in (p_client_num VARCHAR2, p_cursor OUT SYS_REFCURSOR, p_count OUT NUMBER, p_error OUT VARCHAR2) AS
BEGIN
Open p_cursor FOR
WITH data AS
(
SELECT id
FROM tbl_registration
WHERE tbl_client_id = p_client_num
AND tbl_logout_date IS NULL
)
SELECT id
FROM
(
SELECT 1 as sortkey, COUNT(*) AS id FROM data
UNION ALL
SELECT 1 as sortkey, id FROM data
)
ORDER BY sortkey;
FETCH p_cursor INTO p_count;
EXCEPTION WHEN OTHERS THEN
p_error := 'Unable to select Data from tbl_registration' || SQLERRM;
END is_client_logged_in;
Demo: https://dbfiddle.uk/?rdbms=oracle_18&fiddle=24ed333b9c1ed700af6d3a486ef47ccd

Oracle - two loops in procedure

I need some help in writing Oracle PL/SQL procedure that should do the following:
the procedure is called from a trigger after an update of the field in one table with the input parameter B-block or D-activate (this is already done)
the procedure should first open one cursor that will catch the account numbers of a client and open a loop that will process account by account
this one account should be forwarded to another loop that will catch card numbers of that client for that account (second cursor) and when into this loop, the card number should be used as an input parameter for a stored procedure that is called to block/unblock this card - this stored procedure already exists I just need to call it
the procedure don't need to return any parameters, the idea is just to block/activate card number of a client with the already written stored procedure for that
Should I write a package for this or just a procedure? And how can I write one loop in another?
I just realized that i can do this without cursors in a procedure. For simple example:
create or replace procedure blokiraj_proc (core_cust_id varchar2, kyc_blocked varchar2) as
type NumberArray is Array(100) of test_racuni.foracid%type;
type StringArray is Array (1000) of test_kartice.card_num%type;
accnt NumberArray;
card_number StringArray;
begin
select foracid bulk collect into accnt from test_racuni where cif_id = core_cust_id;
for i in accnt.first..accnt.last
loop
select card_num bulk collect into card_number from test_kartice where rbs_acct_num = accnt(i);
dbms_output.enable (100000);
dbms_output.put_line (accnt(i));
for j in 1..card_number.count
loop
dbms_output.put_line (card_number(j));
blokiraj_karticu (card_number(j));
end loop;
end loop;
end;
Is this a better approach then the curssors? And why is dbms_output not printing anything when i trigger the procedure?
As #EdStevens indicated you cannot avoid processing cursors. But you can avoid the looping structure of cursor within cursor. And the implicit open and close cursor for the inner one. The queries have combine into a simple JOIN then bulk collect into a single collection.
For this I created a RECORD to contain both the account number and card number; then a collection of that record. The cursor is then bulk collected into the collection. Your initial code allows for up to 100000 cards to be processed, and while I am a fan of bulk collect (when needed) I am not a fan of filling memory, therefore I limit the number of rows bulk collect gathers of each fetch. This unfortunately introduces a loop-within-loop construct, but the penalty is not near as great as cursor-within-cursor construct. The following is the result.
create or replace procedure blokiraj_proc (core_cust_id varchar2) as
type acct_card_r
is record(
acct_num test_kartice.rbs_acct_num%type
, card_num test_kartice.card_num%type
);
type acct_card_array is table of acct_card_r;
acct_card_list acct_card_array;
k_acct_card_buffer_limit constant integer := 997;
cursor c_acct_card(c_cust_id varchar2) is
select r.foracid
, k.card_num
from test_racuni r
left join test_kartice k
on (k.rbs_acct_num = r.foracid)
where r.cif_id = c_cust_id
order by r.foracid
, k.card_num;
begin
dbms_output.enable (buffer_size => null); -- enable dbms_output with size unlimited
open c_acct_card(core_cust_id);
loop
fetch c_acct_card
bulk collect
into acct_card_list
limit k_acct_card_buffer_limit;
for i in 1 .. acct_card_list.count
loop
dbms_output.put (acct_card_list(i).acct_num || ' ==> ');
if acct_card_list(i).card_num is not null
then
dbms_output.put_line (acct_card_list(i).card_num);
blokiraj_karticu (acct_card_list(i).card_num);
else
dbms_output.put_line ('No card for this account');
end if;
end loop;
-- exit buffer fetch when current buffeer is not full. As that means all rows
-- from cursor have been fetched/processed.
exit when acct_card_list.count < k_acct_card_buffer_limit;
end loop;
close c_acct_card;
end blokiraj_proc;
Well this is just another approach. If it's better for you, great. I also want to repeat and expand Ed Stevens warning of running this from a trigger. If either of the tables here is the table on which the trigger fired you will still get a mutating table exception - you cannot just hide it behind a procedure. And even if not its a lot of looping for trigger.

Validation of result before executing the query

When running a stored procedure to fetch some rows, First I want to validate if the query will return a row before sending the result, and second if it is possible to validate without running the same query twice.
I am using a cursor to store the yielded result, So I tried the cursor attribute %ROWCOUNT & %NOTFOUND. But the doesnt quite work. Plus I want to do this without running a loop on the cursor.
procedure MODULE_LIST_GK(p_module_Id IN MODULE_LIST.MODULE_ID% TYPE,
p_Error_Code out nvarchar2,
p_Error_Msg out nvarchar2,
p_Cursor out sys_refcursor) IS
BEGIN
OPEN p_Cursor FOR
SELECT A.MODULE_ID,
A.MODULE_NM,
A.AUTH_STATUS_ID
FROM MODULE_LIST A
WHERE A.MODULE_ID=p_module_Id;
SELECT COUNT(MODULE_ID)
INTO v_row_num
FROM MODULE_LIST A
WHERE A.MODULE_ID=p_module_Id;
IF v_row_num=0 THEN
p_Error_Code := SQLCODE;
p_Error_Msg := 'Does not Exists';
Return;
end IF;
EXCEPTION
WHEN OTHERS THEN
p_error_code:= SQLCODE;
p_error_msg := SQLERRM;
END MODULE_LIST_GK;
Your implementation have several points that could be improved.
First if you expect that for lot of parameters the returned cursor will be empty,
than first check the empty cursor and only after this check open the cursor. You do it vice versa.
How to check if the cursor is empty? Unfortunatelly you must fetch the first row to be able to verify it.
open l_cur for
select id, status from tab where id = p_id;
fetch l_cur into l_id, l_status;
if l_cur%NOTFOUND then
p_Error_Msg := 'Does not Exists';
Return;
end if;
This check is far more effective that the often used count(*) as it is considering only the first (few) rows and not counting all rows in the cursor.
If the check fails you are ready, othervise simple open the cursor and return it.
open l_cur for
select id, status from tab where id = p_id;
p_Cursor := l_cur;
Two additional thinks come to mind.
You should rething the generall approach if the database is very dynamic. How would you handle the case when other session deletes some row between the check and the second open of the cursor?
Finally consider returning an exception instead of the return code.
In order to know whether a cursor contains rows, you must open it and fetch the first row. Once you've done this, it makes no sense anymore to return that cursor, for the recipient will not be able to fetch that first row, because the cursor already points beyond it.
So, you must select twice. What you'd do is to use ROWNUM or an EXISTS clause here to show the DBMS that you are not interested in any more rows. This can speed up the query extremely.
PROCEDURE module_list_gk(p_module_id IN MODULE_LIST.MODULE_ID%TYPE,
p_error_code OUT NVARCHAR2,
p_error_msg OUT NVARCHAR2,
p_cursor OUT SYS_REFCURSOR) IS
v_count INTEGER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM module_list
WHERE module_id = p_module_Id
AND ROWNUM = 1;
IF v_count = 0 THEN
p_error_code := 0; -- Or -1403 for NO DATA FOUND if you like
p_error_msg := 'Does not Exists';
RETURN;
END IF;
OPEN p_Cursor FOR
SELECT module_id, module_nm, auth_status_id
FROM module_list
WHERE module_id = p_module_id;
EXCEPTION WHEN OTHERS THEN
p_error_code:= SQLCODE;
p_error_msg := SQLERRM;
END module_list_gk;
SQLCODE for the first query will be 0 by the way (SELECT COUNT(*) returns one row with the number of records found - no error hence). This is why you should decide to either return zero explicitly or some error code, such as -1403.
Here is the same with EXISTS:
BEGIN
SELECT CASE WHEN EXISTS
(
SELECT NULL
FROM module_list
WHERE module_id = p_module_Id
) THEN 1 ELSE 0 END
INTO v_count
FROM DUAL;
IF v_count = 0 THEN

Oracle stored procedure cursor always returns zero rows

I have this cursor in a procedure in a package:
PROCEDURE CANCEL_INACTIVE(IN_DAYS_OLD NUMBER)
IS
CURSOR inactive IS
SELECT * FROM MY_TABLE
WHERE STATUS_CHANGED_DATE <= TRUNC(SYSDATE-IN_DAYS_OLD)
AND CANCEL_CD IS NULL;
rec inactive%ROWTYPE;
BEGIN
OPEN inactive;
LOOP
FETCH inactive INTO rec;
EXIT WHEN inactive%NOTFOUND;
-- do an update based on rec.id
END LOOP;
END;
END CANCEL_INACTIVE;
Every time I test or run the procedure, inactive always has zero rows. However, when I put the EXACT same query into a SQL window, I get the rows I'm looking for.
What the heck?
Probably you'are testing on noncommited data.
Or: you're not commiting your update based on rec.id.
Or: your update does nothing. (the where clause is not satisfied by any rows on target table)

How can I find the number of records in an Oracle PL/SQL cursor?

Here's my cursor:
CURSOR C1 IS SELECT * FROM MY_TABLE WHERE SALARY < 50000 FOR UPDATE;
I immediately open the cursor in order to lock these records for the duration of my procedure.
I want to raise an application error in the event that there are < 2 records in my cursor. Using the C1%ROWCOUNT property fails because it only counts the number which have been fetched thus far.
What is the best pattern for this use case? Do I need to create a dummy MY_TABLE%ROWTYPE variable and then loop through the cursor to fetch them out and keep a count, or is there a simpler way? If this is the way to do it, will fetching all rows in my cursor implicitly close it, thus unlocking those rows, or will it stay open until I explicitly close it even if I've fetched them all?
I need to make sure the cursor stays open for a variety of other tasks beyond this count.
NB: i just reread you question.. and you want to fail if there is ONLY 1 record..
i'll post a new update in a moment..
lets start here..
From Oracle® Database PL/SQL User's Guide and Reference
10g Release 2 (10.2)
Part Number B14261-01
reference
All rows are locked when you open the cursor, not as they are fetched. The rows are unlocked when you commit or roll back the transaction. Since the rows are no longer locked, you cannot fetch from a FOR UPDATE cursor after a commit.
so you do not need to worry about the records unlocking.
so try this..
declare
CURSOR mytable_cur IS SELECT * FROM MY_TABLE WHERE SALARY < 50000 FOR UPDATE;
TYPE mytable_tt IS TABLE OF mytable_cur %ROWTYPE
INDEX BY PLS_INTEGER;
l_my_table_recs mytable_tt;
l_totalcount NUMBER;
begin
OPEN mytable_cur ;
l_totalcount := 0;
LOOP
FETCH mytable_cur
BULK COLLECT INTO l_my_table_recs LIMIT 100;
l_totalcount := l_totalcount + NVL(l_my_table_recs.COUNT,0);
--this is the check for only 1 row..
EXIT WHEN l_totalcount < 2;
FOR indx IN 1 .. l_my_table_recs.COUNT
LOOP
--process each record.. via l_my_table_recs (indx)
END LOOP;
EXIT WHEN mytable_cur%NOTFOUND;
END LOOP;
CLOSE mytable_cur ;
end;
ALTERNATE ANSWER
I read you answer backwards to start and thought you wanted to exit if there was MORE then 1 row.. not exactly one.. so here is my previous answer.
2 simple ways to check for ONLY 1 record.
Option 1 - Explicit Fetchs
declare
CURSOR C1 IS SELECT * FROM MY_TABLE WHERE SALARY < 50000 FOR UPDATE;
l_my_table_rec C1%rowtype;
l_my_table_rec2 C1%rowtype;
begin
open C1;
fetch c1 into l_my_table_rec;
if c1%NOTFOUND then
--no data found
end if;
fetch c1 into l_my_table_rec2;
if c1%FOUND THEN
--i have more then 1 row
end if;
close c1;
-- processing logic
end;
I hope you get the idea.
Option 2 - Exception Catching
declare
CURSOR C1 IS SELECT * FROM MY_TABLE WHERE SALARY < 50000 FOR UPDATE;
l_my_table_rec C1%rowtype;
begin
begin
select *
from my_table
into l_my_table_rec
where salary < 50000
for update;
exception
when too_many_rows then
-- handle the exception where more than one row is returned
when no_data_found then
-- handle the exception where no rows are returned
when others then raise;
end;
-- processing logic
end;
Additionally
Remember: with an explicit cursor.. you can %TYPE your variable off the cursor record rather then the original table.
this is especially useful when you have joins in your query.
Also, rememebr you can update the rows in the table with an
UPDATE table_name
SET set_clause
WHERE CURRENT OF cursor_name;
type statement, but I that will only work if you haven't 'fetched' the 2nd row..
for some more information about cursor FOR loops.. try
Here
If you're looking to fail whenver you have more than 1 row returned, try this:
declare
l_my_table_rec my_table%rowtype;
begin
begin
select *
from my_table
into l_my_table_rec
where salary < 50000
for update;
exception
when too_many_rows then
-- handle the exception where more than one row is returned
when no_data_found then
-- handle the exception where no rows are returned
when others then raise;
end;
-- processing logic
end;
If this is the way to do it, will
fetching all rows in my cursor
implicitly close it, thus unlocking
those rows
The locks will be present for the duration of the transaction (ie until you do a commit or rollback) irrespective of when (or whether) you close the cursor.
I'd go for
declare
CURSOR C1 IS SELECT * FROM MY_TABLE WHERE SALARY < 50000 FOR UPDATE;;
v_1 c1%rowtype;
v_cnt number;
begin
open c_1;
select count(*) into v_cnt FROM MY_TABLE WHERE SALARY < 50000 and rownum < 3;
if v_cnt < 2 then
raise_application_error(-20001,'...');
end if;
--other processing
close c_1;
end;
There's a very small chance that, between the time the cursor is opened (locking rows) and the select count, someone inserts one or more rows into the table with a salary under 50000. In that case the application error would be raised but the cursor would only process the rows present when the cursor was opened. If that is a worry, at the end do another check on c_1%rowcount and, if that problem was experienced, you'd need to rollback to a savepoint.
Create a savepoint before you iterate through the cursor and then use a partial rollback when you find there are < 2 records returned.
You can start transaction and check if SELECT COUNT(*) MY_TABLE WHERE SALARY < 50000 greater than 1.

Resources