mysql菜鸟教程
9.3 多对多关系
在现实世界中,许多关系并不是简单的“一个对应多个”,而是“多个对应多个”。例如:
一个学生可以选多门课程,一门课程也可以被多个学生选修。
一个作者可以写多本书,一本书也可以有多个作者(合著)。
一个商品可以出现在多个订单中,一个订单也包含多个商品。
这种双方都是“多”的关系,就是多对多关系(N:N)。在关系型数据库中,我们不能直接用两个表的外键来表示多对多,因为那样会造成数据的冗余和歧义。正确的做法是引入一个中间表(也称为关联表、连接表),将多对多关系拆分成两个一对多关系。
一、什么是多对多关系?
多对多关系 是指:表A中的一条记录,可以与表B中的多条记录相关联;反过来,表B中的一条记录,也可以与表A中的多条记录相关联。
这种关系无法像一对多那样简单地在外键上体现,因为无论把外键放在哪一方,都会导致重复存储和逻辑混乱。
经典示例:学生与课程
学生表(students):存储学生信息。
课程表(courses):存储课程信息。
一个学生可以选择多门课程 → 学生对课程是“多”。
一门课程也可以被多个学生选修 → 课程对学生也是“多”。
如果我们在学生表中加一个外键指向课程,那么一个学生只能选一门课;如果在课程表中加外键指向学生,那么一门课只能有一个学生。这都不符合实际。所以必须引入第三张表——选课表(student_courses)。
二、如何实现多对多关系?
实现多对多关系的标准方法是:创建一张中间表,它包含两个外键,分别引用两个主表的主键。这两个外键的组合通常作为中间表的联合主键(或者再单独加一个自增主键)。
步骤解析:
创建主表:定义两个实体表,各自有主键。
创建中间表:至少包含两个字段,分别作为外键引用两个主表的主键。
添加联合唯一约束(或联合主键):确保两个外键的组合值是唯一的,避免重复关联。
添加外键约束:维护数据的参照完整性。
可选:中间表可以包含额外的属性,如选课时间、成绩等。
示例:学生选课系统
第一步:创建学生表和课程表
-- 学生表 CREATE TABLE students ( student_id INT PRIMARY KEY AUTO_INCREMENT, student_name VARCHAR(50) NOT NULL, class VARCHAR(20) ); -- 课程表 CREATE TABLE courses ( course_id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, credit INT );
第二步:创建中间表(选课表)
CREATE TABLE student_courses ( student_id INT NOT NULL, course_id INT NOT NULL, semester VARCHAR(20), -- 选修学期(额外属性) grade DECIMAL(4,2), -- 成绩(额外属性) PRIMARY KEY (student_id, course_id), -- 联合主键,避免重复选课 FOREIGN KEY (student_id) REFERENCES students(student_id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES courses(course_id) ON DELETE CASCADE );
说明:
PRIMARY KEY (student_id, course_id) 确保一个学生不能重复选择同一门课。
两个外键都设置了 ON DELETE CASCADE,意味着删除学生或课程时,相关的选课记录也会自动删除(避免孤立数据)。
第三步:插入测试数据
-- 插入学生
INSERT INTO students (student_name, class) VALUES
('张三', '计算机1班'),
('李四', '计算机2班'),
('王五', '软件1班');
-- 插入课程
INSERT INTO courses (course_name, credit) VALUES
('数据库原理', 3),
('数据结构', 4),
('操作系统', 3);
-- 学生选课
INSERT INTO student_courses (student_id, course_id, semester, grade) VALUES
(1, 1, '2024春', 85.5), -- 张三选数据库
(1, 2, '2024春', 90.0), -- 张三选数据结构
(2, 1, '2024春', 78.0), -- 李四选数据库
(2, 3, '2024秋', 88.0), -- 李四选操作系统
(3, 2, '2024春', 92.0); -- 王五选数据结构三、查询多对多关系
多对多关系的查询通常需要两次 JOIN:从一张主表到中间表,再到另一张主表。
1. 查询某个学生所选的全部课程
SELECT s.student_name, c.course_name, sc.semester, sc.grade FROM students s JOIN student_courses sc ON s.student_id = sc.student_id JOIN courses c ON sc.course_id = c.course_id WHERE s.student_id = 1;
2. 查询某门课程的所有选课学生
SELECT c.course_name, s.student_name, sc.grade FROM courses c JOIN student_courses sc ON c.course_id = sc.course_id JOIN students s ON sc.student_id = s.student_id WHERE c.course_id = 1;
3. 统计每门课程的选课人数
SELECT c.course_name, COUNT(sc.student_id) AS student_count FROM courses c LEFT JOIN student_courses sc ON c.course_id = sc.course_id GROUP BY c.course_id;
4. 找出同时选了“数据库”和“数据结构”的学生
SELECT s.student_name FROM students s JOIN student_courses sc1 ON s.student_id = sc1.student_id AND sc1.course_id = 1 JOIN student_courses sc2 ON s.student_id = sc2.student_id AND sc2.course_id = 2;
四、中间表的设计模式
中间表通常有以下两种设计模式:
示例:独立主键模式
CREATE TABLE student_courses ( id INT PRIMARY KEY AUTO_INCREMENT, -- 独立自增主键 student_id INT NOT NULL, course_id INT NOT NULL, semester VARCHAR(20), grade DECIMAL(4,2), UNIQUE KEY uk_student_course (student_id, course_id), -- 联合唯一约束 FOREIGN KEY (student_id) REFERENCES students(student_id), FOREIGN KEY (course_id) REFERENCES courses(course_id) );
这种方式更灵活,但会多占用一些空间。
五、注意事项
避免重复关联:务必在中间表上建立联合唯一约束或联合主键,防止出现同一条关联记录多次。
外键约束:强烈建议添加外键约束,保证数据的完整性。如果性能要求极高,可以考虑不加外键(但仍需在应用层保证一致性),但新手请务必加上。
索引:中间表的外键列应该建立索引(MySQL 会自动为外键列创建索引,但如果用联合主键,两个列都在主键中,已包含索引)。
命名规范:中间表名通常由两个表名组合而成,如 student_courses、author_books,使用单数或复数均可,但保持一致。
额外属性:多对多关系往往带有额外属性(如成绩、选课时间、数量等),这些属性应放在中间表中,而不是主表。
删除策略:根据业务需求设置 ON DELETE 和 ON UPDATE。通常选课记录会随着学生或课程的删除而级联删除,比较合理。
六、实战练习:商品与订单
电商系统中,订单与商品就是典型的多对多关系。一个订单可以包含多个商品,一个商品也可以出现在多个订单中。中间表通常叫做 order_items(订单明细表),包含订单ID、商品ID、数量、单价等。
请自行设计:
订单表(orders):订单ID、订单日期、客户ID等。
商品表(products):商品ID、商品名、价格。
订单明细表(order_items):订单ID、商品ID、数量、单价。
要求写出建表语句,并插入测试数据,然后查询某个订单的所有商品及总价。
参考思路:
-- 订单表 CREATE TABLE orders ( order_id INT PRIMARY KEY AUTO_INCREMENT, order_date DATETIME DEFAULT CURRENT_TIMESTAMP, customer_name VARCHAR(100) ); -- 商品表 CREATE TABLE products ( product_id INT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(200), price DECIMAL(10,2) ); -- 订单明细表 CREATE TABLE order_items ( order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, unit_price DECIMAL(10,2) NOT NULL, -- 当时成交价,可能不同于现价 PRIMARY KEY (order_id, product_id), FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE, FOREIGN KEY (product_id) REFERENCES products(product_id) );
查询订单总价:
SELECT o.order_id, o.customer_name, SUM(oi.quantity * oi.unit_price) AS total_amount FROM orders o JOIN order_items oi ON o.order_id = oi.order_id GROUP BY o.order_id;
七、总结
多对多关系 必须通过中间表来实现,中间表包含两个外键,分别指向两个主表。
中间表的两个外键通常组成联合主键或加上联合唯一约束,以避免重复关联。
多对多查询需要两次 JOIN:主表 → 中间表 → 另一主表。
中间表可以携带额外的属性,如数量、时间、成绩等。
合理使用外键约束和索引,保证数据完整性和查询性能。
掌握了多对多关系,你就能够处理数据库中绝大部分关联场景。

发表评论
所有评论