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'))