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 开头的员工
需求可以拆分为:
- 工资大于 500,或者岗位为 MANAGER。
- 姓名以大写字母 J 开头。
- 两个条件组必须同时满足。
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 的配置下,这类查询通常会被拒绝。
正确做法是:
- 先使用 GROUP BY 求部门最高工资。
- 再将结果与 EMP 关联。
- 找回对应的员工记录。
这样才能正确处理并列第一的情况。
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++ 后端开发中,用户信息、订单信息、商品信息和部门信息等往往分布在不同表中,因此多表查询和子查询属于必须掌握的基础能力。