汇总、连接和 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 反馈 — 无需本地设置。
此课程中的所有课时
- 三值逻辑与 UNKNOWN
- IS NULL、IS NOT NULL 与 NULL 安全相等
- COALESCE、NULLIF 与 ISNULL
- 汇总、连接和 DISTINCT 中的 NULL