MySQL 从零入门(六):复合查询详解——多表查询、自连接、子查询与 UNION

MySQL 从零入门(六):复合查询详解------多表查询、自连接、子查询与 UNION(附完整 SQL 实战)


🔥 星恒随风: 个人主页 ❄️ 个人专栏: 《Linux网络编程从0到1》 | 《C语言基础》 | 《数据结构和算法》 | 《MySQL入门心法》 | 《前端基础》 | 《python基础》 | 《C++从入门到入土》 | 《Linux系统编程》 ✨ 数据即知识,压缩即智能


文章目录

  • [MySQL 从零入门(六):复合查询详解------多表查询、自连接、子查询与 UNION(附完整 SQL 实战)](#MySQL 从零入门(六):复合查询详解——多表查询、自连接、子查询与 UNION(附完整 SQL 实战))
  • 一、准备实验环境
    • [1.1 创建数据库](#1.1 创建数据库)
    • [1.2 创建员工表 EMP](#1.2 创建员工表 EMP)
    • [1.3 创建部门表 DEPT](#1.3 创建部门表 DEPT)
    • [1.4 创建工资等级表 SALGRADE](#1.4 创建工资等级表 SALGRADE)
    • [1.5 插入测试数据](#1.5 插入测试数据)
  • [二、8.1 基础查询回顾](#二、8.1 基础查询回顾)
    • [2.1 查询工资大于 500 或岗位为 MANAGER,并且姓名以 J 开头的员工](#2.1 查询工资大于 500 或岗位为 MANAGER,并且姓名以 J 开头的员工)
    • [2.2 按部门升序、工资降序排列](#2.2 按部门升序、工资降序排列)
    • [2.3 按年薪降序排序](#2.3 按年薪降序排序)
    • [2.4 查询工资最高的员工](#2.4 查询工资最高的员工)
    • [2.5 查询工资高于平均工资的员工](#2.5 查询工资高于平均工资的员工)
    • [2.6 显示每个部门的平均工资和最高工资](#2.6 显示每个部门的平均工资和最高工资)
    • [2.7 查询平均工资低于 2000 的部门](#2.7 查询平均工资低于 2000 的部门)
    • [2.8 查询每种岗位的员工数量和平均工资](#2.8 查询每种岗位的员工数量和平均工资)
  • [三、8.2 多表查询](#三、8.2 多表查询)
    • [3.1 多表查询的基本形式](#3.1 多表查询的基本形式)
    • [3.2 什么是笛卡尔积?](#3.2 什么是笛卡尔积?)
    • [3.3 查询员工姓名、工资和部门名称](#3.3 查询员工姓名、工资和部门名称)
    • [3.4 使用显式 INNER JOIN](#3.4 使用显式 INNER JOIN)
    • [3.5 查询 10 号部门的名称、员工姓名和工资](#3.5 查询 10 号部门的名称、员工姓名和工资)
    • [3.6 查询员工姓名、工资和工资等级](#3.6 查询员工姓名、工资和工资等级)
  • [四、8.3 自连接](#四、8.3 自连接)
    • [4.1 什么是自连接?](#4.1 什么是自连接?)
    • [4.2 查询 FORD 的上级领导:子查询方式](#4.2 查询 FORD 的上级领导:子查询方式)
    • [4.3 查询 FORD 的上级领导:自连接方式](#4.3 查询 FORD 的上级领导:自连接方式)
    • [4.4 自连接的显式 JOIN 写法](#4.4 自连接的显式 JOIN 写法)
  • [五、8.4 子查询](#五、8.4 子查询)
    • [5.1 什么是子查询?](#5.1 什么是子查询?)
    • [5.2 子查询的分类](#5.2 子查询的分类)
  • [六、8.4.1 单行子查询](#六、8.4.1 单行子查询)
    • [6.1 基本概念](#6.1 基本概念)
    • [6.2 查询与 SMITH 同一部门的员工](#6.2 查询与 SMITH 同一部门的员工)
    • [6.3 单行子查询的注意事项](#6.3 单行子查询的注意事项)
  • [七、8.4.2 多行子查询](#七、8.4.2 多行子查询)
    • [7.1 IN:匹配结果集合中的任意一个值](#7.1 IN:匹配结果集合中的任意一个值)
    • [7.2 ALL:满足所有比较条件](#7.2 ALL:满足所有比较条件)
    • [7.3 ANY:满足任意一个比较条件](#7.3 ANY:满足任意一个比较条件)
    • [7.4 ALL 与 ANY 的区别](#7.4 ALL 与 ANY 的区别)
  • [八、8.4.3 多列子查询](#八、8.4.3 多列子查询)
    • [8.1 什么是多列子查询?](#8.1 什么是多列子查询?)
    • [8.2 查询与 SMITH 部门和岗位都相同的员工](#8.2 查询与 SMITH 部门和岗位都相同的员工)
    • [8.3 为什么要加括号?](#8.3 为什么要加括号?)
    • [8.4 与多个 AND 条件有什么区别?](#8.4 与多个 AND 条件有什么区别?)
  • [九、8.4.4 在 FROM 子句中使用子查询](#九、8.4.4 在 FROM 子句中使用子查询)
    • [9.1 基本语法](#9.1 基本语法)
    • [9.2 查询工资高于本部门平均工资的员工](#9.2 查询工资高于本部门平均工资的员工)
    • [9.3 使用显式 JOIN 改写](#9.3 使用显式 JOIN 改写)
  • 十、查询每个部门工资最高的员工
    • [10.1 需求分析](#10.1 需求分析)
    • [10.2 第一步:查询每个部门最高工资](#10.2 第一步:查询每个部门最高工资)
    • [10.3 第二步:关联原始员工表](#10.3 第二步:关联原始员工表)
  • 十一、查询每个部门的信息与员工数量
    • [11.1 方法一:直接多表查询](#11.1 方法一:直接多表查询)
    • [11.2 方法二:先统计,再关联](#11.2 方法二:先统计,再关联)
    • [11.3 补充:如何保留没有员工的部门?](#11.3 补充:如何保留没有员工的部门?)
  • [十二、8.4.5 合并查询:UNION 与 UNION ALL](#十二、8.4.5 合并查询:UNION 与 UNION ALL)
    • [12.1 UNION 基本语法](#12.1 UNION 基本语法)
    • [12.2 查询工资大于 2500 或岗位为 MANAGER 的员工](#12.2 查询工资大于 2500 或岗位为 MANAGER 的员工)
    • [12.3 UNION 的去重规则](#12.3 UNION 的去重规则)
    • [12.4 UNION ALL](#12.4 UNION ALL)
    • [12.5 UNION 与 UNION ALL 的区别](#12.5 UNION 与 UNION ALL 的区别)
    • [12.6 UNION 的使用条件](#12.6 UNION 的使用条件)
  • 十三、多表查询、自连接和子查询的区别
    • [13.1 多表查询](#13.1 多表查询)
    • [13.2 自连接](#13.2 自连接)
    • [13.3 子查询](#13.3 子查询)
    • [13.4 三者对比](#13.4 三者对比)
  • [十四、SQL 综合实战](#十四、SQL 综合实战)
    • [14.1 查询每名员工及其直属领导姓名](#14.1 查询每名员工及其直属领导姓名)
    • [14.2 查询每个部门的最高工资](#14.2 查询每个部门的最高工资)
    • [14.3 查询每个部门工资最高的员工姓名](#14.3 查询每个部门工资最高的员工姓名)
    • [14.4 查询工资高于本部门平均工资的员工](#14.4 查询工资高于本部门平均工资的员工)
    • [14.5 查询工资高于所有 MANAGER 的员工](#14.5 查询工资高于所有 MANAGER 的员工)
    • [14.6 查询与 FORD 相同岗位的其他员工](#14.6 查询与 FORD 相同岗位的其他员工)
  • 十五、常见问题总结
    • [15.1 为什么多表查询会返回大量重复组合?](#15.1 为什么多表查询会返回大量重复组合?)
    • [15.2 为什么多表查询提示字段名不明确?](#15.2 为什么多表查询提示字段名不明确?)
    • [15.3 为什么子查询返回多行时报错?](#15.3 为什么子查询返回多行时报错?)
    • [15.4 为什么 FROM 子查询必须有别名?](#15.4 为什么 FROM 子查询必须有别名?)
    • [15.5 ALL 和 ANY 有什么区别?](#15.5 ALL 和 ANY 有什么区别?)
    • [15.6 为什么查询每个部门的最高工资不能直接返回员工姓名?](#15.6 为什么查询每个部门的最高工资不能直接返回员工姓名?)
    • [15.7 为什么 UNION 合并后行数变少?](#15.7 为什么 UNION 合并后行数变少?)
    • [15.8 为什么 INNER JOIN 没有显示全部部门?](#15.8 为什么 INNER JOIN 没有显示全部部门?)
  • [十六、实战 SQL 练习](#十六、实战 SQL 练习)
    • [16.1 查询员工入职时的薪水](#16.1 查询员工入职时的薪水)
    • [16.2 为数据库中的每张表生成 COUNT SQL](#16.2 为数据库中的每张表生成 COUNT SQL)
    • [16.3 查询不是经理的员工](#16.3 查询不是经理的员工)
    • [16.4 查询员工当前的经理](#16.4 查询员工当前的经理)
  • [十七、常用 SQL 语法速查表](#十七、常用 SQL 语法速查表)
  • 十八、总结
    • [1. 多表查询](#1. 多表查询)
    • [2. 自连接](#2. 自连接)
    • [3. 子查询](#3. 子查询)
    • [4. UNION 和 UNION ALL](#4. UNION 和 UNION ALL)
    • [5. 复杂 SQL 的解题思路](#5. 复杂 SQL 的解题思路)

前言

在上一篇《MySQL 从零入门(五):MySQL 内置函数详解》中,我们学习了日期函数、字符串函数、数学函数以及其他常用函数。

结合之前的 SQL 基础知识,我们已经可以完成大量单表查询。

例如:

sql 复制代码
SELECT *
FROM student
WHERE age > 18
ORDER BY age DESC;

但是,实际业务中的数据往往分布在多张表中。

假设我们开发一个员工管理系统,包含:

  • 员工表:保存员工姓名、工资和岗位。
  • 部门表:保存部门编号、部门名称和工作地点。
  • 工资等级表:保存不同工资对应的等级。

如果要求查询:

每名员工的姓名、工资以及所属部门名称。

员工姓名和工资来自员工表,而部门名称来自部门表。

单独查询任何一张表都无法直接得到完整结果。

因此,我们需要学习 复合查询(Compound Query)。

本文按照由浅入深的顺序,先回顾基础查询,再逐步介绍多表查询、自连接、子查询及结果集合并。


一、准备实验环境

为了方便后面的所有案例直接运行,我们先准备一个简单的公司管理数据库。

1.1 创建数据库

连接 MySQL:

bash 复制代码
mycli -h 127.0.0.1 -P 3306 -u root

创建数据库:

sql 复制代码
CREATE DATABASE IF NOT EXISTS lab08
CHARACTER SET utf8
COLLATE utf8_general_ci;

选择数据库:

sql 复制代码
USE lab08;

检查:

sql 复制代码
SELECT DATABASE();

预期返回:

text 复制代码
lab08

1.2 创建员工表 EMP

sql 复制代码
CREATE TABLE EMP (
    empno INT PRIMARY KEY,
    ename VARCHAR(20) NOT NULL,
    job VARCHAR(20),
    mgr INT,
    hiredate DATE,
    sal DECIMAL(10,2),
    comm DECIMAL(10,2),
    deptno INT
) ENGINE=InnoDB;

字段解释:

字段 含义
empno 员工编号
ename 员工姓名
job 工作岗位
mgr 上级领导的员工编号
hiredate 入职日期
sal 工资
comm 奖金或佣金
deptno 所属部门编号

其中最值得注意的是:

sql 复制代码
mgr INT

它保存的是上级领导的员工编号。

例如:

text 复制代码
员工 FORD 的 mgr = 7566

表示 FORD 的直属领导是员工编号为 7566 的员工。

1.3 创建部门表 DEPT

sql 复制代码
CREATE TABLE DEPT (
    deptno INT PRIMARY KEY,
    dname VARCHAR(20) NOT NULL,
    loc VARCHAR(20) NOT NULL
) ENGINE=InnoDB;

字段解释:

字段 含义
deptno 部门编号
dname 部门名称
loc 部门所在地

这里 EMP.deptno 和 DEPT.deptno 具有业务上的关联关系。

本实验先通过查询条件建立关联,不强制添加外键约束。

1.4 创建工资等级表 SALGRADE

sql 复制代码
CREATE TABLE SALGRADE (
    grade INT PRIMARY KEY,
    losal DECIMAL(10,2),
    hisal DECIMAL(10,2)
) ENGINE=InnoDB;

字段解释:

  • grade:工资等级。
  • losal:该等级最低工资。
  • hisal:该等级最高工资。

例如:

grade losal hisal
1 700 1200
2 1201 1400
3 1401 2000
4 2001 3000
5 3001 9999

因此,工资为 2800 的员工属于第 4 等级。

1.5 插入测试数据

先插入部门:

sql 复制代码
INSERT INTO DEPT VALUES
(10, 'ACCOUNTING', 'NEW YORK'),
(20, 'RESEARCH', 'DALLAS'),
(30, 'SALES', 'CHICAGO'),
(40, 'OPERATIONS', 'BOSTON');

插入工资等级:

sql 复制代码
INSERT INTO SALGRADE VALUES
(1, 700, 1200),
(2, 1201, 1400),
(3, 1401, 2000),
(4, 2001, 3000),
(5, 3001, 9999);

插入员工:

sql 复制代码
INSERT INTO EMP VALUES
(7369, 'SMITH', 'CLERK', 7902, '1980-12-17', 800, NULL, 20),
(7499, 'ALLEN', 'SALESMAN', 7698, '1981-02-20', 1600, 300, 30),
(7521, 'WARD', 'SALESMAN', 7698, '1981-02-22', 1250, 500, 30),
(7566, 'JONES', 'MANAGER', 7839, '1981-04-02', 2975, NULL, 20),
(7654, 'MARTIN', 'SALESMAN', 7698, '1981-09-28', 1250, 1400, 30),
(7698, 'BLAKE', 'MANAGER', 7839, '1981-05-01', 2850, NULL, 30),
(7782, 'CLARK', 'MANAGER', 7839, '1981-06-09', 2450, NULL, 10),
(7788, 'SCOTT', 'ANALYST', 7566, '1982-12-09', 3000, NULL, 20),
(7839, 'KING', 'PRESIDENT', NULL, '1981-11-17', 5000, NULL, 10),
(7844, 'TURNER', 'SALESMAN', 7698, '1981-09-08', 1500, 0, 30),
(7876, 'ADAMS', 'CLERK', 7788, '1983-01-12', 1100, NULL, 20),
(7900, 'JAMES', 'CLERK', 7698, '1981-12-03', 950, NULL, 30),
(7902, 'FORD', 'ANALYST', 7566, '1981-12-03', 3000, NULL, 20),
(7934, 'MILLER', 'CLERK', 7782, '1982-01-23', 1300, NULL, 10);

最后验证:

sql 复制代码
SELECT COUNT(*) FROM EMP;
SELECT COUNT(*) FROM DEPT;
SELECT COUNT(*) FROM SALGRADE;

预期分别返回:

text 复制代码
EMP       14
DEPT       4
SALGRADE   5

至此,实验数据准备完毕。


二、8.1 基础查询回顾

在学习复杂查询之前,先复习之前掌握的 SQL 知识。

2.1 查询工资大于 500 或岗位为 MANAGER,并且姓名以 J 开头的员工

需求可以拆分为:

  1. 工资大于 500,或者岗位为 MANAGER。
  2. 姓名以大写字母 J 开头。
  3. 两个条件组必须同时满足。

SQL:

sql 复制代码
SELECT *
FROM EMP
WHERE (sal > 500 OR job = 'MANAGER')
  AND ename LIKE 'J%';

逐个解释:

语法 作用
sal > 500 工资大于 500
OR 两个条件至少有一个成立
job = 'MANAGER' 岗位是经理
AND 两个条件组都必须成立
LIKE 'J%' 姓名以 J 开头
(...) 明确逻辑运算优先级

注意:

AND 的运算优先级高于 OR。

因此,复杂条件中建议主动使用括号,避免 SQL 逻辑与预期不一致。

2.2 按部门升序、工资降序排列

sql 复制代码
SELECT *
FROM EMP
ORDER BY deptno ASC, sal DESC;

其中:

sql 复制代码
ORDER BY deptno ASC

表示首先按照部门编号升序排列。

sql 复制代码
sal DESC

表示同一个部门内部再按照工资降序排列。

多字段排序时,前面字段的优先级高于后面字段。

2.3 按年薪降序排序

sql 复制代码
SELECT
    ename,
    sal * 12 + IFNULL(comm, 0) AS annual_salary
FROM EMP
ORDER BY annual_salary DESC;

这里涉及三个知识点。

第一:计算表达式

sql 复制代码
sal * 12

表示将月工资乘以 12。

第二:IFNULL 函数

sql 复制代码
IFNULL(comm, 0)

如果 comm 为 NULL,就用 0 代替。

否则:

sql 复制代码
sal * 12 + NULL

结果会变成 NULL。

第三:列别名

sql 复制代码
AS annual_salary

为计算结果设置别名。

这里按照示例约定,将 comm 作为额外的一次性奖金加入年收入。

如果业务定义的 comm 是每月奖金,那么年收入计算公式应该相应调整,而不能直接套用这一表达式。

2.4 查询工资最高的员工

sql 复制代码
SELECT ename, job
FROM EMP
WHERE sal = (
    SELECT MAX(sal)
    FROM EMP
);

这条 SQL 出现了一个新知识:

在 WHERE 条件中嵌入另一条 SELECT 查询。

内部:

sql 复制代码
SELECT MAX(sal)
FROM EMP;

用于查询最高工资。

在当前测试数据中,最高工资为:

text 复制代码
5000.00

外部:

sql 复制代码
SELECT ename, job
FROM EMP
WHERE sal = 5000;

最终得到:

text 复制代码
KING    PRESIDENT

这种写法就是子查询,后面会详细介绍。

相比直接使用 ORDER BY sal DESC LIMIT 1,这种写法能够返回所有并列最高工资的员工。

2.5 查询工资高于平均工资的员工

sql 复制代码
SELECT ename, sal
FROM EMP
WHERE sal > (
    SELECT AVG(sal)
    FROM EMP
);

内部查询:

sql 复制代码
SELECT AVG(sal)
FROM EMP;

计算所有员工的平均工资。

外部查询根据平均工资筛选员工。

这里需要理解:

子查询可以提供外层查询需要使用的比较值。

2.6 显示每个部门的平均工资和最高工资

sql 复制代码
SELECT
    deptno,
    FORMAT(AVG(sal), 2) AS avg_salary,
    MAX(sal) AS max_salary
FROM EMP
GROUP BY deptno;

这里:

sql 复制代码
GROUP BY deptno

表示按照部门编号进行分组。

sql 复制代码
AVG(sal)

计算每个部门的平均工资。

sql 复制代码
MAX(sal)

计算每个部门的最高工资。

sql 复制代码
FORMAT(AVG(sal), 2)

将平均工资格式化为保留两位小数的字符串。

需要继续参与数值计算时,通常应直接使用 AVG 的数值结果,而不是 FORMAT 返回的字符串。

2.7 查询平均工资低于 2000 的部门

sql 复制代码
SELECT
    deptno,
    AVG(sal) AS avg_sal
FROM EMP
GROUP BY deptno
HAVING avg_sal < 2000;

这里:

sql 复制代码
HAVING

用于筛选分组后的结果。

区别在于:

  • WHERE:主要筛选分组前的记录。
  • HAVING:主要筛选分组后的结果。

2.8 查询每种岗位的员工数量和平均工资

sql 复制代码
SELECT
    job,
    COUNT(*) AS employee_count,
    FORMAT(AVG(sal), 2) AS avg_salary
FROM EMP
GROUP BY job;

这里按照岗位进行分组。

例如:

text 复制代码
MANAGER
SALESMAN
ANALYST
CLERK
PRESIDENT

每种岗位分别统计员工数量和平均工资。


三、8.2 多表查询

前面的查询主要从一张表读取数据。

但是实际业务中的相关信息往往分布在不同表中。

例如:

员工表 EMP:

empno ename sal deptno
7369 SMITH 800 20
7902 FORD 3000 20
7839 KING 5000 10

部门表 DEPT:

deptno dname
10 ACCOUNTING
20 RESEARCH
30 SALES
40 OPERATIONS

如果需要查询员工所属部门的名字,就必须关联两张表。

3.1 多表查询的基本形式

sql 复制代码
SELECT *
FROM EMP, DEPT;

注意:

这条 SQL 并没有指定两张表之间的关联条件。

因此,它会产生笛卡尔积。

3.2 什么是笛卡尔积?

假设表 A 中有两条记录:

text 复制代码
A
1
2

表 B 中有三条记录:

text 复制代码
B
X
Y
Z

笛卡尔积的结果是:

text 复制代码
A    B
1    X
1    Y
1    Z
2    X
2    Y
2    Z

总共:

2 × 3 = 6 2 \times 3 = 6 2×3=6

条记录。

在我们的实验中:

text 复制代码
EMP:14 条记录
DEPT:4 条记录

如果直接执行:

sql 复制代码
SELECT *
FROM EMP, DEPT;

将得到:

14 × 4 = 56 14 \times 4 = 56 14×4=56

条组合记录。

其中包含大量没有实际关联意义的组合。

因此,多表查询通常需要关联条件。

3.3 查询员工姓名、工资和部门名称

需求:

查询每名员工的:

  • 姓名。
  • 工资。
  • 所属部门名称。

SQL:

sql 复制代码
SELECT
    EMP.ename,
    EMP.sal,
    DEPT.dname
FROM EMP, DEPT
WHERE EMP.deptno = DEPT.deptno;

逐个解释。

第一步:FROM

sql 复制代码
FROM EMP, DEPT

表示查询涉及两张表。

第二步:WHERE

sql 复制代码
WHERE EMP.deptno = DEPT.deptno

表示只保留员工部门编号与部门表编号相同的组合。

例如:

text 复制代码
SMITH.deptno = 20

DEPT.deptno = 20
DEPT.dname = RESEARCH

所以这两条记录能够对应起来。

第三步:SELECT

sql 复制代码
EMP.ename
EMP.sal
DEPT.dname

返回员工姓名、工资和部门名称。

结果示例:

ename sal dname
SMITH 800 RESEARCH
FORD 3000 RESEARCH
KING 5000 ACCOUNTING

这里:

sql 复制代码
EMP.ename

表示 EMP 表中的 ename 字段。

格式是:

text 复制代码
表名.字段名

使用这种写法可以避免不同表中存在同名字段时产生歧义。

3.4 使用显式 INNER JOIN

上述查询还可以写成:

sql 复制代码
SELECT
    EMP.ename,
    EMP.sal,
    DEPT.dname
FROM EMP
INNER JOIN DEPT
    ON EMP.deptno = DEPT.deptno;

其中:

  • INNER JOIN:内连接。
  • ON:指定连接条件。

与前面的逗号连接写法相比,INNER JOIN 能够更清晰地区分连接关系和其他筛选条件。

在实际项目中,通常更推荐显式 JOIN 语法。

不过,对于当前只涉及匹配记录的内连接,两种写法可以实现相同的查询需求。

3.5 查询 10 号部门的名称、员工姓名和工资

sql 复制代码
SELECT
    EMP.ename,
    EMP.sal,
    DEPT.dname
FROM EMP, DEPT
WHERE EMP.deptno = DEPT.deptno
  AND DEPT.deptno = 10;

这里有两个条件:

sql 复制代码
EMP.deptno = DEPT.deptno

负责建立两张表的关联。

sql 复制代码
DEPT.deptno = 10

负责筛选 10 号部门。

需要注意:

连接条件与业务筛选条件并不是同一个概念。

虽然它们都可以写在 WHERE 中,但作用不同。

3.6 查询员工姓名、工资和工资等级

这次需要关联:

text 复制代码
EMP
SALGRADE

其中:

EMP 保存具体工资,SALGRADE 保存工资区间。

例如:

text 复制代码
FORD.sal = 3000

而:

text 复制代码
SALGRADE 第4级:2001~3000

因此,FORD 的工资等级为 4。

SQL:

sql 复制代码
SELECT
    EMP.ename,
    EMP.sal,
    SALGRADE.grade
FROM EMP, SALGRADE
WHERE EMP.sal BETWEEN SALGRADE.losal
                  AND SALGRADE.hisal;

其中:

sql 复制代码
BETWEEN ... AND ...

表示判断一个数值是否处于指定区间内,包含两端边界。

这类连接不是简单的等值连接,而是基于范围条件进行匹配。

也可以使用:

sql 复制代码
SELECT
    EMP.ename,
    EMP.sal,
    SALGRADE.grade
FROM EMP
INNER JOIN SALGRADE
    ON EMP.sal BETWEEN SALGRADE.losal
                   AND SALGRADE.hisal;

需要注意,如果工资等级区间互相重叠,同一名员工可能匹配多个工资等级。

因此,工资区间的设计应避免不合理的重叠。


四、8.3 自连接

4.1 什么是自连接?

自连接(Self Join)是指:

同一张表与自身进行连接查询。

为什么一张表还需要和自己连接?

考虑 EMP 表:

empno ename mgr
7566 JONES 7839
7902 FORD 7566
7839 KING NULL

其中:

text 复制代码
FORD 的 mgr = 7566

而:

text 复制代码
JONES 的 empno = 7566

因此可以判断:

text 复制代码
FORD 的直属领导是 JONES

注意,员工和领导的信息都保存在 EMP 表中。

4.2 查询 FORD 的上级领导:子查询方式

第一种方法:

sql 复制代码
SELECT empno, ename
FROM EMP
WHERE empno = (
    SELECT mgr
    FROM EMP
    WHERE ename = 'FORD'
);

分析。

内部查询:

sql 复制代码
SELECT mgr
FROM EMP
WHERE ename = 'FORD';

得到:

text 复制代码
7566

外部查询:

sql 复制代码
SELECT empno, ename
FROM EMP
WHERE empno = 7566;

得到:

text 复制代码
7566 JONES

因此,FORD 的直属领导是 JONES。

这里假设 ename 能够唯一确定目标员工。在真实系统中,最好使用唯一员工编号定位员工,避免同名造成子查询返回多行。

4.3 查询 FORD 的上级领导:自连接方式

sql 复制代码
SELECT
    leader.empno,
    leader.ename
FROM EMP leader, EMP worker
WHERE leader.empno = worker.mgr
  AND worker.ename = 'FORD';

这条 SQL 的重点是:

sql 复制代码
FROM EMP leader, EMP worker

虽然只使用了一张物理表 EMP,但是给它设置了两个不同的别名。

其中:

  • leader:代表领导角色。
  • worker:代表普通员工角色。

可以理解为:

text 复制代码
EMP
├── leader(领导)
└── worker(员工)

接下来:

sql 复制代码
leader.empno = worker.mgr

表示领导的员工编号等于普通员工的上级领导编号。

最后:

sql 复制代码
worker.ename = 'FORD'

筛选员工 FORD。

得到:

text 复制代码
7566 JONES

4.4 自连接的显式 JOIN 写法

sql 复制代码
SELECT
    leader.empno,
    leader.ename
FROM EMP AS worker
INNER JOIN EMP AS leader
    ON worker.mgr = leader.empno
WHERE worker.ename = 'FORD';

这里的:

sql 复制代码
AS worker

为表设置别名。

由于同一张表参与了两次查询,所以必须使用不同的角色名称区分它们。

自连接并不意味着需要复制一张真实的数据库表。

它只是查询过程中对同一张表使用了不同的逻辑角色。


五、8.4 子查询

5.1 什么是子查询?

子查询(Subquery),又称嵌套查询,是指:

在一条 SQL 语句内部嵌入另一条 SELECT 查询。

例如:

sql 复制代码
SELECT *
FROM EMP
WHERE sal > (
    SELECT AVG(sal)
    FROM EMP
);

这里存在:

  • 外层查询。
  • 内层子查询。

内层子查询计算平均工资,外层查询使用计算结果筛选员工。

需要注意,虽然很多简单子查询可以理解为"先执行内部,再执行外部",但实际执行计划由 MySQL 优化器决定,并不保证物理执行顺序一定如此。

5.2 子查询的分类

根据返回结果的形状,可以分为:

类型 返回结果
单行子查询 一行数据
多行子查询 多行数据
多列子查询 多个字段的数据
FROM 子查询 将查询结果作为临时结果集参与外层查询

这里有两个不同维度:

行数:子查询返回几条记录。

列数:子查询返回几个字段。

因此,一个子查询可以同时是"单行"和"多列"。


六、8.4.1 单行子查询

6.1 基本概念

单行子查询是指子查询返回一行记录。

例如:

sql 复制代码
SELECT deptno
FROM EMP
WHERE ename = 'SMITH';

在当前实验数据中,返回:

text 复制代码
20

因此可以将该查询放入另一个 SQL 的比较条件中。

6.2 查询与 SMITH 同一部门的员工

sql 复制代码
SELECT *
FROM EMP
WHERE deptno = (
    SELECT deptno
    FROM EMP
    WHERE ename = 'SMITH'
);

执行逻辑:

第一步:

sql 复制代码
SELECT deptno
FROM EMP
WHERE ename = 'SMITH';

返回:

text 复制代码
20

第二步:

sql 复制代码
SELECT *
FROM EMP
WHERE deptno = 20;

查询部门编号为 20 的所有员工。

这里使用:

sql 复制代码
=

是因为内部查询预期只返回一个部门编号。

6.3 单行子查询的注意事项

如果使用:

sql 复制代码
WHERE deptno = (子查询)

那么子查询必须返回一个可以用于标量比较的结果。

如果子查询返回多行数据,MySQL 通常会报错:

text 复制代码
ERROR 1242 (21000):
Subquery returns more than 1 row

这时就需要考虑:

  • 是否应该让子查询只返回一行。
  • 是否应该使用 IN。
  • 是否应该使用 ANY 或 ALL。

七、8.4.2 多行子查询

多行子查询指内部 SELECT 返回多条记录。

例如:

sql 复制代码
SELECT job
FROM EMP
WHERE deptno = 10;

10 号部门可能有多个不同的岗位。

如果希望使用这一组结果筛选其他员工,不能简单地使用单值等号比较。

因此,需要学习 IN、ALL、ANY。

7.1 IN:匹配结果集合中的任意一个值

需求:

查询与 10 号部门员工岗位相同的其他部门员工,但不包含 10 号部门本身。

sql 复制代码
SELECT
    ename,
    job,
    sal,
    deptno
FROM EMP
WHERE job IN (
    SELECT DISTINCT job
    FROM EMP
    WHERE deptno = 10
)
AND deptno <> 10;

其中:

sql 复制代码
SELECT DISTINCT job
FROM EMP
WHERE deptno = 10

查询 10 号部门出现过的岗位。

当前数据中,这些岗位包括:

text 复制代码
MANAGER
PRESIDENT
CLERK

接下来:

sql 复制代码
job IN (...)

表示员工岗位属于上述结果集合中的某一个。

最后:

sql 复制代码
deptno <> 10

表示排除 10 号部门。

这里的:

sql 复制代码
<>

表示不等于。

它与:

sql 复制代码
!=

在 MySQL 中具有相同的常见比较用途。

7.2 ALL:满足所有比较条件

需求:

查询工资比 30 号部门所有员工都高的员工。

sql 复制代码
SELECT
    ename,
    sal,
    deptno
FROM EMP
WHERE sal > ALL (
    SELECT sal
    FROM EMP
    WHERE deptno = 30
);

这里的:

sql 复制代码
> ALL (...)

表示工资必须大于子查询返回的每一个工资值。

假设 30 号部门的工资分别为:

text 复制代码
1600
1250
1250
2850
1500
950

那么其他员工如果想满足条件,就必须:

text 复制代码
工资 > 2850

因此,在这组子查询结果非空且不包含 NULL 的情况下:

sql 复制代码
sal > ALL (...)

可以理解为:

sql 复制代码
sal > (
    SELECT MAX(sal)
    ...
)

但两种写法在子查询为空或者包含 NULL 时,语义并不完全相同。

7.3 ANY:满足任意一个比较条件

需求:

查询工资比 30 号部门任意一名员工高的员工。

sql 复制代码
SELECT
    ename,
    sal,
    deptno
FROM EMP
WHERE sal > ANY (
    SELECT sal
    FROM EMP
    WHERE deptno = 30
);

这里的:

sql 复制代码
> ANY (...)

表示只要工资大于子查询结果中的至少一个值,就满足条件。

例如,30 号部门最低工资为:

text 复制代码
950

那么在当前非空、非 NULL 数据条件下,工资大于 950 的员工就能够满足比较条件。

因此:

sql 复制代码
sal > ANY (...)

可以近似理解为:

sql 复制代码
sal > (
    SELECT MIN(sal)
    ...
)

同样需要注意空结果集与 NULL 的特殊情况。

7.4 ALL 与 ANY 的区别

语法 含义
> ALL 大于所有值
> ANY 大于至少一个值
< ALL 小于所有值
< ANY 小于至少一个值

例如,子查询返回:

text 复制代码
10
20
30

那么:

text 复制代码
25 > ALL(10,20,30)

结果为 FALSE,因为 25 不大于 30。

而:

text 复制代码
25 > ANY(10,20,30)

结果为 TRUE,因为 25 大于 10 和 20。

ALL 强调全部满足,ANY 强调至少满足一个。


八、8.4.3 多列子查询

8.1 什么是多列子查询?

前面的子查询主要返回一个字段。

例如:

sql 复制代码
SELECT deptno
FROM EMP
WHERE ename = 'SMITH';

现在假设我们想同时比较两个条件:

  • 部门相同。
  • 岗位相同。

就需要使用多列子查询。

8.2 查询与 SMITH 部门和岗位都相同的员工

需求:

查询与 SMITH 的部门和岗位完全相同的所有员工,不包括 SMITH 本人。

SQL:

sql 复制代码
SELECT ename
FROM EMP
WHERE (deptno, job) = (
    SELECT deptno, job
    FROM EMP
    WHERE ename = 'SMITH'
)
AND ename <> 'SMITH';

首先执行内部查询:

sql 复制代码
SELECT deptno, job
FROM EMP
WHERE ename = 'SMITH';

返回:

text 复制代码
20    CLERK

外部查询等价于查找:

text 复制代码
部门 = 20
岗位 = CLERK

并且不是 SMITH 本人。

在当前实验数据中得到:

text 复制代码
ADAMS

8.3 为什么要加括号?

注意:

sql 复制代码
(deptno, job)

这里表示一个由两个字段组成的行值。

而:

sql 复制代码
(SELECT deptno, job ...)

返回包含两个列值的一行结果。

MySQL 支持对这些行值进行比较。

需要注意:

左右两侧参与比较的列数必须匹配。

如果左边两个字段,右边只有一个字段,通常会出现:

text 复制代码
Operand should contain 2 column(s)

等错误。

8.4 与多个 AND 条件有什么区别?

在本例中,也可以写成:

sql 复制代码
SELECT ename
FROM EMP
WHERE deptno = (
    SELECT deptno
    FROM EMP
    WHERE ename = 'SMITH'
)
AND job = (
    SELECT job
    FROM EMP
    WHERE ename = 'SMITH'
)
AND ename <> 'SMITH';

但是这样需要写两个子查询。

如果两个字段本来就来自同一条记录,那么多列子查询通常更加直观。


九、8.4.4 在 FROM 子句中使用子查询

前面我们把子查询放在 WHERE 中。

例如:

sql 复制代码
WHERE sal > (
    SELECT AVG(sal)
    FROM EMP
)

实际上,子查询也可以放在 FROM 后面。

这种写法可以把子查询的结果作为一个临时结果集,供外层 SQL 继续使用。

这种结果集通常称为 派生表(Derived Table)。

9.1 基本语法

sql 复制代码
SELECT ...
FROM (
    SELECT ...
) AS tmp;

这里:

sql 复制代码
AS tmp

表示给子查询的结果设置别名。

在 MySQL 5.7 中,FROM 后面的派生表必须具有别名。

9.2 查询工资高于本部门平均工资的员工

需求:

查询每名工资高于自己部门平均工资的员工,同时显示:

  • 员工姓名。
  • 部门编号。
  • 员工工资。
  • 所属部门平均工资。

首先计算每个部门的平均工资:

sql 复制代码
SELECT
    deptno,
    AVG(sal) AS asal
FROM EMP
GROUP BY deptno;

得到一个临时结果集:

deptno asal
10 2916.67
20 2175.00
30 1566.67

这里显示的是约值,实际 AVG 结果可能保留更多小数位。

然后把它放进 FROM 中:

sql 复制代码
SELECT
    EMP.ename,
    EMP.deptno,
    EMP.sal,
    FORMAT(tmp.asal, 2) AS dept_avg_salary
FROM EMP,
(
    SELECT
        AVG(sal) AS asal,
        deptno AS dt
    FROM EMP
    GROUP BY deptno
) AS tmp
WHERE EMP.sal > tmp.asal
  AND EMP.deptno = tmp.dt;

逐个分析。

第一步:计算部门平均工资

sql 复制代码
SELECT AVG(sal) AS asal, deptno AS dt
FROM EMP
GROUP BY deptno

生成部门和平均工资的对应关系。

第二步:给结果集设置别名

sql 复制代码
AS tmp

把子查询结果命名为 tmp。

第三步:关联员工与部门平均工资

sql 复制代码
EMP.deptno = tmp.dt

保证员工与所属部门的平均工资对应。

第四步:筛选高于平均工资的员工

sql 复制代码
EMP.sal > tmp.asal

只保留工资大于本部门平均工资的记录。

9.3 使用显式 JOIN 改写

上面的 SQL 也可以写成:

sql 复制代码
SELECT
    E.ename,
    E.deptno,
    E.sal,
    FORMAT(T.asal, 2) AS dept_avg_salary
FROM EMP AS E
INNER JOIN (
    SELECT
        deptno,
        AVG(sal) AS asal
    FROM EMP
    GROUP BY deptno
) AS T
    ON E.deptno = T.deptno
WHERE E.sal > T.asal;

这种写法更清晰地表达了:

text 复制代码
EMP 员工表
      │
      │ 按部门编号连接
      ▼
部门平均工资结果集

对于复杂 SQL,显式 JOIN 通常具有更好的可读性。


十、查询每个部门工资最高的员工

这是一个经典的 SQL 查询问题。

10.1 需求分析

假设有以下员工:

ename deptno sal
JONES 20 2975
SCOTT 20 3000
FORD 20 3000

我们希望查询部门 20 工资最高的员工。

此时最高工资为:

text 复制代码
3000

但是有两名员工:

text 复制代码
SCOTT
FORD

所以不能简单地认为每个部门只有一个工资最高的人。

10.2 第一步:查询每个部门最高工资

sql 复制代码
SELECT
    deptno,
    MAX(sal) AS ms
FROM EMP
GROUP BY deptno;

结果集类似:

deptno ms
10 5000
20 3000
30 2850

10.3 第二步:关联原始员工表

sql 复制代码
SELECT
    EMP.ename,
    EMP.sal,
    EMP.deptno,
    tmp.ms AS max_salary
FROM EMP,
(
    SELECT
        MAX(sal) AS ms,
        deptno
    FROM EMP
    GROUP BY deptno
) AS tmp
WHERE EMP.deptno = tmp.deptno
  AND EMP.sal = tmp.ms;

这里有两个关联条件:

sql 复制代码
EMP.deptno = tmp.deptno

表示部门相同。

sql 复制代码
EMP.sal = tmp.ms

表示员工工资等于该部门的最高工资。

因此,最终会返回所有工资达到部门最高值的员工。

对于 20 号部门:

text 复制代码
SCOTT
FORD

都会被保留下来。

这类问题可以概括为:

先使用 GROUP BY 求出各组最值,再将聚合结果与原始表关联,找回完整记录。


十一、查询每个部门的信息与员工数量

现在希望显示:

  • 部门编号。
  • 部门名称。
  • 部门位置。
  • 部门员工数量。

11.1 方法一:直接多表查询

sql 复制代码
SELECT
    DEPT.deptno,
    DEPT.dname,
    DEPT.loc,
    COUNT(*) AS employee_count
FROM EMP, DEPT
WHERE EMP.deptno = DEPT.deptno
GROUP BY DEPT.deptno, DEPT.dname, DEPT.loc;

这条 SQL 先关联 EMP 和 DEPT,再按照部门信息分组统计。

其中:

sql 复制代码
COUNT(*)

表示统计每个分组中的记录数量。

需要注意:

这种内连接只会保留存在匹配员工的部门。

因此,如果某部门没有员工,它不会出现在结果中。

例如当前测试数据中的部门 40,没有员工,因此该部门不会出现在这条查询的结果中。

11.2 方法二:先统计,再关联

首先:

sql 复制代码
SELECT
    deptno,
    COUNT(*) AS mycnt
FROM EMP
GROUP BY deptno;

计算每个部门的员工数量。

然后:

sql 复制代码
SELECT
    DEPT.deptno,
    DEPT.dname,
    tmp.mycnt,
    DEPT.loc
FROM DEPT,
(
    SELECT
        COUNT(*) AS mycnt,
        deptno
    FROM EMP
    GROUP BY deptno
) AS tmp
WHERE DEPT.deptno = tmp.deptno;

这种方式将复杂问题拆分为两个步骤:

text 复制代码
统计每个部门人数
       ↓
形成临时结果集
       ↓
关联部门表
       ↓
显示部门信息和人数

11.3 补充:如何保留没有员工的部门?

如果希望部门 40 也显示出来,并且人数为 0,可以使用 LEFT JOIN。

sql 复制代码
SELECT
    D.deptno,
    D.dname,
    D.loc,
    COUNT(E.empno) AS employee_count
FROM DEPT AS D
LEFT JOIN EMP AS E
    ON D.deptno = E.deptno
GROUP BY D.deptno, D.dname, D.loc;

LEFT JOIN 的含义是:

保留左表中的全部记录,即使右表没有匹配结果。

这里:

sql 复制代码
COUNT(E.empno)

只统计实际匹配到的非 NULL 员工编号。

因此,没有员工的部门可以得到 0,而不会被错误地统计为 1。

这个查询与前面的 INNER JOIN 不同,它能显示所有部门。


十二、8.4.5 合并查询:UNION 与 UNION ALL

前面的多表查询主要是把不同表的数据按照关联条件组合起来。

有时我们希望做的是另一种操作:

将多条 SELECT 查询的结果合并成一个结果集。

例如:

查询:

  • 工资大于 2500 的员工。
  • 岗位为 MANAGER 的员工。

可以通过 UNION 完成。

12.1 UNION 基本语法

sql 复制代码
SELECT ...
UNION
SELECT ...;

UNION 用于合并两个查询结果,并去除重复的结果行。

12.2 查询工资大于 2500 或岗位为 MANAGER 的员工

sql 复制代码
SELECT ename, sal, job
FROM EMP
WHERE sal > 2500

UNION

SELECT ename, sal, job
FROM EMP
WHERE job = 'MANAGER';

第一条查询:

sql 复制代码
SELECT ename, sal, job
FROM EMP
WHERE sal > 2500;

找出高工资员工。

第二条查询:

sql 复制代码
SELECT ename, sal, job
FROM EMP
WHERE job = 'MANAGER';

找出所有经理。

UNION 将两者合并。

如果某名员工同时满足两个条件,例如 JONES,他不会在最终结果中重复出现两次。

12.3 UNION 的去重规则

UNION 判断重复时,考虑的是结果行中所有对应列的值。

例如:

text 复制代码
JONES 2975 MANAGER

在两个查询结果中都出现,那么 UNION 会去重。

但如果两行只有姓名相同,工资或者岗位不同,它们不一定被判定为重复结果。

12.4 UNION ALL

如果不希望去重,可以使用:

sql 复制代码
UNION ALL

例如:

sql 复制代码
SELECT ename, sal, job
FROM EMP
WHERE sal > 2500

UNION ALL

SELECT ename, sal, job
FROM EMP
WHERE job = 'MANAGER';

这次如果 JONES 同时满足两个条件,就会出现两次。

12.5 UNION 与 UNION ALL 的区别

特性 UNION UNION ALL
合并结果集 支持 支持
自动去重 是 否
保留重复行 否 是
额外去重开销 通常需要 通常不需要

一般来说,如果业务明确允许重复行,UNION ALL 往往更合适,因为它不需要进行去重处理。

12.6 UNION 的使用条件

UNION 对参与合并的 SELECT 有一定要求。

例如:

sql 复制代码
SELECT ename, sal
FROM EMP

UNION

SELECT dname, loc
FROM DEPT;

两个 SELECT 都返回两列,因此在列数方面满足要求。

但是:

sql 复制代码
SELECT ename, sal
FROM EMP

UNION

SELECT dname
FROM DEPT;

第一条返回两列,第二条只返回一列。

因此会报错。

需要记住:

UNION 两侧查询返回的列数必须一致,对应列的数据类型也需要兼容。

另外,UNION 并不保证最终结果顺序。

如果需要排序,应为整个合并结果指定 ORDER BY。


十三、多表查询、自连接和子查询的区别

学习到这里,我们已经接触了三种常见查询方式。

13.1 多表查询

sql 复制代码
SELECT EMP.ename, DEPT.dname
FROM EMP, DEPT
WHERE EMP.deptno = DEPT.deptno;

主要用于:

从不同表中获取相关数据。

例如员工与部门。

13.2 自连接

sql 复制代码
SELECT leader.ename
FROM EMP leader, EMP worker
WHERE leader.empno = worker.mgr
  AND worker.ename = 'FORD';

主要用于:

同一张表中的记录存在关联关系。

例如员工与领导。

13.3 子查询

sql 复制代码
SELECT ename
FROM EMP
WHERE sal > (
    SELECT AVG(sal)
    FROM EMP
);

主要用于:

使用另一条查询的结果作为当前查询的输入。

例如工资与平均工资比较。

13.4 三者对比

查询方式 核心思想 典型用途
多表查询 关联不同表 员工与部门
自连接 一张表扮演两个角色 员工与领导
子查询 将查询结果交给另一层查询使用 高于平均工资
UNION 纵向合并查询结果 合并两类员工

需要注意,这些查询方式并不是互相排斥的。

复杂业务 SQL 往往会同时使用 JOIN、子查询和聚合函数。


十四、SQL 综合实战

下面结合 EMP、DEPT 和 SALGRADE 三张表,完成几个综合查询。

14.1 查询每名员工及其直属领导姓名

sql 复制代码
SELECT
    worker.ename AS employee_name,
    leader.ename AS manager_name
FROM EMP AS worker
LEFT JOIN EMP AS leader
    ON worker.mgr = leader.empno;

这里使用 LEFT JOIN,是为了保留没有上级领导的员工。

例如:

text 复制代码
KING 的 mgr = NULL

所以结果中 KING 的领导姓名为 NULL。

14.2 查询每个部门的最高工资

sql 复制代码
SELECT
    deptno,
    MAX(sal) AS max_salary
FROM EMP
GROUP BY deptno;

这是基础分组查询。

14.3 查询每个部门工资最高的员工姓名

sql 复制代码
SELECT
    E.ename,
    E.deptno,
    E.sal
FROM EMP AS E
INNER JOIN (
    SELECT
        deptno,
        MAX(sal) AS max_sal
    FROM EMP
    GROUP BY deptno
) AS T
    ON E.deptno = T.deptno
   AND E.sal = T.max_sal;

这里先求每个部门的最高工资,再通过连接找回完整员工信息。

14.4 查询工资高于本部门平均工资的员工

sql 复制代码
SELECT
    E.ename,
    E.deptno,
    E.sal,
    T.avg_sal
FROM EMP AS E
INNER JOIN (
    SELECT
        deptno,
        AVG(sal) AS avg_sal
    FROM EMP
    GROUP BY deptno
) AS T
    ON E.deptno = T.deptno
WHERE E.sal > T.avg_sal;

这个查询综合使用:

  • GROUP BY。
  • AVG。
  • FROM 子查询。
  • INNER JOIN。
  • WHERE。

14.5 查询工资高于所有 MANAGER 的员工

sql 复制代码
SELECT
    ename,
    job,
    sal
FROM EMP
WHERE sal > ALL (
    SELECT sal
    FROM EMP
    WHERE job = 'MANAGER'
);

其中:

sql 复制代码
> ALL (...)

要求员工工资高于子查询返回的所有经理工资。

14.6 查询与 FORD 相同岗位的其他员工

sql 复制代码
SELECT
    ename,
    job,
    sal
FROM EMP
WHERE job = (
    SELECT job
    FROM EMP
    WHERE ename = 'FORD'
)
AND ename <> 'FORD';

这里先查询 FORD 的岗位,再筛选相同岗位的其他员工。


十五、常见问题总结

15.1 为什么多表查询会返回大量重复组合?

例如:

sql 复制代码
SELECT *
FROM EMP, DEPT;

没有指定连接条件,因此得到两张表的笛卡尔积。

应根据业务关系增加连接条件:

sql 复制代码
WHERE EMP.deptno = DEPT.deptno

或者使用显式 JOIN。

15.2 为什么多表查询提示字段名不明确?

例如:

sql 复制代码
SELECT deptno
FROM EMP, DEPT;

由于两张表中都有 deptno,MySQL 无法确定你要查询哪个字段。

应写成:

sql 复制代码
SELECT EMP.deptno
FROM EMP, DEPT
WHERE EMP.deptno = DEPT.deptno;

也可以使用表别名来减少重复书写。

15.3 为什么子查询返回多行时报错?

如果使用:

sql 复制代码
WHERE deptno = (子查询)

但内部子查询返回多个部门编号,就会产生单值比较与多行结果不匹配的问题。

此时应根据需求考虑:

sql 复制代码
IN
ANY
ALL

而不是盲目添加 LIMIT 1。

15.4 为什么 FROM 子查询必须有别名?

例如:

sql 复制代码
SELECT *
FROM (
    SELECT deptno, AVG(sal)
    FROM EMP
    GROUP BY deptno
);

在 MySQL 5.7 中,这种派生表缺少别名,会报错。

应修改为:

sql 复制代码
SELECT *
FROM (
    SELECT
        deptno,
        AVG(sal) AS avg_sal
    FROM EMP
    GROUP BY deptno
) AS tmp;

15.5 ALL 和 ANY 有什么区别?

  • ALL:与集合中的所有值比较。
  • ANY:与集合中的至少一个值比较。

例如:

text 复制代码
集合:10、20、30
text 复制代码
25 > ALL → FALSE
25 > ANY → TRUE

15.6 为什么查询每个部门的最高工资不能直接返回员工姓名?

例如:

sql 复制代码
SELECT deptno, ename, MAX(sal)
FROM EMP
GROUP BY deptno;

这条 SQL 存在问题。

因为一个部门中可能有多名员工,而单纯按照部门分组后,MySQL 无法合理确定应该返回哪个非聚合员工姓名。

在 MySQL 5.7 默认启用 ONLY_FULL_GROUP_BY 的配置下,这类查询通常会被拒绝。

正确做法是:

  1. 先使用 GROUP BY 求部门最高工资。
  2. 再将结果与 EMP 关联。
  3. 找回对应的员工记录。

这样才能正确处理并列第一的情况。

15.7 为什么 UNION 合并后行数变少?

因为:

sql 复制代码
UNION

默认会去掉重复结果行。

如果需要保留重复记录,应使用:

sql 复制代码
UNION ALL

15.8 为什么 INNER JOIN 没有显示全部部门?

因为 INNER JOIN 只返回存在匹配关系的记录。

例如,40 号部门没有员工,那么直接连接 EMP 和 DEPT 后,该部门不会出现在结果中。

如果需要保留没有员工的部门,可以使用 LEFT JOIN,并正确处理 COUNT 的统计对象。


十六、实战 SQL 练习

掌握以上内容后,可以继续完成以下几类经典 SQL 题目。

16.1 查询员工入职时的薪水

题目背景:

某些数据库使用历史薪水表保存员工每次薪水变化的记录。

如果需要查找员工入职时的薪水,就应该关注薪水记录的时间信息。

例如,常见的历史薪水表可能包含:

text 复制代码
emp_no
salary
from_date
to_date

需要结合具体题目判断"入职时的薪水"是指最早一条薪水记录,还是需要与入职日期进行关联。

注意,本文的 EMP 示例只有当前的 sal 字段,没有保存完整历史薪水记录,因此不能直接完成这一历史查询。

16.2 为数据库中的每张表生成 COUNT SQL

需求:

针对数据库中的每张表,生成类似:

sql 复制代码
SELECT COUNT(*) FROM EMP;

这样的 SQL 字符串。

这个问题涉及元数据查询。

MySQL 提供 INFORMATION_SCHEMA,可以获取当前数据库中的表信息。

例如:

sql 复制代码
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'lab08';

进一步利用 CONCAT,可以生成查询语句:

sql 复制代码
SELECT CONCAT(
    'SELECT COUNT(*) FROM `',
    REPLACE(table_name, '`', '``'),
    '`;'
) AS sql_text
FROM information_schema.tables
WHERE table_schema = 'lab08'
  AND table_type = 'BASE TABLE';

这里生成的是 SQL 字符串,并不会自动执行这些 COUNT 查询。

16.3 查询不是经理的员工

如果只是排除岗位名称为 MANAGER 的员工,可以执行:

sql 复制代码
SELECT empno, ename, job
FROM EMP
WHERE job <> 'MANAGER';

但是,如果题目的意思是"没有被其他员工当作上级领导的员工",那么查询逻辑不同。

可以使用:

sql 复制代码
SELECT E.empno, E.ename
FROM EMP AS E
WHERE NOT EXISTS (
    SELECT 1
    FROM EMP AS W
    WHERE W.mgr = E.empno
);

这里的 NOT EXISTS 表示:

不存在任何员工将当前员工的编号作为 mgr。

这说明在 SQL 题目中,业务描述的准确理解与语法本身同样重要。

16.4 查询员工当前的经理

对于保存历史经理关系的数据库,需要先根据时间条件筛选当前有效的记录。

例如,某些 SQL 题目使用:

sql 复制代码
to_date = '9999-01-01'

表示一条关系当前仍然有效。

如果题目要求排除自己管理自己的情况,还需要增加:

sql 复制代码
emp_no <> manager_no

等条件。

这里的具体 SQL 应根据题目提供的表结构编写,不能直接套用当前 EMP 表中的 mgr 字段。


十七、常用 SQL 语法速查表

语法 作用
FROM A, B 使用多张表,未加关联条件时产生笛卡尔积
INNER JOIN 内连接
LEFT JOIN 左外连接
ON 指定 JOIN 连接条件
表名.字段名 指定字段所属的表
AS 给字段或表设置别名
WHERE 筛选记录
IN 判断是否属于结果集合
ALL 要求比较条件对全部值成立
ANY 要求比较条件对至少一个值成立
EXISTS 判断子查询是否存在记录
NOT EXISTS 判断子查询是否没有记录
GROUP BY 分组
HAVING 分组结果筛选
MAX 获取最大值
AVG 计算平均值
UNION 合并结果并去重
UNION ALL 合并结果并保留重复行

十八、总结

本篇围绕 MySQL 的复合查询展开,主要学习了以下内容。

1. 多表查询

通过不同表之间的关联关系,获取完整的业务信息。

例如:

sql 复制代码
SELECT E.ename, D.dname
FROM EMP AS E
INNER JOIN DEPT AS D
    ON E.deptno = D.deptno;

其中最重要的是理解:

多表查询需要正确的关联条件,否则容易产生无意义的笛卡尔积。

2. 自连接

通过给同一张表设置不同别名,让它扮演不同角色。

例如:

sql 复制代码
SELECT leader.ename
FROM EMP AS worker
INNER JOIN EMP AS leader
    ON worker.mgr = leader.empno;

这种方式适合处理员工与领导、分类与父分类等具有层级关系的数据。

3. 子查询

子查询可以出现在 WHERE、FROM 等位置。

其中:

  • 单行子查询通常配合普通比较运算符。
  • 多行子查询可以配合 IN、ALL、ANY。
  • 多列子查询可以同时比较多个字段。
  • FROM 子查询可以形成临时结果集。

例如:

sql 复制代码
SELECT ename, sal
FROM EMP
WHERE sal > (
    SELECT AVG(sal)
    FROM EMP
);

这里利用子查询计算平均工资,再筛选员工。

4. UNION 和 UNION ALL

UNION 用于合并多个查询结果,并且去重。

UNION ALL 用于合并结果,但不去重。

因此:

sql 复制代码
SELECT ...
UNION
SELECT ...;

和:

sql 复制代码
SELECT ...
UNION ALL
SELECT ...;

虽然形式相似,但结果和执行开销可能不同。

5. 复杂 SQL 的解题思路

通过本篇的学习,我们可以总结一个实用的方法:

面对复杂 SQL,不要急着一次写完,而应该先将业务需求拆分为多个简单查询,再逐步组合。

例如:

查询工资高于本部门平均工资的员工。

可以拆解为:

text 复制代码
第一步:统计每个部门的平均工资
         ↓
第二步:将平均工资结果作为临时表
         ↓
第三步:通过部门编号关联员工表
         ↓
第四步:筛选工资高于部门平均值的员工

这种思路对于理解复杂 SQL、提高 SQL 题目正确率以及后续进行查询优化都十分重要。

在实际 C/C++ 后端开发中,用户信息、订单信息、商品信息和部门信息等往往分布在不同表中,因此多表查询和子查询属于必须掌握的基础能力。

相关推荐
少司府17 分钟前
嵌入式入门篇(二):初识STM32F103ZET6
linux·python·stm32·单片机·嵌入式硬件·mcu·嵌入式软件
无足鸟ICT1 小时前
【RHCA+】IO调度算法
linux·服务器
wacsii1 小时前
【Linux进程(四)】命令行参数与环境变量
linux·服务器
无奈笑天下1 小时前
银河麒麟上lsblk命令教程详解
linux·运维·服务器·数据库
xiaoye-duck3 小时前
《C++ 项目:手写大模型接入 SDK》会话模块整合:内存 SessionManager 与 DataManager 持久化联动
开发语言·数据库·c++·语言模型
科研邪修法3 小时前
培训PDF保护配置:把需求转换成可验收权限
linux·服务器·网络
AllData公司负责人3 小时前
AI大模型数据开发底座系列说明书:AllData集成DB-GPT开源项目建设 AI 多模态分析底座,涵盖模态解析/智能大屏/MCP 扩展!
大数据·数据库·人工智能·gpt·数据采集·数据接入·模态解析
Felven3 小时前
中科驭数100G网卡RDMA性能测试
linux·rdma·中科驭数·100g网卡
隔窗听雨眠3 小时前
MySQL进程卡死的定位思路与恢复实践
mysql·故障排查·数据库运维