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

zer*_*ble 7 sql postgresql referential-integrity foreign-keys common-table-expression

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

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

Erw*_*ter 10

在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   -- !
   );
Run Code Online (Sandbox Code Playgroud)

SQL小提琴.

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

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

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

子语句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   -- !
   );
Run Code Online (Sandbox Code Playgroud)

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

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

FOR NO KEY UPDATE与Postgres 9.4一起介绍.手册中的详细信息.在旧版本中使用更强的锁定FOR UPDATE.