498 lines
18 KiB
Markdown
498 lines
18 KiB
Markdown
|
|
---
|
|||
|
|
tags: [MySQL, 子查询, EXISTS, IN, 派生表, CTE]
|
|||
|
|
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;
|
|||
|
|
-- 副作用:这个查询其实也是"相关子查询"的典型例子
|
|||
|
|
-- 因为内层的 user_id = users.id 引用了外层表的列
|
|||
|
|
-- 每一行 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));
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!QUESTION] 上面用法 1 的子查询,每一行都要重新执行吗?
|
|||
|
|
> 是的——但不是因为"放在 SELECT 里",而是因为它引用了外层的 `users.id`。
|
|||
|
|
> 这就引出了子查询最重要的分类维度:**相关 vs 无关**。
|
|||
|
|
|
|||
|
|
### 相关 vs 无关子查询
|
|||
|
|
|
|||
|
|
这是理解子查询性能的基石:**子查询是否引用了外层查询的列?**
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
UC["子查询"] --> UCX{"是否引用外层表的列?"}
|
|||
|
|
UCX -->|否:无关子查询<br/>Uncorrelated| UCN["先执行一次<br/>结果供外层复用 ✅"]
|
|||
|
|
UCX -->|是:相关子查询<br/>Correlated| UCR["对每一行外层记录<br/>都要重新执行 ⚠️"]
|
|||
|
|
|
|||
|
|
style UCN fill:#00B6BC,color:#fff
|
|||
|
|
style UCR fill:#FF9F43,color:#000
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!EXAMPLE] 对比示例
|
|||
|
|
> ```sql
|
|||
|
|
> -- 🟢 无关子查询:只执行一次
|
|||
|
|
> -- 子查询完全不依赖 users 表
|
|||
|
|
> SELECT * FROM products
|
|||
|
|
> WHERE price > (SELECT AVG(price) FROM products);
|
|||
|
|
>
|
|||
|
|
> -- 🔴 相关子查询:执行 N 次(N = users 行数)
|
|||
|
|
> -- 子查询引用了外层 u.id
|
|||
|
|
> SELECT u.username,
|
|||
|
|
> (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS cnt
|
|||
|
|
> FROM users u;
|
|||
|
|
> -- 10 万用户 → 子查询执行 10 万次
|
|||
|
|
> ```
|
|||
|
|
|
|||
|
|
> [!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;
|
|||
|
|
|
|||
|
|
-- ✅ 可 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;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
对比两种情况的执行计划:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph LR
|
|||
|
|
subgraph "❌ 物化型派生表"
|
|||
|
|
T1["select_type: DERIVED"] -->|orders | T2["type: ALL<br/>Extra: Using temporary, Using filesort"]
|
|||
|
|
P1["PRIMARY → <derived2>"] -->|全表扫描| P2["type: ALL"]
|
|||
|
|
P3["PRIMARY → u"] -->|全表扫描| P4["Extra: Using where"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph "✅ 可展开型派生表"
|
|||
|
|
F1["select_type: SIMPLE"] -->|orders | F2["type: ALL<br/>Extra: NULL(无额外开销)"]
|
|||
|
|
F3["SIMPLE → u"] -->|索引扫描| F4["type: index<br/>Extra: Using index"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
style T2 fill:#FF9F43,color:#000
|
|||
|
|
style P2 fill:#EE5A24,color:#fff
|
|||
|
|
style F2 fill:#00D866,color:#fff
|
|||
|
|
style F4 fill:#00D866,color:#fff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!TIP] 性能优化技巧
|
|||
|
|
> - 当发现派生表产生临时表时,考虑**手动提升为 CTE**(MySQL 8.0+),有时能改变 Optimizer 行为
|
|||
|
|
> - `optimizer_switch='derived_merge=on'`(默认开启)控制是否允许 Flattening
|
|||
|
|
> - 对于超大结果集物化,注意 `tmp_table_size` / `max_heap_table_size` 限制
|
|||
|
|
|
|||
|
|
### 常见陷阱与最佳实践
|
|||
|
|
|
|||
|
|
| 坑 | 症状 | 解法 |
|
|||
|
|
|----|------|------|
|
|||
|
|
| **缺少别名** | `Every derived table must have its own alias` | 派生表必须起别名:`FROM (...) AS t` |
|
|||
|
|
| **自引用冲突** | `Table 'xxx' is specified twice` | UPDATE/DELETE 中的派生表需用嵌套包裹 |
|
|||
|
|
| **NULL 传播** | NOT IN 返回空集 | 改用 `NOT EXISTS` 或加 `IS NOT NULL` 过滤 |
|
|||
|
|
| **隐式类型转换** | 索引失效,全表扫描 | 确保比较列类型一致(如 `VARCHAR` vs `INT`)|
|
|||
|
|
| **大结果集物化** | `Using temporary; Using filesort` | 用 JOIN 重写或拆分为多次查询 |
|
|||
|
|
|
|||
|
|
## 实战:性能排查 Checklist
|
|||
|
|
|
|||
|
|
遇到慢的子查询,按以下顺序逐项检查:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
S["慢查询:含子查询"] --> C1{"EXPLAIN 看了吗?"}
|
|||
|
|
C1 -->|没看| STOP["🛑 先跑 EXPLAIN<br/>别盲猜,数据说话"]
|
|||
|
|
C1 -->|看了| C2{"Extra 有 Using temporary 吗?"}
|
|||
|
|
|
|||
|
|
C2 -->|"是:有临时表"| D1["派生表物化了 → 考虑改 JOIN 或提升为 CTE"]
|
|||
|
|
C2 -->|"否"| C3{"有 Using filesort 吗?"}
|
|||
|
|
|
|||
|
|
C3 -->|"是"| D2["排序开销大 → 检查是否有可用索引"]
|
|||
|
|
C3 -->|"否"| C4{"子查询是否在 SELECT 列表中?"}
|
|||
|
|
|
|||
|
|
C4 -->|"是:标量子查询"| D3["N+1 问题 → 改写为 LEFT JOIN + GROUP BY"]
|
|||
|
|
C4 -->|"否"| C5{"用了 NOT IN 吗?"}
|
|||
|
|
|
|||
|
|
C5 -->|"是"| D4["NULL 陷阱 → 换 NOT EXISTS"]
|
|||
|
|
C5 -->|"否"| C6{"关联列类型一致吗?"}
|
|||
|
|
|
|||
|
|
C6 -->|"不一致"| D5["隐式类型转换导致索引失效 → 统一类型"]
|
|||
|
|
C6 -->|"一致"| DONE["✅ 执行计划合理,可能是业务逻辑本身复杂"]
|
|||
|
|
|
|||
|
|
style STOP fill:#EE5A24,color:#fff
|
|||
|
|
style D1 fill:#FF9F43,color:#000
|
|||
|
|
style D2 fill:#FF9F43,color:#000
|
|||
|
|
style D3 fill:#FF9F43,color:#000
|
|||
|
|
style D4 fill:#EE5A24,color:#fff
|
|||
|
|
style D5 fill:#FF9F43,color:#000
|
|||
|
|
style DONE fill:#00D866,color:#fff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> 核心原则:**先看 EXPLAIN,再动手改**。很多情况下不是子查询的问题,而是缺了索引或类型不匹配。
|
|||
|
|
|
|||
|
|
## 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
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## UPDATE / DELETE 中的子查询
|
|||
|
|
|
|||
|
|
子查询不仅限于 SELECT,UPDATE 和 DELETE 同样可以使用。
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- UPDATE:用子查询结果更新列
|
|||
|
|
UPDATE employees e
|
|||
|
|
SET salary = (
|
|||
|
|
SELECT avg_salary FROM (
|
|||
|
|
SELECT AVG(salary) AS avg_salary
|
|||
|
|
FROM employees WHERE department = e.department
|
|||
|
|
) tmp
|
|||
|
|
)
|
|||
|
|
WHERE dept_level = 'manager';
|
|||
|
|
-- ⚠️ MySQL 要求嵌套一层:不能直接在 UPDATE 中引用被更新的表
|
|||
|
|
-- 所以要用派生表包裹一下(tmp)来绕过这个限制
|
|||
|
|
|
|||
|
|
-- DELETE:条件过滤删除记录
|
|||
|
|
DELETE FROM sessions
|
|||
|
|
WHERE user_id IN (
|
|||
|
|
SELECT id FROM users WHERE status = 'banned'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
-- 关联删除:保留每个分组中最新的一条
|
|||
|
|
DELETE t1 FROM logs t1
|
|||
|
|
INNER JOIN logs t2
|
|||
|
|
ON t1.category = t2.category
|
|||
|
|
AND t1.created_at < t2.created_at;
|
|||
|
|
-- 虽然这不是子查询写法,但效果等价于上面的子查询逻辑
|
|||
|
|
-- 实际场景中更推荐 JOIN 写法,性能更好
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## CTE:子查询的现代写法(MySQL 8.0+)
|
|||
|
|
|
|||
|
|
`WITH` 子句(Common Table Expression)是 MySQL 8.0 引入的新语法,本质上是给派生表起了一个「有名字的、可复用的」外壳。
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 传统派生表写法
|
|||
|
|
SELECT dept, avg_salary
|
|||
|
|
FROM (
|
|||
|
|
SELECT department AS dept, AVG(salary) AS avg_salary
|
|||
|
|
FROM employees WHERE status = 'active'
|
|||
|
|
GROUP BY department
|
|||
|
|
) AS dt
|
|||
|
|
WHERE avg_salary > 15000;
|
|||
|
|
|
|||
|
|
-- ✅ 等价 CTE 写法
|
|||
|
|
WITH dept_stats AS (
|
|||
|
|
SELECT department AS dept, AVG(salary) AS avg_salary
|
|||
|
|
FROM employees WHERE status = 'active'
|
|||
|
|
GROUP BY department
|
|||
|
|
)
|
|||
|
|
SELECT dept, avg_salary
|
|||
|
|
FROM dept_stats
|
|||
|
|
WHERE avg_salary > 15000;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
subgraph "传统派生表"
|
|||
|
|
D1["FROM (...)"] -->|可读性差| D2["重复写的逻辑难以复用"]
|
|||
|
|
D3["嵌套深时层级混乱 😵"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph "CTE 写法"
|
|||
|
|
C1["WITH name AS (...)"] -->|语义清晰| C2["可在主查询中多次引用"]
|
|||
|
|
C3["链式 CTE 层层递进 🧩"]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
D1 -.->|"功能等价"| C1
|
|||
|
|
D2 -.->|"可读性提升"| C2
|
|||
|
|
D3 -.-> "|简化复杂查询|" C3
|
|||
|
|
|
|||
|
|
style C1 fill:#00D866,color:#fff
|
|||
|
|
style C2 fill:#00B6BC,color:#fff
|
|||
|
|
style C3 fill:#00B6BC,color:#fff
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 递归 CTE
|
|||
|
|
|
|||
|
|
这是子查询体系无法做到的能力——自我引用的 CTE 可以遍历树形结构。
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 递归 CTE:生成从 1 到 10 的序列
|
|||
|
|
WITH RECURSIVE nums AS (
|
|||
|
|
SELECT 1 AS n -- 锚点成员(递归起点)
|
|||
|
|
UNION ALL
|
|||
|
|
SELECT n + 1 FROM nums WHERE n < 10 -- 递归成员
|
|||
|
|
)
|
|||
|
|
SELECT * FROM nums;
|
|||
|
|
|
|||
|
|
-- 实战:查询组织的完整汇报线
|
|||
|
|
WITH RECURSIVE org_chain AS (
|
|||
|
|
SELECT id, name, manager_id, 1 AS level
|
|||
|
|
FROM employees WHERE manager_id IS NULL -- 根节点:CEO
|
|||
|
|
UNION ALL
|
|||
|
|
SELECT e.id, e.name, e.manager_id, oc.level + 1
|
|||
|
|
FROM employees e
|
|||
|
|
INNER JOIN org_chain oc ON e.manager_id = oc.id -- 递归:逐级下钻
|
|||
|
|
)
|
|||
|
|
SELECT * FROM org_chain ORDER BY level, name;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!QUESTION] 为什么叫「自我引用」?递归 CTE 到底怎么执行的?
|
|||
|
|
> `org_chain` 这个名字在定义内部就被引用了——`FROM employees e INNER JOIN org_chain oc`。
|
|||
|
|
> 第一轮:只执行锚点成员,得到根节点(CEO);
|
|||
|
|
> 第二轮:用第一轮的结果去 JOIN employees,得到 CEO 的直接下属;
|
|||
|
|
> 第三轮:用第二轮的结果去 JOIN,得到下属的下属……
|
|||
|
|
> 直到某一轮 JOIN 结果为空,递归自动终止。
|
|||
|
|
> **关键:没有终止条件 → 无限循环 → 触发 `cte_max_recursion_depth` 报错。**
|
|||
|
|
|
|||
|
|
> [!WARNING] 递归 CTE 的注意事项
|
|||
|
|
> - MySQL 默认递归深度上限为 **1000**(`cte_max_recursion_depth`),超出会报错
|
|||
|
|
> - 务必设置合理的终止条件,否则会无限递归直到达到上限
|
|||
|
|
> - 递归部分不能直接用 `LIMIT` 来截断结果
|
|||
|
|
|
|||
|
|
> [!TIP] 何时用 CTE vs 派生表?
|
|||
|
|
> | 场景 | 推荐 |
|
|||
|
|
> |------|------|
|
|||
|
|
> | 只需引用一次,逻辑简单 | 派生表(更轻量) |
|
|||
|
|
> | 需要多次引用同一段逻辑 | CTE(DRY 原则) |
|
|||
|
|
> | 需要嵌套多层 | CTE(可读性碾压) |
|
|||
|
|
> | 需要递归查询(树形结构) | 只能 CTE |
|
|||
|
|
> | 兼容 MySQL 5.7 | 只能派生表 |
|
|||
|
|
|
|||
|
|
## 总结:核心要点回顾
|
|||
|
|
|
|||
|
|
| 主题 | 一句话 |
|
|||
|
|
|------|--------|
|
|||
|
|
| 标量子查询 | MySQL 8.0.21 之前不可物化,百万行数据必踩 N+1 陷阱,优先改写为 JOIN |
|
|||
|
|
| 相关 vs 无关 | 相关子查询引用了外层列,每行都要重新执行——性能杀手 |
|
|||
|
|
| IN vs EXISTS | 语义上 IN 看值、EXISTS 看存在性;不确定时选 EXISTS,更安全的默认选项 |
|
|||
|
|
| NOT IN 陷阱 | 子查询出现 NULL 即返回空集,生产中几乎永远该用 `NOT EXISTS` 替代 |
|
|||
|
|
| 派生表物化 | 有 GROUP BY / LIMIT / UNION 等操作的子查询无法被展开,必然产生临时表开销 |
|
|||
|
|
| ANY / ALL | 对应 SQL 的 OR / AND 累加,ALL 在空子查询结果时恒返回 TRUE(反直觉,需注意) |
|
|||
|
|
| CTE | MySQL 8.0+ 现代写法,可读性和可维护性优于匿名派生表,唯一支持递归的子查询形式 |
|
|||
|
|
|
|||
|
|
> [!TIP] 核心心法
|
|||
|
|
> **子查询不是万能的——它首先是为了表达清晰,其次才是性能。**
|
|||
|
|
> 写完后务必跑 `EXPLAIN`,确认没有意料之外的临时表或多表扫描。当数据量上去后,能改写成 JOIN 的子查询就尽量改写,因为 JOIN 的执行路径对 Optimizer 更加透明。
|
|||
|
|
|
|||
|
|
> [!TIP] 学习路径建议
|
|||
|
|
> 如果你刚接触子查询,建议按这个顺序学习:
|
|||
|
|
> 1. **标量子查询** → 先理解「子查询就是一次独立的 SELECT」
|
|||
|
|
> 2. **相关 vs 无关** → 理解执行次数差异(本文核心概念)
|
|||
|
|
> 3. **IN vs EXISTS** → 掌握两种过滤范式,重点理解 NOT IN 陷阱
|
|||
|
|
> 4. **派生表** → 子查询作为虚拟表,理解物化与 Flattening
|
|||
|
|
> 5. **CTE** → 用现代语法重写派生表,最终挑战递归 CTE
|
|||
|
|
>
|
|||
|
|
> 每一步都要跑 `EXPLAIN` 看执行计划——**看懂 EXPLAIN 比背语法重要十倍**。
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[hhs/MySQL/02-SQL核心/08-DQL SELECT 全解析]] — SELECT 执行顺序与子查询的关系
|
|||
|
|
- [[hhs/MySQL/02-SQL核心/09-JOIN 原理与优化]] — EXISTS 子查询常可重写为 JOIN
|