Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

SQL (PostgreSQL, MySQL, SQLite)  Module 9 – Creating tables and constraints

Foreign keys and ON DELETE actions

Declare foreign keys in PostgreSQL, MySQL and SQLite, choose CASCADE, SET NULL or RESTRICT for each link, and defer checks for batch and circular inserts.

  • Intermediate
  • 25 minutes
  • Examples run with sql.js 1.14.2, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 9 of 25 minutes free
Free: Three links, three actions · Writing a foreign key · Choosing an action for each link · 2 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 16 minutes.

What you will learn

  • Declare foreign keys and choose a referential action for each relationship
  • Switch on foreign-key enforcement in SQLite and check data written without it
  • Use deferrable constraints for batch loads and circular references
  • Index foreign key columns and explain what a missing index costs

Before you start

What you’ll learn · Before you start

What you will learn

  • Declare foreign keys and choose a referential action for each relationship
  • Switch on foreign-key enforcement in SQLite and check data written without it
  • Use deferrable constraints for batch loads and circular references
  • Index foreign key columns and explain what a missing index costs

Before you start

On this page

A foreign key ties a child row to a parent row: orders.customer_id must name a customer who exists. Foreign keys refuse two things, a child that points to a missing parent and a parent that disappears while children still point to it, and the second rule is where the ON DELETE actions come in. They decide what happens to the children when their parent is deleted: CASCADE deletes them too, SET NULL empties their link, SET DEFAULT puts the column’s default in it, and RESTRICT or NO ACTION refuse the delete while children exist. Choosing one per relationship is real design work: an order’s items belong to the order and can go with it, a customer who has orders must not vanish, and an optional link, such as the agent who delivered an order, can simply be forgotten. Every example on this page runs in sql.js 1.14.2, which packages SQLite 3.49.1, and like any SQLite connection it checks foreign keys only after PRAGMA foreign_keys = ON.

CASCADE, SET NULL and RESTRICT in one shop SQL · actions.sql
-- One shop, three links, three ON DELETE actions.
PRAGMA foreign_keys = ON;   -- SQLite checks foreign keys only after this, on every new connection

CREATE TABLE customers (customer_id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE agents    (agent_id    INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE orders (
  order_id    INTEGER PRIMARY KEY,
  customer_id INTEGER NOT NULL REFERENCES customers (customer_id) ON DELETE RESTRICT,
  agent_id    INTEGER          REFERENCES agents (agent_id)       ON DELETE SET NULL
);
CREATE TABLE order_items (
  order_id INTEGER NOT NULL REFERENCES orders (order_id) ON DELETE CASCADE,
  product  TEXT    NOT NULL,
  qty      INTEGER NOT NULL,
  PRIMARY KEY (order_id, product)
);

INSERT INTO customers VALUES (1, 'Asha Kulkarni'), (2, 'Vikram Nair');
INSERT INTO agents    VALUES (7, 'Ravi'), (8, 'Fatima');
INSERT INTO orders    VALUES (101, 1, 7), (102, 1, 8), (103, 2, 7);
INSERT INTO order_items VALUES
  (101, 'Basmati rice 1 kg', 2), (101, 'Toor dal 500 g', 1), (102, 'Masala chai 250 g', 2), (103, 'Coconut oil 1 L', 1);

DELETE FROM orders    WHERE order_id = 101;     -- CASCADE: the order's two items go with it
DELETE FROM agents    WHERE agent_id = 7;       -- SET NULL: order 103 stays, without an agent
DELETE FROM customers WHERE customer_id = 1;    -- RESTRICT: refused, Asha still has order 102

SELECT order_id, customer_id, agent_id FROM orders;
SELECT order_id, product, qty FROM order_items;

Output (exit status 1)

┌──────────┬─────────────┬──────────┐
│ order_id │ customer_id │ agent_id │
├──────────┼─────────────┼──────────┤
│ 102      │ 1           │ 8        │
│ 103      │ 2           │          │
└──────────┴─────────────┴──────────┘
┌──────────┬───────────────────┬─────┐
│ order_id │      product      │ qty │
├──────────┼───────────────────┼─────┤
│ 102      │ Masala chai 250 g │ 2   │
│ 103      │ Coconut oil 1 L   │ 1   │
└──────────┴───────────────────┴─────┘

Printed as an error (standard error)

Runtime error near line 26: FOREIGN KEY constraint failed

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < actions.sql

Each delete did what its action promises. Deleting order 101 took its two items with it, because order_items declares CASCADE. Deleting agent Ravi kept order 103 but set its agent_id to NULL, because the link is optional and declares SET NULL. Deleting Asha failed, because she still has order 102 and the link from orders declares RESTRICT; SQLite’s message, “FOREIGN KEY constraint failed”, does not say which key, while PostgreSQL names the constraint and the key value, and MySQL prints the whole foreign key definition with error 1451.

Deleting a customer with orders is refused (RESTRICT), an agent leaves orders without one (SET NULL), an order takes its items (CASCADE).customerscustomer_id (key)agentsagent_id (key)orderscustomer_id NOT NULLagent_id may be NULLorder_itemsorder_id NOT NULL(deleted with its order)ON DELETERESTRICTON DELETESET NULLON DELETECASCADE

What deleting a parent row does to its children

Text description of the diagram

The diagram shows the four tables of the lesson's shop and the action on each foreign key. Each arrow runs from a parent table to the child table that refers to it.

  1. customers to orders, ON DELETE RESTRICT: every order needs a customer, so deleting a customer who still has orders is refused.
  2. agents to orders, ON DELETE SET NULL: an order's delivery agent is optional, so deleting an agent keeps the orders and sets their agent_id to NULL.
  3. orders to order_items, ON DELETE CASCADE: items belong to their order, so deleting an order deletes its items as well.

Writing a foreign key

A foreign key can follow its column, as in the example, or stand among the table constraints, where it can span several columns and take a name: CONSTRAINT items_order_fk FOREIGN KEY (order_id) REFERENCES orders (order_id) ON DELETE CASCADE. The parent’s columns must be its primary key or carry a UNIQUE constraint, so that each value points to exactly one row. The types must match as well: PostgreSQL 18.6 refused a text column pointing to an integer key because the types are incompatible, and MySQL 8.4.11 refused it with error 3780. A child column that allows NULL makes the link optional, because a NULL points nowhere and is never checked. Name the constraint as you would a CHECK: PostgreSQL and MySQL print the name in every error about it, and changing the action later means dropping the constraint by that name and adding it again.

The ON DELETE actions
Criterion CASCADESET NULLSET DEFAULTRESTRICTNO ACTION
What happens to the children They are deleted tooTheir foreign key becomes NULLTheir foreign key takes its defaultThe delete is refusedThe delete is refused
What the child column needs NothingIt must allow NULLA default that names an existing parentNothingNothing
When the check happens At onceAt onceAt onceAt once, never deferredAt the end of the statement, or at COMMIT when deferred
Typical link Order items to their orderAn optional delivery agentRare: an "unassigned" parent rowOrders to the customer who placed themThe same, and the default when no action is named
When to choose The children are part of the parentThe link is optional and the child stands aloneEvery child must point somewhere, and a fallback parent existsThe children record history that must stayAs RESTRICT, when a transaction may repair the link before it ends

Ask one question per link: do the children mean anything without their parent? Items of a deleted order mean nothing, so CASCADE. A delivery means something without its agent, so SET NULL. An order is a financial record, so a customer who has orders must not be deleted at all, and RESTRICT makes the database refuse; such long-lived records are usually marked inactive instead of deleted. NO ACTION, the default when you name no action, refuses the same deletes as RESTRICT and differs only in when it looks, which matters inside transactions. ON UPDATE takes the same actions for a parent key that changes, which surrogate keys never do. MySQL’s InnoDB does not carry out SET DEFAULT, so keep that action for PostgreSQL and SQLite.

Turning the checks on

PostgreSQL always enforces foreign keys. SQLite enforces them only on a connection that has run PRAGMA foreign_keys = ON, and sql.js starts with it off, as SQLite does unless it was compiled otherwise, so every program that opens the database must switch it on, and data written by one that did not may already hold orphans. MySQL’s InnoDB enforces foreign keys written as a FOREIGN KEY clause, but ignores a REFERENCES written after a column, as the lesson on keys and relationships showed, and a session can switch every check off with SET FOREIGN_KEY_CHECKS = 0, which dumps and bulk loads often do. Switching the checks back on in SQLite or MySQL does not look at the rows that are already there.

Your turn

Exercise · Easy · SQL

Foreign keys for a college recruitment portal

A college's recruitment portal has four tables: recruiters, the drives each recruiter holds, students, and the applications students make to drives. Add the foreign keys, each with its ON DELETE action:

  • drives.recruiter_id points to recruiters. A recruiter who still has drives must never be deleted: use RESTRICT.
  • applications.drive_id points to drives. When a drive is cancelled and deleted, its applications go with it.
  • applications.student_id points to students. When a student's record is deleted, their applications go with it.

Leave the last statement as it is: it reads the foreign keys back from SQLite's catalog, one row per link, so the check compares the design you wrote, not the data.

Starter code · recruitment.sql

PRAGMA foreign_keys = ON;

-- Add the three foreign keys, each with its ON DELETE action.
CREATE TABLE recruiters (recruiter_id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE drives (
  drive_id     INTEGER PRIMARY KEY,
  recruiter_id INTEGER NOT NULL,
  held_on      TEXT    NOT NULL
);
CREATE TABLE students (student_id INTEGER PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE applications (
  drive_id   INTEGER NOT NULL,
  student_id INTEGER NOT NULL,
  PRIMARY KEY (drive_id, student_id)
);

-- Leave this as it is: the foreign keys SQLite now knows about, one row per link.
SELECT m.name AS child_table, f."from" AS child_column, f."table" AS parent_table, f.on_delete
FROM sqlite_schema AS m, pragma_foreign_key_list(m.name) AS f
WHERE m.type = 'table'
ORDER BY child_table, child_column;
The sample tests · tests.yaml
# Sample test: your script creates the tables, and its last query lists their foreign keys from SQLite's catalog.
tests:
  - name: three foreign keys, with RESTRICT for recruiters and CASCADE for applications
    ordered: true
    setup: ''
    expected:
      columns: [child_table, child_column, parent_table, on_delete]
      rows:
        - ['applications', 'drive_id', 'drives', 'CASCADE']
        - ['applications', 'student_id', 'students', 'CASCADE']
        - ['drives', 'recruiter_id', 'recruiters', 'RESTRICT']
A hint

A foreign key can follow the column's type: recruiter_id INTEGER NOT NULL REFERENCES recruiters (recruiter_id) ON DELETE RESTRICT. The action that deletes the children together with their parent is CASCADE.

The sample tests run on this device, in your browser (sql.js): nothing is sent to mysmartcopilot.com. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

3 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 3 order_items.order_id is declared REFERENCES orders (order_id) ON DELETE CASCADE, and foreign keys are switched on. What does DELETE FROM orders WHERE order_id = 101 do to the items of order 101?

    Choose one answer.

    Show the answer to question 1

    Answer: They are deleted in the same statement

    CASCADE deletes the children together with their parent, in the same statement. SET NULL would empty their order_id, RESTRICT or NO ACTION would refuse the delete, and only a connection without foreign-key checks would leave them pointing to a missing order.

  2. Question 2 of 3 Which ON DELETE action fails whenever the child's foreign key column is declared NOT NULL?

    Choose one answer.

    Show the answer to question 2

    Answer: SET NULL

    SET NULL writes NULL into the child's column, so a NOT NULL on that column makes the parent's delete fail. The other actions never write into the child row.

  3. Question 3 of 3 A program opens the shop's SQLite database on a new connection and, without running any PRAGMA, deletes customer 1, who still has an order. What happens?

    Choose one answer.

    Show the answer to question 3

    Answer: The delete succeeds, and the order now points to a customer who does not exist

    A new SQLite connection does not check foreign keys until it runs PRAGMA foreign_keys = ON, so no action and no check runs. The orphan stays until pragma_foreign_key_check or a LEFT JOIN finds it.

SQL to ER Diagram Paste your CREATE TABLE statements to see every foreign key drawn as a line between its tables.

Each action in depth: what it removes, when it checks, how it fails Pro: opens with a Pro, Premium or Ultimate pass

Every action has a failure that shows up only in production: a cascade that removes more than anyone expected, a check that runs earlier than the code assumed, a SET that breaks the child’s own rules. This part reproduces each one.

In this part:

  • What one DELETE removes, measured
  • RESTRICT or NO ACTION: the difference is timing
  • SET NULL and SET DEFAULT, failure cases reproduced
  • ON UPDATE, for keys that change

3 runnable examples (SQL) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Deferred checks, data loads and the index every foreign key needs, with numbers Pro: opens with a Pro, Premium or Ultimate pass

Foreign keys also decide how data can be loaded and how fast a parent can be deleted. This part covers inserts that need each other, checks within one statement, the cost of a missing index measured in comparisons, and the repair of data written while checks were off.

In this part:

  • Circular references with deferred constraints
  • When each engine checks within one statement
  • The index every foreign key needs, measured
  • Finding orphans after the checks were off

3 runnable examples (Python, SQL) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Mid to senior

    A support agent deleted a duplicate customer account, and finance then found that the customer's 214 orders and their payments had disappeared too. Every foreign key in the schema says ON DELETE CASCADE. What happened, how do you recover, and how should these foreign keys be declared?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    In PostgreSQL, what is the difference between ON DELETE RESTRICT and ON DELETE NO ACTION, and when does it matter? Describe a transaction that works with one and fails with the other.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    Deleting one customer in PostgreSQL takes about 4 seconds, although the customers table has only 50,000 rows and the DELETE finds the row by its primary key. What do you look for, and how do you fix it?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Foreign keys refuse a child without a parent; the ON DELETE actions decide what deleting a parent does to its children.
  • CASCADE for children that belong to the parent, SET NULL for optional links, RESTRICT or NO ACTION for history that must stay.
  • NO ACTION is the default; it refuses the same deletes as RESTRICT and differs only in timing.
  • SQLite checks foreign keys only after PRAGMA foreign_keys = ON, on every connection; MySQL ignores a column-level REFERENCES.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.