skip to content

Why does deleting a parent row fail with a foreign-key error, and in what order must you delete?

level: middleimportance: should knowfreq 65%

answer

  1. Constraint refuses to create orphans
  2. Statement is atomic — nothing is removed
  3. Follow the arrows backwards
  4. Leaves first, root last
  5. Cascading is declared in DDL, not requested at run time

basics

~20 s

A foreign key requires every child row to reference an existing parent, so removing a still-referenced parent would break that promise and the statement is rejected. Delete from the leaves inward: children first, parents last.

solid answer

~50 s

A `FOREIGN KEY` declares that each child value must match a parent key. Deleting a customer who still has orders would leave those orders pointing at nothing, so with the default referential action the engine rejects the `DELETE` and the whole statement removes no rows at all. The fix is ordering, not force: walk the reference graph from the leaves inward. ```sql BEGIN; DELETE FROM order_items WHERE order_id IN (SELECT id FROM orders WHERE customer_id = 7); DELETE FROM orders WHERE customer_id = 7; DELETE FROM customers WHERE id = 7; COMMIT; ``` Run them in one transaction so a failure anywhere leaves nothing half-deleted, and note that the child cleanup has to find the order ids *before* the orders are gone. Two escape hatches exist but are schema decisions, not statement ones: the foreign key can declare `ON DELETE CASCADE`, and standard `DEFERRABLE` constraints checked at `COMMIT` make the order within a transaction irrelevant — engine support for deferral varies, so check yours.

code

sql · 11 lines
sql
-- fails while orders still reference customer 7:
-- DELETE FROM customers WHERE id = 7;

BEGIN;
DELETE FROM order_items
 WHERE EXISTS (SELECT 1 FROM orders o
               WHERE o.id = order_items.order_id
                 AND o.customer_id = 7);
DELETE FROM orders    WHERE customer_id = 7;
DELETE FROM customers WHERE id = 7;
COMMIT;

go deeper

for a junior

Recognise the referential-integrity error on sight and know the rule it enforces: children must be deleted before the parent they reference.

for a middle

Explain that the statement is rejected atomically, derive the delete order from the reference graph, and know that cascading actions and deferrable constraints are schema declarations rather than statement options.

for a senior

Handle the awkward shapes: several children per parent, self-referencing trees, capturing intermediate id sets before the parent rows disappear, and doing the whole sequence in one transaction.

for a principal

Own the policy question — which relationships get cascading deletes, which keep strict rejection, and how routine purge and erasure operations are expressed so nobody is hand-ordering deletes on production under pressure.

## What the foreign key promises `FOREIGN KEY (customer_id) REFERENCES customers (id)` is a standing guarantee: for every row of `orders`, either `customer_id` is NULL or a `customers` row with that id exists. The database enforces the guarantee on **every** statement that could break it — inserts and updates on the child side, and deletes and key updates on the parent side. ## Why the parent delete is rejected When you run `DELETE FROM customers WHERE id = 7` and three orders still reference customer 7, completing the delete would leave those orders referencing a customer that does not exist. With the default referential action the engine refuses: you get a referential-integrity error, and because a SQL statement is atomic, **no** rows are removed — not the customer, not even the parts of the statement that would have been fine. Nothing is half-done, which is exactly the behaviour you want; the error is the constraint doing its job, not an obstacle to route around. ## Delete order is a topological order Draw the arrows the way the references point — child to parent — and delete against them, leaves first: ``` order_items ──> orders ──> customers ``` so the order is `order_items`, then `orders`, then `customers`. Generalised: a table may be deleted from only once nothing that still has rows references it. For a real schema with several children per parent and children of children, that is a topological sort of the reference graph. Practically, you find it by listing the foreign keys that point *at* each table you intend to empty and handling those tables first, recursively. Run the whole sequence inside one transaction: ```sql BEGIN; DELETE FROM order_items WHERE EXISTS (SELECT 1 FROM orders o WHERE o.id = order_items.order_id AND o.customer_id = 7); DELETE FROM orders WHERE customer_id = 7; DELETE FROM customers WHERE id = 7; COMMIT; ``` Note the ordering trap hidden in that script: the first statement must find the order ids *before* the orders are gone. If you delete the orders first, the child cleanup no longer knows which items to remove — and the parent delete would have failed anyway. Where the intermediate set is large or the correlation awkward, capture the ids first (into a temporary table or a list) and delete by that set. ## Self-referencing hierarchies A table can reference itself — `employees.manager_id REFERENCES employees(id)`, a category tree, a comment thread. The same rule applies within the one table: a row cannot go while another row still points at it. So you delete from the deepest level upward, or you first detach the children by setting their pointer to NULL (or reparenting them to the grandparent) and then delete. Collecting the subtree with a recursive query and deleting deepest-first is the usual approach; a single `DELETE ... WHERE id = :root` will simply fail while descendants exist. ## The two schema-level escape hatches **Cascading actions.** The foreign key can be declared with `ON DELETE CASCADE`, and then deleting the parent instructs the engine to remove the referencing children itself, recursively. That is a property of the constraint, declared in DDL when the table is created or altered — not something a `DELETE` statement can request at run time. It changes the blast radius of every future parent delete, which is precisely why it is a schema decision made deliberately rather than a convenience switched on to make an error go away. **Deferred checking.** SQL allows a constraint to be declared `DEFERRABLE` and then deferred within a transaction, so the check happens at `COMMIT` instead of at each statement. With deferral in force, order inside the transaction stops mattering — the parent may go first, provided the children are gone by commit time. Engine support for deferral genuinely differs, so treat it as something to verify in your database's documentation rather than assume. ## What interviewers listen for The candidate who says "just disable the constraints" has answered the wrong question. Turning enforcement off during a delete does not make the orphans acceptable; it makes them invisible until the constraint is re-enabled, and re-enabling it on a table full of orphans fails. The good answer is: read the reference graph, delete leaves first, wrap it in one transaction, and if this operation happens routinely, decide at the schema level whether the relationship deserves a cascading action.

  • If the parent DELETE fails, are any rows removed by that statement?
    No. A SQL statement is atomic, so a constraint violation rolls the whole statement back and the table is left exactly as it was — even if the statement targeted many rows and only one of them was still referenced. Inside an explicit transaction, earlier successful statements remain pending until you commit or roll back.
  • How do you handle a self-referencing foreign key such as employees.manager_id?
    The same leaves-first rule applies inside the single table. Either collect the subtree with a recursive query and delete the deepest rows first, or detach first — set the descendants' `manager_id` to NULL or reparent them — and then delete the row you wanted gone.
  • Why is disabling the foreign keys a poor answer to this error?
    It does not remove the inconsistency, it hides it: the child rows become orphans, and re-enabling or revalidating the constraint afterwards fails on exactly those rows. It also drops enforcement for every concurrent writer during the window, so unrelated bad data can slip in while it is off.

You cannot demolish the foundation while the floors above still rest on it. Take the building down from the top, storey by storey — or arrange in advance for the demolition to bring the upper floors with it.

saying these in an interview costs you the question

  • Suggests disabling constraints to make the error go away
  • Deletes the parent first and expects children to vanish
  • Thinks a failed DELETE still removes the unreferenced rows
  • Believes ON DELETE CASCADE can be requested inside a DELETE statement
  • Ignores self-referencing foreign keys within one table

context