SQL Interview Prep · 课时

将相关子查询重写为连接

为提升性能,将相关逻辑展开为连接或窗口函数

第 4 / 4 课13 个步骤

将相关子查询重写为连接 是 CoddyKit 上的免费 SQL Interview Prep 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 SQL Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 SQL Interview Prep 课程共包含 4 节课。

为什么要重写

相关子查询虽然易读,但可能速度较慢:内层查询可能会针对每一行外层数据执行一次。面试官经常会要求您将其重写为连接或窗口函数,以提升性能。

目标是在保持结果不变的情况下,只遍历一次数据,而不是反复扫描内层数据。

掌握两三种重写模式,并知道每种模式何时能够保持正确性,是中级开发者的核心技能。

模式 1:将 EXISTS 重写为 INNER JOIN

用于测试至少存在一项匹配的相关 EXISTS,通常可以重写为 INNER JOIN。

但请注意:如果多个内层行匹配,连接可能会产生重复的外层行。请添加 DISTINCT 或执行聚合,以恢复每个外层键对应一行的结果。

-- Correlated EXISTS
SELECT c.customer_id, c.name
FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o
             WHERE o.customer_id = c.customer_id);

-- Join rewrite (DISTINCT avoids dupes from fan-out)
SELECT DISTINCT c.customer_id, c.name
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id;

扇出陷阱

最常见的重写错误是忘记扇出。无论客户有多少笔订单,EXISTS 都只会返回每个客户一次。简单的连接则会为每笔订单返回一行,从而使计数膨胀。

如果下游步骤在该连接结果上执行 COUNT(*) 或 SUM(amount),却没有谨慎分组,得到的数字就会错误。

请始终问自己:连接是否会使行数增加?如果会,请使用 DISTINCT 或 GROUP BY 将结果重新合并。

模式 2:将 NOT EXISTS 重写为 LEFT JOIN / IS NULL

反连接重写是面试中几乎必考的模式。相关的 NOT EXISTS 会变成一个 LEFT JOIN,并检查右侧是否为 NULL。

未匹配的外层行在右侧会得到 NULL 值;筛选出该 NULL 值后,保留下来的恰好就是没有匹配项的行。

-- Correlated NOT EXISTS
SELECT c.customer_id FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o
                  WHERE o.customer_id = c.customer_id);

-- LEFT JOIN / IS NULL rewrite
SELECT c.customer_id
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.customer_id IS NULL;

选择一个不可为 NULL 的列进行测试

在 LEFT JOIN / IS NULL 重写中,请测试右侧一个在真实匹配中绝不会为 NULL 的列,最好是连接键或主键。

如果测试可为空的列,您就无法区分真正的不匹配(没有对应行)和已匹配但该列恰好为 NULL 的行。这个错误会返回错误的行。

使用连接键(此处为 o.customer_id)或 o.order_id,可以保证 NULL 表示“没有匹配的行”。

模式 3:将标量聚合重写为 JOIN + GROUP BY

SELECT 中的相关聚合可以变成与已分组子查询(派生表)的连接。

先计算每组的聚合值,再将其连接回明细行。这样内层查询只需执行一次,而不是每行执行一次。

-- Correlated scalar aggregate
SELECT e1.name,
       (SELECT MAX(e2.salary) FROM employees e2
        WHERE e2.dept_id = e1.dept_id) AS dept_max
FROM employees e1;

-- Join + GROUP BY rewrite
SELECT e.name, m.dept_max
FROM employees e
JOIN (SELECT dept_id, MAX(salary) AS dept_max
      FROM employees GROUP BY dept_id) m
  ON m.dept_id = e.dept_id;

模式 4:窗口函数重写

通常最简洁的重写方式是使用窗口函数。MAX(salary) OVER (PARTITION BY dept_id) 可以完全取代相关聚合,无需连接。

它只需遍历一次数据即可计算组值,同时保留每一行明细。对于分析类查询,这通常是面试官最希望看到的答案。

SELECT name,
       dept_id,
       salary,
       MAX(salary) OVER (PARTITION BY dept_id) AS dept_max
FROM employees;

每组最高 N 项的重写

选择每组最高行的相关子查询(salary = MAX per dept)可以使用 ROW_NUMBER 进行简洁的重写。

按组分区,按指标排序,并保留排名为 1 的行。如果您希望保留所有并列的最高行,请改用 RANK。

SELECT name, dept_id, salary
FROM (
    SELECT name, dept_id, salary,
           ROW_NUMBER() OVER (PARTITION BY dept_id
                              ORDER BY salary DESC) AS rn
    FROM employees
) t
WHERE rn = 1;

何时不应重写

重写并不总能带来好处。在以下情况下,请保留相关子查询:

  • 外层集合很小,因此逐行执行的成本可以忽略。
  • 相关列已建立良好索引,并且优化器已经将其转换为高效的半连接。
  • 在需要维护的代码中,可读性比微优化更重要。

现代优化器经常会自动将 EXISTS 转换为半连接。请说明在假定重写有帮助之前,您会使用 EXPLAIN 进行测量。

验证等价性

完成任何重写后,请确认它返回的行和基数都与原查询相同。

  • 检查行数是否一致。
  • 检查连接扇出是否引入了重复项。
  • 检查 NULL 和空组边界情况的行为是否仍然正确。

一种快速方法是运行两个版本,并分别用 EXCEPT 求两个方向的差集;如果结果为空,就说明二者一致。面试官看重的是您会进行验证,而不是想当然地认为它们等价。

SELECT customer_id FROM query_a
EXCEPT
SELECT customer_id FROM query_b;
-- and the reverse; both empty => equivalent

将 IN 重写为 JOIN

非相关的 IN 子查询通常也可以重写为连接,但同样需要注意扇出问题。IN 会对成员关系去重,而连接不会。

如果内层列表包含重复键,连接就会重复外层行。请在内层使用 DISTINCT,或对最终结果使用 DISTINCT,以匹配 IN 的语义。

-- IN subquery
SELECT c.name FROM customers c
WHERE c.customer_id IN (SELECT o.customer_id FROM orders o);

-- Join rewrite, de-duplicated to match IN
SELECT DISTINCT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id;

快速检查

请选择相关 NOT EXISTS 反连接的正确连接重写方式。

回顾:将相关子查询重写为连接

要点:

  • EXISTS → INNER JOIN(添加 DISTINCT 以避免扇出导致的重复项)。
  • NOT EXISTS → LEFT JOIN ... WHERE key IS NULL(测试不可为空的列)。
  • 相关标量聚合 → 将已分组的派生表与 JOIN 连接,或者更好地使用窗口函数。
  • 每组最高项 → ROW_NUMBER(并列时使用 RANK)。
  • 请验证等价性,并在假定重写更快之前使用 EXPLAIN 进行检查。

掌握这两种形式和扇出陷阱,正是中级面试要考察的内容。

免费开始

用 AI 导师学习 SQL — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
30
课程
120

常见问题解答

「将相关子查询重写为连接」课时是免费的吗?

是的 — 「将相关子查询重写为连接」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 SQL Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 SQL Interview Prep 课程共包含 4 节课。

「将相关子查询重写为连接」这节课中我会学到什么?

为提升性能,将相关逻辑展开为连接或窗口函数 你通过在浏览器中直接运行的动手代码来练习 SQL Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 SQL Interview Prep 需要有经验吗?

无需任何先前经验。CoddyKit 上的 SQL Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「将相关子查询重写为连接」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 SQL Interview Prep 课中编写并运行代码吗?

能。每节 SQL Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 相关子查询的结构
  2. 不使用 GROUP BY 的分组汇总
  3. 相关 EXISTS 与 NOT EXISTS
  4. 将相关子查询重写为连接
← 返回 SQL Interview Prep