Sql 涉及子选择和外键的Postgres比赛条件

Sql 涉及子选择和外键的Postgres比赛条件,sql,postgresql,concurrency,foreign-keys,subquery,Sql,Postgresql,Concurrency,Foreign Keys,Subquery,我们有两个表,定义如下 CREATE TABLE foo ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE ); CREATE TABLE bar ( foo_id BIGINT UNIQUE, foo_name TEXT NOT NULL UNIQUE REFERENCES foo (name) ); 我注意到当同时执行以下两个查询时 INSERT INTO foo (name) VALUES ('BAZ') 在

我们有两个表,定义如下

CREATE TABLE foo (
  id BIGSERIAL PRIMARY KEY,
  name TEXT NOT NULL UNIQUE
);

CREATE TABLE bar (
  foo_id BIGINT UNIQUE,
  foo_name TEXT NOT NULL UNIQUE REFERENCES foo (name)
);

我注意到当同时执行以下两个查询时

INSERT INTO foo (name) VALUES ('BAZ')
在某些情况下,可能会在
bar
中插入一行,其中
foo\u id
NULL
。这两个查询由两个完全不同的进程在不同的事务中执行

这怎么可能?我希望第二条语句要么由于外键冲突而失败(如果
foo
中的记录不存在),要么以
foo\u id
的非空值成功(如果存在)

是什么导致了这种比赛状态?是由于子选择,还是由于检查外键约束的时间

我们正在使用隔离级别的“读提交”和postgres版本10.3

编辑


我认为这个问题并不是特别清楚是什么让我困惑。问题是在执行一条语句的过程中,如何以及为什么观察到数据库的两种不同状态。子select将foo中的记录视为不存在,而fk检查将其视为存在。如果只是没有规则阻止这种比赛状态,那么这本身就是一个有趣的问题-为什么不可能使用事务ID来确保对这两个对象都观察到相同的数据库状态?

插入到条中的
子选项无法看到同时插入到
foo
中的新行,因为后者尚未提交

但是,在执行检查外键约束的查询时,
INSERT-INTO-foo
已提交,因此外键约束不会报告错误


解决这个问题的一个简单方法是使用
可重复读取
隔离级别来隔离
插入整型条
。然后,外键检查使用与插入相同的快照,它将看不到新提交的行,并且将抛出约束冲突错误。

逻辑建议命令的顺序(包括子查询),以及Postgres检查约束的时间(不一定是立即的)可能导致问题。所以你可以

  • 让第二个命令先启动
  • 选择
    组件运行并返回NULL
  • 第一个命令启动并插入行
  • 第二个命令插入行(带有'name'字段和空值)
  • FK引用检查成功,因为“名称”存在
可重延迟约束请参见和

建议答案

  • 对Foo_Id的条进行NOTNULL检查,或将其作为外键检查的一部分
  • 重写这两个命令以连续运行,而不是同时运行(如果可能)

你确实有比赛条件。如果没有某种类型的锁定或使用事务对事件进行排序,则没有规则排除该顺序

  • 执行
    插入的子选择,产生
    NULL
  • 插入到
    foo
  • 插入到
    ,现在没有任何FK冲突,但有空值

  • 当然,这是你真正的程序的玩具版,我不能推荐最好的修复方法。如果按特定顺序要求这些事件是有意义的,那么它们可以在单个线程上的事务中。在其他一些情况下,您可能会禁止直接插入
    foo
    bar
    (根据需要撤销权限),并且只允许通过函数/过程或具有触发器(可能是规则)的视图进行修改。

    匿名plpgsql块将帮助您避免争用条件(通过确保插入在同一事务中顺序运行)而不深入Postgres内部:

    do语言plpgsql
    $$
    声明
    v_foo_id bigint;
    开始
    在foo(name)值中插入('BAZ'),将id返回到v_foo_id中;
    将(foo_名称、foo_id)值('BAZ',v_foo_id)插入到条中;
    结束;
    $$;
    
    或者将普通SQL与CTE结合使用,以避免在plpgsql之间切换上下文:

    t(id)为的
    
    (
    在返回id的foo(name)值('BAZ')中插入
    ) 
    将(foo_名称,foo_id)值('BAZ',(从t中选择id))插入到条中;
    

    顺便说一句,您确定示例中的两个插入在同一事务中以正确的顺序执行吗?如果不确定,那么您的问题的简短答案是“MVCC”,因为第二个语句不是原子语句。

    这似乎更可能是两个查询一个接一个执行,但事务未提交的情况

    过程1

    插入到foo(名称)值('BAZ')中

    事务未提交,但进程2执行下一个查询

    将(foo_name,foo_id)值('BAZ',(从foo中选择id,其中name='BAZ')插入到条中

    在这种情况下,进程2查询将等待进程1事务未提交

    从PostgreSQL文档:


    UPDATE、DELETE、SELECT FOR UPDATE和SELECT FOR SHARE命令在搜索目标行方面的行为与SELECT相同:它们将仅查找在命令开始时提交的目标行。但是,此类目标行可能已被更新(或删除或锁定)在这种情况下,可能的更新程序将等待第一个更新事务提交或回滚(如果它仍在进行中).

    不要问我们这是怎么可能的,这是要求我们用定制的教程重写文档,要求我们在不知道您的推理是什么的情况下解决您的错误推理。说出您的期望,并通过参考文档说明您期望的原因。如果在执行第二个插入之前没有提交第一个插入ed这是绝对可能的。@a_马_
    INSERT INTO bar (foo_name, foo_id) VALUES ('BAZ', (SELECT id FROM foo WHERE name = 'BAZ'))