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复合查询的资料请关注代码网其它相关文章!
发表评论