SQL 数据分析 · 指南 · 2026年10月30日 · 14 分钟阅读

SQL NULL 值处理:IS NULL、COALESCE,以及 NULL 在连接、计数和比较中的行为

一排坐满乘客的机舱座位中空着一个座位

NULL 的意思是「这里没有值」:未知、缺失或不适用。 它不是 0,不是空字符串,也不是 false。大多数关于 NULL 的意外都来自同一条规则:任何和 NULL 的比较结果都是 NULL(未知),既不是真也不是假,而 WHERE 只保留条件为真的行。所以 x = NULL 什么也查不到,列表里有 NULL 的 NOT IN 什么也查不到,在条件前面加 NOT 也不会把 NULL 行翻到另一边去。

实用的工具并不多:

下面的例子都在 Nomto SQL 课程使用的航空公司数据库上运行:2026 年夏季的 33,121 个航班、53,000 名会员、150 万条 App 事件。SQL 采用 PostgreSQL 语法,每个结果都是真实运行得到的。

NULL 从哪里来

真实的表里出现 NULL 有不同的原因,而原因决定了你该怎么处理它们。在这个数据库里:

按状态统计非 NULL 值的数量,第一种情况一目了然:

SELECT status,
       COUNT(*) AS flights,
       COUNT(actual_departure) AS dep,
       COUNT(actual_arrival) AS arr
FROM flights
GROUP BY status
ORDER BY flights DESC;
status flights dep arr
Arrived 16707 16707 16707
Scheduled 15383 0 0
On Time 518 0 0
Cancelled 414 8 8
Departed 58 58 0
Delayed 41 0 0

已起飞的航班有起飞时间,还没有到达时间;计划中的航班两者都没有。还有 8 个已取消的航班居然有时间记录,这种数据上的怪现象只有统计 NULL 才看得出来。

比较:为什么 = NULL 查不到数据

SELECT NULL = NULL AS eq,
       NULL <> 1 AS ne,
       NULL IS NULL AS isnull,
       1 + NULL AS plus,
       'a' || NULL AS concat_op,
       concat('a', NULL) AS concat_fn;
eq ne isnull plus concat_op concat_fn
NULL NULL true NULL NULL a

连 NULL = NULL 都不为真:两个未知的值,不能确定它们相等。算术运算和 || 运算符同样会把 NULL 传下去;concat() 是例外,它把 NULL 当作空字符串。

所以下面这个查询返回 0,而且不会报任何错:

SELECT COUNT(*) FROM flights WHERE actual_departure = NULL;
count_star()
0

而这个查询能找到 16,348 个还没起飞的航班:

SELECT COUNT(*) FROM flights WHERE actual_departure IS NULL;
count_star()
16348

三值逻辑:NOT 抓不住 NULL

SQL 里的条件可以是真、假或未知。WHERE 只保留真。NOT 未知 仍然是未知,所以 NULL 行在两边都会被丢掉:

SELECT COUNT(*) FILTER (WHERE birth_year < 1980) AS before_1980,
       COUNT(*) FILTER (WHERE NOT (birth_year < 1980)) AS from_1980,
       COUNT(*) FILTER (WHERE birth_year IS NULL) AS unknown,
       COUNT(*) AS total
FROM loyalty_members;
before_1980 from_1980 unknown total
17334 29347 6319 53000

一个条件和它的否定加起来是 46,681,而不是 53,000。如果你用 WHERE cond 和 WHERE NOT cond 把一张表分成「符合」和「其余」两半,NULL 哪一半都不会进。要在 NULL 该去的那一边加上 OR col IS NULL。

<> 也一样。WHERE status <> 'Arrived' 会悄悄跳过 status 为 NULL 的行(这张表里没有,但很多表有)。

IS DISTINCT FROM:对 NULL 安全的比较

如果你希望把 NULL 当作普通值,让两个 NULL 算作相等,就用 IS DISTINCT FROM 和 IS NOT DISTINCT FROM:

SELECT NULL IS DISTINCT FROM NULL AS a,
       1 IS DISTINCT FROM NULL AS b,
       NULL IS NOT DISTINCT FROM NULL AS c;
a b c
false true true

它们永远返回真或假,不会返回 NULL。在比较两个快照「这个值变了没有」时很好用,因为从 NULL 变成某个值也必须算作变化。PostgreSQL 和 DuckDB 支持它们;MySQL 用的是 <=> 运算符。

聚合函数里的 NULL

除了 COUNT(*),所有聚合函数都忽略 NULL。三个值,其中一个是 NULL:

SELECT AVG(x) AS avg_x, SUM(x) AS sum_x, COUNT(x) AS cnt_x, COUNT(*) AS cnt
FROM (VALUES (1), (NULL), (3)) t(x);
avg_x sum_x cnt_x cnt
2 4 2 3

平均值是 4 / 2 = 2,而不是 4 / 3。COUNT(x) 数的是非 NULL 值,COUNT(*) 数的是行。看真实数据:

SELECT COUNT(*) AS events,
       COUNT(member_id) AS with_member,
       COUNT(*) - COUNT(member_id) AS anonymous
FROM app_events;
events with_member anonymous
1494723 582098 912625

大多数事件来自没登录的访客。COUNT(DISTINCT member_id) 同样忽略 NULL,所以它返回 33,624 个真实会员,匿名访客不会被算成多出来的一个「会员」。

跳过 NULL 通常正是你想要的,而把 NULL 换成 0 通常是错的。对比一下加不加 COALESCE 时会员的平均年龄:

SELECT ROUND(AVG(2026 - birth_year), 1) AS avg_age_known,
       ROUND(AVG(COALESCE(2026 - birth_year, 0)), 1) AS avg_age_wrong
FROM loyalty_members;
avg_age_known avg_age_wrong
43.2 38

第二列把 6,319 名年龄未知的会员当成了新生儿。只有当 NULL 真的意味着 0 时(比如「这一天没有销售」),才把它换成 0。

零行的 SUM 是 NULL,不是 0

SELECT SUM(amount) AS revenue,
       COALESCE(SUM(amount), 0) AS revenue_zero,
       COUNT(*) AS segments
FROM ticket_flights
WHERE amount > 100000;
revenue revenue_zero segments
NULL 0.00 0

没有哪个航段这么贵,所以没有东西可以求和。零行时 COUNT 返回 0,但 SUM、AVG、MIN 和 MAX 返回 NULL。如果结果要进报表或参与计算,就用 COALESCE(..., 0) 包起来,让「没有收入」显示为 0,而不是空单元格。

COALESCE:第一个非 NULL 的值

COALESCE(a, b, c) 返回第一个不是 NULL 的参数。典型用法是兜底:如果已经起飞就用实际起飞时间,否则用计划起飞时间。

SELECT flight_no, scheduled_departure, actual_departure,
       COALESCE(actual_departure, scheduled_departure) AS best_known_departure
FROM flights
WHERE flight_no = 'PG0405'
  AND scheduled_departure BETWEEN '2026-08-10' AND '2026-08-14'
ORDER BY scheduled_departure;
flight_no scheduled_departure actual_departure best_known_departure
PG0405 2026-08-10 09:35:00 2026-08-10 09:37:00 2026-08-10 09:37:00
PG0405 2026-08-11 09:35:00 2026-08-11 09:38:00 2026-08-11 09:38:00
PG0405 2026-08-12 09:35:00 NULL 2026-08-12 09:35:00
PG0405 2026-08-13 09:35:00 NULL 2026-08-13 09:35:00

所有参数的类型必须兼容。COALESCE(birth_year, 'unknown') 把整数和文本混在一起:PostgreSQL 会拒绝执行,DuckDB 一遇到 NULL 行就会报错「Could not convert string ‘unknown’ to INT32」。先转换类型:

SELECT COALESCE(CAST(birth_year AS VARCHAR), 'unknown') AS birth_year,
       COUNT(*) AS members
FROM loyalty_members
WHERE birth_year IS NULL OR birth_year > 2007
GROUP BY 1
ORDER BY 1;
birth_year members
2008 574
unknown 6319

NULLIF:把某个值变成 NULL

NULLIF(a, b) 在 a = b 时返回 NULL,否则返回 a。它最出名的用途是保护除法:除以 NULL 得到 NULL,而不是报错。

SELECT aircraft_code,
       COUNT(*) AS seats,
       COUNT(*) FILTER (WHERE fare_conditions = 'Business') AS business,
       ROUND(COUNT(*) * 1.0
             / NULLIF(COUNT(*) FILTER (WHERE fare_conditions = 'Business'), 0), 1) AS seats_per_business
FROM seats
GROUP BY aircraft_code
ORDER BY seats DESC;
aircraft_code seats business seats_per_business
773 402 30 13.4
763 222 30 7.4
321 170 28 6.1
320 140 20 7
733 130 12 10.8
319 116 20 5.8
SU9 97 12 8.1
CR2 50 0 NULL
CN1 12 0 NULL

没有 NULLIF 的话,PostgreSQL 会因为两架小飞机报「division by zero」,整个查询停止执行。(DuckDB 在这里不会报错:没有 NULLIF 时,它给 CR2 和 CN1 显示 Infinity,之后在它上面算的任何平均值或总数都会被污染。所以在 DuckDB 里能跑的查询,到了 PostgreSQL 上仍然可能失败,而且在两边都给出奇怪的数字。把 NULLIF 写上。)

第二个用途:清理本该是 NULL 的假值。有些系统在值未知时会存一个占位值,比如 1900 或 0。这份数据里有 148 名会员的出生年份是「1900」:

SELECT COUNT(*) FILTER (WHERE birth_year = 1900) AS placeholder_1900,
       ROUND(AVG(2026 - birth_year), 1) AS avg_age_raw,
       ROUND(AVG(2026 - NULLIF(birth_year, 1900)), 1) AS avg_age_clean
FROM loyalty_members;
placeholder_1900 avg_age_raw avg_age_clean
148 43.2 42.9

一位 126 岁的常旅客不是真实客户。NULLIF 把占位值变成 NULL,AVG 就会像跳过其他缺失值一样跳过它。

GROUP BY 和 ORDER BY 里的 NULL

GROUP BY 是少数把 NULL 当作相等的地方:所有 NULL 归为同一组。

SELECT birth_year - birth_year % 10 AS decade, COUNT(*) AS members
FROM loyalty_members
GROUP BY decade
ORDER BY decade NULLS FIRST;
decade members
NULL 6319
1900 148
1940 47
1950 626
1960 4275
1970 12238
1980 16346
1990 9881
2000 3120

(birth_year - birth_year % 10 在任何数据库里都能向下取整到年代。更直观的 birth_year / 10 * 10 在 PostgreSQL 里能用,因为整数除法会丢掉余数;在 DuckDB 里却不行,因为 / 返回小数,你会得到原来的 1946。)

排序是 PostgreSQL 和 DuckDB 意见不一的地方。PostgreSQL 把 NULL 当作比任何值都大:升序时 NULL 排在最后,降序时排在最前。DuckDB 默认在两个方向上都把 NULL 排在最后。 所以这个查询:

SELECT member_id, birth_year
FROM loyalty_members
WHERE member_id <= 12
ORDER BY birth_year DESC;
member_id birth_year
7 2000
6 1987
2 1983
3 1977
5 1977
9 1973
10 1972
11 1969
1 1967
12 1967
4 NULL
8 NULL

在 DuckDB 里,4 号和 8 号会员排在最下面(如上所示),在 PostgreSQL 里却排在最上面。用 LIMIT 10 查「最年轻的 10 名会员」,在 PostgreSQL 里会返回两个年龄未知的人。只要列可能为 NULL,就显式写上 NULLS FIRST 或 NULLS LAST;两个数据库都支持。

连接里的 NULL

NULL 键永远匹配不上

连接条件本身就是比较,所以任何一边是 NULL,都不会和任何值相等:

SELECT COUNT(*) AS events, COUNT(m.member_id) AS joined
FROM app_events e
LEFT JOIN loyalty_members m ON m.member_id = e.member_id;
events joined
1494723 582098

912,625 条匿名事件找不到会员。如果用内连接,它们会从结果里彻底消失:算「每个会员的事件数」可能没问题,算「每天的事件数」就错得离谱。如果你真的需要两个 NULL 键相互匹配,可以写 ON a.key IS NOT DISTINCT FROM b.key,但要三思:两个匿名访客并不是同一个人。

LEFT JOIN 会产生 NULL

左表的某一行没有匹配时,右表的列会用 NULL 填充。这就是找「没有子记录的父记录」的方法,也是 LEFT JOIN 之后 COUNT(*) 容易误导人的原因:

SELECT a.aircraft_code, a.model,
       COUNT(*) AS count_star,
       COUNT(f.flight_id) AS flights
FROM aircrafts a
LEFT JOIN flights f ON f.aircraft_code = a.aircraft_code
GROUP BY a.aircraft_code, a.model
ORDER BY flights
LIMIT 3;
aircraft_code model count_star flights
320 Airbus A320-200 1 0
773 Boeing 777-300 610 610
763 Boeing 767-300 1221 1221

A320 没有航班,但 LEFT JOIN 仍然为它产生了一行,航班那一侧全是 NULL。COUNT(*) 数到了这一行,说是 1;COUNT(f.flight_id) 只数非 NULL 值,正确地给出 0。LEFT JOIN 之后,要数右表的某一列。

WHERE 陷阱

在 WHERE 里筛选右表,恰好会把 LEFT JOIN 产生的那些 NULL 行扔掉:

SELECT a.aircraft_code, COUNT(f.flight_id) AS cancelled
FROM aircrafts a
LEFT JOIN flights f ON f.aircraft_code = a.aircraft_code
WHERE f.status = 'Cancelled'
GROUP BY a.aircraft_code
ORDER BY cancelled;
aircraft_code cancelled
773 1
321 3
763 6
733 15
319 24
SU9 69
CN1 115
CR2 181

只有 8 种机型,不是 9 种。对 A320 那一行来说,f.status 是 NULL,NULL = 'Cancelled' 是未知,于是 WHERE 把它丢了。把条件挪到 ON 里:

SELECT a.aircraft_code, COUNT(f.flight_id) AS cancelled
FROM aircrafts a
LEFT JOIN flights f ON f.aircraft_code = a.aircraft_code AND f.status = 'Cancelled'
GROUP BY a.aircraft_code
ORDER BY cancelled;
aircraft_code cancelled
320 0
773 1
321 3
763 6
733 15
319 24
SU9 69
CN1 115
CR2 181

现在筛选条件决定的是哪些航班参与连接,每种机型都保留了下来。SQL JOIN 详解和日期序列指南展示了同一个陷阱在其他场景下的表现。

NOT IN 陷阱

x NOT IN (a, b, c) 的意思是 x <> a AND x <> b AND x <> c。只要有一个元素是 NULL,其中一个比较就是未知,整个条件就永远不可能为真:

SELECT 1 IN (1, NULL) AS in_hit,
       1 IN (2, NULL) AS in_miss,
       1 NOT IN (2, NULL) AS not_in_miss,
       1 NOT IN (2, 3) AS not_in_clean;
in_hit in_miss not_in_miss not_in_clean
true NULL NULL true

换成子查询,NULL 就看不见了,情况更糟。哪些会员没有任何 App 事件?

SELECT COUNT(*) AS members_without_events
FROM loyalty_members
WHERE member_id NOT IN (SELECT member_id FROM app_events);
members_without_events
0

子查询返回了几十万个 NULL(那些匿名事件),所以答案是 0。NOT EXISTS 逐行比较,不受影响:

SELECT COUNT(*) AS members_without_events
FROM loyalty_members m
WHERE NOT EXISTS (
  SELECT 1 FROM app_events e WHERE e.member_id = m.member_id
);
members_without_events
19376

找「没有匹配的行」,用 NOT EXISTS(或者 LEFT JOIN ... WHERE right.key IS NULL 这种反连接)。如果坚持用 NOT IN,就在子查询里加上 WHERE member_id IS NOT NULL。更多子查询写法见 SQL 子查询。

CASE 里的 NULL

条件为 NULL 的 CASE 分支会被跳过,所以 NULL 行会落到 ELSE。CASE WHEN birth_year < 1980 THEN 'older' ELSE 'younger' END 会把所有出生年份未知的会员都归到「younger」里。要先写 WHEN birth_year IS NULL THEN 'unknown'。SQL CASE WHEN 用法里用真实数字演示了这一点。

检查清单

常见问题

为什么 WHERE column = NULL 一行都查不到?

因为任何值和 NULL 比较的结果都是 NULL(未知),而不是真,WHERE 只保留条件为真的行。要写 WHERE column IS NULL。

COUNT(*) 和 COUNT(column) 有什么区别?

COUNT(*) 数行数。COUNT(column) 只数该列不为 NULL 的行。在 app_events 里,两者相差 912,625,也就是没有会员的那些事件。

COALESCE 和 NULLIF 有什么区别?

它们正好相反。COALESCE(a, b) 把 NULL 换成一个值。NULLIF(a, b) 在 a 等于 b 时把值换成 NULL,典型用法是在除法前写 NULLIF(x, 0)。

NULL 等于空字符串吗?

不等于。在 PostgreSQL 和 DuckDB 里,'' 是一个长度为零的真实值,'' IS NULL 为假。Oracle 是例外:它把空字符串当作 NULL。

SQL 里 NULL 怎么排序?

取决于数据库。PostgreSQL 升序时把 NULL 排在最后,降序时排在最前;DuckDB 在两个方向上都排在最后。在支持的数据库里写上 NULLS FIRST 或 NULLS LAST,就能在各处得到相同的结果。

其他语言: English, Español, Português (Brasil), Tiếng Việt