如果未被任何其他子项引用,则删除父项

时间:2013-04-04 11:08:46

标签: sql postgresql foreign-keys common-table-expression referential-integrity

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

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

2 个答案:

答案 0 :(得分:10)

在PostgreSQL 9.1或更高版本中,您可以使用data-modifying 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 Fiddle.

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

  

WITH中的数据修改语句只执行一次,并且   始终完成,与主查询是否读取无关   所有(或实际上任何)他们的输出。请注意,这是不同的   来自SELECTWITH的规则:如上一节所述,   只有主查询才会执行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 1
   FROM   child c
   WHERE  c.parent_id = l.parent_id
   AND    c.child_id <> l.child_id   -- !
   );

这种方式一次只能一个事务可以锁定同一个父项。因此,不可能发生多个事务删除同一父项的子项,仍然看到其他子项并保留父项,而所有子项都在之后消失。 ({1}}仍允许对非键列进行更新。)

如果这种情况从未发生过,或者你可以忍受它(几乎从未发生过) - 第一个查询更便宜。否则,这是安全的道路。

Postgres 9.4引入了

FOR NO KEY UPDATEDetails in the manual.在旧版本中,请使用更强的锁FOR NO KEY UPDATE

答案 1 :(得分:0)

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;