Thread: isn't "insert into where not exists" atomic?

isn't "insert into where not exists" atomic?

From
Mage
Date:
Hello,

I just received an error message:

   PGError: ERROR:  duplicate key value violates unique constraint "chu_user_id_chat_room_id"
DETAIL:  Key (user_id, chat_room_id)=(8, 2) already exists.
CONTEXT:  SQL statement "insert into chat_room_users (user_id, chat_room_id, active_at) (select NEW.user_id,
NEW.chat_room_id,now() where not exists (select 1 from chat_room_users where user_id = NEW.user_id and chat_room_id =
NEW.chat_room_id))"
PL/pgSQL function "trf_chat_room_users_insert" line 3 at SQL statement
: INSERT INTO "chat_room_users" ("user_id", "chat_room_id", "active_at") VALUES (8, 2, NULL)


The important line is:
insert into chat_room_users (user_id, chat_room_id, active_at) (select NEW.user_id, NEW.chat_room_id, now() where not
exists(select 1 from chat_room_users where user_id = NEW.user_id and chat_room_id = NEW.chat_room_id)) 


I always thought this is atomic and can not fail. Was I wrong?

If it isn't then I have to rewrite my triggers. Do I have to use "lock
table" instead of the above to avoid errors in parallel inserts?

The trigger looks like:

create or replace function trf_chat_room_users_insert() returns trigger
as $$
begin
         if NEW.active_at is null then
                 insert into chat_room_users (user_id, chat_room_id,
active_at) (select NEW.user_id, NEW.chat_room_id, now() where not exists
(select 1 from chat_room_users where user_id = NEW.user_id and
chat_room_id = NEW.chat_room_id));
                 if not found then
                         update chat_room_users set active_at = now()
where user_id = NEW.user_id and chat_room_id = NEW.chat_room_id;
                 end if;
                 return null;
         end if;
         return NEW;
end;
$$ language plpgsql;

And it meant to be "insert or update".

         Mage


Re: isn't "insert into where not exists" atomic?

From
Alban Hertroys
Date:
On 3 Feb 2011, at 2:17, Mage wrote:

> The trigger looks like:
>
> create or replace function trf_chat_room_users_insert() returns trigger as $$
> begin
>        if NEW.active_at is null then
>                insert into chat_room_users (user_id, chat_room_id, active_at) (select NEW.user_id, NEW.chat_room_id,
now()where not exists (select 1 from chat_room_users where user_id = NEW.user_id and chat_room_id = NEW.chat_room_id)); 
>                if not found then
>                        update chat_room_users set active_at = now() where user_id = NEW.user_id and chat_room_id =
NEW.chat_room_id;
>                end if;
>                return null;
>        end if;
>        return NEW;
> end;
> $$ language plpgsql;


Your trigger is the wrong way around. Insert doesn't set found, but update does.

Alban Hertroys

--
If you can't see the forest for the trees,
cut the trees and you'll see there is no forest.


!DSPAM:737,4d4a559711736475013765!



Re: isn't "insert into where not exists" atomic?

From
Mage
Date:
On 02/03/2011 08:13 AM, Alban Hertroys wrote:
> On 3 Feb 2011, at 2:17, Mage wrote:
>
>> The trigger looks like:
>>
>> create or replace function trf_chat_room_users_insert() returns trigger as $$
>> begin
>>         if NEW.active_at is null then
>>                 insert into chat_room_users (user_id, chat_room_id, active_at) (select NEW.user_id,
NEW.chat_room_id,now() where not exists (select 1 from chat_room_users where user_id = NEW.user_id and chat_room_id =
NEW.chat_room_id));
>>                 if not found then
>>                         update chat_room_users set active_at = now() where user_id = NEW.user_id and chat_room_id =
NEW.chat_room_id;
>>                 end if;
>>                 return null;
>>         end if;
>>         return NEW;
>> end;
>> $$ language plpgsql;
>
> Your trigger is the wrong way around. Insert doesn't set found, but update does.
>
> Alban Hertroys
I think you missed the point that the insert contains a select which
sets found.

My trigger works fine and it was called thousands times. It just dropped
an exception two times.

The main question is that isn't "insert into ... select ... where not
exists" atomic?

Anyway, it you'd try it:

create table chat_room_users (
user_id int not null,
chat_room_id int not null,
active_at timestamp with time zone not null
);

create unique index chu_user_id_chat_room_id on chat_room_users
(user_id, chat_room_id);

create or replace function trf_chat_room_users_insert() returns trigger
as $$
begin
     if NEW.active_at is null then
         insert into chat_room_users (user_id, chat_room_id, active_at)
(select NEW.user_id, NEW.chat_room_id, now() where not exists (select 1
from chat_room_users where user_id = NEW.user_id and chat_room_id =
NEW.chat_room_id));
         if not found then
             update chat_room_users set active_at = now() where user_id
= NEW.user_id and chat_room_id = NEW.chat_room_id;
         end if;
         return null;
     end if;
     return NEW;
end;
$$ language plpgsql;

create trigger tr_chat_room_users_insert before insert on
chat_room_users for each row execute procedure trf_chat_room_users_insert();

insert into chat_room_users (user_id, chat_room_id) values (1, 1);
insert into chat_room_users (user_id, chat_room_id) values (2, 1);
insert into chat_room_users (user_id, chat_room_id) values (1, 1);
insert into chat_room_users (user_id, chat_room_id) values (2, 1);



         Mage



Re: isn't "insert into where not exists" atomic?

From
Tom Lane
Date:
Mage <mage@mage.hu> writes:
> The main question is that isn't "insert into ... select ... where not
> exists" atomic?

No, it isn't: it *will* fail in the presence of other transactions doing
the same thing, because the EXISTS test will only see rows that
committed before the command started.  You might care to read the
manual's chapter about concurrency:
http://www.postgresql.org/docs/9.0/static/mvcc.html

            regards, tom lane

Re: isn't "insert into where not exists" atomic?

From
pasman pasmański
Date:
Your trigger is wrong. You try to insert the same row twice.

Re: isn't "insert into where not exists" atomic?

From
Mage
Date:
On 02/03/2011 10:35 PM, pasman pasmański wrote:
> Your trigger is wrong. You try to insert the same row twice.
>
I assume you didn't try it. If active_at field is null then the trigger
does another insert instead of the original one. This avoids looping or
inserting twice.

The only mistake is that I tought the insert with select will be atomic
and I was wrong. So the trigger has concurrency problem in
multi-threaded environment. It runs flawlessly in single thread and it
does only a single insert.

         Mage


Re: isn't "insert into where not exists" atomic?

From
Mage
Date:
On 02/03/2011 08:23 PM, Tom Lane wrote:
> Mage<mage@mage.hu>  writes:
>> The main question is that isn't "insert into ... select ... where not
>> exists" atomic?
> No, it isn't: it *will* fail in the presence of other transactions doing
> the same thing, because the EXISTS test will only see rows that
> committed before the command started.  You might care to read the
> manual's chapter about concurrency:
> http://www.postgresql.org/docs/9.0/static/mvcc.html
Thank you, Tom. I will read that.

However I googled a bit before written this trigger and I would like to
ask you: what is the best practice for doing "insert or update"-like
thing, especially in this case, in trigger? I would use lock table from
now. Is it the recommended way?

(I just don't like the "insert -> on exception -> update" method).

         Mage

Re: isn't "insert into where not exists" atomic?

From
Tom Lane
Date:
Mage <mage@mage.hu> writes:
> On 02/03/2011 08:23 PM, Tom Lane wrote:
>> No, it isn't: it *will* fail in the presence of other transactions doing
>> the same thing, because the EXISTS test will only see rows that
>> committed before the command started.  You might care to read the
>> manual's chapter about concurrency:
>> http://www.postgresql.org/docs/9.0/static/mvcc.html
> Thank you, Tom. I will read that.

> However I googled a bit before written this trigger and I would like to
> ask you: what is the best practice for doing "insert or update"-like
> thing, especially in this case, in trigger? I would use lock table from
> now. Is it the recommended way?

> (I just don't like the "insert -> on exception -> update" method).

AFAIR the basic alternatives are insert -> exception -> update or
taking a lock at the table level.  The latter is simpler and cleaner
but distinctly worse for concurrent-insert performance, especially if
you can't keep the transactions very short.  Pick your poison ...

            regards, tom lane

Re: isn't "insert into where not exists" atomic?

From
pasman pasmański
Date:
Mage, add "raise notice" at the begin of your buggy trigger.

Re: isn't "insert into where not exists" atomic?

From
Mage
Date:
On 02/04/2011 06:57 AM, pasman pasmański wrote:
> Mage, add "raise notice" at the begin of your buggy trigger.
>
There is a little bit of difference between "Your trigger is wrong. You
try to insert the same row twice" and "The trigger will be fired twice."

You stated the first but the second is the truth.

Nevermind, my solution will be using table level lock for this case.

         Mage