10

我有一个示例情况:parent表有一个名为 的列id,在child表中作为外键引用。

删除子行时,如果没有任何其他子行引用,如何删除父行?

4

2 回答 2

14

在 PostgreSQL 9.1 或更高版本中,您可以通过使用数据修改 CTE的单个语句来执行此操作。这通常不太容易出错。它最小化了两个 DELETE 之间的时间范围,其中竞争条件可能导致并发操作的惊人结果:

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 1
   FROM   child c
   WHERE  c.parent_id = x.parent_id
   AND    c.child_id <> x.child_id   -- !
   );

SQL小提琴。

无论如何,孩子都会被删除。我引用手册

中的数据修改语句WITH只执行一次,并且 总是完成,与主查询是否读取所有(或实际上任何)输出无关。SELECT请注意,这与in的规则不同WITH:如上一节所述,SELECT仅在主查询需要其输出时才执行 a。

只有在没有其他子项时才会删除父项。
注意最后一个条件。与人们的预期相反,这是必要的,因为:

中的子语句彼此同时WITH执行并与主查询同时执行。因此,当在 中使用数据修改语句时,指定更新实际发生的顺序是不可预测的。所有语句都使用同一个快照执行(参见第 13 章),因此它们无法“看到”彼此对目标表的影响。WITH

大胆强调我的。
我用列名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 1
   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

于 2013-04-04T11:44:06.103 回答
2
delete from child
where parent_id = 1

在子项中删除后,在父项中执行:

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;
于 2013-04-04T11:17:54.623 回答