相关子查询的结构
了解内部查询如何引用外部行,以及按行执行的模型
相关子查询的结构 是 CoddyKit 上的免费 Coding Interview Prep 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Coding Interview Prep 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Coding Interview Prep 课程共包含 4 节课。
子查询为何会成为相关子查询
面试官通常将子查询分为两类。普通(不相关)子查询可以独立运行。相关子查询引用外层查询中的列,因此无法单独运行。
- 不相关子查询:只计算一次,结果供每一行外层数据重复使用。
- 相关子查询:由于依赖外层行,因此针对每一行外层数据重新计算一次。
最明显的标志是:外层表中的列出现在内层查询中。发现这一点后,您就能立即识别出这种模式。
逐行执行模型
请想象一下引擎循环处理外层行。对于每一条外层行,它都会将该行的值代入内层查询,执行查询,再利用结果进行判断或计算。
这就是面试官希望您能够清楚表达的思维模型:“内层查询会针对每一条外层行执行一次。”
这种说法也暗示了一个经典追问:相关子查询可能很慢,因为内层查询可能执行数千次。我们会在第 4 课中解决这个问题。
识别外层引用
这里,employees 和一个表示薪资员工的外层别名 e1 驱动着一个读取 e1.dept_id 的内层查询。这个指向外层行的引用就是相关性。
移除别名前缀后,内层查询就无法独立编译。这种依赖关系正是它成为相关查询的原因。
SELECT e1.name, e1.salary
FROM employees e1
WHERE e1.salary > (
SELECT AVG(e2.salary)
FROM employees e2
WHERE e2.dept_id = e1.dept_id
);大声读出该查询
请像在面试中那样,将上一条查询翻译成通俗易懂的中文:
“对于每名员工 e1,找出其所在部门的平均薪资,并且仅保留薪资高于该部门平均值的员工。”
内层查询中的 WHERE e2.dept_id = e1.dept_id 将平均值限定为当前员工所在部门的平均值。没有这一行,您比较的就会是所有员工的公司整体平均值。
别名不可缺少
当内层查询和外层查询访问同一张表时,必须为两者都设置别名,这样引擎才能知道某个列属于哪一行。
e1= 正在接受测试的外层行。e2= 对该表进行内层扫描。
去掉别名后,dept_id 就会产生歧义;许多引擎随后会静默地将它绑定到内层表,从而破坏相关性。面试官经常会故意设置这个错误。
SELECT 中的相关子查询
相关子查询并不局限于 WHERE。在 SELECT 列表中,它们可以生成一个计算列,并且同样会针对每条外层行进行计算。
下面的查询会显示每个订单的同一客户所下的其他订单数量。内层计数通过 o.customer_id 建立相关性。
SELECT o.order_id,
o.customer_id,
(SELECT COUNT(*)
FROM orders o2
WHERE o2.customer_id = o.customer_id) AS customer_order_count
FROM orders o;标量表示恰好一个值
在 SELECT 中使用,或与 =、>、< 进行比较的相关子查询,必须为每条外层行返回一个单独的标量值。
如果它返回多行,数据库就会引发错误,例如“子查询返回了多行。”
COUNT、MAX 或 AVG 等聚合函数可以保证返回一个值,因此它们常用于标量相关子查询中。了解这条规则可以避免常见的运行时意外。
子查询返回 NULL 时
标量相关子查询可能匹配到零条内层行。此时聚合函数会返回 NULL(而 COUNT 会返回 0)。
这个 NULL 会传递到您的外层表达式中。与 NULL 的比较会得到 UNKNOWN,因此外层行可能会被静默排除。
如果您需要备用值,请将子查询包装在 COALESCE 中。面试官喜欢追问没有内层行匹配时会发生什么,并希望您提到 NULL 的行为。
SELECT c.customer_id,
COALESCE((SELECT MAX(o.amount)
FROM orders o
WHERE o.customer_id = c.customer_id), 0) AS biggest_order
FROM customers c;示例:最新订单日期
一个常见任务是:显示每位客户及其最近的订单日期。在 SELECT 中使用相关子查询即可直接完成。
对于每一条客户行,内层查询通过 o.customer_id = c.customer_id 找出该客户订单日期的 MAX 值。
SELECT c.customer_id,
c.name,
(SELECT MAX(o.order_date)
FROM orders o
WHERE o.customer_id = c.customer_id) AS last_order_date
FROM customers c;为什么可能很慢
由于内层查询会针对每条外层行执行一次,因此在大型外层表上使用相关子查询,可能会触发数百万次内层执行。
- 在相关列上建立索引(这里是
orders.customer_id)可以让每次内层执行快速完成。 - 没有索引时,每次执行都可能扫描整张表,工作量大致为 O(n*m)。
在面试中,请务必提到索引和改写为连接,这是您可以采用的性能优化手段。
相关与非相关并列对比
两者的差别只有一行。非相关版本将每个人与公司整体平均值进行比较;相关版本则将每个人与其所在部门的平均值进行比较。
请阅读这两个版本,注意单独的 WHERE e2.dept_id = e1.dept_id 行是如何改变整体含义的。
-- Uncorrelated: one global average, computed once
SELECT name FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
-- Correlated: per-department average, recomputed per row
SELECT e1.name FROM employees e1
WHERE e1.salary > (
SELECT AVG(e2.salary) FROM employees e2
WHERE e2.dept_id = e1.dept_id
);快速检查
请检验您对相关子查询定义的理解。
回顾:相关子查询的结构
要点:
- 相关子查询会引用外层行,并且针对每条外层行执行一次。
- 当两张表实际上是同一张表时,请为两者都设置别名,以保持相关性明确。
- 标量用法必须返回恰好一个值;零条匹配会得到 NULL,因此请使用
COALESCE进行保护。 - 它可以位于 SELECT 或 WHERE 中,性能取决于是否为相关列建立索引。
在面试中说出“针对每条外层行执行一次”,您就准确抓住了核心概念。
常见问题解答
「相关子查询的结构」课时是免费的吗?
是的 — 「相关子查询的结构」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Coding Interview Prep 课程的其余内容,请升级到 CoddyKit PRO。 Coding Interview Prep 课程共包含 4 节课。
「相关子查询的结构」这节课中我会学到什么?
了解内部查询如何引用外部行,以及按行执行的模型 你通过在浏览器中直接运行的动手代码来练习 Coding Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Coding Interview Prep 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Coding Interview Prep 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「相关子查询的结构」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Coding Interview Prep 课中编写并运行代码吗?
能。每节 Coding Interview Prep 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。