文章目录
- MySQL复合查询入门指南:从单表"单打独斗"到多表"组队开黑"
-
- 前置:本次用到的三张基础表
-
- [员工表 EMP(14名员工)](#员工表 EMP(14名员工))
- [部门表 DEPT(4个部门)](#部门表 DEPT(4个部门))
- [工资等级表 SALGRADE(5个等级)](#工资等级表 SALGRADE(5个等级))
- 一、温故知新:先掏出你的单表查询家底
-
- [1. 条件拼接:and/or 组合拳](#1. 条件拼接:and/or 组合拳)
- [2. 排序:order by 的升降之道](#2. 排序:order by 的升降之道)
- [3. 分组统计:group by + having](#3. 分组统计:group by + having)
- 二、多表查询:把几张表"焊"在一起查
-
- [1. 先讲个"恐怖故事":笛卡尔积](#1. 先讲个“恐怖故事”:笛卡尔积)
- [2. 正确打开方式:等值连接](#2. 正确打开方式:等值连接)
- [3. 更多场景练习](#3. 更多场景练习)
- 三、自连接:自己和自己玩"找领导"
- 四、子查询:SQL界的"套娃"艺术
-
- [1. 单行子查询:只返回一个值](#1. 单行子查询:只返回一个值)
- [2. 多行子查询:返回一堆值](#2. 多行子查询:返回一堆值)
-
- [① in:在集合里就行](#① in:在集合里就行)
- [② all:比所有都...](#② all:比所有都…)
- [③ any:比任意一个都...](#③ any:比任意一个都…)
- [3. 多列子查询:一次返回多列](#3. 多列子查询:一次返回多列)
- [4. from子句里的子查询:把查询结果当表用](#4. from子句里的子查询:把查询结果当表用)
- 五、合并查询:把多个结果"拼"起来
-
- [1. union:去重合并](#1. union:去重合并)
- [2. union all:不去重合并](#2. union all:不去重合并)
- 六、新手避坑总结
MySQL复合查询入门指南:从单表"单打独斗"到多表"组队开黑"
哈喽各位SQL新手村的玩家!当你敲出 select * from emp 能顺利出结果的时候,是不是觉得自己SQL已经入门了?
别急着高兴,真实业务里的数据从来不会乖乖待在一张表里。员工信息在EMP表,部门信息在DEPT表,工资等级在SALGRADE表......就像你查一个员工的信息,总不能只知道他叫啥,还得知道他哪个部门、工资几级、领导是谁吧?
这就是咱们今天的主角------MySQL复合查询。说白了就是把单表查询的招式组合起来,玩出多表联动、嵌套套娃、结果合并的花活。咱们就用经典的公司员工表举例,全程人话+案例+真实运行输出,保证看完就能上手。
前置:本次用到的三张基础表
先把测试数据摆出来,所有例子的结果都从这几张表里来,方便大家对照。
员工表 EMP(14名员工)
| empno | ename | job | mgr | hiredate | sal | comm | deptno |
|---|---|---|---|---|---|---|---|
| 7369 | SMITH | CLERK | 7902 | 1980-12-17 | 800.00 | NULL | 20 |
| 7499 | ALLEN | SALESMAN | 7698 | 1981-02-20 | 1600.00 | 300.00 | 30 |
| 7521 | WARD | SALESMAN | 7698 | 1981-02-22 | 1250.00 | 500.00 | 30 |
| 7566 | JONES | MANAGER | 7839 | 1981-04-02 | 2975.00 | NULL | 20 |
| 7654 | MARTIN | SALESMAN | 7698 | 1981-09-28 | 1250.00 | 1400.00 | 30 |
| 7698 | BLAKE | MANAGER | 7839 | 1981-05-01 | 2850.00 | NULL | 30 |
| 7782 | CLARK | MANAGER | 7839 | 1981-06-09 | 2450.00 | NULL | 10 |
| 7788 | SCOTT | ANALYST | 7566 | 1987-04-19 | 3000.00 | NULL | 20 |
| 7839 | KING | PRESIDENT | NULL | 1981-11-17 | 5000.00 | NULL | 10 |
| 7844 | TURNER | SALESMAN | 7698 | 1981-09-08 | 1500.00 | 0.00 | 30 |
| 7876 | ADAMS | CLERK | 7788 | 1987-05-23 | 1100.00 | NULL | 20 |
| 7900 | JAMES | CLERK | 7698 | 1981-12-03 | 950.00 | NULL | 30 |
| 7902 | FORD | ANALYST | 7566 | 1981-12-03 | 3000.00 | NULL | 20 |
| 7934 | MILLER | CLERK | 7782 | 1982-01-23 | 1300.00 | NULL | 10 |
部门表 DEPT(4个部门)
| deptno | dname | loc |
|---|---|---|
| 10 | ACCOUNTING | NEW YORK |
| 20 | RESEARCH | DALLAS |
| 30 | SALES | CHICAGO |
| 40 | OPERATIONS | BOSTON |
工资等级表 SALGRADE(5个等级)
| grade | losal | hisal |
|---|---|---|
| 1 | 700 | 1200 |
| 2 | 1201 | 1400 |
| 3 | 1401 | 2000 |
| 4 | 2001 | 3000 |
| 5 | 3001 | 9999 |
一、温故知新:先掏出你的单表查询家底
先快速过一遍单表查询的核心操作,这是复合查询的基本功。就像打怪前先检查装备:
1. 条件拼接:and/or 组合拳
需求:找「工资高于500 或者 岗位是经理」,并且姓名首字母是大写J的员工
sql
select * from EMP
where (sal>500 or job='MANAGER')
and ename like 'J%';
运行输出:
| empno | ename | job | mgr | hiredate | sal | comm | deptno |
|---|---|---|---|---|---|---|---|
| 7566 | JONES | MANAGER | 7839 | 1981-04-02 | 2975.00 | NULL | 20 |
| 7900 | JAMES | CLERK | 7698 | 1981-12-03 | 950.00 | NULL | 30 |
💡 小提醒:and 的优先级比 or 高,不确定优先级就加括号,准没错。
2. 排序:order by 的升降之道
需求:按部门号升序,同部门内按工资降序排序
sql
select empno, ename, deptno, sal
from EMP
order by deptno, sal desc;
运行输出:
| empno | ename | deptno | sal |
|---|---|---|---|
| 7839 | KING | 10 | 5000.00 |
| 7782 | CLARK | 10 | 2450.00 |
| 7934 | MILLER | 10 | 1300.00 |
| 7788 | SCOTT | 20 | 3000.00 |
| 7902 | FORD | 20 | 3000.00 |
| 7566 | JONES | 20 | 2975.00 |
| 7876 | ADAMS | 20 | 1100.00 |
| 7369 | SMITH | 20 | 800.00 |
| 7698 | BLAKE | 30 | 2850.00 |
| 7499 | ALLEN | 30 | 1600.00 |
| 7844 | TURNER | 30 | 1500.00 |
| 7521 | WARD | 30 | 1250.00 |
| 7654 | MARTIN | 30 | 1250.00 |
| 7900 | JAMES | 30 | 950.00 |
还可以给列起别名再排序,比如算年薪再降序:
sql
select ename, sal*12 + ifnull(comm,0) as '年薪'
from EMP
order by 年薪 desc;
运行输出:
| ename | 年薪 |
|---|---|
| KING | 60000 |
| SCOTT | 36000 |
| FORD | 36000 |
| JONES | 35700 |
| BLAKE | 34200 |
| CLARK | 29400 |
| MARTIN | 29000 |
| ALLEN | 22800 |
| TURNER | 18000 |
| MILLER | 15600 |
| WARD | 15000 |
| ADAMS | 13200 |
| JAMES | 11400 |
| SMITH | 9600 |
3. 分组统计:group by + having
分组是统计利器,比如每个部门的平均工资和最高工资:
sql
select deptno,
format(avg(sal), 2) as 平均工资,
max(sal) as 最高工资
from EMP
group by deptno;
运行输出:
| deptno | 平均工资 | 最高工资 |
|---|---|---|
| 10 | 2916.67 | 5000.00 |
| 20 | 2175.00 | 3000.00 |
| 30 | 1566.67 | 2850.00 |
⚠️ 新手必踩坑:where 是分组前 过滤行,having 是分组后 过滤结果。
比如找平均工资低于2000的部门,只能用having:
sql
select deptno, avg(sal) as avg_sal
from EMP
group by deptno
having avg_sal < 2000;
运行输出:
| deptno | avg_sal |
|---|---|
| 30 | 1566.6667 |
再拓展一个:显示每种岗位的雇员总数、平均工资
sql
select job,
count(*) as 人数,
format(avg(sal),2) as 平均工资
from EMP
group by job;
运行输出:
| job | 人数 | 平均工资 |
|---|---|---|
| CLERK | 4 | 1037.50 |
| SALESMAN | 4 | 1400.00 |
| MANAGER | 3 | 2758.33 |
| ANALYST | 2 | 3000.00 |
| PRESIDENT | 1 | 5000.00 |
二、多表查询:把几张表"焊"在一起查
单表再强也架不住数据分散。真实开发里,80%的场景都要查2张及以上的表,这就是多表查询。
1. 先讲个"恐怖故事":笛卡尔积
先看个错误示范:如果直接查两张表,不加任何条件:
sql
select * from EMP, DEPT;
运行结果 :14个员工 × 4个部门 = 56条记录 ,每个员工都会和所有部门匹配一遍,这就是笛卡尔积。
这就好比公司14个员工,4个部门,让每个员工都去每个部门"报到"一次,纯纯无效数据,数据量大的时候直接把库跑崩。
2. 正确打开方式:等值连接
怎么避免笛卡尔积?加连接条件 !
员工和部门靠 deptno(部门号)关联,所以我们只要「员工表的部门号 = 部门表的部门号」的记录:
sql
select e.ename, e.sal, d.dname
from EMP e, DEPT d
where e.deptno = d.deptno;
运行输出(共14条):
| ename | sal | dname |
|---|---|---|
| SMITH | 800.00 | RESEARCH |
| ALLEN | 1600.00 | SALES |
| WARD | 1250.00 | SALES |
| JONES | 2975.00 | RESEARCH |
| MARTIN | 1250.00 | SALES |
| BLAKE | 2850.00 | SALES |
| CLARK | 2450.00 | ACCOUNTING |
| SCOTT | 3000.00 | RESEARCH |
| KING | 5000.00 | ACCOUNTING |
| TURNER | 1500.00 | SALES |
| ADAMS | 1100.00 | RESEARCH |
| JAMES | 950.00 | SALES |
| FORD | 3000.00 | RESEARCH |
| MILLER | 1300.00 | ACCOUNTING |
📌 开发小技巧:给表起别名(e代表EMP,d代表DEPT),代码更简洁,也避免字段重名报错。
💡 补充:更规范的 JOIN 写法
上面逗号分隔的是"隐式内连接",现在业界更推荐用标准的 join ... on 写法,语义更清晰,也方便后续加左连接、右连接:
sql
select e.ename, e.sal, d.dname
from EMP e
inner join DEPT d
on e.deptno = d.deptno;
inner join 也可以简写成 join,效果完全一样。
3. 更多场景练习
场景1:查10号部门的部门名、员工名和工资
sql
select e.ename, e.sal, d.dname
from EMP e, DEPT d
where e.deptno = d.deptno
and d.deptno = 10;
运行输出:
| ename | sal | dname |
|---|---|---|
| CLARK | 2450.00 | ACCOUNTING |
| KING | 5000.00 | ACCOUNTING |
| MILLER | 1300.00 | ACCOUNTING |
场景2:查员工的工资等级
工资等级表SALGRADE里存了每个等级的薪资区间,用 between and 就能匹配每个员工的工资级别:
sql
select e.ename, e.sal, s.grade
from EMP e, SALGRADE s
where e.sal between s.losal and s.hisal;
运行输出:
| ename | sal | grade |
|---|---|---|
| SMITH | 800.00 | 1 |
| ADAMS | 1100.00 | 1 |
| JAMES | 950.00 | 1 |
| WARD | 1250.00 | 2 |
| MARTIN | 1250.00 | 2 |
| MILLER | 1300.00 | 2 |
| ALLEN | 1600.00 | 3 |
| TURNER | 1500.00 | 3 |
| JONES | 2975.00 | 4 |
| BLAKE | 2850.00 | 4 |
| CLARK | 2450.00 | 4 |
| SCOTT | 3000.00 | 4 |
| FORD | 3000.00 | 4 |
| KING | 5000.00 | 5 |
三、自连接:自己和自己玩"找领导"
有个很经典的场景:员工表里面存了每个人的领导编号(mgr),但领导本身也是员工,也在这张表里。怎么查一个员工的领导是谁?
这就要用到自连接------一张表当成两张用,自己连自己。
例子:找员工FORD的上级领导
写法1:子查询(笨办法)
先查FORD的领导编号,再用编号查姓名:
sql
select empno, ename
from emp
where empno = (select mgr from emp where ename='FORD');
运行输出:
| empno | ename |
|---|---|
| 7566 | JONES |
写法2:自连接(优雅写法)
给表起两个别名,worker 当员工表,leader 当领导表:
sql
select leader.empno, leader.ename
from emp leader, emp worker
where leader.empno = worker.mgr
and worker.ename = 'FORD';
运行输出:
| empno | ename |
|---|---|
| 7566 | JONES |
原理很简单:把一张表拆成"员工"和"领导"两个角色,通过「员工的领导号 = 领导的员工号」关联,就把上下级关系查出来了。
🎯 适用场景:树形结构、上下级关系、同表内的关联数据,都用自连接搞定。
四、子查询:SQL界的"套娃"艺术
子查询,就是select里面还套着select,也叫嵌套查询。就像拆快递,大盒子里还有小盒子。
根据返回结果不同,分好几种类型:
1. 单行子查询:只返回一个值
子查询结果只有一行一列,可以用 = > < 这些比较运算符。
例子1:查询和SMITH同一个部门的所有员工
sql
select empno, ename, deptno, sal
from EMP
where deptno = (select deptno from EMP where ename='SMITH');
jieshi
运行输出:
| empno | ename | deptno | sal |
|---|---|---|---|
| 7369 | SMITH | 20 | 800.00 |
| 7566 | JONES | 20 | 2975.00 |
| 7788 | SCOTT | 20 | 3000.00 |
| 7876 | ADAMS | 20 | 1100.00 |
| 7902 | FORD | 20 | 3000.00 |
例子2:找工资最高的员工姓名和岗位
sql
select ename, job
from EMP
where sal = (select max(sal) from EMP);
解释:这条SQL是单行子查询,会先在员工表中查出SMITH的部门号20,再用这个部门号作为过滤条件,查询员工表里所有部门号为20的员工;注意等号要求子查询只能返回单个值,若子查询返回多条数据就不能用等号,需要改用in,该语句等价于自连接查询,作用就是找出和SMITH同一个部门的全部员工。
运行输出:
| ename | job |
|---|---|
| KING | PRESIDENT |
⚠️ 注意:单行子查询必须保证只返回一行,不然SQL直接报错。
2. 多行子查询:返回一堆值
子查询返回多行一列,这时候就不能用 = 了,要用 in、all、any。
① 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;
运行输出:
| ename | job | sal | deptno |
|---|---|---|---|
| JONES | MANAGER | 2975.00 | 20 |
| BLAKE | MANAGER | 2850.00 | 30 |
| SMITH | CLERK | 800.00 | 20 |
| ADAMS | CLERK | 1100.00 | 20 |
| JAMES | CLERK | 950.00 | 30 |
in 就是"只要岗位在子查询的结果里,就算符合条件"。
② all:比所有都...
需求 :工资比30部门所有员工都高的人(碾压全场)
sql
select ename, sal, deptno
from EMP
where sal > all(select sal from EMP where deptno=30);
运行输出:
| ename | sal | deptno |
|---|---|---|
| JONES | 2975.00 | 20 |
| SCOTT | 3000.00 | 20 |
| KING | 5000.00 | 10 |
| FORD | 3000.00 | 20 |
可以理解成:比30部门最高工资(2850)还高才算数,真正的降维打击。
③ any:比任意一个都...
需求 :工资比30部门任意一个员工高的人(打赢一个就算赢)
sql
select ename, sal, deptno
from EMP
where sal > any(select sal from EMP where deptno=30);
运行输出(共13条,展示全部):
| ename | sal | deptno |
|---|---|---|
| ALLEN | 1600.00 | 30 |
| WARD | 1250.00 | 30 |
| JONES | 2975.00 | 20 |
| MARTIN | 1250.00 | 30 |
| BLAKE | 2850.00 | 30 |
| CLARK | 2450.00 | 10 |
| SCOTT | 3000.00 | 20 |
| KING | 5000.00 | 10 |
| TURNER | 1500.00 | 30 |
| ADAMS | 1100.00 | 20 |
| FORD | 3000.00 | 20 |
| MILLER | 1300.00 | 10 |
只要比30部门最低工资(950)高就算符合条件,除了工资800的SMITH,其他人都满足。
📌 记忆口诀:
- all:全部都要满足,碾压全场
- any:满足一个就行,菜鸡里拔将军
3. 多列子查询:一次返回多列
子查询还能一次返回多个列,一起做匹配。
需求:找部门和岗位都和SMITH完全一样的员工(不含SMITH本人)
sql
select ename
from EMP
where (deptno, job) = (select deptno, job from EMP where ename='SMITH')
and ename <> 'SMITH';
运行输出:
| ename |
|---|
| ADAMS |
把两个字段打包成一组去对比,比分开写两个子查询简洁多了。
4. from子句里的子查询:把查询结果当表用
子查询不止能写在where后面,还能写在from后面,当成一张临时表来用。这是进阶必学技巧。
例子1:找出工资高于自己部门平均工资的员工
思路:
- 先算出每个部门的平均工资,得到一张临时表
- 把员工表和这张临时表关联,对比工资
sql
select e.ename, e.deptno, e.sal, format(tmp.asal,2) as 部门平均工资
from EMP e,
(select avg(sal) asal, deptno dt from EMP group by deptno) tmp
where e.deptno = tmp.dt
and e.sal > tmp.asal;
运行输出:
| ename | deptno | sal | 部门平均工资 |
|---|---|---|---|
| KING | 10 | 5000.00 | 2916.67 |
| JONES | 20 | 2975.00 | 2175.00 |
| SCOTT | 20 | 3000.00 | 2175.00 |
| FORD | 20 | 3000.00 | 2175.00 |
| ALLEN | 30 | 1600.00 | 1566.67 |
| BLAKE | 30 | 2850.00 | 1566.67 |
例子2:查找每个部门工资最高的人的姓名、工资、部门、最高工资
sql
select e.ename, e.sal, e.deptno, tmp.ms as 部门最高工资
from EMP e,
(select max(sal) ms, deptno from EMP group by deptno) tmp
where e.deptno = tmp.deptno
and e.sal = tmp.ms;
运行输出:
| ename | sal | deptno | 部门最高工资 |
|---|---|---|---|
| BLAKE | 2850.00 | 30 | 2850.00 |
| KING | 5000.00 | 10 | 5000.00 |
| SCOTT | 3000.00 | 20 | 3000.00 |
| FORD | 3000.00 | 20 | 3000.00 |
注意:20部门有SCOTT和FORD两个人都是3000的最高工资,所以会返回两条。
例子3:显示每个部门的信息(部门名、编号、地址)和人员数量
写法1:多表关联 + 分组
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.mycnt as 部门人数
from DEPT d,
(select count(*) mycnt, deptno from EMP group by deptno) tmp
where d.deptno = tmp.deptno;
两种写法运行输出一致:
| dname | deptno | loc | 部门人数 |
|---|---|---|---|
| ACCOUNTING | 10 | NEW YORK | 3 |
| RESEARCH | 20 | DALLAS | 5 |
| SALES | 30 | CHICAGO | 6 |
⚠️ 注意:这两种写法都不会显示40号部门,因为40部门没有员工。如果想让没人的部门也显示(人数为0),需要用左外连接,后面进阶篇再讲。
五、合并查询:把多个结果"拼"起来
实际开发中,有时候需要把两个select的结果上下拼在一起,这时候就用集合操作符 union 和 union all。
1. union:去重合并
union 会自动去掉结果集中的重复行。
需求:找出工资大于2500 或者 职位是MANAGER的员工
sql
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同时满足两个条件,但结果里只出现一次,因为union自动去重了。
2. union all:不去重合并
union all 就是简单粗暴地把两个结果拼在一起,不做去重,性能比union高。
sql
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各出现了两次,总共8条记录。
📌 开发经验:
- 不需要去重的场景优先用
union all,性能好很多 - 使用union/union all时,两个select的列数、列类型必须对应一致
六、新手避坑总结
- 多表查询必加连接条件,不然会产生笛卡尔积,数据量爆炸
- 分组后过滤用having,不要用where筛分组结果
- 单行子查询用=,多行用in/all/any,别搞混运算符
- union去重,union all不去重,优先用union all性能更好
- 子查询可以当临时表用,from后面套select是进阶必备技巧
复合查询看着花哨,本质还是单表查询的组合拳。把每个基础招式练熟,多套几层、多连几张表,都是一样的逻辑。