MySQL复合查询(重点)
目录
前言:学习目标与基本概念
一、单表基本查询回顾与思维推导
1、示例一:复合条件查询
需求: 查询工资高于500或岗位为MANAGER的雇员,同时还要满足他们的姓名首字母为大写的J。
第一步:理解核心需求并拆解
第二步:构建基础查询框架
第三步:应用第一个组合条件
第四步:应用第二个“并且”条件
2、示例二:多列排序
需求: 按照部门号升序而雇员的工资降序排序。
第一步:确定排序的列和顺序
第二步:使用ORDER BY子句
第三步:指定每列的排序方式
3、示例三:使用计算列排序
需求: 使用年薪进行降序排序。
第一步:理解“年薪”的计算方式
第二步:在SELECT子句中定义别名
第三步:根据别名进行排序
补充说明:
4、 示例四:子查询获取极值
需求: 显示工资最高的员工的名字和工作岗位。
第一步:分解问题
第二步:使用子查询解决内部问题
第三步:将子查询结果作为外部查询的条件
5、示例五:子查询与平均值比较
需求: 显示工资高于平均工资的员工信息。
推导过程:这个问题的思路与示例四完全一致,只是把“最大值”换成了“平均值”。
6、示例六:分组聚合查询
需求: 显示每个部门的平均工资和最高工资。
第一步:识别分组关键字
第二步:确定需要对每组数据计算的聚合函数
第三步:组合SELECT和GROUP BY子句
7、示例七:使用HAVING对分组后结果筛选
需求: 显示平均工资低于2000的部门号和它的平均工资。
第一步:先完成分组聚合
第二步:理解WHERE和HAVING的区别
第三步:应用HAVING子句进行筛选
8、示例八:按岗位分组统计
需求: 显示每种岗位的雇员总数,平均工资。
推导过程:
二、多表查询
笛卡尔积的回顾
笛卡尔积的初步过滤
1、示例一:基础等值连接(内连接)
需求: 显示雇员名、雇员工资以及所在部门的名称。
第一步:分析数据来源
第二步:确定表之间的关联条件
第三步:构建FROM和WHERE子句
第四步:处理列名歧义(使用表名前缀)
2、示例二:带筛选条件的多表查询
需求: 显示部门号为10的部门名,员工名和工资。
第一步:复用基础连接
第二步:附加额外的筛选条件
第三步:组合所有条件
3、示例三:非等值连接
需求: 显示各个员工的姓名,工资,及工资级别。
第一步:分析数据来源
第二步:确定表之间的关联条件
第三步:使用BETWEEN...AND...进行连接
三、自连接
1、需求:显示员工FORD的上级领导的编号和姓名
2、背景知识: 在emp表中,有两个关键字段
3、第一步:理解数据关系(核心难点)
4、第二步:解决方案一:使用子查询(分两步走)
第一步(内部查询):找出FORD的领导的编号。
第二步(外部查询):根据编号查找领导信息。
5、第三步:解决方案二:使用自连接(一步到位)
给表起别名(关键步骤)
建立连接条件(寻找关系)
附加筛选条件
选择要显示的列
最终的自连接SQL
6、两种方案对比的思考过程
核心要点:
四、子查询
1、单行子查询
需求:显示SMITH同一部门的员工相关信息。
第一步:分解问题
第二步:组合成子查询
2、多行子查询
示例一:使用 IN 关键字
需求:查询和10号部门的工作岗位相同的雇员的名字,岗位,工资,部门号,但是不包含10部门自己的员工。
第一步:分析内部查询
第二步:分析外部查询条件
示例二:使用 ALL 关键字
需求:显示工资比部门30的所有员工的工资都高的员工的姓名、工资和部门号。
第一步:理解“所有”的含义
第二步:使用 ALL 运算符
示例三:使用 ANY 关键字(注意!!!)
需求:显示工资比部门30的任意员工的工资高的员工的姓名、工资和部门号(包含自己部门的员工)。
第一步:理解“任意”的含义
正确的理解:(容易混淆,要注意!!!)
两种表达方式的区别:
结论总结(记住就行):
第二步:使用 ANY 运算符
3、多列子查询
例子1:单行多列子查询(子查询返回一行)
需求:查询和SMITH的部门和岗位完全相同的所有雇员,不含SMITH本人。
第一步:分析需要比较的列
第二步:使用多列比较语法
语法结构:
含义:
使用前提:
例子2:多行多列子查询(子查询返回多行)
4、在 FROM 子句中使用子查询(派生表)
示例一:显示每个高于自己部门平均工资的员工。
第一步:分解问题,先解决“部门的平均工资”
第二步:将子查询作为临时表(派生表)
示例二:显示每个部门的信息和人员数量(使用子查询方法)
第一步:先统计每个部门的人数
第二步:将部门信息表与统计结果临时表连接
5、合并查询
1. UNION
需求:将工资大于2500或职位是MANAGER的人找出来。
第一步:分别写出两个条件的查询
第二步:使用 UNION 合并
2. UNION ALL
需求:同上,但保留重复记录。
前言:学习目标与基本概念
学习目标:掌握多表查询和复杂条件查询的能力,解决实际开发中的复杂数据检索需求。
思维导引:在实际应用中,数据通常分布在多个表中,单一表查询无法满足复杂业务需求。我们需要学会:如何关联多个表进行查询、如何使用子查询解决复杂问题、如何组合多个查询结果
基本查询回顾:准备测试表(之前使用过的scott数据库里面的表)
-
下面给出三张表,分别是员工表(emp)、部门表(dept)和工资等级表(salgrade)。
-
后续所要进行的查询操作都将以这三张表作为数据源,包括基本查询和复合查询。
员工表(emp)中包含如下字段:雇员编号(empno)、雇员姓名(ename)、雇员职位(job)、雇员领导编号(mgr)、雇佣时间(hiredate)、工资月薪(sal)、奖金(comm)、部门编号(deptno)。
员工表(emp)中的内容如下:
部门表(dept)中包含如下字段:部门编号(deptno)、部门名称(dname)、部门所在地点(loc)。
部门表(dept)中的内容如下:
工资等级表(salgrade)中包含如下字段:等级(grade)、此等级最低工资(losal)、此等级最高工资(hisal)。
工资等级表(salgrade)中的内容如下:
一、单表基本查询回顾与思维推导
1、示例一:复合条件查询
需求: 查询工资高于500或岗位为MANAGER的雇员,同时还要满足他们的姓名首字母为大写的J。
第一步:理解核心需求并拆解
这个需求实际上包含了两个主要的筛选条件,它们之间是“并且”的关系:
-
第一个条件:
工资高于500
或者岗位是MANAGER
。这是一个“或”关系的组合条件。 -
第二个条件:
姓名首字母为大写的J
。
第二步:构建基础查询框架
最基础的查询是获取所有员工信息,使用 SELECT * FROM emp;
SELECT * FROM emp
第三步:应用第一个组合条件
我们先处理“或”关系的条件。为了确保逻辑正确,使用括号()
将sal > 500
和job = 'MANAGER'
括起来,表示优先计算这个“或”运算。得到的SQL是:SELECT * FROM emp WHERE (sal > 500 OR job = 'MANAGER');
SELECT * FROM emp WHERE (sal > 500 OR job = 'MANAGER')
这一步会找出所有工资高或者职位是经理的员工,如下:
第四步:应用第二个“并且”条件
现在,我们需要从第三步的结果中,筛选出姓名满足条件的。使用 AND
关键字连接姓名条件 ename LIKE 'J%'。LIKE 'J%'
表示名字以大写字母‘J’开头,后面可以是任意字符。
最终SQL为:SELECT * FROM emp WHERE (sal > 500 OR job = 'MANAGER') AND ename LIKE 'J%';
在where子句中指明筛选条件为工资高于500或岗位为MANAGER,并且通过模糊匹配指明员工姓名的首字母为大写的J,在select的column列表中指明要查询的列为姓名、工资和岗位。如下:
SELECT * FROM emp WHERE (sal > 500 OR job = 'MANAGER') AND ename LIKE 'J%';
思考过程总结: 先通过括号明确“或”逻辑的边界,再通过“并”逻辑连接另一个独立条件。
2、示例二:多列排序
需求: 按照部门号升序而雇员的工资降序排序。
第一步:确定排序的列和顺序
这里有两个排序关键字:“部门号”和“工资”。需求是“部门号升序”(从小到大),“工资降序”(从高到低)。
第二步:使用ORDER BY子句
ORDER BY
子句用于排序。在 ORDER BY
后面按顺序列出排序的列。如下:
SELECT * FROM emp ORDER BY deptno;
-- 或者SELECT * FROM emp ORDER BY deptno ASC;
第三步:指定每列的排序方式
在列名后使用关键字 ASC
(升序,可省略)或 DESC
(降序)。
所以,先写 deptno ASC
(或直接写 deptno
),再写 sal DESC
。它们用逗号分隔,表示首先按部门号排序,在同一个部门内,再按工资排序。
最终SQL为:SELECT * FROM emp ORDER BY deptno, sal DESC;
在select的column列表中指明要查询的列为姓名、部门号和工资,在order by子句中依次指明按部门号排升序和按员工工资排降序,即不同部门的员工按照部门号排升序,而同一部门的员工按员工工资排降序。如下:
SELECT * FROM emp ORDER BY deptno, sal DESC;
-- 或者SELECT * FROM emp ORDER BY deptno ASC, sal DESC;
3、示例三:使用计算列排序
需求: 使用年薪进行降序排序。
第一步:理解“年薪”的计算方式
年薪通常由(月薪 × 12)+ 奖金构成。需要注意的是,奖金(comm)可能为NULL(即空值),直接与数字运算会导致结果为NULL。因此需要使用 IFNULL(comm, 0)
函数,如果comm是NULL,就当作0来处理。
第二步:在SELECT子句中定义别名
我们需要在查询结果中显示这个计算列,并给它一个清晰的名称(别名),比如‘年薪’。这使用 AS
关键字实现:SELECT ename, sal*12+ifnull(comm,0) AS '年薪' from emp;
SELECT ename, sal*12+ifnull(comm,0) AS '年薪' FROM emp;
第三步:根据别名进行排序
在 ORDER BY
子句中,可以直接使用这个别名‘年薪’来进行降序排序,这样写更简洁易懂。
最终SQL为:SELECT ename, sal*12+ifnull(comm,0) AS '年薪' FROM emp ORDER BY 年薪 DESC;
(注意:ORDER BY
后面使用数字(如 ORDER BY 2
)表示按第二列排序也可行,但使用别名可读性更强,且不受SELECT列顺序变化的影响)。
在select的column列表中指明要查询的列为姓名和年薪,在order by子句中指明按年薪进行降序排序。如下:
SELECT ename, sal*12+ifnull(comm,0) AS '年薪' FROM emp ORDER BY 年薪 DESC;
补充说明:
-
由于NULL与任何值做计算得到的结果都是NULL,因此在计算年薪时不能直接用月薪的12倍加上每个员工的奖金,这样可能导致得到的年薪为NULL值。
-
在计算每个员工的年薪时,应该通过ifnull函数判断员工的奖金是否为NULL,如果不为NULL则ifnull函数返回员工的奖金,如果为NULL则ifnull函数返回0,避免让NULL值参与计算。
4、 示例四:子查询获取极值
需求: 显示工资最高的员工的名字和工作岗位。
第一步:分解问题
要找到“工资最高”的员工,首先需要知道最高工资是多少。这是一个独立的问题。
第二步:使用子查询解决内部问题
通过一个子查询来获取最高工资:(SELECT max(sal) FROM emp);
这个查询会返回一个单一的值(例如5000)。
SELECT max(sal) FROM emp;
第三步:将子查询结果作为外部查询的条件
现在,我们在主查询中查找,条件是工资(sal)等于子查询返回的那个最大值。主查询负责选择需要显示的列(ename, job)。
最终SQL为:SELECT ename, job FROM emp WHERE sal = (SELECT max(sal) FROM emp);
SELECT ename, job FROM emp WHERE sal = (SELECT max(sal) FROM emp);
思考过程: 将复杂问题拆解。先解决“最大值是多少”这个内部问题,再将答案作为外部查询的筛选条件。
5、示例五:子查询与平均值比较
需求: 显示工资高于平均工资的员工信息。
推导过程:这个问题的思路与示例四完全一致,只是把“最大值”换成了“平均值”。
内部查询: 计算平均工资 -> (SELECT avg(sal) FROM emp);
SELECT avg(sal) FROM emp;
外部查询: 筛选出工资大于这个平均值的员工信息。
最终SQL为:SELECT ename, sal FROM emp WHERE sal > (SELECT avg(sal) FROM emp);
解决该问题也需要进行两次查询,先对员工表进行一次查询得到平均工资,然后再根据平均工资对员工表进行一次查询,筛选出工资高于平均工资的员工信息,该问题同样可以使用子查询。如下:
SELECT ename, sal FROM emp WHERE sal > (SELECT avg(sal) FROM emp);
6、示例六:分组聚合查询
需求: 显示每个部门的平均工资和最高工资。
第一步:识别分组关键字
问题的关键是“每个部门”,这意味着我们需要将数据按照部门编号(deptno)进行分组。使用 GROUP BY deptno
。
select deptno from emp group by deptno;
第二步:确定需要对每组数据计算的聚合函数
对于每个分组(即每个部门),我们需要计算:平均工资 -> avg(sal)
、最高工资 -> max(sal)
select deptno,avg(sal),max(sal) from emp group by deptno;
第三步:组合SELECT和GROUP BY子句
在 SELECT
子句中,除了聚合函数,还必须包含 GROUP BY
后面的列(deptno)。
最终SQL为:SELECT deptno, format(avg(sal), 2), max(sal) FROM emp GROUP BY deptno;
(format(avg(sal),2)
是为了将平均工资格式化为保留两位小数的形式)。
在group by子句中指明按照部门号进行分组,在select语句中使用avg函数和max函数,分别查询每个部门的平均工资和最高工资。如下:
SELECT deptno, format(avg(sal), 2), max(sal) FROM emp GROUP BY deptno;
7、示例七:使用HAVING对分组后结果筛选
需求: 显示平均工资低于2000的部门号和它的平均工资。
第一步:先完成分组聚合
这个需求仍然是基于“每个部门”的,所以首先要像示例六一样分组并计算平均工资:SELECT deptno, avg(sal) AS avg_sal FROM emp GROUP BY deptno;
SELECT deptno, avg(sal) AS avg_sal FROM emp GROUP BY deptno;
第二步:理解WHERE和HAVING的区别
我们不能使用 WHERE avg_sal < 2000
,因为 WHERE
子句是在分组前对原始数据进行筛选,它无法识别聚合函数的结果(如avg_sal)。HAVING
子句是专门用来对分组后的结果集进行筛选的。这个区别很重要!!!我们要时刻记住!!!
第三步:应用HAVING子句进行筛选
在 GROUP BY
之后,使用 HAVING
来指定条件 avg_sal < 2000
。
最终SQL为:SELECT deptno, avg(sal) AS avg_sal FROM emp GROUP BY deptno HAVING avg_sal < 2000;
在group by子句中指明按照部门号进行分组,在select语句中使用avg函数查询每个部门的平均工资,在having子句中指明筛选条件为平均工资小于2000。如下:
SELECT deptno, avg(sal) AS avg_sal FROM emp GROUP BY deptno HAVING avg_sal < 2000;
核心要点: WHERE
过滤行,HAVING
过滤组。WHERE
在分组前执行,HAVING
在分组后执行。
8、示例八:按岗位分组统计
需求: 显示每种岗位的雇员总数,平均工资。
推导过程:
这个需求是示例六分组聚合思想的直接应用,只是分组的关键字变成了岗位(job)。
分组依据: GROUP BY job
select job from emp group by job;
聚合计算:雇员总数 -> count(*)
、平均工资 -> avg(sal)
SELECT job, count(*), avg(sal), FROM emp GROUP BY job;
最终SQL为:SELECT job, count(*), format(avg(sal),2) FROM emp GROUP BY job;
在group by子句中指明按照岗位进行分组,在select语句中使用count函数和avg函数,分别查询每种岗位的雇员总数和平均工资。如下:
SELECT job, count(*), format(avg(sal),2) FROM emp GROUP BY job;
二、多表查询
笛卡尔积的回顾
当需要的数据分布在数据库的不同表中时,我们就需要使用多表查询。核心思想是通过表中的关联字段,将多个表的数据连接起来。回顾之前讲解的内容:
-
上面的基础查询都是在一张表的基础上进行的查询,而实际开发中往往需要将多张表关联起来进行查询,这就叫做多表查询。
-
在进行多表查询时,只需要将多张表的表名依次放到from子句之后,用逗号隔开即可,这时MySQL将会对给定的这多张表取笛卡尔积,作为多表查询的初始数据源。
-
多表查询的本质,就是对给定的多张表取笛卡尔积,然后在笛卡尔积中进行查询。
笛卡尔积的简单讲解:在 SELECT * FROM emp, dept;
中,笛卡尔积就是:员工表的每一行 都与 部门表的每一行进行配对组合。具体过程:如果 emp
表有 14 行员工数据,如果 dept
表有 4 行部门数据,结果就会产生:14 × 4 = 56 行数据。一句话总结:笛卡尔积就是把两个表的所有行进行"全排列"组合。
所谓的对多张表取笛卡尔积,就是得到这多张表的记录的所有可能有序对组成的集合,比如下面对员工表和部门表进行多表查询,由于查询语句中没有指明筛选条件,因此最终得到的结果便是员工表和部门表的笛卡尔积。
select * from emp,dept;
补充说明:
-
员工表和部门表的笛卡尔积由两部分组成,前半部分是员工表的列信息,后半部分是部门表的列信息。
-
对员工表和部门表取笛卡尔积时,会先从员工表中选出一条记录与部门表中的所有记录进行组合,然后再从员工表中选出一条记录与部门表中的所有记录进行组合,以此类推,最终得到的就是这两张表的笛卡尔积。
笛卡尔积的初步过滤
需要注意的是,对多张表取笛卡尔积后得到的数据并不都是有意义的,比如对员工表和部门表取笛卡尔积时,员工表中的每一个员工信息都会和部门表中的每一个部门信息进行组合,而实际一个员工只有和自己所在的部门信息进行组合才是有意义的,因此需要从笛卡尔积中筛选出员工的部门号和部门的编号相等记录。如下:
select * from emp,dept where emp.deptno=dept.deptno;
说明一下: 进行笛卡尔积的多张表中可能会存在相同的列名,这时在选中列名时需要通过表名.列名
的方式进行指明。
解决多表问题的本质:想办法将多表转化成为单表,所以MySQL中,所有select(查询)的问题全部都可以转成单表问题!!!(多表查询的指导思想)
1、示例一:基础等值连接(内连接)
需求: 显示雇员名、雇员工资以及所在部门的名称。
第一步:分析数据来源
当前使用的数据库和其中数据库里面的表如下:
这个查询需要的数据来自两张表:
雇员名(ename
)和工资(sal
)来自 emp 表。
部门名称(dname
)来自 dept 表。
第二步:确定表之间的关联条件
emp 表和 dept 表是通过部门编号(deptno
)关联的。emp 表中的 deptno
字段表示雇员所属的部门,它对应着 dept 表中唯一标识一个部门的 deptno
字段。
因此,连接条件是:emp.deptno
的值必须等于 dept.deptno
的值。这被称为“等值连接”!!!
第三步:构建FROM和WHERE子句
在 FROM
子句中,我们需要同时列出这两张表,用逗号分隔:FROM EMP, DEPT
。如下:
select * from emp,dept;
可以看到上面输出的结果,如果只写这一步,数据库会进行笛卡尔积运算(关系运算),即 EMP
表的每一行都会与 DEPT
表的每一行进行组合。这会产生大量无意义的垃圾数据(例如,会计部的员工会错误地关联到销售部、研发部等)。
所以,必须在 WHERE
子句中指定关键的连接条件,来过滤出有意义的组合:WHERE emp.deptno = dept.deptno
。如下:
select * from emp,dept WHERE emp.deptno = dept.deptno;
第四步:处理列名歧义(使用表名前缀)
由于 deptno
这个列名在两个表中都存在,为了明确指定我们用的是哪个表的 deptno
,需要在列名前加上表名作为前缀,如 emp.deptno
。这是一种良好的编程习惯,即使在某些情况下可以省略,也建议写上以提高可读性和避免未来可能出现的歧义。
最终SQL为:SELECT emp.ename, emp.sal, dept.dname FROM emp, dept WHERE emp.deptno = dept.deptno;
SELECT emp.ename, emp.sal, dept.dname FROM emp, dept WHERE emp.deptno = dept.deptno;
思考过程总结: 先确定需要哪些表,再找到连接这些表的“桥梁”字段,最后在WHERE条件中精确描述这个连接关系,避免产生笛卡尔积。
2、示例二:带筛选条件的多表查询
需求: 显示部门号为10的部门名,员工名和工资。
第一步:复用基础连接
这个需求的基础仍然是 empt 表和 dept 表的连接,所以首先构建和示例一相同的基础连接查询:SELECT ename, sal, dname FROM emp, dept WHERE emp.deptno = dept.deptno
。这一步已经能查询出所有员工及其对应的部门信息。
SELECT ename, sal, dname FROM emp, dept WHERE emp.deptno = dept.deptno;
第二步:附加额外的筛选条件
现在,我们需要从所有员工中,只筛选出部门编号为10的员工。这是一个额外的筛选条件。在SQL中,可以使用 AND
关键字将多个筛选条件连接在 WHERE
子句中。我们添加条件 dept.deptno = 10
。(注意:这里使用 emp.deptno = 10
效果是一样的,因为连接条件已经确保它们相等)。
第三步:组合所有条件
最终的 WHERE
子句包含两个必须同时满足的条件:1) 两个表必须正确连接;2) 部门号必须是10。
最终SQL为:SELECT ename, sal, dname FROM emp, dept WHERE emp.deptno = dept.deptno and emp.deptno=10;
由于部门名只有部门表中才有,而员工名和员工工资只有员工表中才有,因此需要同时使用员工表和部门表进行多表查询,在where子句中指明筛选条件为员工的部门号等于部门编号,并且部门号为10的记录。如下:
SELECT ename, sal, dname FROM emp, dept WHERE emp.deptno = dept.deptno and emp.deptno=10;
推导逻辑: 先建立正确的表间连接,再在此基础上像单表查询一样添加过滤条件。
说明一下: 第一个筛选条件已经筛选出员工的部门号和部门编号相等的记录,因此在筛选部门号等于10的部门时,可以使用员工表中的部门号,也可以使用部门表中的部门编号。
3、示例三:非等值连接
需求: 显示各个员工的姓名,工资,及工资级别。
第一步:分析数据来源
员工姓名(ename
)和工资(sal
)来自 emp 表。
工资级别(grade
)来自 salgrade 表(工资等级表)。
SELECT ename, sal, grade FROM emp, salgrade;
第二步:确定表之间的关联条件
emp 表和 salgrade 表之间没有像“部门编号”那样直接相等的关联字段。它们的关联逻辑是:判断一个员工的工资(emp.sal
)落在哪个工资级别区间(salgrade 表通常有 losal
和 hisal
字段,分别代表该级别的最低和最高工资)。这种关联关系是一种“范围匹配”,而不是精确的等值匹配。连接条件是:员工的工资 sal
必须介于某个级别的下限 losal
和上限 hisal
之间。
第三步:使用BETWEEN...AND...进行连接
在 WHERE
子句中,我们使用 BETWEEN...AND...
操作符来描述这种范围关系:WHERE emp.sal BETWEEN salgrade.losal AND salgrade.hisal
。这个条件会为每个员工找到其工资所属的那个唯一的工资级别区间,从而将 emp
表与 salgrade 表正确地关联起来。
最终SQL为:SELECT ename, sal, grade FROM emp, salgrade WHERE emp.sal BETWEEN salgrade.losal AND salgrade.hisal;
由于员工名和工资只有员工表中才有,而工资级别只有工资等级表中才有,因此需要同时使用员工表和工资等级表进行多表查询,在where子句中指明筛选条件为员工的工资在losal和hisal之间的记录。如下:
SELECT ename, sal, grade FROM emp, salgrade WHERE emp.sal BETWEEN salgrade.losal AND salgrade.hisal;
核心要点: 多表连接的本质是定义表行之间的匹配规则。这个规则可以是等值匹配,也可以是像BETWEEN
这样的范围匹配或其他形式的条件匹配。
补充说明:
-
员工表和工资等级表的笛卡尔积中,将每一个员工的信息和每一个工资等级的信息都进行了组合,而实际一个员工只有和自己的工资对应的工资等级信息进行组合才是有意义的。
-
因此需要根据各个工资等级的最低工资和最高工资判断一个员工是否属于该工资等级,进而筛选出有意义的记录。
三、自连接
自连接是一种特殊的多表查询,其特殊之处在于,查询所涉及的表是同一张表。为了能够进行连接,我们需要通过给表起别名的方式,在逻辑上将其视为两张不同的表。
-
自连接是指在同一张表进行连接查询,也就是说我们不仅可以取不同表的笛卡尔积,也可以对同一张表取笛卡尔积。
-
如果一张表中的某个字段能够将表中的多条记录关联起来,那么就可以通过自连接将表中通过该字段关联的记录组合起来。
1、需求:显示员工FORD的上级领导的编号和姓名
2、背景知识: 在emp表中,有两个关键字段
-
empno
:雇员的唯一编号。 -
mgr
:该雇员的直接上级领导的编号。这个mgr
值指向的是另一条雇员记录的empno
。
3、第一步:理解数据关系(核心难点)
要解决这个问题,关键在于理解数据都存在于同一张emp表中。我们可以这样想象:
-
表中有一条记录代表员工
FORD
,这条记录里的mgr
字段存储着他领导的编号。 -
表中还有另一条记录代表
FORD
的领导,这条记录里的empno
字段等于FORD
记录中的mgr
字段值。
我们的目标是找到这“另一条记录”,并从中取出领导的编号(empno
)和姓名(ename
)。
4、第二步:解决方案一:使用子查询(分两步走)
这是一种直观的、分两步解决的思路。
第一步(内部查询):找出FORD的领导的编号。
我们首先查询FORD
这条记录,获取他的mgr
字段值。
SELECT mgr FROM emp WHERE ename='FORD';
这个查询会返回一个单一的值(比如7566),这就是领导编号。
第二步(外部查询):根据编号查找领导信息。
我们拿着第一步得到的结果(7566),去雇员表中查找empno
等于这个值的记录。
SELECT empno, ename FROM emp WHERE empno = (第一步的结果);
将两步合并,就得到了子查询方案:
SELECT empno, ename FROM emp WHERE empno = (SELECT mgr FROM emp WHERE ename='FORD');
5、第三步:解决方案二:使用自连接(一步到位)
自连接的思路是,将emp表同时当作“员工表”和“领导表”来使用。
给表起别名(关键步骤)
由于是同一张表,为了区分“员工”角色和“领导”角色,我们必须为它起两个不同的别名。例如:worker
:代表员工视角的表;leader
:代表领导视角的表。在FROM
子句中写成:FROM emp worker, emp leader
。这相当于在逻辑上创建了两张表worker
和leader
,它们的数据内容完全相同。
select * from emp worker,emp leader;
建立连接条件(寻找关系)
我们需要描述worker
表和leader
表之间的关系。这个关系就是:员工(worker
)的上级编号(mgr
) 必须等于领导(leader
)的雇员编号(empno
)。连接条件为:worker.mgr = leader.empno
。这个条件会确保将每一位员工与他正确的上级领导记录关联起来。
select * from emp worker,emp leader where worker.mgr = leader.empno;
附加筛选条件
现在,我们从所有“员工-领导”配对中,只筛选出员工姓名是FORD
的那一条记录。条件为:worker.ename = 'FORD'
。
select * from emp worker,emp leader where worker.mgr = leader.empno and worker.ename = 'FORD';
选择要显示的列
最终我们需要显示的是领导的信息,因此SELECT
后面应该从leader
表中选择列:SELECT leader.empno, leader.ename
。
最终的自连接SQL
SELECT leader.empno, leader.ename FROM emp worker, emp leader WHERE worker.mgr = leader.empno AND worker.ename = 'FORD';
6、两种方案对比的思考过程
-
子查询思路: 非常符合人类的直觉思维——“先查A,再根据A的结果查B”。它分两步执行,逻辑清晰,易于理解。适合解决这类“基于一个值去查找另一个值”的问题。
-
自连接思路: 更像是一种“关联匹配”的思维。它将所有“员工-领导”关系一次性构建出来,然后从中筛选出我们需要的那一组。这种思路在处理更复杂的、需要同时显示多个层级或关联信息时更具优势(例如,“显示所有员工及其领导的名字”)。
核心要点:
自连接的本质是通过表的别名,将一张表虚拟成多张表,然后像多表查询一样,根据业务逻辑(如上下级关系、前后顺序关系等)建立它们之间的连接条件。也就是说,由于自连接是对同一张表取笛卡尔积,因此在自连接时至少需要给一张表取别名,否则无法区分这两张表中的列。
四、子查询
子查询是指嵌套在其他SQL语句(如 SELECT
, INSERT
, UPDATE
, DELETE
)中的 SELECT
语句。它也被称为嵌套查询或内部查询。子查询的结果可以被外部查询使用。
1、单行子查询
单行子查询的特点: 子查询只返回单行单列的结果(一个值)。因此,外部查询通常使用单值比较运算符,如 =
, >
, <
, >=
, <=
, <>
。
需求:显示SMITH同一部门的员工相关信息。
第一步:分解问题
这个问题可以拆解为两个步骤:
内部问题: SMITH在哪个部门? -> SELECT deptno FROM emp WHERE ename='SMITH';
这个查询会返回一个单一的值(比如20)。
SELECT deptno FROM emp WHERE ename='SMITH';
外部问题: 有哪些员工的部门编号等于第一步查到的结果? -> SELECT * FROM EMP WHERE deptno = ?
第二步:组合成子查询
我们将内部查询的语句直接放在外部查询需要值的位置,用括号()
括起来。
最终SQL为:SELECT * FROM emp WHERE deptno = (SELECT deptno FROM emp WHERE ename='SMITH');
SELECT * FROM emp WHERE deptno = (SELECT deptno FROM emp WHERE ename='SMITH');
执行顺序: 数据库会先执行内部的子查询,得到部门号(如20),然后再执行外部查询,相当于执行 SELECT * FROM EMP WHERE deptno = 20
。
2、多行子查询
多行子查询的特点: 子查询返回单列多行的结果(一个值的集合)。外部查询需要使用集合比较运算符,如 IN
, NOT IN
, ANY
, ALL
。
示例一:使用 IN 关键字
需求:查询和10号部门的工作岗位相同的雇员的名字,岗位,工资,部门号,但是不包含10部门自己的员工。
第一步:分析内部查询
我们需要先知道10号部门有哪些工作岗位。使用 SELECT DISTINCT job FROM emp WHERE deptno=10;
这个查询可能返回多行结果(如 'MANAGER', 'PRESIDENT')。
SELECT DISTINCT job FROM emp WHERE deptno=10;
第二步:分析外部查询条件
外部查询的条件是:岗位(job
)必须出现在第一步得到的岗位集合中。这正好是 IN
运算符的功能。同时,要排除10号部门的员工:deptno <> 10
。
deptno <> 10
的意思是 部门编号不等于10(<>
:不等于运算符),其等价写法如下:
deptno <> 10
-- 等价于
deptno != 10
-- 也等价于
NOT deptno = 10
最终SQL为:SELECT ename, job, sal, deptno FROM emp WHERE job IN (SELECT DISTINCT job FROM emp WHERE deptno=10) AND deptno <> 10;
SELECT ename, job, sal, deptno FROM emp WHERE job IN (SELECT DISTINCT job FROM emp WHERE deptno=10) AND deptno <> 10;
逻辑: 找出所有岗位属于 {10号部门岗位集合} 且部门不是10的员工。
示例二:使用 ALL 关键字
需求:显示工资比部门30的所有员工的工资都高的员工的姓名、工资和部门号。
第一步:理解“所有”的含义
“比部门30的所有员工工资都高”意味着:该员工的工资必须大于部门30中最高的那个工资。
第二步:使用 ALL 运算符
子查询是获取部门30的所有工资:SELECT sal FROM emp WHERE deptno=30;
SELECT sal FROM emp WHERE deptno=30;
> ALL(子查询)
表示:大于子查询结果集中的每一个值,即大于最大值。它是一个 比较运算符,表示要大于子查询返回的所有值。
含义:只有当值大于子查询结果中的每一个值时,条件才为真。也就是相当于:大于子查询结果中的最大值
语法:
表达式 > ALL(子查询)
等价写法:
表达式 > ALL(子查询)
-- 等价于
表达式 > (SELECT MAX(子查询列) FROM ...)
最终SQL为:SELECT ename, sal, deptno FROM emp WHERE sal > ALL(SELECT sal FROM emp WHERE deptno=30);
SELECT ename, sal, deptno FROM emp WHERE sal > ALL(SELECT sal FROM emp WHERE deptno=30);
示例三:使用 ANY 关键字(注意!!!)
需求:显示工资比部门30的任意员工的工资高的员工的姓名、工资和部门号(包含自己部门的员工)。
第一步:理解“任意”的含义
“比部门30的任意员工工资高”意味着:该员工的工资只要大于部门30中最低的那个工资即可。中文的"任意"在这里确实容易引起歧义,在SQL中应该理解为"至少一个"而不是"每一个"。
正确的理解:(容易混淆,要注意!!!)
"比任意一个员工的工资高" 实际上应该理解为:大于部门30中至少一个员工的工资
两种表达方式的区别:
-
"比任意一个高" = 大于至少一个 = 大于最小值:只要比部门30中最低工资高就满足条件、使用
> ANY
或> SOME
-
"比所有都高" = 大于每一个 = 大于最大值:必须比部门30中最高工资还要高、使用
> ALL
结论总结(记住就行):
-
"比任意一个高" = 大于至少一个 = 使用
> ANY
= 大于最小值 -
"比所有都高" = 大于每一个 = 使用
> ALL
= 大于最大值
第二步:使用 ANY 运算符
> ANY(子查询)
表示:大于子查询结果集中的至少一个值,即大于最小值。
最终SQL为:SELECT ename, sal, deptno FROM emp WHERE sal > ANY(SELECT sal FROM emp WHERE deptno=30);(
这里的字查询同上面的一样;)
SELECT ename, sal, deptno FROM emp WHERE sal > ANY(SELECT sal FROM emp WHERE deptno=30);
3、多列子查询
多列子查询特点: 子查询返回多列的数据(可以是一行或多行)。外部查询需要同时比较多个字段。
例子1:单行多列子查询(子查询返回一行)
需求:查询和SMITH的部门和岗位完全相同的所有雇员,不含SMITH本人。
第一步:分析需要比较的列
我们需要同时比较两个字段:部门号(deptno
)和岗位(job
)。
第二步:使用多列比较语法
我们可以使用 (列1, 列2) = (子查询)
的语法,这表示要求外部查询记录的列1和列2,必须分别等于子查询返回的对应列的值。子查询返回SMITH的部门和岗位:SELECT deptno, job FROM emp WHERE ename='SMITH';
(返回单行两列,如 20, 'CLERK')。
SELECT deptno, job FROM emp WHERE ename='SMITH';
最终SQL为:SELECT ename FROM emp WHERE (deptno, job) = (SELECT deptno, job FROM emp WHERE ename='SMITH') AND ename <> 'SMITH';
SELECT ename FROM emp WHERE (deptno, job) = (SELECT deptno, job FROM emp WHERE ename='SMITH') AND ename <> 'SMITH';
逻辑: 查找所有 (deptno, job) 组合等于 (20, 'CLERK') 且名字不是SMITH的员工。
(列1, 列2) = (子查询)
是 SQL 中的 行值构造器 语法,用于同时比较多个列。
语法结构:
(列1, 列2) = (SELECT 列A, 列B FROM ...)
含义:
-
同时比较多个列的值
-
只有当 所有对应列的值都相等 时,条件才返回 TRUE
-
相当于:
列1 = 值1 AND 列2 = 值2
使用前提:
- 子查询必须返回 且仅返回一行,否则会报错。
例子2:多行多列子查询(子查询返回多行)
-- 子查询返回多行多列
SELECT * FROM emp
WHERE (job, deptno) IN (SELECT job, deptno FROM emp WHERE deptno = 10 -- 可能返回多行
);
补充说明:
-
多列子查询得到的结果是多列数据,在比较多列数据时需要将待比较的多个列用圆括号括起来。
-
多列子查询返回的如果是多行数据,在筛选数据时也可以使用in、all和any关键字。
4、在 FROM 子句中使用子查询(派生表)
特点: 将子查询的结果集当作一张临时表使用,放在 FROM
子句中。这张临时表必须有一个别名。
-
子查询语句不仅可以出现在where子句中,也可以出现在from子句中。
-
子查询语句出现from子句中,其查询结果将会被当作一个临时表使用。
示例一:显示每个高于自己部门平均工资的员工。
第一步:分解问题,先解决“部门的平均工资”
我们需要每个部门的平均工资,这可以通过分组聚合得到:SELECT deptno, AVG(sal) AS asal FROM emp GROUP BY deptno;
这个查询的结果就像一张新表,有deptno
和asal
两列。
SELECT deptno, AVG(sal) AS asal FROM emp GROUP BY deptno;
第二步:将子查询作为临时表(派生表)
我们把第一步的查询当作一张临时表,命名为 tmp
,与原始的 emp 表进行连接。
-
连接条件:emp
.deptno = tmp.deptno
(确保员工和本部门的平均工资关联)。 -
筛选条件:emp
.sal > tmp.asal
(筛选出工资高于自己部门平均工资的员工)。
最终SQL为:
SELECT ename, emp.deptno, sal, FORMAT(tmp.asal, 2) AS ‘部门平均工资’
FROM emp, (SELECT deptno, AVG(sal) AS asal FROM emp GROUP BY deptno) tmp
WHERE emp.deptno = tmp.deptno AND emp.sal > tmp.asal;
思考过程: 当所需的数据不能直接从原始表中获得,而是需要先经过一次聚合或复杂计算时,可以先用子查询生成这个中间结果(派生表),再与主表连接查询。
说明一下: 在from子句中使用子查询时,必须给子查询得到的临时表取一个别名,否则查询将会出错。
跟示例一相似的案例也是同样的思路:
查找每个部门工资最高的人的姓名、工资、部门、最高工资
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 deptno, COUNT(*) AS mycnt FROM emp GROUP BY deptno;
这个查询生成一个包含部门号和对应人数的临时表。
SELECT deptno, COUNT(*) AS mycnt FROM emp GROUP BY deptno;
根据部门号分组,形成逻辑子表(变成一行了),然后统计一个逻辑子表(行)中的人员数量,以此类推,然后再整合成一张表:
第二步:将部门信息表与统计结果临时表连接
将 DEPT
表(包含部门详细信息)与第一步的派生表 tmp
进行连接,连接条件是部门编号相等。
最终SQL为:
SELECT dept.deptno, dname, loc, tmp.mycnt AS ‘部门人数’
FROM dept, (SELECT deptno, COUNT(*) AS mycnt FROM emp GROUP BY deptno) tmp
WHERE dept.deptno = tmp.deptno;
我们也可以使用多表的方法来直接筛选出来符合条件的数据:(但是我觉得不直观,易出错!!!)
select dept.dname, dept.deptno, dept.loc,count(*) '部门人数' from emp, dept
where emp.deptno=dept.deptno group by dept.deptno,dept.dname,dept.loc;
补充说明:
-
因为在select语句中新增了要显示部门名和所在地址,因此需要在group by子句中也添加这两个字段,表明当部门号相同时按照部门名进行分组,当部门名也相同时继续按照所在地址进行分组。
-
但实际在上述场景中部门号相同的记录,它们的部门名和所在地址也一定是相同的,因此在我们看来group by中继续添加这两个字段没什么意义,但MySQL语句要求我们必须添加。
5、合并查询
合并查询,是指将多个查询结果进行合并,可使用的操作符 UNION
或 UNION ALL
将多个 SELECT
语句的结果集合并成一个结果集:
-
union用于取得两个查询结果的并集,union会自动去掉结果集中的重复行。
-
union all也用于取得两个查询结果的并集,但union all不会去掉结果集中的重复行。
1. UNION
特点: 取两个结果集的并集,并自动去除重复行。
需求:将工资大于2500或职位是MANAGER的人找出来。
第一步:分别写出两个条件的查询
查询1(工资大于2500的人):SELECT ename, sal, job FROM emp WHERE sal > 2500;
SELECT ename, sal, job FROM emp WHERE sal > 2500;
查询2(职位是MANAGER的人):SELECT ename, sal, job FROM emp WHERE job = 'MANAGER';
SELECT ename, sal, job FROM emp WHERE job = 'MANAGER';
第二步:使用 UNION 合并
使用 UNION
关键字连接两个查询。数据库会先执行这两个查询,然后将结果合并,并自动去掉完全重复的行(即所有列的值都相同的行)。
最终SQL为:
SELECT ename, sal, job FROM emp WHERE sal > 2500
UNION
SELECT ename, sal, job FROM emp WHERE job = 'MANAGER';
注意: 使用 UNION
时,每个 SELECT
语句必须拥有相同数量的列,且列的数据类型也必须相似。
2. UNION ALL
特点: 取两个结果集的并集,但不会去除重复行。效率比 UNION
高,因为不需要去重。
需求:同上,但保留重复记录。
只需将 UNION
替换为 UNION ALL
。
最终SQL为:
SELECT ename, sal, job FROM emp WHERE sal > 2500
UNION ALL
SELECT ename, sal, job FROM emp WHERE job = 'MANAGER';
结果分析: 比如JONES,他既是经理(MANAGER)工资又高于2500,所以他会在两个查询结果中都出现。使用 UNION
只会显示一次JONES,而使用 UNION ALL
会显示两次。
补充说明:
-
待合并的两个查询结果的列的数量必须一致,否则无法合并。
-
待合并的两个查询结果对应的列属性可以不一样,但不建议这样做。