Cascade Deletes
There are 5 options for foreign key constraint deletes:
- CASCADE: When a row is deleted from the parent table, all related rows in the child table(s) are deleted as well.
- RESTRICT: When a row is deleted from the parent table, the delete operation is aborted if there are any related rows in the child table(s).
- SET NULL: When a row is deleted from the parent table, the values of the foreign key columns in the child table(s) are set to NULL.
- SET DEFAULT: When a row is deleted from the parent table, the values of the foreign key columns in the child table(s) are set to their default values.
- NO ACTION: This option is similar to RESTRICT, but it also has the option to be “deferred” to the end of a transaction. This means that other cascading deletes can run first, and then this delete constraint will only throw an error if there is referenced data remaining at the end of the transaction.
These options can be specified when defining a foreign key constraint using the "ON DELETE" clause. For example, the following SQL statement creates a foreign key constraint with the CASCADE
option:
_10alter table child_table_10add constraint fk_parent foreign key (parent_id) references parent_table (id)_10 on delete cascade;
This means that when a row is deleted from the "parent_table", all related rows in the "child_table" will be deleted as well.
RESTRICT
vs NO ACTION
The difference between NO ACTION
and RESTRICT
is subtle and can be a bit confusing.
Both NO ACTION
and RESTRICT
are used to prevent deletion of a row in a parent table if there are related rows in a child table. However, there is a subtle difference in how they behave.
When a foreign key constraint is defined with the option RESTRICT
, it means that if a row in the parent table is deleted, the database will immediately raise an error and prevent the deletion of the row in the parent table. The database will not delete, update or set to NULL any rows in the referenced table(s).
When a foreign key constraint is defined with the option NO ACTION
, it means that if a row in the parent table is deleted, the database will also raise an error and prevent the deletion of the row in the parent table. However unlike RESTRICT
, NO ACTION
has the option defer the check using INITIALLY DEFERRED
. This will only raise the above error if the referenced rows still exist at the end of the transaction.
The difference from RESTRICT
is that a constraint marked as NO ACTION INITIALLY DEFERRED
is deferred until the end of the transaction, rather than running immediately. If, for example there is another foreign key constraint between the same tables marked as CASCADE
, the cascade will occur first and delete the referenced rows, and no error will be thrown by the deferred constraint. Otherwise if there are still rows referencing the parent row by the end of the transaction, an error will be raised just like before. Just like RESTRICT
, the database will not delete, update or set to NULL any rows in the referenced table(s).
In practice, you can use either NO ACTION
or RESTRICT
depending on your needs. NO ACTION
is the default behavior if you do not specify anything. If you prefer to defer the check until the end of the transaction, use NO ACTION INITIALLY DEFERRED
.
Example
Let's further illustrate the difference with an example. We'll use the following data:
grandparent
id | name |
---|---|
1 | Elizabeth |
parent
id | name | parent_id |
---|---|---|
1 | Charles | 1 |
2 | Diana | 1 |
child
id | name | father | mother |
---|---|---|---|
1 | William | 1 | 2 |
To create these tables and their data, we run:
_39create table grandparent (_39 id serial primary key,_39 name text_39);_39_39create table parent (_39 id serial primary key,_39 name text,_39 parent_id integer references grandparent (id)_39 on delete cascade_39);_39_39create table child (_39 id serial primary key,_39 name text,_39 father integer references parent (id)_39 on delete restrict_39);_39_39insert into grandparent_39 (id, name)_39values_39 (1, 'Elizabeth');_39_39insert into parent_39 (id, name, parent_id)_39values_39 (1, 'Charles', 1);_39_39insert into parent_39 (id, name, parent_id)_39values_39 (2, 'Diana', 1);_39_39-- We'll just link the father for now_39insert into child_39 (id, name, father)_39values_39 (1, 'William', 1);
RESTRICT
RESTRICT
will prevent a delete and raise an error:
_10postgres=# delete from grandparent;_10ERROR: update or delete on table "parent" violates foreign key constraint "child_father_fkey" on table "child"_10DETAIL: Key (id)=(1) is still referenced from table "child".
Even though the foreign key constraint between parent and grandparent is CASCADE
, the constraint between child and father is RESTRICT
. Therefore an error is raised and no records are deleted.
NO ACTION
Let's change the child-father relationship to NO ACTION
:
_10alter table child_10drop constraint child_father_fkey;_10_10alter table child_10add constraint child_father_fkey foreign key (father) references parent (id)_10 on delete no action;
We see that NO ACTION
will also prevent a delete and raise an error:
_10postgres=# delete from grandparent;_10ERROR: update or delete on table "parent" violates foreign key constraint "child_father_fkey" on table "child"_10DETAIL: Key (id)=(1) is still referenced from table "child".
NO ACTION INITIALLY DEFERRED
We'll change the foreign key constraint between child and father to be NO ACTION INITIALLY DEFERRED
:
_10alter table child_10drop constraint child_father_fkey;_10_10alter table child_10add constraint child_father_fkey foreign key (father) references parent (id)_10 on delete no action initially deferred;
Here you will see that INITIALLY DEFFERED
seems to operate like NO ACTION
or RESTRICT
. When we run a delete, it seems to make no difference:
_10postgres=# delete from grandparent;_10ERROR: update or delete on table "parent" violates foreign key constraint "child_father_fkey" on table "child"_10DETAIL: Key (id)=(1) is still referenced from table "child".
But, when we combine it with other constraints, then any other constraints take precedence. For example, let's run the same but add a mother
column that has a CASCADE
delete:
_10alter table child_10add column mother integer references parent (id)_10 on delete cascade;_10_10update child_10set mother = 2_10where id = 1;
Then let's run a delete on the grandparent
table:
_12postgres=# delete from grandparent;_12DELETE 1_12_12postgres=# select * from parent;_12 id | name | parent_id_12----+------+-----------_12(0 rows)_12_12postgres=# select * from child;_12 id | name | father | mother_12----+------+--------+--------_12(0 rows)
The mother
deletion took precedence over the father
, and so William was deleted. After William was deleted, there was no reference to “Charles” and so he was free to be deleted, even though previously he wasn't (without INITIALLY DEFERRED
).