0Pricing
SQL Interview Prep · 课时

汇总、连接和 DISTINCT 中的 NULL

了解 NULL 在分组、连接和唯一性判断中的不同表现

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

三个令人意外的 NULL 场景

NULL 在不同场景中的行为并不相同。最后一课将讲解最容易让候选人感到意外的三个上下文:聚合、连接以及 DISTINCT / GROUP BY。

反复出现的关键点是:聚合和筛选会把 NULL 当作“跳过我”,而分组和 DISTINCT 会把 NULL 当作“一个等于其他 NULL 的值”。这种不一致正是面试官考察的重点。

掌握这些内容,您就补齐了 SQL 面试中最常见的 NULL 问题。

聚合函数忽略 NULL

核心规则是:聚合函数会跳过 NULL。SUM、AVG、MIN、MAX 以及 COUNT(column) 都会完全忽略 NULL 输入,而不是将其视为零。

这就是 AVG 可能返回与您预期不同的数字的原因。它用非 NULL 值的总和除以非 NULL 值的数量,而不是除以总行数。

-- bonus values: 100, 200, NULL
SELECT
  SUM(bonus) AS total,   -- 300 (NULL ignored)
  AVG(bonus) AS average, -- 150 = 300 / 2, not / 3
  COUNT(bonus) AS cnt    -- 2 (NULL not counted)
FROM employees;

COUNT(*) 与 COUNT(column)

这是关于聚合与 NULL 最常见的问题。COUNT(*) 会统计行数,包括含有 NULL 的行。COUNT(column) 只统计该列为非 NULL 的行。

因此,两者的差值正好是该列中 NULL 的数量。COUNT(DISTINCT column) 更进一步:在去除重复值的同时也会忽略 NULL。

SELECT
  COUNT(*)              AS rows_total,    -- all rows
  COUNT(bonus)          AS non_null_bonus, -- excludes NULLs
  COUNT(DISTINCT bonus) AS distinct_bonus, -- excludes NULLs + dups
  COUNT(*) - COUNT(bonus) AS null_bonus
FROM employees;

AVG 与 SUM/COUNT(*):经典陷阱

面试官会问:“AVG(x) 是否等于 SUM(x) / COUNT(*)?”当存在 NULL 时,答案是否。

AVG(x) 等于 SUM(x) / COUNT(x),除以的是非 NULL 值的数量。如果改为除以 COUNT(*),就会将 NULL 当作零,从而拉低平均值。

如果您确实希望将 NULL 计为零,就必须通过 COALESCE 明确说明这一点。

-- These differ when bonus has NULLs:
SELECT
  AVG(bonus)                       AS avg_ignoring_nulls,
  SUM(bonus) * 1.0 / COUNT(*)      AS avg_nulls_as_zero,
  AVG(COALESCE(bonus, 0))          AS explicit_nulls_as_zero
FROM employees;

全为 NULL 时的聚合边界情况

当每个输入都是 NULL,或者根本没有行时,聚合会返回什么?这是面试官喜欢考察的一个精确区分:

  • 对全为 NULL 的数据(或零行)使用 SUM、AVG、MIN、MAX,结果为 NULL。
  • COUNT 始终返回 0,不会返回 NULL。

因此,如果报表显示空白总计,全为 NULL 的 SUM 很可能就是原因。用 COALESCE 包装它即可显示 0。

-- No matching rows or all bonuses NULL:
SELECT SUM(bonus) FROM employees WHERE 1 = 0;  -- NULL
SELECT COUNT(bonus) FROM employees WHERE 1 = 0; -- 0

-- Present a clean zero:
SELECT COALESCE(SUM(bonus), 0) FROM employees;

JOIN 条件中的 NULL

在 JOIN 的 ON 子句中,NULL = NULL 仍然是 UNKNOWN,因此包含 NULL 的键永远不会匹配普通等值连接。两行即使都包含 NULL 连接键,也不会配对。

在可选外键上进行连接时,这一点很容易让人出错。如果预期行为是让 NULL 与 NULL 匹配,就需要使用 NULL 安全运算符(IS NOT DISTINCT FROM 或 <=>),具体内容见前面的课程。

-- Rows with region IS NULL on both sides do NOT match
SELECT *
FROM a JOIN b ON a.region = b.region;

-- To match NULL-to-NULL (ANSI):
SELECT *
FROM a JOIN b ON a.region IS NOT DISTINCT FROM b.region;

外连接产生的 NULL

外连接会为不匹配的行生成 NULL。在 LEFT JOIN 之后,对于没有找到匹配项的左侧行,每个右侧列都是 NULL。

这正是反连接模式的基础:筛选 WHERE right_table.key IS NULL,即可找出没有匹配项的行,例如没有订单的客户。

不过请注意,在 WHERE 中筛选外连接产生的列,可能会意外地将外连接重新变成内连接,这正是下一场景的主题。

-- Find customers who have never ordered (anti-join)
SELECT c.id, c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;

外连接中使用 WHERE 的 NULL 陷阱

这是一个常见陷阱。您先对订单执行 LEFT JOIN,然后添加 WHERE o.status = 'shipped'。这样一来,没有订单的客户会突然消失,使外连接实际上变成内连接。

为什么?对于不匹配的行,o.status 是 NULL,而 NULL = 'shipped' 的结果是 UNKNOWN,因此 WHERE 会将它们过滤掉。若要保留不匹配的行,请将该条件移到 ON 子句中。

-- Accidental inner join: drops customers with no orders
SELECT c.name, o.status
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.status = 'shipped';

-- Correct: keep unmatched customers
SELECT c.name, o.status
FROM customers c
LEFT JOIN orders o
  ON o.customer_id = c.id AND o.status = 'shipped';

DISTINCT 将所有 NULL 值视为相等

这里有一个令所有人意外的不一致之处。聚合函数会跳过 NULL,但 DISTINCT 会保留恰好一个 NULL,将所有 NULL 视为彼此的重复项。

因此,对值 100、100、NULL、NULL 执行 SELECT DISTINCT bonus 会返回三行:100、NULL,仅此而已。两个 NULL 会合并为一个,尽管在其他地方 NULL = NULL 的结果是 UNKNOWN。

-- bonus: 100, 100, NULL, NULL, 200
SELECT DISTINCT bonus FROM employees;
-- Returns: 100, 200, NULL  (the two NULLs become one row)

GROUP BY 将所有 NULL 归入同一组

GROUP BY 遵循与 DISTINCT 相同的规则:所有 NULL 键都会汇入单个分组。这与比较逻辑相反,在比较逻辑中 NULL 永远不会彼此相等。

因此,按可为 NULL 的列分组时,会得到一行代表所有 NULL 键记录,这通常正是报表所需要的结果。提及这种分组与比较之间的对比,可以体现您理解得很深入。

-- All employees with NULL department form ONE group
SELECT department, COUNT(*) AS headcount
FROM employees
GROUP BY department;
-- A single row where department is NULL totals all of them

面试表达要点

能够给面试官留下深刻印象的统一总结:

  • 聚合函数会忽略 NULL;AVG 除以的是 COUNT(列),而不是 COUNT(*)。
  • COUNT(*) 会统计行数;COUNT(列) 和 COUNT(DISTINCT 列) 会跳过 NULL。
  • 对没有行的数据执行 SUM/AVG/MIN/MAX 会返回 NULL;COUNT 会返回 0。
  • 在连接中,NULL 键永远不会匹配;在 WHERE 中筛选外连接产生的列,会在不易察觉的情况下变成内连接。
  • DISTINCT 和 GROUP BY 会将所有 NULL 视为相等,这与比较逻辑相反。

一句话总结:“聚合和比较时会忽略 NULL,但去重时会将它们归为一组。”

快速检查

测试您对分组与聚合之间差异的理解。

回顾

您已经完成了面试中的 NULL 处理部分:

  • 聚合函数会跳过 NULL;AVG 除以非 NULL 值的数量;全为 NULL 的 SUM 会返回 NULL,而 COUNT 会返回 0。
  • COUNT(*) 包含 NULL 行;COUNT(列) 不包含,二者的差值就是 NULL 的数量。
  • 值为 NULL 的连接键永远不会匹配;在 WHERE 中筛选外连接产生的列,可能会使查询退化为内连接。
  • DISTINCT 和 GROUP BY 会将所有 NULL 合并为一个,这与比较逻辑相反。

请记住这句口诀:聚合和比较时会忽略 NULL,但去重时会将它们归为一组。仅凭这一点,就能回答大多数关于 NULL 的面试问题。

常见问题解答

「汇总、连接和 DISTINCT 中的 NULL」课时是免费的吗?

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

「汇总、连接和 DISTINCT 中的 NULL」这节课中我会学到什么?

了解 NULL 在分组、连接和唯一性判断中的不同表现 你通过在浏览器中直接运行的动手代码来练习 SQL Interview Prep,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

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

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

「汇总、连接和 DISTINCT 中的 NULL」课时需要多长时间?

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

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

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

此课程中的所有课时

  1. 三值逻辑与 UNKNOWN
  2. IS NULL、IS NOT NULL 与 NULL 安全相等
  3. COALESCE、NULLIF 与 ISNULL
  4. 汇总、连接和 DISTINCT 中的 NULL
← 返回 SQL Interview Prep