连接键为 NULL 会怎样
NULL 与任何值比较的结果都是 UNKNOWN,而不是 TRUE,NULL = NULL 同样不成立——因为每个 NULL 表示「未知」,两个未知不能认定为同一个值。后果很直接:连接键含 NULL 的行在内连接里直接消失,在左连接里左表行保留、右表列补 NULL,而且怎么都配不上。「关联数据莫名少了一大部分」这类问题,绝大多数源头就是连接键为空。
基本信息
| 内连接 | NULL 键的行被丢弃,不再出现在结果中 |
|---|---|
| 左连接 | 左表 NULL 键行保留,右表列全部补 NULL |
| 判空写法 | 必须用 IS NULL / IS NOT NULL,写 = NULL 永远为 UNKNOWN |
| NULL 安全匹配 | MySQL 用 <=>;PostgreSQL 用 IS NOT DISTINCT FROM |
| 设计建议 | 连接键尽量设为 NOT NULL,或用统一的哨兵值表示未知 |
典型使用场景
- 排查关联结果行数比预期少:先统计连接键的 NULL 比例
- 左连接后统计子表数据:注意 COUNT(*) 与 COUNT(子表主键) 的口径差异
- 反连接查询:NULL 键行会被误判成「右表不存在」,需要额外排除
- 表设计评审:决定连接键是否允许为 NULL、是否使用哨兵值
- 数据清洗:把无法归属的外键补上默认维度行,再保证连接结果完整
两种连接的行为差异
两种连接的行为差异
-- 内连接:a.k 为 NULL 的行不会出现在结果里
SELECT a.id, b.name
FROM a JOIN b ON a.k = b.k;
-- 左连接:NULL 键行保留,右表列补 NULL
SELECT a.id, b.name
FROM a LEFT JOIN b ON a.k = b.k;
-- MySQL 的 NULL 安全等值:NULL 与 NULL 视为相同
SELECT a.id, b.id
FROM a JOIN b ON a.k <=> b.k;PostgreSQL 写法与反连接
PostgreSQL 写法与反连接
-- PostgreSQL 没有 <=>,改用 IS NOT DISTINCT FROM
SELECT a.id, b.id
FROM a JOIN b ON a.k IS NOT DISTINCT FROM b.k;
-- 反连接:必须显式排除 NULL 键行,否则它们会被算成「右表不存在」
SELECT a.id
FROM a LEFT JOIN b ON a.k = b.k
WHERE b.k IS NULL AND a.k IS NOT NULL;NULL 比较三值逻辑
| 表达式 | 结果 | 说明 |
|---|---|---|
| NULL = NULL | UNKNOWN | 不是 TRUE,WHERE 中该行被丢弃 |
| NULL <> 1 | UNKNOWN | 取 NOT 之后仍是 UNKNOWN |
| a.k <=> b.k | TRUE / FALSE | MySQL 的 NULL 安全等值 |
| a.k IS NULL | TRUE / FALSE | 判断空值的唯一正确写法 |
左连接后统计子表行数要用 COUNT(子表主键) 而不是 COUNT(*):后者会把没匹配上的补 NULL 行也算成 1,指标会明显虚高。
左连接后补 NULL 的行还会连带影响过滤:想限制「右表满足某条件」时必须把条件写在 ON 里,写到 WHERE 会把补 NULL 的行一起筛掉,左连接退化成内连接。
两边都用 NULL 表示「未知归属」,连接关系永远对不上。先做数据归一化(补哨兵值或补一条默认维度行),再谈查询优化,否则只是把问题藏起来。