How to handle Oracle Error [ Unique Constraint ] error - sql

I have a table named TABLE_1 which has 3 columns
row_id row_name row_descr
1 check1 checks here
2 check2 checks there
These rows are created through a front end application. Now suppose I delete the entry with row_name check2 from the front end and create another entry from front end with row_name check3, in database my entries will be as follows.
row_id row_name row_descr
1 check1 checks here
3 check3 checks
Now row_id if you observe is not a normal one time increment, Now my problem is i'm writing an insert statement to automate something and i don't know what i should insert in the row_id column. Previously i thought it is just new row_id = old row_id +1. But this is not the case here. Please help
EDIT :
Currently im inserting like this which is Wrong :
insert into TABLE1 (row_id, row_name, row_descr
) values ( (select max (row_id) + 1 from TABLE1),'check1','checks here');
row_id is not a normal one time increment.

Never ever calculate ids by max(id)+1 unless you can absolutly exclude simultaneous actions ( which is almost never ever the case). In oracle (pre version 12 see Kumars answer) create a sequence once and insert the values from that sequences afterwards.
create sequence my_sequence;
Either by a trigger which means you don't have to care about the ids during the insert at all:
CREATE OR REPLACE TRIGGER myTrigger
BEFORE INSERT ON TABLE1 FOR EACH ROW
BEGIN
SELECT my_sequence.NEXTVAL INTO :NEW.row_id FROM DUAL;
END;
/
Or directly with the insert
insert into TABLE1 (row_id, row_name, row_descr
) values ( my_sequence.nextval,'check1','checks here');
Besides using row_id as column name in oracle might be a little confusing, because of the pseudocolumn rowid which has a special meaning.
To anwser your quetstion though: If you really need to catch oracle errors as excpetions you can do this with PRAGMA EXCEPTION INIT by using a procedure for your inserts. It might look somehow like this:
CREATE OR REPLACE PROCEDURE myInsert( [...] )
IS
value_allready_exists EXCEPTION;
PRAGMA EXCEPTION_INIT ( value_allready_exists, -00001 );
--ORA-00001: unique constraint violated
BEGIN
/*
* Do your Insert here
*/
EXCEPTION
WHEN value_allready_exists THEN
/*
* Do what you think is necessary on your ORA-00001 here
*/
END myInsert;

Oracle 12c introduced IDENTITY columns. Precisely, Release 12.1. It is very handy with situations where you need to have a sequence for your primary key column.
For example,
SQL> DROP TABLE identity_tab PURGE;
Table dropped.
SQL>
SQL> CREATE TABLE identity_tab (
2 ID NUMBER GENERATED ALWAYS AS IDENTITY,
3 text VARCHAR2(10)
4 );
Table created.
SQL>
SQL> INSERT INTO identity_tab (text) VALUES ('Text');
1 row created.
SQL> DELETE FROM identity_tab WHERE ID = 1;
1 row deleted.
SQL> INSERT INTO identity_tab (text) VALUES ('Text');
1 row created.
SQL> INSERT INTO identity_tab (text) VALUES ('Text');
1 row created.
SQL> INSERT INTO identity_tab (text) VALUES ('Text');
1 row created.
SQL> DELETE FROM identity_tab WHERE ID = 2;
1 row deleted.
SQL> SELECT * FROM identity_tab;
ID TEXT
---------- ----------
3 Text
4 Text
SQL>
Now let's see what's under the hood -
SQL> SELECT table_name,
2 column_name,
3 generation_type,
4 identity_options
5 FROM all_tab_identity_cols
6 WHERE owner = 'LALIT'
7 /
TABLE_NAME COLUMN_NAME GENERATION IDENTITY_OPTIONS
-------------------- --------------- ---------- --------------------------------------------------
IDENTITY_TAB ID ALWAYS START WITH: 1, INCREMENT BY: 1, MAX_VALUE: 9999999
999999999999999999999, MIN_VALUE: 1, CYCLE_FLAG: N
, CACHE_SIZE: 20, ORDER_FLAG: N
SQL>
So, there you go. A sequence implicitly created by Oracle.
And don't forget, you can get rid off the sequence only with the purge option with table drop.

If you are not worried about which values are causing the error, then you could handle it by including a /*+ hint */ in the insert statement.
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);
Oracle will blow past the redundant row. This is not a great solution if you care about know WHICH row is causing the issue, or anything else. But if you don't care about that and are fine just keeping the first value that was inserted, then this should do the job.

You can use an exception build in which will raise whenever there will be duplication on unique key
DECLARE
emp_count number;
BEGIN
select count(*) into emp_count from emp;
if emp_count < 1 then
insert into emp
values(1, 'First', 'CLERK', '7839', SYSDATE, 1200, null, 30);
dbms_output.put_line('Clerk added');
else
dbms_output.put_line('No data added');
end if;
EXCEPTION
when dup_val_on_index then
dbms_output.put_line('Tried to add row with duplicated index');
END;

Related

How to add a unique constraint which complex conditions in Oracle?

There is a table t1. It has a uuid field (which cannot be made primary key) and a deleted_ts field.
Whenever a new record is going to be added, we should check if "select count(1) from t1 where uuid = [recourd.uuid] and deleted_ts is not null" is 0 or not. If it is not 0, the record should not be added.
The same thing should be done when updating an record.
I think I should use constraint or trigger... but I have investigated it for a while but still don't know how to do it. Any suggestion?
You probably want a unique function-based index
create unique index idx_uuid_uniq_or_del
on t1( case when deleted_ts is not null
then uuid
else null
end );
Note that your description seems odd to me. You seem to want to allow multiple "live" rows with the same uuid value but only one "deleted" row with that value. Normally, it would be the reverse-- you want to ensure that there is only one "live" row per uuid while allowing multiple deleted rows. If that's really what you want
create unique index idx_one_live_uuid
on t1( case when deleted_ts is null
then uuid
else null
end );
If you try to use a single trigger like #Littlefoot shows, that will work so long as you only ever do single-row insert ... values statements. As soon as someone comes along and does a multi-row insert (i.e. an insert ... select)
insert into test( uuid, deleted_ts, name )
select 2, null, 'a' from dual union all
select 2, null, 'b' from dual union all
select 2, 100, 'c' from dual;
the trigger will throw a mutating table exception. You could have a compound trigger with row- and statement-level sections (or a combination of row- and statement-level triggers) to get around the mutating table exception but you'd still have transaction control issues where two sessions are making changes that together violate your rule but neither session can see the violation because they can't see the other session's uncommitted changes. You could work around those transaction control issues by adding some additional locking but now we're getting into a pretty complicated bit of code that is going to be relatively easy for you (or some future developer) to make a mistake in and/or create support or scalability issues. A function-based unique index is much simpler and takes care of these issues automatically.
Would this be OK?
SQL> create table test (uuid number, deleted_ts number, name varchar2(10));
Table created.
SQL> create or replace trigger trg_biu_test
2 before insert or update on test
3 for each row
4 declare
5 l_cnt number;
6 begin
7 select count(*) into l_cnt
8 from test
9 where uuid = :new.uuid
10 and deleted_ts is not null;
11 if l_cnt > 0 then
12 raise_application_error(-20000, 'Error - uniqueness violated');
13 end if;
14 end;
15 /
Trigger created.
Testing (read comments):
SQL> -- OK, first row ever
SQL> insert into test values (1, null, 'a');
1 row created.
SQL> -- OK, as there's no row with a non-empty DELETED_TS
SQL> insert into test values (1, null, 'b');
1 row created.
SQL> -- OK, as this is the first non-empty DELETED_TS for UUID = 1
SQL> insert into test values (1, 100, 'c');
1 row created.
SQL> -- Error, as this is the 2nd non-empty DELETED_TS for UUID = 1
SQL> insert into test values (1, 200, 'd');
insert into test values (1, 200, 'd')
*
ERROR at line 1:
ORA-20000: Error - uniqueness violated
ORA-06512: at "SCOTT.TRG_BIU_TEST", line 9
ORA-04088: error during execution of trigger 'SCOTT.TRG_BIU_TEST'
SQL> -- OK, as this is the first non-empty DELETED_TS for UUID = 2
SQL> insert into test values (2, 300, 'e');
1 row created.
SQL>

In an INSERT, how to make the null values to be zero or null?

I have a table of 10 columns, and my INSERT statement only refers to specific columns in the table.
INSERT INTO SCHEMA.TABLE
(COL_1, COL_2)
VALUES
(VAL_1, VAL_2);
... or...
INSERT INTO SCHEMA.TABLE
(COL_1, COL_2)
SELECT VAL_1, VAL_2 FROM SCHEMA_2.TABLE_2;
However, when I execute it, the other columns are inserted always with a null value, instead of having the corresponding one depending on the column type (i.e. number). This is, if I have a numeric column, I should see a zero.
How can I do that insert properly?
*** Please consider I have no DDL privileges & I'm trying to insert into an existing table.
The easiest approach would probably to give your columns default values:
ALTER TABLE schema.table MODIFY (COL_1 NUMBER DEFAULT 0);
DEFAULT value it is; however, note that you have to pay attention to what you do because column might not get its default value. Here's an example:
SQL> create table test
2 (id number primary key,
3 name varchar2(10),
4 address varchar2(20) default 'Unknown', --> columns with default
5 num_val number default 0 --> values
6 );
Table created.
If you're inserting values without specifying column(s) that are supposed to get default values, everything will be as you'd want it to be:
SQL> insert into test (id, name) values (1, 'Little');
1 row created.
SQL> select * from test;
ID NAME ADDRESS NUM_VAL
---------- ---------- -------------------- ----------
1 Little Unknown 0
See? Both ADDRESS and NUM_VAL got default values.
However, if you mention those columns in INSERT statement, although setting them to NULL, they won't be set to their default values but NULL:
SQL> insert into test (id, name, address, num_val)
2 values (2, 'Foot', null, null);
1 row created.
SQL> select * from test;
ID NAME ADDRESS NUM_VAL
---------- ---------- -------------------- ----------
1 Little Unknown 0
2 Foot
As you can see, row with ID = 2 didn't get default values in ADDRESS and NUM_VAL columns.
Therefore, pay attention to what you do.
USE DEFAULT AS 0 for that column
or
use NVL( column_name, 0 ) --as per oracle syntax
--this would mean whenever theres null found for
-- that column set it to 0 (will work on insert)
or
Update column set column=0 where column IS NULL
--(will work after insert as the name suggests update)
Although frankly I don't recommend doing this, you can use a trigger to accomplish your goal:
CREATE OR REPLACE TRIGGER SCHEMA.TABLE_BI
BEFORE INSERT ON SCHEMA.TABLE
FOR EACH ROW
BEGIN
:NEW.COL_1 := COALESCE(:NEW.COL_1, 0); -- NUMBER column
:NEW.COL_2 := COALESCE(:NEW.COL_2, ' '); -- VARCHAR column
:NEW.COL_3 := COALESCE(:NEW.COL_3, SYSDATE); -- DATE column
END SCHEMA.TABLE_BI;
However, creating a trigger may require privileges you don't have.
To answer the question: that needs to be defined at table creation, determining default values. In this case, I wasn't able to do that because the table definition indicated NULL, even in the case of numbers.
Thanks anyway.

In oracle procedure how to fetch the PKId just added to be added as FK ID for next table

In an oracle procedure, I need to insert data in EmployeeHeader table and then later insert the PK id of this table as the FK id of EmployeeDetails table. How can we achieve this?
INSERT INTO EmployeeHeader(
HEADER_PK_ID
empNo
)
VALUES(
HEADER_PK_ID_SEQ.NEXTVAL,
'SOMETHING'
);
INSERT INTO EmployeeDetails (
DTLHEADER_PK_ID,
HEADER_fK_ID
empname,
age
)
VALUES(
DTLHEADER_PK_ID_SEQ.NEXTVAL,
HEADER_PK_IDn, -- (THIS NEEDS TO BE FETCHED FROM EmployeeHeader)
21
);
You can use currval in most cases:
select HEADER_PK_ID_SEQ.CURRVAL
from dual;
You might need to wrap the two inserts in a single transaction, if you want the values to be safe for concurrent inserts.
Use the RETURNING clause of the INSERT statement:
DECLARE
nHeader_pk_id NUMBER;
BEGIN
INSERT INTO EmployeeHeader
(HEADER_PK_ID, EMPNO)
VALUES
(HEADER_PK_ID_SEQ.NEXTVAL, 'SOMETHING')
RETURNING HEADER_PK_ID INTO nHeader_pk_id;
INSERT INTO EmployeeDetails
(DTLHEADER_PK_ID, HEADER_FK_ID, EMPNAME, AGE)
VALUES
(DTLHEADER_PK_ID_SEQ.NEXTVAL, nHeader_pk_id, 'Somebody', 21);
END;
My personal preference is to use ON INSERT triggers to handle the population of primary key fields, in the following manner:
CREATE OR REPLACE TRIGGER EMPLOYEEHEADER_BI
BEFORE INSERT ON EMPLOYEEHEADER
FOR EACH ROW
BEGIN
IF :NEW.HEADER_PK_ID IS NULL THEN
:NEW.HEADER_PK_ID := HEADER_PK_ID_SEQ.NEXTVAL;
END IF;
END EMPLOYEEHEADER_BI;
CREATE OR REPLACE TRIGGER EMPLOYEEDETAILS_BI
BEFORE INSERT ON EMPLOYEEDETAILS
FOR EACH ROW
BEGIN
IF :NEW.DTLHEADER_PK_ID IS NULL THEN
:NEW.DTLHEADER_PK_ID := DTLHEADER_PK_ID_SEQ.NEXTVAL;
END IF;
END EMPLOYEEDETAILS_BI;
and the INSERT statements become:
DECLARE
nHeader_pk_id NUMBER;
nDtlheader_pk_id NUMBER;
BEGIN
INSERT INTO EmployeeHeader
(EMPNO) -- Note: PK field not mentioned - will be populated by trigger
VALUES
('SOMETHING')
RETURNING HEADER_PK_ID INTO nHeader_pk_id;
INSERT INTO EmployeeDetails
(HEADER_FK_ID, EMPNAME, AGE) -- Note: PK field not mentioned - will be populated by trigger
VALUES
(nHeader_pk_id, 'Somebody', 21)
RETURNING DTLHEADER_PK_ID INTO nDtlheader_pk_id;
END;
(I use the IF pk_field IS NULL THEN construct because I often need to copy data from production to development databases and wish to preserve any key values pulled from production to simplify debugging. If you don't have this requirement you can eliminated the IS NULL check and just assign the sequence's NEXTVAL directly to the column).
Done in this manner the application code doesn't need to know or care about which sequence is used to generate the primary key value for a particular table, and the primary key field is always going to end up populated.
Best of luck.
You can use the RETURNING clause.
Here's an example:
create sequence test_seq1
start with 1
maxvalue 999
minvalue 1
nocycle
cache 20
noorder;
create sequence test_seq2
start with 100
maxvalue 999
minvalue 1
nocycle
cache 20
noorder;
create table test_tab_p (col1 number, col2 number);
create table test_tab_c (col1 number, col2 number, col3 number);
declare
v_p_col2 number := 1;
v_c_col3 number := 10;
v_p_col1 number;
begin
insert into test_tab_p (col1, col2)
values (test_seq1.nextval, v_p_col2)
returning col1 into v_p_col1;
insert into test_tab_c (col1, col2, col3)
values (test_seq2.nextval, v_p_col1, v_c_col3);
commit;
end;
/
select * from test_tab_p;
COL1 COL2
---------- ----------
1 1
select * from test_tab_c;
COL1 COL2 COL3
---------- ---------- ----------
100 1 10
Use currval
INSERT INTO EmployeeHeader
(header_pk_id, empNo)
VALUES
(header_pk_id_seq.nextval, 'SOMETHING');
INSERT INTO EmployeeDetails
(dtlheader_pk_id, header_fk_id, empname, age)
VALUES
(dtlheader_pk_id_seq.nextval, header_pk_id_seq.currval, 21);
currval is safe to use here. It always returns the last value obtained by nextval for the current connection. So even if other transactions (which means a different connection) calls nextval between those two statements, currval will still reflect the value of "this" nextval call.
Quote from the manual:
Each user that references a sequence has access to his or her current sequence number, which is the last sequence generated in the session. A user can issue a statement to generate a new sequence number or use the current number last generated by the session. After a statement in a session generates a sequence number, it is available only to this session.
The very efficient way will be just save the seq id value into a variable and then use it for the whole transaction. Below is the mentioned example. Let me know for any issues.
DECLARE
lv_pkid PLS_INTEGER:=SEQ.NEXTVAL;
BEGIN
INSERT
INTO EmployeeHeader
(
HEADER_PK_ID,
empNo
)
VALUES
(
lv_pkid,
'SOMETHING'
);
INSERT
INTO EmployeeDetails
(
DTLHEADER_PK_ID,
HEADER_fK_ID,
empname,
age
)
VALUES
(
DTLHEADER_PK_ID_SEQ.NEXTVAL,
lv_pkid, -- (THIS NEEDS TO BE FETCHED FROM EmployeeHeader)
21
);
COMMIT;
END;

Different timestamp for different DML queries in single transaction in oracle

I am doing an insert and delete operation on a table in single transaction.I have trigger on this table which update the log.The log has primary key as sequenceId which always gets incremented on insertion.I do delete first and then insert in the transaction.
I have two issues :
The timestamp in the log for the insert and delete is being same. Can I force it to be different.
The order of operation(insert/delete) in the log is getting reversed. It shows delete operation coming after insert operation(according to sequenceId).How can I ensure that the order is consistent in the log(insert after delete).
Example :
create table address (ID number, COUNTRY char(2));
create table address_log(SEQ_ID number, ID number, COUNTRY char(2), DML_TYPE char(1), CHANGE_DATE timestamp(6));
create sequence seq_id start with 1 increment by 100 nominvalue nomaxvalue cache 20 noorder;
create or replace trigger trg_add
before insert or delete on address
FOR EACH ROW
BEGIN
if inserting then
insert into address_log values(SEQ_ID.nextval, :new.ID, :new.COUNTRY, 'I', sysdate);
else
insert into address_log values(SEQ_ID.nextval, :old.ID, :old.COUNTRY, 'D', sysdate);
end if;
end;
insert into address values(1,'US');
insert into address values(2,'CA');
delete from address where id = 1;
insert into address values(3,'UK');
delete from address where id = 3;
if I commit last DML queries in single transaction, then I should see the same order in address_log.
What is the datatype of your timestamp column?
If you use TIMESTAMP with a large enough precision, the order should be preserved.
For example TIMESTAMP(6) (precision to the micro-second) -- which is the default precision:
SQL> CREATE TABLE t_data (ID NUMBER, d VARCHAR2(30));
Table created
SQL> CREATE TABLE t_log (ts TIMESTAMP (6), ID NUMBER, action VARCHAR2(1));
Table created
SQL> CREATE OR REPLACE TRIGGER trg
2 BEFORE INSERT ON t_data
3 FOR EACH ROW
4 BEGIN
5 INSERT INTO t_log VALUES (systimestamp, :NEW.id, 'I');
6 END;
7 /
Trigger created
SQL> INSERT INTO t_data (SELECT ROWNUM, 'x' FROM dual CONNECT BY LEVEL <= 10);
10 rows inserted
SQL> SELECT * FROM t_log ORDER BY ts;
TS ID ACTION
----------------------------- ---------- ------
19/06/13 15:47:51,686192 1 I
19/06/13 15:47:51,686481 2 I
19/06/13 15:47:51,686595 3 I
19/06/13 15:47:51,686699 4 I
19/06/13 15:47:51,686800 5 I
19/06/13 15:47:51,686901 6 I
...
In any case, if you really want to distinguish simultaneous events (concurrent inserts for instance), you can always use a sequence in addition, with the ORDER keyword to guarantee that the rows will be ordered:
CREATE SEQUENCE log_sequence ORDER
This would allow you to have a reliable sort order, even though the events took place at the same time.

Creating a sequence for a varchar2 field in Oracle

I want to create a sequence for this varchar. It would have been easier had it been a number instead of varchar. In that case, I could do
seq_no := seq_no + 1;
But what can I do when I want to store next value in column as A0000002, when the previous value was A0000001 (to increment the number in the next varchar rowby 1)?
This can be done by
to_char(seq_no,'FM0000000')
your example can be done by creating sequence in oracle
create sequence seq_no start with 1 increment by 1;
then
select 'A'||to_char(seq_no.nextval,'FM0000000') from dual;
Right now i have used in dual ..but place this
'A'||to_char(seq_no.nextval,'FM0000000')
in your required query ..this will create sequence as you mentioned
sqlfiddle
Sequences are purely numeric. However, you need a trigger anyway, so simply adapt such trigger to insert the desired prefix:
CREATE OR REPLACE TRIGGER FOO_TRG1
BEFORE INSERT
ON FOO
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
BEGIN
IF :NEW.FOO_ID IS NULL THEN
SELECT 'A' || TO_CHAR(FOO_SEQ1.NEXTVAL, 'FM0000000') INTO :NEW.FOO_ID FROM DUAL;
END IF;
END FOO_TRG1;
/
ALTER TRIGGER FOO_TRG1 ENABLE;
If you're able I'd actually use a virtual column as defined in the CREATE TABLE syntax. It makes it more easily extensible should the need arise.
Here's a working example.
SQL> create table tmp_test (
2 id number(7,0) primary key
3 , col1 number
4 , seq varchar2(8 char) generated always as (
5 'A' || to_char(id, 'FM0999999'))
6 );
Table created.
SQL>
SQL> create sequence tmp_test_seq;
Sequence created.
SQL>
SQL> create or replace trigger tmp_test_trigger
2 before insert on tmp_test
3 for each row
4 begin
5
6 :new.id := tmp_test_seq.nextval;
7 end;
8 /
Trigger created.
SQL> show errors
No errors.
SQL>
SQL> insert into tmp_test (col1)
2 values(1);
1 row created.
SQL>
SQL> select * from tmp_test;
ID COL1 SEQ
---------- ---------- --------------------------------
1 1 A0000001
Having said that; you would be better off if you did not do this unless you have an unbelievably pressing business need. There is little point to making life more difficult for yourself by prepending a constant value onto a number. As A will always be A it doesn't matter whether it's there or not.
If the format is always a letter followed by 7 digits you can do:
sequence = lpad(substr(sequence,2,7)+1,7,'0')