mysql菜鸟教程
8.2 外键(FOREIGN KEY):表间关系
在现实世界中,数据不是孤立存在的。比如,一个学生属于某个班级,一个订单属于某个客户,一篇文章属于某个作者。这种“属于”的关系在数据库中就是通过外键(FOREIGN KEY) 来实现的。外键是连接两张表的桥梁,它保证了数据之间的参照完整性。
一、什么是外键?
外键 是一个表中的一列(或一组列),它的值必须引用另一个表中的主键或唯一键。简单来说,外键告诉数据库:“这张表的这一列的值,必须在另一张表的对应列中存在”。
例如,在 students 表中,我们有一个 class_id 列,它引用 classes 表的 id 主键。这样,每个学生都被分配到一个真实存在的班级。
为什么需要外键?
保证数据一致性:防止插入无效的引用(比如给一个学生分配一个不存在的班级)。
防止孤立数据:当删除被引用的数据时,可以自动处理相关的子记录(例如,删除班级时,自动删除该班级的所有学生,或将其置空)。
清晰表达关系:数据库结构能够直观反映业务逻辑,便于理解和维护。
二、外键的创建语法
外键可以在创建表时定义,也可以使用 ALTER TABLE 后期添加。
1. 创建表时定义外键
CREATE TABLE 子表名 ( 列定义, ... [CONSTRAINT 外键名称] FOREIGN KEY (子表外键列) REFERENCES 父表名(父表主键列) [ON DELETE 行为] [ON UPDATE 行为] );
示例:班级表和学生表
-- 先创建父表:班级表 CREATE TABLE classes ( id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL, grade INT ); -- 创建子表:学生表,包含外键 class_id CREATE TABLE students ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, age TINYINT, class_id INT, CONSTRAINT fk_student_class -- 为外键起个名字(便于管理) FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE SET NULL ON UPDATE CASCADE );
2. 使用 ALTER TABLE 添加外键
如果表已经存在,可以这样添加外键(前提是已有数据满足外键约束):
ALTER TABLE students ADD CONSTRAINT fk_student_class FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE SET NULL ON UPDATE CASCADE;
3. 删除外键
ALTER TABLE students DROP FOREIGN KEY fk_student_class;
三、外键约束的行为:ON DELETE 和 ON UPDATE
当父表中的被引用行发生删除或更新时,我们可以指定子表该如何应对。MySQL 支持以下四种行为:
注意:如果使用 SET NULL,外键列必须允许 NULL 值,否则操作会失败。
四、实战案例:订单与客户
让我们通过一个更贴近业务的例子来理解外键的应用。
场景
一个客户可以拥有多个订单(一对多关系)。
订单必须属于一个已存在的客户。
如果客户被删除,应该如何处理其订单?(这里我们选择:删除客户时,同时删除其所有订单)
建表
-- 客户表 CREATE TABLE customers ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, email VARCHAR(100) UNIQUE, phone VARCHAR(20) ); -- 订单表,包含外键 customer_id CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_date DATETIME DEFAULT CURRENT_TIMESTAMP, amount DECIMAL(10,2) NOT NULL, customer_id INT NOT NULL, CONSTRAINT fk_order_customer FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE ON UPDATE CASCADE );
插入数据测试
-- 插入客户
INSERT INTO customers (name, email) VALUES
('张三', 'zhangsan@example.com'),
('李四', 'lisi@example.com');
-- 插入订单
INSERT INTO orders (amount, customer_id) VALUES
(299.00, 1),
(59.90, 1),
(1200.00, 2);
-- 尝试插入一个不存在的客户ID(应该失败)
INSERT INTO orders (amount, customer_id) VALUES (100.00, 99);
-- 错误:Cannot add or update a child row: a foreign key constraint fails删除客户,观察级联效果
-- 删除客户“张三”(id=1) DELETE FROM customers WHERE id = 1; -- 查询 orders 表,发现张三的订单也被自动删除了 SELECT * FROM orders; -- 只剩下 id=2 的订单(属于李四)
如果使用 SET NULL 会怎样?
假设我们将外键设为 ON DELETE SET NULL,并允许 customer_id 为 NULL:
-- 修改外键行为 ALTER TABLE orders DROP FOREIGN KEY fk_order_customer; ALTER TABLE orders ADD CONSTRAINT fk_order_customer FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE SET NULL; -- 注意:需要先让 customer_id 允许 NULL ALTER TABLE orders MODIFY customer_id INT NULL; -- 再删除客户“李四” DELETE FROM customers WHERE id = 2; -- 查询 orders,发现原先属于李四的订单的 customer_id 变成了 NULL
五、外键使用注意事项
数据类型必须匹配:外键列与引用的主键列必须数据类型一致(长度、符号等也要一致)。
引用的列必须是主键或唯一键:通常引用主键,但也可以引用唯一键(UNIQUE)。
存储引擎限制:MySQL 中只有 InnoDB 存储引擎支持外键约束(MyISAM 不支持,但可以定义语法,实际不生效)。
性能影响:外键约束会带来额外的检查开销,但在保证数据完整性方面非常值得。对高并发写入频繁的表,可能需要权衡。
循环依赖:应避免两个表互相作为外键(鸡生蛋蛋生鸡问题),这会很难插入数据。
外键名:建议给外键起一个有意义的名字(如 fk_订单_客户),便于管理和错误排查。
六、如何查看外键关系?
可以使用以下 SQL 查看某个数据库中的所有外键:
SELECT CONSTRAINT_NAME, TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_SCHEMA = '数据库名' AND REFERENCED_TABLE_NAME IS NOT NULL;
七、外键的最佳实践
为每个表设计主键:这是建立外键关系的基础。
谨慎选择约束行为:根据业务逻辑选择 CASCADE、SET NULL 或 RESTRICT,避免意外删除大量数据。
命名规范:外键名建议以 fk_ 开头,后跟子表名和父表名,如 fk_orders_customers。
备份与测试:在生产环境添加或修改外键前,务必在测试环境验证,并备份数据。
考虑性能:如果表非常大且写操作频繁,可考虑在应用层实现参照完整性,以换取更高性能。
八、综合练习
设计一个“博客系统”的数据库,包含三张表:
users(用户表):id(主键)、username、email
posts(文章表):id(主键)、title、content、user_id(外键引用 users.id,级联删除)
comments(评论表):id(主键)、content、post_id(外键引用 posts.id,级联删除)、user_id(外键引用 users.id,设置 NULL)
要求:
写出建表语句,包含合适的外键约束。
插入几条测试数据。
尝试删除一个用户,观察其文章和评论的变化。
九、总结
外键 是表与表之间的“法律条约”,它强制子表的引用必须在父表中存在。
外键保证了数据的参照完整性,防止了“孤儿数据”的出现。
通过 ON DELETE 和 ON UPDATE 可以定义父表变化时的自动行为。
外键的设计需要结合实际业务,选择适当的约束策略。
虽然外键会带来一些性能开销,但对于保证数据一致性来说,它是最简单可靠的方案。

发表评论
所有评论