从多表到子查询详解MySQL中复合查询的核心重点
作者:时间的拾荒人
1. 前言
在实际开发中,我们经常需要从多张表中提取数据,而不仅仅是单表查询。MySQL 的复合查询(多表查询、子查询、合并查询)是面试中的高频考点,也是日常开发必备的技能。本文将基于一个经典的公司管理系统(EMP、DEPT、SALGRADE 三张表),带你从基础回顾到高级实战,彻底掌握复合查询。
2. 基本查询回顾
在进入多表查询之前,我们先快速回顾一下单表查询的常用技巧,这些是复合查询的基础。
2.1 条件与排序组合
案例:查询工资高于 500 或岗位为 MANAGER 的雇员,同时还要满足他们的姓名首字母为大写的 J。
select * from EMP where (sal > 500 or job = 'MANAGER') and ename like 'J%';
小贴士:like 'J%' 用于匹配以 ‘J’ 开头的字符串,% 是通配符,代表任意长度的字符。
案例:按照部门号升序而雇员的工资降序排序。
select * from EMP order by deptno, sal desc;
小贴士:order by 后面可以跟多个字段,先按第一个字段排序,如果相同再按第二个字段排序。desc 表示降序,asc 表示升序(默认)。建议显式写出排序方式,避免歧义。
2.2 使用别名与 NULL 处理
案例:使用年薪进行降序排序。
select ename, sal*12+ifnull(comm, 0) as '年薪' from EMP order by 年薪 desc;
小贴士:
ifnull(comm, 0):如果comm(奖金)为NULL,则用 0 代替。NULL 不参与运算,任何数据和 NULL 运算结果都为 NULL,所以一定要处理。as '年薪':给计算结果起一个别名,方便在order by中引用。
2.3 聚合与分组
案例:显示工资最高的员工的名字和工作岗位。
select ename, job from EMP where sal = (select max(sal) from EMP);
小贴士:这里用到了子查询,先查出最高工资,再查对应员工。
案例:显示工资高于平均工资的员工信息。
select ename, sal from EMP where sal > (select avg(sal) from EMP);
案例:显示每个部门的平均工资和最高工资。
select deptno, format(avg(sal), 2), max(sal) from EMP group by deptno;
小贴士:group by 用于分组,format(avg(sal), 2) 将平均工资格式化为两位小数。
案例:显示平均工资低于 2000 的部门号和它的平均工资。
select deptno, avg(sal) as avg_sal from EMP group by deptno having avg_sal < 2000;
小贴士:having 用于对分组后的数据进行过滤,而 where 用于对分组前的数据进行过滤。
案例:显示每种岗位的雇员总数,平均工资。
select job, count(*), format(avg(sal), 2) from EMP group by job;
3. 多表查询
实际开发中,数据往往分散在不同的表中。多表查询的核心思想是将多张表通过关联条件组合成一张临时表,然后进行单表查询。
3.1 笛卡尔积与等值连接
原理:当查询多张表时,如果不加任何条件,MySQL 会将两张表的每一行进行组合,形成笛卡尔积。例如,EMP 表有 14 条记录,DEPT 表有 4 条记录,笛卡尔积就有 14 * 4 = 56 条记录。这通常不是我们想要的,所以需要通过 where 条件过滤出有效数据。

案例:显示雇员名、雇员工资以及所在部门的名字。
select EMP.ename, EMP.sal, DEPT.dname from EMP, DEPT where EMP.deptno = DEPT.deptno;
小贴士:EMP.deptno = DEPT.deptno 就是连接条件,它确保了只取部门编号相同的记录,这就是等值连接。
3.2 多表查询实战
案例:显示部门号为 10 的部门名、员工名和工资。
select ename, sal, dname from EMP, DEPT where EMP.deptno = DEPT.deptno and DEPT.deptno = 10;
案例:显示各个员工的姓名、工资及工资级别。
select ename, sal, grade from EMP, SALGRADE where EMP.sal between losal and hisal;
小贴士:between ... and ... 用于范围匹配,这里将员工的工资与工资等级表进行匹配。
4. 自连接
自连接是指在同一张表内进行连接查询。核心技巧是给表起别名,将其视为两张独立的表。
案例:显示员工 FORD 的上级领导的编号和姓名(mgr 是员工领导的编号,对应 empno)。
方法一:子查询
select empno, ename from emp where emp.empno = (select mgr from emp where ename = 'FORD');
方法二:自连接(推荐)
-- 使用到表的别名 --from emp leader, emp worker,给自己的表起别名,因为要先做笛卡尔积,所以别名可以先识别 -- 给表起别名:leader 代表领导表,worker 代表员工表 select leader.empno, leader.ename from emp leader, emp worker where leader.empno = worker.mgr and worker.ename = 'FORD';
小贴士:
- 自连接的本质是将同一张表进行笛卡尔积,然后通过别名区分不同角色。
- 先找到 FORD 员工的领导工号(
worker.mgr),再通过leader.empno匹配到领导的信息。 - 对同一张表进行聚合,该表必须取别名,不能直接聚合成为一张表
5. 子查询
子查询是指嵌入在其他 SQL 语句中的 select 语句,也叫嵌套查询。子查询可以出现在 where、from、having 等子句中。
5.1 单行子查询
返回一行记录的子查询。
案例:显示 SMITH 同一部门的员工。
select * from EMP WHERE deptno = (select deptno from EMP where ename = 'smith');
5.2 多行子查询
返回多行记录的子查询,常与 in、all、any 关键字配合使用。
in 关键字:判断列值是否在某一集合中。
案例:查询和 10 号部门的工作岗位相同的雇员的名字、岗位、工资、部门号,但是不包含 10 号部门自己的。
select ename, job, sal, deptno from emp where job in (select distinct job from emp where deptno = 10) and deptno <> 10;
all 关键字:与所有值比较,返回满足所有条件的记录。
案例:显示工资比部门 30 的所有员工的工资都高的员工的姓名、工资和部门号。
select ename, sal, deptno from EMP where sal > all(select sal from EMP where deptno = 30);
any 关键字:与任意值比较,返回满足任一条件的记录。
案例:显示工资比部门 30 的任意员工的工资高的员工的姓名、工资和部门号。
select ename, sal, deptno from EMP where sal > any(select sal from EMP where deptno = 30);
5.3 多列子查询
单行子查询是指子查询只返回单列,单行数据;多行子查询是指返回单列多行数据,都是针对单列而言的,而多列子查询则是指查询返回多个列数据的子查询语句
案例:查询和 SMITH 的部门和岗位完全相同的所有雇员,不含 SMITH 本人。
select ename from EMP where (deptno, job) = (select deptno, job from EMP where ename = 'SMITH') and ename <> 'SMITH';
小贴士:(deptno, job) 是一个行构造器,用于同时比较多个列的值。目前全部的子查询,全部都在where子句中,充当判断条件!任何时刻查询出来的临时结构,本质在逻辑上都是表结构。
5.4 在from子句中使用子查询
子查询的结果可以作为一个临时表,放在 from 子句中。这是解决复杂查询的利器。
案例:显示每个高于自己部门平均工资的员工的姓名、部门、工资、平均工资。
select ename, deptno, sal, format(asal, 2) from EMP,
(select avg(sal) asal, deptno dt from EMP group by deptno) tmp
where EMP.sal > tmp.asal and EMP.deptno = tmp.dt;
小贴士:将子查询 (select avg(sal) asal, deptno dt from EMP group by deptno) 看作一张临时表 tmp,然后与 EMP 表进行连接查询。
案例:查找每个部门工资最高的人的姓名、工资、部门、最高工资。
select EMP.ename, EMP.sal, EMP.deptno, ms from EMP,
(select max(sal) ms, deptno from EMP group by deptno) tmp
where EMP.deptno = tmp.deptno and EMP.sal = tmp.ms;
案例:显示每个部门的信息(部门名,编号,地址)和人员数量。
方法一:多表查询
select DEPT.dname, DEPT.deptno, DEPT.loc, count(*) '部门人数' from EMP, DEPT where EMP.deptno = DEPT.deptno group by DEPT.deptno, DEPT.dname, DEPT.loc;
方法二:子查询(推荐)
-- 1. 对 EMP 表进行人员统计
select count(*), deptno from EMP group by deptno;
-- 2. 将上面的表看作临时表
select DEPT.deptno, dname, mycnt, loc from DEPT,
(select count(*) mycnt, deptno from EMP group by deptno) tmp
where DEPT.deptno = tmp.deptno;
核心思想:解决多表问题的本质是:想办法将多表转化成为单表。 所以 MySQL 中,所有 select 的问题全部都可以转化为单表问题。
6. 合并查询
在实际应用中,为了合并多个 select 的执行结果,可以使用集合操作符 union 和 union all。
6.1union:去重合并
该操作符用于取得两个结果集的并集,并自动去掉重复行。
案例:将工资大于 2500 或职位是 MANAGER 的人找出来。
select ename, sal, job from EMP where sal > 2500 union select ename, sal, job from EMP where job = 'MANAGER';
结果:
+-------+---------+-----------+
| ename | sal | job |
+-------+---------+-----------+
| JONES | 2975.00 | MANAGER |
| BLAKE | 2850.00 | MANAGER |
| SCOTT | 3000.00 | ANALYST |
| KING | 5000.00 | PRESIDENT |
| FORD | 3000.00 | ANALYST |
| CLARK | 2450.00 | MANAGER |
+-------+---------+-----------+
小贴士:可以看到,JONES、BLAKE、CLARK 虽然同时满足两个条件,但在结果中只出现一次,因为 union 去重了。
6.2union all:不去重合并
该操作符用于取得两个结果集的并集,但不会去掉重复行。
案例:将工资大于 2500 或职位是 MANAGER 的人找出来。
select ename, sal, job from EMP where sal > 2500 union all select ename, sal, job from EMP where job = 'MANAGER';
结果:
+-------+---------+-----------+
| ename | sal | job |
+-------+---------+-----------+
| JONES | 2975.00 | MANAGER |
| BLAKE | 2850.00 | MANAGER |
| SCOTT | 3000.00 | ANALYST |
| KING | 5000.00 | PRESIDENT |
| FORD | 3000.00 | ANALYST |
| JONES | 2975.00 | MANAGER |
| BLAKE | 2850.00 | MANAGER |
| CLARK | 2450.00 | MANAGER |
+-------+---------+-----------+
小贴士:JONES 和 BLAKE 出现了两次,因为 union all 只是简单地将结果拼接在一起。
7. 实战 OJ
这里是一些经典的 SQL 面试题,来自牛客网等平台,帮助你巩固所学知识。
题目 1:查找所有员工入职时候的薪水情况,给出 emp_no 以及 salary,并按照 emp_no 进行逆序。
-- 假设有 employees 表和 salaries 表 select e.emp_no, s.salary from employees e join salaries s on e.emp_no = s.emp_no where s.from_date = e.hire_date order by e.emp_no desc;
题目 2:针对库中的所有表生成 select count(*) from tableName 对应的 SQL 语句。
-- 使用 information_schema 获取所有表名
select concat('select count(*) from ', table_name, ';') as 'SQL语句'
from information_schema.tables
where table_schema = 'your_database_name';
题目 3:获取所有非 manager 的员工 emp_no。
-- 假设有 employees 表和 dept_manager 表 select emp_no from employees where emp_no not in (select emp_no from dept_manager);
题目 4:获取所有员工当前的 manager,如果当前的 manager 是自己的话结果不显示,当前表示 to_date='9999-01-01'。
-- 假设有 dept_emp 表和 dept_manager 表 select de.emp_no, dm.emp_no as manager_no from dept_emp de join dept_manager dm on de.dept_no = dm.dept_no where dm.to_date = '9999-01-01' and de.emp_no <> dm.emp_no;
8. 总结
本文从基本查询回顾开始,逐步深入到多表查询、自连接、子查询和合并查询,涵盖了 MySQL 复合查询的所有核心知识点。
- 多表查询:核心是笛卡尔积 + 连接条件,将多表转化为单表。
- 自连接:通过给表起别名,将一张表视为两张表。
- 子查询:可以出现在
where、from等子句中,from子句中的子查询可以作为临时表,是解决复杂问题的利器。 - 合并查询:
union去重,union all不去重,用于合并多个查询结果。
核心心法:解决多表问题的本质是:想办法将多表转化成为单表。 所以 MySQL 中,所有 select 的问题全部都可以转化为单表问题。
9. 经典面试题与解答
题目 1:什么是笛卡尔积?如何避免?
解答:笛卡尔积是指在没有连接条件的情况下,将两张表的每一行进行组合,结果集的行数是两张表行数的乘积。避免笛卡尔积的方法是在 where 子句中添加有效的连接条件(如等值连接)。
题目 2:where 和 having 的区别是什么?
解答:
where用于对分组前的数据进行过滤,不能使用聚合函数。having用于对分组后的数据进行过滤,可以使用聚合函数(如avg(),sum(),count()等)。
题目 3:union 和 union all 的区别是什么?
解答:
union:合并两个结果集,并自动去除重复行,性能稍差。union all:合并两个结果集,保留所有行(包括重复行),性能更好。
题目 4:什么是子查询?子查询可以出现在哪些子句中?
解答:子查询是嵌套在其他 SQL 语句中的 select 语句。它可以出现在 where、from、having、select 等子句中。其中,from 子句中的子查询被当作临时表使用,是解决复杂查询的常用技巧。
题目 5:请用一条 SQL 语句查询出每个部门工资最高的员工信息。
解答:
select e.* from emp e join (select deptno, max(sal) as max_sal from emp group by deptno) tmp on e.deptno = tmp.deptno and e.sal = tmp.max_sal;
以上就是从多表到子查询详解MySQL中复合查询的核心重点的详细内容,更多关于MySQL复合查询的资料请关注脚本之家其它相关文章!
