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

Started by Mageabout 15 years ago10 messagesgeneral
Jump to latest
#1Mage
mage@mage.hu

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

#2Alban Hertroys
dalroi@solfertje.student.utwente.nl
In reply to: Mage (#1)
Re: isn't "insert into where not exists" atomic?

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!

#3Mage
mage@mage.hu
In reply to: Alban Hertroys (#2)
Re: isn't "insert into where not exists" atomic?

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

#4Tom Lane
tgl@sss.pgh.pa.us
In reply to: Mage (#3)
Re: isn't "insert into where not exists" atomic?

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

#5pasman pasmański
pasman.p@gmail.com
In reply to: Mage (#1)
Re: isn't "insert into where not exists" atomic?

Your trigger is wrong. You try to insert the same row twice.

#6Mage
mage@mage.hu
In reply to: pasman pasmański (#5)
Re: isn't "insert into where not exists" atomic?

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

#7Mage
mage@mage.hu
In reply to: Tom Lane (#4)
Re: isn't "insert into where not exists" atomic?

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

#8Tom Lane
tgl@sss.pgh.pa.us
In reply to: Mage (#7)
Re: isn't "insert into where not exists" atomic?

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

#9pasman pasmański
pasman.p@gmail.com
In reply to: Mage (#6)
Re: isn't "insert into where not exists" atomic?

Mage, add "raise notice" at the begin of your buggy trigger.

#10Mage
mage@mage.hu
In reply to: pasman pasmański (#9)
Re: isn't "insert into where not exists" atomic?

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