文章目录
- [第一章 基本查询回顾](#第一章 基本查询回顾)
-
- [1.1 条件过滤与多条件组合](#1.1 条件过滤与多条件组合)
- [1.2 排序:多字段排序与方向控制](#1.2 排序:多字段排序与方向控制)
- [1.3 使用表达式与别名生成新列](#1.3 使用表达式与别名生成新列)
- [1.4 子查询:获取极值记录](#1.4 子查询:获取极值记录)
- [1.5 子查询与平均值比较](#1.5 子查询与平均值比较)
- [1.6 分组聚合:GROUP BY 与聚合函数](#1.6 分组聚合:GROUP BY 与聚合函数)
- [1.7 分组后过滤:HAVING 子句](#1.7 分组后过滤:HAVING 子句)
- [1.8 多维度分组与统计](#1.8 多维度分组与统计)
- [第二章 多表查询](#第二章 多表查询)
-
- [2.1 为什么需要多表查询](#2.1 为什么需要多表查询)
- [2.2 笛卡尔积------不加条件的连接](#2.2 笛卡尔积——不加条件的连接)
- [2.3 等值连接------通过外键关联](#2.3 等值连接——通过外键关联)
- [2.4 多条件过滤------连接条件 + 业务条件](#2.4 多条件过滤——连接条件 + 业务条件)
- [2.5 非等值连接------范围匹配](#2.5 非等值连接——范围匹配)
- [2.6 表别名------简化书写与自连接](#2.6 表别名——简化书写与自连接)
- [2.7 多表连接------超过两张表](#2.7 多表连接——超过两张表)
- [2.8 关于列名的限定规则](#2.8 关于列名的限定规则)
- [第三章 自连接](#第三章 自连接)
-
- [3.1 为什么需要自连接](#3.1 为什么需要自连接)
- [3.2 解决思路一:子查询](#3.2 解决思路一:子查询)
- [3.3 解决思路二:自连接](#3.3 解决思路二:自连接)
- [3.4 自连接的执行细节------笛卡尔积视角](#3.4 自连接的执行细节——笛卡尔积视角)
- [3.5 表别名在自连接中的必要性](#3.5 表别名在自连接中的必要性)
- [3.6 自连接的扩展应用](#3.6 自连接的扩展应用)
- [3.7 自连接与 JOIN 语法的对应](#3.7 自连接与 JOIN 语法的对应)
- [3.8 自连接的注意事项](#3.8 自连接的注意事项)
- [3.9 对比总结:子查询 vs 自连接](#3.9 对比总结:子查询 vs 自连接)
- [第四章 子查询与合并查询](#第四章 子查询与合并查询)
-
- [4.1 子查询的基本概念](#4.1 子查询的基本概念)
- [4.2 单行子查询](#4.2 单行子查询)
- [4.3 多行子查询](#4.3 多行子查询)
-
- [4.3.1 IN 关键字](#4.3.1 IN 关键字)
- [4.3.2 ALL 关键字](#4.3.2 ALL 关键字)
- [4.3.3 ANY 关键字](#4.3.3 ANY 关键字)
- [4.4 多列子查询](#4.4 多列子查询)
- [4.5 在 FROM 子句中使用子查询------派生表](#4.5 在 FROM 子句中使用子查询——派生表)
- [4.6 多表转单表的指导思想](#4.6 多表转单表的指导思想)
- [4.7 合并查询:UNION 与 UNION ALL](#4.7 合并查询:UNION 与 UNION ALL)
-
- [4.7.1 UNION------去重合并](#4.7.1 UNION——去重合并)
- [4.7.2 UNION ALL------保留重复](#4.7.2 UNION ALL——保留重复)
- [4.8 子查询与合并查询的实战建议](#4.8 子查询与合并查询的实战建议)
第一章 基本查询回顾

在深入复合查询之前,我们有必要先夯实单表查询的基础。本章将基于经典的员工(EMP)和部门(DEPT)表,系统回顾条件过滤、排序、函数计算、分组聚合以及子查询等核心操作。所有示例均围绕上图所示的两张表展开(EMP表包含员工编号、姓名、岗位、上级编号、入职日期、工资、奖金、部门编号;DEPT表包含部门编号、部门名称、所在地)。
1.1 条件过滤与多条件组合
查询的本质是从表中提取满足特定逻辑的行。WHERE 子句用于设定过滤条件,支持比较运算符(=、>、<、>=、<=、<>)、逻辑运算符(AND、OR、NOT)以及模糊匹配(LIKE)等。
示例需求 :查询工资高于500 或 岗位为 MANAGER 的雇员,同时还要满足他们的姓名首字母为大写的 J。
SQL 实现:
sql
SELECT * FROM EMP
WHERE (sal > 500 OR job = 'MANAGER')
AND ename LIKE 'J%';
语法拆解:
sal > 500 OR job = 'MANAGER':使用OR连接两个条件,满足其一即可。注意括号的运用------此处括号将OR组合成一个整体,再与后面的AND结合,确保逻辑优先级正确(AND优先级高于OR,若不添加括号,实际含义会变为sal > 500 OR (job='MANAGER' AND ename LIKE 'J%'),这与需求不符)。ename LIKE 'J%':LIKE用于字符串模式匹配。'J%'表示以字母 J 开头的任意长度字符串(%通配任意个字符)。若要匹配单个字符则使用_。SELECT *返回所有列,实际开发中建议明确列出所需字段以提高可读性和性能。
执行结果解读 :该语句会筛选出所有姓名以 J 开头,且(工资>500 或 岗位为 MANAGER)的员工。例如 JONES(工资2975,MANAGER)和 JAMES(工资950,CLERK,满足工资>500)都会被选中,而 SMITH 虽工资>500但姓名不以 J 开头则被排除。
1.2 排序:多字段排序与方向控制
ORDER BY 子句用于对结果集排序,支持按一个或多个字段升序(ASC,默认)或降序(DESC)排列。
示例需求:按照部门号升序,而雇员的工资降序排序。
SQL 实现:
sql
SELECT * FROM EMP
ORDER BY deptno ASC, sal DESC;
语法要点:
- 排序优先级从左至右:先按
deptno升序(10 → 20 → 30 → 40),在相同deptno内部再按sal降序(工资高者在前)。 ASC可省略(默认升序),但为清晰起见建议保留。DESC必须显式指定。- 支持按列别名、表达式或列的位置(如
ORDER BY 2, 3)排序,但位置排序不推荐,因为列顺序变更会导致错误。
扩展思考 :若需对 NULL 值排序,MySQL 默认将 NULL 视为最小值(升序时排在最前),可通过 ORDER BY sal IS NULL, sal 等方式自定义 NULL 位置。
1.3 使用表达式与别名生成新列
查询时不仅可以原样输出列,还可以对列进行算术运算或函数处理,并利用 AS 赋予临时别名,便于后续引用。
示例需求:使用年薪进行降序排序。年薪 = 月薪 × 12 + 奖金(奖金可能为 NULL,需处理)。
SQL 实现:
sql
SELECT ename, sal * 12 + IFNULL(comm, 0) AS '年薪'
FROM EMP
ORDER BY 年薪 DESC;
核心知识点:
sal * 12 + IFNULL(comm, 0):计算年薪。IFNULL(comm, 0)表示若comm为 NULL 则替换为 0,避免算术运算结果为 NULL。MySQL 中也可用COALESCE(comm, 0)。AS '年薪':定义别名为"年薪",中文别名需加引号(单引号或反引号均可)。别名在ORDER BY中可直接引用(MySQL 允许,但标准 SQL 中ORDER BY可使用别名,而WHERE中不行,因为WHERE先于SELECT执行)。- 排序依据是计算后的年薪,而非原始工资。
执行结果:年薪最高的员工(如 KING,5000×12=60000)排在最前。
1.4 子查询:获取极值记录
当查询条件依赖于表中数据的统计结果(如最大值、平均值)时,需借助子查询(内层查询)返回动态值,外层查询再使用该值。
示例需求:显示工资最高的员工的名字和工作岗位。
SQL 实现:
sql
SELECT ename, job FROM EMP
WHERE sal = (SELECT MAX(sal) FROM EMP);
分步解析:
- 内层子查询
(SELECT MAX(sal) FROM EMP)计算出整个表中的最高工资(如 5000)。 - 外层查询查找工资等于该值的员工。若存在多个员工同享最高工资,则会全部返回(本例仅 KING 一人)。
- 子查询必须用括号包裹,且返回标量值(单行单列)时可用于
=等比较运算符。若子查询返回多行,则需使用IN、ANY、ALL等。
替代方案 :也可用 ORDER BY sal DESC LIMIT 1 实现,但若有多人并列最高,LIMIT 1 只会返回一条,而子查询方式更准确。
1.5 子查询与平均值比较
示例需求:显示工资高于平均工资的员工信息。
SQL 实现:
sql
SELECT ename, sal FROM EMP
WHERE sal > (SELECT AVG(sal) FROM EMP);
详细说明:
- 子查询
AVG(sal)计算所有员工的平均工资(包含 NULL 吗?AVG自动忽略 NULL 值,仅对非空行求平均,这符合业务逻辑)。 - 外层查询筛选出工资大于该平均值的员工。同样,子查询返回单值,可直接比较。
- 注意:此查询每次执行都会重新计算平均值,若表数据量极大,子查询可先单独计算并存入变量以优化,但本例无此必要。
1.6 分组聚合:GROUP BY 与聚合函数
聚合函数(AVG、MAX、MIN、SUM、COUNT 等)用于对一组行进行计算并返回单值。结合 GROUP BY 可按某个维度分组统计。
示例需求1:显示每个部门的平均工资和最高工资。
SQL 实现:
sql
SELECT deptno, FORMAT(AVG(sal), 2) AS avg_sal, MAX(sal) AS max_sal
FROM EMP
GROUP BY deptno;
关键点:
GROUP BY deptno将数据按部门分组,每个部门形成一个组。AVG(sal)计算每组内的平均工资;MAX(sal)计算组内最高工资。FORMAT(AVG(sal), 2)将平均工资格式化为保留两位小数的字符串(便于阅读)。若需数值类型,可直接用AVG(sal),MySQL 返回小数。SELECT中出现的非聚合列必须出现在GROUP BY中(此处deptno满足)。
执行结果 :输出部门 10、20、30 的平均和最高工资,部门 40 无员工则不显示(因为 INNER JOIN ?不,此处仅 EMP 表,无员工自然无分组)。
1.7 分组后过滤:HAVING 子句
WHERE 用于过滤原始行,而 HAVING 用于过滤分组后的结果,常与聚合函数配合使用。
示例需求:显示平均工资低于 2000 的部门号和它的平均工资。
SQL 实现:
sql
SELECT deptno, AVG(sal) AS avg_sal
FROM EMP
GROUP BY deptno
HAVING avg_sal < 2000;
区别辨析:
HAVING avg_sal < 2000中的avg_sal是分组后的平均工资,不能使用WHERE替换(因为WHERE在分组前执行,无法识别聚合结果)。- 可先分组再筛选,得到符合条件的组(例如部门 30 平均工资约 1566.67,满足条件;部门 20 平均工资 2175 不满足)。
HAVING也可使用聚合函数而不必别名,如HAVING AVG(sal) < 2000。
1.8 多维度分组与统计
GROUP BY 支持按多个字段分组,从而进行更细粒度的统计。
示例需求:显示每种岗位的雇员总数和平均工资。
SQL 实现:
sql
SELECT job, COUNT(*) AS emp_count, FORMAT(AVG(sal), 2) AS avg_sal
FROM EMP
GROUP BY job;
讲解:
COUNT(*)统计该岗位的员工总数(包含 NULL 值的行?COUNT(*)统计所有行,不论列是否为 NULL;若统计非空工资可用COUNT(sal))。GROUP BY job按岗位分组,输出每个岗位的人数及平均工资。- 结果包含 CLERK(3人?实际表中 CLERK 有多位)、MANAGER、SALESMAN、ANALYST、PRESIDENT 等。
进阶提示 :若需同时按部门和岗位分组,可写 GROUP BY deptno, job,此时统计的是每个部门内每种岗位的明细。
第二章 多表查询
实际业务中,数据往往分散在不同的表中------员工信息在 emp 表,部门名称在 dept 表,工资等级在 salgrade 表。要回答"某个员工属于哪个部门"或"他的工资属于哪个等级"这类问题,就必须同时查询多张表,并将它们关联起来。本章我们围绕这三张表(emp、dept、salgrade),系统讲解多表查询的核心原理与常见写法。
2.1 为什么需要多表查询
单表查询只能获取一张表内的信息。例如 SELECT * FROM emp 只能看到部门编号 deptno,而看不到部门名称 dname;SELECT * FROM dept 只能看到部门信息,却无法关联到具体员工。如果想让结果同时包含员工的姓名、工资以及他所在部门的名称,就必须将 emp 和 dept 连接起来。
连接的本质是:将两张表的记录按照某种条件组合成一条新的记录。
2.2 笛卡尔积------不加条件的连接

最简单的多表查询是直接列出多张表,不添加任何过滤条件:
sql
SELECT * FROM emp, dept;
这条语句的执行过程如下:
- 从第一张表(
emp)中取出第一条记录,与第二张表(dept)中的每一条记录依次组合,生成若干条临时记录。 - 再从
emp中取出第二条记录,同样与dept的每一条记录组合。 - 如此循环,直到
emp中的所有记录都被处理完毕。
这种无条件组合的结果称为笛卡尔积 (Cartesian Product)。若 emp 有 14 行,dept 有 4 行,则结果共有 14 × 4 = 56 行。每行都包含 emp 的所有列和 dept 的所有列。
笛卡尔积在数学上有意义,但在实际业务中几乎总是无用的------它会将每个员工强行匹配到每个部门,包括那些根本不存在的隶属关系(比如 SMITH 匹配到部门 40 OPERATIONS,而 SMITH 实际属于部门 20)。因此,多表查询必须通过连接条件来过滤出有意义的组合。
2.3 等值连接------通过外键关联
最常见的连接条件就是相等关系 :emp 表中的 deptno 必须等于 dept 表中的 deptno。这样每个员工只匹配到他真正所属的部门。
案例 1:显示雇员名、雇员工资以及所在部门的名字。
数据来自 emp(提供 ename、sal)和 dept(提供 dname),连接条件是 emp.deptno = dept.deptno。
sql
SELECT emp.ename, emp.sal, dept.dname
FROM emp, dept
WHERE emp.deptno = dept.deptno;
要点解析:
FROM emp, dept先产生笛卡尔积(56 行),然后WHERE子句逐行检查,只保留满足emp.deptno = dept.deptno的行。- 由于两张表中都有
deptno列,为了消除歧义,必须在列名前加表名限定 ,如emp.deptno或dept.deptno。对于其他唯一列(如ename、sal、dname),可以省略表名前缀,但为清晰起见,建议始终加上。
执行结果 :每个员工都会关联到正确的部门名称,例如 SMITH 显示为 RESEARCH,ALLEN 显示为 SALES。部门 40 没有员工,因此不会出现在结果中(因为没有匹配的 emp.deptno = 40)。
2.4 多条件过滤------连接条件 + 业务条件
WHERE 子句中可以同时包含连接条件和额外的业务过滤条件。两者用 AND 组合。
案例 2:显示部门号为 10 的部门名、员工名和工资。
sql
SELECT emp.ename, emp.sal, dept.dname
FROM emp, dept
WHERE emp.deptno = dept.deptno
AND dept.deptno = 10;
执行逻辑:
- 先做笛卡尔积。
- 通过
emp.deptno = dept.deptno保留有效关联。 - 再通过
dept.deptno = 10筛选出部门 10 的记录(等价于emp.deptno = 10,因为二者相等)。
结果只包含部门 10 的三位员工:CLARK、KING、MILLER,以及对应的部门名称 ACCOUNTING。
技巧:如果连接条件中的字段名相同,且业务条件也基于该字段,可以合并简化,但为可读性不建议过度简化。
2.5 非等值连接------范围匹配
连接条件不一定是相等,也可以是大于、小于或区间判断。典型的场景是工资等级表。
假设存在一张 salgrade 表,结构如下:
| grade | losal | hisal |
|---|---|---|
| 1 | 700 | 1200 |
| 2 | 1201 | 1400 |
| 3 | 1401 | 2000 |
| 4 | 2001 | 3000 |
| 5 | 3001 | 9999 |
losal 和 hisal 分别表示该等级的最低工资和最高工资。
案例 3:显示各个员工的姓名、工资及工资级别。
sql
SELECT emp.ename, emp.sal, salgrade.grade
FROM emp, salgrade
WHERE emp.sal BETWEEN salgrade.losal AND salgrade.hisal;
关键点:
- 连接条件不再是等号,而是区间判断
BETWEEN ... AND ...。 BETWEEN包含边界值,因此工资为 800 的 SMITH 会匹配到等级 1(700~1200),而工资为 3000 的 SCOTT 会匹配到等级 4(2001~3000),因为 3000 在 4 级范围内(注意等级 5 是 3001 以上,所以 3000 不算 5 级)。- 这种连接称为非等值连接,它在多表查询中同样常见。
2.6 表别名------简化书写与自连接
当查询涉及多张表时,表名可能很长(例如 employees、departments),每次都写全称很繁琐。MySQL 允许为表起别名,别名仅在当前查询中有效。
写法:
sql
SELECT e.ename, e.sal, d.dname
FROM emp e, dept d
WHERE e.deptno = d.deptno;
这里 e 代表 emp,d 代表 dept。
在 FROM 子句中为表定义别名后,后续 SQL 语句中必须使用该别名引用表,不能再使用原表名。
表别名还有一个重要用途:自连接 (同一张表与自身连接)。例如查询某个员工的上级姓名,需要将 emp 表既当作员工表又当作上级表。自连接时,必须给同一张表起不同的别名加以区分。
sql
-- 示例:显示员工及其上级的姓名
SELECT worker.ename AS 员工, manager.ename AS 上级
FROM emp worker, emp manager
WHERE worker.mgr = manager.empno;
自连接的本质是两张物理相同但逻辑不同的表,通过别名实现"虚拟多表"。
2.7 多表连接------超过两张表
多表查询同样可以连接三张或更多表。只需在 FROM 中列出所有表,并在 WHERE 中写出所有连接条件即可。
案例:显示员工姓名、部门名称以及工资等级。
需要同时连接 emp、dept 和 salgrade:
sql
SELECT e.ename, d.dname, s.grade
FROM emp e, dept d, salgrade s
WHERE e.deptno = d.deptno
AND e.sal BETWEEN s.losal AND s.hisal;
执行步骤:
- 先对
emp和dept做等值连接,形成中间结果。 - 再与
salgrade做非等值连接(基于工资区间)。 - 最终结果包含员工姓名、部门名称和工资等级。
对于三张以上表的连接,原则是:
每两个表之间都必须有连接条件,否则会产生无意义的笛卡尔积。一般 N 张表至少需要 N-1 个连接条件,但具体数量视业务关系而定。
2.8 关于列名的限定规则
在多表查询中,如果某个列名在所有参与表中唯一 (只出现在一张表中),则可以不添加表前缀。例如 ename 只存在于 emp,dname 只存在于 dept,所以 SELECT ename, dname 是合法的。
但若列名重复(如 deptno 同时存在于 emp 和 dept),则必须 加表名限定,否则 MySQL 会报错 ambiguous column(歧义列)。
最佳实践:即使列名唯一,也建议始终使用表别名前缀。这样做既提高可读性,也便于日后表结构变动时避免意外错误。
sql
-- 推荐写法
SELECT e.ename, e.sal, d.dname
FROM emp e, dept d
WHERE e.deptno = d.deptno;
第三章 自连接
在实际业务中,有一类特殊的连接查询------自连接 (Self-Join)。它指的是同一张表与自身进行连接操作。自连接并非连接不同的物理表,而是将一张表在逻辑上视为两张独立的表,通过别名加以区分,然后按照多表查询的规则进行关联 。本章我们以员工表 emp 中"员工-上级"的层级关系为例,深入剖析自连接的原理与写法。
3.1 为什么需要自连接
emp 表中存储了所有员工的信息,同时也通过 mgr 字段记录了每位员工的直接上级编号(mgr 指向另一位员工的 empno)。如果我们要查询"某位员工的上级的姓名",就面临一个矛盾:员工和上级都在同一张表里。
例如,员工 FORD 的 mgr 值为 7566,而 empno = 7566 对应的是 JONES。要同时获取 FORD 和 JONES 的信息,就必须把 emp 表既当作"员工表"使用,又当作"上级表"使用。单表查询只能返回同一行内的数据,无法跨行引用,因此必须借助自连接。
3.2 解决思路一:子查询
在讲解自连接之前,我们先看一种更直观的替代方案------子查询。子查询可以分步执行,先查出上级的编号,再根据编号查询上级的详细信息。
需求 :显示员工 FORD 的上级领导的编号和姓名。
SQL 实现:
sql
SELECT empno, ename
FROM emp
WHERE empno = (SELECT mgr FROM emp WHERE ename = 'FORD');
执行过程:
- 内层子查询
(SELECT mgr FROM emp WHERE ename = 'FORD')返回FORD的上级编号7566。 - 外层查询变为
SELECT empno, ename FROM emp WHERE empno = 7566,得到JONES的记录。
这种方法简单清晰,但存在局限性:
- 如果子查询返回多行(例如多个员工同名),则
=会报错,需改用IN。 - 子查询只能获取上级的信息,若想同时列出员工本人和上级的信息(例如"FORD 的上级是 JONES"),子查询无法在一行中同时展示两个不同行的数据,除非再嵌套更多子查询,效率较低且可读性差。
3.3 解决思路二:自连接
自连接通过将同一张表赋予不同的别名,把它视为两张独立的表来参与连接。这样我们就可以在一行结果中同时包含员工的信息和上级的信息。
案例 :显示员工 FORD 的上级领导的编号和姓名。
SQL 实现:
sql
SELECT leader.empno, leader.ename
FROM emp leader, emp worker
WHERE leader.empno = worker.mgr
AND worker.ename = 'FORD';
拆解分析:
FROM emp leader, emp worker:这里给emp起了两个别名,leader(代表上级表)和worker(代表员工表)。此时 MySQL 会针对这两张"虚拟表"生成笛卡尔积,即leader中的每一行与worker中的每一行组合。WHERE leader.empno = worker.mgr:连接条件限定为"上级表的员工编号 = 员工表的上级编号",这样只有正确的上下级关系才会被保留。AND worker.ename = 'FORD':业务过滤条件,只保留员工名为FORD的行。
执行结果 :返回一条记录,包含 leader.empno = 7566 和 leader.ename = 'JONES'。
3.4 自连接的执行细节------笛卡尔积视角
为了更透彻理解自连接,我们模拟其执行过程:
- 笛卡尔积生成 :
emp leader有 14 行,emp worker也有 14 行,二者组合产生 14 × 14 = 196 行临时数据。每一行都包含leader的全部列和worker的全部列(由于列名相同,必须用别名区分)。 - 连接条件过滤 :保留
leader.empno = worker.mgr的行。这个条件会将每个"员工记录"(worker)匹配到它的"上级记录"(leader)。注意,worker.mgr为NULL的员工(如KING)不会匹配任何leader,因此被排除。 - 业务条件过滤 :在剩余的匹配行中,再筛选出
worker.ename = 'FORD'的那一行。
最终得到 FORD 的上级信息。
这种写法不仅能够查询上级,还可以在一行中同时展示员工和上级的多个字段,例如:
sql
SELECT worker.ename AS 员工, leader.ename AS 上级
FROM emp leader, emp worker
WHERE leader.empno = worker.mgr;
这样就能列出所有员工及其上级的姓名,这是子查询难以优雅实现的。
3.5 表别名在自连接中的必要性
自连接的核心是别名。如果不使用别名,如下写法:
sql
SELECT empno, ename FROM emp, emp WHERE ... -- 错误
MySQL 将无法区分两个 emp,会报错"表名重复"。必须为至少一个实例赋予别名,让系统能够区分它们。通常两个实例都会起别名,一个代表"左表",一个代表"右表",便于理解。
别名一旦定义,在 SELECT、WHERE、ORDER BY 等子句中必须使用别名引用列,不能再写原表名(否则会歧义)。
3.6 自连接的扩展应用
自连接不仅仅用于层级关系查询,还可以用于:
- 查找相同字段的记录:例如找出工资相同的员工对。
- 比较同一张表中不同行的时间或数值:比如找出入职日期早于其上级的员工。
- 树形结构遍历:如组织架构、分类层级等。
举一个比较型的例子:查询工资高于员工 SMITH 的所有员工信息。
使用子查询:
sql
SELECT * FROM emp WHERE sal > (SELECT sal FROM emp WHERE ename = 'SMITH');
使用自连接(亦可):
sql
SELECT e2.*
FROM emp e1, emp e2
WHERE e1.ename = 'SMITH' AND e2.sal > e1.sal;
这里自连接将 SMITH 的记录作为参照,与所有其他员工进行比较。
3.7 自连接与 JOIN 语法的对应
现代 JOIN 语法同样支持自连接,写法更清晰:
sql
SELECT leader.empno, leader.ename
FROM emp worker
JOIN emp leader ON leader.empno = worker.mgr
WHERE worker.ename = 'FORD';
或者将 leader 表放在前面:
sql
SELECT leader.empno, leader.ename
FROM emp leader
JOIN emp worker ON leader.empno = worker.mgr
WHERE worker.ename = 'FORD';
两种写法等价 ,建议根据直觉选择哪个作为主表。在自连接中,JOIN ... ON 将连接条件与过滤条件分离,可读性优于逗号+WHERE。
3.8 自连接的注意事项
必须使用别名:这是语法强制要求。- 连接条件要准确 :自连接中很容易写反条件(如写成
leader.mgr = worker.empno),导致逻辑错误。务必理清"谁是谁的上级"。 - 避免大表笛卡尔积 :自连接同样会先生成笛卡尔积,如果表数据量很大,性能开销显著。务必确保连接条件和过滤条件能有效缩减中间结果,必要时可使用索引(如
empno和mgr字段)。 - NULL 值处理 :
mgr为NULL的员工(如KING)不会出现在等值连接结果中,除非使用外连接(后续章节介绍)。
3.9 对比总结:子查询 vs 自连接
| 维度 | 子查询 | 自连接 |
|---|---|---|
| 适用场景 | 查询结果来自同一张表的单行或单列统计 | 需要同时展示同一张表中不同行的多个字段 |
| 性能 | 通常两次查询(或一次查询 + 子查询) | 一次连接,但可能产生较大中间结果 |
| 可读性 | 简单需求时直观 | 复杂层级关系时更自然 |
| 限制 | 难以返回多行多维度的对比结果 | 可以任意组合同一张表中的任意两行 |
在实际开发中,两种方式各有优势。对于"查询上级信息"这种需求,自连接更为灵活;对于"查询工资大于平均工资"这类聚合比较,子查询更直接。本章重点掌握自连接的语法和思维方式,后续在复合查询中,您将能够根据业务场景灵活选择。
第四章 子查询与合并查询
多表查询虽然强大,但有时我们并不需要真正的"多表物理连接",而是希望基于某个查询的结果再去进行二次筛选或构造。这时,子查询 (Subquery)便派上了用场。子查询本质上是将一个 SELECT 语句嵌套在另一个 SQL 语句(SELECT、INSERT、UPDATE、DELETE)中,因此也常称为嵌套查询。
在 MySQL 中,有一个非常核心的设计哲学:一切皆表 。 任何查询结果------无论来自单表、多表连接,还是子查询本身------在逻辑上都可以看作一张"临时表"。 这种思想贯穿整个子查询和后续的复合查询。理解这一点,您就能明白为什么子查询可以出现在 FROM 子句中,为什么必须为其指定别名,以及如何将复杂的多表问题转化为单表问题。
本章我们系统梳理子查询的四种常见形式(单行、多行、多列、FROM 型)以及合并查询(UNION / UNION ALL),并通过大量案例帮助您掌握这套灵活的工具。
4.1 子查询的基本概念
子查询就是 SELECT 语句内部的 SELECT。它可以出现在:
WHERE子句中,作为过滤条件的一部分;FROM子句中,作为派生表(Derived Table);HAVING子句中,作为分组过滤的条件;SELECT列表中,作为标量运算的一部分(但较少见)。
子查询的返回值形态决定了它的分类:
- 单行子查询:返回一行记录(可能包含多列)。
- 多行子查询:返回多行记录(但通常单列)。
- 多列子查询:返回多列且可能多行,一般用于比较多个字段。
我们先从最常用的 WHERE 型子查询开始。
4.2 单行子查询
单行子查询是指内层查询最多返回一行 结果。因为返回行数确定,外层可以使用 =、>、<、>=、<=、<> 等标量比较运算符。
经典案例 :显示与 SMITH 在同一部门的员工信息(不包括 SMITH 本人)。
SQL 实现:
sql
SELECT * FROM emp
WHERE deptno = (SELECT deptno FROM emp WHERE ename = 'SMITH');
执行过程:
- 内层子查询
(SELECT deptno FROM emp WHERE ename = 'SMITH')返回SMITH的部门编号(20)。 - 外层查询变为
SELECT * FROM emp WHERE deptno = 20,返回部门 20 的所有员工。
注意事项:
- 如果子查询返回
空值(例如SMITH不存在),外层查询将不会返回任何行(因为deptno = NULL的结果是UNKNOWN,不满足WHERE)。 - 如果子查询返回
多行(例如有多个SMITH),使用=会报错,此时应改为IN
单行子查询也支持其他比较运算,例如查询工资高于平均工资的员工(已在第一章示例),或者查询入职日期早于 JONES 的员工等。
4.3 多行子查询
当子查询返回多行记录 (通常是一列多行)时,外层不能直接使用 =,必须使用多行操作符:
IN:匹配子查询结果中的任意一个。ALL:必须大于/小于/等于子查询结果中的所有值。ANY(或SOME):只要大于/小于/等于子查询结果中的任意一个值即可。
4.3.1 IN 关键字
需求:查询和部门 10 的工作岗位相同的雇员的名字、岗位、工资、部门号,但排除部门 10 本身的员工。
SQL 实现:
sql
SELECT ename, job, sal, deptno
FROM emp
WHERE job IN (SELECT DISTINCT job FROM emp WHERE deptno = 10)
AND deptno <> 10;
详细解析:
- 子查询
(SELECT DISTINCT job FROM emp WHERE deptno = 10)返回部门 10 的所有岗位(MANAGER、PRESIDENT、CLERK)。 - 外层查询筛选出岗位在这个列表中的员工,同时排除部门号为 10 的记录,从而得到其他部门中与部门 10 同岗位的员工。
IN 是最常用的多行操作符,等价于多个 OR 条件,但写法更简洁。
4.3.2 ALL 关键字
ALL 要求比较值必须大于(或小于)子查询返回的每一个值。例如"比部门 30 的所有员工工资都高"意味着工资要高于部门 30 中的最高工资。
需求 :显示工资比部门 30 的所有员工的工资都高的员工的姓名、工资和部门号。
SQL 实现:
sql
SELECT ename, sal, deptno
FROM emp
WHERE sal > ALL (SELECT sal FROM emp WHERE deptno = 30);
逻辑:
- 子查询返回部门 30 的所有工资(1600、1250、1250、1500、950 等,最高 1600)。
sal > ALL ( ... )等价于sal > 1600,因此结果会是工资高于 1600 的员工(如JONES、SCOTT、KING等)。
ALL 也可与 <、= 等组合,但 = 与 ALL 结合通常不如 IN 直观(除非子查询只有一个值)。
4.3.3 ANY 关键字
ANY 表示只要满足子查询结果中的任意一个即可。例如"比部门 30 的任意员工的工资高"等价于高于部门 30 的最低工资。
需求 :显示工资比部门 30 的任意员工的工资都高的员工的姓名、工资和部门号(包含自己部门的员工)。
SQL 实现:
sql
SELECT ename, sal, deptno
FROM emp
WHERE sal > ANY (SELECT sal FROM emp WHERE deptno = 30);
解析:
- 子查询返回部门 30 的工资列表,最低为 950。
sal > ANY ( ... )等价于sal > 950,因此几乎所有工资高于 950 的员工都会出现(包括部门 30 自己的员工,如ALLEN的 1600 满足条件)。- 注意:如果部门 30 存在工资为 NULL 的行,
ANY会忽略 NULL(因为比较sal > NULL结果未知),但本例中无 NULL 工资。
ANY 常与 > 或 < 配合,用于"超过任意一个"或"低于任意一个"的场景。
4.4 多列子查询
前面讨论的子查询都只返回一列 数据(无论单行还是多行)。但有时我们需要同时比较多个列。例如,不仅要岗位相同,还要部门相同。这时子查询可以返回多列,外层用括号将多个列组合起来进行比较。
需求 :查询和 SMITH 的部门 和岗位 完全相同的所有雇员,但不包含 SMITH 本人。
SQL 实现:
sql
SELECT ename
FROM emp
WHERE (deptno, job) = (SELECT deptno, job FROM emp WHERE ename = 'SMITH')
AND ename <> 'SMITH';
核心要点:
- 子查询
(SELECT deptno, job FROM emp WHERE ename = 'SMITH')返回一行两列,即(20, 'CLERK')。 - 外层
(deptno, job) = (20, 'CLERK')要求两个字段同时相等,这是一种行构造器比较语法。 - 最终结果会找出与
SMITH同部门、同岗位的其他员工(本例中为ADAMS)。
多列子查询同样支持 IN 形式,例如:
sql
SELECT * FROM emp
WHERE (deptno, job) IN (SELECT deptno, job FROM emp WHERE deptno = 20);
这表示找出与部门 20 内任意员工的部门和岗位组合相同的所有员工。
注意 :
多列子查询返回的列数必须与外层比较的列数一致,且顺序对应。
4.5 在 FROM 子句中使用子查询------派生表
这是子查询最强大的应用之一:将子查询结果作为一张临时表(派生表)参与到外层查询中。因为"一切皆表",任何查询结果都可以被当作普通表来使用。
语法要求 :子查询出现在 FROM 后面时,必须为其指定别名,否则 MySQL 无法引用该临时表的列。这一点与普通表名必须存在类似。
案例 1:显示每个高于自己部门平均工资的员工的姓名、部门、工资以及该部门的平均工资。
分析:需要先计算每个部门的平均工资,然后将此结果与员工表关联,筛选出工资高于本部门平均值的员工。
SQL 实现:
sql
SELECT e.ename, e.deptno, e.sal, tmp.avg_sal
FROM emp e,
(SELECT deptno, AVG(sal) AS avg_sal FROM emp GROUP BY deptno) tmp
WHERE e.deptno = tmp.deptno
AND e.sal > tmp.avg_sal;
拆解:
- 派生表
tmp:(SELECT deptno, AVG(sal) AS avg_sal FROM emp GROUP BY deptno)得到每个部门的平均工资,别名为tmp,包含两列deptno和avg_sal。 - 外层将
emp e与tmp等值连接(部门号相同),并筛选e.sal > tmp.avg_sal。
这样我们就将一个"需要分组后再比较"的多表问题转化为两张"表"的连接------一张是物理表 emp,另一张是逻辑上的统计表 tmp。
案例 2:查找每个部门工资最高的人的姓名、工资、部门以及该部门的最高工资。
思路类似,只需将 AVG 改为 MAX:
sql
SELECT e.ename, e.sal, e.deptno, tmp.max_sal
FROM emp e,
(SELECT deptno, MAX(sal) AS max_sal FROM emp GROUP BY deptno) tmp
WHERE e.deptno = tmp.deptno
AND e.sal = tmp.max_sal;
这里将员工表与"部门最高工资表"关联,找出每个部门中工资等于最高工资的员工(可能存在并列)。
案例 3:显示每个部门的信息(部门名、编号、地址)和人员数量。
有两种方法,我们分别展示多表连接和子查询两种思路。
方法 1 :直接使用 GROUP BY 与 DEPT 表连接。
sql
SELECT d.dname, d.deptno, d.loc, COUNT(*) AS 部门人数
FROM emp e, dept d
WHERE e.deptno = d.deptno
GROUP BY d.deptno, d.dname, d.loc;
方法 2:使用子查询先统计人数,再与部门表连接。
sql
SELECT d.deptno, d.dname, d.loc, tmp.cnt
FROM dept d,
(SELECT deptno, COUNT(*) AS cnt FROM emp GROUP BY deptno) tmp
WHERE d.deptno = tmp.deptno;
两种方法结果相同,但子查询方式将统计逻辑封装在派生表中,使主查询更加专注。尤其在复杂场景下,这种"分而治之"的思路非常有用。
4.6 多表转单表的指导思想
回顾上述所有案例,我们可以提炼出解决多表查询的一个核心思想:想办法将多表问题转化为单表问题。
- 多表连接的本质是先用笛卡尔积组合,再用条件筛选;但派生表的思路则是先将一部分数据聚合成一张"中间表",然后与另一张表做简单关联。
- 子查询让您能够分步处理:先独立构造一个结果集(临时表),再基于它做二次查询。这实际上是"化整为零",将复杂的多步逻辑拆解为若干单表或简单连接的组合。
在 MySQL 中,只要您掌握了"一切皆表"和"给派生表起别名"这两个要点,就可以灵活地组合各种子查询,实现任何复杂的统计需求。
4.7 合并查询:UNION 与 UNION ALL
除了子查询,有时我们需要将多个独立查询的结果集 合并到一起输出。例如,既要找出工资大于 2500 的员工,又要找出岗位为 MANAGER 的员工,并且要求结果中不重复(或保留重复)。MySQL 提供了 UNION 和 UNION ALL 两个操作符。
4.7.1 UNION------去重合并
UNION 用于合并两个或多个 SELECT 语句的结果集,并自动去除重复行 (即对最终结果做 DISTINCT)。
需求 :找出工资大于 2500 或职位是 MANAGER 的员工(显示姓名、工资、岗位)。
SQL 实现:
sql
SELECT ename, sal, job FROM emp WHERE sal > 2500
UNION
SELECT ename, sal, job FROM emp WHERE job = 'MANAGER';
结果解读:
- 第一个查询返回工资 >2500 的员工(
JONES、BLAKE、SCOTT、KING、FORD)。 - 第二个查询返回所有
MANAGER(JONES、BLAKE、CLARK)。 - 由于
JONES和BLAKE同时满足两个条件,UNION会去除重复,最终结果包含 6 行(比简单相加少 2 行)。
注意:
- 两个
SELECT语句的列数必须相同,且对应列的数据类型应兼容。 - 列名以第一个
SELECT的列名为准。 UNION会对整个结果集去重,性能开销较大 ,若确定无重复或允许重复,应优先使用UNION ALL。
4.7.2 UNION ALL------保留重复
UNION ALL 直接合并所有结果,不进行去重,因此效率更高。
需求 :同样找工资大于 2500 或职位是 MANAGER 的员工,但保留所有匹配记录(包括重复)。
SQL 实现:
sql
SELECT ename, sal, job FROM emp WHERE sal > 2500
UNION ALL
SELECT ename, sal, job FROM emp WHERE job = 'MANAGER';
结果:
- 工资 >2500 的 5 条记录全部列出。
- 岗位为
MANAGER的 3 条记录全部列出。 - 因为
JONES和BLAKE在两边的结果中都存在,所以会重复出现,总行数为 8。
使用建议:
- 如果业务逻辑允许重复,或者两个子查询的结果集互不重叠,务必使用
UNION ALL以避免不必要的去重开销。 - 如果希望去掉重复行,再使用
UNION。
重要约束 :无论是 UNION 还是 UNION ALL,所有参与合并的 SELECT 语句必须拥有相同数量的列,且列的顺序必须一致。否则 MySQL 会报错。
4.8 子查询与合并查询的实战建议
- 子查询的优先级 :能用
JOIN实现的,不一定非用子查询,但子查询在逻辑分层上往往更清晰(尤其是派生表)。 - 性能考量 :
IN子查询在数据量大时可能效率偏低,可改写为EXISTS或JOIN来优化。但作为入门,我们优先掌握正确性。 - 派生表的别名 :在
FROM中的子查询必须有别名,即使不使用它,语法也强制要求。 UNION的列对齐:若列名不同,可以用别名统一;若数据类型不一致,MySQL 会尝试隐式转换,但最好显式转换以避免意外。- 避免过度嵌套:虽然子查询可以多层嵌套,但过深的嵌套影响可读性和性能,应尽量控制在三层以内。