🛡️ Lesson 12: Foreign Keys & Referential Integrity
In the last lesson, you designed tables with foreign key columns — but what stops someone from inserting an order with customer_id = 999 when no such customer exists? What happens when you delete a customer who has orders? Foreign key constraints are the rules MySQL enforces to keep your data consistent and your relationships trustworthy.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain what referential integrity means and why it matters
- Create foreign key constraints with
FOREIGN KEY ... REFERENCES - Choose the right
ON DELETEaction: CASCADE, SET NULL, RESTRICT, NO ACTION - Choose the right
ON UPDATEaction for your use case - Add, drop, and modify foreign keys on existing tables
- Troubleshoot common foreign key errors
Estimated Time: 45 minutes
Prerequisites: Table relationships (Lesson 11)
📑 In This Lesson
What Is Referential Integrity?
Referential integrity means every foreign key value in a child table must match an existing primary key value in the parent table (or be NULL, if the FK allows NULLs). In plain English: you can't reference something that doesn't exist.
Without Foreign Key Constraints
Without constraints, MySQL happily accepts bad data:
-- Without a FK constraint, this succeeds even though customer 999 doesn't exist!
INSERT INTO orders (customer_id, order_date, total)
VALUES (999, '2026-04-15', 29.99);
-- No error — but now you have an "orphan" order pointing to nobody
With Foreign Key Constraints
With constraints, MySQL blocks invalid references:
-- With a FK constraint, this FAILS:
INSERT INTO orders (customer_id, order_date, total)
VALUES (999, '2026-04-15', 29.99);
-- ERROR 1452: Cannot add or update a child row:
-- a foreign key constraint fails
✅ What Referential Integrity Guarantees
- No orphan records — every child row points to a real parent
- No broken links — you can't delete a parent while children still reference it (unless you specify what should happen)
- Data consistency — relationships are always valid, enforced at the database level
- Application safety — even if application code has bugs, the database prevents corruption
Creating Foreign Key Constraints
Inline Syntax (During CREATE TABLE)
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_id INT NOT NULL,
order_date DATE NOT NULL,
total DECIMAL(10, 2) DEFAULT 0,
-- Foreign key constraint
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
Named Constraints
Giving your constraints explicit names makes them easier to manage later:
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_id INT NOT NULL,
order_date DATE NOT NULL,
total DECIMAL(10, 2) DEFAULT 0,
-- Named foreign key constraint
CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
💡 Naming Convention
A common pattern is fk_childtable_parenttable or fk_childtable_column. For example:
fk_orders_customer— orders table's FK to customersfk_order_items_order— order_items table's FK to ordersfk_order_items_book— order_items table's FK to books
If you don't name them, MySQL generates names like orders_ibfk_1 — not very helpful when debugging.
Requirements for Foreign Keys
Both the FK column and the referenced column must:
| Requirement | Details |
|---|---|
| Same data type | INT must reference INT, not VARCHAR |
| Same size/sign | INT UNSIGNED must reference INT UNSIGNED |
| Referenced column must be indexed | Primary keys are automatically indexed; other columns need an explicit index |
| InnoDB engine | Foreign keys only work with InnoDB (MySQL's default engine) |
ON DELETE — What Happens When You Delete a Parent
The big question: what should MySQL do when you try to delete a customer who has orders? You have four choices:
RESTRICT (The Default)
Blocks the delete entirely if any child rows exist:
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_id INT NOT NULL,
CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE RESTRICT
);
-- If customer 1 has orders:
DELETE FROM customers WHERE id = 1;
-- ERROR 1451: Cannot delete or update a parent row:
-- a foreign key constraint fails
This is the safest option — it forces you to deal with the child rows first.
CASCADE — Delete Everything
Automatically deletes all child rows when the parent is deleted:
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_id INT NOT NULL,
CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE CASCADE
);
-- Deleting customer 1 ALSO deletes all their orders automatically
DELETE FROM customers WHERE id = 1;
-- Customer gone, all their orders gone too
⚠️ CASCADE Can Be Dangerous
CASCADE can trigger chain reactions. If orders cascade-delete, and order_items also cascade on orders, then deleting one customer wipes out the customer + all their orders + all their order items. This might be exactly what you want (cleaning up a test user) — or it might be disastrous (accidentally deleting years of sales history).
Rule of thumb: Only use CASCADE when the child data truly has no meaning without the parent.
SET NULL — Preserve the Child, Break the Link
Keeps the child rows but sets their FK column to NULL:
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_id INT, -- Must allow NULL for SET NULL to work!
CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE SET NULL
);
-- Deleting customer 1 sets their orders' customer_id to NULL
DELETE FROM customers WHERE id = 1;
-- Customer gone, but orders still exist with customer_id = NULL
💡 SET NULL Requires a Nullable Column
If the FK column is NOT NULL, ON DELETE SET NULL will fail with an error. Make sure the column allows NULLs if you use this option.
NO ACTION
In MySQL, NO ACTION is functionally identical to RESTRICT — it blocks the delete. The distinction matters in other databases that support deferred constraint checking, but in MySQL they behave the same way.
Comparison
| Action | Parent Deleted? | Child Rows | When to Use |
|---|---|---|---|
RESTRICT |
No — blocked | Unchanged | Must manually handle children first |
CASCADE |
Yes | Deleted automatically | Children meaningless without parent |
SET NULL |
Yes | FK set to NULL | Children should survive, link is optional |
NO ACTION |
No — blocked | Unchanged | Same as RESTRICT in MySQL |
ON UPDATE — What Happens When You Change a Parent's Key
ON UPDATE controls what happens when you change a parent's primary key value. The options are the same as ON DELETE:
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_id INT NOT NULL,
CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE RESTRICT
ON UPDATE CASCADE -- if customer.id changes, update all orders too
);
| Action | What Happens |
|---|---|
RESTRICT |
Blocks the update if any children reference this PK |
CASCADE |
Updates all child FK values to match the new PK |
SET NULL |
Sets child FK values to NULL |
NO ACTION |
Same as RESTRICT in MySQL |
💡 In Practice, ON UPDATE Is Rarely Needed
If your primary keys use AUTO_INCREMENT (as they should in most cases), you'll almost never change a PK value. The ID is just a surrogate key — it has no real-world meaning to change. So ON UPDATE CASCADE is mostly a safety net.
If you use natural keys (like an email address as a PK), then ON UPDATE CASCADE becomes very useful — emails do change.
Combining ON DELETE and ON UPDATE
-- A practical combination
CREATE TABLE order_items (
id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL,
book_id INT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
price_at_time DECIMAL(6, 2) NOT NULL,
CONSTRAINT fk_items_order
FOREIGN KEY (order_id) REFERENCES orders(id)
ON DELETE CASCADE -- delete items when order is deleted
ON UPDATE CASCADE,
CONSTRAINT fk_items_book
FOREIGN KEY (book_id) REFERENCES books(id)
ON DELETE RESTRICT -- can't delete a book that's been ordered
ON UPDATE CASCADE
);
Notice how different relationships use different rules: order items should disappear with their order (CASCADE), but you shouldn't be able to delete a book that appears in past orders (RESTRICT).
Choosing the Right Action
Here's a decision framework for choosing your ON DELETE action:
Common Patterns
| Scenario | ON DELETE | Reasoning |
|---|---|---|
| Order → Order Items | CASCADE | Line items are meaningless without their order |
| Customer → Orders | RESTRICT | Don't lose sales history; deactivate instead of deleting |
| Book → Order Items | RESTRICT | Can't delete a book that's part of order history |
| Author → Books | RESTRICT | Don't delete an author with published books |
| Post → Comments | CASCADE | Comments belong to the post |
| User → Posts | SET NULL | Keep posts but show "deleted user" |
| Department → Employees | SET NULL | Employees still exist if department is dissolved |
| Book ↔ Categories (junction) | CASCADE | Remove links when either side is deleted |
✅ The Golden Rule
When in doubt, use RESTRICT. It's the safest default — it prevents accidental data loss and forces you to think about the consequences. You can always change it later, but you can't un-delete cascaded rows.
Adding & Dropping Foreign Keys
You can add foreign keys to existing tables with ALTER TABLE:
Adding a Foreign Key
-- Add a FK to an existing table
ALTER TABLE orders
ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE RESTRICT
ON UPDATE CASCADE;
⚠️ Existing Data Must Be Valid
If the orders table already has rows with customer_id values that don't exist in customers, the ALTER TABLE will fail. You must fix the invalid data first:
-- Find orphan orders
SELECT o.id, o.customer_id
FROM orders o
LEFT JOIN customers c ON o.customer_id = c.id
WHERE c.id IS NULL;
-- Fix them (delete or update) before adding the constraint
DELETE FROM orders WHERE customer_id NOT IN (SELECT id FROM customers);
Dropping a Foreign Key
-- Drop by constraint name
ALTER TABLE orders DROP FOREIGN KEY fk_orders_customer;
-- If you also want to drop the index MySQL created:
ALTER TABLE orders DROP INDEX fk_orders_customer;
💡 Why Drop a FK?
Common reasons include: changing the constraint rules (drop and re-add), bulk importing data where temporary constraint violations are expected, or restructuring the schema. Always re-add constraints after bulk operations.
Changing a Foreign Key's Rules
You can't directly modify an FK constraint — you have to drop and recreate it:
-- Step 1: Drop the old constraint
ALTER TABLE orders DROP FOREIGN KEY fk_orders_customer;
-- Step 2: Add it back with new rules
ALTER TABLE orders
ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers(id)
ON DELETE CASCADE -- changed from RESTRICT to CASCADE
ON UPDATE CASCADE;
Temporarily Disabling FK Checks
For bulk data operations, you can temporarily disable FK checking:
-- Disable FK checks
SET FOREIGN_KEY_CHECKS = 0;
-- Perform bulk operations...
-- (imports, migrations, table restructuring)
-- Re-enable FK checks
SET FOREIGN_KEY_CHECKS = 1;
⚠️ Always Re-Enable!
Forgetting to turn FK checks back on leaves your database unprotected. Invalid data can sneak in and cause problems later. Make it a habit to pair every SET FOREIGN_KEY_CHECKS = 0 with a matching SET FOREIGN_KEY_CHECKS = 1.
Viewing Foreign Key Information
Several ways to see what foreign keys exist on a table:
SHOW CREATE TABLE
SHOW CREATE TABLE orders;
-- Shows the full CREATE TABLE including all constraints
Information Schema
-- List all FKs for a specific table
SELECT
CONSTRAINT_NAME,
COLUMN_NAME,
REFERENCED_TABLE_NAME,
REFERENCED_COLUMN_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = 'bookstore'
AND TABLE_NAME = 'orders'
AND REFERENCED_TABLE_NAME IS NOT NULL;
Output:
+---------------------+-------------+-----------------------+------------------------+
| CONSTRAINT_NAME | COLUMN_NAME | REFERENCED_TABLE_NAME | REFERENCED_COLUMN_NAME |
+---------------------+-------------+-----------------------+------------------------+
| fk_orders_customer | customer_id | customers | id |
+---------------------+-------------+-----------------------+------------------------+
Checking ON DELETE/ON UPDATE Rules
-- See the actual rules for each FK
SELECT
CONSTRAINT_NAME,
DELETE_RULE,
UPDATE_RULE
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = 'bookstore'
AND TABLE_NAME = 'orders';
Output:
+---------------------+-------------+-------------+
| CONSTRAINT_NAME | DELETE_RULE | UPDATE_RULE |
+---------------------+-------------+-------------+
| fk_orders_customer | RESTRICT | CASCADE |
+---------------------+-------------+-------------+
Troubleshooting FK Errors
Foreign key errors are among the most common MySQL frustrations. Here's how to handle each one:
Error 1452: Cannot Add or Update a Child Row
-- ERROR 1452: Cannot add or update a child row:
-- a foreign key constraint fails
-- Cause: You're inserting a FK value that doesn't exist in the parent table
INSERT INTO orders (customer_id, order_date) VALUES (999, '2026-04-15');
-- Fix: Use a customer_id that actually exists
INSERT INTO orders (customer_id, order_date) VALUES (1, '2026-04-15');
Error 1451: Cannot Delete or Update a Parent Row
-- ERROR 1451: Cannot delete or update a parent row:
-- a foreign key constraint fails
-- Cause: You're deleting a parent that has children (RESTRICT is active)
DELETE FROM customers WHERE id = 1;
-- Fix: Delete or reassign children first
DELETE FROM orders WHERE customer_id = 1;
DELETE FROM customers WHERE id = 1;
Error 1215: Cannot Add Foreign Key Constraint
-- ERROR 1215: Cannot add foreign key constraint
-- Common causes:
-- 1. Data type mismatch
ALTER TABLE orders ADD FOREIGN KEY (customer_id) REFERENCES customers(id);
-- customer_id is INT but customers.id is INT UNSIGNED → FAIL
-- 2. Referenced column isn't indexed
-- (Primary keys are automatically indexed — this happens with non-PK references)
-- 3. Referenced table doesn't exist yet
-- Fix: Create parent table first
-- 4. Different character sets/collations on the columns
-- Fix: Ensure both use the same charset
Debugging with SHOW ENGINE
-- Get detailed FK error information
SHOW ENGINE INNODB STATUS;
-- Look for the "LATEST FOREIGN KEY ERROR" section
✅ FK Error Checklist
- Do the data types match exactly (including UNSIGNED)?
- Does the referenced table and column exist?
- Is the referenced column indexed (PK or explicit index)?
- Are both tables using InnoDB?
- Does the existing data satisfy the constraint?
- Do the character sets and collations match?
Exercises
🏋️ Exercise 1: Create Tables with FK Constraints
Write the CREATE TABLE statements for the following, including named foreign key constraints with appropriate ON DELETE rules:
- A
departmentstable (id, name) - An
employeestable (id, name, email, department_id) — if a department is deleted, set the employee's department to NULL - An
assignmentstable (id, employee_id, project_name) — if an employee is deleted, delete their assignments too
✅ Solution
CREATE TABLE departments (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
CREATE TABLE employees (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE,
department_id INT,
CONSTRAINT fk_employees_department
FOREIGN KEY (department_id) REFERENCES departments(id)
ON DELETE SET NULL
ON UPDATE CASCADE
);
CREATE TABLE assignments (
id INT AUTO_INCREMENT PRIMARY KEY,
employee_id INT NOT NULL,
project_name VARCHAR(200) NOT NULL,
CONSTRAINT fk_assignments_employee
FOREIGN KEY (employee_id) REFERENCES employees(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
🏋️ Exercise 2: Choose the Right ON DELETE
For each scenario, choose the most appropriate ON DELETE action and explain why:
- A
reviewstable linked to aproductstable — what happens to reviews when a product is deleted? - A
messagestable linked to auserstable — what happens to messages when a user is deleted? - A
invoicestable linked to aclientstable — what happens to invoices when a client is deleted? - A
student_coursesjunction table linked tostudents— what happens to enrollments when a student is deleted?
✅ Solution
- CASCADE — Reviews are about that specific product; they're meaningless without it. Or RESTRICT if you want to archive products rather than delete them.
- SET NULL — Messages should be preserved (for the other participants) but show "deleted user." Or CASCADE if messages should disappear with the user (like private DMs).
- RESTRICT — Invoices are legal/financial records and should never be automatically deleted. Deactivate the client instead.
- CASCADE — Enrollment records link to the student; if the student is removed, their enrollments should go too.
Note: These are common choices, but the "right" answer depends on your specific business requirements. The important thing is that you can justify your choice.
🏋️ Exercise 3: Alter Existing Tables
Write the SQL to:
- Add a foreign key named
fk_books_authorto the books table, linkingauthor_idtoauthors(id)with ON DELETE RESTRICT - Drop that foreign key
- Re-add it with ON DELETE SET NULL (assume you've made author_id nullable)
✅ Solution
-- 1. Add the FK
ALTER TABLE books
ADD CONSTRAINT fk_books_author
FOREIGN KEY (author_id) REFERENCES authors(id)
ON DELETE RESTRICT;
-- 2. Drop it
ALTER TABLE books DROP FOREIGN KEY fk_books_author;
-- 3. Make column nullable and re-add with SET NULL
ALTER TABLE books MODIFY author_id INT NULL;
ALTER TABLE books
ADD CONSTRAINT fk_books_author
FOREIGN KEY (author_id) REFERENCES authors(id)
ON DELETE SET NULL
ON UPDATE CASCADE;
🎯 Quick Quiz
Question 1: What is the default ON DELETE behavior if you don't specify one?
Question 2: What does ON DELETE CASCADE do?
Question 3: What must be true about a column to use ON DELETE SET NULL?
Question 4: Error 1452 means you tried to:
Summary
🎉 Key Takeaways
- Referential integrity ensures every FK value points to a real parent row
- FOREIGN KEY ... REFERENCES creates the constraint; name your constraints for easier management
- ON DELETE RESTRICT (default) blocks parent deletion if children exist — safest option
- ON DELETE CASCADE auto-deletes children with the parent — use when children are meaningless alone
- ON DELETE SET NULL preserves children but breaks the link — requires a nullable FK column
- ON UPDATE CASCADE is useful for natural keys that might change
- When in doubt, use RESTRICT — you can always change it later
- Use
ALTER TABLEto add/drop FKs on existing tables SET FOREIGN_KEY_CHECKS = 0/1temporarily disables checks for bulk operations- Error 1452 = bad child data; Error 1451 = trying to delete a referenced parent; Error 1215 = constraint creation failed
Quick Reference
| Pattern | Syntax |
|---|---|
| Create FK | FOREIGN KEY (col) REFERENCES parent(id) |
| Named FK | CONSTRAINT name FOREIGN KEY (col) REFERENCES parent(id) |
| With rules | ON DELETE CASCADE ON UPDATE CASCADE |
| Add FK | ALTER TABLE t ADD CONSTRAINT name FOREIGN KEY ... |
| Drop FK | ALTER TABLE t DROP FOREIGN KEY name |
| Disable checks | SET FOREIGN_KEY_CHECKS = 0; |
| View FKs | SHOW CREATE TABLE t; |
📚 Additional Resources
- MySQL Docs — Foreign Key Constraints
- MySQL Docs — InnoDB Foreign Key Constraints
- MySQL Docs — foreign_key_checks Variable
🚀 What's Next?
You've built the relationships and enforced them with constraints. Now it's time to use them! In the next lesson, you'll learn JOINs — the SQL technique for querying data across multiple related tables in a single statement. INNER JOIN, LEFT JOIN, RIGHT JOIN, CROSS JOIN, and self-joins — the most powerful (and most-asked-about-in-interviews) topic in SQL.
🎉 Congratulations!
Your database now has a safety net. Foreign key constraints ensure your data stays consistent, even when applications have bugs. The database protects itself!