MySQL Foreign Keys
A foreign key (FK) is a column (or group of columns) in one table whose values must match existing values in a column of another table. Foreign keys enforce referential integrity — the guarantee that relationships between rows are always valid. Without them, you can end up with orphaned orders pointing to deleted customers, or invoice items referencing products that no longer exist.
Basic Foreign Key Syntax
CREATE TABLE categories (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE products (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
category_id INT,
PRIMARY KEY (id),
CONSTRAINT fk_product_category
FOREIGN KEY (category_id)
REFERENCES categories (id)
ON DELETE SET NULL
ON UPDATE CASCADE
);fk_product_category). Named constraints produce clearer error messages and are much easier to drop or inspect later.ON DELETE Actions — All 5 Options
The ON DELETE clause controls what happens to child rows when a referenced parent row is deleted.
Action | Exact Behavior | InnoDB Timing | Best For |
|---|---|---|---|
RESTRICT | Reject the DELETE if any child rows reference the parent row. | Checked immediately during the statement | Strict data safety — the default |
NO ACTION | Same rejection behavior as RESTRICT in MySQL InnoDB. | Checked at end of statement (deferred in standard SQL) | Standard SQL equivalent — behaves like RESTRICT in MySQL |
CASCADE | Automatically delete all child rows that reference the deleted parent. | Immediate | Log entries, order items, or any record that cannot exist without its parent |
SET NULL | Set the FK column to NULL in all matching child rows. The FK column must be nullable. | Immediate | Optional relationships where the child can exist without a parent |
SET DEFAULT | Set the FK column to its default value. Not reliably supported in InnoDB. | Immediate | Avoid in InnoDB — use SET NULL instead |
DEFERRABLE INITIALLY DEFERRED constraints exist.-- CASCADE: deleting a post automatically deletes all its comments
CREATE TABLE comments (
id INT NOT NULL AUTO_INCREMENT,
post_id INT NOT NULL,
body TEXT NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_comment_post
FOREIGN KEY (post_id) REFERENCES posts (id)
ON DELETE CASCADE
);
-- SET NULL: deleting a category nullifies the product's category_id
CREATE TABLE products (
id INT NOT NULL AUTO_INCREMENT,
category_id INT DEFAULT NULL, -- must be nullable for SET NULL
PRIMARY KEY (id),
CONSTRAINT fk_product_cat
FOREIGN KEY (category_id) REFERENCES categories (id)
ON DELETE SET NULL
);
-- RESTRICT: prevent deleting a user who still has orders
CREATE TABLE orders (
id INT NOT NULL AUTO_INCREMENT,
user_id INT NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_order_user
FOREIGN KEY (user_id) REFERENCES users (id)
ON DELETE RESTRICT
);ON UPDATE Actions
ON UPDATE uses the same 5 options as ON DELETE, but applies when the referenced parent primary key value is changed.
-- CASCADE update: changing the parent PK propagates to all child FK columns
CREATE TABLE order_items (
order_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL,
PRIMARY KEY (order_id, product_id),
CONSTRAINT fk_item_order
FOREIGN KEY (order_id) REFERENCES orders (id)
ON DELETE CASCADE
ON UPDATE CASCADE
);AUTO_INCREMENT surrogate primary keys, ON UPDATE CASCADE is rarely needed because PKs should never change. It is more relevant when using natural keys (e.g., a product code that gets corrected).Multi-Column Foreign Key
A foreign key can span multiple columns when the referenced parent table uses a composite primary key:
-- Parent with composite PK
CREATE TABLE warehouse_locations (
warehouse_id INT NOT NULL,
aisle VARCHAR(10) NOT NULL,
shelf INT NOT NULL,
capacity INT,
PRIMARY KEY (warehouse_id, aisle, shelf)
);
-- Child with matching composite FK
CREATE TABLE inventory (
product_id INT NOT NULL,
warehouse_id INT NOT NULL,
aisle VARCHAR(10) NOT NULL,
shelf INT NOT NULL,
quantity INT NOT NULL,
PRIMARY KEY (product_id, warehouse_id, aisle, shelf),
CONSTRAINT fk_inventory_location
FOREIGN KEY (warehouse_id, aisle, shelf)
REFERENCES warehouse_locations (warehouse_id, aisle, shelf)
ON DELETE RESTRICT
ON UPDATE CASCADE
);Self-Referencing Foreign Key
A table can have a FK that references its own PK. This models hierarchical data such as organizational charts, category trees, and threaded comments.
CREATE TABLE categories (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
parent_id INT DEFAULT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_category_parent
FOREIGN KEY (parent_id)
REFERENCES categories (id)
ON DELETE SET NULL
);
-- Insert top-level category (no parent)
INSERT INTO categories (name, parent_id) VALUES ('Electronics', NULL);
-- Insert sub-categories
INSERT INTO categories (name, parent_id) VALUES ('Phones', 1);
INSERT INTO categories (name, parent_id) VALUES ('Smartphones', 2);
INSERT INTO categories (name, parent_id) VALUES ('Budget Phones', 2);
-- Query the hierarchy with a self-join
SELECT
child.name AS category,
parent.name AS parent_category
FROM categories child
LEFT JOIN categories parent ON child.parent_id = parent.id;Adding a Foreign Key to an Existing Table
-- The FK column must already exist; all existing values must satisfy the constraint ALTER TABLE products ADD CONSTRAINT fk_product_category FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE if any orphaned values are found. Check first with a query like SELECT COUNT(*) FROM products p WHERE p.category_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM categories c WHERE c.id = p.category_id);Named Constraints and Dropping a Foreign Key
-- Find all FK constraint names if you don't know them SELECT kcu.TABLE_NAME, kcu.COLUMN_NAME, kcu.CONSTRAINT_NAME, kcu.REFERENCED_TABLE_NAME, kcu.REFERENCED_COLUMN_NAME, rc.UPDATE_RULE, rc.DELETE_RULE FROM information_schema.KEY_COLUMN_USAGE kcu JOIN information_schema.REFERENTIAL_CONSTRAINTS rc ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME AND kcu.TABLE_SCHEMA = rc.CONSTRAINT_SCHEMA WHERE kcu.TABLE_SCHEMA = DATABASE() AND kcu.REFERENCED_TABLE_NAME IS NOT NULL ORDER BY kcu.TABLE_NAME; -- Drop a foreign key by constraint name ALTER TABLE products DROP FOREIGN KEY fk_product_category; -- Drop the associated index if no longer needed ALTER TABLE products DROP INDEX fk_product_category;
Disabling FK Checks for Bulk Loads
During bulk data imports or when reloading a dump, you may need to insert data in a different order than the FK constraints require (e.g., child rows before parent rows). Temporarily disable FK checks:
-- Disable FK checks for this session only SET foreign_key_checks = 0; -- Now you can insert in any order LOAD DATA INFILE '/tmp/products.csv' INTO TABLE products ...; LOAD DATA INFILE '/tmp/categories.csv' INTO TABLE categories ...; -- Re-enable FK checks SET foreign_key_checks = 1; -- Verify no orphaned rows were introduced SELECT COUNT(*) AS orphans FROM products p WHERE p.category_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM categories c WHERE c.id = p.category_id);
foreign_key_checks after your import. Leaving it OFF allows orphaned records to silently enter the database. Also verify integrity after re-enabling — fix any violations before they cause application errors.FK Locking Behavior
When MySQL evaluates a FK constraint, it acquires a shared lock on the referenced parent row. This prevents the parent row from being deleted while the child insert/update is in progress. Implications:
- Heavy child inserts can briefly lock parent rows, increasing lock wait times on the parent table.
- The child table FK column must be indexed — without an index, every parent DELETE/UPDATE causes a full scan of the child table to check for referencing rows.
-- MySQL automatically creates an index on the FK column IF one does not exist -- Verify the index was created: SHOW INDEXES FROM order_items; -- You should see an index on order_id -- The FK index is a normal B-tree index and can be used by application queries EXPLAIN SELECT * FROM order_items WHERE order_id = 42; -- If the FK column has no index, every parent DELETE causes a full child scan: -- Add the index manually if needed ALTER TABLE order_items ADD INDEX idx_fk_order_id (order_id);
FK and Online Schema Changes
Adding or dropping a FK on a large table in production requires care. The INPLACE algorithm is available for some FK operations in MySQL 8.0, but not all:
-- MySQL 8.0: check if INPLACE is possible ALTER TABLE order_items ADD CONSTRAINT fk_items_order FOREIGN KEY (order_id) REFERENCES orders(id), ALGORITHM=INPLACE, LOCK=NONE; -- fails if not supported; falls back to COPY -- For large tables in production, use pt-online-schema-change from Percona Toolkit -- to add/drop FKs without locking the table: -- pt-online-schema-change --alter "ADD CONSTRAINT fk_items_order -- FOREIGN KEY (order_id) REFERENCES orders(id)" -- D=mydb,t=order_items --execute
Foreign Key Requirements Checklist
Requirement | Why |
|---|---|
Both tables must use InnoDB | MyISAM silently parses FKs but never enforces them |
Parent column must be PRIMARY KEY or UNIQUE | FK must reference a uniquely identified row |
Child and parent column data types must match exactly | Including sign for integers (INT vs INT UNSIGNED) |
String columns must share the same character set and collation | Mismatched charsets prevent the FK from being created |
FK column in child table should be indexed | MySQL creates one automatically, but verify it exists |
-- Common mistake: signed vs unsigned mismatch CREATE TABLE parent (id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY); -- This will fail — child.parent_id is signed, parent.id is unsigned CREATE TABLE child ( id INT AUTO_INCREMENT PRIMARY KEY, parent_id INT, -- signed — WRONG FOREIGN KEY (parent_id) REFERENCES parent(id) ); -- Correct: match the sign CREATE TABLE child ( id INT AUTO_INCREMENT PRIMARY KEY, parent_id INT UNSIGNED, -- unsigned — matches parent FOREIGN KEY (parent_id) REFERENCES parent(id) );
Checking for FK Violations Before Adding a Constraint
-- Before adding a FK, find orphaned child rows that would violate it SELECT p.id AS orphaned_product_id, p.category_id FROM products p LEFT JOIN categories c ON p.category_id = c.id WHERE p.category_id IS NOT NULL AND c.id IS NULL; -- Fix orphaned rows before adding the FK: -- Option A: delete orphans DELETE p FROM products p LEFT JOIN categories c ON p.category_id = c.id WHERE p.category_id IS NOT NULL AND c.id IS NULL; -- Option B: set to NULL (if the column is nullable) UPDATE products p LEFT JOIN categories c ON p.category_id = c.id SET p.category_id = NULL WHERE p.category_id IS NOT NULL AND c.id IS NULL; -- Now you can safely add the FK ALTER TABLE products ADD CONSTRAINT fk_product_category FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL;
Cascading DELETE Example — End to End
-- Schema: blog posts with cascading deletes
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
user_id INT NOT NULL,
CONSTRAINT fk_post_user
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE CASCADE
);
CREATE TABLE comments (
id INT AUTO_INCREMENT PRIMARY KEY,
post_id INT NOT NULL,
body TEXT NOT NULL,
CONSTRAINT fk_comment_post
FOREIGN KEY (post_id) REFERENCES posts(id)
ON DELETE CASCADE
);
CREATE TABLE comment_likes (
id INT AUTO_INCREMENT PRIMARY KEY,
comment_id INT NOT NULL,
user_id INT NOT NULL,
CONSTRAINT fk_like_comment
FOREIGN KEY (comment_id) REFERENCES comments(id)
ON DELETE CASCADE
);
-- Deleting a user cascades 3 levels deep:
-- DELETE user -> deletes posts -> deletes comments -> deletes comment_likes
DELETE FROM users WHERE id = 42;
-- All posts, comments, and comment_likes for user 42 are automatically removedViewing All Foreign Keys in a Database
-- Complete FK dependency map for the current database SELECT kcu.TABLE_NAME AS child_table, kcu.COLUMN_NAME AS child_column, kcu.CONSTRAINT_NAME AS constraint_name, kcu.REFERENCED_TABLE_NAME AS parent_table, kcu.REFERENCED_COLUMN_NAME AS parent_column, rc.UPDATE_RULE, rc.DELETE_RULE FROM information_schema.KEY_COLUMN_USAGE kcu JOIN information_schema.REFERENTIAL_CONSTRAINTS rc ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME AND kcu.TABLE_SCHEMA = rc.CONSTRAINT_SCHEMA WHERE kcu.TABLE_SCHEMA = DATABASE() AND kcu.REFERENCED_TABLE_NAME IS NOT NULL ORDER BY kcu.TABLE_NAME, kcu.CONSTRAINT_NAME;
Deferrable FK Constraints — MySQL Limitation
Standard SQL supports DEFERRABLE constraints — constraints that are checked at the end of a transaction rather than immediately. MySQL InnoDB does NOT support deferred constraints. This matters for circular FK dependencies:
-- Circular FK problem: table A references B, and B references A -- Standard SQL solution (NOT supported by MySQL): -- ALTER TABLE a ADD FOREIGN KEY (b_id) REFERENCES b(id) DEFERRABLE INITIALLY DEFERRED; -- ALTER TABLE b ADD FOREIGN KEY (a_id) REFERENCES a(id) DEFERRABLE INITIALLY DEFERRED; -- MySQL workaround: use SET foreign_key_checks = 0 to insert both rows, then re-enable SET foreign_key_checks = 0; INSERT INTO table_a (id, b_id) VALUES (1, 10); -- b_id = 10 does not exist yet INSERT INTO table_b (id, a_id) VALUES (10, 1); -- creates the circular reference SET foreign_key_checks = 1; -- Now both rows exist and the circular FK is satisfied -- Best practice: avoid circular FK dependencies in schema design -- Use a nullable FK in one direction to break the cycle
FK Error Messages Decoded
Error | Cause | Fix |
|---|---|---|
ERROR 1452: Cannot add or update a child row: a foreign key constraint fails | INSERT or UPDATE references a parent value that does not exist | Insert the parent row first, or check for typos in the FK value |
ERROR 1451: Cannot delete or update a parent row: a foreign key constraint fails | DELETE or UPDATE on a parent row that has child rows pointing to it (RESTRICT behavior) | Delete or update child rows first, or use CASCADE/SET NULL on the FK |
ERROR 1005: Cannot create table (errno: 150) | FK definition is invalid: type mismatch, missing parent index, or wrong engine | Verify data types match, parent column has an index, and both tables use InnoDB |
ERROR 3730: Cannot drop table with foreign key constraint | Trying to DROP a parent table while child tables still reference it | Drop child table first, or use SET foreign_key_checks=0 (with caution) |
FK Best Practices Summary
Always name FK constraints explicitly — anonymous constraints are harder to drop and produce confusing error messages.
Use CASCADE for records that cannot exist without their parent (order_items without an order, comments without a post).
Use SET NULL for optional relationships where the child can still be useful without a parent (products without a category).
Use RESTRICT (the default) when orphaned child rows would represent a data integrity error.
Always index the FK column in the child table — without an index, parent DELETE/UPDATE operations scan the entire child table.
Test FK behavior in development by running the referenced DELETE/UPDATE operations and verifying child table behavior.
When disabling FK checks for a bulk load, always verify integrity immediately after re-enabling checks.