多表关系
学习目标
- 理解什么是一对多关系,能举出业务场景(如部门与员工)。
- 掌握一对多在数据库中的实现方式:在“多”的一方添加字段,关联“一”的一方主键。
- 能看懂并写出
dept(部门)与emp(员工)这类一对多表结构及建表语句。 - 知道父表 / 子表、主键 / 外键在一对多中的角色。
1. 什么是多表关系?
在实际业务中,数据往往分散在多张表里,表与表之间通过字段关联形成关系。常见有三种:
- 一对多(1 : N):一个 A 对应多个 B,例如一个部门下有多个员工。
- 多对多(M : N):多个 A 对应多个 B,例如学生与课程(一个学生选多门课,一门课被多个学生选),通常通过中间表实现。
- 一对一(1 : 1):一个 A 对应一个 B,例如用户与用户详情表。
本笔记重点讲 一对多,它是业务中最常见、也最容易上手的一种。
2. 一对多(1 : N)概念
2.1 场景
典型场景:部门与员工的关系 —— 一个部门下有多个员工。
- “一”的一方:部门(dept) —— 学工部、教研部、咨询部等。
- “多”的一方:员工(emp) —— 每个员工属于一个部门,多个员工可以属于同一个部门。

2.2 父表与子表
- 父表(一的一方):部门表
dept,存储部门信息,主键为id。 - 子表(多的一方):员工表
emp,存储员工信息,其中有一个字段dept_id用来“指向”部门表的主键id。
关系可以理解为:子表中的多条记录,通过外键字段关联到父表中的一条记录。
2.3 一对多如何实现?
核心结论:在数据库表中,在“多”的一方添加一个字段,用来关联“一”的一方的主键。
也就是说:
- 在**员工表(多的一方)**里增加字段
dept_id。 dept_id的取值来源于 部门表(一的一方)的主键id。- 这样,每条员工记录都能通过
dept_id找到自己所属的部门;一个部门 id 可以被多条员工记录引用,形成“一对多”。
若在数据库层面要强制“员工所属部门必须存在”,可以为 dept_id 添加 外键约束(FOREIGN KEY),引用 dept(id)。是否建外键可根据项目规范决定(有的团队在应用层校验,不在库中建外键)。
3. 示例:部门表(dept)与员工表(emp)
下面用你提供的 部门表 dept 和 员工表 emp 作为一对多示例。
3.1 父表:部门表 dept(一的一方)
CREATE TABLE dept (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT 'ID, 主键',
name VARCHAR(10) NOT NULL UNIQUE COMMENT '部门名称',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
update_time DATETIME DEFAULT NULL COMMENT '修改时间'
) COMMENT '部门表';- 主键:
id,唯一标识一个部门。 - 示例数据:学工部(1)、教研部(2)、咨询部(3)、就业部(4)、人事部(5)、行政部(15) 等。
3.2 子表:员工表 emp(多的一方)
在多的一方增加 dept_id,用于关联部门表主键 id:
CREATE TABLE emp (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT 'ID, 主键',
username VARCHAR(20) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(32) DEFAULT '123456' COMMENT '密码',
name VARCHAR(10) NOT NULL COMMENT '姓名',
gender TINYINT UNSIGNED NOT NULL COMMENT '性别, 1:男, 2:女',
phone CHAR(11) NOT NULL UNIQUE COMMENT '手机号',
job TINYINT UNSIGNED COMMENT '职位, 1班主任, 2讲师, 3学工主管, 4教研主管, 5咨询师',
salary INT UNSIGNED COMMENT '薪资',
image VARCHAR(255) COMMENT '头像',
entry_date DATE COMMENT '入职日期',
dept_id INT UNSIGNED COMMENT '部门ID', -- 关联 dept.id,实现一对多
create_time DATETIME COMMENT '创建时间',
update_time DATETIME COMMENT '修改时间'
) COMMENT '员工表';- 主键:
id,唯一标识一个员工。 - 外键字段:
dept_id,表示“该员工属于哪个部门”,取值应对应dept.id。 - 多条员工记录的
dept_id可以相同(例如多个人都属于教研部dept_id=2),从而形成“一个部门对应多个员工”的一对多关系。
3.3 关系示意(对应你提供的图示)
- 父表
dept:一的一方,主键id。 - 子表
emp:多的一方,字段dept_id指向dept.id。 - 箭头:从
dept.id指向emp.dept_id,表示“多的一方通过外键关联一的一方的主键”。
示例数据对应关系:
dept_id = 1(学工部)→ 多条员工记录(如扈三娘、柴进、李逵等)。dept_id = 2(教研部)→ 多条员工记录(如施耐庵、宋江、卢俊义等)。dept_id = 3(咨询部)→ 多条员工记录(如阮小五、阮小七等)。
3.4 可选:为 dept_id 添加外键约束
若要在数据库层面保证“员工所属部门必须存在”,可以给 emp.dept_id 加外键:
ALTER TABLE emp
ADD CONSTRAINT fk_emp_dept
FOREIGN KEY (dept_id) REFERENCES dept(id);- 含义:
emp.dept_id的取值必须在dept.id中存在。 - 这种在数据库中用
FOREIGN KEY定义的外键,称为物理外键;与之相对的是在业务代码里做关联校验的逻辑外键,见下文。
3.5 物理外键与逻辑外键
表与表之间的“关联”有两种常见做法:物理外键(在数据库中建外键约束)和逻辑外键(只在业务层维护关联关系)。

物理外键
- 概念:使用
FOREIGN KEY在数据库层面定义外键,直接关联另一张表。 - 缺点:
- 影响增、删、改的效率:每次操作都要检查外键关系(如插入/更新子表要查父表是否存在,删除父表要检查是否有子表引用)。
- 仅适用于单节点数据库:分布式、分库分表、读写分离等场景下,外键约束难以维护,一般不适用。
- 容易引发死锁、消耗性能:多表并发修改时,外键检查会加锁,可能造成死锁或性能下降。
逻辑外键
- 概念:不在表上建
FOREIGN KEY,只在业务层逻辑中保证“关联关系”正确(例如插入/更新员工时,在 Java 代码里校验dept_id是否在部门表中存在)。 - 优点:通过逻辑外键,可以避免上述物理外键带来的效率、分布式、死锁等问题;表结构更灵活,便于分库分表和水平扩展。
- 注意:数据一致性完全由应用层保证,需要在业务代码里做好校验与约束,否则可能出现“脏数据”(如
dept_id指向不存在的部门)。
实践建议:很多互联网项目采用逻辑外键(表上有
dept_id字段,但不建FOREIGN KEY),由应用层保证关联正确;传统单体、强一致性场景下仍可能使用物理外键。团队可根据业务场景、是否分布式、对性能与一致性的要求来选型。
4. 一对一(1 : 1)
4.1 应用场景与单表拆分
一对一关系在实际开发中应用起来比较简单,常用于单表拆分:将一张大表拆成两张小表,把大表中的基础字段放在一张表里,把其他字段放在另一张表里,从而提高数据的操作效率。
典型应用场景:用户表(基本信息 + 身份信息)
- 一张表存用户基本信息(姓名、性别、手机号、学历等)。
- 另一张表存用户身份信息(民族、生日、身份证号、签发机关、有效期限等)。
- 两者通过一对一关联(一个用户对应一条身份信息)。
4.2 一对一如何实现?
实现方式:在任意一方添加外键,关联另一方的主键,并给该外键加上 UNIQUE 约束,保证“一一对应”。
4.3 示例:用户基本信息表与用户身份信息表
用户基本信息表 tb_user
-- 用户基本信息表
CREATE TABLE tb_user (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT 'ID',
name VARCHAR(10) NOT NULL COMMENT '姓名',
gender TINYINT UNSIGNED NOT NULL COMMENT '性别, 1 男 2 女',
phone CHAR(11) COMMENT '手机号',
degree VARCHAR(10) COMMENT '学历'
) COMMENT '用户基本信息表';
-- 测试数据
INSERT INTO tb_user VALUES (1,'白眉鹰王',1,'18812340001','初中'),
(2,'青翼蝠王',1,'18812340002','大专'),
(3,'金毛狮王',1,'18812340003','初中'),
(4,'紫衫龙王',2,'18812340004','硕士');用户身份信息表 tb_user_card
在身份信息表中增加 user_id,关联 tb_user(id),并设置 UNIQUE,保证一个用户只有一条身份信息:
-- 用户身份信息表
CREATE TABLE tb_user_card (
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT 'ID',
nationality VARCHAR(10) NOT NULL COMMENT '民族',
birthday DATE NOT NULL COMMENT '生日',
idcard CHAR(18) NOT NULL COMMENT '身份证号',
issued VARCHAR(20) NOT NULL COMMENT '签发机关',
expire_begin DATE NOT NULL COMMENT '有效期限-开始',
expire_end DATE COMMENT '有效期限-结束',
user_id INT UNSIGNED NOT NULL UNIQUE COMMENT '用户ID',
CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id)
) COMMENT '用户身份信息表';
-- 测试数据
INSERT INTO tb_user_card VALUES (1,'汉','1960-11-06','100000100000100001','朝阳区公安局','2000-06-10',NULL,1),
(2,'汉','1971-11-06','100000100000100002','静安区公安局','2005-06-10','2025-06-10',2),
(3,'汉','1963-11-06','100000100000100003','昌平区公安局','2006-06-10',NULL,3),
(4,'回','1980-11-06','100000100000100004','海淀区公安局','2008-06-10','2028-06-10',4);user_id:外键,关联tb_user(id);UNIQUE 保证一个用户只对应一条身份记录,形成一对一。- 若采用逻辑外键,可不建
FOREIGN KEY,在业务层保证user_id在tb_user中存在且唯一即可。
5. 多对多(M : N)
5.1 场景
多对多关系在开发中也比较常见,例如:
- 学生与老师:一个学生可以有多个授课老师,一个老师也可以教多个学生。
- 学生与课程:一个学生可以选修多门课程,一门课程也可以供多个学生选修。
5.2 多对多如何实现?
实现方式:建立一张中间表,中间表中至少包含两个外键字段,分别关联两方的主键。
也就是说:
- 学生表与课程表各自独立,不直接互相引用。
- 新增学生课程表(中间表):每条记录表示“某学生选了某课程”,通过
student_id关联学生表主键、course_id关联课程表主键。 - 这样,一个学生可以对应中间表多条记录(选多门课),一门课程也可以对应中间表多条记录(被多个学生选),形成多对多。
5.3 示例:学生表、课程表与中间表

学生表 tb_student
-- 学生表
CREATE TABLE tb_student (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
name VARCHAR(10) COMMENT '姓名',
no VARCHAR(10) COMMENT '学号'
) COMMENT '学生表';
-- 学生表测试数据
INSERT INTO tb_student(name, no) VALUES ('黛绮丝', '2000100101'),
('谢逊', '2000100102'),
('殷天正', '2000100103'),
('韦一笑', '2000100104');课程表 tb_course
-- 课程表
CREATE TABLE tb_course (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
name VARCHAR(10) COMMENT '课程名称'
) COMMENT '课程表';
-- 课程表测试数据
INSERT INTO tb_course (name) VALUES ('Java'), ('PHP'), ('MySQL'), ('Hadoop');学生课程表(中间表)tb_student_course
中间表包含两个外键:student_id 关联学生表主键,course_id 关联课程表主键。
-- 学生课程表(中间表)
CREATE TABLE tb_student_course (
id INT AUTO_INCREMENT COMMENT '主键' PRIMARY KEY,
student_id INT NOT NULL COMMENT '学生ID',
course_id INT NOT NULL COMMENT '课程ID',
CONSTRAINT fk_courseid FOREIGN KEY (course_id) REFERENCES tb_course(id),
CONSTRAINT fk_studentid FOREIGN KEY (student_id) REFERENCES tb_student(id)
) COMMENT '学生课程中间表';
-- 学生课程表测试数据
INSERT INTO tb_student_course(student_id, course_id) VALUES (1,1),(1,2),(1,3),(2,2),(2,3),(3,4);student_id:外键,关联tb_student(id)。course_id:外键,关联tb_course(id)。- 一条中间表记录表示“学生 student_id 选修了课程 course_id”;多条记录可共享同一
student_id(一个学生多门课)或同一course_id(一门课多个学生),形成多对多。 - 若采用逻辑外键,可不建
FOREIGN KEY,在业务层保证student_id、course_id在对应表中存在即可;中间表也可增加联合唯一约束UNIQUE(student_id, course_id),防止同一学生重复选同一门课。
6. 小结与记忆要点
- 一对多:一个 A 对应多个 B;在多的一方增加字段关联一的一方的主键(如
emp.dept_id→dept.id)。 - 一对一:一个 A 对应一个 B;在任意一方增加外键关联另一方主键,并设置外键为 UNIQUE(如
tb_user_card.user_id→tb_user.id)。 - 多对多:多个 A 对应多个 B;需要建立一张中间表,中间表中有两个外键字段,分别关联两方的主键(如
tb_student_course.student_id→tb_student.id,tb_student_course.course_id→tb_course.id)。 - 物理外键 vs 逻辑外键:物理外键用
FOREIGN KEY在库中约束;逻辑外键只在业务层校验,表结构更灵活,适合分布式场景。