第18章:子查询
本章目标
学完本章后,你将能够:
- 理解子查询的四种分类(标量/行/列/表子查询)
- 区分独立子查询和相关子查询的性能差异
- 掌握 EXISTS/NOT EXISTS 及其与 IN/NOT IN 的安全性对比
- 使用 CTE(WITH 子句)编写可读性更高的复杂查询
- 编写递归 CTE 处理树形结构数据
- 合理选择子查询 vs JOIN 的适用场景
前置知识
- 第12章:查询基础 SELECT
- 第13章:WHERE 条件过滤
- 第17章:连接查询 JOIN
18.1 什么是子查询
子查询(Subquery)是嵌套在另一个 SQL 语句中的 SELECT 查询。子查询可以出现在 SELECT、FROM、WHERE、HAVING 等多个位置:
-- 准备测试数据
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(100),
category VARCHAR(50),
price DECIMAL(10,2)
);
CREATE TABLE order_items (
id INT PRIMARY KEY,
product_id INT,
quantity INT,
order_date DATE
);
INSERT INTO products VALUES
(1, '机械键盘', '电脑配件', 399.00),
(2, '无线鼠标', '电脑配件', 129.00),
(3, '27寸显示器', '电脑配件', 1999.00),
(4, 'Python编程书', '图书', 89.00),
(5, 'MySQL必知必会', '图书', 69.00),
(6, '办公椅', '家具', 599.00),
(7, 'LED台灯', '家具', 199.00),
(8, '移动硬盘', '电脑配件', 459.00);
INSERT INTO order_items VALUES
(1, 1, 2, '2026-06-01'),
(2, 2, 5, '2026-06-10'),
(3, 1, 3, '2026-06-15'),
(4, 3, 1, '2026-07-01'),
(5, 4, 10, '2026-07-05'),
(6, 5, 8, '2026-07-10'),
(7, 2, 4, '2026-07-12'),
(8, 1, 1, '2026-07-15'),
(9, 6, 2, '2026-07-20'),
(10, 5, 15, '2026-07-25');18.2 子查询分类(按返回值)
2.1 标量子查询(Scalar Subquery)
返回单个值(1 行 1 列),可以与比较运算符(=, >, < 等)一起使用:
-- 查询价格高于平均价格的产品
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);
-- 子查询返回一个数值:所有产品的平均价格
-- 查询与"机械键盘"同分类的产品
SELECT name, category, price
FROM products
WHERE category = (SELECT category FROM products WHERE name = '机械键盘')
AND name != '机械键盘';标量子查询不能返回多行:
-- ❌ 如果子查询返回多行,标量子查询会报错
-- SELECT name FROM products
-- WHERE price = (SELECT price FROM products WHERE category = '电脑配件');
-- ERROR 1242 (21000): Subquery returns more than 1 row
-- 因为"电脑配件"分类有多个产品,各有不同价格
-- ✅ 改用 IN
SELECT name FROM products
WHERE price IN (SELECT price FROM products WHERE category = '电脑配件');2.2 行子查询(Row Subquery)
返回单行多列,需要在 WHERE 中使用行构造函数 (a, b, c) 来比较:
-- 查询与"机械键盘" category 和 price 都相同的产品
SELECT name, category, price
FROM products
WHERE (category, price) = (
SELECT category, price FROM products WHERE name = '机械键盘'
);
-- 也可以单独用于比较
SELECT name, category, price
FROM products
WHERE (category, price) IN (
SELECT category, MAX(price)
FROM products
GROUP BY category
);
-- 查询每个分类中价格最高的产品2.3 列子查询(Column Subquery)
返回多行单列,通常与 IN、ANY、ALL 一起使用:
-- IN:等于子查询结果中的任意一个值
SELECT name, price
FROM products
WHERE category IN (SELECT DISTINCT category FROM products WHERE price > 500);
-- NOT IN:不等于子查询结果中的任意一个值
SELECT name, category
FROM products
WHERE id NOT IN (
SELECT DISTINCT product_id FROM order_items
);
-- 没有被订购过的产品2.4 表子查询(Table Subquery / 派生表)
返回多行多列,必须放在 FROM 子句中,且必须有别名:
-- 派生表(Derived Table):查询结果作为临时表使用
SELECT category, avg_price
FROM (
SELECT
category,
AVG(price) AS avg_price,
COUNT(*) AS product_count
FROM products
GROUP BY category
) AS category_stats -- ← 派生表必须有别名!
WHERE product_count >= 2;
-- ❌ 派生表忘记别名 → 报错
-- SELECT * FROM (SELECT * FROM products);
-- ERROR 1248 (42000): Every derived table must have its own alias18.3 子查询分类(按依赖性)
3.1 独立子查询(Non-correlated Subquery)
子查询不依赖外部查询的任何列,可以独立运行。这种子查询只执行一次:
-- 子查询完全独立,先执行子查询得到平均价格,再执行外部查询
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);
-- 执行过程:
-- 第 1 步:SELECT AVG(price) FROM products → 得到 492.75
-- 第 2 步:SELECT name, price FROM products WHERE price > 492.753.2 相关子查询(Correlated Subquery)
子查询引用了外部查询的列,因此外部查询的每一行都要执行一次子查询:
-- 相关子查询:子查询中引用了外部查询的 p1.id
SELECT p1.name, p1.price
FROM products p1
WHERE p1.price > (
SELECT AVG(p2.price)
FROM products p2
WHERE p2.category = p1.category -- ← 引用了外部查询的 p1.category
);
-- 查询每个分类中价格高于该分类平均价格的产品
-- 执行过程:
-- 对 products 表的每一行:
-- 读取该行的 category 值
-- 执行子查询:SELECT AVG(price) FROM products WHERE category = 该行的category
-- 比较 price 和平均价格
-- 10 行产品 → 执行 10 次子查询!独立 vs 相关子查询的性能对比
-- 独立子查询:只执行 1 次子查询
SELECT * FROM products
WHERE category IN (SELECT DISTINCT category FROM products WHERE price > 500);
-- 子查询执行 1 次
-- 相关子查询:每行执行 1 次子查询
SELECT * FROM products p1
WHERE EXISTS (
SELECT 1 FROM products p2
WHERE p2.category = p1.category AND p2.price > 1000
);
-- 子查询执行 N 次(N = products 的行数)
-- 大数据量时性能差异巨大!优化提示:尽量把相关子查询改写为 JOIN,MySQL 优化器对 JOIN 的优化远好于对相关子查询的优化。
18.4 EXISTS 与 NOT EXISTS
EXISTS:检查子查询是否返回行
-- EXISTS:如果子查询返回至少一行,条件为 TRUE
-- 查询"有订单的产品"
SELECT name, category
FROM products p
WHERE EXISTS (
SELECT 1 FROM order_items oi
WHERE oi.product_id = p.id
);
-- SELECT 1(或 SELECT *)是惯例写法
-- EXISTS 只关心子查询有没有结果,不关心返回值是什么NOT EXISTS:检查子查询是否无结果
-- NOT EXISTS:如果子查询返回 0 行,条件为 TRUE
-- 查询"从没被订购过的产品"
SELECT name, category
FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM order_items oi
WHERE oi.product_id = p.id
);EXISTS 的执行方式
-- EXISTS 使用"短路"逻辑:
-- 对外部查询的每一行,执行子查询
-- 一旦子查询找到第一行匹配结果就停止(不再继续扫描)
-- 这个特性使其在特定场景下非常高效
-- 等价 JOIN 写法:
SELECT DISTINCT p.name, p.category
FROM products p
JOIN order_items oi ON p.id = oi.product_id;
-- EXISTS 和 JOIN 哪个更快取决于数据分布和索引18.5 EXISTS vs IN:安全性对比
这是 MySQL 中最为重要的子查询对比之一:
IN 的安全性
-- 假设某订单的 product_id 可能为 NULL
INSERT INTO order_items VALUES (11, NULL, 1, '2026-07-30');
-- IN 查询:所有有订单的产品
SELECT name FROM products
WHERE id IN (SELECT product_id FROM order_items);
-- 正常工作:NULL 在 IN 中不影响结果(只是不会匹配任何值)NOT IN 的 NULL 陷阱
-- ⚠️ NOT IN + NULL 子查询 = 灾难!
SELECT name FROM products
WHERE id NOT IN (SELECT product_id FROM order_items);
-- 如果在 order_items 的 product_id 列中有 NULL,结果会是空集!
-- 为什么?
-- WHERE id NOT IN (1, 2, 3, NULL)
-- 等价于:
-- WHERE id <> 1 AND id <> 2 AND id <> 3 AND id <> NULL
-- 而 id <> NULL → UNKNOWN(不是 FALSE)
-- UNKNOWN 不是 TRUE → 行被排除
-- 所以 NOT IN 中有 NULL 时,整个条件对任意 id 都返回 UNKNOWN → 空集!EXISTS 的安全性
-- ✅ NOT EXISTS 没有 NULL 陷阱!推荐使用
SELECT name FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM order_items oi
WHERE oi.product_id = p.id
);
-- 不受子查询中 NULL 的影响对比总结
-- | 特性 | IN | EXISTS |
-- |-------------------|-------------------|---------------------------|
-- | NULL 安全 | IN 安全 | 安全 |
-- | NOT NULL 安全 | NOT IN 不安全! | NOT EXISTS 安全 |
-- | 子查询执行方式 | 先执行完子查询 | 逐行检查,找到匹配即停止 |
-- | 子查询返回值 | 返回实际值 | 只关心有无返回值(用 SELECT 1)|
-- | 适用场景 | 小结果集 | 大结果集 + 相关子查询 |实战推荐
-- 查找"有订单的产品"
-- IN 和 EXISTS 都可以,IN 通常更直观:
SELECT name FROM products
WHERE id IN (SELECT DISTINCT product_id FROM order_items);
-- 查找"没有订单的产品"
-- ✅ 强烈推荐 NOT EXISTS(避免 NULL 陷阱):
SELECT name FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM order_items oi WHERE oi.product_id = p.id
);
-- ❌ 不推荐 NOT IN(NULL 陷阱):
-- SELECT name FROM products
-- WHERE id NOT IN (SELECT product_id FROM order_items);18.6 ANY、ALL 运算符
ANY / SOME
ANY 与比较运算符结合使用,表示"与子查询结果中的任意一个值比较":
-- SOME 是 ANY 的同义词
-- 价格大于"任意一个家具产品的价格" → 大于家具中最便宜的价格
SELECT name, price
FROM products
WHERE price > ANY (
SELECT price FROM products WHERE category = '家具'
);
-- 家具价格:599, 199 → 价格大于 199 的都满足
-- 等价于 price > (SELECT MIN(price) FROM products WHERE category = '家具')-- 价格大于"所有家具产品的价格" → 大于家具中最贵的价格
SELECT name, price
FROM products
WHERE price > ALL (
SELECT price FROM products WHERE category = '家具'
);
-- 家具价格:599, 199 → 价格必须大于 599
-- 等价于 price > (SELECT MAX(price) FROM products WHERE category = '家具')ALL
-- = ALL:等于子查询中的所有值
SELECT name, price FROM products WHERE price = ALL (
SELECT price FROM products WHERE category = '家具'
);
-- 等同于 price = 599 AND price = 199 → 不可能满足
-- <> ALL:不等于子查询中的任何值(等价于 NOT IN)
SELECT name, price FROM products WHERE price <> ALL (
SELECT price FROM products WHERE category = '家具'
);
-- 等同于 price != 599 AND price != 19918.7 子查询在各个子句中的位置
在 SELECT 中(标量子查询)
-- 每个产品旁边显示其分类的平均价格
SELECT
name,
price,
category,
(SELECT AVG(price) FROM products p2 WHERE p2.category = p1.category) AS category_avg
FROM products p1;在 FROM 中(派生表)
-- 从分类统计结果中查询
SELECT *
FROM (
SELECT
category,
COUNT(*) AS product_count,
AVG(price) AS avg_price,
MAX(price) AS max_price
FROM products
GROUP BY category
) AS stats
WHERE product_count >= 2
ORDER BY avg_price DESC;在 HAVING 中
-- 价格总和大于所有分类平均价格总和的分类
SELECT category, SUM(price) AS total_price
FROM products
GROUP BY category
HAVING SUM(price) > (
SELECT AVG(total) FROM (
SELECT SUM(price) AS total FROM products GROUP BY category
) AS cat_totals
);18.8 CTE:公用表表达式(MySQL 8.0+)
CTE(Common Table Expression)提供了一种更优雅的方式来组织复杂查询,类似"在查询中定义临时命名结果集":
基本 CTE
-- 使用 WITH 定义 CTE
WITH category_stats AS (
SELECT
category,
COUNT(*) AS product_count,
AVG(price) AS avg_price
FROM products
GROUP BY category
)
SELECT
p.name,
p.price,
p.category,
cs.avg_price,
p.price - cs.avg_price AS price_diff
FROM products p
JOIN category_stats cs ON p.category = cs.category
ORDER BY price_diff DESC;
-- 语法:WITH cte_name AS (子查询) SELECT ... FROM cte_name ...
-- CTE 可以像普通表一样被引用多个 CTE
-- 一次定义多个 CTE
WITH
category_stats AS (
SELECT category, AVG(price) AS avg_price
FROM products GROUP BY category
),
expensive_products AS (
SELECT * FROM products WHERE price > 500
)
SELECT
ep.name,
ep.price,
cs.avg_price,
ep.price - cs.avg_price AS above_avg
FROM expensive_products ep
JOIN category_stats cs ON ep.category = cs.category
ORDER BY above_avg DESC;CTE 相比子查询的优势
-- 不需要 CTE(可读性差)
SELECT * FROM (
SELECT * FROM (
SELECT category, AVG(price) AS avg_price
FROM products GROUP BY category
) AS t1
WHERE avg_price > 300
) AS t2
JOIN products p ON p.category = t2.category;
-- 使用 CTE(清晰)
WITH
cat_avg AS (
SELECT category, AVG(price) AS avg_price
FROM products GROUP BY category
),
expensive_cats AS (
SELECT * FROM cat_avg WHERE avg_price > 300
)
SELECT p.*, ec.avg_price
FROM products p
JOIN expensive_cats ec ON p.category = ec.category;CTE 的复用
-- 同一个 CTE 可以在后续查询中被多次引用(派生表做不到这一点)
WITH cat_stats AS (
SELECT category, AVG(price) AS avg_price, COUNT(*) AS cnt
FROM products GROUP BY category
)
SELECT
'高均价分类' AS type,
category,
avg_price
FROM cat_stats
WHERE avg_price > 500
UNION ALL
SELECT
'多产品分类' AS type,
category,
avg_price
FROM cat_stats
WHERE cnt >= 3;18.9 递归 CTE
递归 CTE 用于处理层次结构数据(树形结构),例如组织架构、分类树、评论嵌套等:
生成序列
-- 使用递归 CTE 生成 1 到 100 的序列
WITH RECURSIVE numbers AS (
-- 基础查询:起始数据
SELECT 1 AS n
UNION ALL
-- 递归查询:引用 CTE 自身,生成下一个值
SELECT n + 1 FROM numbers WHERE n < 100
)
SELECT * FROM numbers;处理组织架构(树形结构)
-- 准备组织架构数据
CREATE TABLE org (
id INT PRIMARY KEY,
name VARCHAR(50),
parent_id INT
);
INSERT INTO org VALUES
(1, 'CEO', NULL),
(2, 'CTO', 1),
(3, 'CFO', 1),
(4, '技术总监', 2),
(5, '产品总监', 2),
(6, '前端组长', 4),
(7, '后端组长', 4),
(8, '前端工程师A', 6),
(9, '前端工程师B', 6),
(10, '后端工程师A', 7);-- 查询 CTO 的完整下属树
WITH RECURSIVE subordinates AS (
-- 起始行:CTO
SELECT id, name, parent_id, 1 AS level
FROM org
WHERE id = 2
UNION ALL
-- 递归:找所有下属
SELECT o.id, o.name, o.parent_id, s.level + 1
FROM org o
JOIN subordinates s ON o.parent_id = s.id
)
SELECT
CONCAT(REPEAT(' ', level - 1), name) AS hierarchy,
level
FROM subordinates
ORDER BY level, id;
-- 输出:
-- +------------------------+-------+
-- | hierarchy | level |
-- +------------------------+-------+
-- | CTO | 1 |
-- | 技术总监 | 2 |
-- | 产品总监 | 2 |
-- | 前端组长 | 3 |
-- | 后端组长 | 3 |
-- | 前端工程师A | 4 |
-- | 前端工程师B | 4 |
-- | 后端工程师A | 4 |
-- +------------------------+-------+查询完整路径
-- 查询从某个员工到 CEO 的完整链
WITH RECURSIVE path_to_root AS (
SELECT id, name, parent_id, CAST(name AS CHAR(200)) AS path
FROM org
WHERE id = 8 -- 前端工程师A
UNION ALL
SELECT o.id, o.name, o.parent_id,
CONCAT(o.name, ' → ', p.path)
FROM org o
JOIN path_to_root p ON o.id = p.parent_id
)
SELECT * FROM path_to_root
WHERE parent_id IS NULL;
-- 输出包含完整路径:CEO → CTO → 技术总监 → 前端组长 → 前端工程师A递归 CTE 的关键要素
-- 递归 CTE 语法模板:
-- WITH RECURSIVE cte_name AS (
-- 基本查询(非递归部分,提供初始数据)
-- UNION ALL
-- 递归查询(引用 cte_name,产生新行)
-- )
-- SELECT * FROM cte_name;
-- 关键规则:
-- 1. 必须用 RECURSIVE 关键字
-- 2. 基本查询和递归查询用 UNION ALL 连接(不能是 UNION)
-- 3. 递归查询必须引用 CTE 自身
-- 4. 必须有终止条件(否则递归不会停止)
-- 5. 默认递归深度限制为 1000,可通过 cte_max_recursion_depth 修改
-- 设置递归深度
-- SET SESSION cte_max_recursion_depth = 5000;
-- 防止无限递归
-- 递归查询的 WHERE 条件应确保在某一步不再返回新行18.10 子查询 vs JOIN
| 对比维度 | 子查询 | JOIN |
|---|---|---|
| 可读性 | 嵌套层次深时可读性差 | 表关联关系清晰 |
| 性能 | 相关子查询可能很慢 | JOIN 优化器成熟,通常更快 |
| 维护性 | 简单子查询易维护 | 复杂 JOIN 可能难以理解 |
| 灵活性 | 可以在 SELECT、WHERE、HAVING 中 | 只能在 FROM 中 |
| NULL 安全 | NOT IN 有 NULL 陷阱 | JOIN + IS NULL 更安全 |
| 适用场景 | 简单的值比较、存在性检查 | 需要多表数据的复杂查询 |
改写示例
-- 子查询版本(相关子查询,每行执行一次)
SELECT p.name
FROM products p
WHERE EXISTS (
SELECT 1 FROM order_items oi
WHERE oi.product_id = p.id AND oi.quantity > 5
);
-- JOIN 版本(可通过 DISTINCT 或 GROUP BY 去重)
SELECT DISTINCT p.name
FROM products p
JOIN order_items oi ON p.id = oi.product_id
WHERE oi.quantity > 5;
-- 哪种更快?
-- 小数据量:差不多
-- 大数据量(products 100 万行,order_items 1000 万行):
-- JOIN + DISTINCT 通常更快(优化器可以批量处理)
-- 但如果有合适的索引,EXISTS 也可以很快(短路求值)常见错误
1. NOT IN 子查询中有 NULL
-- ❌ 结果为空!
-- SELECT name FROM products
-- WHERE id NOT IN (SELECT product_id FROM order_items);
-- 如果 order_items.product_id 有 NULL,整个查询返回 0 行
-- ✅ 使用 NOT EXISTS
SELECT name FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM order_items oi WHERE oi.product_id = p.id
);2. 派生表没有别名
-- ❌ 报错
-- SELECT * FROM (SELECT * FROM products);
-- ERROR 1248: Every derived table must have its own alias
-- ✅ 必须加别名
SELECT * FROM (SELECT * FROM products) AS p;3. 标量子查询返回多行
-- ❌ 报错
-- SELECT name FROM products
-- WHERE price = (SELECT price FROM products WHERE category = '电脑配件');
-- ERROR 1242: Subquery returns more than 1 row
-- ✅ 改用 IN 或加 LIMIT 1
SELECT name FROM products
WHERE price IN (SELECT price FROM products WHERE category = '电脑配件');
-- 或
-- WHERE price = (SELECT price FROM products WHERE category = '电脑配件' LIMIT 1);4. 相关子查询性能差
-- ❌ 慢:对外部查询每行,都执行一次子查询
SELECT p.name
FROM products p
WHERE p.price > (
SELECT AVG(price) FROM products WHERE category = p.category
);
-- ✅ 快:先算好每个分类的平均值,再 JOIN
SELECT p.name
FROM products p
JOIN (
SELECT category, AVG(price) AS avg_price
FROM products GROUP BY category
) AS stats ON p.category = stats.category
WHERE p.price > stats.avg_price;5. 递归 CTE 没有终止条件
-- ❌ 无限递归!子查询中没有限制条件
-- WITH RECURSIVE cte AS (
-- SELECT 1 AS n
-- UNION ALL
-- SELECT n + 1 FROM cte
-- )
-- SELECT * FROM cte;
-- 直到达到 cte_max_recursion_depth 才停止(默认 1000 次)
-- ✅ 必须有 WHERE 条件终止递归
WITH RECURSIVE cte AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM cte WHERE n < 10
)
SELECT * FROM cte;本章练习
标量子查询练习:查询 products 表中价格最高的产品名称和价格。查询价格高于所有产品平均价格的产品列表。
IN vs NOT EXISTS 练习:
- 用 IN 查询"有订单的产品"
- 用 NOT EXISTS 查询"没有订单的产品"
- 向 order_items 添加 product_id 为 NULL 的行,再用 NOT IN 查询,观察并解释结果
相关子查询练习:使用相关子查询,查询每个分类中价格高于该分类平均价格的产品。
派生表练习:使用派生表(FROM 中的子查询)统计每个分类的产品数、平均价格、最高价格,然后从中筛选出产品数 >= 2 的分类。
CTE 练习:将第 4 题改用 CTE 实现,并额外用多个 CTE 定义不同粒度的统计结果。
递归 CTE 练习:
- 使用递归 CTE 生成 1 到 50 的序列
- 基于 org 表,查询任意员工的所有下属(递归向下)
- 查询任意员工到 CEO 的完整汇报链路(递归向上)
改写练习:将以下子查询改写为 JOIN 形式,并比较两者的 EXPLAIN 输出:
sqlSELECT DISTINCT category FROM products p WHERE EXISTS ( SELECT 1 FROM order_items oi WHERE oi.product_id = p.id AND oi.quantity > 5 );