NULL的各种坑,90%的人至少踩过一个
本文目录
总结
| 陷阱 | 正确做法 |
|---|---|
| ------ | --------- |
| column = NULL 查不出数据 | 用 IS NULL |
| WHERE col != 'x' 漏掉NULL行 | 加 OR col IS NULL |
| 算术运算产生意外NULL | 用 COALESCE(col, 0) 兜底 |
| AVG忽略NULL导致结果偏大 | 视业务决定是否 COALESCE |
| NOT IN遇到NULL返回空集 | 用 NOT EXISTS 替代 |
| JOIN Key为NULL不匹配 | 提前过滤NULL |
| NULL排序位置不确定 | 显式指定 NULLS LAST/FIRST |
一句话总结:写SQL的时候,每看到一个列,先问自己"这列会不会有NULL?如果有NULL会怎样?"养成这个习惯,你的SQL准确性会提升一个档次。
明天 DAY 13 聊多表JOIN的执行顺序和优化,当你的SQL里有3张甚至5张表JOIN在一起的时候,顺序不同性能差别巨大。
你被NULL坑过最离谱的经历是什么?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
NULL是SQL里最容易被忽视但杀伤力最大的"暗雷"。你可能SQL写了好几年,但NULL的某些行为还是会让你翻车。今天把NULL相关的面试考点和实战陷阱一网打尽。
先搞清楚:NULL不是值,是"未知"
NULL不等于0,不等于空字符串'',不等于任何东西。NULL表示的是"不知道"。
这导致了SQL中一个反直觉的设计——三值逻辑:TRUE、FALSE、UNKNOWN。
-- 这些表达式的结果都是UNKNOWN,不是TRUE也不是FALSE
SELECT 1 = NULL; -- UNKNOWN
SELECT NULL = NULL; -- UNKNOWN ← 这个最反直觉
SELECT NULL != NULL; -- UNKNOWN
SELECT NULL > 0; -- UNKNOWN
NULL = NULL的结果不是TRUE。两个"不知道"的东西,你不能说它们相等。
面试官经常这么问:"WHERE column = NULL能查出NULL值吗?"答案是不能,必须用IS NULL。
陷阱一:WHERE条件过滤NULL
-- 表数据
| user_id | age |
|---------|------|
| u001 | 25 |
| u002 | NULL |
| u003 | 30 |
| u004 | NULL |
-- 查年龄不是25的用户
SELECT * FROM users WHERE age != 25;
-- 结果只有u003(age=30),NULL的行不会出现!
age != 25对于NULL行的结果是UNKNOWN,WHERE只保留TRUE的行,UNKNOWN和FALSE一样被过滤掉了。
如果你想要"年龄不是25的,包括不知道年龄的",要这么写:
SELECT * FROM users WHERE age != 25 OR age IS NULL;
-- 或者用NULL安全的比较
SELECT * FROM users WHERE NOT (age <=> 25); -- MySQL的<=>运算符
我在做广告归因数据的时候遇到过类似的问题。投放团队说"帮我查一下不是Facebook渠道的所有转化",我写了WHERE channel != 'Facebook',结果漏掉了所有自然流量(channel为NULL),被投放同学追着问了半天。
陷阱二:NULL的算术运算
任何值和NULL做算术运算,结果都是NULL:
SELECT 100 + NULL; -- NULL
SELECT 100 * NULL; -- NULL
SELECT NULL / 0; -- NULL(不是报错!)
这在计算指标的时候特别危险:
-- 计算ROI:revenue / cost
SELECT
channel,
SUM(revenue) AS total_revenue,
SUM(cost) AS total_cost,
SUM(revenue) / SUM(cost) AS roi -- 如果某渠道没有revenue记录,会怎样?
FROM ad_metrics
GROUP BY channel
如果某个渠道只有cost没有revenue,SUM(revenue)是NULL(不是0),整个ROI也变成NULL。更隐蔽的是,如果revenue列有NULL值混在里面,SUM会自动忽略NULL,但这可能不是你想要的行为。
解法:用COALESCE兜底。
SELECT
channel,
COALESCE(SUM(revenue), 0) AS total_revenue,
SUM(cost) AS total_cost,
ROUND(
COALESCE(SUM(revenue), 0) / NULLIF(SUM(cost), 0),
4
) AS roi
FROM ad_metrics
GROUP BY channel
COALESCE(x, 0)的意思是"如果x是NULL就返回0"。NULLIF(SUM(cost), 0)是"如果cost和为0就返回NULL",避免除以0。COALESCE和NULLIF这对组合,是处理NULL和除零问题的标配。
陷阱三:聚合函数对NULL的处理
不同的聚合函数对NULL的态度不一样:
-- 数据
| score |
|-------|
| 80 |
| NULL |
| 90 |
| NULL |
| 70 |
SELECT
COUNT(*) AS cnt_star, -- 5(计所有行)
COUNT(score) AS cnt_col, -- 3(只计非NULL)
SUM(score) AS sum_val, -- 240(忽略NULL)
AVG(score) AS avg_val, -- 80(240/3,不是240/5!)
MAX(score) AS max_val, -- 90(忽略NULL)
MIN(score) AS min_val -- 70(忽略NULL)
FROM exam
最容易出问题的是AVG:它计算的是非NULL值的平均值。如果你期望NULL按0处理来算平均,需要自己转换:
-- NULL按0算的平均值
SELECT AVG(COALESCE(score, 0)) AS avg_with_zero -- 240/5 = 48
FROM exam
DAY 6讲COUNT的时候提到过COUNT(*)和COUNT(column)的区别,根源就在NULL的处理上。这是SQL里最基础也最容易混的知识点。
陷阱四:JOIN中的NULL
NULL值无法通过等值JOIN匹配:
-- 左表
| user_id |
|---------|
| u001 |
| NULL |
-- 右表
| user_id | name |
|---------|------|
| u001 | Tom |
| NULL | ??? |
SELECT *
FROM left_table a
JOIN right_table b ON a.user_id = b.user_id
-- 结果只有u001这一行!两个NULL不会匹配上
因为NULL = NULL是UNKNOWN,不是TRUE,所以JOIN条件不成立。
这在实际工作中的影响:DAY 8讲大表JOIN优化的时候说过,JOIN Key有大量NULL会导致这些行白白参与Shuffle但不产生任何结果,浪费资源。所以JOIN之前先过滤NULL是基本操作。
陷阱五:ORDER BY中NULL的排序位置
SELECT * FROM users ORDER BY age ASC;
-- NULL排在最前面还是最后面?
答案是:取决于引擎。
MySQL / Spark / Hive:NULL排在最前面(ASC时)
PostgreSQL / Oracle:NULL排在最后面(ASC时)
如果你需要确定NULL的排序位置,显式指定:
-- 标准SQL写法(PostgreSQL/Oracle支持)
SELECT * FROM users ORDER BY age ASC NULLS LAST;
-- MySQL/Hive的workaround
SELECT * FROM users ORDER BY CASE WHEN age IS NULL THEN 1 ELSE 0 END, age ASC;
我做报表的时候踩过这个坑。给运营出一张用户消费排行榜,没处理NULL排序,结果排行榜第一页全是"暂无消费"的NULL用户,真正的高消费用户被挤到后面去了。
陷阱六:DISTINCT和GROUP BY对NULL的处理
好消息是,DISTINCT和GROUP BY会把所有NULL归为一组:
SELECT DISTINCT channel FROM ad_metrics;
-- 如果有多行channel为NULL,只返回一个NULL
SELECT channel, COUNT(*) FROM ad_metrics GROUP BY channel;
-- 所有channel为NULL的行会被归为一组
这个行为是合理的,但要注意它和NULL != NULL(UNKNOWN)的逻辑是不一致的。SQL标准在这里做了特殊处理——如果DISTINCT也遵循三值逻辑,那每个NULL都是独一无二的,DISTINCT就没法去重了。
面试必会:NULL相关函数全家桶
-- 1. IS NULL / IS NOT NULL:判断NULL的唯一正确方式
WHERE column IS NULL
WHERE column IS NOT NULL
-- 2. COALESCE:返回第一个非NULL值
COALESCE(a, b, c) -- 如果a是NULL就看b,b也是NULL就看c
-- 3. NULLIF:相等时返回NULL
NULLIF(a, 0) -- 如果a=0,返回NULL(常用于防除零)
-- 4. IFNULL / NVL:两参数版的COALESCE
IFNULL(a, 0) -- MySQL
NVL(a, 0) -- Oracle/Hive
-- 5. CASE WHEN:最灵活的NULL处理
CASE WHEN column IS NULL THEN '未知' ELSE column END
工作中我用得最多的是COALESCE和NULLIF的组合,基本上所有涉及除法的指标计算都要用到:
ROUND(
COALESCE(SUM(clicks), 0) * 100.0 / NULLIF(SUM(impressions), 0),
2
) AS ctr
这行代码翻译成人话就是:"点击数如果为NULL就当0,曝光量如果为0就不算CTR(返回NULL而不是报错)"。