我有一个示例情况:
parent
表有一个名为 id
的列,在 child
表中作为外键引用。
删除子行时,如果父行没有被任何其他子行引用,如何同时删除父行?
在 PostgreSQL 9.1 或更高版本中,您可以使用 数据修改 CTE 通过一条语句来完成此操作。这通常不太容易出错。它最小化两次删除之间的时间范围,其中“竞争条件”可能会导致并发操作出现令人惊讶的结果:
WITH del_child AS (
DELETE FROM child
WHERE child_id = 1
RETURNING parent_id, child_id
)
DELETE FROM parent p
USING del_child x
WHERE p.parent_id = x.parent_id
AND NOT EXISTS (
SELECT FROM child c
WHERE c.parent_id = x.parent_id
AND c.child_id <> x.child_id -- !
);
otherWITH
中的数据修改语句只执行一次,并且
始终完成,与主查询是否读取无关 他们的所有(或者实际上是任何)输出。请注意,这是不同的 根据SELECT
中的规则:如上一节所述,WITH
的执行仅执行到主查询 需要它的输出。 仅当父级没有SELECT
子级时才会被删除。
请注意最后一个条件。与人们的预期相反,这是必要的,因为:
我使用列名称WITH
中的子语句彼此
并发执行 以及主要查询。因此,在使用数据修改时WITH
中的语句,指定的实际更新顺序 发生的事情是不可预测的。所有语句都以相同的方式执行 快照(参见第13章),因此他们无法“看到”彼此的效果 在目标表上。 我的粗体强调。
parent_id
代替非描述性
id
。消除竞争条件
消除可能的竞争条件,请首先锁定父行。所有类似的操作都必须遵循相同的程序才能起作用。
WITH lock_parent AS (
SELECT p.parent_id, c.child_id
FROM child c
JOIN parent p ON p.parent_id = c.parent_id
WHERE c.child_id = 12 -- provide child_id here once
FOR NO KEY UPDATE -- locks parent row.
)
, del_child AS (
DELETE FROM child c
USING lock_parent l
WHERE c.child_id = l.child_id
)
DELETE FROM parent p
USING lock_parent l
WHERE p.parent_id = l.parent_id
AND NOT EXISTS (
SELECT FROM child c
WHERE c.parent_id = l.parent_id
AND c.child_id <> l.child_id -- !
);
这样一次只有一个
事务可以锁定同一个父事务。因此,不会发生多个事务删除同一父级的子级,但仍然看到其他子级并保留父级,而所有子级随后都消失的情况。 (仍然允许使用 FOR NO KEY UPDATE
更新非键列。)
FOR NO KEY UPDATE
是在 Postgres 9.4 中引入的。
手册中有详细信息。在旧版本中请使用更坚固的锁
FOR UPDATE
。在子级中删除后,在父级中执行此操作:
delete from parent
where
id = 1
and not exists (
select 1 from child where parent_id = 1
)
not exists
条件将确保只有在子级中不存在它时才会将其删除。您可以将两个删除命令包装在一个事务中:
begin;
first_delete;
second_delete;
commit;