SELECT FOR UPDATE wrong result - sql

i ran into PostgreSQL (probably not only psql) transaction race condition troubles. I'm trying to achieve such a simple task using multiple threads:
BEGIN;
SELECT * FROM t WHERE id = 1;
DELETE FROM t WHERE id = 1;
INSERT INTO t (id, value) VALUES (1, 'thread X'); -- X = 1,2,3,..
SELECT 1 FROM pg_sleep(10); -- only for race condition simulation
COMMIT;
However threads are colliding inside these transactions so multiple inserts are executed (primary key collision error). So i tried to use SELECT FOR UPDATE statement:
BEGIN;
SELECT * FROM t WHERE id = 1 FOR UPDATE;
DELETE FROM t WHERE id = 1;
INSERT INTO t (id, value) VALUES (1, 'thread X'); -- X = 1,2,3,..
SELECT 1 FROM pg_sleep(10); -- only for race condition simulation
COMMIT;
Transactions are correctly blocking on FOR UPDATE statement waiting for other threads commit.
However after "semaphore up" (waking up on that statement after another thread transaction has commited) empty result set is returned from DBMS although data are correctly available in table (from INSERT statement from faster thread):
BEGIN;
SELECT * FROM t WHERE id = 1 FOR UPDATE; -- blocking ... then return 0 records WRONG
SELECT * FROM t WHERE id = 1 FOR UPDATE; -- second try ... returns 1 record CORRECT
DELETE FROM t WHERE id = 1;
INSERT INTO t (id, value) VALUES (1, 'thread X'); -- X = 1,2,3,..
SELECT 1 FROM pg_sleep(10); -- only for race condition simulation
COMMIT;
As seen above, second (duplicated) select statement behaves correctly. Why?

The reason is that the blocked statement's snapshot is older than the transaction that inserted the new row, so it cannot see it once the lock is removed.
You can see it in the following statement because in READ COMMITTED isolation level each statement gets its own snapshot, so the second statement's snapshot includes the newly inserted row.
You could use REPEATABLE READ isolation level. In that case you should get a serialization error (I didn't test that, so please try it out – maybe you need SERIALIZABLE). Then you have to write your program so that it retries the transaction if it gets a serialization error, and everything should work.

Related

SQL: update column counter and insert new row

I have two tables (this is a very simplified model of my use case):
- TableCounter with 2 columns: idEntry, counter
- TableObject with 1 column : idEntry , seq (with the pair idEntry/seq unique)
I need to be able in 1 transaction to:
- increase counter for idEntry = x
- insert (x,new_counter_value) in the TableObject.
knowing that I must not lose any sequence, and it is a transaction highly concurrent and called a lot.
How would you write such a transaction in a statement (not for a stored procedure)? Would you lock the row of TableCounter for idEntry = x?
So far, I have this, but I look for a better solution.
BEGIN TRANSACTION;
SELECT counter FROM TableCounter WHERE idEntry=1 FOR UPDATE;
UPDATE TableCounter SET counter=counter+1 WHERE idEntry=1;
INSERT INTO TableObject(idEntry, seq) SELECT TableCounter.idEntry, TableCounter.counter FROM TableCounter WHERE TableCounter.idEntry = 1;
COMMIT TRANSACTION
Thank you
The select for update is useless if the next thing you do is to update the row anyway (this is true for any DBMS that supports select for update)
For Postgres this can be done in a single statement using a data modifying CTE:
with updated as (
update tablecounter
set counter = counter + 1
where identry = 1
returning identry, counter
)
insert into tableobject (identry, seq)
select identry, counter
from updated;
The update will lock the row, which means that any concurrent insert/update (for the same identry) will have to wait until the above is committed or rolled back.
If I (really) needed a gapless sequence and I could live with the scalability issues of such a solution (because the requirement is more important then performance or scalability) I would probably put that into a function. Something like the following:
Define the sequence (=counter) table
create table gapless_sequence
(
entity text not null primary key,
sequence_value integer not null default 0
);
-- "create" a new sequence
insert into gapless_sequence (entity) values ('some_table');
commit;
Now create a function that claims a new value
create function next_value(p_entity text)
returns integer
as
$$
update gapless_sequence
set sequence_value = sequence_value + 1
where entity = p_entity
returning sequence_value;
$$
language sql;
Same as above: the transaction that acquires the next sequence for an entity will block all subsequent calls to the function for the same entity, until the first transaction is committed (or rolled back).
Now defining a table that uses the gapless sequence is quite easy:
create table some_table
(
id integer primary key default next_value('some_table'),
some_column text
);
And then you simply do:
insert into some_table (some_column) values ('foo');
A concurrent insert into some_table would wait until the first transaction commits. The update will then see the committed value and return the appropriate next sequence value.
Of course this can also be done without using a default clause in the table definition, but then you would need to call the function explicitly in the insert statement:
insert into some_table
(id, some_column)
values
(next_value('some_table'), 'foo');
However that has the potential pitfall that nothing forces you to use the correct entity name when calling the function.
All the examples above assume that auto commit is turned off

Force Oracle to return TOP N rows with SKIP LOCKED

There are a few questions on how to implement a queue-like table (lock specific rows, selecting a certain number of them, and skipping currently locked rows) in Oracle and SQL Server.
How can I guarantee that I retrieve a certain number (N) rows, assuming there are at least N rows eligible?
From what I have seen, Oracle applies the WHERE predicate before determining what rows to skip. This means that if I want to pull one row from a table, and two threads concurrently execute the same SQL, one will receive the row and the other an empty result set (even if there are more eligible rows).
This is contrary to how SQL Server appears to handle the UPDLOCK, ROWLOCK and READPAST lock hints. In SQL Server, TOP magically appears to limit the number of records after successfully attaining locks.
Note, two interesting articles here and here.
ORACLE
CREATE TABLE QueueTest (
ID NUMBER(10) NOT NULL,
Locked NUMBER(1) NULL,
Priority NUMBER(10) NOT NULL
);
ALTER TABLE QueueTest ADD CONSTRAINT PK_QueueTest PRIMARY KEY (ID);
CREATE INDEX IX_QueuePriority ON QueueTest(Priority);
INSERT INTO QueueTest (ID, Locked, Priority) VALUES (1, NULL, 4);
INSERT INTO QueueTest (ID, Locked, Priority) VALUES (2, NULL, 3);
INSERT INTO QueueTest (ID, Locked, Priority) VALUES (3, NULL, 2);
INSERT INTO QueueTest (ID, Locked, Priority) VALUES (4, NULL, 1);
In two separate sessions, execute:
SELECT qt.ID
FROM QueueTest qt
WHERE qt.ID IN (
SELECT ID
FROM
(SELECT ID FROM QueueTest WHERE Locked IS NULL ORDER BY Priority)
WHERE ROWNUM = 1)
FOR UPDATE SKIP LOCKED
Note that the first returns a row, and the second session does not return a row:
Session 1
ID
----
4
Session 2
ID
----
SQL SERVER
CREATE TABLE QueueTest (
ID INT IDENTITY NOT NULL,
Locked TINYINT NULL,
Priority INT NOT NULL
);
ALTER TABLE QueueTest ADD CONSTRAINT PK_QueueTest PRIMARY KEY NONCLUSTERED (ID);
CREATE INDEX IX_QueuePriority ON QueueTest(Priority);
INSERT INTO QueueTest (Locked, Priority) VALUES (NULL, 4);
INSERT INTO QueueTest (Locked, Priority) VALUES (NULL, 3);
INSERT INTO QueueTest (Locked, Priority) VALUES (NULL, 2);
INSERT INTO QueueTest (Locked, Priority) VALUES (NULL, 1);
In two separate sessions, execute:
BEGIN TRANSACTION
SELECT TOP 1 qt.ID
FROM QueueTest qt
WITH (UPDLOCK, ROWLOCK, READPAST)
WHERE Locked IS NULL
ORDER BY Priority;
Note that both sessions return a different row.
Session 1
ID
----
4
Session 2
ID
----
3
How can I get similar behavior in Oracle?
"From what I have seen, Oracle applies the WHERE predicate before determining what rows to skip."
Yup. It is the only possible way. You can't skip a row from a resultset until you have determined the resultset.
The answer is simply not to limit the number of rows returned by the SELECT statement. You can still use the FIRST_ROWS_n hints to direct the optimizer that you won't be grabbing the full data set.
The software calling the SELECT should only select the first n rows. In PL/SQL, it would be
DECLARE
CURSOR c_1 IS
SELECT /*+FIRST_ROWS_1*/ qt.ID
FROM QueueTest qt
WHERE Locked IS NULL
ORDER BY PRIORITY
FOR UPDATE SKIP LOCKED;
BEGIN
OPEN c_1;
FETCH c_1 into ....
IF c_1%FOUND THEN
...
END IF;
CLOSE c_1;
END;
The solution Gary Meyers posted is about all I can think of, short of using AQ, which does all this for you and much more.
If you really want to avoid the PLSQL, you should be able to translate the PLSQL into Java JDBC calls. All you need to do is prepare the same SQL statement, execute it and then keep doing single row fetches on it (or N row fetches).
The Oracle documentation at http://download.oracle.com/docs/cd/B10501_01/java.920/a96654/resltset.htm#1023642 gives some clue how to do this at the statement level:
To set the fetch size for a query, call setFetchSize() on the statement object prior to executing the query. If you set the fetch size to N, then N rows are fetched with each trip to the database.
So you could code up something in Java that looks something like (in Pseudo code):
stmt = Prepare('SELECT /*+FIRST_ROWS_1*/ qt.ID
FROM QueueTest qt
WHERE Locked IS NULL
ORDER BY PRIORITY
FOR UPDATE SKIP LOCKED');
stmt.setFetchSize(10);
stmt.execute();
batch := stmt.fetch();
foreach row in batch {
-- process row
}
commit (to free the locks from the update)
stmt.close;
UPDATE
Based on the comments below, a suggestion was made to use ROWNUM to limit the results received, but that won't work in this case. Consider the example:
create table lock_test (c1 integer);
begin
for i in 1..10 loop
insert into lock_test values (11 - i);
end loop;
commit;
end;
/
Now we have a table with 10 rows. Note that I have carefully inserted the rows in reverse order, the row containing 10 is first, then 9 etc.
Say you want the first 5 rows, ordered ascending - ie 1 to 5. Your first try is this:
select *
from lock_test
where rownum <= 5
order by c1 asc;
Which gives the results:
C1
--
6
7
8
9
10
That is clearly wrong, and is a mistake almost everyone makes! Look at the explain plan for the query:
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 5 | 65 | 4 (25)| 00:00:01 |
| 1 | SORT ORDER BY | | 5 | 65 | 4 (25)| 00:00:01 |
|* 2 | COUNT STOPKEY | | | | | |
| 3 | TABLE ACCESS FULL| LOCK_TEST | 10 | 130 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter(ROWNUM<=5)
Oracle executes the plan from the bottom up - notice that the filter on rownum is carried out before the sort, Oracle takes the rows in the order it finds them (order they were inserted here { 10, 9, 8, 7, 6}), stops after it gets 5 rows, and then sorts that set.
So, to get the correct first 5 you need to do the sort first and then the order by using an inline view:
select * from
(
select *
from lock_test
order by c1 asc
)
where rownum <= 5;
C1
--
1
2
3
4
5
Now, to finally get to the point - can you put a for update skip locked in the correct place?
select * from
(
select *
from lock_test
order by c1 asc
)
where rownum <= 5
for update skip locked;
This gives an error:
ORA-02014: cannot select FOR UPDATE from view with DISTINCT, GROUP BY, etc
Trying to move the for update into the view gives a syntax error:
select * from
(
select *
from lock_test
order by c1 asc
for update skip locked
)
where rownum <= 5;
The only thing that will work is the following, which GIVES THE WRONG RESULT:
select *
from lock_test
where rownum <= 5
order by c1 asc
for update skip locked;
Infact, if you run this query in session 1, and then run it again in session two, session two will give zero rows, which is really really wrong!
So what can you do? Open the cursor and fetch how many rows you want from it:
set serveroutput on
declare
v_row lock_test%rowtype;
cursor c_lock_test
is
select c1
from lock_test
order by c1
for update skip locked;
begin
open c_lock_test;
fetch c_lock_test into v_row;
dbms_output.put_line(v_row.c1);
close c_lock_test;
end;
/
If you run that block in session 1, it will print out '1' as it got a locked the first row. Then run it again in session 2, and it will print '2' as it skipped row 1 and got the next free one.
This example is in PLSQL, but using the setFetchSize in Java you should be able get the exact same behaviour.
In your first session, when you execute:
SELECT qt.ID
FROM QueueTest qt
WHERE qt.ID IN (
SELECT ID
FROM
(SELECT ID FROM QueueTest WHERE Locked IS NULL ORDER BY Priority)
WHERE ROWNUM = 1)
FOR UPDATE SKIP LOCKED
Your inner select attempt to grab only id=4 and lock it. This is successful because this single row is not yet locked.
In second session, your inner select STILL tries to grab ONLY id=4 and lock it. This is not successful because that single row is still locked by first session.
Now, if you updated the "locked" field in first session, the next session to run that select will grab id=3.
Basically, in your example you are depending on a flag that isn't being set. To use your locked flag, you probably mean to do something like:
select IDs you want based on some criteria.
Immediate update the locked flag = 1 for these IDs (if resource busy, another session beat you to this step for 1 or more IDs, goto 1 again)
Do whatever on these IDs
update the locked flag back to null
You can then use your select for update skip locked statement since your locked flag is being maintained.
Personally, I don't like all the updates to flags (your solution may require them for whatever reason), so I'd probably just try to select the IDs I want to update (by whatever criteria) in each session:
select * from queuetest where ... for update skip locked;
For example (in reality, my criteria wouldn't be based on a list of ids, but queuetest table is overly simplistic):
sess 1: select * from queuetest where
id in (4,3) for update skip locked;
sess 2: select * from queuetest where
id in (4,3,2) for update skip locked;
Here sess1 would lock 4,3 and sess2 would lock only 2.
You cannot to my knowledge do a top-n or use group_by/order_by etc in the select for update statement, you'll get a ORA-02014.
My solution - is to write stored procedure like this:
CREATE OR REPLACE FUNCTION selectQueue
RETURN SYS_REFCURSOR
AS
st_cursor SYS_REFCURSOR;
rt_cursor SYS_REFCURSOR;
i number(19, 0);
BEGIN
open st_cursor for
select id
from my_queue_table
for update skip locked;
fetch st_cursor into i;
close st_cursor;
open rt_cursor for select i as id from dual;
return rt_cursor;
END;
This is simple example - returning TOP FIRST non blocked row. To retrieve TOP N rows - replace single fetch into local variable ("i") with loop fetch into temp table.
PS: returning cursor - is for hibernate friendship.
I met this problem, we spend a lot of time to solve it. Some use for update for update skip locked, in oracle 12c, a new method is to use fetch first n rows only. But we use oracle 11g.
Finally, we try this method, and found works well.
CURSOR c_1 IS
SELECT *
FROM QueueTest qt
WHERE Locked IS NULL
ORDER BY PRIORITY;
myRow c_1%rowtype;
i number(5):=0;
returnNum := 10;
BEGIN
OPEN c_1;
loop
FETCH c_1 into myRow
exit when c_1%notFOUND
exit when i>=returnNum;
update QueueTest set Locked='myLock' where id=myrow.id and locked is null;
i := i + sql%rowcount;
END
CLOSE c_1;
commit;
END;
I wirte it in notepad, so something maybe wrong, you can modify it as a procedure or else.
Firstly thanks for top 2 answers ..Learnt a lot from them.I have tested the following code and after running Practicedontdel.java main method ,I found that the two classes prints different rows every time. Please let me know if in any case this code might fail.(P.S : thanks to stack overflow)
Practicedontdel.java:
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs =null;
String val="";
int count =0;
conn = getOracleConnection();
conn.setAutoCommit(false);
ps = prepareStatement(conn,"SELECT /*+FIRST_ROWS_3*/ t.* from
REPROCESS_QUEUE t FOR UPDATE SKIP LOCKED");
ps.setFetchSize(3);
boolean rss = ps.execute();
rs = ps.getResultSet();
new Practisethread().start();
while(count<3 && rs.next())
{
val = rs.getString(1);
System.out.println(val);
count++;
Thread.sleep(10000);
}
conn.commit();
System.out.println("end of main program");
Practisethread.java: in run():
conn = getOracleConnection();
conn.setAutoCommit(false);
ps = prepareStatement(conn,"SELECT /*+FIRST_ROWS_3*/ t.* from REPROCESS_QUEUE t FOR UPDATE SKIP LOCKED");
ps.setFetchSize(3);
boolean rss = ps.execute();
rs = ps.getResultSet();
while(count<3 && rs.next())
{
val = rs.getString(1);
System.out.println("******thread******");
System.out.println(val);
count++;
Thread.sleep(5000);
}
conn.commit();
System.out.println("end of thread program");

Do changes made within one transaction "see" each other?

Suppose I do the following set of SQL queries (pseudocode) in a table with only one column CITY:
BEGIN TRANSACTION;
INSERT INTO MyTable VALUES( 'COOLCITY' );
SELECT * FROM MyTable WHERE ALL;
COMMIT TRANSACTION;
is the SELECT guaranteed to return COOLCITY?
Yes.
The INSERT operation would take an X lock out on at least the newly added row. This won't get released until the end of the transaction thus preventing a concurrent transaction from deleting or updating this row.
A transaction is not blocked by its own locks so the SELECT would return COOLCITY.

Get count of records affected by INSERT or UPDATE in PostgreSQL

My database driver for PostgreSQL 8/9 does not return a count of records affected when executing INSERT or UPDATE.
PostgreSQL offers the non-standard syntax "RETURNING" which seems like a good workaround. But what might be the syntax? The example returns the ID of a record, but I need a count.
INSERT INTO distributors (did, dname) VALUES (DEFAULT, 'XYZ Widgets')
RETURNING did;
I know this question is oooolllllld and my solution is arguably overly complex, but that's my favorite kind of solution!
Anyway, I had to do the same thing and got it working like this:
-- Get count from INSERT
WITH rows AS (
INSERT INTO distributors
(did, dname)
VALUES
(DEFAULT, 'XYZ Widgets'),
(DEFAULT, 'ABC Widgets')
RETURNING 1
)
SELECT count(*) FROM rows;
-- Get count from UPDATE
WITH rows AS (
UPDATE distributors
SET dname = 'JKL Widgets'
WHERE did <= 10
RETURNING 1
)
SELECT count(*) FROM rows;
One of these days I really have to get around to writing a love sonnet to PostgreSQL's WITH clause ...
I agree w/ Milen, your driver should do this for you. What driver are you using and for what language? But if you are using plpgsql, you can use GET DIAGNOSTICS my_var = ROW_COUNT;
http://www.postgresql.org/docs/current/static/plpgsql-statements.html#PLPGSQL-STATEMENTS-DIAGNOSTICS
You can take ROW_COUNT after update or insert with this code:
insert into distributors (did, dname) values (DEFAULT, 'XYZ Widgets');
get diagnostics v_cnt = row_count;
It's not clear from your question how you're calling the statement. Assuming you're using something like JDBC you may be calling it as a query rather than an update. From JDBC's executeQuery:
Executes the given SQL statement, which returns a single ResultSet
object.
This is therefore appropriate when you execute a statement that returns some query results, such as SELECT or INSERT ... RETURNING. If you are making an update to the database and then want to know how many tuples were affected, you need to use executeUpdate which returns:
either (1) the row count for SQL Data Manipulation Language (DML)
statements or (2) 0 for SQL statements that return nothing
You could wrap your query in a transaction and it should show you the count before you ROLLBACK or COMMIT. Example:
BEGIN TRANSACTION;
INSERT .... ;
ROLLBACK TRANSACTION;
If you run the first 2 lines of the above, it should give you the count. You can then ROLLBACK (undo) the insert if you find that the number of affected lines isn't what you expected. If you're satisfied that the INSERT is correct, then you can run the same thing, but replace line 3 with COMMIT TRANSACTION;.
Important note: After you run any BEGIN TRANSACTION; you must either ROLLBACK; or COMMIT; the transaction, otherwise the transaction will create a lock that can slow down or even cripple an entire system, if you're running on a production environment.

Oracle: how to UPSERT (update or insert into a table?)

The UPSERT operation either updates or inserts a row in a table, depending if the table already has a row that matches the data:
if table t has a row exists that has key X:
update t set mystuff... where mykey=X
else
insert into t mystuff...
Since Oracle doesn't have a specific UPSERT statement, what's the best way to do this?
The MERGE statement merges data between two tables. Using DUAL
allows us to use this command. Note that this is not protected against concurrent access.
create or replace
procedure ups(xa number)
as
begin
merge into mergetest m using dual on (a = xa)
when not matched then insert (a,b) values (xa,1)
when matched then update set b = b+1;
end ups;
/
drop table mergetest;
create table mergetest(a number, b number);
call ups(10);
call ups(10);
call ups(20);
select * from mergetest;
A B
---------------------- ----------------------
10 2
20 1
The dual example above which is in PL/SQL was great becuase I wanted to do something similar, but I wanted it client side...so here is the SQL I used to send a similar statement direct from some C#
MERGE INTO Employee USING dual ON ( "id"=2097153 )
WHEN MATCHED THEN UPDATE SET "last"="smith" , "name"="john"
WHEN NOT MATCHED THEN INSERT ("id","last","name")
VALUES ( 2097153,"smith", "john" )
However from a C# perspective this provide to be slower than doing the update and seeing if the rows affected was 0 and doing the insert if it was.
An alternative to MERGE (the "old fashioned way"):
begin
insert into t (mykey, mystuff)
values ('X', 123);
exception
when dup_val_on_index then
update t
set mystuff = 123
where mykey = 'X';
end;
Another alternative without the exception check:
UPDATE tablename
SET val1 = in_val1,
val2 = in_val2
WHERE val3 = in_val3;
IF ( sql%rowcount = 0 )
THEN
INSERT INTO tablename
VALUES (in_val1, in_val2, in_val3);
END IF;
insert if not exists
update:
INSERT INTO mytable (id1, t1)
SELECT 11, 'x1' FROM DUAL
WHERE NOT EXISTS (SELECT id1 FROM mytble WHERE id1 = 11);
UPDATE mytable SET t1 = 'x1' WHERE id1 = 11;
None of the answers given so far is safe in the face of concurrent accesses, as pointed out in Tim Sylvester's comment, and will raise exceptions in case of races. To fix that, the insert/update combo must be wrapped in some kind of loop statement, so that in case of an exception the whole thing is retried.
As an example, here's how Grommit's code can be wrapped in a loop to make it safe when run concurrently:
PROCEDURE MyProc (
...
) IS
BEGIN
LOOP
BEGIN
MERGE INTO Employee USING dual ON ( "id"=2097153 )
WHEN MATCHED THEN UPDATE SET "last"="smith" , "name"="john"
WHEN NOT MATCHED THEN INSERT ("id","last","name")
VALUES ( 2097153,"smith", "john" );
EXIT; -- success? -> exit loop
EXCEPTION
WHEN NO_DATA_FOUND THEN -- the entry was concurrently deleted
NULL; -- exception? -> no op, i.e. continue looping
WHEN DUP_VAL_ON_INDEX THEN -- an entry was concurrently inserted
NULL; -- exception? -> no op, i.e. continue looping
END;
END LOOP;
END;
N.B. In transaction mode SERIALIZABLE, which I don't recommend btw, you might run into
ORA-08177: can't serialize access for this transaction exceptions instead.
I'd like Grommit answer, except it require dupe values. I found solution where it may appear once: http://forums.devshed.com/showpost.php?p=1182653&postcount=2
MERGE INTO KBS.NUFUS_MUHTARLIK B
USING (
SELECT '028-01' CILT, '25' SAYFA, '6' KUTUK, '46603404838' MERNIS_NO
FROM DUAL
) E
ON (B.MERNIS_NO = E.MERNIS_NO)
WHEN MATCHED THEN
UPDATE SET B.CILT = E.CILT, B.SAYFA = E.SAYFA, B.KUTUK = E.KUTUK
WHEN NOT MATCHED THEN
INSERT ( CILT, SAYFA, KUTUK, MERNIS_NO)
VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO);
I've been using the first code sample for years. Notice notfound rather than count.
UPDATE tablename SET val1 = in_val1, val2 = in_val2
WHERE val3 = in_val3;
IF ( sql%notfound ) THEN
INSERT INTO tablename
VALUES (in_val1, in_val2, in_val3);
END IF;
The code below is the possibly new and improved code
MERGE INTO tablename USING dual ON ( val3 = in_val3 )
WHEN MATCHED THEN UPDATE SET val1 = in_val1, val2 = in_val2
WHEN NOT MATCHED THEN INSERT
VALUES (in_val1, in_val2, in_val3)
In the first example the update does an index lookup. It has to, in order to update the right row. Oracle opens an implicit cursor, and we use it to wrap a corresponding insert so we know that the insert will only happen when the key does not exist. But the insert is an independent command and it has to do a second lookup. I don't know the inner workings of the merge command but since the command is a single unit, Oracle could execute the correct insert or update with a single index lookup.
I think merge is better when you do have some processing to be done that means taking data from some tables and updating a table, possibly inserting or deleting rows. But for the single row case, you may consider the first case since the syntax is more common.
A note regarding the two solutions that suggest:
1) Insert, if exception then update,
or
2) Update, if sql%rowcount = 0 then insert
The question of whether to insert or update first is also application dependent. Are you expecting more inserts or more updates? The one that is most likely to succeed should go first.
If you pick the wrong one you will get a bunch of unnecessary index reads. Not a huge deal but still something to consider.
Try this,
insert into b_building_property (
select
'AREA_IN_COMMON_USE_DOUBLE','Area in Common Use','DOUBLE', null, 9000, 9
from dual
)
minus
(
select * from b_building_property where id = 9
)
;
From http://www.praetoriate.com/oracle_tips_upserts.htm:
"In Oracle9i, an UPSERT can accomplish this task in a single statement:"
INSERT
FIRST WHEN
credit_limit >=100000
THEN INTO
rich_customers
VALUES(cust_id,cust_credit_limit)
INTO customers
ELSE
INTO customers SELECT * FROM new_customers;