普通语言里的布尔型只有true和false两个值,这种逻辑体系被称为二值逻辑。而SQL语言里,除此之外还有第三个值unknown,因此这种逻辑体系被称为三值逻辑(three-valued logic)。
为什么SQL语言采用了三值逻辑呢?
问题的答案就在于NULL。关系数据库里引进了NULL,所以不得不同时引进第三个值。这样的三值逻辑一次次地违背常识,深深地困扰着数据库工程师们。
目录
1 理论篇
1.1 两种NULL、三值逻辑还是四值逻辑
1.2 为什么必须写成 ’IS NULL‘ ,而不是 ’= NULL‘
1.3 unknown 第三个真值
1.4 三值逻辑的真值表
2 实践篇
2.1 比较谓词和NULL(1):排中律不成立
2.2 比较谓词和NULL(2):CASE表达式和NULL
2.3 NOT IN 和 NOT EXISTS不是等价的
2.4 限定谓词和 NULL
2.5 限定谓词和极值函数不是等价的
2.6 聚合函数 和 NULL
1 理论篇
1.1 两种NULL、三值逻辑还是四值逻辑
SQL里只存在一种NULL。然而在讨论NULL时,我们一般都会将它分成两种类型来思考。因此这里先来介绍一些基础知识,即两种NULL之间的区别。
两种NULL分别指的是“未知”(unknown)和“不适用”(not applicable,inapplicable)。以“不知道戴墨镜的人眼睛是什么颜色”这种情况为例,这个人的眼睛肯定是有颜色的,但是如果他不摘掉眼镜,别人就不知道他的眼睛是什么颜色。这就叫作未知。而“不知道冰箱的眼睛是什么颜色”则属于“不适用”。因为冰箱根本就没有眼睛,所以“眼睛的颜色”这一属性并不适用于冰箱。(这个例子举的多少有点本土不服了哈哈哈)
“冰箱的眼睛的颜色”这种说法和“圆的体积”“男性的分娩次数”一样,都是没有意义的。平时,我们习惯了说“不知道”,但是“不知道”也分很多种。“不适用”这种情况下的NULL,在语义上更接近于“无意义”,而不是“不确定”。这里总结一下:“未知”指的是“虽然现在不知道,但加上某些条件后就可以知道”;而“不适用”指的是“无论怎么努力都无法知道”。
Codd曾经认为应该严格地区分两种类型的NULL,并提倡在关系数据库中使用四值逻辑[插图]。不知道是幸运还是不幸(笔者认为肯定是幸运),他的这个想法并没有得到广泛支持,现在所有的DBMS都将两种类型的NULL归为了一类并采用了三值逻辑。但是他的这种分类方法本身还是有很多优点的,因此后来依然有很多学者支持。
(具体的四值逻辑判断略)
1.2 为什么必须写成 ’IS NULL‘ ,而不是 ’= NULL‘
对NULL使用比较谓词后得到的结果总是unknown。而查询结果只会包含WHERE子句里的判断结果为true的行,不会包含判断结果为false和unknown的行。不只是等号,对NULL使用其他比较谓词,结果也都是一样的。所以无论col_1是不是NULL,比较结果都是unknown。
--以下的式子都会被判为 unknown
1 = NULL
2 > NULL
3 < NULL
4 <> NULL
NULL = NULL
那么,为什么对NULL使用比较谓词后得到的结果永远不可能为真呢?这是因为,NULL既不是值也不是变量。NULL只是一个表示“没有值”的标记,而比较谓词只适用于值。因此,对并非值的NULL使用比较谓词本来就是没有意义的注。
所以,牢记,NULL并不是值;
“列的值为NULL”“NULL值”这样的说法本身就是错误的。因为NULL不是值,所以不在定义域(domain)中。相反,如果有人认为NULL是值,那它是什么类型的值?关系数据库中存在的值必然属于某种类型,比如字符型或数值型等。所以,假如NULL是值,那么它就必须属于某种类型。
NULL容易被认为是值的原因恐怕有两个。第一个是在C语言等编程语言里面,NULL被定义为了一个常量(很多语言将其定义为了整数0),这导致了人们的混淆。但是,其实SQL里的NULL和其他编程语言里的NULL是完全不同的东西(请参考本节末尾参考文献中的“C语言初级Q&A”)。
第二个原因是,IS NULL这样的谓词是由两个单词构成的,所以人们容易把IS当作谓词,而把NULL当作值。特别是SQL里还有IS TRUE、IS FALSE这样的谓词,人们由此类推,从而这样认为也不是没有道理。但是正如讲解标准SQL的书里提醒人们注意的那样,我们应该把IS NULL看作是一个谓词。因此,如果可以的话,写成IS_NULL这样也许更合适。
1.3 unknown 第三个真值
真值unknown和作为NULL的一种的UNKNOWN(未知)是不同的东西。前者是明确的布尔型的真值,后者既不是值也不是变量。为了便于区分,前者采用粗体的小写字母unknown,后者用普通的大写字母UNKNOWN来表示。
为了让大家理解两者的不同,我们来看一个x=x这样的简单等式。x是真值unknown时,x=x被判断为true,而x是UNKNOWN时被判断为unknown。
--这个是明确的真值的比较
unknown = unknown → true
--这个相当于NULL = NULL
UNKNOWN = UNKNOWN → unknown
1.4 三值逻辑的真值表
还是比较好记的,优先级上看:
● AND的情况: false > unknown > true
● OR的情况: true > unknown > false
2 实践篇
2.1 比较谓词和NULL(1):排中律不成立
我们假设约翰是一个人。那么,下面的语句(以下称为“命题”)是真是假?
约翰是20岁,或者不是20岁,二者必居其一。——P
在SQL的世界里排中律是不成立的
--添加第3个条件:年龄是20岁,或者不是20岁,或者年龄未知
SELECT *
FROM Students
WHERE age = 20
OR age <> 20
OR age IS NULL;
实际上约翰这个人是有年龄的,只是我们无法从这张表中知道而已。
换句话说,关系模型并不是用于描述现实世界的模型,而是用于描述人类认知状态的核心(知识)的模型。因此,我们有限且不完备的知识也会直接反映在表里。(这句话看起来好有深意)
即使不知道约翰的年龄,他在现实世界中也一定“要么是20岁,要么不是20岁”——我们容易自然而然地这样认为。然而,这样的常识在三值逻辑里却未必正确。
2.2 比较谓词和NULL(2):CASE表达式和NULL
在CASE表达式里将NULL作为条件使用时经常会出现的错误:
--col_1为1时返回○、为NULL时返回×的CASE表达式?
CASE col_1
WHEN 1 THEN'○'
WHEN NULL THEN'×'
END
这个CASE表达式一定不会返回×。这是因为,第二个WHEN子句是col_1 = NULL的缩写形式。正如大家所知,这个式子的真值永远是unknown。而且CASE表达式的判断方法与WHERE子句一样,只认可真值为true的条件。正确的写法是像下面这样使用搜索CASE表达式。
CASE WHEN col_1 = 1 THEN'○'
WHEN col_1 IS NULL THEN'×'
END
(经典!)
这里请再次确认自己已经记住“NULL并不是值”这点!
2.3 NOT IN 和 NOT EXISTS不是等价的
在对SQL语句进行性能优化时,经常用到的一个技巧是将IN改写成EXISTS。这是等价改写,并没有什么问题。问题在于,将NOT IN改写成NOT EXISTS时,结果未必一样。
请注意,B班山田的年龄是NULL。我们考虑一下如何根据这两张表查询“与B班住在东京的学生年龄不同的A班学生”。也就是说,希望查询到的是拉里和伯杰。因为布朗与齐藤年龄相同,所以不是我们想要的结果。如果单纯地按照这个条件去实现,则SQL语句如下所示。
--查询与B班住在东京的学生年龄不同的A班学生的SQL语句?
SELECT *
FROM Class_A
WHERE age NOT IN ( SELECT age
FROM Class_B
WHERE city =’东京’);
这条SQL语句查询不到任何数据。
如果山田的年龄不是NULL(且与拉里和伯杰年龄不同),是能顺利找到拉里和伯杰的。
--1.执行子查询,获取年龄列表
SELECT *
FROM Class_A
WHERE age NOT IN (22, 23, NULL);
--2.用NOT和IN等价改写NOT IN
SELECT *
FROM Class_A
WHERE NOT age IN (22, 23, NULL);
--3.用OR等价改写谓词IN
SELECT *
FROM Class_A
WHERE NOT ( (age = 22) OR (age = 23) OR (age = NULL) );
--4.使用德·摩根定律等价改写
SELECT *
FROM Class_A
WHERE NOT (age = 22) AND NOT(age = 23) AND NOT (age = NULL);
--5.用<>等价改写NOT和=
SELECT *
FROM Class_A
WHERE (age <> 22) AND (age <> 23) AND (age <> NULL);
--6.对NULL使用<>后,结果为unknown
SELECT *
FROM Class_A
WHERE (age <> 22) AND (age <> 23) AND unknown;
--7.如果AND运算里包含unknown,则结果不为true(参考“理论篇”中的矩阵)
SELECT *
FROM Class_A
WHERE false或unknown;
这里对A班的所有行都进行了如此繁琐的判断,然而没有一行在WHERE子句里被判断为true。也就是说,如果NOT IN子查询中用到的表里被选择的列中存在NULL,则SQL语句整体的查询结果永远是空。(我的天!)
想得到正确的结果,需要用EXISTS
--正确的SQL语句:拉里和伯杰将被查询到
SELECT *
FROM Class_A A
WHERE NOT EXISTS ( SELECT *
FROM Class_B B
WHERE A.age = B.age
AND B.city = ’东京’);
执行结果
name age city
----- ---- ----
拉里 19 埼玉
伯杰 21 千叶
来看下使用EXISTS 对 NULL 来说与 IN 的不同。
--1.在子查询里和NULL进行比较运算
SELECT *
FROM Class_A A
WHERE NOT EXISTS ( SELECT *
FROM Class_B B
WHERE A.age = NULL
AND B.city =’东京’);
--2.对NULL使用“=”后,结果为 unknown
SELECT *
FROM Class_A A
WHERE NOT EXISTS ( SELECT *
FROM Class_B B
WHERE unknown
AND B.city =’东京’);
--3.如果AND运算里包含unknown,结果不会是true
SELECT *
FROM Class_A A
WHERE NOT EXISTS ( SELECT *
FROM Class_B B
WHERE false或unknown);
--4.子查询没有返回结果,因此相反地,NOT EXISTS为true
SELECT *
FROM Class_A A
WHERE true;
也就是说,山田被作为“与任何人的年龄都不同的人”来处理了(但是,还要把与年龄不是NULL的齐藤及田尻进行比较后的处理结果通过AND连接,才能得出最终结果)。产生这样的结果,是因为EXISTS谓词永远不会返回unknown。EXISTS只会返回true或者false。因此就有了IN和EXISTS可以互相替换使用,而NOT IN和NOT EXISTS却不可以互相替换的混乱现象。虽然写代码的时候很难做到绝对不依赖直觉,但作为数据库工程师来说,还是需要好好理解一下这种现象。
2.4 限定谓词和 NULL
SQL里有ALL和ANY两个限定谓词。因为ANY与IN是等价的,所以我们不经常使用ANY。在这里,我们主要看一下更常用的ALL的一些注意事项。
ALL可以和比较谓词一起使用,用来表达“与所有的××都相等”,或“比所有的××都大”的意思。接下来,我们给B班表里为NULL的列添上具体的值。然后,使用这张新表来思考一下用于查询“比B班住在东京的所有学生年龄都小的A班学生”的SQL语句。
使用ALL谓词时,SQL语句可以像下面这样写。
--查询比B班住在东京的所有学生年龄都小的A班学生
SELECT *
FROM Class_A
WHERE age < ALL ( SELECT age
FROM Class_B
WHERE city =’东京’);
执行结果
name age city
----- ---- ----
拉里 19 埼玉
查询到的只有比山田年龄小的拉里,到这里都没有问题。但是如果山田年龄不详,就会有问题了。凭直觉来说,此时查询到的可能是比22岁的齐藤年龄小的拉里和伯杰。然而,这条SQL语句的执行结果还是空。这是因为,ALL谓词其实是多个以AND连接的逻辑表达式的省略写法。具体的分析步骤如下所示。
--1.执行子查询获取年龄列表
SELECT *
FROM Class_A
WHERE age < ALL ( 22, 23, NULL );
--2.将ALL谓词等价改写为AND
SELECT *
FROM Class_A
WHERE (age < 22) AND (age < 23) AND (age < NULL);
--3.对NULL使用“<”后,结果变为 unknown
SELECT *
FROM Class_A
WHERE (age < 22) AND (age < 23) AND unknown;
--4. 如果AND运算里包含unknown,则结果不为true
SELECT *
FROM Class_A
WHERE false 或 unknown;
2.5 限定谓词和极值函数不是等价的
--查询比B班住在东京的年龄最小的学生还要小的A班学生
SELECT *
FROM Class_A
WHERE age < ( SELECT MIN(age)
FROM Class_B
WHERE city =’东京’);
执行结果
name age city
----- ---- ----
拉里 19 埼玉
伯杰 21 千叶
没有问题。即使山田的年龄无法确定,这段代码也能查询到拉里和伯杰两人。这是因为,极值函数在统计时会把为NULL的数据排除掉。使用极值函数能使Class_B这张表里看起来就像不存在NULL一样。
● ALL谓词:他的年龄比在东京住的所有学生都小——Q1
● 极值函数:他的年龄比在东京住的年龄最小的学生还要小——Q2
这两个命题,在有NULL时不是等价的,同时谓词(或者函数)的输入为空集时,这两个命题也不等价。
若Class_B没有住在东京的学生!
B班里没有学生住在东京。这时,使用ALL谓词的SQL语句会查询到A班的所有学生。然而,用极值函数查询时一行数据都查询不到。这是因为,极值函数在输入为空表(空集)时会返回NULL。因此,使用极值函数的SQL语句会像下面这样一步步被执行。
--1.极值函数返回NULL
SELECT *
FROM Class_A
WHERE age < NULL;
--2.对NULL使用“<”后结果为 unknown
SELECT *
FROM Class_A
WHERE unknown;
比较对象原本就不存在时,根据业务需求有时需要返回所有行,有时需要返回空集。需要返回所有行时(感觉这类似于“不战而胜”),需要使用ALL谓词,或者使用COALESCE函数将极值函数返回的NULL处理成合适的值。
2.6 聚合函数 和 NULL
实际上,当输入为空表时返回NULL的不只是极值函数,COUNT以外的聚合函数也是如此。
--查询比住在东京的学生的平均年龄还要小的A班学生的SQL语句?
SELECT *
FROM Class_A
WHERE age < ( SELECT AVG(age)
FROM Class_B
WHERE city =’东京’);
没有住在东京的学生时,AVG函数返回NULL。因此,外侧的WHERE子句永远是unknown,也就查询不到行。使用SUM也是一样。这种情况的解决方法只有两种:要么把NULL改写成具体值,要么闭上眼睛接受NULL。但是如果某列有NOT NULL约束,而我们需要往其中插入平均值或汇总值,那么就只能选择将NULL改写成具体值了。
聚合函数和极值函数的这个陷阱是由函数自身带来的,所以仅靠为具体列加上NOT NULL约束是无法从根本上消除的。因此我们在编写SQL代码的时候需要特别注意。
小结
1.NULL不是值。
2.因为NULL不是值,所以不能对其使用谓词。
3.对NULL使用谓词后的结果是unknown。
4.unknown参与到逻辑运算时,SQL的运行会和预想的不一样。
5.按步骤追踪SQL的执行过程能有效应对4中的情况。
最后说明一下,要想解决NULL带来的各种问题,最佳方法应该是往表里添加NOT NULL约束来尽力排除NULL。这样就可以回到美妙的二值逻辑世界(虽然并不能完全回到)
(这章比上一节理解的好多了!)
———————————————————————————————————————————
更多文章点击主页查看~