Thread: Solution to UPDATE or INSERT Problem

Solution to UPDATE or INSERT Problem

From
Curt Sampson
Date:
I've seen a couple of questions here in the past about how to update an
existing row or insert a row if it doesn't exist without race conditions
that could cause you to have to retry a transaction. I didn't find any
answers to this question in the archives however, so I thought I'd post
my solution here for the edification of others.

    INSERT INTO my_table (key, value) SELECT 1, 'a value'
    WHERE NOT EXISTS (SELECT key FROM my_table WHERE key = 1);
    UPDATE my_table SET value = 'a value' WHERE key = 1;

This, as far as I can tell, will never fail, though it might invoke
triggers that would not otherwise be invoked if only the single
necessary statement (the INSERT or the UPDATE) were executed. I'd imagine
in most cases that this would not be a big problem.

IMHO, this is a little bit better than the MySQL REPLACE command, since
it doesn't cause deletion of an entire row and reinsertion, which can
play hell with things like my change logging triggers. (It would record
all the deleted values in the change log table and then an insert,
rather than recording just the changed values.)

If you have any comments you want me to see, please be sure that I'm in
the cc list on the message, as I'm not subscribed to this list.

cjs
--
Curt Sampson  <cjs@cynic.net>   +81 90 7737 2974   http://www.NetBSD.org
    Don't you know, in this new Dark Age, we're all light.  --XTC

Re: Solution to UPDATE or INSERT Problem

From
Tom Lane
Date:
Curt Sampson <cjs@cynic.net> writes:
> I've seen a couple of questions here in the past about how to update an
> existing row or insert a row if it doesn't exist without race conditions
> that could cause you to have to retry a transaction. I didn't find any
> answers to this question in the archives however, so I thought I'd post
> my solution here for the edification of others.

>     INSERT INTO my_table (key, value) SELECT 1, 'a value'
>     WHERE NOT EXISTS (SELECT key FROM my_table WHERE key = 1);
>     UPDATE my_table SET value = 'a value' WHERE key = 1;

> This, as far as I can tell, will never fail,

You're quite mistaken.  Have you made any effort to test it?

<< session 1 >>

regression=# create table my_table (key int unique, value text);
NOTICE:  CREATE TABLE / UNIQUE will create implicit index "my_table_key_key" for table "my_table"
CREATE TABLE
regression=# begin;
BEGIN
regression=# INSERT INTO my_table (key, value) SELECT 1, 'a value'
regression-# WHERE NOT EXISTS (SELECT key FROM my_table WHERE key = 1);
INSERT 429665 1

<< session 2 >>

regression=# begin;
BEGIN
regression=# INSERT INTO my_table (key, value) SELECT 1, 'a value'
regression-# WHERE NOT EXISTS (SELECT key FROM my_table WHERE key = 1);

<< session 2 hangs >>

<< back to session 1 >>

regression=#  UPDATE my_table SET value = 'a value' WHERE key = 1;
UPDATE 1
regression=# commit;
COMMIT
regression=#

<< now session 2 fails: >>

ERROR:  duplicate key violates unique constraint "my_table_key_key"
regression=#

            regards, tom lane

Re: Solution to UPDATE or INSERT Problem

From
Tom Lane
Date:
Curt Sampson <cjs@cynic.net> writes:
> On Mon, 19 Jan 2004, Tom Lane wrote:
>> You're quite mistaken.  Have you made any effort to test it?

> Yes. You appear to have changed my code somewhat:
> ...
> Take out that BEGIN.

The BEGIN was merely a convenient way of slowing down operations enough
so that a trivial manual test would expose the problem.  Concurrent
executions of that INSERT/SELECT *will* fail, it's just a matter of
getting them to actually overlap in time.  With BEGIN the window for
concurrency failures is wider than without --- but it's not zero without.

> Yup. You re-created the race condition that I'd gotten rid of when you
> put the INSERT and the UPDATE into the same transaction.

You need to go back and re-read the documentation... the UPDATE does not
actually have anything to do with the failure.

            regards, tom lane

Re: Solution to UPDATE or INSERT Problem

From
Tom Lane
Date:
Curt Sampson <cjs@cynic.net> writes:
> But why does session 2 hang
> in this instance?

Because the sub-SELECT sees a snapshot of transactions that were already
committed at the start of session 2's transaction.  If session 1 hasn't
committed yet, the EXISTS will return false.  The actual INSERT will
notice the inserted-but-not-yet-committed row, and will block waiting to
see whether it gets committed or not.

            regards, tom lane