📊 SQL 入门 Day 7:子查询 — 查询中的查询

📌 一句话总结

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 保可读
相关推荐
一条泥憨鱼29 分钟前
【从0开始学习计算机网络】| ARP 协议:地址解析,ARP 欺骗,链路层寻址
网络·后端·学习·计算机网络
2501_9339232534 分钟前
SpringCloud组件:Eureka服务注册与发现
java·开发语言·后端·spring cloud·eureka
leikooo38 分钟前
ARTS 1011: 双堆动态对顶求中位数、IPC 注入与空密码串联接管账户与程序从不做决策
后端
合尘猫44 分钟前
AI 项目上线就停:一次事件循环阻塞的完整排查过程
人工智能·后端·python
Re_zero1 小时前
一次 SSE 断流排查:PrintWriter 吞掉了我的 IOException
java·后端
1m555555551 小时前
RAG 工程实战(四):明明召回了正确知识,为什么最终 Top-K 还是漏了?
后端
我是人✓1 小时前
Java核心语法:作用域、循环与大数值处理
java·开发语言·后端
newerp2 小时前
GC Pacer 调优算法:GOGC、GOMEMLIMIT 与容器 OOM 防御
后端·程序员·go
邱杰4702 小时前
数智人事系统(二):一条命令起齐四个中间件
后端