8. 复合查询【由浅入深-MySQL】

文章目录

  • [第一章 基本查询回顾](#第一章 基本查询回顾)
    • [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;

这条语句的执行过程如下:

  1. 从第一张表(emp)中取出第一条记录,与第二张表(dept)中的每一条记录依次组合,生成若干条临时记录。
  2. 再从 emp 中取出第二条记录,同样与 dept 的每一条记录组合。
  3. 如此循环,直到 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;

执行逻辑:

  1. 先做笛卡尔积。
  2. 通过 emp.deptno = dept.deptno 保留有效关联。
  3. 再通过 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;

执行步骤:

  1. 先对 emp 和 dept 做等值连接,形成中间结果。
  2. 再与 salgrade 做非等值连接(基于工资区间)。
  3. 最终结果包含员工姓名、部门名称和工资等级。

对于三张以上表的连接,原则是:每两个表之间都必须有连接条件 ,否则会产生无意义的笛卡尔积。一般 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');

执行过程:

  1. 内层子查询 (SELECT mgr FROM emp WHERE ename = 'FORD') 返回 FORD 的上级编号 7566。
  2. 外层查询变为 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 自连接的执行细节------笛卡尔积视角

为了更透彻理解自连接,我们模拟其执行过程:

  1. 笛卡尔积生成 :emp leader 有 14 行,emp worker 也有 14 行,二者组合产生 14 × 14 = 196 行临时数据。每一行都包含 leader 的全部列和 worker 的全部列(由于列名相同,必须用别名区分)。
  2. 连接条件过滤 :保留 leader.empno = worker.mgr 的行。这个条件会将每个"员工记录"(worker)匹配到它的"上级记录"(leader)。注意,worker.mgr 为 NULL 的员工(如 KING)不会匹配任何 leader,因此被排除。
  3. 业务条件过滤 :在剩余的匹配行中,再筛选出 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 自连接的注意事项

  1. 必须使用别名 :这是语法强制要求。
  2. 连接条件要准确 :自连接中很容易写反条件(如写成 leader.mgr = worker.empno),导致逻辑错误。务必理清"谁是谁的上级"。
  3. 避免大表笛卡尔积 :自连接同样会先生成笛卡尔积,如果表数据量很大,性能开销显著。务必确保连接条件和过滤条件能有效缩减中间结果,必要时可使用索引(如 empno 和 mgr 字段)。
  4. 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');

执行过程:

  1. 内层子查询 (SELECT deptno FROM emp WHERE ename = 'SMITH') 返回 SMITH 的部门编号(20)。
  2. 外层查询变为 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 子查询与合并查询的实战建议

  1. 子查询的优先级 :能用 JOIN 实现的,不一定非用子查询,但子查询在逻辑分层上往往更清晰(尤其是派生表)。
  2. 性能考量 :IN 子查询在数据量大时可能效率偏低,可改写为 EXISTS 或 JOIN 来优化。但作为入门,我们优先掌握正确性。
  3. 派生表的别名 :在 FROM 中的子查询必须有别名,即使不使用它,语法也强制要求。
  4. UNION 的列对齐:若列名不同,可以用别名统一;若数据类型不一致,MySQL 会尝试隐式转换,但最好显式转换以避免意外。
  5. 避免过度嵌套:虽然子查询可以多层嵌套,但过深的嵌套影响可读性和性能,应尽量控制在三层以内。
相关推荐
Gh0st_Lx1 小时前
【2】MySQL 为什么使用 B+树
数据库·b树·mysql
熊文豪1 小时前
机器人开发者生态分析:行业分工变化与启元的定位
android·机器人·启元机器人
北风toto1 小时前
数据库模式分解:如何判定“保持函数依赖”?
数据库
郑州光合科技余经理2 小时前
同城商城源码:商品库和订单库怎么随包交付
android·java·开发语言·后端·uni-app·php·ai编程
极限挑战哥2 小时前
深度拆解团购多商户分账系统:规则引擎 + 账户体系 + 逆向清算全解析
网络·数据库·人工智能
数据狐(Datafox)2 小时前
一文读懂京东商品详情 API:返回字段解读与 JSON 样例
开发语言·数据库·爬虫·json
zyseo82 小时前
谷歌SEO 抓取日志分析脚本:解析访问日志,看穿 Googlebot 的抓取行为
大数据·数据库·搜索引擎·自动化
蒸鱼Yuzheng2 小时前
移动游戏功耗与能效归因:帧率、画质、温控和续航如何一起评估
android·性能测试·功耗优化·游戏性能·温控