📌 一句话总结
SQL 子查询是在一个查询内部嵌套另一个完整查询,用于分步筛选、跨表比对以及生成动态条件。
1. 子查询分类
按返回结果可分为三类:
| 类型 | 返回 | 典型位置 | 运算符 |
|---|---|---|---|
| 标量子查询 | 单个值(一行一列) | SELECT / WHERE / HAVING | = > < <> |
| 行子查询 | 单行多列 | WHERE | = <> |
| 表子查询 | 多行多列 | FROM / EXISTS / IN | IN EXISTS ANY ALL |
2. 标量子查询
返回一个值,可以用在任何需要单值的地方。
sql
-- 查询薪资高于平均的员工
SELECT employee_name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
sql
-- SELECT 子句中用子查询
SELECT
department_id,
department_name,
(SELECT COUNT(*) FROM employees e WHERE e.dept_id = d.department_id) AS emp_count
FROM departments d;
sql
-- HAVING 中使用
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
HAVING AVG(salary) > (SELECT AVG(salary) FROM employees);
执行顺序: 标量子查询对外层每一行 都会执行一次(若关联),性能敏感。上面第 2 个例子相当于对每个 department 都跑一次 COUNT。
3. IN / NOT IN 子查询
返回多行一列,配合 IN 使用。
sql
-- 查找有订单的客户
SELECT customer_name
FROM customers
WHERE customer_id IN (
SELECT DISTINCT customer_id
FROM orders
);
sql
-- 查找从未下单的客户
SELECT customer_name
FROM customers
WHERE customer_id NOT IN (
SELECT DISTINCT customer_id
FROM orders
WHERE customer_id IS NOT NULL -- ⚠️ NOT IN 遇到 NULL 全表无结果!
);
⚠️ 致命陷阱:NOT IN + NULL
sql
-- 如果子查询包含 NULL:
SELECT 1 WHERE 1 NOT IN (1, 2, NULL);
-- 结果:空集(不返回任何行!)
原因: 1 <> NULL 结果是 UNKNOWN,NOT IN 要求所有行都为 TRUE,但凡有一个 NULL 就全表凉凉。NOT IN 一定要先排除 NULL,或者改用 NOT EXISTS。
4. EXISTS / NOT EXISTS
EXISTS 只关心子查询是否有行返回,不关心返回值本身。
sql
-- 查找有订单的客户(等价 IN,但通常更快)
SELECT customer_name
FROM customers c
WHERE EXISTS (
SELECT 1 -- 写 1 或 * 都行,EXISTS 只看行数
FROM orders o
WHERE o.customer_id = c.customer_id
);
sql
-- 找出从未下单的客户(安全版本,不怕 NULL)
SELECT customer_name
FROM customers c
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
);
| 对比 | IN |
EXISTS |
|---|---|---|
| NULL 处理 | 危险(NOT IN 踩坑) | 安全 |
| 性能 | 先执行子查询(物化结果集) | 半关联,外层找到第一个就停 |
| 大数据量 | 子查询结果小时 OK | 外层驱动且子查询有索引时更快 |
| 关联性 | 可关联可不关联 | 通常带关联条件 |
5. ANY / ALL
用于多行单值与单值比较。
sql
-- ANY:大于任何一个(即 > 最小值)
SELECT employee_name, salary
FROM employees
WHERE salary > ANY (
SELECT salary FROM employees WHERE department_id = 10
);
-- 等价于 salary > (SELECT MIN(salary) FROM employees WHERE dept_id = 10)
sql
-- ALL:大于所有(即 > 最大值)
SELECT employee_name, salary
FROM employees
WHERE salary > ALL (
SELECT salary FROM employees WHERE department_id = 10
);
-- 等价于 salary > (SELECT MAX(salary) FROM employees WHERE dept_id = 10)
sql
-- ALL 的另一用法:找最年轻的员工
SELECT employee_name, hire_date
FROM employees e
WHERE hire_date >= ALL (
SELECT hire_date FROM employees
);
-- 等价于 hire_date = (SELECT MAX(hire_date) FROM employees)
| 运算符 | 含义 | 等价写法 |
|---|---|---|
> ANY(...) |
大于任何一个(最小值) | > MIN(...) |
> ALL(...) |
大于所有(最大值) | > MAX(...) |
< ANY(...) |
小于任何一个(最大值) | < MAX(...) |
< ALL(...) |
小于所有(最小值) | < MIN(...) |
= ANY(...) |
等于任意一个 | IN (...) |
<> ALL(...) |
不等于所有 | NOT IN (...) ⚠️ 同样怕 NULL |
6. 关联子查询
子查询引用外层表的列,对外层每一行都执行一次。
sql
-- 查询每个部门薪资最高的员工
SELECT employee_name, salary, department_id
FROM employees e
WHERE salary = (
SELECT MAX(salary)
FROM employees
WHERE department_id = e.department_id -- 关联条件
);
sql
-- 找出超出本部门平均薪资的员工
SELECT employee_name, salary, department_id
FROM employees e
WHERE salary > (
SELECT AVG(salary)
FROM employees
WHERE department_id = e.department_id
);
性能注意: 关联子查询对 n 行外层的每行执行一次,最坏 O(n×m)。如果表大,优先考虑用窗口函数或 JOIN 替代。
7. FROM 子句子查询(派生表)
把子查询结果当成临时表来查。
sql
-- 统计每个部门的平均薪资,再筛选出平均薪资 > 5000 的部门
SELECT dept_avg.department_id, dept_avg.avg_salary
FROM (
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
) dept_avg
WHERE dept_avg.avg_salary > 5000;
sql
-- 排名前 3 的订单(ROW_NUMBER 也可,这里演示子查询)
SELECT *
FROM (
SELECT *,
ROW_NUMBER() OVER (ORDER BY total_amount DESC) AS rank
FROM orders
) ranked
WHERE rank <= 3;
⚠️ FROM 子句的子查询必须加别名 (上面的
dept_avg和ranked),否则报错。
8. 子查询 vs JOIN:怎么选?
sql
-- 方案 A:子查询
SELECT customer_name
FROM customers
WHERE customer_id IN (SELECT customer_id FROM orders);
-- 方案 B:JOIN + DISTINCT
SELECT DISTINCT c.customer_name
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id;
| 场景 | 推荐方案 |
|---|---|
| 仅检查存在性(有/无) | EXISTS(最安全最快) |
| 需要子查询的值 | JOIN |
| 聚合后再比较 | 子查询或 CTE |
| 分步逻辑复杂 | CTE(下一章) |
| 只取一个计算值 | 标量子查询 |
📝 要点总结
| 概念 | 关键点 |
|---|---|
| 标量子查询 | 返回单值,用于 SELECT/WHERE/HAVING |
| IN | 查多行,注意 NOT IN + NULL 踩坑 |
| EXISTS | 只关心是否存在,半关联效率高,不怕 NULL |
| ANY / ALL | 多行比较,= ANY = IN,<> ALL = NOT IN |
| 关联子查询 | 外层驱动,逐行执行,性能敏感 |
| 派生表 | FROM 子句的子查询,必须加别名 |
| 子查询 vs JOIN | EXISTS 查存在,JOIN 取列值,CTE 保可读 |