50

I need to be able to run an Oracle query which goes to insert a number of rows, but it also checks to see if a primary key exists and if it does, then it skips that insert. Something like:

INSERT ALL
    IF NOT EXISTS( SELECT 1 WHERE fo.primary_key='bar' )
    (
        INSERT INTO 
            schema.myFoo fo ( primary_key, value1, value2 )
        VALUES
            ('bar','baz','bat')
    ),
    
    IF NOT EXISTS( SELECT 1 WHERE fo.primary_key='bar1' )
    (
        INSERT INTO 
            schema.myFoo fo ( primary_key, value1, value2 )
        VALUES
            ('bar1','baz1','bat1')
    )
SELECT * FROM schema.myFoo;

Is this at all possible with Oracle?

Bonus points if you can tell me how to do this in PostgreSQL or MySQL.

ZygD
  • 22,092
  • 39
  • 79
  • 102
cwallenpoole
  • 79,954
  • 26
  • 128
  • 166
  • I like to use CTE - Common Table Expressions [Oracle: how to INSERT if a row doesn't exist](http://stackoverflow.com/questions/3841441/oracle-how-to-insert-if-a-row-doesnt-exist/39704301#39704301) – it3xl Sep 26 '16 at 14:16

10 Answers10

37

Coming late to the party, but...

With oracle 11.2.0.1 there is a semantic hint that can do this: IGNORE_ROW_ON_DUPKEY_INDEX

Example:

insert /*+ IGNORE_ROW_ON_DUPKEY_INDEX(customer_orders,pk_customer_orders) */
  into customer_orders
       (order_id, customer, product)
values (    1234,     9876,  'K598')
     ;

UPDATE: Although this hint works (if you spell it correctly), there are better approaches which don't require Oracle 11R2:

First approach—direct translation of above semantic hint:

begin
  insert into customer_orders
         (order_id, customer, product)
  values (    1234,     9876,  'K698')
  ;
  commit;
exception
  when DUP_VAL_ON_INDEX
  then ROLLBACK;
end;

Second aproach—a lot faster than both above hints when there's a lot of contention:

begin
    select count (*)
    into   l_is_matching_row
    from   customer_orders
    where  order_id = 1234
    ;

    if (l_is_matching_row = 0)
    then
      insert into customer_orders
             (order_id, customer, product)
      values (    1234,     9876,  'K698')
      ;
      commit;
    end if;
exception
  when DUP_VAL_ON_INDEX
  then ROLLBACK;
end;
Michael Deardeuff
  • 10,386
  • 5
  • 51
  • 74
  • 1
    I like the second approach 'cause it's clear and easy to understand what one is trying to done. – Captain Sensible Sep 30 '11 at 09:04
  • Looking for an alternative of how I was tackling that situation, I came across your answer that reinforces that my approach was correct. I implemented the second approach, and it is clear and fast! +1 –  Feb 10 '15 at 07:48
  • A tip for those, who want to access objects created through static SQL (Not PL/SQL, which is dynamic SQL). For example, if you want to get NEXTVAL from sequence, which you created outside the PL/SQL "BEGIN END;" block. Do "DECLARE seq_value NUMBER;" and then "SELECT employees_seq.NEXTVAL INTO seq_value FROM dual;" https://docs.oracle.com/cd/B19306_01/appdev.102/b14261/sqloperations.htm#i7112 – Peeter Kokk Jul 06 '17 at 09:47
  • You say the second approach is faster. Is there any number to back up your claim? – Just a HK developer Nov 21 '18 at 01:51
  • @user3454439 the speedup depends on your use case; if you don't have as much contention then you may not even care. Profile! – Michael Deardeuff Nov 21 '18 at 03:17
30

The statement is called MERGE. Look it up, I'm too lazy.

Beware, though, that MERGE is not atomic, which could cause the following effect (thanks, Marius):

SESS1:

create table t1 (pk int primary key, i int);
create table t11 (pk int primary key, i int);
insert into t1 values(1, 1);
insert into t11 values(2, 21);
insert into t11 values(3, 31);
commit;

SESS2: insert into t1 values(2, 2);

SESS1:

MERGE INTO t1 d
USING t11 s ON (d.pk = s.pk)
WHEN NOT MATCHED THEN INSERT (d.pk, d.i) VALUES (s.pk, s.i);

SESS2: commit;

SESS1: ORA-00001

erikkallen
  • 33,800
  • 13
  • 85
  • 120
  • 1
    Again, without locking the table(or the master record first), there is a race. This method always requires using a temp table. I wouldn't say it's wrong, but sometimes can be just too much. – Marius Burz Nov 09 '09 at 19:45
  • No, the merge should be atomic. – erikkallen Nov 09 '09 at 20:07
  • 1
    Yes, MERGE itself is atomic. But... Sess1: INSERT pk=1 INTO myFoo; Sess2: MERGE INTO myFoo d USING tmpTable s ON (d.pk = s.pk)... Sess1: COMMIT; Sess2: ORA-00001; For cases when the number of rows inserted is low, it really doesn't make sense to use a temp table. Everything has its price, and CREATE TABLE and MERGE don't come cheap(look at the required latches/locks and the like). – Marius Burz Nov 09 '09 at 21:08
  • 1
    You don't need a temp table. If you only have a few rows, (SELECT 1 FROM dual UNION SELECT 2 FROM dual) will do. Why would your example give ORA-0001? Wouldn't merge take the update lock on the index key and not continue until Sess1 has either committed or rolled back? – erikkallen Nov 09 '09 at 21:37
  • Erik, please see the answer below. There wasn't enough space to post it as a comment, nor was any formatting available. – Marius Burz Nov 09 '09 at 22:17
  • Under the read commited isolation level the MERGEing session cannot see the inserted row from the other session, therefore it attempts an insert which is then blocked waiting for the other session to commit or rollback. A commit causes a PK violation to be raised. entirely correct behaviour. – David Aldridge Nov 10 '09 at 10:09
  • 100% correct behavior. Where did I write something else? Not all people out there know about what I wrote, that's why I decided to write about it. – Marius Burz Nov 10 '09 at 12:41
  • I don't have any problem with the information you wrote about the MERGE, I was just supplying some "theoretical" background to clarify that it is the correct behaviour, not (as erik suggested) a bug. It is of course exactly the same behaviour in theory as two sessions attempting to insert duplicate PK values. – David Aldridge Nov 10 '09 at 13:47
  • @David: I understand this. However, MERGE could also try to take the update lock on all keys it uses, rather than (as it obviously does) take a read lock to determine which operation to perform, and then take the update lock to perform the operation. – erikkallen Nov 10 '09 at 14:01
  • I might not be understanding your point correctly Erik, but it will obviously take out a lock on records that it matches because it will update them, while there really isn't a row-level read lock (other than SELECT ... FOR UPDATE). If I had to guess I'd say that a MERGE takes an immediate row-level exclusive lock on every matching row. – David Aldridge Nov 10 '09 at 14:10
23

We can combine the DUAL and NOT EXISTS to achieve your requirement:

INSERT INTO schema.myFoo ( 
    primary_key, value1, value2
) 
SELECT
    'bar', 'baz', 'bat' 
FROM DUAL
WHERE NOT EXISTS (
    SELECT 1 
    FROM schema.myFoo
    WHERE primary_key = 'bar'
);
Koen
  • 461
  • 2
  • 5
  • 16
Raymond Chiu
  • 934
  • 9
  • 15
18

This only inserts if the item to be inserted is not already present.

Works the same as:

if not exists (...) insert ... 

in T-SQL

insert into destination (DESTINATIONABBREV) 
  select 'xyz' from dual 
  left outer join destination d on d.destinationabbrev = 'xyz' 
  where d.destinationid is null;

may not be pretty, but it's handy :)

Kirk Woll
  • 76,112
  • 22
  • 180
  • 195
AlanG
  • 181
  • 1
  • 2
15

If you do NOT want to merge in from an other table, but rather insert new data... I came up with this. Is there perhaps a better way to do this?

MERGE INTO TABLE1 a
    USING DUAL
    ON (a.C1_pk= 6)
WHEN NOT MATCHED THEN
    INSERT(C1_pk, C2,C3,C4)
    VALUES (6, 1,0,1);
Nikita
  • 4,576
  • 1
  • 14
  • 11
W_O_L_F
  • 1,049
  • 1
  • 9
  • 16
5

It that code is on the client then you have many trips to the server so to eliminate that.

Insert all the data into a temportary table say T with the same structure as myFoo

Then

insert myFoo
  select *
     from t
       where t.primary_key not in ( select primary_key from myFoo) 

This should work on other databases as well - I have done this on Sybase

It is not the best if very few of the new data is to be inserted as you have copied all the data over the wire.

mmmmmm
  • 32,227
  • 27
  • 88
  • 117
  • Definitely cleaver +1. I'd have to create a temporary table first, but that really isn't a terrible difficulty. – cwallenpoole Nov 09 '09 at 19:20
  • inserting this way is SLOW.... BULK COLLECT is a much better option... google it :) It requires some pl-sql, but it's infinitely faster than a blind insert from a select. – Matt May 27 '12 at 03:31
4
DECLARE
   tmp NUMBER(3,1);
BEGIN
  SELECT COUNT(content_id) INTO tmp FROM contents WHERE (condition);
  if tmp != 0 then
    INSERT INTO contents VALUES (...);
  else
    INSERT INTO contents VALUES (...);
  end if;
END;

I used the code above. It is long, but, simple and worked for me. Similar, to Micheal's code.

askmish
  • 6,464
  • 23
  • 42
Selin
  • 41
  • 1
1

If your table is "independent" from others (I mean, it will not trigger a cascade delete or will not set any foreign keys relations to null), a nice trick could be to first DELETE the row and then INSERT it again. It could go like this:

DELETE FROM MyTable WHERE prop1 = 'aaa'; //assuming it will select at most one row!

INSERT INTO MyTable (prop1, ...) VALUES ('aaa', ...);

If your are deleting something which does not exist, nothing will happen.

zetzer
  • 21
  • 1
0

This is an answer to the comment posted by erikkallen:

You don't need a temp table. If you only have a few rows, (SELECT 1 FROM dual UNION SELECT 2 FROM dual) will do. Why would your example give ORA-0001? Wouldn't merge take the update lock on the index key and not continue until Sess1 has either committed or rolled back? – erikkallen

Well, try it yourself and tell me whether you get the same error or not:

SESS1:

create table t1 (pk int primary key, i int);
create table t11 (pk int primary key, i int);
insert into t1 values(1, 1);
insert into t11 values(2, 21);
insert into t11 values(3, 31);
commit;

SESS2: insert into t1 values(2, 2);

SESS1:

MERGE INTO t1 d
USING t11 s ON (d.pk = s.pk)
WHEN NOT MATCHED THEN INSERT (d.pk, d.i) VALUES (s.pk, s.i);

SESS2: commit;

SESS1: ORA-00001

Marius Burz
  • 4,555
  • 2
  • 18
  • 28
  • I don't have access to Oracle, so I can't try it, but I believe you. I do think, though, that this should be considered a bug. – erikkallen Nov 10 '09 at 09:15
  • 2
    this is correct behaviour under the read commited transaction isolation level, not a bug. the behaviour of MERGE is entirely consistent with the behaviour of an update that affects no rows followed by an attempted insert. – David Aldridge Nov 10 '09 at 10:07
  • @David: I realize that those things are equivalend, but I wonder how many people know this. I sure didn't, and I really expected it to work without problem. If I want the semantics of an INSERT which inserts no rows, then an UPDATE, then I write an INSERT and then an UPDATE. – erikkallen Nov 10 '09 at 10:20
  • And why -1 for this? It's (apparently) correct, and it taught me something. +1. – erikkallen Nov 10 '09 at 10:20
  • -1 because it doesn't answer the question, although I agree that it's interesting information. You might add the information into your answer or make yours community wiki so that others can. Also it certainly shouldn't be considered as a bug. – David Aldridge Nov 10 '09 at 11:57
  • If you would have read what I wrote you would have understood this answer was meant to be a reply to Erik not an answer to the original question, which I answered on the other answer I posted here(and which of course you also voted down). – Marius Burz Nov 10 '09 at 12:40
  • I voted this down because it is not an answer to the question, as I mention above, so I did read it in exactly the context that you suggest I didn't. I voted your other answer down because it is horrible RDBMS development practice. – David Aldridge Nov 10 '09 at 13:42
  • OK, I inserted this content into my answer. – erikkallen Nov 10 '09 at 13:58
0

INSERT INTO schema.myFoo ( primary_key        , value1          , value2         )
                         SELECT 'bar1' AS primary_key ,'baz1' AS value1 ,'bat1' AS value2 FROM DUAL WHERE (SELECT 1 AS value FROM schema.myFoo WHERE LOWER(primary_key) ='bar1' AND ROWNUM=1) is null;
RaZieRSarE
  • 89
  • 4