📑 本页目录(点开跳转)
04 · NULL 与三值逻辑
⏱ 108 分钟 | ⭐ 实测:NOT IN ('BJ', NULL) 返回空集——一行都没有,且不报错
🎯 一句话
NULL 不是一个值,是「不知道」;任何跟它比较的结果都不是真也不是假,而是第三种真值 UNKNOWN——而 WHERE 只放行判为真的行。
这一条是本教程里「不报错的错」的总源头:一条语法完全正确、跑得飞快、连警告都没有的查询,可以把整个结果集静静地清空。
⚠️ 零、先划清一件事:这不是「缺失值处理」
站内有一章叫《数据这一关》· 05 缺失不是一种东西,它讲的是统计学意义上的缺失:这个值为什么没有(MCAR / MAR / MNAR)、能不能填、填了会不会把偏差带进模型。
这一章讲的是完全不同的另一件事:值已经缺在库里了,NULL 这个东西在 SQL 里怎么参与运算和比较。
| 那一章问 | 这一章问 | |
|---|---|---|
| 题目 | 这个值为什么没有?填成什么? | 已经是 NULL 了,WHERE/SUM/JOIN 会怎么对待它? |
| 判据 | 缺失机制(MCAR/MAR/MNAR) | 三值逻辑(TRUE/FALSE/UNKNOWN) |
| 出错的样子 | 填错了,系数偏了 | 查询静默少了一批行,数据本身一个字没错 |
⚠️ 同名不同物,别混。 你可以在缺失机制上做得完全正确,仍然被本章这些坑打中——因为它们发生在你写 SQL 的那一刻,跟数据怎么来的没有半点关系。
⭐ 顺带一提,《数据这一关》· 04 脏数据的十种形态讲了一种更坏的东西:哨兵值(
-999、"N/A"、1970-01-01)——它们不是NULL,所以所有缺失率监控都是全绿的。 相比之下NULL至少是诚实的:它明说自己不知道。本章讲的是这份诚实要你付出的代价。
🚦 一、招牌事故:一个 NULL 清空整个结果集
一张 5 行的用户表,一张 2 行的「总部所在城市」表——其中一行的城市还没录:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE users(id INTEGER PRIMARY KEY, city TEXT)")
db.executemany("INSERT INTO users VALUES(?,?)", [
(1, "BJ"), (2, "SH"), (3, None), (4, "SH"), (5, "GZ"),
])
db.execute("CREATE TABLE hq(city TEXT)")
db.executemany("INSERT INTO hq VALUES(?)", [("BJ",), (None,)]) # ⭐ 混进了一行 NULL
print("NOT IN 字面量:",
db.execute("SELECT id FROM users WHERE city NOT IN ('BJ', NULL)").fetchall())
print("NOT IN 子查询:",
db.execute("SELECT id FROM users WHERE city NOT IN (SELECT city FROM hq)").fetchall())
print("NOT EXISTS:",
db.execute("SELECT id FROM users u WHERE NOT EXISTS "
"(SELECT 1 FROM hq h WHERE h.city = u.city)").fetchall())
print("IN 子查询(对照):",
db.execute("SELECT id FROM users WHERE city IN (SELECT city FROM hq)").fetchall())
实跑输出:
NOT IN 字面量: []
NOT IN 子查询: []
NOT EXISTS: [(2,), (3,), (4,), (5,)]
IN 子查询(对照): [(1,)]
💀 NOT IN 返回了空集。 没有异常,没有警告,sqlite3 高高兴兴地给你一个 []。
如果这条查询挂在一个「非总部城市的用户列表」报表上,你看到的就是「今天没有这样的用户」——而真实答案是四个。
⚠️ 注意最后一行的对照:IN 完全正常,返回了 (1,)。坏掉的只有加了 NOT 的那一半。
这就是它这么难发现的原因:你测 IN 的时候一切正常,NOT IN 是同一个写法反过来,谁会怀疑它?
⭐ 只要 NOT IN 的右边(字面量列表或子查询结果)里出现哪怕一个 NULL,整条 NOT IN 就永远判不出真,结果集必然为空。 为什么,看下一节。
🧩 二、三值逻辑:多出来的那个 UNKNOWN
普通编程语言里布尔值有两个:真、假。SQL 有三个。
NULL 的含义不是「空」也不是「零」,是「这里有个值,但我不知道它是多少」。
于是「不知道的那个值等于 5 吗」这个问题,唯一诚实的回答是:不知道。
import sqlite3
db = sqlite3.connect(":memory:")
def q(sql):
v = db.execute("SELECT " + sql).fetchone()[0]
return {None: "UNKNOWN", 1: "TRUE", 0: "FALSE"}[v]
for e in ["NULL = NULL", "NULL <> NULL", "NULL > 1", "NULL = 0",
"NULL IS NULL", "NULL IS NOT NULL"]:
print(f"{e:<18} -> {q(e)}")
print("--- AND / OR / NOT ---")
for a in ["1", "0", "NULL"]:
for b in ["1", "0", "NULL"]:
print(f"{a:>4} AND {b:<5} -> {q(f'{a} AND {b}'):<8}"
f"{a:>4} OR {b:<5} -> {q(f'{a} OR {b}')}")
for a in ["1", "0", "NULL"]:
print(f"NOT {a:<5} -> {q('NOT ' + a)}")
实跑输出:
NULL = NULL -> UNKNOWN
NULL <> NULL -> UNKNOWN
NULL > 1 -> UNKNOWN
NULL = 0 -> UNKNOWN
NULL IS NULL -> TRUE
NULL IS NOT NULL -> FALSE
--- AND / OR / NOT ---
1 AND 1 -> TRUE 1 OR 1 -> TRUE
1 AND 0 -> FALSE 1 OR 0 -> TRUE
1 AND NULL -> UNKNOWN 1 OR NULL -> TRUE
0 AND 1 -> FALSE 0 OR 1 -> TRUE
0 AND 0 -> FALSE 0 OR 0 -> FALSE
0 AND NULL -> FALSE 0 OR NULL -> UNKNOWN
NULL AND 1 -> UNKNOWN NULL OR 1 -> TRUE
NULL AND 0 -> FALSE NULL OR 0 -> UNKNOWN
NULL AND NULL -> UNKNOWN NULL OR NULL -> UNKNOWN
NOT 1 -> FALSE
NOT 0 -> TRUE
NOT NULL -> UNKNOWN
⭐ 第一行就是全章的地基:NULL = NULL 判出来是 UNKNOWN,不是真。
两个「不知道」凑在一起,仍然不知道它们是不是同一个东西。
⭐ 但 UNKNOWN 不会污染一切,看真值表里这两行:
| 式子 | 结果 | 为什么 |
|---|---|---|
0 AND NULL |
FALSE | 一个连乘里有个零,另一项是几都不影响 |
1 OR NULL |
TRUE | 已经有一项为真,另一项是几都不影响 |
⭐ 判据:只有当「那个未知值到底是多少」会改变答案时,结果才是 UNKNOWN。
这不是随便定的规则,是「不知道」这个语义的直接推论——把它当成推论去理解,比背九宫格可靠得多。
⚠️ 算术和字符串也一样被吞:1 + NULL、0 * NULL、'abc' || NULL、length(NULL)、upper(NULL) 实跑全部返回 None。
注意 0 * NULL 也是 NULL——数学上零乘任何数都是零,但 SQL 不做这个推理,⭐ 只有布尔运算享受上面那两条短路优待。
现在回头看第一节:city NOT IN ('BJ', NULL) 会被展开成
NOT (city = 'BJ' OR city = NULL)
对 city='SH' 这一行:('SH'='BJ') OR ('SH'=NULL) → FALSE OR UNKNOWN → UNKNOWN(正是上表第 6 行),再 NOT 一下还是 UNKNOWN。
每一行都是 UNKNOWN,一行都放不出来。
⭐ 而 IN 没事,因为它展开成 city='BJ' OR city=NULL——对 city='BJ' 那行是 TRUE OR UNKNOWN → TRUE(上表第 3 行),照样放行。
💀 NOT 把「有一项为真就够了」变成了「必须每一项都为假」,而「为假」正是 UNKNOWN 永远给不出的。
🧯 三、WHERE 只放行 TRUE——排中律在这里失效
01 章说「WHERE 逐行判断,扔掉不要的行」。⭐ 更准确的说法是:WHERE 只保留判为 TRUE 的行,FALSE 和 UNKNOWN 一起扔。
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, v INTEGER)")
db.executemany("INSERT INTO t VALUES(?,?)", [(1, 5), (2, None), (3, 20)])
a = db.execute("SELECT id FROM t WHERE v > 10").fetchall()
b = db.execute("SELECT id FROM t WHERE NOT (v > 10)").fetchall()
print("WHERE v > 10 :", a)
print("WHERE NOT (v > 10) :", b)
print("两边相加:", len(a) + len(b), "行 / 全表 3 行")
print("WHERE v > 10 OR NOT (v > 10):",
db.execute("SELECT id FROM t WHERE v > 10 OR NOT (v > 10)").fetchall())
print("= NULL 写法 :", db.execute("SELECT id FROM t WHERE v = NULL").fetchall())
print("IS NULL 写法:", db.execute("SELECT id FROM t WHERE v IS NULL").fetchall())
实跑输出:
WHERE v > 10 : [(3,)]
WHERE NOT (v > 10) : [(1,)]
两边相加: 2 行 / 全表 3 行
WHERE v > 10 OR NOT (v > 10): [(1,), (3,)]
= NULL 写法 : []
IS NULL 写法: [(2,)]
💀💀 一个条件和它的否定,加起来不等于全表。 3 行的表,两边各拿走 1 行,有 1 行谁都没要。
连 WHERE v > 10 OR NOT (v > 10) 这种看上去必然恒真的条件,都漏掉了 id=2。
⚠️ 这条在业务上非常贵:「大额订单」和「非大额订单」两张报表加起来对不上总数,两边的 SQL 都是对的,两边的开发都查不出问题。
⭐ 判据:任何时候你把一个集合按条件劈成两半,如果那一列可能有 NULL,你就必须显式接管第三种情况(... OR v IS NULL)。
IS NULL 为什么不能写成 = NULL
看实跑:WHERE v = NULL 返回 [],WHERE v IS NULL 返回 [(2,)]。
因为 = 是比较运算,比较两个未知量得到 UNKNOWN(第二节第一行),WHERE 不放行。
而 IS NULL 根本不是比较,它是一个判定式:问的不是「这个值等于什么」,而是「这个格子里有没有值」——这个问题永远有明确答案,所以只会给出 TRUE 或 FALSE。
⭐ 整章最值得记住的一句:
=问「值是多少」,IS NULL问「有没有值」。 前一个问题在没有值的时候无法回答,后一个永远能回答。
⚠️ 一个反直觉的例外:CHECK 约束放行 UNKNOWN
ON、HAVING 跟 WHERE 一样只认 TRUE,但约束是反过来的:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE c(id INTEGER, age INTEGER CHECK (age >= 18))")
db.execute("INSERT INTO c VALUES(1, 30)")
for label, age in [("age=10", 10), ("age=NULL", None)]:
try:
db.execute("INSERT INTO c VALUES(?,?)", (99, age))
print(f" {label:<9} 插入成功")
except Exception as e:
print(f" {label:<9} ->", type(e).__name__, ":", e)
print(" 表里最终:", db.execute("SELECT id, age FROM c").fetchall())
实跑输出:
age=10 -> IntegrityError : CHECK constraint failed: age >= 18
age=NULL 插入成功
表里最终: [(1, 30), (99, None)]
⭐ CHECK 的规则是「只在判为 FALSE 时拒绝」——UNKNOWN 放行。
💀 所以 CHECK (age >= 18) 挡不住 NULL。要真的挡住,得写 CHECK (age IS NOT NULL AND age >= 18),或者给列加 NOT NULL。
⭐ 两条规则记成一对:查询要求「证明为真」,约束要求「证明为假」,UNKNOWN 在两边的待遇正好相反。
🩹 四、反连接的三种写法,只有一种不会被毒化
「A 表里、B 表里没有的那些」叫反连接。三种常见写法在有 NULL 时给出三个不同答案:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE users(id INTEGER PRIMARY KEY, city TEXT)")
db.executemany("INSERT INTO users VALUES(?,?)", [
(1, "BJ"), (2, "SH"), (3, None), (4, "SH"), (5, "GZ")])
db.execute("CREATE TABLE hq(city TEXT)")
db.executemany("INSERT INTO hq VALUES(?)", [("BJ",), (None,)])
print("A. NOT IN :",
db.execute("SELECT id FROM users WHERE city NOT IN (SELECT city FROM hq)").fetchall())
print("B. NOT EXISTS :",
db.execute("SELECT id FROM users u WHERE NOT EXISTS "
"(SELECT 1 FROM hq h WHERE h.city = u.city)").fetchall())
print("C. LEFT JOIN IS NULL :",
db.execute("SELECT u.id FROM users u LEFT JOIN hq h ON h.city = u.city "
"WHERE h.city IS NULL").fetchall())
print("D. NOT IN + 内层过滤 :",
db.execute("SELECT id FROM users WHERE city NOT IN "
"(SELECT city FROM hq WHERE city IS NOT NULL)").fetchall())
print("--- 子查询为空表时 ---")
db.execute("CREATE TABLE empty_hq(city TEXT)")
print("NOT IN 空表:",
db.execute("SELECT id FROM users WHERE city NOT IN (SELECT city FROM empty_hq)").fetchall())
实跑输出:
A. NOT IN : []
B. NOT EXISTS : [(2,), (3,), (4,), (5,)]
C. LEFT JOIN IS NULL : [(2,), (3,), (4,), (5,)]
D. NOT IN + 内层过滤 : [(2,), (4,), (5,)]
--- 子查询为空表时 ---
NOT IN 空表: [(1,), (2,), (3,), (4,), (5,)]
三种写法 + 一个补救版,跑出三个不同答案。 逐个解释:
| 写法 | 结果 | 发生了什么 |
|---|---|---|
A NOT IN |
空 💀 | 内层的 NULL 让每一行都判成 UNKNOWN |
B NOT EXISTS |
4 行 | ⭐ 不被毒化:EXISTS 只问「有没有配上的行」,h.city = u.city 判 UNKNOWN 就是没配上,是个明确的「否」 |
C LEFT JOIN … IS NULL |
4 行 | 和 B 一致(02 章第二节那个合法例外) |
D NOT IN + 内层滤掉 NULL |
3 行 | 内层干净了,但 ⚠️ 外层的 id=3 自己是 NULL,NULL NOT IN ('BJ') 仍是 UNKNOWN,被扔了 |
⭐ B 和 D 差的那一行(id=3)不是 bug,是两个不同的问题。
id=3 这个用户的城市未知——他到底在不在总部城市?NOT EXISTS 的口径是「配不上就算不在」,NOT IN 的口径是「不知道就不给结论」。
这是业务判断,你得自己决定要哪个,但你至少要知道自己选了哪个。
⭐ 默认结论:反连接一律写 NOT EXISTS。 它不需要你去操心内层有没有 NULL,而 NOT IN 需要你记得加 WHERE ... IS NOT NULL——而人是会忘的,尤其是在那一列今天还没有 NULL 的时候。
⚠️ 最后一个实跑值得单独看:子查询返回空表时,
NOT IN把 5 行全部放行(包括城市为NULL的id=3)。 这是对的——「不在一个空集合里」对任何东西都成立。⭐ 但它意味着NOT IN的行为在「内层空」和「内层有一个NULL」之间是从全放到全不放的跳变,而这两种情况在数据上只差一行。
⚠️ NOT IN 到底该不该完全禁用? 不必——NOT IN 接一串写死的字面量(status NOT IN ('paid','refunded'))是安全常见的写法,只要那串字面量里没有 NULL。⭐ 危险的只有「右边是子查询、而那一列可空」这一种组合。
🛑 读到这里可以停 —— 前半章讲完了(约 42 分钟)。 后半章还有:那些
NULL行,到底是在哪一步没的 · ⭐ SQL 里其实有两套NULL语义 · 日常工具:COALESCE/NULLIF/ 空串不是NULL回来的时候不用重读,直接从下一节接着看就行。
🧮 五、那些 NULL 行,到底是在哪一步没的
⚠️ 先划界:聚合函数怎么对待 NULL(只有 COUNT(*) 数行、其余都数值,AVG 的分母是 COUNT(score) 不是 COUNT(*),空集上 SUM 给 NULL 要用 COALESCE 兜底)——这一整套是 03 章的正题,那里讲得比这里细,本节不重讲。
本节只回答一个 NULL 语义的问题:一份数据里有一批行的键是 NULL,它们是在哪一步从你的看板上消失的?
⭐ 答案很反直觉——不是 GROUP BY:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE calls(id INTEGER PRIMARY KEY, city TEXT, cents REAL)")
db.executemany("INSERT INTO calls VALUES(?,?,?)", [
(1, "BJ", 1.0), (2, "SH", 2.0), (3, None, 4.0), (4, "BJ", 8.0), (5, None, 16.0)])
db.execute("CREATE TABLE dim_city(code TEXT, name TEXT)")
db.executemany("INSERT INTO dim_city VALUES(?,?)", [("BJ", "北京"), ("SH", "上海")])
print("全表 SUM =", db.execute("SELECT SUM(cents) FROM calls").fetchone()[0])
print("GROUP BY :", db.execute("SELECT city, SUM(cents) FROM calls GROUP BY city").fetchall())
print("--- 换成下面任意一步,NULL 那 20.0 就没了(都不报错)---")
print("a) INNER JOIN 维表 :",
db.execute("SELECT SUM(c.cents) FROM calls c JOIN dim_city d ON d.code=c.city").fetchone()[0])
print("b) WHERE city <> 'BJ' :",
db.execute("SELECT SUM(cents) FROM calls WHERE city <> 'BJ'").fetchone()[0],
" <- 期望 2+4+16=22")
print("c) WHERE city IN (…) :",
db.execute("SELECT SUM(cents) FROM calls WHERE city IN ('BJ','SH')").fetchone()[0])
print("b 补上 NULL 之后 :",
db.execute("SELECT SUM(cents) FROM calls WHERE city IS NULL OR city <> 'BJ'").fetchone()[0])
实跑输出:
全表 SUM = 31.0
GROUP BY : [(None, 20.0), ('BJ', 9.0), ('SH', 2.0)]
--- 换成下面任意一步,NULL 那 20.0 就没了(都不报错)---
a) INNER JOIN 维表 : 11.0
b) WHERE city <> 'BJ' : 2.0 <- 期望 2+4+16=22
c) WHERE city IN (…) : 11.0
b 补上 NULL 之后 : 22.0
⭐ GROUP BY 一分钱没丢:三组加起来正好 31.0,NULL 老老实实自成一组,占 20.0——接近全表的三分之二。
(GROUP BY 把所有 NULL 收进同一组,用的是本章第六节要讲的「另一套语义」。)
💀 丢在后面三步,而且都不报错:
| 那一步 | 结果 | 为什么 |
|---|---|---|
a 连维表(INNER JOIN) |
31.0 → 11.0 | 连接键 NULL 配不上对(02 章第六节),那两行被内连接扔了 |
b WHERE city <> 'BJ' |
应是 22.0,实得 2.0 | ⚠️ NULL <> 'BJ' 判 UNKNOWN,WHERE 不放行——一个「不等于」吞掉 20.0 |
c WHERE city IN (…) |
11.0 | 白名单同理,NULL 不在任何名单里 |
⚠️ b 这一格最值得盯:写的人想表达「除了北京的都算上」,实际算出来的是「城市已知且不是北京的」——少了 20.0,占全表 64.5%。
补一句 city IS NULL OR 就回到 22.0。
⭐ 判据:凡是写 <>、NOT IN、NOT LIKE 这类「否定式过滤」,先问一句「这一列可空吗」。
肯定式过滤(=、IN、LIKE)不需要问,因为 NULL 本来也不该被它选中;
否定式过滤则默认把「未知」也一起排除掉了,而这几乎从来不是你的本意。
🔀 六、⭐ SQL 里其实有两套 NULL 语义
第二节刚说 NULL = NULL 判不出真。但下面这些地方,NULL 又被当成能和自己配上的一个值:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE u(id INTEGER PRIMARY KEY, city TEXT)")
db.executemany("INSERT INTO u VALUES(?,?)", [(1, "BJ"), (2, None), (3, None), (4, "SH")])
print("GROUP BY :", db.execute("SELECT city, COUNT(*) FROM u GROUP BY city").fetchall())
print("DISTINCT :", db.execute("SELECT DISTINCT city FROM u").fetchall())
print("UNION :", db.execute("SELECT city FROM u UNION SELECT city FROM u").fetchall())
print("NULL IS NULL / 1 IS NULL:", db.execute("SELECT (NULL IS NULL), (1 IS NULL)").fetchone())
db.execute("CREATE TABLE uq(id INTEGER PRIMARY KEY, email TEXT UNIQUE)")
db.execute("INSERT INTO uq VALUES(1, 'a@x.com')")
try:
db.execute("INSERT INTO uq VALUES(2, 'a@x.com')")
except Exception as ex:
print("重复字符串 ->", type(ex).__name__, ":", ex)
db.executemany("INSERT INTO uq VALUES(?,?)", [(3, None), (4, None), (5, None)])
print("UNIQUE 列里的三个 NULL:", db.execute("SELECT id, email FROM uq").fetchall())
实跑输出:
GROUP BY : [(None, 2), ('BJ', 1), ('SH', 1)]
DISTINCT : [('BJ',), (None,), ('SH',)]
UNION : [(None,), ('BJ',), ('SH',)]
NULL IS NULL / 1 IS NULL: (1, 0)
重复字符串 -> IntegrityError : UNIQUE constraint failed: uq.email
UNIQUE 列里的三个 NULL: [(1, 'a@x.com'), (3, None), (4, None), (5, None)]
⭐ 两套语义,记住这张表就够了:
用「不知道」语义(NULL 配不上自己) |
用「同一个值」语义(NULL 配得上自己) |
|---|---|
WHERE / ON / HAVING 里的 =、<>、IN |
GROUP BY(所有 NULL 一组) |
| 连接键的匹配(02 章第六节) | DISTINCT、UNION、INTERSECT、EXCEPT |
CHECK 约束(判 FALSE 才拒) |
ORDER BY(NULL 之间视为并列) |
IS / IS NOT(判定式,不是比较) |
💀 UNIQUE 是最贵的一格:实跑里同一个 email 插第二遍被拒,但三行 NULL 全部插入成功。
⚠️ 一个「邮箱唯一」的约束完全挡不住三个没填邮箱的账号——它用的是比较语义,NULL = NULL 判不出真,也就谈不上重复。
⭐ 真想唯一,那一列得同时是 NOT NULL。(🗓️ 未实跑:Postgres 15+ 提供 UNIQUE NULLS NOT DISTINCT 改变这一行为,SQLite 无此语法。)
⭐ 想要一个「NULL 安全的等号」:SQLite 用 IS(实跑:NULL IS NULL → 1、1 IS NULL → 0),Postgres 用 IS NOT DISTINCT FROM——02 章第六节讲连接键时提过,那里是它最常见的用武之地。
ORDER BY 里 NULL 排哪儿:三个库不一样
import sqlite3
print("sqlite version:", sqlite3.sqlite_version)
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, v INTEGER)")
db.executemany("INSERT INTO t VALUES(?,?)", [(1, 30), (2, None), (3, 10), (4, None), (5, 20)])
print("ORDER BY v :", db.execute("SELECT id, v FROM t ORDER BY v").fetchall())
print("ORDER BY v DESC :", db.execute("SELECT id, v FROM t ORDER BY v DESC").fetchall())
print("ORDER BY (v IS NULL), v :",
db.execute("SELECT id, v FROM t ORDER BY (v IS NULL), v").fetchall())
print("MIN(v) =", db.execute("SELECT MIN(v) FROM t").fetchone()[0])
print("ORDER BY v LIMIT 1 =", db.execute("SELECT v FROM t ORDER BY v LIMIT 1").fetchone()[0])
实跑输出(SQLite 3.50.4):
sqlite version: 3.50.4
ORDER BY v : [(2, None), (4, None), (3, 10), (5, 20), (1, 30)]
ORDER BY v DESC : [(1, 30), (5, 20), (3, 10), (2, None), (4, None)]
ORDER BY (v IS NULL), v : [(3, 10), (5, 20), (1, 30), (2, None), (4, None)]
MIN(v) = 10
ORDER BY v LIMIT 1 = None
💀 MIN(v) 是 10,ORDER BY v LIMIT 1 是 NULL。 两句在英语里都念作「最小的那个」,SQLite 上给出不同答案——因为 MIN 跳过 NULL(聚合的规则,03 章),而 ORDER BY 把它排在最前(排序的规则,上面那张表)。⭐ 同一个 NULL,两套语义,两个答案。
| 库 | ORDER BY x ASC 里 NULL 在 |
支持 NULLS FIRST/LAST |
验证情况 |
|---|---|---|---|
| SQLite | 最前(视为最小) | ✅ 3.30+ | ⭐ 实跑于 3.50.4 |
| PostgreSQL | 最后(视为最大) | ✅ | 🗓️ 未实跑 |
| MySQL | 最前(视为最小) | ❌ 无此语法 | 🗓️ 未实跑 |
⚠️ 同一条「取最新的一条」查询,从 SQLite 搬到 Postgres 上,ORDER BY updated_at DESC LIMIT 1 取到的可能是完全不同的一行——SQLite 上 DESC 把 NULL 排在最后,Postgres 上排在最前。
⭐ 可移植的写法:ORDER BY (v IS NULL), v(实跑把 NULL 稳定压到末尾,把 IS NULL 换成 IS NOT NULL 就置顶)。三个库都支持,不依赖任何一家的默认值。
🛑 第二个休息点 —— 中段讲完了(约 29 分钟)。 最后一段还有:日常工具:
COALESCE/NULLIF/ 空串不是NULL这一章确实长,分三次读完全没问题 —— 回来直接从下一节接着看。
🛑 读到这里可以停 —— 已经读了约 71 分钟。 最后一段还有(约 33 分钟):日常工具:
COALESCE/NULLIF/ 空串不是NULL· 检查点与走神救援 回来的时候不用重读,直接从下一节接着看就行。
🧰 七、日常工具:COALESCE / NULLIF / 空串不是 NULL
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE p(id INTEGER PRIMARY KEY, nick TEXT, name TEXT, note TEXT)")
db.executemany("INSERT INTO p VALUES(?,?,?,?)", [
(1, "小明", "王明", "hi"),
(2, None, "李雷", ""), # note 是空串
(3, "", "韩梅", None), # nick 是空串
(4, None, None, None),
])
print("COALESCE :", db.execute("SELECT id, COALESCE(nick, name, '(匿名)') FROM p").fetchall())
print("套一层 NULLIF :",
db.execute("SELECT id, COALESCE(NULLIF(nick,''), name, '(匿名)') FROM p").fetchall())
print("note = '' :", db.execute("SELECT id FROM p WHERE note = ''").fetchall())
print("note IS NULL :", db.execute("SELECT id FROM p WHERE note IS NULL").fetchall())
print("COUNT(note) :", db.execute("SELECT COUNT(note) FROM p").fetchone()[0])
print("length('') / length(NULL):", db.execute("SELECT length(''), length(NULL)").fetchone())
print("拼接被吞 :", db.execute("SELECT id, '你好, ' || nick FROM p").fetchall())
print("NULLIF 防除零 :", db.execute("SELECT NULLIF(5,5), 10 / NULLIF(0,0)").fetchone())
实跑输出:
COALESCE : [(1, '小明'), (2, '李雷'), (3, ''), (4, '(匿名)')]
套一层 NULLIF : [(1, '小明'), (2, '李雷'), (3, '韩梅'), (4, '(匿名)')]
note = '' : [(2,)]
note IS NULL : [(3,), (4,)]
COUNT(note) : 2
length('') / length(NULL): (0, None)
拼接被吞 : [(1, '你好, 小明'), (2, None), (3, '你好, '), (4, None)]
NULLIF 防除零 : (None, None)
⚠️ 先看「拼接被吞」那行:id=2 和 id=4 的 nick 是 NULL,'你好, ' || NULL 整体变成 NULL(第二节那条「字符串也被吞」)——你想拼一句问候语,拿到的是一个空值。
而 id=3 的 nick 是空串,拼出来是 '你好, ':不是 NULL,只是少了个名字。⭐ 这一行输出里同时藏着本节要讲的两件事。
| 函数 | 做什么 | 典型用途 |
|---|---|---|
COALESCE(a, b, c) |
返回第一个非 NULL 的参数 |
兜底默认值;⭐ 三个库通用,优先于 SQLite 的 IFNULL / MySQL 的 IFNULL / Oracle 的 NVL |
NULLIF(a, b) |
a = b 时返回 NULL,否则返回 a |
⭐ 防除零(x / NULLIF(y, 0));把哨兵值转回 NULL(NULLIF(age, -999)) |
💀 空串 '' 不是 NULL,它是一个长度为 0 的真实字符串。 实跑三处都在证明这件事:
note IS NULL 只找到 id=3,4,note = '' 找到的是 id=2;COUNT(note) 是 2(空串被数进去了);length('') 是 0 而 length(NULL) 是 NULL。
⚠️ 所以 COALESCE(nick, name, '(匿名)') 对 id=3 返回空串——它的 nick 是空串不是 NULL,COALESCE 认这个值。
⭐ 修法是套一层 NULLIF:COALESCE(NULLIF(nick,''), name, '(匿名)'),实跑正确返回 韩梅。
⭐ 判据:在写入端就定一个规矩——一列里要么统一存
NULL,要么统一存空串,别两种都有。 一个前端没填就提交空串的表单,会让表里同时存在两种「没有」,而所有IS NULL检查只看得见其中一种。 (🗓️ 未实跑:Oracle 是唯一把空串当作NULL的主流库,本机无该环境。)
🔗 这一章连到哪里
| 相关的地方 | 为什么 |
|---|---|
| 01-一条查询是怎么跑的.html | 那一章说「WHERE 逐行判断,扔掉不要的行」——本章第三节给出精确版本:只放行 TRUE,FALSE 和 UNKNOWN 一起扔,于是一个条件和它的否定加起来不等于全表 |
| 02-JOIN的几种语义.html | 那一章有两处欠着本章一个解释:LEFT JOIN 补出来的 NULL 为什么会让 WHERE 把整行扔掉,以及连接键是 NULL 时为什么两边都 NULL 也配不上对 |
| 03-聚合与分组.html | ⭐ 聚合函数怎么对待 NULL 全部归它(COUNT(*) 数行、其余数值,AVG 的分母,空集上 SUM 给 NULL)。本章第五节接着往下问一步:那些 NULL 行是在哪一步从看板上消失的 |
| 05-子查询的三种形状.html | 本章第四节说「反连接一律写 NOT EXISTS」——IN / EXISTS / JOIN 三种形状还有性能和可读性上的取舍,那一章接着讲 |
| 10-拿SQL审问一份数据.html | ⭐ 那一章把 COUNT(*) - COUNT(city) 当探针用来量一份数据脏在哪;本章解释这个探针为什么成立,以及它那条白名单断言的 NULL 陷阱 |
| 数据这一关 · 05 缺失不是一种东西 | ⚠️ 同名不同物:那一章讲缺失的成因(MCAR/MAR/MNAR)和该不该填;本章讲已经是 NULL 之后 SQL 怎么对待它。见本章第零节 |
| 数据这一关 · 04 脏数据的十种形态 | 比 NULL 更坏的是哨兵值(-999、"N/A")——它们不是 NULL,缺失率监控全绿。本章第七节的 NULLIF(age, -999) 就是把它们转回 NULL 的那一步 |
✅ 检查点
- 本章和《数据这一关》05 章都在讲「值没有」,两者的题目分别是什么?
NULL = NULL和NULL IS NULL判出来分别是什么,为什么不同?另外0 AND NULL和1 OR NULL是什么,这两条为什么不算例外?- 把
city NOT IN ('BJ', NULL)展开成AND/OR的形式,说明它为什么对每一行都判不出真。为什么IN没事? - 实跑那张 3 行表上,
WHERE v > 10和WHERE NOT (v > 10)各返回几行?加起来是几?说明了什么? CHECK (age >= 18)挡得住age = NULL吗?查询和约束对UNKNOWN的处理有什么区别?- 反连接的四种写法(
NOT IN/NOT EXISTS/LEFT JOIN … IS NULL/NOT IN+ 内层过滤)在实跑里各返回几行?B 和 D 差的那一行差在哪? GROUP BY city会丢掉city为NULL的行吗?那 31.0 变成 11.0 和 2.0 分别是在哪一步发生的?由此得到的判据是什么?- 列出至少三个「
NULL被当成能和自己配上的同一个值」的地方。UNIQUE属于哪一套语义,后果是什么?再说明 SQLite 上MIN(v)和ORDER BY v LIMIT 1为什么给出不同答案、可移植的排序写法是什么。 COALESCE(nick, name, '(匿名)')对一个nick是空串的行返回什么?怎么修?
👀 答案
- 《数据这一关》05 讲统计学意义上的缺失:这个值为什么没有(MCAR / MAR / MNAR)、能不能填、填了会不会把偏差带进模型。
本章讲 SQL 的
NULL语义:值已经缺在库里了,它怎么参与比较和运算。⚠️ 同名不同物——缺失机制处理得再对,照样会被三值逻辑打中。 NULL = NULL→ UNKNOWN;NULL IS NULL→ TRUE。因为=是比较运算(问「值是多少」,两个未知量比不出结果),IS NULL是判定式(问「有没有值」,这个问题永远有明确答案)。0 AND NULL→ FALSE,1 OR NULL→ TRUE。不是例外,是「不知道」这个语义的直接推论:只有当那个未知值到底是多少会改变答案时,结果才是UNKNOWN——这两种情况下它改变不了答案。 ⚠️ 但算术不享受这个待遇:0 * NULL实跑仍是NULL。- 展开成
NOT (city = 'BJ' OR city = NULL)。对city='SH':FALSE OR UNKNOWN→UNKNOWN,NOT UNKNOWN还是UNKNOWN,WHERE不放行;每一行都如此,所以返回[]。IN没事是因为它是city='BJ' OR city=NULL,对city='BJ'那行是TRUE OR UNKNOWN→ TRUE。 💀NOT把「有一项为真就够了」变成了「必须每一项都为假」,而「为假」正是UNKNOWN给不出的。 - 各 1 行(
[(3,)]和[(1,)]),加起来 2 行,而全表 3 行。 💀 一个条件和它的否定加起来不等于全表——id=2(v是NULL)谁都没要。连WHERE v > 10 OR NOT (v > 10)都漏掉它。 - 挡不住——实跑
age=NULL插入成功,而age=10被IntegrityError : CHECK constraint failed: age >= 18拒绝。 查询要求「证明为真」,约束要求「证明为假」:WHERE/ON/HAVING只放行TRUE,CHECK只在判为FALSE时拒绝,UNKNOWN在两边待遇正好相反。要挡住得写CHECK (age IS NOT NULL AND age >= 18)或给列加NOT NULL。 - A
NOT IN→ 0 行([]);BNOT EXISTS→ 4 行(2,3,4,5);CLEFT JOIN … IS NULL→ 4 行(同 B);DNOT IN+ 内层过滤 → 3 行(2,4,5)。 B 和 D 差在id=3,它自己的city是NULL:NOT EXISTS的口径是「配不上就算不在」,NOT IN的口径是「不知道就不给结论」。这是业务判断,不是 bug,但你得知道自己选了哪个。默认写NOT EXISTS。 - 不会丢——实跑
GROUP BY city给出三组[(None, 20.0), ('BJ', 9.0), ('SH', 2.0)],加起来正好 31.0,NULL自成一组占 20.0(接近全表三分之二)。 丢在后面:a 连维表(INNER JOIN) → 11.0(连接键NULL配不上对);bWHERE city <> 'BJ'→ 2.0 而正确答案是 22.0(⚠️NULL <> 'BJ'判UNKNOWN,一个「不等于」吞掉 20.0,占全表 64.5%);cWHERE city IN (…)→ 11.0。补city IS NULL OR后回到 22.0。 判据:写<>、NOT IN、NOT LIKE这类否定式过滤前,先问「这一列可空吗」——肯定式过滤不用问,否定式过滤默认把「未知」也一起排除了。 GROUP BY(所有NULL一组)、DISTINCT、UNION/INTERSECT/EXCEPT、ORDER BY(NULL之间视为并列)、IS/IS NOT。 ⚠️UNIQUE属于另一套(比较语义):实跑里重复的'a@x.com'被拒,但三行NULL全部插入成功——「邮箱唯一」的约束完全挡不住三个没填邮箱的账号。要真唯一,那列得同时NOT NULL。 排序上MIN(v)= 10(聚合跳过NULL),ORDER BY v LIMIT 1=NULL(SQLite 把NULL视为最小排在最前);而 Postgres 视NULL为最大排在最后,MySQL 同 SQLite 且不支持NULLS FIRST/LAST——同一条「取最新一条」跨库搬会取到不同的行。 可移植写法:ORDER BY (v IS NULL), v(垫底)或ORDER BY (v IS NOT NULL), v(置顶)。- 返回空串——因为
id=3的nick是空串不是NULL,COALESCE认这个值(空串是长度为 0 的真实字符串:length('')是0,length(NULL)是NULL,COUNT(note)把空串数了进去所以是 2)。 修法:套一层NULLIF——COALESCE(NULLIF(nick,''), name, '(匿名)'),实跑返回韩梅。
🛑 可以停在这里
⚡ 走神救援
⭐
NULL不是值,是「不知道」,所以 SQL 的布尔值有三个:TRUE / FALSE / UNKNOWN。地基是
NULL = NULL判出来是 UNKNOWN 而不是真——⭐ 因为=问「值是多少」,IS NULL问「有没有值」。UNKNOWN 不污染一切:判据是「只有当那个未知值会改变答案时,结果才是 UNKNOWN」。但算术不享受这个优待,任何数乘NULL、任何串接NULL都是NULL。💀 招牌事故:
WHERE city NOT IN (SELECT …)在子查询里混进一个NULL之后返回空集、一行都没有、而且不报错;⚠️ 同时不带NOT的IN完全正常——坏掉的只有加了NOT的那一半。⭐
WHERE只放行 TRUE,FALSE 和 UNKNOWN 一起扔。 所以一个条件和它的否定加起来不等于全表。⚠️ 约束反过来:CHECK只在判 FALSE 时拒绝,于是它会放行NULL——查询要求证明为真,约束要求证明为假。反连接四种写法实测给出三个不同答案,⭐ 默认一律写
NOT EXISTS,它不会被内层的NULL毒化。⚠️ 而「那些
NULL行是在哪一步从看板上没的」,答案不是GROUP BY——NULL会老老实实自成一组。真正丢在后面三步,而且都不报错:INNER JOIN维表(连接键NULL配不上对)、<>过滤、IN (…)过滤。💀 实测里一个「不等于」就吞掉了近三分之二的金额。
下一节 👉 05-子查询的三种形状.md