This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MySQL/14-子查询与派生表.md
T
2026-05-17 00:06:11 +08:00

266 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [MySQL, 子查询, EXISTS, IN, 派生表]
create time: 2026-05-16 00:00
---
# 子查询与派生表
## 概述
子查询是嵌套在另一个查询中的 SELECT 语句。它可以在 WHERE、FROM、SELECT 等多个位置出现,每种位置的语义和执行方式不同。
## 分类
```mermaid
graph BT
subgraph "标量子查询"
S1["返回一行一列<br/>用在 SELECT / WHERE"]
end
subgraph "行子查询"
S2["返回一行多列<br/>用在 ROW() 比较"]
end
subgraph "列子查询"
S3["返回多行一列<br/>用在 IN / ANY / ALL"]
end
subgraph "表子查询(派生表)"
S4["返回多行多列<br/>用在 FROM 子句"]
end
subgraph "EXISTS 子查询"
S5["返回布尔值<br/>用于 EXISTS / NOT EXISTS"]
end
```
## 标量子查询
```sql
-- 用法 1:在 SELECT 中调用
SELECT
username,
(SELECT COUNT(*) FROM orders WHERE user_id = users.id) AS order_count
FROM users;
-- 用法 2:在 WHERE 中等价于常量
SELECT * FROM products
WHERE price > (SELECT AVG(price) FROM products);
-- 用法 3:在 INSERT 中赋值
INSERT INTO reports (month, order_total)
VALUES ('2026-05', (SELECT SUM(amount) FROM orders WHERE MONTH(created_at) = 5));
```
> [!WARNING] 标量子查询的性能隐患
> MySQL 8.0.21 之前,标量子查询**无法被物化**,会导致每次执行都重新计算(N+1 问题)。
> ```sql
> -- ❌ 慢:每个用户都要查一次 orders 表
> SELECT u.username,
> (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS cnt
> FROM users u;
> -- 如果有 10 万用户,就要查 10 万次 orders 表
>
> -- ✅ 换成 JOIN 或窗口函数
> SELECT u.username, COALESCE(SUB.cnt, 0) AS cnt
> FROM users u
> LEFT JOIN (
> SELECT user_id, COUNT(*) AS cnt
> FROM orders GROUP BY user_id
> ) SUB ON u.id = SUB.user_id;
> ```
## IN vs EXISTS
这是面试经典题,也是实际中最纠结的选择。
```sql
-- IN 子查询
SELECT * FROM users
WHERE id IN (SELECT user_id FROM orders);
-- EXISTS 子查询
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
```
```mermaid
flowchart TD
A["Optimizer 选择策略"] --> B{"哪张表更小?"}
B -->|"子查询结果小"<| C["转换 IN → EXISTS<br/>先跑子查询,外层仅匹配结果"]
B -->|"子查询结果大"<| D["转换 EXISTS → IN<br/>外层先行过滤,减少子查询次数"]
C --> E["NOT IN 永远转不成 EXISTS!<br/>NULL 值会导致语义错误"]
D --> F["NOT EXISTS 是最安全的反查写法"]
style E fill:#EE5A24,color:#fff
style F fill:#00D866,color:#fff
```
> [!QUESTION] NOT IN 和 NOT EXISTS 有什么区别?
> ```sql
> -- ⚠️ 致命陷阱
> SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM orders);
>
> -- 如果子查询返回任何 NULL 值,整个 NOT IN 结果为 UNKNOWN
> -- 最终返回空结果集!
>
> -- ✅ 正确写法
> SELECT * FROM users
> WHERE id NOT IN (SELECT user_id FROM orders WHERE user_id IS NOT NULL);
>
> -- 或者直接用 NOT EXISTS(更安全)
> SELECT * FROM users u
> WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
> ```
### 选型决策:IN vs EXISTS vs JOIN
实际开发中三者往往能写出等价逻辑,选法参考下表:
| 场景 | 推荐写法 | 理由 |
|------|----------|------|
| 外大内小(外层百万、子查询几百) | `EXISTS` | 外层每行只需匹配一条即可停止,短路效应明显 |
| 外小内大 | `IN` | Optimizer 会先物化子查询结果再匹配,效率高 |
| 子查询含 `NULL` 值可能 | `EXISTS` / `NOT EXISTS` | `NOT IN` 遇到 NULL 直接返回空集(见上文陷阱) |
| 需要返回外键表的完整列 | `JOIN` + `DISTINCT` | EXISTS 只能判断存在性,无法获取关联表字段 |
| 只是判断"有没有" | `EXISTS` | 语义最清晰,性能也最优 |
> [!TIP] 经验法则
> **不确定时优先用 EXISTS**——它的语义是"是否存在"而非"值是否匹配",即使 MySQL Optimizer 最终把 IN 和 EXISTS 优化成执行计划也一样。用 EXISTS 能让读代码的人一眼看懂意图,同时给 Optimizer 留下优化空间。
## 派生表(Derived Table / Subquery in FROM)
```sql
-- 派生表:子查询作为一个虚拟表出现在 FROM 位置
SELECT dept, avg_salary
FROM (
SELECT department AS dept, AVG(salary) AS avg_salary
FROM employees
WHERE status = 'active'
GROUP BY department
) AS dept_stats
WHERE avg_salary > 15000
ORDER BY avg_salary DESC;
```
### 物化(Materialization)
MySQL 是否将子查询物化为临时表,取决于查询复杂度:
```mermaid
flowchart LR
A["子查询"] --> B{"能否被推入外层?"}
B -->|能| C["Flattening<br/>展开为 JOIN,零额外开销 ✅"]
B -->|不能| D{"是否需要聚合?"}
D -->|是| E["Materialization<br/>物化为临时表 ⚠️"]
D -->|否| F["Semi-join 优化<br/>半连接优化 ✅"]
style C fill:#00D866,color:#fff
style F fill:#00B6BC,color:#fff
style E fill:#FF9F43,color:#000
```
```sql
-- ✅ 可以被 Flattening 优化(无额外开销)
SELECT u.id, o.amount
FROM users u
JOIN (SELECT user_id, amount FROM orders) AS o ON u.id = o.user_id;
-- Optimizer 将其转换为普通的 JOIN
-- ❌ 必须 Materialization(有临时表开销)
SELECT u.id, SUB.total
FROM users u
JOIN (
SELECT user_id, SUM(amount) AS total
FROM orders GROUP BY user_id
) AS SUB ON u.id = SUB.user_id;
-- 子查询因为有 GROUP BY,必须先物化成临时表
```
> [!NOTE] 如何判断是否物化?
> 使用 `EXPLAIN FORMAT=JSON`:
> ```json
> {
> "select_type": "DERIVED",
> "materialized_from_subquery": { ... }
> }
> ```
> 如果出现 `"using_temporary_table": true`,说明使用了临时表。
实战中更常用的方式是 `EXPLAIN` + 观察 type 和 Extra:
```sql
-- ❌ 物化型派生表 → Extra 出现 "Using temporary"
EXPLAIN SELECT u.id, SUB.total
FROM users u
JOIN (SELECT user_id, SUM(amount) AS total FROM orders GROUP BY user_id) AS SUB ON u.id = SUB.user_id;
-- id | select_type | table | type | Extra
-- ---|----------------|------------|-------|----------------------------
-- 1 | PRIMARY | <derived2> | ALL | NULL
-- 1 | PRIMARY | u | ALL | Using where
-- 2 | DERIVED | orders | ALL | Using temporary; Using filesort
-- ✅ 可 Flattening → 无临时表,Extra 干净
EXPLAIN SELECT u.id, o.amount
FROM users u JOIN (SELECT user_id, amount FROM orders) AS o ON u.id = o.user_id;
-- id | select_type | table | type | Extra
-- 1 | SIMPLE | orders| ALL | NULL
-- 1 | SIMPLE | u | index | Primary key
```
> [!TIP] 性能优化技巧
> - 当发现派生表产生临时表时,考虑**手动提升为 CTE**(MySQL 8.0+),有时能改变 Optimizer 行为
> - `optimizer_switch='derived_merge=on'`(默认开启)控制是否允许 Flattening
> - 对于超大结果集物化,注意 `tmp_table_size` / `max_heap_table_size` 限制
## ANY / ALL / SOME
```sql
-- ANY:子查询返回的值中,任意一个满足即可
SELECT product_name, price
FROM products
WHERE price > ANY (SELECT price FROM products WHERE category = 'premium');
-- 只要比任意一件 premium 产品便宜就行
-- ALL:必须大于子查询返回的所有值
SELECT product_name, price
FROM products
WHERE price > ALL (SELECT price FROM products WHERE category = 'premium');
-- 必须比所有 premium 产品都贵(即最大值之上)
-- SOME 等价于 ANY(同义词)
```
```mermaid
graph TB
P["Products: 10, 50, 100, 200"]
ANY -->|"price > ANY(...)"<| A1["> 10 OR > 50 OR > 100 OR > 200"]
A1 --> R1["结果: 所有 > 10 的商品"]
ALL -->|"price > ALL(...)"<| A2["> 10 AND > 50 AND > 100 AND > 200"]
A2 --> R2["结果: 只有 > 200 的商品"]
style R1 fill:#00B6BC,color:#fff
style R2 fill:#C44569,color:#fff
```
## 总结:核心要点回顾
| 主题 | 一句话 |
|------|--------|
| 标量子查询 | MySQL 8.0.21 之前不可物化,百万行数据必踩 N+1 陷阱,优先改写为 JOIN |
| IN vs EXISTS | 语义上 IN 看值、EXISTS 看存在性;不确定时选 EXISTS,更安全的默认选项 |
| NOT IN 陷阱 | 子查询出现 NULL 即返回空集,生产中几乎永远该用 `NOT EXISTS` 替代 |
| 派生表物化 | 有 GROUP BY / LIMIT / UNION 等操作的子查询无法被展开,必然产生临时表开销 |
| ANY / ALL | 对应 SQL 的 OR / AND 累加,ALL 在空子查询结果时恒返回 TRUE(反直觉,需注意) |
> [!TIP] 核心心法
> **子查询不是万能的——它首先是为了表达清晰,其次才是性能。**
> 写完后务必跑 `EXPLAIN`,确认没有意料之外的临时表或多表扫描。当数据量上去后,能改写成 JOIN 的子查询就尽量改写,因为 JOIN 的执行路径对 Optimizer 更加透明。
## 关联笔记
- [[hhs/MySQL/12-DQL SELECT 全解析]] — SELECT 执行顺序与子查询的关系
- [[hhs/MySQL/13-JOIN 原理与优化]] — EXISTS 子查询常可重写为 JOIN