窗口函数区分度核心

1.1 子查询的三种类型

 
-- ============================================================
 
-- ① 标量子查询 — SELECT 里写子查询,返回单值
 
-- ⚠️ 外层每行执行一次!慎用!
 
-- ============================================================
 
-- ❌ 危险写法(orders 10万行 → 子查询执行10万次)
 
SELECT o.order_id,                                          -- 订单ID
 
       (SELECT name FROM customers WHERE id = o.customer_id) AS cust_name  -- 子查询在外层每行执行一次!
 
FROM orders o;                                              -- 外层表有10万行
 
-- ✅ 优化写法(一次 JOIN 搞定)
 
SELECT o.order_id, c.name AS cust_name                      -- 需要的列
 
FROM orders o                                               -- 左表
 
LEFT JOIN customers c ON o.customer_id = c.id;              -- 一次 JOIN,走索引,0.1秒
 
-- ============================================================
 
-- ② 派生表子查询 — FROM 里写子查询,作为一个临时表
 
-- ✅ 只执行一次,性能好
 
-- ============================================================
 
SELECT dept_id, avg_salary                                   -- 外层从子查询结果里取
 
FROM (
 
    SELECT dept_id, AVG(salary) AS avg_salary               -- 内层:分组算平均工资(只执行1次)
 
    FROM employees
 
    GROUP BY dept_id                                        -- 按部门分组
 
) AS dept_avg                                               -- ← MySQL 要求派生表必须有别名!
 
WHERE avg_salary > 10000;                                   -- 外层过滤:只要平均工资>1万的
 
-- ============================================================
 
-- ③ 相关子查询 — WHERE 里子查询引用了外层字段
 
-- ⚠️ 和标量子查询一样——外层每行触发一次内查询
 
-- ============================================================
 
-- 查询"工资高于本部门平均工资的员工"
 
SELECT name, salary, dept_id                                 -- 员工信息
 
FROM employees e1                                            -- 外层表:每个员工
 
WHERE salary > (                                             -- 当前员工的工资 >
 
    SELECT AVG(salary) FROM employees e2                     -- 内层算平均工资
 
    WHERE e2.dept_id = e1.dept_id                           -- ← 引用了外层的 dept_id(每行不同!)
 
);                                                           -- 外层10个员工 → 内层执行10次
 
-- ✅ 优化写法:先用派生表算好各部门平均,再 JOIN
 
SELECT e.name, e.salary, e.dept_id                           -- 员工信息
 
FROM employees e                                             -- 左表
 
JOIN (                                                       -- 子查询算好各部门平均工资(只执行1次)
 
    SELECT dept_id, AVG(salary) AS avg_sal                  -- 各部门 + 平均工资
 
    FROM employees GROUP BY dept_id                         -- 按部门分组
 
) d ON e.dept_id = d.dept_id                                -- 连接:配对本部门平均工资
 
WHERE e.salary > d.avg_sal;                                 -- 过滤:工资高于平均的
 
 
三种子查询的执行次数:
 
标量子查询(SELECT 里):         外层 N 行 → 子查询执行 N 次 💀
 
派生表(FROM 里):               子查询执行 1 次 → 结果当临时表用 ✅
 
相关子查询(WHERE 里引外层):     外层 N 行 → 子查询执行 N 次 💀
 
非相关子查询(WHERE 里不引外层): 子查询执行 1 次 → 缓存结果 ✅
 

1.2 🔴 NOT IN + NULL —— 经典钓鱼题

这道题是 SQL 的最高频陷阱,没有之一。就等着你踩。

 
-- 场景:查询"没有下过单的客户"
 
-- customers: id=1(张三),2(李四),3(王五)
 
-- orders: user_id=1, user_id=2, NULL  ← 注意:有一行 NULL!
 
-- ❌ 错误写法1 — NOT IN(有 NULL 时返回 0 行!)
 
SELECT * FROM customers                                    -- 客户表
 
WHERE id NOT IN (                                          -- 不在"下过单的用户ID"列表里
 
    SELECT user_id FROM orders                             -- 子查询结果: (1, 2, NULL)
 
);                                                         -- ⚠️ 有NULL → 整条SQL返回0行!
 
-- 结果:0 行!!!(张三和李四也被过滤了)
 
-- 原因分析:
 
-- id=3 NOT IN (1, 2, NULL)
 
-- → 3 != 1 AND 3 != 2 AND 3 != NULL
 
-- → TRUE AND TRUE AND UNKNOWN
 
-- → UNKNOWN → 被 WHERE 过滤掉(WHERE 只要 FALSE 和 UNKNOWN 都过滤)
 
-- 所有行都被过滤!返回 0 行!
 
-- ✅ 正确写法1 — NOT EXISTS(推荐,语义清晰不受 NULL 影响)
 
SELECT * FROM customers c                                  -- 外层:遍历每个客户
 
WHERE NOT EXISTS (                                         -- "不存在"匹配行
 
    SELECT 1 FROM orders o                                 -- SELECT 1 只是占位,EXISTS 不关心返回值
 
    WHERE o.user_id = c.id                                -- 关键:这里引用了外层 c.id(相关子查询)
 
);                                                         -- 找到一条就停,不受 NULL 影响
 
-- 结果:王五 ✅
 
-- ✅ 正确写法2 — LEFT JOIN + IS NULL
 
SELECT c.* FROM customers c                                -- 客户全部列
 
LEFT JOIN orders o ON c.id = o.user_id                     -- 左连接:保留没订单的客户
 
WHERE o.user_id IS NULL;                                   -- 右表字段为NULL = 没匹配到 = 没下过单
 
-- 结果:王五 ✅
 
-- ✅ 正确写法3 — NOT IN 但子查询里过滤 NULL(不推荐,容易忘)
 
SELECT * FROM customers
 
WHERE id NOT IN (
 
    SELECT user_id FROM orders WHERE user_id IS NOT NULL  -- 手动排除 NULL
 
);
 
 
NOT IN 的 NULL 陷阱原理:
 
  SELECT * FROM customers WHERE id NOT IN (1, 2, NULL)
 
  展开为:
 
  id != 1    → 对 id=3 来说 = TRUE   → 通过
 
  AND id != 2  → 对 id=3 来说 = TRUE   → 通过
 
  AND id != NULL → 对 id=3 来说 = UNKNOWN → ❌ 整行被过滤
 
  TRUE AND TRUE AND UNKNOWN = UNKNOWN
 
  WHERE 子句里 UNKNOWN 被视为 FALSE → 行不返回
 
  结果:整条 SQL 返回 0 行!!!
 

🔴 必背口诀NOT IN 遇 NULL,全军覆没。生产写 SQL 永远用 NOT EXISTS。

一句话讲清:“NOT IN 的问题是三值逻辑——NULL 参与比较运算的结果是 UNKNOWN,UNKNOWN 在 WHERE 中被当作 FALSE 处理。所以子查询只要有一个 NULL,NOT IN 就返回空结果集。这是 SQL 标准的行为,不是 MySQL 的 bug。解决方案是用 NOT EXISTS 或 LEFT JOIN…IS NULL,两者都不受 NULL 影响。“

1.3 IN vs EXISTS — 怎么选?

维度INEXISTS
NULL 安全❌ 子查询有 NULL 时行为诡异✅ 不受 NULL 影响
子查询返回大结果集内存占用高(结果集要缓存)✅ 只判断存在性,找到即停
外层大、子查询小✅ 子查询结果集小,IN 快也行
外层小、子查询大慢(大结果集缓存)✅ EXISTS 找到一条就停
MySQL 5.6+ Semi-Join✅ 优化器自动把 IN 转成 Semi-Join
 
-- IN 适用场景:子查询结果集小且确定无 NULL
 
SELECT * FROM orders WHERE user_id IN (1, 2, 3);   -- 常量列表,等价于 user_id=1 OR user_id=2 OR user_id=3
 
-- EXISTS 适用场景:子查询大或可能含 NULL
 
SELECT * FROM customers c                           -- 外层:遍历客户
 
WHERE EXISTS (                                      -- "存在"匹配行
 
    SELECT 1 FROM orders o                          -- SELECT 1 = 占位符,只要有一条就返回 TRUE
 
    WHERE o.user_id = c.id AND o.amount > 1000     -- 找金额>1000的订单
 
);                                                  -- 找到第一条就停止扫描 → 比 IN 快
 

一图流(Mermaid)


flowchart TD

    A[写子查询] --> B{子查询类型?}

    B -->|标量子查询 SELECT里| C[外层每行执行1次 💀 改JOIN]

    B -->|派生表 FROM里| D[只执行1次 ✅]

    B -->|相关子查询 WHERE引外层| E[外层每行执行1次 💀 先算好再JOIN]

    B -->|查不存在| F[用 NOT EXISTS 别用 NOT IN]

    F --> G[NOT IN 遇 NULL → 返回0行]

速记卡(面试闪卡)

Q1:一句话讲清「窗口函数区分度核心」到底是什么?

A:SQL 子查询分三种:标量与相关子查询每行触发一次要改 JOIN,派生表只跑一次;NOT IN 遇 NULL 会翻车。

Q2:1.1 子查询三种类型与执行次数 —— 怎么理解?

A:标量子查询(SELECT 里)外层 N 行执行 N 次、相关子查询(WHERE 引外层)同理——都是 DEPENDENT SUBQUERY 性能炸弹;派生表(FROM 里)和非相关子查询只执行 1 次当临时表。原则:能改 JOIN 就改 JOIN,10 万行别让子查询跑 10 万次。

Q3:1.2 NOT IN 遇 NULL 全军覆没 —— 怎么理解?

A:NOT IN 遇 NULL 返回 0 行:三值逻辑下 NULL 比较得 UNKNOWN,WHERE 把 UNKNOWN 当 FALSE,于是 TRUE AND TRUE AND UNKNOWN=UNKNOWN→整行被过滤。这是 SQL 标准行为不是 bug。解法:NOT EXISTS 或 LEFT JOIN…IS NULL,都不受 NULL 影响。

Q4:1.3 IN vs EXISTS 怎么选 —— 怎么理解?

A:子查询小且确定无 NULL→IN 快(结果集缓存);子查询大或可能含 NULL→EXISTS(找到即停、NULL 安全)。MySQL 5.6+ 优化器会把 IN 自动转 Semi-Join。查”没下过单的客户”用 NOT EXISTS 或 LEFT JOIN IS NULL,别用 NOT IN。

Q5:实战:怎么查”没下过单的客户” —— 怎么理解?

A:NOT EXISTS:外层遍历客户,内层 SELECT 1 引用 c.id 找匹配,找到一条就停,不受 NULL 影响,结果王五。LEFT JOIN orders ON c.id=o.user_id WHERE o.user_id IS NULL:右表为 NULL=没匹配到=没下过单。两种都返回王五,NOT IN 却返回 0 行。

Q6:核心速记主线有哪些?

  • 三类型:标量(SELECT)/相关(WHERE 引外层)每行跑一次,派生表(FROM)只一次

  • NOT IN 遇 NULL→三值逻辑 UNKNOWN 当 FALSE→返回 0 行

  • 解法:NOT EXISTS 或 LEFT JOIN … IS NULL

  • IN 小快;EXISTS 大稳且 NULL 安全

  • MySQL 5.6+ 把 IN 自动转 Semi-Join

口诀

A:子查询三种类,标量相关每行费

派生表只跑一遭,JOIN 改写最实惠

NOT IN 遇 NULL,全军覆没罪

NOT EXISTS 兜底,LEFT JOIN 也对

相关链接