Обсуждение: unexpected rowlock mode when trigger is on the table

Поиск
Список
Период
Сортировка

unexpected rowlock mode when trigger is on the table

От
Tomáš Záluský
Дата:
Hi pgsql hackers,

I'd like to ask you for kind advice with problem I'm pretty long stuck on. I simplified it just to two tables and
trigger.I described it thoroughly on Stack Overflow a week ago but nobody responded so far, so I was advised by Czech
PGexpert Pavel Stěhule to use this forum.
 

Database setup:

------------
create table detail (
  id bigint not null,
  code varchar(255) not null,
  primary key (id)
);

create table master (
  id bigint not null,
  name varchar(255),
  detail_id bigint, -- "preferred" detail is one-to-one relation
  primary key (id),
  unique (detail_id),
  foreign key (detail_id) references detail(id)
);

create or replace function trgf() returns trigger as $$
begin
  return NEW;
end;
$$ language plpgsql;

create trigger trg
  before insert or update
  on master
  for each row execute procedure trgf();

insert into master (id, name) values (1000, 'x');
insert into detail (code, id) values ('a', 1);

create extension pgrowlocks;
------------

In psql console, I open first transaction and run:

postgres=# begin;
BEGIN
postgres=# update master set detail_id=null, name='y' where id=1000;
UPDATE 1

In another psql console, I run:

postgres=# select * from pgrowlocks('master');
 locked_row | locker | multi | xids  |  modes   | pids
------------+--------+-------+-------+----------+-------
 (0,3)      |    564 | f     | {564} | {Update} | {138}
(1 row)

Note the mode is Update, which means the attempt of taking FOR KEY SHARE lock is not successful:

postgres=# set statement_timeout = 4000;
SET
postgres=# SELECT 1 FROM ONLY "public"."master" x WHERE "id" OPERATOR(pg_catalog.=) 1000 FOR KEY SHARE OF x;
ERROR:  canceling statement due to statement timeout
CONTEXT:  while locking tuple (0,3) in relation "master"

This is ok. What is weird: this behaviour disappears when whole experiment is performed without trigger trg set up on
mastertable. Mode is then No Key Update, which lets second transaction to acquire FOR KEY SHARE lock and perform select
correctly.(Actually there's another table representing many-to-many relation between master and detail, at first I
obtainedtimeout during attempt of insert binding row.)
 

So what I can't understand is:

1. Why the rowlock mode is only No Key Update in case without trigger? According to
https://www.postgresql.org/docs/9.6/explicit-locking.html#LOCKING-ROWS, thanks to unique constraint on master.detail_id
column,the rowlock mode should be Update anyway, shouldn't it? Why is it Update only after adding trigger?
 

2. How to make this case working with trigger on the table? I need it to be there, it worked before trigger addition.

I reproduced it on Postgres 9.6.12 (embedded), 9.6.15 (in Docker) and 11.5 (in Docker).
I dockerized database with setup above to DockerHub image tomaszalusky/trig-example , Dockerfile here:
https://gist.github.com/tomaszalusky/4b953c678c806408025d05d984d30ed3
 
Original SO question: https://stackoverflow.com/q/57681970/653539 (captures some history of my investigations which I
considerunnecessary to state here)
 

Thank you for all the effort.

Tomáš Záluský



Re: unexpected rowlock mode when trigger is on the table

От
Alvaro Herrera
Дата:
On 2019-Sep-03, Tomáš Záluský wrote:

> postgres=# begin;
> BEGIN
> postgres=# update master set detail_id=null, name='y' where id=1000;
> UPDATE 1
> 
> In another psql console, I run:
> 
> postgres=# select * from pgrowlocks('master');
>  locked_row | locker | multi | xids  |  modes   | pids
> ------------+--------+-------+-------+----------+-------
>  (0,3)      |    564 | f     | {564} | {Update} | {138}
> (1 row)

Hmm, so I'm guessing that this tuple lock comes from GetTupleForTrigger
called from ExecBRUpdateTriggers, which uses ExecUpdateLockMode() in
order to figure out the lockmode to use, depending on whether the
modified columns by the update overlap columns indexed by any unique
index.  I think there should be no overlap (PK is column "id", not modified)
but I may be missing something.

(gdb) bt
#0  heap_lock_tuple (relation=relation@entry=0x7eff2157b4d8, 
    tuple=tuple@entry=0x7ffe570db3e0, cid=0, 
    mode=mode@entry=LockTupleExclusive, 
    wait_policy=wait_policy@entry=LockWaitBlock, 
    follow_updates=follow_updates@entry=0 '\000', buffer=0x7ffe570db3cc, 
    hufd=0x7ffe570db3d0)
    at /pgsql/source/REL9_6_STABLE/src/backend/access/heap/heapam.c:4577
#1  0x00005648b1d52f15 in GetTupleForTrigger (
    estate=estate@entry=0x5648b3894110, 
    epqstate=epqstate@entry=0x5648b3894750, tid=tid@entry=0x7ffe570db674, 
    lockmode=LockTupleExclusive, newSlot=0x7ffe570db498, 
    relinfo=<optimized out>, relinfo=<optimized out>)
    at /pgsql/source/REL9_6_STABLE/src/backend/commands/trigger.c:2709
#2  0x00005648b1d579a0 in ExecBRUpdateTriggers (
    estate=estate@entry=0x5648b3894110, 
    epqstate=epqstate@entry=0x5648b3894750, 
    relinfo=relinfo@entry=0x5648b3894260, 
    tupleid=tupleid@entry=0x7ffe570db674, 
    fdw_trigtuple=fdw_trigtuple@entry=0x0, slot=slot@entry=0x5648b3896670)
    at /pgsql/source/REL9_6_STABLE/src/backend/commands/trigger.c:2432
#3  0x00005648b1d8ddc2 in ExecUpdate (tupleid=tupleid@entry=0x7ffe570db674, 
    oldtuple=oldtuple@entry=0x0, slot=slot@entry=0x5648b3896670, 
    planSlot=planSlot@entry=0x5648b3895998, 
    epqstate=epqstate@entry=0x5648b3894750, 
    estate=estate@entry=0x5648b3894110, canSetTag=1 '\001')
    at /pgsql/source/REL9_6_STABLE/src/backend/executor/nodeModifyTable.c:850

Maybe we're passing an argument wrong somewhere.  Unclear ...

-- 
Álvaro Herrera                https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services



Re: unexpected rowlock mode when trigger is on the table

От
Tomáš Záluský
Дата:
Thanks for response.

> I think there should be no overlap (PK is column "id", not modified)

The update command sets the detail_id column which has unique constraint.
If I read documentation correctly (13.3.2. Row-level Locks), unique columns also count to columns whose presence in
updatestatement causes choosing FOR UPDATE lock.
 

What is unclear to me, why FOR NO KEY UPDATE is chosen when there is no trigger.
Perhaps the execution path to ExecUpdateLockMode is somehow different?
And if FOR NO KEY UPDATE is correct, how to achieve it also with trigger?

Tomáš


______________________________________________________________
> Od: "Alvaro Herrera" <alvherre@2ndquadrant.com>
> Komu: "Tomáš Záluský" <zalusky@centrum.cz>
> Datum: 05.09.2019 00:52
> Předmět: Re: unexpected rowlock mode when trigger is on the table
>
> CC: <pgsql-hackers@postgresql.org>
>On 2019-Sep-03, Tomáš Záluský wrote:
>
>> postgres=# begin;
>> BEGIN
>> postgres=# update master set detail_id=null, name='y' where id=1000;
>> UPDATE 1
>> 
>> In another psql console, I run:
>> 
>> postgres=# select * from pgrowlocks('master');
>>  locked_row | locker | multi | xids  |  modes   | pids
>> ------------+--------+-------+-------+----------+-------
>>  (0,3)      |    564 | f     | {564} | {Update} | {138}
>> (1 row)
>
>Hmm, so I'm guessing that this tuple lock comes from GetTupleForTrigger
>called from ExecBRUpdateTriggers, which uses ExecUpdateLockMode() in
>order to figure out the lockmode to use, depending on whether the
>modified columns by the update overlap columns indexed by any unique
>index.  I think there should be no overlap (PK is column "id", not modified)
>but I may be missing something.
>
>(gdb) bt
>#0  heap_lock_tuple (relation=relation@entry=0x7eff2157b4d8, 
>    tuple=tuple@entry=0x7ffe570db3e0, cid=0, 
>    mode=mode@entry=LockTupleExclusive, 
>    wait_policy=wait_policy@entry=LockWaitBlock, 
>    follow_updates=follow_updates@entry=0 '\000', buffer=0x7ffe570db3cc, 
>    hufd=0x7ffe570db3d0)
>    at /pgsql/source/REL9_6_STABLE/src/backend/access/heap/heapam.c:4577
>#1  0x00005648b1d52f15 in GetTupleForTrigger (
>    estate=estate@entry=0x5648b3894110, 
>    epqstate=epqstate@entry=0x5648b3894750, tid=tid@entry=0x7ffe570db674, 
>    lockmode=LockTupleExclusive, newSlot=0x7ffe570db498, 
>    relinfo=<optimized out>, relinfo=<optimized out>)
>    at /pgsql/source/REL9_6_STABLE/src/backend/commands/trigger.c:2709
>#2  0x00005648b1d579a0 in ExecBRUpdateTriggers (
>    estate=estate@entry=0x5648b3894110, 
>    epqstate=epqstate@entry=0x5648b3894750, 
>    relinfo=relinfo@entry=0x5648b3894260, 
>    tupleid=tupleid@entry=0x7ffe570db674, 
>    fdw_trigtuple=fdw_trigtuple@entry=0x0, slot=slot@entry=0x5648b3896670)
>    at /pgsql/source/REL9_6_STABLE/src/backend/commands/trigger.c:2432
>#3  0x00005648b1d8ddc2 in ExecUpdate (tupleid=tupleid@entry=0x7ffe570db674, 
>    oldtuple=oldtuple@entry=0x0, slot=slot@entry=0x5648b3896670, 
>    planSlot=planSlot@entry=0x5648b3895998, 
>    epqstate=epqstate@entry=0x5648b3894750, 
>    estate=estate@entry=0x5648b3894110, canSetTag=1 '\001')
>    at /pgsql/source/REL9_6_STABLE/src/backend/executor/nodeModifyTable.c:850
>
>Maybe we're passing an argument wrong somewhere.  Unclear ...
>
>-- 
>Álvaro Herrera                https://www.2ndQuadrant.com/
>PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
>
>
>
>



Re: unexpected rowlock mode when trigger is on the table

От
Alvaro Herrera
Дата:
On 2019-Sep-05, Tomáš Záluský wrote:

> Thanks for response.
> 
> > I think there should be no overlap (PK is column "id", not modified)
> 
> The update command sets the detail_id column which has unique constraint.

Oh, I see, yeah that explains it.

> What is unclear to me, why FOR NO KEY UPDATE is chosen when there is no trigger.
> Perhaps the execution path to ExecUpdateLockMode is somehow different?

heap_update on its own uses a slightly different method to determine
which columns are modified -- see HeapDetermineModifiedColumns.  In this
case, since the old value is NULL and the updated value is NULL, that
function decides that the column has not changed and thus it doesn't
need the stronger lock.  I bet it would work differently if you had a
different detail_id originally, or if you set it to a different value
afterwards.

> And if FOR NO KEY UPDATE is correct, how to achieve it also with trigger?

Not sure that's feasible, short of patching the Pg source.

-- 
Álvaro Herrera                https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services



Re: unexpected rowlock mode when trigger is on the table

От
Tomáš Záluský
Дата:
I confirm the update statement with new value distinct from old value causes taking FOR UPDATE lock.
In my original example, the `set detail_id=null` clause is actually generated by Hibernate and was preserved during
exampleminification.
 
So I'll have to either find a way how to stop generating unnecessary clauses or refactor database to avoid detail_id
columnat all.
 

Patching PG source is not option for me at this moment, however, chapter 13.3.2 in documentation could be improved to
explicitlystate 1. the unchanged value of key/unique column causes FOR NO KEY UPDATE lock and 2. the sole usage of
key/uniquecolumn in statement causes FOR UPDATE lock when table has trigger.
 

Thank you for clarification.


______________________________________________________________
> Od: "Alvaro Herrera" <alvherre@2ndquadrant.com>
> Komu: "Tomáš Záluský" <zalusky@centrum.cz>
> Datum: 05.09.2019 16:00
> Předmět: Re: unexpected rowlock mode when trigger is on the table
>
> CC: <pgsql-hackers@postgresql.org>
>On 2019-Sep-05, Tomáš Záluský wrote:
>
>> Thanks for response.
>> 
>> > I think there should be no overlap (PK is column "id", not modified)
>> 
>> The update command sets the detail_id column which has unique constraint.
>
>Oh, I see, yeah that explains it.
>
>> What is unclear to me, why FOR NO KEY UPDATE is chosen when there is no trigger.
>> Perhaps the execution path to ExecUpdateLockMode is somehow different?
>
>heap_update on its own uses a slightly different method to determine
>which columns are modified -- see HeapDetermineModifiedColumns.  In this
>case, since the old value is NULL and the updated value is NULL, that
>function decides that the column has not changed and thus it doesn't
>need the stronger lock.  I bet it would work differently if you had a
>different detail_id originally, or if you set it to a different value
>afterwards.
>
>> And if FOR NO KEY UPDATE is correct, how to achieve it also with trigger?
>
>Not sure that's feasible, short of patching the Pg source.
>
>-- 
>Álvaro Herrera                https://www.2ndQuadrant.com/
>PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
>
>
>
>