📊 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 结果是 UNKNOWNNOT 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_avgranked),否则报错。


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 保可读
相关推荐
IT_陈寒17 小时前
为什么我的JavaScript异步代码总是不按顺序执行?
前端·人工智能·后端
星栈17 小时前
Node 框架怎么选?Express、Koa、Egg、NestJS 场景化选型指南
后端·node.js
锋行天下17 小时前
打造企业内部知识库系统RAG全栈项目
前端·后端·架构
码事漫谈18 小时前
Kimi K3 真实体验:全网评价整理,优缺点一次性说清楚
后端
Fanta丶18 小时前
16.Activiti8 SpringBoot3.X 部署与测试
后端
用户2080468045618 小时前
Python3 数据类型转换新手实战指南
后端
苏三说技术19 小时前
为什么Spring要“抛弃”Feign?
后端
神奇小汤圆19 小时前
Java代理模式深度解析:静态代理 vs 动态代理 vs CGLIB
后端
爱勇宝19 小时前
AI不会淘汰所有人,但会淘汰这6种人
前端·后端·程序员