How to catch a unique constraint error in a PL/SQL block? - oracle

Say I have an Oracle PL/SQL block that inserts a record into a table and need to recover from a unique constraint error, like this:
begin
insert into some_table ('some', 'values');
exception
when ...
update some_table set value = 'values' where key = 'some';
end;
Is it possible to replace the ellipsis for something in order to catch an unique constraint error?

EXCEPTION
WHEN DUP_VAL_ON_INDEX
THEN
UPDATE

I'm sure you have your reasons, but just in case... you should also consider using a "merge" query instead:
begin
merge into some_table st
using (select 'some' name, 'values' value from dual) v
on (st.name=v.name)
when matched then update set st.value=v.value
when not matched then insert (name, value) values (v.name, v.value);
end;
(modified the above to be in the begin/end block; obviously you can run it independantly of the procedure too).

I suspect the condition you are looking for is DUP_VAL_ON_INDEX
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
DBMS_OUTPUT.PUT_LINE('OH DEAR. I THINK IT IS TIME TO PANIC!')

As an alternative to explicitly catching and handling the exception you could tell Oracle to catch and automatically ignore the exception by including a /*+ hint */ in the insert statement. This is a little faster than explicitly catching the exception and then articulating how it should be handled. It is also easier to setup. The downside is that you do not get any feedback from Oracle that an exception was caught.
Here is an example where we would be selecting from another table, or perhaps an inner query, and inserting the results into a table called TABLE_NAME which has a unique constraint on a column called IDX_COL_NAME.
INSERT /*+ ignore_row_on_dupkey_index(TABLE_NAME(IDX_COL_NAME)) */
INTO TABLE_NAME(
INDEX_COL_NAME
, col_1
, col_2
, col_3
, ...
, col_n)
SELECT
INDEX_COL_NAME
, col_1
, col_2
, col_3
, ...
, col_n);
This is not a great solution if your goal it to catch and handle (i.e. print out or update the row that is violating the constraint). But if you just wanted to catch it and ignore the violating row then then this should do the job.

Related

Is the insert into ... where not exists costlier in performance?

We are using Oracle database (12c)
A table ABCD has below structure with around o(10^5) rows
CCOL* - Varchar2 column
DCOL* - Timestamp column
Name Null? Type
----- -------- -----------------
CCOL1 NOT NULL VARCHAR2(64 CHAR)
CCOL2 NOT NULL VARCHAR2(30 CHAR)
CCOL3 NOT NULL VARCHAR2(64 CHAR)
DCOL1 NOT NULL TIMESTAMP(6)
CCOL4 NOT NULL VARCHAR2(64 CHAR)
DCOL2 NOT NULL TIMESTAMP(6)
CCOL5 VARCHAR2(32 CHAR)
The primary key on this table is (CCOL1, CCOL2)
We have a below insert statement:
Insert 1:
BEGIN
INSERT INTO abcd (
ccol1, ccol2, ccol3, dcol1, ccol4, dcol2, ccol5)
SELECT
:b1, :b2, :b3, :b4, :b5, :b6, :b7
FROM
dual
WHERE
NOT EXISTS (
SELECT 1 FROM abcd WHERE ccol1 = :b1 AND ccol2 = :b2 );
EXCEPTION
WHEN dup_val_on_index THEN
NULL;
WHEN OTHERS THEN
RAISE;
END;
And alternative to this we have (removed the where not exists part)
Insert 2:
BEGIN
INSERT INTO abcd (
ccol1, ccol2, ccol3, dcol1, ccol4, dcol2, ccol5)
VALUES
(:b1, :b2, :b3, :b4, :b5, :b6, :b7);
EXCEPTION
WHEN dup_val_on_index THEN
NULL;
WHEN OTHERS THEN
RAISE;
END;
Which of these inserts is better?
I assume you're asking because you call this piece of code a large number of times. Generally, which approach is faster will depend on how probable it is that the values you're inserting already exist in the table. Throwing and catching an exception is orders of magnitude slower than checking that a primary key value does not already exist. But if you're only going to throw the exception once on every million inserts, the second approach is likely to be more efficient. When I had something like this in the past, there were also significant differences between Oracle versions in terms of how much more costly it was to throw and catch exceptions so the exact break-even point will vary depending on the Oracle version as well. Realistically, you'd need to benchmark on your system to be sure.
Personally, I'd probably write this (as #Boneist suggest) as a MERGE with just a when not matched clause
MERGE INTO abcd dest
USING( SELECT :b1 ccol1, :b2 ccol2, :b3 ccol3, ...
FROM dual ) src
ON( src.ccol1 = dest.ccol1 AND
src.ccol2 = dest.ccol2 )
WHEN NOT MATCHED THEN
INSERT( ccol1, ccol2, ccol3, ... )
VALUES( src.ccol1, src.ccol2, src.ccol3, ... )
Separately, having a WHEN OTHERS exception handler that just does a RAISE doesn't make much sense-- just don't catch the exception if you aren't going to do anything with it. In your first piece of code, it doesn't make sense to catch and ignore the dup_val_on_index check because you've already got the NOT EXISTS clause. At that point, the dup_val_on_index is no longer an expected exception so you shouldn't be catching and ignoring it.

Oracle Merge Append hint causes ORA-01400

I have an error I'm failing to understand. This is happening on some production code, but I created dummy tables to simulate the issue.
create table dl_test_nullable_src(col1 varchar2(20) NOT NULL, col2 varchar2(20) NOT NULL);
create table dl_test_nullable_tgt(col1 varchar2(20) NOT NULL, col2 varchar2(20) NOT NULL);
begin
for i in 1..10 loop
insert into dl_test_nullable_src(col1,col2) values(i*100,i*300);
insert into dl_test_nullable_tgt(col1,col2) values(i*100,i*300);
end loop;
commit;
end;
The below works
merge into dl_test_nullable_tgt tgt
using
(select * from dl_test_nullable_src) src
on (tgt.col1 = src.col1)
when matched then update
set tgt.col2 = src.col2
when not matched then insert
(tgt.col2)
values
(src.col2);
10 rows merged.
Now I add some parallel and append to direct path load any NEW records.
alter session enable parallel dml;
merge /*+ append parallel(tgt,4) */ into dl_test_nullable_tgt tgt
using
(select * from dl_test_nullable_src) src
on (tgt.col1 = src.col1)
when matched then update
set tgt.col2 = src.col2
when not matched then insert
(tgt.col2)
values
(src.col2);
SQL Error: ORA-01400: cannot insert NULL into ("PBTUSER5"."DL_TEST_NULLABLE_TGT"."COL1")
01400. 00000 - "cannot insert NULL into (%s)"
*Cause: An attempt was made to insert NULL into previously listed objects.
*Action: These objects cannot accept NULL values.
Even though this doesn't make sense on its own, it should not try to be inserting any records since everything that's in the SRC table is in the TGT table. The only way to bypass this is to also add col1 in the insert and values clause.
Table Contents
as far as I can see this is described in MOS document 1547251.1 - and classified as "not a bug, but a feature". The difference is based on the evaluation of contraints in parallel and non-parallel execution.

Oracle 11g - Is it possible to determine update or insert in a function / procedure?

I am working with Oracle 11g and I got a question.
What I want to do is :
make a procedure
when this procedure has called, it will move data form one table to the other (column is almost same, but has different primary key)
Since, they do not use same field as PK, it occurs an error if use insert statement only.
So I want to do if table has a key than update, otherwise insert.
i.e. DDL like follow. It has almost same, but pk.
create table Tbl_A (
a_pk number constraints pk_tbl_a primary key
, b_pk number
, some_text varchar2(10)
, created date
, changed date
);
create table Tbl_B (
a_pk number
, b_pk number constraints pk_tbl_b primary key
, some_text varchar2(10)
, created date
, changed date
);
Psuedo of what I want :
create or replace procedure mv_data
is
begin
case when [if Tbl_B has same b_pk] then [update statement] end
else [create statement] end;
commit;
end;
I know I can not use case when like above, but that what I am trying to achieve is something like that. MyBatis could be a solution, but the client want to this with only DB.(Actually, this job will be executed by Oracle DBMS_SCHEDULE)
Thanks for your kind answers :D
As an alternative to the MERGE solution as suggested by valentin you can use the following construction:
begin
-- try the insert
insert...
exception
when dup_val_on_index
then
update...
end;
You want to use MERGE
Some references:
https://oracle-base.com/articles/9i/merge-statement
http://psoug.org/reference/merge.html

Merge function error - On Clause cannot be updated - PK issue

Procedure is to check the the eid ,and do merge. While updating the existing row, it needs to update with the eid.seq.nextval.
I have created a the sequence and calling in Procedure.
CREATE OR REPLACE PROCEDURE Temp(
Eid Number,
dpt varchar2
) As
BEGIN
MERGE INTO Src1 e
USING (select v_eid as eid
, v_dept as dept
FROM dual) d
ON (e.eid = d.eid)
WHEN NOT MATCHED THEN
INSERT (e.eid,e.dept)
VALUES(d.eid, d.dept)
WHEN MATCHED THEN
UPDATE SET e.eid = eid_SEQ.nextval, e.dept = d.dept;
END;
/
Error:
1.ORA--38104:ORA-38104: Columns referenced in the ON Clause cannot be updated.
IF I remove the ON clause condition then PK cannot be null error .
Also, the best procedure to call the seq.nextval in the procedure.
Any help is greatly appreciated!
What you want to do is impossible with a merge statement. Think about it: you're trying to update the value of a field that is used to determine if the field should be updated. This is not safe, especially if the merge statement updates/inserts more than one row.
Apart from that it is close to impossible to suggest an alternative since eid seems to be a primary key and updating primary keys is normally a bad thing.

Capture values that trigger DUP_VAL_ON_INDEX

Given this example (DUP_VAL_ON_INDEX Exception), is it possible to capture the values that violated the constraint so they may be logged?
Would the approach be the same if there are multiple violations generated by a bulk insert?
BEGIN
-- want to capture '01' and '02'
INSERT INTO Employee(ID)
SELECT ID
FROM (
SELECT '01' ID FROM DUAL
UNION
SELECT '02' ID FROM DUAL
);
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
-- log values here
DBMS_OUTPUT.PUT_LINE('Duplicate value on an index');
END;
Ideally, I would suggest using DML error logging. For example
Create the error log table
begin
dbms_errlog.create_error_log( dml_table_name => 'EMPLOYEE',
err_log_table_name => 'EMPLOYEE_ERR' );
end;
Use DML error logging
BEGIN
insert into employee( id )
select id
from (select '01' id from dual
union all
select '02' from dual)
log errors into employee_err
reject limit unlimited;
END;
For every row that fails, this will log the data for the row into the EMPLOYEE_ERR table along with the exception. You can then query the error log table to see all the errors rather than getting just the first row that failed.
If creating the error log table isn't an option, you could move from SQL to PL/SQL with bulk operations. That will be slower but you could use the SAVE EXCEPTIONS clause of the FORALL statement to create a nested table of exceptions that you could then iterate over.
For people who would be interested to know more about this, please go through this link.

Resources