9.1 概述
子查询指一个查询语句嵌套在另一个查询语句内部,这个特性从 MySQL 4.1 开始引入。
从相对位置来说,子查询又被称为内查询,主查询又被称为外查询
9.1.1 子查询的结构
子查询的结构如下所示:
SELECT select_list
FROM table
WHERE expr operator
(
SELECT select_list
FROM table
);
说明:将在下面的内容中逐一细说
- 子查询在主查询之前执行(不相关子查询执行一次,相关子查询执行多次)
- 子查询的结果被主查询使用
- 子查询要包含在一对括号内
- 将子查询放在比较条件的右侧(规范,不遵守不会导致出错)
- 单行操作符对应单行子查询,多行操作符对应多行子查询
- 编写子查询 SQL 语句的技巧:① 从里往外写;② 从外往里写
9.1.2 子查询的分类
(1) 分类方式一
根据内查询的结果,是返回一条还是多条记录,将子查询分为单行子查询和多行子查询。
-
单行子查询:
-
多行子查询:
(2) 分类方式二
按照内查询是否被执行多次,将子查询划分为相关(或关联)子查询和不相关(或非关联)子查询。
-
不相关子查询:子查询从数据表中查询得到结果,这个结果只执行一次,并且作为主查询的条件执行
-
相关子查询:从外部查询开始,主查询“传给”子查询条件,然后子查询根据条件进行查询,并将结果反馈给外部,这个过程需要重复多次
9.2 单行子查询
9.2.1 单行比较操作符
操作符 | 含义 |
---|---|
= | 等于 |
> | 大于 |
>= | 大于等于 |
< | 小于 |
<= | 小于等于 |
<> | 不等于 |
代码示例:
例题 1:查询工资大于 149 号员工工资的员工的信息
SELECT employee_id, last_name, salary
FROM employees
WHERE salary > (
SELECT salary
FROM employees
WHERE employee_id = 149
);
通常,我们将子查询放在比较条件的右侧。当然,放在左侧不会产生错误,只是不美观,如下所示:
SELECT employee_id, last_name, salary
FROM employees
WHERE (
SELECT salary
FROM employees
WHERE employee_id = 149
) < salary;
9.2.2 HAVING中使用子查询
执行的流程:
- 首先执行子查询
- 向主查询中的 HAVING 子句返回结果
例题 2:查询最低工资大于 110 号部门最低工资的部门 id 和其最低工资
SELECT department_id, MIN(salary)
FROM employees
WHERE department_id IS NOT NULL
GROUP BY department_id
HAVING MIN(salary) > (
SELECT MIN(salary)
FROM employees
WHERE 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_id
FROM departments
WHERE location_id = 1800
) THEN 'Canada'
ELSE 'USA' END AS "location"
FROM employees;
9.2.4 可能遇到的问题 1
(1) 空值问题
子查询返回的是空值,导致主查询失败。
SELECT last_name, job_id
FROM employees
WHERE job_id =
(
SELECT job_id
FROM employees
WHERE last_name = 'Haas'
);
说明:公司没 “Hass” 这个人,内查询的值为 NULL,导致外查询啥也查不到,如下图所示
(2) 子查询包含在括号内
子查询必须包含在一对括号内,否则报错,如下所示:
SELECT employee_id, last_name, salary
FROM employees
WHERE salary >
SELECT salary
FROM employees
WHERE employee_id = 149;
强调:子查询要包含在一对括号内
(3) 非法使用操作符
在多行子查询中使用单行操作符,导致错误。
SELECT employee_id, last_name
FROM employees
WHERE 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, salary
FROM employees
WHERE 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, salary
FROM employees
WHERE 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, salary
FROM employees
WHERE 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 # 50 FROM employees GROUP BY department_id HAVING 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_id FROM employees GROUP 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_name
FROM employees
WHERE employee_id NOT IN (
SELECT manager_id
FROM employees
);
将 NOT IN 修改为 IN 会如何呢?成功查询,不报错。因为 IN 表示“任一”,只要与多行子查询返回结果中的一个值匹配就可以,不一定与子查询中的 NULL 值进行比较。
SELECT last_name
FROM employees
WHERE employee_id IN (
SELECT manager_id
FROM employees
);
9.4 相关子查询
9.4.1 相关子查询的执行流程
每执行一次主查询,子查询都要执行一次,并且在主查询之前执行。子查询使用主查询中的某个列,并返回给主查询某个(些)行。流程如下图所示:
相关子查询的书写格式:
SELECT column1, column2, ...
FROM table1 outer
WHERE column1 operator
(
SELECT column1, column2
FROM table2 inner
WHERE inner.expr1 = outer.expr2
);
例题 1:查询员工中工资大于本部门平均工资的员工的 last_name、salary 及其 department_id
SELECT last_name, salary, department_id
FROM employees e1
WHERE salary > (
SELECT AVG(salary)
FROM employees e2
WHERE 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 employees
GROUP 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, salary
FROM employees e
ORDER BY (
SELECT department_name
FROM departments d
WHERE d.`department_id` = e.`department_id`
);
9.4.4 关键字EXISTS
与NOT EXISTS
相关子查询通常也会与 EXISTS 关键字一起使用,用来检查在子查询中是否存在满足条件的行。
EXISTS:
- 如果子查询中的当前行不满足条件,则条件返回 FALSE,并继续查询下一行
- 如果子查询中的当前行满足条件,则条件返回 TRUE,并停止查找
附:
NOT EXISTS
与EXISTS
相反,如果不满足条件则返回 TRUE,满足条件则返回 FALSE。
例题 3:查询公司管理者的 employee_id、last_name、job_id、department_id
SELECT employee_id, last_name, job_id, department_id
FROM employees e1
WHERE EXISTS (
SELECT *
FROM employees e2
WHERE e2.`manager_id` = e1.`employee_id`
);
解释:对于当前员工 e 1 e_1 e1,如果他有下属 e 2 e_2 e2,这说明 e 1 e_1 e1 是管理者。
当然,还有其他方法:
方法二:自连接
SELECT DISTINCT emp.`employee_id`, emp.`last_name`, emp.`job_id`, emp.`department_id` FROM employees mgr, employees emp WHERE mgr.`manager_id` = emp.`employee_id`;
方法三:不相关的多行子查询,先查找出所有管理者的 employee_id,然后逐一核实每个员工是否为管理者中的一员
SELECT employee_id, last_name, job_id, department_id FROM 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 d
WHERE 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 d
ON e.`department_id` = d.`department_id`
WHERE e.`department_id` IS NULL;
总结:
- 由上面子查询的所有内容可知,在查询语句中,除了
GROUP BY
和LIMIT
,其他位置都可以声明子查询。- 子查询 SQL 编写技巧:
- 如果子查询相对简单,则可以从外往里写;如果子查询较复杂,则从里往外写
- 如果子查询是相关子查询,通常是从外往里写