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

9.0 KiB
Raw Blame History

tags, create time
tags create time
MySQL
子查询
EXISTS
IN
派生表
2026-05-16 00:00

子查询与派生表

概述

子查询是嵌套在另一个查询中的 SELECT 语句。它可以在 WHERE、FROM、SELECT 等多个位置出现,每种位置的语义和执行方式不同。

分类

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

标量子查询

-- 用法 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 问题)。

-- ❌ 慢:每个用户都要查一次 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

这是面试经典题,也是实际中最纠结的选择。

-- 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);
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 有什么区别?

-- ⚠️ 致命陷阱
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)

-- 派生表:子查询作为一个虚拟表出现在 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 是否将子查询物化为临时表,取决于查询复杂度:

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
-- ✅ 可以被 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:

{
  "select_type": "DERIVED",
  "materialized_from_subquery": { ... }
}

如果出现 "using_temporary_table": true,说明使用了临时表。

实战中更常用的方式是 EXPLAIN + 观察 type 和 Extra:

-- ❌ 物化型派生表 → 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

-- 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(同义词)
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 更加透明。

关联笔记