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

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>

Related

is there a way to automatically update a column value when a new record is inserted into an oracle sql table

we have an api operation that enters a row into our table with a report_type=5, is there some sort of operation i can apply to the table to make it so whenever a record gets entered or pulled
with a report_id=12 it returns the report_type as 4?
As commented, trigger would do. Here's an example.
Sample table:
SQL> create table test
2 (report_id number,
3 report_type number);
Table created.
Trigger:
SQL> create or replace trigger trg_bi_test
2 before insert on test
3 for each row
4 when (new.report_id = 12)
5 begin
6 :new.report_type := 4;
7 end;
8 /
Trigger created.
Testing:
SQL> insert into test (report_id, report_type) values (1, 13);
1 row created.
SQL> insert into test (report_id, report_type) values (12, 99);
1 row created.
SQL> select * from test;
REPORT_ID REPORT_TYPE
---------- -----------
1 13
12 4 --> I inserted report_type = 99, but trigger modified it to 4
--> because report_id = 12
SQL>
It's not clear which value you want to be STORED in the database: 12 (as entered), or 4 (as translated).
A trigger as proposed by another commenter would certainly be able to translate the value on insert or update.
If you want the original value to be stored, you'd need to set up a different column, that is derived based on the original one. An example swiped from an Oracle publication:
create table PERSON (
(employee_id integer,
employee_id_disp computed by
SUBSTRING (CAST(employee_id + 100000 as VARCHAR (6)) from 2)
);
In your case, you might do something like
create table MYTABLE (
somekey varchar(20) not null,
entered_office int,
display_office computed by decode(entered_office,12,4,entered_office)
);
Then, anything that needs to display the office number would need to use the display_office field, not the entered one. Any tool that does an insert into the table would also need to insert the entered_office field, as display_office is not updateable.

oracle concurrent select for update and insert

I have a table with two columns: k (primary key) and value.
I'd like to:
select for update by k, if k is not found, insert a new row with a default value.
with the returned value ( existent or new inserted row value) make some processing.
update the row and commit.
Is it possible to make this "select for update and insert default value if not found"?
If implement (1) as a select/check if found/insert if not found, we have concurrency problems, since two sessions could make the select concurrently on non existent key, both will try to insert and one of the instances will fail.
In this case the desired behavior is to perform atomically the select/insert and one of the instance perform it and the second one keep locked until the first one commits, and then use the value inserted by the first one.
We implement it always doing an "insert ... if not exist.../commit" before the "select for update" but this implies always trying to insert when it is a unlikely needed.
Is there any way to implement it on one sql sentence?
Thanks!!
See if k is available
SELECT * FROM table
WHERE k = value
FOR UPDATE
If no rows returned, then it doesn't exist. Insert it:
INSERT INTO table(k, col1, col2)
VALUES (value, val1, default))
select ... for update is the first step you should make; without it, you can't "reserve" that row for further processing (unless you're willing to lock the whole table in exclusive mode; if that "processing" takes no time, that could also be an option, especially if there are not many users who will be doing it).
If row exists, the rest is simple - process it, update it, commit.
But, if it doesn't exist, you'll have to insert a new row (just as you said), and here's a problem of two (or more) users inserting the same value.
To avoid it, create a function which
will return unique ID value for a new row
is an autonomous transaction
why? Because you're performing DML in it (update or insert), and you can't do that in a function unless it is an autonomous transaction
Users will have to use that function to get the next ID value. Here's an example: you'll need a table (my_id) which holds the last used ID (and every user who accesses it via the function will create a new value).
Table:
SQL> create table my_id (id number);
Table created.
Function:
SQL> create or replace function f_id
2 return number
3 is
4 pragma autonomous_transaction;
5 l_nextval number;
6 begin
7 select id + 1
8 into l_nextval
9 from my_id
10 for update of id;
11
12 update my_id set
13 id = l_nextval;
14
15 commit;
16 return (l_nextval);
17
18 exception
19 when no_data_found then
20 lock table my_id in exclusive mode;
21
22 insert into my_id (id)
23 values (1);
24
25 commit;
26 return(1);
27 end;
28 /
Function created.
Use it as
SQL> select f_id from dual;
F_ID
----------
1
SQL>
That's it ... code you'll use will then be something like this:
SQL> create table test
2 (id number constraint pk_test primary key,
3 name varchar2(10),
4 datum date
5 );
Table created.
SQL> create or replace procedure p_test (par_id in number)
2 is
3 l_id test.id%type;
4 begin
5 select id
6 into l_id
7 from test
8 where id = par_id
9 for update;
10
11 update test set datum = sysdate where id = par_id;
12 exception
13 when no_data_found then
14 insert into test (id, name, datum)
15 values (f_id, 'Little', sysdate); --> function call is here
16 end;
17 /
Procedure created.
SQL> exec p_test (1);
PL/SQL procedure successfully completed.
SQL> select * from test;
ID NAME DATUM
---------- ---------- -------------------
1 Little 04.09.2021 20:49:21
SQL> exec p_test (1);
PL/SQL procedure successfully completed.
SQL> select * from test;
ID NAME DATUM
---------- ---------- -------------------
1 Little 04.09.2021 20:49:21 --> row was inserted
SQL> exec p_test (1);
PL/SQL procedure successfully completed.
SQL> select * from test;
ID NAME DATUM
---------- ---------- -------------------
1 Little 04.09.2021 20:49:30 --> row was updated
SQL>
Use a sequence to generate a surrogate primary key instead of using a natural key. If you had a real natural key, then it would be extremely unlikely for two users to submit the same value at the same time.
There are several ways to automatically generate primary keys. I prefer to use sequence defaults, like this:
create sequence test_seq;
create table test1
(
k number default test_seq.nextval,
value varchar2(4000),
constraint test1_pk primary key(k)
);
If you can't switch to a surrogate key or a real natural key:
Change the "insert ... if not exist.../commit" to a simpler "insert ... if not exist", and perform all operations in a single transaction. Inserting the same primary key in different sessions, even uncommitted, is impossible in Oracle. Although the SELECT won't cause a block, the INSERT will. This behavior is an exception to Oracle's implementation of isolation, the "I" in "ACID", and in this case that behavior can work to your advantage.
If two sessions attempt to insert the same primary key at the same time, the second session will hang, and when the first session finally commits, the second session will fail with the exception "ORA-00001: unique constraint (X.Y) violated". Let that exception become your flag for knowing when a user has submitted a duplicate value. You can catch the exception in the application and have the user try again.

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.

How to handle Oracle Error [ Unique Constraint ] error

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;

What is the syntax to use a Select statement inside a PL/SQL Trigger?

This is what I currently have:
CREATE OR REPLACE TRIGGER MYTRIGGER
AFTER INSERT ON SOMETABLE
FOR EACH ROW
DECLARE
v_emplid varchar2(10);
BEGIN
SELECT
personnum into v_emplid
FROM PERSON
WHERE PERSONID = :new.EMPLOYEEID;
dbms_output.put(v_emplid);
/* INSERT INTO SOMEOTHERTABLE USING v_emplid and some of the other values from the trigger table*/
END MYTRIGGER;
DBA_ERRORS has this error:
PL/SQL: ORA-00923: FROM keyword not found where expected
1) There must be something else to your example because that sure seems to work for me
SQL> create table someTable( employeeid number );
Table created.
SQL> create table person( personid number, personnum varchar2(10) );
Table created.
SQL> ed
Wrote file afiedt.buf
1 CREATE OR REPLACE TRIGGER MYTRIGGER
2 AFTER INSERT ON SOMETABLE
3 FOR EACH ROW
4 DECLARE
5 v_emplid varchar2(10);
6 BEGIN
7 SELECT personnum
8 into v_emplid
9 FROM PERSON
10 WHERE PERSONID = :new.EMPLOYEEID;
11 dbms_output.put(v_emplid);
12 /* INSERT INTO SOMEOTHERTABLE USING v_emplid and some of the other values
from the trigger table*/
13* END MYTRIGGER;
14 /
Trigger created.
SQL> insert into person values( 1, '123' );
1 row created.
SQL> insert into sometable values( 1 );
1 row created.
2) You probably want to declare V_EMPLID as being of type Person.PersonNum%TYPE so that you can be certain that the data type is correct and so that if the data type of the table changes you won't need to change your code.
3) I assume that you know that your trigger cannot query or update the table on which the trigger is defined (so no queries or inserts into someTable).
You are playing with Lava (not just fire) in your trigger. DBMS_OUTPUT in a trigger is really, really bad. You can blow-out on a buffer overflow in your trigger and the whole transaction is shot. Good luck tracking that down. If you must do output-to-console like behavior, invoke an AUTONOMOUS TRANSACTION procedure that writes to a table.
Triggers are pretty evil. I used to like them, but they are too hard to remember about. They affect data often times leading to MUTATING data (scary and not just because Halloween is close).
We use triggers to change the value of columns like .new:LAST_MODIFIED := sysdate and .new:LAST_MODIFIED_BY := user. That's it.
Don't ever allow a TRIGGER to prevent a transaction from completing. Find another option.
I would not use a select statment in a trigger ever. Insert into the table rather than a select into. Once the table already exists select into does not work in most databases.