Oracle PL/SQL capture lock exception in loop and continue - oracle

I have a procedure below, which loops through a cursor does some logic. I have put FOR UPDATE NOWAIT on the cursor to lock my records set, in case someone from another session wants to update the same set.
But there may be a scenario where the records I have selected in my cursor, is locked by someone as well, in which case, I want to simply log the locked record in a log table and continue to the next record in the loop.
PROCEDURE test(p_id IN NUMBER)
IS
CURSOR cur_test IS
SELECT emp_id,
emp_name
FROM
EMP
FOR UPDATE NOWAIT;
row_locked EXCEPTION;
PRAGMA EXCEPTION_INIT(row_locked, -54);
BEGIN
FOR r_cur_test IN cur_test
LOOP
BEGIN
--do something
EXCEPTION
WHEN row_locked THEN
--log locked record in log table
log_lock('Record is locked');
COMMIT;
END;
END LOOP;
--call log function to log a successful run
COMMIT;
EXCEPTION
WHEN OTHERS THEN
--Unsuccessful completion of the run.Trap all unhandled exceptions.
--log error in error table
log_lock('Process exited with error');
ROLLBACK;
END;
for testing, I open session 1 and executed
select * from EMP FOR UPDATE NOWAIT
and in session 2, I executed
begin
test(1);
end;
it always went to the OTHERS exception, as a result of which, the procecure just terminated without continuing looping through the cursor.
Can anyone give me some advice? Thanks a lot

When you do open cursor for update this statement works in scenario: at first run select query and set lock for all records and after then do fetch operations.
PROCEDURE test(p_id IN NUMBER)
IS
CURSOR cur_test IS
SELECT emp_id,
emp_name,
rowid as row_id
FROM
EMP;
row_locked EXCEPTION;
PRAGMA EXCEPTION_INIT(row_locked, -54);
v_sql varchar2(4000) := 'SELECT 1 FROM EMP t WHERE rowid = :row_id FOR UPDATE NOWAIT';
c int;
n int;
BEGIN
c := dbms_sql.open_cursor;
dbms_sql.parse(c, v_sql, dbms_sql.native);
FOR r_cur_test IN cur_test
LOOP
BEGIN
dbms_sql.bind_variable (c, 'row_id', i.row_id);
n := dbms_sql.execute(c);
--do something
EXCEPTION
WHEN row_locked THEN
--log locked record in log table
log_lock('Record is locked');
COMMIT;
END;
END LOOP;
dbms_sql.close_cursor(c);
--call log function to log a successful run
COMMIT;
EXCEPTION
WHEN OTHERS THEN
--Unsuccessful completion of the run.Trap all unhandled exceptions.
--log error in error table
log_lock('Process exited with error');
dbms_sql.close_cursor(c);
ROLLBACK;
END;

Related

Detecting a Deadlock in a Stored Procedure

I am trying to write a deadlock proof stored procedure. It is based on the following concept.
I have been trying to write a stored procedure which is based on the following concept. The procedure will try to drop a constraint on a table and if in case it detects a deadlock situation, it wait for some time before trying again. The important thing is it should only retry in case of a Deadlock or a NOWAIT error, all other errors are to be handled via exceptions.
Procedure test
is
BEGIN
<<label>>
DROP constraint on a table
if (deadlock(ORA-00060)/Nowait Error (ORA-0054)) detected
then
sleep for 60 seconds
Goto label
exception
when others.
It would be great if any of the experts please help me with this. A similar example would be highly helpful. Thank you for your help.
While some people harbour an irrational aversion to goto it remains true that usually we can implement the same logic without using the construct. That is true here: a simple while loop is all that is necessary:
create or replace procedure drop_constraint_for_sure
( p_table_name in varchar2
, p_constraint_name in varchar2
)
is
x_deadlock exception;
pragma exception_init(x_deadlock, -60);
x_nowait exception;
pragma exception_init(x_nowait, -54);
begin
loop
begin
execute immediate 'alter table '|| p_table_name
|| ' drop constraint ' || p_constraint_name
|| ' cascade' ;
exit;
exception
when x_deadlock then null;
when x_nowait then null;
end;
dbms_lock.sleep(60);
end loop;
end;
/
Note that the sleep function requires the execute privilege on SYS.DBMS_LOCK. This is not granted by default, so if you don't have it you'll need to ask your friendly DBA to grant it.
Also note that this implementation doesn't have any form of abort. So it will loop eternally, until the constraint is dropped or some other exception occurs. In real life you should include a loop count with an additional exit test on a threshold for the count. Although in real life I probably wouldn't want a stored procedure like this anyway: I prefer knowing as soon as possible when someone is using a table I'm trying to alter.
Hope below snippet gives you can an idea to achieve your requirement.
CREATE OR REPLACE
PROCEDURE DRP_CONST_DEALDOCK
AS
DEADLOCK_EX EXCEPTION;
NO_WAIT_EX EXCEPTION;
PRAGMA EXCEPTION_INIT(DEADLOCK_EX,-60);
PRAGMA EXCEPTION_INIT(NO_WAIT_EX, -54);
lv_cnt PLS_INTEGER;
BEGIN
BEGIN
EXECUTE IMMEDIATE 'ALTER TABLE STACK_OF_TEST DROP CONSTRAINT SYS_C00375020';
EXCEPTION
WHEN DEADLOCK_EX THEN
dbms_output.put_line('nowait exception');
DBMS_LOCK.SLEEP(10);
SELECT COUNT(1)
INTO lv_cnt
FROM v$locked_object
WHERE object_id IN
(SELECT OBJECT_ID FROM DBA_OBJECTS WHERE OBJECT_NAME = 'STACK_OF_TEST'
);
WHILE lv_cnt > 0
LOOP
dbms_lock.sleep(10);
SELECT COUNT(1)
INTO lv_cnt
FROM v$locked_object
WHERE object_id IN
(SELECT OBJECT_ID FROM DBA_OBJECTS WHERE OBJECT_NAME = 'STACK_OF_TEST'
);
END LOOP;
WHEN NO_WAIT_EX THEN
dbms_output.put_line('nowait exception');
DBMS_LOCK.SLEEP(10);
SELECT COUNT(1)
INTO lv_cnt
FROM v$locked_object
WHERE object_id IN
(SELECT OBJECT_ID FROM DBA_OBJECTS WHERE OBJECT_NAME = 'STACK_OF_TEST'
);
WHILE lv_cnt > 0
LOOP
dbms_lock.sleep(10);
SELECT COUNT(1)
INTO lv_cnt
FROM v$locked_object
WHERE object_id IN
(SELECT OBJECT_ID FROM DBA_OBJECTS WHERE OBJECT_NAME = 'STACK_OF_TEST'
);
END LOOP;
WHEN OTHERS THEN
dbms_output.put_line(SQLCODE||'-->'||SQLERRM);
END;
END;

Exception dose not working in execute immediate insert in oracle

I have some problem in execute immediate insert statement exception part.
I have a table query_tb that contains two columns (DEPT and SOURCE_VALUE)
The column contains data in below
CLERK
select a.empno,a.ename,a.job,a.mgr,a.hiredate,b.deptno,b.dname,b.loc
from emp a,dept b where a.deptno=b.deptno and a.empno= '#V_GCIF#'
SALESMAN
select e.empno,e.ename,e.job,d.deptno,d.dname,d.loc from emp e,dept
d where e.deptno=d.deptno and e.empno= '#V_GCIF#'
MANAGER
select a.empno,a.ename,a.job,b.deptno,b.dname,b.loc from employee
a,department b where a.deptno=b.deptno and a.empno= '#V_GCIF#'
ADMIN
select a.empno,a.ename,a.job,b.deptno,b.dname,b.loc from employee
a,department b where a.deptno=b.deptno and a.empno= '#V_GCIF#'
If I pass the correct empno which is keep on the emp table it runs fine. But if I pass the incorrect empno (no data) then exception part not working.
create or replace
PROCEDURE test_emp_sp(
p_id IN VARCHAR2)
AS
CURSOR rec
IS
SELECT dept,
source_value
FROM query_tb;
v_query VARCHAR2(1000);
BEGIN
FOR rec IN
(SELECT dept,source_value FROM query_tb
)
LOOP
IF rec.dept='CLERK' THEN
v_query :=REPLACE(rec.source_value,'#V_GCIF#',p_id);
EXECUTE IMMEDIATE 'INSERT INTO emp_tb (empno,ename,job,mgr,hiredate,deptno,dname,loc) ('||v_query|| ')';
dbms_output.put_line(v_query||' inserted');
ELSE
v_query:=REPLACE(rec.source_value,'#V_GCIF#',p_id);
EXECUTE IMMEDIATE 'INSERT INTO emp_tb (empno,ename,job,deptno,dname,loc) ('||v_query||')';
dbms_output.put_line(v_query||' inserting others');
END IF;
END LOOP;
commit;
EXCEPTION
WHEN others THEN
dbms_output.put_line('No data Found...');
END;
That's because you are not running a select command, it is an insert command (insert select) which means that if the select won't return rows it just doesn't insert anything and no error is thrown for that. You should check whether the insert command has affected any rows. The way you do that in Oracle is checking the SQL%ROWCOUNT immediate after the execution, if it turns to be 0 then you do your job raising an exception. It would be something like:
DECLARE
customException EXCEPTION;
PRAGMA EXCEPTION_INIT( customException, -20001 );
....
BEGIN
EXECUTE IMMEDIATE 'INSERT INTO emp_tb (empno,ename,job,mgr,hiredate,deptno,dname,loc)
('||v_query|| ')';
IF (SQL%ROWCOUNT) = 0 then
raise_application_error( -20001, 'This is a custom error' );
end if;
EXCEPTION
WHEN customException THEN
dbms_output.put_line('No data Found...');
END;
It's a long time without programming in Oracle PLSql So somethings there on the provided code may not compile but it is all there look into it in the internet and you will be good.
Exception does not work because there is no exception.
If SELECT returns NULL then 0 rows will be INSERT.
Example:
insert into t1(c1)
select 100 from dual where 1=0;
Result: 0 rows inserted.
insert into ... select from..
will not raise a no_data_found exception. It will just insert 0 records.
To test if you have inserted any records you can use SQL%ROWCOUNT after the insert statement.
execute immedate 'INSERT...;
if SQL%ROWCOUNT=0
then
dbms_output.put_line('no records inserted');
else
...
end if;
Also you might want to consider changing #V_GCIF# into something that can be used as a bind varaiable such as :P_ID.
You can skip the replace statement and change the execute immediate into something like:
execute immediate 'INSERT INTO ...'||v_query
using p_id;
This will bind the value of p_id to the :p_id in the statement.
The very basic thing here is to capture the error appropriately so that you can able to track back. Here i have put loggers and EXCEPTION handlers jsut to check the error. Also i have put a RAISE_APPLICATION_ERROR. Did some modifications too in the snipet. Hope it helps.
PS: Have not checked for the syntax as i dont have workspace now with me.
CREATE OR REPLACE
PROCEDURE test_emp_sp(
p_id IN VARCHAR2)
AS
--Not needed
-- CURSOR rec
-- IS
-- SELECT dept,
-- source_value
-- FROM query_tb;
--Not needed
v_query VARCHAR2(1000);
BEGIN
FOR rec IN
(SELECT dept,source_value FROM query_tb
)
LOOP
IF rec.dept='CLERK' THEN
v_query :=REPLACE(rec.source_value,'#V_GCIF#',p_id);
BEGIN
EXECUTE IMMEDIATE 'INSERT INTO emp_tb (empno,ename,job,mgr,hiredate,deptno,dname,loc) ('||v_query|| ')';
dbms_output.put_line(v_query||' inserted');
EXCEPTION WHEN OTHERS THEN
dbms_output.put_line(' Error while inserting data in emp_tab for Clerk--> '||SQLCODE||'---'||SQLERRM);
END;
ELSE
v_query:=REPLACE(rec.source_value,'#V_GCIF#',p_id);
BEGIN
EXECUTE IMMEDIATE 'INSERT INTO emp_tb (empno,ename,job,deptno,dname,loc) ('||v_query||')';
dbms_output.put_line(v_query||' inserting others');
EXCEPTION WHEN OTHERS THEN
dbms_output.put_line(' Error while inserting data in emp_tab for other then clerk --> '||SQLCODE||'---'||SQLERRM);
END;
END IF;
END LOOP;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
RAISE_APPLICATION_ERROR(-20001,'Error occurred in the plsql block',TRUE);
END;
/

Oracle Bulk Collect with Limit and For All Not Processing All Records Correctly

I need to process close to 60k records of an Oracle table through a stored procedure. The processing is that for each such row, I need to delete and update a row in a second table and insert a row in a third table.
Using cursor looping, the procedure takes around 6-8 hours to complete. If I switch to Bulk Collect with Limit, the execution time is reduced but processing is not correct. Following is the bulk collect version of the procedure
create or replace procedure myproc()
is
cursor c1 is select col1,col2,col3 from tab1 where col4=3;
type t1 is table of c1%rowtype;
v_t1 t1;
begin
open c1;
loop
fetch c1 bulk collect into v_t1 limit 1000;
exit when v_t1.count=0;
forall i in 1..v_t1.count
delete from tab2 where tab2.col1=v_t1(i).col1;
commit;
forall i in 1..v_t1.count
update tab2 set tab2.col1=v_t1(i).col1 where tab2.col2=v_t1(i).col2;
commit;
forall i in 1..v_t1.count
insert into tab3 values(v_t1(i).col1,v_t1(i).col2,v_t1(i).col3);
commit;
end loop;
close c2;
end;
For around 20k of these records, the first delete operation is processed correctly but subsequent update and insert is not processed. For the remaining 40k records all three operations are processed correctly.
Am I missing something? Also what is the maximum LIMIT value I can use with Bulk Collect?
You should try using SAVE EXCEPTIONS clause of FORALL, something like (untested):
create or replace procedure myproc
as
cursor c1 is select col1,col2,col3 from tab1 where col4=3;
type t1 is table of c1%rowtype;
v_t1 t1;
dml_errors EXCEPTION;
PRAGMA exception_init(dml_errors, -24381);
l_errors number;
l_errno number;
l_msg varchar2(4000);
l_idx number;
begin
open c1;
loop
fetch c1 bulk collect into v_t1 limit 1000;
-- process v_t1 data
BEGIN
forall i in 1..v_t1.count SAVE EXCEPTIONS
delete from tab2 where tab2.col1=v_t1(i).col1;
commit;
EXCEPTION
when DML_ERRORS then
l_errors := sql%bulk_exceptions.count;
for i in 1 .. l_errors
loop
l_errno := sql%bulk_exceptions(i).error_code;
l_msg := sqlerrm(-l_errno);
l_idx := sql%bulk_exceptions(i).error_index;
-- log these to a table maybe, or just output
end loop;
END;
exit when c1%notfound;
end loop;
close c2;
end;

passing cursor to a function does not work always

I have the below procedure and the functions, I pass the cursor result as an argument to the function load_in_parallel. The issue is that sometimes the entries are processed and other time it doesn't. I really don't understand why it happens. Could it be due to the size of the result? Please see the PL/SQL procedure and function as below,
procedure COPY_REPORT_CONTENTS is
begin
begin
-- START PARALLEL processing from here
for rec in (
select /*+ parallel(5) */ * from table(load_in_parallel(CURSOR(
select gen_report_id, creation_ts, selection_criteria, content from stage_table stgrep where not exists(select 1 from content rep where rep.report_id = stgrep.gen_report_id) and nvl(stgrep.gen_report_id, 0) <> 0)))
)
LOOP
--some processing here
END LOOP;
exception
when others then
ROLLBACK;
raise;
end;
end;
function load_in_parallel (c_pis_a1 IN SYS_REFCURSOR)
return vdps_load_stats
PARALLEL_ENABLE (PARTITION c_pis_a1 BY ANY)
PIPELINED
IS
PRAGMA AUTONOMOUS_TRANSACTION;
begin
loop
begin
fetch c_pis_a1 into r_pis_a1;
exit when c_pis_a1%notfound;
--some processing
commit;
exception
when others then
rollback;
--move the records to failed table and continue with next records
commit;
end;
end loop;
close c_pis_a1;
PIPE ROW(v_vdps_load_stats);
RETURN;
end;
Any help will really be appreciated.
Note: included the missing exception block.

Oracle, drop table if it exists AND empty

I need to drop an Oracle table only if it 1) exists AND 2) Is NOT Empty
I wrote this code but if the table does not exist the code does not work:
DECLARE
rec_cnt1 NUMBER :=0;
rec_cnt2 NUMBER :=0;
BEGIN
SELECT COUNT(*) INTO rec_cnt1 FROM ALL_TABLES WHERE TABLE_NAME = 'MyTable';
SELECT num_rows INTO rec_cnt2 FROM USER_TABLES WHERE TABLE_NAME = 'MyTable';
IF rec_cnt1 = 1 THEN
BEGIN
IF rec_cnt2 < 1 THEN
EXECUTE IMMEDIATE 'DROP TABLE MyTable cascade constraints';
END IF;
END;
END IF;
END;
/
What am I doing wrong? Please help.
Many thanks in advance
If you want to drop a table if it exists and empty(as the title of the question states) you could do this as follows:
create or replace procedure DropTableIfEmpty(p_tab_name varchar2)
is
l_tab_not_exists exception;
pragma exception_init(l_tab_not_exists, -942);
l_is_empty number;
l_query varchar2(1000);
l_table_name varchar2(32);
begin
l_table_name := dbms_assert.simple_sql_name(p_tab_name);
l_query := 'select count(*)
from ' || l_table_name ||
' where rownum = 1';
execute immediate l_query
into l_is_empty;
if l_is_empty = 0
then
execute immediate 'drop table ' || l_table_name;
dbms_output.put_line('Table "'|| p_tab_name ||'" has been dropped');
else
dbms_output.put_line('Table "'|| p_tab_name ||'" exists and is not empty');
end if;
exception
when l_tab_not_exists
then dbms_output.put_line('Table "'|| p_tab_name ||'" does not exist');
end;
When you are trying to drop a table, or query a table, which does not exist, Oracle will raise ORA-00942 exception and execution of a pl/sql block halts. We use pragma exception_init statement to associate ORA-00942 exception with our locally defined exception l_tab_not_exists in order to handle it appropriately.
Test case:
SQL> exec droptableifempty('tb_test'); -- tb_test table does not exists
Table "tb_test" does not exist
SQL> create table tb_test(
2 col number
3 );
table TB_TEST created.
SQL> exec droptableifempty('tb_test');
Table "tb_test" has been dropped
As a side note. Before querying num_rows column of [dba][all][user]_tables in order to determine number of rows a table has, you need to gather table statistic by executing dbms_stats.gather_table_stats(user, '<<table_name>>');, otherwise you wont get the actual number of rows.
In PL/SQL it is 'normal' to catch the exception.
If it is the correct exception then continue with the next part of your code.
DECLARE
rec_cnt1 NUMBER :=0;
rec_cnt2 NUMBER :=0;
BEGIN
SELECT COUNT(*) INTO rec_cnt1 FROM ALL_TABLES WHERE TABLE_NAME = 'MyTable';
SELECT num_rows INTO rec_cnt2 FROM USER_TABLES WHERE TABLE_NAME = 'MyTable';
IF rec_cnt1 = 1 THEN
BEGIN
IF rec_cnt2 < 1 THEN
EXECUTE IMMEDIATE 'DROP TABLE MyTable cascade constraints';
END IF;
END;
END IF;
EXCEPTION
DBMS_OUTPUT.PUT_LINE('OH DEAR AN EXCEPTION WAS THROWN DUE TO' || SQLERRM);
DBMS_OUTPUT.PUT_LINE('THE ORACLE CODE IS ' || SQLCODE);
-- if it is the oracle code for no such table, or no data selected
-- everything is fine.
END;
Of course it won't work if the table doesn't exist. Your second select would get a "No data found" exception, and you're not doing any exception handling. At least you should move the second select inside the first IF block. Best to add exception handling.
here is an easy way to solve this problem:
BEGIN
EXECUTE IMMEDIATE 'DROP TABLE [sssss]';
EXCEPTION WHEN OTHERS THEN NULL;
END;

Resources