大多数编程语言都是基于二值逻辑的,即逻辑真值只有真和假两个。而 SQL 语言则采用一种特别的 逻辑体系——三值逻辑,即逻辑真值除了真和假,还有第三个值“不确定”。三值逻辑经常会带来一些意想不到的情况,这让程序员很是烦恼。
1.1 三值逻辑还是四值逻辑
SQL 里只存在 一种 NULL。然而在讨论 NULL 时,我们一般都会将它分成两种类型来思考。 因此这里先来介绍一些基础知识,即两种 NULL 之间的区别。
两种 NULL 分别指的是“未知”(unknown)和“不适用”(not applicable, inapplicable)。
以“不知道戴墨镜的人眼睛是什么颜色”这种情 况为例,这个人的眼睛肯定是有颜色的,但是如果他不摘掉眼镜,别人就 不知道他的眼睛是什么颜色。这就叫作未知。
而“不知道冰箱的眼睛是什 么颜色”则属于“不适用”。因为冰箱根本就没有眼睛,所以“眼睛的颜色” 这一属性并不适用于冰箱。
Codd 曾经认为应该严格地区分两种类型的 NULL,并提倡在关系数据 库中使用四值逻辑 。现在所有的 DBMS 都将两种类型的 NULL 归为了一类并采用了三值逻辑。
1.2 IS NULL
为什么必须写成“IS NULL”,而不是“= NULL”
1 | --以下的式子都会被判为 unknown |
那么,为什么对 NULL 使用比较谓词后得到的结果永远不可能为真呢? 这是因为,NULL 既不是值也不是变量。NULL 只是一个表示“没有值”的 标记,而比较谓词只适用于值。因此,对并非值的 NULL 使用比较谓词本来就是没有意义的。
NULL 容易被认为是值的原因恐怕有两个。第一个是在 C 语言等编程 语言里面,NULL 被定义为了一个常量(很多语言将其定义为了整数 0), 这导致了人们的混淆。但是,其实 SQL 里的 NULL 和其他编程语言里的 NULL 是完全不同的东西。
第二个原因是,IS NULL 这样的谓词是由两个单词构成的,所以人们 容易把 IS 当作谓词,而把 NULL 当作值。我们应该把 IS NULL 看作是一个谓词。因此,如果可以的话,写成 IS_NULL 这样也许更合适。
1.3 真值unknown
真值 unknown 和作为 NULL 的一种的 UNKNOWN(未知)是不同的东西。(前者是明确的布尔型的真值,后者既不是值也不 是变量)。
x 是真值 unknown 时,x=x 被判断为 true,而 x 是 UNKNOWN 时被判断为 unknown。
1 | -- 这个是明确的真值的比较 |
- SQL 遵循的三值逻辑的真值表
图中浅蓝色部分是三值逻辑中独有的运算,这在二值逻辑中是没有的。 其余的 SQL 谓词全部都能由这三个逻辑运算组合而来。从这个意义上讲, 这个矩阵可以说是 SQL 的母体(matrix)。
为了便于记忆,请注 意这三个真值之间有下面这样的优先级顺序。
- AND 的情况: false > unknown > true
- OR 的情况: true > unknown > false
优先级高的真值会决定计算结果。例如true AND unknown,因为 unknown的优先级更高,所以结果是unknown。而true OR unknown的话,因为 true 优先级更高,所以结果是 true。
1.4 比较谓词和 NULL
- 约翰是 20 岁,或者不是 20 岁,二者必居其一。
“把命题和它的否命题通 过‘或者’连接而成的命题全都是真命题”这个命题在二值逻辑中被称为 排中律(Law of Excluded Middle)。
1 | -- 查询年龄是 20 岁或者不是 20 岁的学生 |
遗憾的是,在 SQL 的世界里,排中律是不成立的。如果有一条存在NULL。
1 | -- 添加第 3 个条件 :年龄是 20 岁,或者不是 20 岁,或者年龄未知 |
- CASE WHEN时也要注意
1 | -- col_1 为 1 时返回○、为 NULL 时返回 × 的 CASE 表达式? |
- NOT IN 和 NOT EXISTS
1 | -- 这条 SQL 语句真的能正确地查询到NULL age 的学生吗?遗憾的是不能。 结果是空,查询不到任何数据。 |
产生这样的结果,是因为 EXISTS 谓词永 远不会返回 unknown。EXISTS 只会返回 true 或者 false。因此就有了 IN 和 EXISTS 可以互相替换使用,而 NOT IN 和 NOT EXISTS 却不可以互 相替换的混乱现象。虽然写代码的时候很难做到绝对不依赖直觉,但作为 数据库工程师来说,还是需要好好理解一下这种现象。
1.5 限定谓词和极值函数对待NULL非等价
- 限定谓词
1 | -- 查询比 B 班住在东京的所有学生年龄都小的 A 班学生 |
- 极值函数
1 | -- 查询比 B 班住在东京的年龄最小的学生还要小的 A 班学生 |
- 总结
极值函数在统计时会把为 NULL 的数据排除掉。
- ALL谓词:他的年龄比在东京住的所有学生都小
- 极值函数:他的年龄比在东京住的年龄最小的学生还要小
在现实世界中,这两个命题是一个意思。但是,正如我们通过前面的 例题看到的那样,表里存在 NULL 时它们是不等价的。
1.6 聚合函数和 NULL
COUNT 以外 的聚合函数也是如此。
1 | -- 查询比住在东京的学生的平均年龄还要小的 A 班学生的 SQL 语句? |
没有住在东京的学生时,AVG 函数返回 NULL。因此,外侧的 WHERE 子句永远是 unknown,也就查询不到行。
聚合函数和极值函数的这个陷阱是由函数自身带来的,所以仅靠为具体列加上 NOT NULL 约束是无法从根本上消除的。因此我们在编写 SQL 代码的时候需要特别注意。
1.7 总结
NULL 不是值。
因为 NULL 不是值,所以不能对其使用谓词。
对 NULL 使用谓词后的结果是 unknown。
unknown 参与到逻辑运算时,SQL 的运行会和预想的不一样。
要想解决 NULL 带来的各种问题,最佳方法应该是往 表里添加NOT NULL约束来尽力排除NULL。这样就可以回到美妙的二值逻 辑世界(虽然并不能完全回到)。