数据库

级联删除


外键约束删除有5种选项:

  1. CASCADE(级联删除): 当父表中的行被删除时,子表中所有关联行也会被删除。
  2. RESTRICT(限制删除): 当父表中的行被删除时,如果子表中存在关联行,则删除操作会被中止。
  3. SET NULL(设为空值): 当父表中的行被删除时,子表中外键列的值会被设为NULL。
  4. SET DEFAULT(设为默认值): 当父表中的行被删除时,子表中外键列的值会被设为其默认值。
  5. NO ACTION(无动作): 该选项与RESTRICT类似,但可以选择"延迟"到事务结束时检查。这意味着其他级联删除可以先执行,只有当事务结束时仍有被引用的数据存在时才会抛出错误。

这些选项可以在定义外键约束时使用"ON DELETE"子句指定。例如,以下SQL语句创建了一个使用CASCADE选项的外键约束:

1
2
3
alter table child_tableadd constraint fk_parent foreign key (parent_id) references parent_table (id) on delete cascade;

这意味着当从parent_table中删除一行时,child_table中所有关联行也会被删除。

RESTRICTNO ACTION 的区别

NO ACTIONRESTRICT 之间的区别非常微妙,可能会让人感到困惑。

两者都用于在子表中存在关联行时阻止父表行的删除操作,但它们在行为上存在细微差别。

当外键约束定义为 RESTRICT 选项时,意味着如果尝试删除父表中的某行,数据库会立即抛出错误并阻止该删除操作。数据库不会删除、更新或将引用表中的任何行设为 NULL。

当外键约束定义为 NO ACTION 选项时,意味着如果尝试删除父表中的某行,数据库同样会抛出错误并阻止删除操作。但与 RESTRICT 不同的是,NO ACTION 可以通过 INITIALLY DEFERRED 选项延迟检查。这种情况下,只有当事务结束时引用行仍然存在,才会触发上述错误。

RESTRICT 的关键区别在于:标记为 NO ACTION INITIALLY DEFERRED 的约束会延迟到事务结束时才检查,而非立即执行。例如,如果相同表之间存在另一个标记为 CASCADE 的外键约束,级联操作会先执行并删除引用行,这样延迟约束就不会抛出错误。反之,如果到事务结束时仍有行引用父行,则会像之前一样抛出错误。与 RESTRICT 相同,数据库不会删除、更新或将引用表中的任何行设为 NULL。

实际应用中,您可以根据需求选择使用 NO ACTIONRESTRICT。如果不指定任何选项,默认行为是 NO ACTION。如果您希望将检查延迟到事务结束,请使用 NO ACTION INITIALLY DEFERRED

示例

让我们通过一个示例进一步说明两者的区别。我们将使用以下数据:

grandparent(祖父母表)

idname
1Elizabeth

parent(父母表)

idnameparent_id
1Charles1
2Diana1

child(子女表)

idnamefathermother
1William12

创建这些表及其数据的SQL语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
create table grandparent ( id serial primary key, name text);create table parent ( id serial primary key, name text, parent_id integer references grandparent (id) on delete cascade);create table child ( id serial primary key, name text, father integer references parent (id) on delete restrict);insert into grandparent (id, name)values (1, 'Elizabeth');insert into parent (id, name, parent_id)values (1, 'Charles', 1);insert into parent (id, name, parent_id)values (2, 'Diana', 1);-- 目前我们只关联父亲insert into child (id, name, father)values (1, 'William', 1);

RESTRICT 约束

RESTRICT 会阻止删除操作并抛出错误:

1
2
3
postgres=# delete from grandparent;ERROR: update or delete on table "parent" violates foreign key constraint "child_father_fkey" on table "child"DETAIL: Key (id)=(1) is still referenced from table "child".

尽管父母表与祖父母表之间的外键约束是CASCADE,但子女表与父亲之间的约束是RESTRICT。因此会抛出错误且不会删除任何记录。

设置 NO ACTION 约束

让我们将子表-父表关系改为 NO ACTION 约束:

1
2
3
4
5
6
alter table childdrop constraint child_father_fkey;alter table childadd constraint child_father_fkey foreign key (father) references parent (id) on delete no action;

可以看到 NO ACTION 同样会阻止删除操作并报错:

1
2
3
postgres=# delete from grandparent;ERROR: update or delete on table "parent" violates foreign key constraint "child_father_fkey" on table "child"DETAIL: Key (id)=(1) is still referenced from table "child".

NO ACTION INITIALLY DEFERRED

我们将修改子表与父表之间的外键约束为NO ACTION INITIALLY DEFERRED

1
2
3
4
5
6
alter table childdrop constraint child_father_fkey;alter table childadd constraint child_father_fkey foreign key (father) references parent (id) on delete no action initially deferred;

这里你会发现INITIALLY DEFFERED的行为似乎与NO ACTIONRESTRICT类似。当我们执行删除操作时,似乎没有区别:

1
2
3
postgres=# delete from grandparent;ERROR: update or delete on table "parent" violates foreign key constraint "child_father_fkey" on table "child"DETAIL: Key (id)=(1) is still referenced from table "child".

但是,当与其他约束结合使用时,其他约束会优先处理。例如,我们执行相同操作,但添加一个具有CASCADE删除的mother列:

1
2
3
4
5
6
7
alter table childadd column mother integer references parent (id) on delete cascade;update childset mother = 2where id = 1;

然后对grandparent表执行删除:

1
2
3
4
5
6
7
8
9
10
11
12
postgres=# delete from grandparent;DELETE 1postgres=# select * from parent; id | name | parent_id----+------+-----------(0 rows)postgres=# select * from child; id | name | father | mother----+------+--------+--------(0 rows)

mother列的删除操作优先于father列,因此William被删除了。在William被删除后,就不再存在对"Charles"的引用,所以即使之前不能删除(没有INITIALLY DEFERRED),现在也可以自由删除了。