--- tags: [MySQL, 子查询, EXISTS, IN, 派生表] create time: 2026-05-16 00:00 --- # 子查询与派生表 ## 概述 子查询是嵌套在另一个查询中的 SELECT 语句。它可以在 WHERE、FROM、SELECT 等多个位置出现,每种位置的语义和执行方式不同。 ## 分类 ```mermaid graph BT subgraph "标量子查询" S1["返回一行一列
用在 SELECT / WHERE"] end subgraph "行子查询" S2["返回一行多列
用在 ROW() 比较"] end subgraph "列子查询" S3["返回多行一列
用在 IN / ANY / ALL"] end subgraph "表子查询(派生表)" S4["返回多行多列
用在 FROM 子句"] end subgraph "EXISTS 子查询" S5["返回布尔值
用于 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
先跑子查询,外层仅匹配结果"] B -->|"子查询结果大"<| D["转换 EXISTS → IN
外层先行过滤,减少子查询次数"] C --> E["NOT IN 永远转不成 EXISTS!
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
展开为 JOIN,零额外开销 ✅"] B -->|不能| D{"是否需要聚合?"} D -->|是| E["Materialization
物化为临时表 ⚠️"] D -->|否| F["Semi-join 优化
半连接优化 ✅"] 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 | | 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