📘 MySQL 多表查询、多表关系建模全解析|从基础语法到高阶技巧(含子查询、连接、联合查询实战;多表关系建模:一对一 / 一对多 / 多对多 实战)
💡 本文基于 MySQL 实践,结合实际业务场景,讲解多表查询的核心知识。适合初学者系统学习,也适合进阶者查漏补缺 ✅
一、前言:为什么要学多表查询?
在真实开发中,一个系统往往涉及多个数据表。例如:
- 员工信息(
emp) - 部门信息(
dept) - 学生与课程关系(
stu,course,student_course)
这些表之间存在 多对多、一对多、一对一 等复杂关系。
想要从这些表中查询出有意义的数据,就必须掌握 多表查询技术。
本文将带你全面掌握:
多表连接查询(内连接、外连接、自连接)
隐式 vs 显式连接的区别
联合查询:UNION 与 UNION ALL 的差异
子查询分类详解(标量、列、行、表)
附完整建表语句 + 实战案例 + 常见误区解析
二、建模准备:准备两张典型业务表 ------ emp 与 dept
1. 创建部门表 dept
sql
CREATE TABLE dept (
dept_id INT AUTO_INCREMENT PRIMARY KEY,
dept_name VARCHAR(50) NOT NULL UNIQUE COMMENT '部门名称',
location VARCHAR(100) COMMENT '办公地点'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门表';
2. 创建员工表 emp(带层级结构)
sql
CREATE TABLE emp (
emp_id INT AUTO_INCREMENT PRIMARY KEY,
emp_name VARCHAR(50) NOT NULL COMMENT '姓名',
gender CHAR(1) DEFAULT NULL COMMENT '性别:男/女',
age INT DEFAULT NULL COMMENT '年龄',
job VARCHAR(50) DEFAULT NULL COMMENT '职位',
salary DECIMAL(10, 2) DEFAULT NULL COMMENT '薪资',
manager_id INT DEFAULT NULL COMMENT '上级 ID(指向 emp_id)',
hire_date DATE DEFAULT NULL COMMENT '入职日期',
dept_id INT DEFAULT NULL COMMENT '部门 ID',
CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES dept(dept_id),
CONSTRAINT fk_emp_manager FOREIGN KEY (manager_id) REFERENCES emp(emp_id),
CONSTRAINT chk_gender CHECK (gender IN ('男', '女') OR gender IS NULL)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工表';
3. 添加索引(提升查询性能)
sql
CREATE INDEX idx_emp_dept_id ON emp(dept_id);
CREATE INDEX idx_emp_name ON emp(emp_name);
CREATE INDEX idx_emp_manager ON emp(manager_id);
4. 插入测试数据
sql
-- 插入部门
INSERT INTO dept (dept_name, location) VALUES
('技术部', '北京中关村'),
('市场部', '上海浦东'),
('人事部', '广州天河');
-- 插入员工(含上下级关系)
INSERT INTO emp (emp_name, gender, age, job, salary, hire_date, dept_id, manager_id) VALUES
('董事长', '男', 55, 'CEO', 100000.00, '2010-01-01', 1, NULL),
('张三', '男', 40, '技术总监', 35000.00, '2015-03-15', 1, 1),
('李四', '女', 38, '市场总监', 32000.00, '2016-07-01', 2, 1),
('王五', '男', 42, '人事总监', 30000.00, '2014-09-10', 3, 1),
('赵六', '男', 28, 'Java 工程师', 15000.00, '2020-03-15', 1, 2),
('钱七', '女', 26, '前端工程师', 13000.00, '2021-09-10', 1, 2),
('孙八', '男', 29, '后端工程师', 16000.00, '2019-05-20', 1, 2),
('周九', '女', 27, '市场专员', 12000.00, '2020-11-15', 2, 3),
('吴十', '男', 30, '市场经理', 18000.00, '2019-02-01', 2, 3),
('郑十一', '女', 25, '招聘专员', 10000.00, '2021-06-20', 3, 4),
('刘十二', '男', 33, '薪酬主管', 14000.00, '2018-12-01', 3, 4);
三、多表查询:连接查询详解
什么是笛卡尔积?
当你不加条件进行多表查询时,会生成 笛卡尔积(Cartesian Product):
sql
SELECT * FROM emp, dept;
-- 结果:emp 有11条,dept 有3条 → 共 11×3 = 33 条记录
⚠️ 必须通过
WHERE条件过滤!
1. 内连接:返回两个表的交集数据
(1)隐式内连接(旧式写法)
sql
SELECT e.emp_name, d.dept_name, e.dept_id
FROM emp e, dept d
WHERE e.dept_id = d.dept_id;
(2)显式内连接(推荐写法)
sql
SELECT e.emp_name, d.dept_name, e.dept_id
FROM emp e
INNER JOIN dept d ON e.dept_id = d.dept_id;
显式内连接 vs 隐式内连接 区别总结:
| 项目 | 隐式内连接 | 显式内连接 |
|---|---|---|
| 语法 | FROM A, B WHERE A.id = B.id |
FROM A INNER JOIN B ON A.id = B.id |
| 可读性 | 较差,结构混乱 | 清晰,易理解 |
| 推荐度 | 不推荐 | 强烈推荐 |
| 标准性 | 非标准 SQL(SQL-89) | 标准 SQL(SQL-92) |
| 扩展性 | 无法嵌套复杂连接 | 支持链式连接(如多表 JOIN) |
结论:永远优先使用
INNER JOIN显式连接!
2. 外连接:包含不匹配的数据
(1)左外连接(LEFT OUTER JOIN)
保留左表所有数据,右表没有匹配的则为
NULL
sql
SELECT e.emp_name, d.dept_name
FROM emp e
LEFT OUTER JOIN dept d ON e.dept_id = d.dept_id;
场景:查询所有员工,包括没有分配部门的员工
(2)右外连接(RIGHT OUTER JOIN)
保留右表所有数据
sql
SELECT e.emp_name, d.dept_name
FROM emp e
RIGHT OUTER JOIN dept d ON e.dept_id = d.dept_id;
场景:查询所有部门,即使没有员工
💡 一般很少用右外连接,可用左外连接倒换表顺序实现。
3. 自连接:表与自身连接
用于查询层级关系,如员工与上级关系
查询员工及其上级姓名(使用别名)
sql
SELECT e1.emp_name AS 员工, e2.emp_name AS 上级
FROM emp e1
LEFT JOIN emp e2 ON e1.manager_id = e2.emp_id;
常见用途:
- 组织架构图
- 评论回复链(parent_id)
- 多级分类树
四、联合查询:UNION vs UNION ALL
将多个
SELECT查询结果合并成一个结果集。
1、前提条件:
- 查询列数相同
- 每列数据类型兼容
2、示例:查询薪资低于 5000 或年龄大于 40 的人
sql
-- 使用 UNION ALL:保留所有数据(包含重复)
SELECT emp_name, salary, age, job
FROM emp
WHERE salary < 5000
UNION ALL
SELECT emp_name, salary, age, job
FROM emp
WHERE age > 40;
sql
-- 使用 UNION:会自动去重
SELECT emp_name, salary, age, job
FROM emp
WHERE salary < 5000
UNION
SELECT emp_name, salary, age, job
FROM emp
WHERE age > 40;
3、UNION vs UNION ALL 区别对比:
| 项目 | UNION |
UNION ALL |
|---|---|---|
| 是否去重 | 是 | 否 |
| 性能 | 较慢(需排序比对) | 快 |
| 使用建议 | 想要去重时 | 不关心重复或数据量大时 |
| 是否允许重复 | 否 | 允许 |
最佳实践:能用
UNION ALL就不用UNION!
五、子查询详解(附实战案例)
1、子查询定义:
SQL 语句中嵌套一个
SELECT语句,称为子查询。
⚠️ 注意:
- 子查询必须用小括号
()包裹 - 外层可以是
SELECT / INSERT / UPDATE / DELETE - 可出现在
WHERE、FROM、SET等子句中
(1)标量子查询(返回一个值)
返回 单行单列 的结果
sql
-- 查询技术部的部门 ID
SELECT dept_id FROM dept WHERE dept_name = '技术部';
-- 查询属于技术部的所有员工
SELECT *
FROM emp
WHERE dept_id = (SELECT dept_id FROM dept WHERE dept_name = '技术部');
常用操作符:=, >, <, <>, IN(可搭配 ANY)
(2)列子查询(返回一列,多行)
返回 多行一列 ,常用于
IN,ANY,SOME,ALL
sql
-- 查询市场部、人事部的员工
SELECT *
FROM emp
WHERE dept_id IN (
SELECT dept_id FROM dept WHERE dept_name IN ('市场部', '人事部')
);
拓展:ANY, SOME, ALL
sql
-- 薪资高于所有人事部员工的员工
SELECT * FROM emp
WHERE salary > ALL (
SELECT salary FROM emp WHERE dept_id = (SELECT dept_id FROM dept WHERE dept_name = '人事部')
);
-- 薪资比任意一个人事部员工高的员工
SELECT * FROM emp
WHERE salary > ANY (
SELECT salary FROM emp WHERE dept_id = (SELECT dept_id FROM dept WHERE dept_name = '人事部')
);
ANY≈SOME,ALL表示"所有"
(3)行子查询(返回一行多列)
返回 一行,多列 ,常用于
(col1, col2) IN (...)
sql
-- 查询与赵六薪资、上级相同的所有员工
SELECT *
FROM emp
WHERE (salary, manager_id) IN (
SELECT salary, manager_id
FROM emp
WHERE emp_name = '赵六'
);
适合:主键匹配、联合键匹配
(4)表子查询(返回一张表)
子查询结果是一张完整的二维表,常用于
FROM子句
sql
-- 查询 2019 年入职的员工,连同其部门信息
SELECT t1.*, d.dept_name
FROM (
SELECT *
FROM emp
WHERE hire_date >= '2019-01-01' AND hire_date <= '2019-12-31'
) AS t1
LEFT JOIN dept d ON t1.dept_id = d.dept_id;
为什么必须加别名
AS t1?
答案:
- 如果不加别名,MySQL 将无法识别子查询结果的表名
- 所有子查询在
FROM中都必须有别名 - 否则会报错:
You have an error in your SQL syntax... near 'FROM (...) as t1'
通用规则:任何子查询在
FROM或JOIN中,都必须加别名
六、常见误区 & 最佳实践
| 误区 | 正确做法 |
|---|---|
用隐式连接(,)写法 |
改用 JOIN ON |
用 UNION 而不考虑性能 |
优先用 UNION ALL |
| 子查询没有别名 | FROM (SELECT ...) AS t 必须写 |
JOIN 时忘记 ON 条件 |
必须写清楚联合条件 |
| 多表查询不加索引 | 为 JOIN 字段创建索引 |
七、总结:多表查询核心要点
| 类型 | 核心语法 | 推荐写法 |
|---|---|---|
| 内连接 | JOIN ON |
✅ 显式推荐 |
| 左/右外连接 | LEFT JOIN / RIGHT JOIN |
✅ 推荐 |
| 自连接 | T1 JOIN T1 + 别名 |
✅ 必须加别名 |
| 联合查询 | UNION ALL > UNION |
✅ 性能优先 |
| 子查询 | 别名不能省略 | ✅ AS t 是必须的 |
八、多表关系建模:一对一 / 一对多 / 多对多(实战详解)
在实际开发中,我们常需要设计复杂的表关系。以下是三种最常见的数据关系建模方式,并结合你提供的数据结构,进行完整演示。
1. 多对多关系:学生 ↔ 课程(一个学生可选多门课,一门课可被多个学生选)
举例:学生"小王"选了 Java 和 Oracle 课程;"Java"课程有 3 个学生。
建表设计(三张表)
sql
-- 课程表
CREATE TABLE course (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL UNIQUE COMMENT '课程名称'
);
-- 学生表
CREATE TABLE stu (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL COMMENT '学生姓名'
);
-- 中间表:学生-课程关系(多对多)
CREATE TABLE student_course (
stu_id INT NOT NULL,
course_id INT NOT NULL,
PRIMARY KEY (stu_id, course_id), -- 联合主键,防止重复选课
FOREIGN KEY (stu_id) REFERENCES stu(id) ON DELETE CASCADE,
FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE
);
插入数据
sql
INSERT INTO course (name) VALUES ('Java'), ('Oracle'), ('MySQL');
INSERT INTO stu (name) VALUES ('小王'), ('小张'), ('小李'), ('小赵');
INSERT INTO student_course (stu_id, course_id)
VALUES
(1, 1), -- 小王 选 Java
(1, 2), -- 小王 选 Oracle
(2, 1), -- 小张 选 Java
(3, 3), -- 小李 选 MySQL
(4, 1); -- 小赵 选 Java
查询示例:查选了"Java"的学生
sql
SELECT s.name AS 学生姓名, c.name AS 课程名称
FROM stu s
JOIN student_course sc ON s.id = sc.stu_id
JOIN course c ON sc.course_id = c.id
WHERE c.name = 'Java';
特点总结:
- 必须使用中间表
- 中间表主键为联合主键
(stu_id, course_id) - 支持去重(一个学生不能重复选同一门课)
- 外键级联删除,数据一致性高
2. 一对多关系:老师 ↔ 学生(一个老师教多个学生,一个学生只能有一个老师)
举例:"张老师"教了小王、小张;"小李"只属于"李老师"。
建表设计
sql
-- 老师表
CREATE TABLE teacher (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL UNIQUE COMMENT '老师姓名'
);
-- 学生表(外键指向 teacher.id)
CREATE TABLE student_teacher (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL COMMENT '学生姓名',
teacher_id INT,
FOREIGN KEY (teacher_id) REFERENCES teacher(id) ON DELETE SET NULL
);
ON DELETE SET NULL:当老师删除时,学生teacher_id变为NULL,不级联删除。
插入数据
sql
INSERT INTO teacher (name) VALUES ('张老师'), ('李老师');
INSERT INTO student_teacher (name, teacher_id)
VALUES
('小王', 1),
('小张', 1),
('小李', 2),
('小赵', 2);
查询示例:查"张老师"教的所有学生
sql
SELECT t.name AS 老师, s.name AS 学生
FROM teacher t
JOIN student_teacher s ON t.id = s.teacher_id
WHERE t.name = '张老师';
特点总结:
- 从"多"的一方添加外键
- 一对多关系建模标准写法
- 外键可设为
NULL(表示未分配老师)
3. 一对一关系:老师 ↔ 专属学生(一个老师只能有一个专属学生)
举例:"王老师"只有一个专用学生"小李";"小李"只能属于"王老师"。
建表设计
sql
-- 老师表
CREATE TABLE teacher_one_to_one (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL UNIQUE
);
-- 学生表(一对一,外键 + 唯一约束)
CREATE TABLE student_one_to_one (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
teacher_id INT UNIQUE, -- 唯一约束,确保一个老师对应一个学生
FOREIGN KEY (teacher_id) REFERENCES teacher_one_to_one(id) ON DELETE CASCADE
);
关键点:
teacher_id字段加了UNIQUE,保证"一个老师只能有一个专属学生"。
插入数据
sql
INSERT INTO teacher_one_to_one (name) VALUES ('王老师'), ('陈老师');
INSERT INTO student_one_to_one (name, teacher_id)
VALUES
('小李', 1), -- 王老师专属学生
('小赵', 2); -- 陈老师专属学生
查询示例:查所有专属师生关系
sql
SELECT t.name AS 老师, s.name AS 学生
FROM teacher_one_to_one t
JOIN student_one_to_one s ON t.id = s.teacher_id;
特点总结:
- 通过外键 +
UNIQUE实现 - 可双向查询
- 适合"专属角色"场景:如秘书、导师、医生--患者等
三类关系对比(一图胜千言)
| 关系类型 | 表结构 | 主键/外键设计 | 建模要点 |
|---|---|---|---|
| 多对多 | 3 张表:A、B、中间表 | 中间表用联合主键 (a_id, b_id) |
必须用中间表,防止重复 |
| 一对多 | 2 张表:A(主),B(从) | 从表加外键,可为 NULL |
外键放在"多"的一方 |
| 一对一 | 2 张表:A(主),B(从) | 从表外键 + UNIQUE |
用 UNIQUE 约束保证唯一性 |
九、总结:如何选择正确的建模方式?
| 需求场景 | 推荐关系 | 建议写法 |
|---|---|---|
| 一个用户可关注多个标签,一个标签可被多个用户关注 | 多对多 | 用中间表 + 联合主键 |
| 一个老师可带多个学生,一个学生只能有一个老师 | 一对多 | 外键放学生表 |
| 一个医生只负责一个病人,一个病人只能由一个医生负责 | 一对一 | 外键 + UNIQUE |
| 一个订单可有多个订单项,一个订单项只能属于一个订单 | 一对多 | 外键放项表 |
| 一个图书可被多个借阅人借阅,一个借阅人可借多本书 | 多对多 | 中间表:借阅记录表 |
特别提醒:建模时请遵守原则
- 不要把外键放在主表中 (如将
student_id放进teacher表) - 不要用
VARCHAR做主键(除非必要) - 外键必须有索引(提高查询性能)
- 使用
ON DELETE CASCADE/SET NULL考虑数据一致性 - 中间表命名要清晰 ,如
student_course、order_item