【MySQL】第四节 - 多表查询、多表关系全解析

📘 MySQL 多表查询、多表关系建模全解析|从基础语法到高阶技巧(含子查询、连接、联合查询实战;多表关系建模:一对一 / 一对多 / 多对多 实战)

💡 本文基于 MySQL 实践,结合实际业务场景,讲解多表查询的核心知识。适合初学者系统学习,也适合进阶者查漏补缺 ✅

一、前言:为什么要学多表查询?

在真实开发中,一个系统往往涉及多个数据表。例如:

  • 员工信息(emp
  • 部门信息(dept
  • 学生与课程关系(stu, course, student_course

这些表之间存在 多对多、一对多、一对一 等复杂关系。

想要从这些表中查询出有意义的数据,就必须掌握 多表查询技术

本文将带你全面掌握:

多表连接查询(内连接、外连接、自连接)

隐式 vs 显式连接的区别

联合查询:UNIONUNION ALL 的差异

子查询分类详解(标量、列、行、表)

附完整建表语句 + 实战案例 + 常见误区解析


二、建模准备:准备两张典型业务表 ------ empdept

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
  • 可出现在 WHEREFROMSET 等子句中

(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 = '人事部')
);

ANYSOMEALL 表示"所有"


(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'

通用规则:任何子查询在 FROMJOIN 中,都必须加别名


六、常见误区 & 最佳实践

误区 正确做法
用隐式连接(,)写法 改用 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
一个订单可有多个订单项,一个订单项只能属于一个订单 一对多 外键放项表
一个图书可被多个借阅人借阅,一个借阅人可借多本书 多对多 中间表:借阅记录表

特别提醒:建模时请遵守原则

  1. 不要把外键放在主表中 (如将 student_id 放进 teacher 表)
  2. 不要用 VARCHAR 做主键(除非必要)
  3. 外键必须有索引(提高查询性能)
  4. 使用 ON DELETE CASCADE / SET NULL 考虑数据一致性
  5. 中间表命名要清晰 ,如 student_courseorder_item
相关推荐
Database_Cool_3 小时前
OpenClaw-Observability:基于 DuckDB 构建 OpenClaw 的全链路可观测体系
数据库·阿里云·ai
刘~浪地球3 小时前
Redis 从入门到精通(五):哈希操作详解
数据库·redis·哈希算法
zzh0814 小时前
MySQL高可用集群笔记
数据库·笔记·mysql
Shely20174 小时前
MySQL数据表管理
数据库·mysql
爬山算法4 小时前
MongoDB(80)如何在MongoDB中使用多文档事务?
数据库·python·mongodb
APguantou5 小时前
NCRE-三级数据库技术-第2章-需求分析
数据库·需求分析
寂夜了无痕5 小时前
MySQL 主从延迟全链路根因诊断与破局法则
数据库·mysql·mysql主从延迟
爱丽_5 小时前
分页为什么越翻越慢:offset 陷阱、seek 分页与索引排序优化
数据库·mysql
APguantou5 小时前
NCRE-三级数据库技术-第12章-备份与数据库恢复
数据库·sqlserver