INNER JOIN 会静默过滤所有连接字段含 NULL 的行,因 NULL = NULL 返回 UNKNOWN 而非 TRUE,故不满足匹配条件;LEFT JOIN 虽保留左表行,但右表 NULL 字段在 SUM/COUNT 中仍被跳过,需用 COALESCE 显式补值。

INNER JOIN 里 NULL 直接丢数据,不是“没连上”,是根本没进结果集
只要 ON 条件里任一字段为 NULL,整行就从 INNER JOIN 结果中消失——数据库不报错、不警告,只静默过滤。比如订单表 orders.user_id 有 400 个 NULL,而用户表 users.id 没这些值,那这 400 条订单在 INNER JOIN 后就彻底没了,COUNT(*) 会比原始订单数少 400。
常见错误写法:SELECT COUNT(*) FROM orders o INNER JOIN users u ON o.user_id = u.id,你以为在查“已关联用户”的订单数,实际查的是“user_id 非 NULL 且能匹配到用户”的订单数——中间漏掉了所有 user_id IS NULL 的记录,也漏掉了 user_id 有值但用户表里不存在的记录。
- 排查方法:先单独查
SELECT COUNT(*) FROM orders WHERE user_id IS NULL,看有多少行被隐式丢弃 - 如果业务上
user_id IS NULL表示“游客下单”,那必须用LEFT JOIN,而不是强行改INNER JOIN - 别指望后续
WHERE能捞回来——INNER JOIN执行完,这些行已经不在结果集里了
LEFT JOIN 后字段为 NULL,SUM/COUNT 仍会跳过,不是“保留了”就等于“能算”
LEFT JOIN 确实保留左表所有行,但右表字段(如 coupon_discount)为 NULL 时,SUM(coupon_discount) 依然忽略它们——结果不是“0”,而是少算了这部分本该计为 0 的金额。
典型场景:订单主表 LEFT JOIN 优惠券表,未用券订单的 coupon_discount 是 NULL,但财务口径要求“没用券 = 折扣为 0”。这时写 SUM(coupon_discount) 得到的是“已用券订单的折扣总和”,不是“全部订单的折扣总和”。
- 正确写法:
SUM(COALESCE(coupon_discount, 0))—— 先把每行NULL补成 0,再加总 - 错误写法:
COALESCE(SUM(coupon_discount), 0)—— 先加总,发现全NULL就返 0,但中间已漏掉几百行 - 更隐蔽的问题:
SUM(total - discount)中任意一列是NULL,整行计算结果变NULL,得拆成SUM(COALESCE(total, 0) - COALESCE(discount, 0))
ON 条件里写 = 永远无法让两个 NULL 匹配成功
NULL = NULL 在 SQL 中不返回 TRUE,而是 UNKNOWN;而 JOIN 只保留条件为 TRUE 的行。所以即使两表都有 work_type IS NULL 的记录,ON t1.work_type = t2.work_type 也不会把它们连起来。
后果很直接:这类记录在 LEFT JOIN 中右表字段全为 NULL,后续聚合(如 SUM(t2.metric))照样跳过——你看到的“空”不是数据缺失,是匹配逻辑失效。
- 安全写法(通用):
ON (t1.col = t2.col) OR (t1.col IS NULL AND t2.col IS NULL) - 简洁写法(PostgreSQL / SQL Server 2022+):
ON t1.col IS NOT DISTINCT FROM t2.col - 避免用
COALESCE(t1.col, 'N/A') = COALESCE(t2.col, 'N/A')—— 如果真实数据里真有'N/A',就会误匹配
GROUP BY 和 WHERE 对 NULL 的处理完全不同,混用容易漏行
GROUP BY col 会把所有 col IS NULL 的行归为一组,看起来“统一处理”了;但一旦你在 WHERE 里写 col != 'active',col IS NULL 的行也会被过滤掉——因为 NULL != 'active' 是 UNKNOWN,不满足 WHERE 的 TRUE 要求。
最麻烦的是:同一列里 NULL 可能代表不同意思。比如 status 列中,有些 NULL 是“待审核”,有些是“已取消”,有些是“ETL 失败未写入”。这时候统一 COALESCE(status, 'unknown') 看似省事,实则掩盖了数据质量问题,后续按状态分组统计时偏差不可逆。
- 查清 NULL 分布:
SELECT status, COUNT(*) FROM orders GROUP BY status,确认NULL是否真属于同一语义 - WHERE 中判断 NULL 必须显式:
WHERE status IS NULL OR status = 'draft',不能靠!=或IN - 聚合前先分类清洗:
CASE WHEN status IS NULL AND created_at > '2025-01-01' THEN 'pending' ELSE status END,比一刀切COALESCE更贴近业务
真正难的不是写 COALESCE,是搞清楚每一处 NULL 在当前上下文中到底代表什么——是缺失、未发生、不适用,还是上游流程出错了。补 0 容易,定性难。

















