1. 什么是子查询
子查询(Subquery)是指出现在其他 SQL 语句(如 SELECT、INSERT、UPDATE、DELETE)中的 SELECT 语句,也被称为内查询(Inner Query)或嵌套查询。外层的主查询(Outer Query)会使用子查询返回的结果作为条件、数据源或字段值,从而完成更复杂的查询需求。
子查询本质上是一张临时生成的虚表,主查询在执行时会把子查询的结果作为输入。理解子查询的分类和适用场景,是掌握 SQL 高级查询能力的关键一步。
2. 子查询的分类
2.1 按子查询出现的位置分类
- SELECT 后面:仅支持标量子查询,即子查询结果必须是一行一列。
- WHERE 或 HAVING 后面:支持标量子查询(单行)、列子查询(多行)、行子查询。
- EXISTS 后面(相关子查询):支持表子查询,用于判断结果是否存在。
- FROM 后面:子查询结果充当临时表,必须取别名。
2.2 按结果集的行数列数分类
- 标量子查询:结果只有一行一列。
- 列子查询:结果只有一列多行。
- 行子查询:结果有一行多列。
- 表子查询:结果多行多列。
3. WHERE 或 HAVING 后面的子查询
这是子查询最常用的位置,用于在条件判断中引用其他查询的结果。使用时有以下几个重要特点:
- 子查询必须放在小括号内。
- 子查询一般放在条件的右侧。
- 标量子查询一般搭配单行操作符使用:
>、<、>=、<=、!=、<>、<=>。 - 列子查询一般搭配多行操作符使用:
IN、ANY、SOME、ALL。 - 子查询先于主查询执行,主查询的条件会用到子查询的结果(虚表)。
3.1 标量子查询(单行子查询)
标量子查询返回的结果只有一行一列,通常搭配单行比较操作符使用。下面通过几个经典案例来理解。
案例一:查询谁的工资比 Lex 高。
第一步,先查询 Lex 的工资:
sql
SELECT salary
FROM employees
WHERE first_name = 'Lex';
第二步,查询工资大于该结果的员工信息:
sql
SELECT *
FROM employees
WHERE salary > (
SELECT salary
FROM employees
WHERE first_name = 'Lex'
);
案例二:返回 job_id 与 141 号员工相同,且 salary 比 143 号员工高的员工名、job_id 和工资。
第一步,查询 141 号员工的 job_id:
sql
SELECT job_id
FROM employees
WHERE employee_id = 141; -- 结果为 ST_CLERK
第二步,查询 143 号员工的工资:
sql
SELECT salary
FROM employees
WHERE employee_id = 143;
第三步,组合条件查询目标员工信息:
sql
SELECT first_name, job_id, salary
FROM employees
WHERE job_id = (
SELECT job_id
FROM employees
WHERE employee_id = 141
)
AND salary > (
SELECT salary
FROM employees
WHERE employee_id = 143
);
案例三:返回公司工资最少的员工的 last_name、job_id、salary。
第一步,查询公司最低工资:
sql
SELECT MIN(salary)
FROM employees;
第二步,查询工资等于该最低工资的员工信息:
sql
SELECT last_name, job_id, salary
FROM employees
WHERE salary = (
SELECT MIN(salary)
FROM employees
);
案例四:查询最低工资大于 50 号部门最低工资的部门 id 和其最低工资。
第一步,查询 50 号部门的最低工资:
sql
SELECT MIN(salary)
FROM employees
WHERE department_id = 50;
第二步,查询每个部门的最低工资:
sql
SELECT MIN(salary), department_id
FROM employees
GROUP BY department_id;
第三步,在第二步基础上增加 HAVING 条件,筛选出最低工资大于 50 号部门最低工资的部门:
sql
SELECT MIN(salary), department_id
FROM employees
GROUP BY department_id
HAVING MIN(salary) > (
SELECT MIN(salary)
FROM employees
WHERE department_id = 50
);
**注意:非法使用标量子查询。**标量子查询的结果必须是一行一列,如果子查询返回多行,就会导致非法使用。例如下面的写法是错误的:
sql
-- 错误示例:子查询返回多行,无法与单值比较
SELECT MIN(salary), department_id
FROM employees
GROUP BY department_id
HAVING MIN(salary) > (
SELECT salary
FROM employees
WHERE department_id = 50
);
因为 department_id = 50 可能对应多个员工的工资,子查询返回多行,而 MIN(salary) 是单值,无法与多行结果直接比较,因此会报错。
3.2 列子查询(多行子查询)
列子查询返回的结果是一列多行,通常搭配多行比较操作符使用:
- IN / NOT IN:等于列表中的任意一个。
- ANY / SOME:和子查询返回的某一个值进行比较。
- ALL:和子查询返回的所有值进行比较。
案例一:返回 location_id 是 1400 或 1700 的部门中的所有员工名。
第一步,查询 location_id 为 1400 或 1700 的部门编号:
sql
SELECT DISTINCT department_id
FROM departments
WHERE location_id IN (1400, 1700);
第二步,查询部门号属于上述结果集的员工名:
sql
SELECT first_name
FROM employees
WHERE department_id IN (
SELECT DISTINCT department_id
FROM departments
WHERE location_id IN (1400, 1700)
);
案例二:返回其他工种中,比 job_id 为 IT_PROG 工种任一工资低的员工的工号、名、job_id 以及 salary。
第一步,查询 job_id 为 IT_PROG 的所有工资:
sql
SELECT DISTINCT salary
FROM employees
WHERE job_id = 'IT_PROG';
第二步,使用 ANY 操作符,查询工资小于上述任意一个工资的员工信息:
sql
SELECT employee_id, first_name, job_id, salary
FROM employees
WHERE salary < ANY (
SELECT DISTINCT salary
FROM employees
WHERE job_id = 'IT_PROG'
);
等价写法:小于 IT_PROG 工种的最大工资。
sql
SELECT employee_id, first_name, job_id, salary
FROM employees
WHERE salary < (
SELECT MAX(salary)
FROM employees
WHERE job_id = 'IT_PROG'
);
案例三:返回其他工种中,比 job_id 为 IT_PROG 工种所有工资都低的员工的工号、名、job_id 以及 salary。
第一步,查询 job_id 为 IT_PROG 的所有工资:
sql
SELECT DISTINCT salary
FROM employees
WHERE job_id = 'IT_PROG';
第二步,使用 ALL 操作符,查询工资小于上述所有工资的员工信息:
sql
SELECT employee_id, first_name, job_id, salary
FROM employees
WHERE salary < ALL (
SELECT DISTINCT salary
FROM employees
WHERE job_id = 'IT_PROG'
);
等价写法:小于 IT_PROG 工种的最小工资。
sql
SELECT employee_id, first_name, job_id, salary
FROM employees
WHERE salary < (
SELECT MIN(salary)
FROM employees
WHERE job_id = 'IT_PROG'
);
3.3 行子查询(结果一行多列)
行子查询返回的结果是一行多列,通常用于同时匹配多个字段的条件场景。
案例:查询员工编号最小并且工资最高的员工信息。
第一步,查询最小的员工编号:
sql
SELECT MIN(employee_id)
FROM employees;
第二步,查询最高工资:
sql
SELECT MAX(salary)
FROM employees;
第三步,组合条件查询员工信息:
sql
SELECT *
FROM employees
WHERE employee_id = (
SELECT MIN(employee_id)
FROM employees
)
AND salary = (
SELECT MAX(salary)
FROM employees
);
4. SELECT 后面的子查询
SELECT 后面仅支持标量子查询,即子查询的结果必须是一行一列,通常用于在查询结果中动态生成一个字段值。
案例一:查询每个部门的员工个数。
sql
SELECT d.*,
(SELECT COUNT(*)
FROM employees e
WHERE e.department_id = d.department_id) AS 个数
FROM departments d;
这里通过关联子查询,为每个部门动态统计员工数量,结果作为新的一列返回。
案例二:查询员工工号等于 102 的部门名。
sql
SELECT (
SELECT department_name
FROM departments d
INNER JOIN employees e
ON d.department_id = e.department_id
WHERE e.employee_id = 102
) AS 部门名;
5. FROM 后面的子查询
FROM 后面的子查询会将查询结果充当一张临时表,此时必须为子查询结果取别名,否则会报错。
案例:查询每个部门的平均工资等级。
第一步,查询每个部门的平均工资:
sql
SELECT ROUND(AVG(salary)), department_id
FROM employees
GROUP BY department_id;
第二步,将第一步的结果作为临时表,与 job_grades 表连接,筛选平均工资位于对应工资等级区间内的记录:
sql
SELECT *
FROM (
SELECT ROUND(AVG(salary)) AS ag, department_id
FROM employees
GROUP BY department_id
) AS ag_dep
INNER JOIN job_grades g
ON ag_dep.ag BETWEEN lowest_sal AND highest_sal;
通过 FROM 子查询,可以把复杂的聚合结果当作一张普通表来参与连接查询,极大提升了 SQL 的表达能力。
6. EXISTS 后面的子查询(相关子查询)
EXISTS 用于判断子查询是否有结果返回,语法为 EXISTS(完整 SQL 语句),返回结果为 1(真)或 0(假)。如果子查询有任意一条记录返回,EXISTS 即为真。
sql
SELECT EXISTS(
SELECT employee_id
FROM employees
WHERE salary = 80000
);
EXISTS 子查询通常与主查询的字段相关联,因此也被称为相关子查询。它常用于判断某个记录是否存在,从而决定主查询是否保留该行。
7. 子查询使用注意事项总结
- 子查询必须放在小括号内,保证优先级清晰。
- 子查询一般放在比较条件的右侧,符合 SQL 阅读习惯。
- 标量子查询搭配单行操作符,列子查询搭配多行操作符。
- FROM 后面的子查询必须取别名,否则语法错误。
- SELECT 后面的子查询仅支持标量子查询,且必须保证只返回一行一列。
- EXISTS 子查询关注的是结果是否存在,而不是结果的具体内容。
- 子查询先于主查询执行,主查询依赖子查询的结果(虚表)进行条件过滤。
8. 结语
子查询是 SQL 查询中非常强大的工具,掌握标量、列、行、表四种子查询的适用场景,能够帮助开发者写出更简洁、更高效的查询语句。在实际开发中,建议结合 EXPLAIN 分析执行计划,在子查询和 JOIN 之间做出合理选择,以兼顾可读性与性能。