9.1 概述

​ 子查询指一个查询语句嵌套在另一个查询语句内部,这个特性从 MySQL 4.1 开始引入。

​ 从相对位置来说,子查询又被称为内查询,主查询又被称为外查询

9.1.1 子查询的结构

子查询的结构如下所示:

SELECT select_listFROM tableWHERE expr operator(SELECT select_listFROM table);

说明:将在下面的内容中逐一细说

  • 子查询在主查询之前执行(不相关子查询执行一次,相关子查询执行多次)
  • 子查询的结果被主查询使用
  • 子查询要包含在一对括号内
  • 将子查询放在比较条件的右侧(规范,不遵守不会导致出错)
  • 单行操作符对应单行子查询,多行操作符对应多行子查询
  • 编写子查询 SQL 语句的技巧:① 从里往外写;② 从外往里写

9.1.2 子查询的分类

(1) 分类方式一

​ 根据内查询的结果,是返回一条还是多条记录,将子查询分为单行子查询多行子查询

  • 单行子查询:

  • 多行子查询:

(2) 分类方式二

​ 按照内查询是否被执行多次,将子查询划分为相关(或关联)子查询不相关(或非关联)子查询

  • 不相关子查询:子查询从数据表中查询得到结果,这个结果只执行一次,并且作为主查询的条件执行

  • 相关子查询:从外部查询开始,主查询“传给”子查询条件,然后子查询根据条件进行查询,并将结果反馈给外部,这个过程需要重复多次

9.2 单行子查询

9.2.1 单行比较操作符

操作符含义
=等于
>大于
>=大于等于
<小于
<=小于等于
不等于

代码示例:

例题 1:查询工资大于 149 号员工工资的员工的信息

SELECT employee_id, last_name, salaryFROM employeesWHERE salary > (SELECT salaryFROM employeesWHERE employee_id = 149);

​ 通常,我们将子查询放在比较条件的右侧。当然,放在左侧不会产生错误,只是不美观,如下所示:

SELECT employee_id, last_name, salaryFROM employeesWHERE(SELECT salaryFROM employeesWHERE employee_id = 149) < salary;

9.2.2 HAVING中使用子查询

执行的流程:

  • 首先执行子查询
  • 向主查询中的 HAVING 子句返回结果

例题 2:查询最低工资大于 110 号部门最低工资的部门 id 和其最低工资

SELECT department_id, MIN(salary)FROM employeesWHERE department_id IS NOT NULLGROUP BY department_idHAVING MIN(salary) > (SELECT MIN(salary)FROM employeesWHERE department_id = 110 );

9.2.3 CASE中的子查询

例题 3:显示员工的 employee_id、last_name 和 location。其中,若员工 department_id 与 location_id 为 1800 的 department_id 相同,则 location 为 “Canada”,其余则为 “USA”

SELECT employee_id, last_name, CASE department_id WHEN (SELECT department_idFROM departmentsWHERE location_id = 1800 ) THEN 'Canada' ELSE 'USA' END AS "location"FROM employees;

9.2.4 可能遇到的问题 1

(1) 空值问题

子查询返回的是空值,导致主查询失败。

SELECT last_name, job_idFROM employeesWHERE job_id = (SELECT job_idFROM employeesWHERE last_name = 'Haas' );

说明:公司没 “Hass” 这个人,内查询的值为 NULL,导致外查询啥也查不到,如下图所示

(2) 子查询包含在括号内

子查询必须包含在一对括号内,否则报错,如下所示:

SELECT employee_id, last_name, salaryFROM employeesWHERE salary > SELECT salaryFROM employeesWHERE employee_id = 149;

强调:子查询要包含在一对括号内

(3) 非法使用操作符

在多行子查询中使用单行操作符,导致错误。

SELECT employee_id, last_nameFROM employeesWHERE salary = (SELECT MIN(salary) FROM employees GROUP BY department_id );

说明:单行操作符对应单行子查询,多行操作符对应多行子查询。有时,由于没有预判到子查询会返回多条数据(多行子查询),而使用了单行操作符,导致出错,上述的查询结果如下

由上面的几个例题可以体会到:

  • 子查询在主查询之前一次执行完成:无论主查询执行多少次,子查询都只执行一次,所有的主查询使用的都是子查询返回的那个唯一结果(不相关的单行子查询)

  • 子查询的结果被主查询使用

9.3 多行子查询

说明:

  • 子查询返回多条记录
  • 使用多行操作符

9.3.1 多行操作符

操作符含义
IN等于列表中的任一一个
ANY需要和单行比较操作符一起使用,和子查询返回的某一个值比较
ALL需要和单行比较操作符一起使用,和子查询返回的所有值比较
SOME实际上是 ANY 的别名,作用相同,一般常使用 ANY

例题 1:返回其它 job_id 中比 job_id 为 “IT_PROG” 部门任一工资低的员工的员工号、姓名、job_id以及 salary。

SELECT employee_id, last_name, job_id, salaryFROM employeesWHERE salary < ANY ( # 查询部门 id 为 "IT_PROG" 的员工的工资 SELECT salary FROM employees WHERE job_id = 'IT_PROG')AND job_id  'IT_PROG';

说明:“任一”、“低” 表示只要小于 “IT_PROG” 部门中的最高工资即可,等价代码如下

SELECT employee_id, last_name, job_id, salaryFROM employeesWHERE salary < (# 查询'IT_PROG'部门的最高员工工资 SELECT MAX(salary) FROM ( # 查询'IT_PROG'部门的员工工资 SELECT salary FROM employees WHERE job_id = 'IT_PROG' ) it_prog_sal) AND job_id  'IT_PROG';

例题 2:返回其它 job_id 中比 job_id 为 “IT_PROG” 部门所有工资低的员工的员工号、姓名、job_id 以及 salary。

SELECT employee_id, last_name, job_id, salaryFROM employeesWHERE salary < ALL ( SELECT salary FROM employees WHERE job_id = 'IT_PROG')AND job_id  'IT_PROG';

说明:“所有” 意味着要低于 “IT_PROG” 部门中的最低工资。

附:可以使用 ALL 搭配 “=” 求解最小值或最大值。

例题 3:查询平均工资最低的部门 id

(1) 常规思路:求每个部门的平均工资 → 求最小的平均工资 → 求哪个部门的平均工资是这个最小值

# 求哪个部门的平均工资是这个最小值SELECT department_id # 50FROM employeesGROUP BY department_idHAVING AVG(salary) = (# 求最小的平均工资SELECT MIN(avg_sal)FROM ( # 求每个部门的平均工资 SELECT AVG(salary) AS "avg_sal" FROM employees GROUP BY department_id ) t_dept_avg_sal );

本题可以使用 <= 和 ALL 来求解:

SELECT department_idFROM employeesGROUP BY department_id# <= ALL:只有最小值满足这个条件,所以这是在求最小值HAVING AVG(salary) <= ALL ( # 求所有部门的平均工资 SELECT AVG(salary) FROM employees GROUP BY department_id );

9.3.2 可能遇到的问题 2

空值问题

​ 如下面代码所示,在子查询中包含一个 NULL 值,而 NOT IN 要与子查询中的所有值比较,也就是会与 NULL 值运算,所以导致最终的查询为空

SELECT last_nameFROM employeesWHERE employee_id NOT IN ( SELECT manager_id FROM employees );

​ 将 NOT IN 修改为 IN 会如何呢?成功查询,不报错。因为 IN 表示“任一”,只要与多行子查询返回结果中的一个值匹配就可以,不一定与子查询中的 NULL 值进行比较。

SELECT last_nameFROM employeesWHERE employee_id IN (SELECT manager_idFROM employees );

9.4 相关子查询

9.4.1 相关子查询的执行流程

​ 每执行一次主查询,子查询都要执行一次,并且在主查询之前执行。子查询使用主查询中的某个列,并返回给主查询某个(些)行。流程如下图所示:

相关子查询的书写格式:

SELECT column1, column2, ...FROM table1 outerWHERE column1 operator ( SELECT column1, column2 FROM table2 inner WHERE inner.expr1 = outer.expr2);

例题 1:查询员工中工资大于本部门平均工资的员工的 last_name、salary 及其 department_id

SELECT last_name, salary, department_idFROM employees e1WHERE salary > (SELECT AVG(salary)FROM employees e2WHERE e2.`department_id` = e1.`department_id` );

说明:由这个例题可以看出

  • 子查询在主查询之前执行,并且需要执行多次
  • 子查询的结果被主查询使用
  • 子查询包含在一对小括号内

9.4.2 FROM中使用相关子查询

例题 1 可以改写为多表连接的形式:

SELECT e.`last_name`, e.`salary`, e.`department_id`# 两张表连接,求笛卡尔积FROM employees e, (# “虚拟表”:部门 + 部门平均工资SELECT department_id, AVG(salary) AS "dept_avg_sal"FROM employeesGROUP BY department_id ) t_dept_avg_sal# 过滤数据:只有 department_id 相同的才可以拼接为一行数据WHERE e.`department_id` = t_dept_avg_sal.`department_id`# 一行中,员工的工资需大于部门平均工资AND e.`salary` > t_dept_avg_sal.dept_avg_sal;

说明:子查询作为 FROM 从句中的一张“虚拟表”,要使用一对小括号 ( ) 括起来,而且必须要为“虚拟表”起别名,否则报错,如下所示:

9.4.3 ORDER BY中使用相关子查询

例题 2:查询员工的 id、salary,按照 department_name 排序

SELECT employee_id, salaryFROM employees eORDER BY ( SELECT department_name FROM departments d WHERE d.`department_id` = e.`department_id`);

9.4.4 关键字EXISTSNOT EXISTS

​ 相关子查询通常也会与 EXISTS 关键字一起使用,用来检查在子查询中是否存在满足条件的行。

EXISTS:

  • 如果子查询中的当前行不满足条件,则条件返回 FALSE,并继续查询下一行
  • 如果子查询中的当前行满足条件,则条件返回 TRUE,并停止查找

附:NOT EXISTSEXISTS 相反,如果不满足条件则返回 TRUE,满足条件则返回 FALSE。

例题 3:查询公司管理者的 employee_id、last_name、job_id、department_id

SELECT employee_id, last_name, job_id, department_idFROM employees e1WHERE EXISTS ( SELECT * FROM employees e2 WHERE e2.`manager_id` = e1.`employee_id` );

解释:对于当前员工 e1 e_1e1,如果他有下属 e2 e_2e2,这说明 e1 e_1e1 是管理者。

当然,还有其他方法:

方法二:自连接

SELECT DISTINCT emp.`employee_id`, emp.`last_name`, emp.`job_id`, emp.`department_id`FROM employees mgr, employees empWHERE mgr.`manager_id` = emp.`employee_id`;

方法三:不相关的多行子查询,先查找出所有管理者的 employee_id,然后逐一核实每个员工是否为管理者中的一员

SELECT employee_id, last_name, job_id, department_idFROM employees# 逐一核实每个员工是否为管理者中的一员WHERE employee_id IN (# 查找出所有管理者的 employee_id SELECT DISTINCT manager_id FROM employees );

思考:方式二的自连接好还是方式一、方式三的子查询好?

  • 自连接好。在许多 DBMS 的处理过程中,对于自连接的处理速度要比子查询快得多,DBMS 将子查询转化成了自连接。

例题 4:查询 departments 表中,不存在于 employees 表中的部门的 department_id 和 department_name

SELECT d.`department_id`, d.`department_name`FROM departments dWHERE NOT EXISTS ( SELECT * FROM employees e WHERE e.`department_id` = d.`department_id` ); 

解释:NOT EXISTS 与 EXISTS 相反,子查询为 FALSE,即 employees 表中不存在当前这个 department_id 时,子查询返回 TRUE,主查询则返回当前这个 department_id 和 department_name。

附:这个题翻译一下就是求 departments 表相对于 employees 表中独有的数据,也就是求下图中的紫色区域,利用外连接的变式求解

SELECT d.`department_id`, d.`department_name`FROM employees e RIGHT JOIN departments dON e.`department_id` = d.`department_id`WHERE e.`department_id` IS NULL;

总结:

  1. 由上面子查询的所有内容可知,在查询语句中,除了 GROUP BYLIMIT,其他位置都可以声明子查询。
  2. 子查询 SQL 编写技巧:
    • 如果子查询相对简单,则可以从外往里写;如果子查询较复杂,则从里往外写
    • 如果子查询是相关子查询,通常是从外往里写