在表上添加触发器时,PSQLException和锁定问题

Tom*_*ský 12 postgresql triggers locking

更新:我从问题中消除了休眠。我完全重新编写了对问题的描述,以尽可能地简化它。

我有master带noop触发器的detail表和带masterdetail表之间的两个关系的表:

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 table detail_candidate ( -- "candidate" details = many-to-many relation modeled as join table
  master_id bigint not null,
  detail_id bigint not null,
  primary key (master_id, detail_id),
  foreign key (detail_id) references detail(id),
  foreign key (master_id) references master(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'); -- this is part of database setup
insert into detail (code, id) values ('a', 1);    -- this is part of database setup

Run Code Online (Sandbox Code Playgroud)

在这种设置中,我使用打开两个终端窗口,psql然后执行以下步骤:

  1. 在第一个终端中,更改主服务器(将事务保持打开状态)
begin;
update master set detail_id=null, name='y' where id=1000;
Run Code Online (Sandbox Code Playgroud)
  1. 在第二终端中,添加详细信息候选者以自己进行交易
begin;
set statement_timeout = 4000;
insert into detail_candidate (master_id, detail_id) values (1000, 1);
Run Code Online (Sandbox Code Playgroud)

第二个终端超时中的最后一条命令,带有消息

ERROR:  canceling statement due to statement timeout
CONTEXT:  while locking tuple (0,1) in relation "master"
SQL statement "SELECT 1 FROM ONLY "public"."master" x WHERE "id" OPERATOR(pg_catalog.=) $1 FOR KEY SHARE OF x"
Run Code Online (Sandbox Code Playgroud)

我的观察和问题(更改是独立的):

  • 当数据库设置为无触发时,即drop trigger trg on master;在初始设置后调用时,一切正常。为什么存在noop触发器会产生这种影响?我不明白
  • 当数据库被设置为没有唯一约束时master.detail_id(即alter table master drop constraint master_detail_id_key;在初始设置后被调用),一切也都可以正常工作。为什么?
  • 当我detail=null在第一个终端的update语句中省略显式分配时(因为无论如何安装程序中都存在null值),一切也都正常。为什么?

在Postgres 9.6.12(嵌入式),9.6.15(在Docker中),11.5(在Docker中)中试用。

问题tomaszalusky/trig-example可以在DockerHub上可用的Docker映像中重现,也可以从此Dockerfile(内部指令)构建。


更新2:我发现上面三个观察的常见行为。我在第二个事务中select * from pgrowlocks('master')pgrowlocks扩展中生成了查询。该行级锁更新行的masterFOR UPDATE在失败的情况下,但FOR NO KEY UPDATE在所有三个工作情况。这是与文档中的模式匹配表完全一致的,因为FOR UPDATEmode是更强大的模式,而insert语句请求的mode是FOR KEY SHARE(从错误消息中可以明显看出,也调用该select ... for key share命令具有与command相同的效果insert)。

FOR UPDATE模式文档说明:

FOR UPDATE锁定模式还可以通过(...)UPDATE来修改,该UPDATE会修改某些列上的值。当前,在UPDATE情况下考虑的那组列是可以在外键(...)中使用的唯一索引。

master.detail_id列是正确的。但是,仍然不清楚为什么FOR UPDATE没有根据触发条件单独选择模式,以及为什么触发条件导致了它。

mem*_*tha 5

有趣的问题。这是我最好的猜测。我没有测试过。

一般来说,Postgres 对语句对数据产生什么影响的有根据的猜测不会扩展到触发逻辑。当执行第二条语句时,postgres 看到外键约束,并且知道它必须检查分配(插入)的值是否有效,即它是否代表外表中的有效键。无论实践多么糟糕,触发器都有可能对所提议的外键的有效性产生影响(例如,如果触发器删除记录)。

(情况 1)如果没有触发器,则它可以查看数据(预提交和暂存提交)并确定建议的值是否保证有效。(情况2)如果没有FK约束,那么触发器不会影响插入的有效性,因此是允许的。(情况 3)如果省略detail_id=null,则更新中没有任何更改,因此触发器不会触发,因此它的存在无关紧要。

我尽可能避免 FK 约束和触发器。在我看来,最好是让数据库意外地包含部分不正确的数据,然后让它完全挂起,就像您在这里看到的那样。我会删除所有 FK 约束和触发器,并强制所有更新和插入操作通过存储函数进行操作,这些函数在开始/提交锁内执行验证,并立即适当地处理不正确/无效的插入/更新尝试,而不是强制 postgres等待命令 1 提交,然后再决定是否允许命令 2。

编辑:看到这个问题

编辑2:我能找到的关于触发器相对于约束检查的时间的官方文​​档最接近的内容是来自触发器文档

可以指定触发器在尝试对行进行操作之前触发(在检查约束并尝试 INSERT、UPDATE 或 DELETE 之前);或操作完成后(检查约束并完成 INSERT、UPDATE 或 DELETE 后);或者代替操作(在视图上插入、更新或删除的情况下)。如果触发器在事件之前或代替事件触发,则触发器可以跳过当前行的操作,或更改正在插入的行(仅适用于 INSERT 和 UPDATE 操作)。

这有点不清楚,约束检查之前发生的触发是否适用于其他事务的约束检查。不管怎样,这个问题要么是一个错误,要么是记录不足。