外键
导学
关系型数据库的"关系"不仅体现在 JOIN 查询上,更体现在数据一致性约束上。外键(Foreign Key)是确保引用完整性的核心机制——防止出现"订单指向不存在的用户"这类脏数据。
定义
外键(Foreign Key):在一个表中建立一个或多个列,引用另一个表的主键(或唯一键)。外键约束确保引用关系的数据完整性,防止出现无效的关联数据。
核心语法
建表时定义外键
当前数据状态:先创建父表
CREATE TABLE departments (
dept_id INT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(20) NOT NULL
);
INSERT INTO departments (dept_name) VALUES
('技术部'),
('产品部');
SELECT * FROM departments;
| dept_id | dept_name |
|---|---|
| 1 | 技术部 |
| 2 | 产品部 |
操作语句:
-- 创建子表,带外键约束
CREATE TABLE employees (
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(20),
dept_id INT,
score DECIMAL(5,2),
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
);
INSERT INTO employees (emp_name, dept_id, score) VALUES
('大翔', 1, 100),
('白歌', 1, NULL);
SELECT * FROM employees;
操作后数据状态:
| emp_id | emp_name | dept_id | score |
|---|---|---|---|
| 1 | 大翔 | 1 | 100 |
| 2 | 白歌 | 1 | NULL |
结果解读:dept_id 列引用了 departments(dept_id)。子表插入时,MySQL 会自动检查 dept_id 是否在父表中存在。
插入违反外键的数据
当前数据状态:departments 表只有 dept_id 为 1、2 的记录。
操作语句:
-- 尝试插入不存在的 dept_id
INSERT INTO employees (emp_name, dept_id, score) VALUES ('小崔', 999, 90);
操作后数据状态:
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails
SELECT * FROM employees;
| emp_id | emp_name | dept_id | score |
|---|---|---|---|
| 1 | 大翔 | 1 | 100 |
| 2 | 白歌 | 1 | NULL |
结果解读:插入被拒绝!dept_id=999 在 departments 表中不存在,外键约束阻止了这条脏数据的插入。
建表后添加外键
当前数据状态:创建一张没有外键的表
CREATE TABLE scores (
score_id INT PRIMARY KEY AUTO_INCREMENT,
emp_id INT,
score DECIMAL(5,2)
);
INSERT INTO scores (emp_id, score) VALUES
(1, 90),
(2, 85);
操作语句:
ALTER TABLE scores
ADD CONSTRAINT fk_emp
FOREIGN KEY (emp_id) REFERENCES employees(emp_id);
SHOW CREATE TABLE scores;
操作后数据状态(节选):
CREATE TABLE `scores` (
`score_id` int(11) NOT NULL AUTO_INCREMENT,
`emp_id` int(11) DEFAULT NULL,
`score` decimal(5,2) DEFAULT NULL,
PRIMARY KEY (`score_id`),
KEY `fk_emp` (`emp_id`),
CONSTRAINT `fk_emp` FOREIGN KEY (`emp_id`) REFERENCES `employees` (`emp_id`)
) ENGINE=InnoDB
结果解读:MySQL 自动为外键列 emp_id 创建了索引 fk_emp,这是 MySQL 外键的强制要求(没有索引的外键列会自动创建索引)。
级联操作
| 选项 | 含义 |
|---|---|
RESTRICT / NO ACTION | 默认。删除/更新父表记录时,如果子表有引用,则拒绝操作。 |
CASCADE | 删除/更新父表记录时,自动删除/更新子表的关联记录。 |
SET NULL | 删除/更新父表记录时,将子表的外键设为 NULL(要求外键列允许 NULL)。 |
SET DEFAULT | 设为默认值(InnoDB 不支持)。 |
场景一:RESTRICT(默认,保护数据)
当前数据状态:重新创建带 RESTRICT 的表
CREATE TABLE employees_restrict (
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(20),
dept_id INT,
score DECIMAL(5,2),
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
ON DELETE RESTRICT
ON UPDATE RESTRICT
);
INSERT INTO employees_restrict (emp_name, dept_id, score) VALUES
('大翔', 1, 100),
('白歌', 2, NULL);
SELECT * FROM employees_restrict;
| emp_id | emp_name | dept_id | score |
|---|---|---|---|
| 1 | 大翔 | 1 | 100 |
| 2 | 白歌 | 2 | NULL |
操作语句:
-- 尝试删除有员工引用的部门
DELETE FROM departments WHERE dept_id = 1;
操作后数据状态:
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails
SELECT * FROM departments;
| dept_id | dept_name |
|---|---|
| 1 | 技术部 |
| 2 | 产品部 |
结果解读:ON DELETE RESTRICT 阻止了删除操作,因为 employees_restrict 表中还有 dept_id=1 的记录。这保护了数据,防止误删导致引用断裂。
场景二:CASCADE(级联删除)
当前数据状态:创建带 CASCADE 的表
CREATE TABLE employees_cascade (
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(20),
dept_id INT,
score DECIMAL(5,2),
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
INSERT INTO employees_cascade (emp_name, dept_id, score) VALUES
('大翔', 1, 100),
('白歌', 1, NULL);
SELECT * FROM employees_cascade;
| emp_id | emp_name | dept_id | score |
|---|---|---|---|
| 1 | 大翔 | 1 | 100 |
| 2 | 白歌 | 1 | NULL |
操作语句:
-- 删除技术部(dept_id = 1)
DELETE FROM departments WHERE dept_id = 1;
SELECT * FROM departments;
操作后数据状态:
| dept_id | dept_name |
|---|---|
| 2 | 产品部 |
SELECT * FROM employees_cascade;
| emp_id | emp_name | dept_id | score |
|---|
结果解读:ON DELETE CASCADE 使得删除 departments 表中 dept_id=1 的记录时,employees_cascade 表中所有 dept_id=1 的记录(大翔和白歌)被自动删除。子表已无任何数据。
⚠️ 危险警告:
ON DELETE CASCADE在父表删除时会自动级联删除子表数据。如果级联链很长,一次删除可能触发大量级联操作,造成"删一条丢一片"的事故。
场景三:SET NULL
当前数据状态:重新插入技术部部门,创建带 SET NULL 的表
INSERT INTO departments (dept_id, dept_name) VALUES (1, '技术部');
CREATE TABLE employees_setnull (
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(20),
dept_id INT, -- 必须允许 NULL
score DECIMAL(5,2),
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
ON DELETE SET NULL
ON UPDATE SET NULL
);
INSERT INTO employees_setnull (emp_name, dept_id, score) VALUES
('大翔', 1, 100),
('白歌', 1, NULL);
SELECT * FROM employees_setnull;
| emp_id | emp_name | dept_id | score |
|---|---|---|---|
| 1 | 大翔 | 1 | 100 |
| 2 | 白歌 | 1 | NULL |
操作语句:
-- 删除技术部
DELETE FROM departments WHERE dept_id = 1;
SELECT * FROM employees_setnull;
操作后数据状态:
| emp_id | emp_name | dept_id | score |
|---|---|---|---|
| 1 | 大翔 | NULL | 100 |
| 2 | 白歌 | NULL | NULL |
结果解读:ON DELETE SET NULL 使得删除技术部后,原本属于该部门的员工(大翔和白歌)的 dept_id 被自动设为 NULL,表示"未分配部门",而不是被删除。
场景四:ON UPDATE CASCADE
当前数据状态:employees_cascade 表当前无数据,重新准备数据
INSERT INTO departments (dept_id, dept_name) VALUES (1, '技术部');
INSERT INTO employees_cascade (emp_name, dept_id, score) VALUES
('大翔', 1, 100),
('白歌', 2, NULL);
SELECT * FROM employees_cascade;
| emp_id | emp_name | dept_id | score |
|---|---|---|---|
| 1 | 大翔 | 1 | 100 |
| 2 | 白歌 | 2 | NULL |
操作语句:
-- 修改产品部的 dept_id
UPDATE departments SET dept_id = 20 WHERE dept_id = 2;
SELECT * FROM departments;
操作后数据状态:
| dept_id | dept_name |
|---|---|
| 1 | 技术部 |
| 20 | 产品部 |
SELECT * FROM employees_cascade;
| emp_id | emp_name | dept_id | score |
|---|---|---|---|
| 1 | 大翔 | 1 | 100 |
| 2 | 白歌 | 20 | NULL |
结果解读:ON UPDATE CASCADE 使得父表的 dept_id 从 2 改为 20 时,子表中对应的 dept_id 也自动同步为 20。这保证了引用关系的一致性。
外键的代价
- 插入子表时:需要查询父表确认引用有效,增加一次查找
- 删除父表时:需要检查子表是否有引用,可能锁定子表
- 高并发场景:外键检查会增加锁竞争
- 分库分表:跨库无法建立外键约束
常见误区
| 误区 | 正解 |
|---|---|
| 外键会降低性能所以不要用 | 外键确实有开销,但在单机 MySQL 中,它保证数据一致性的收益通常大于性能开销。高并发或分库场景可在应用层保证一致性。 |
| 外键只能在应用层替代 | 应用层无法完全替代外键的原子性保障(竞态条件窗口)。单机场景推荐外键。 |
| 所有关联表都必须建外键 | 视场景而定。日志表、临时表、高写入表可考虑不加外键。 |
面试考点
Q:外键的优缺点?
优点:保证引用完整性,支持级联操作,减少应用层代码。缺点:增加写入开销,删除父表时需检查子表,高并发下增加锁竞争,分库分表无法使用。
Q:ON DELETE CASCADE 有什么风险?
级联删除可能导致"删一条丢一片"的连锁反应。如果表之间的级联关系复杂,一次误删可能删除大量关联数据。生产环境使用级联删除需谨慎。
Q:MySQL 外键有什么限制?
- 父表和子表必须使用相同的存储引擎(InnoDB);2. 外键列和引用列必须有相同的数据类型和长度;3. 外键列必须有索引(MySQL 会自动创建);4. 不支持跨数据库的外键。
小结
- 外键保证表与表之间的引用完整性
ON DELETE RESTRICT保护数据,CASCADE级联操作,SET NULL置空- 外键有性能开销,高并发和分库场景可在应用层处理一致性
- 使用
CASCADE需谨慎,避免误删导致连锁反应
下一章引子:外键约束底层数据,视图则提供一个灵活的数据呈现层——它像一张虚拟的表。