神奇的 SQL 之温柔的陷阱 → 三值逻辑 与 NULL !

摘要:
逻辑值unknown和作为NULL的一种的UNKNOWN(未知)是不同的东西。其余的SQL谓词全部都能由这三个逻辑运算组合而来。因此,对并非值的NULL使用比较谓词本来就是没有意义的。“列的值为NULL”、“NULL值”这样的说法本身就是错误的。NULL容易被认为是值的原因有两个。但是,SQL里的NULL和其他编程语言里的NULL是完全不同的东西。但是正如讲解标准SQL的书里提醒人们注意的那样,我们应该把ISNULL看作是一个谓词。因此,写成IS_NULL这样也许更合适。

转载 https://www.cnblogs.com/youzhibing/p/11337745.html

 两种 NULL 这种说法大家可能会觉得很奇怪,因为 SQL 里只存在一种 NULL 。然而在讨论 NULL 时,我们一般都会将它分成两种类型来思考:“未知”(unknown)和“不适用”(not applicable,inapplicable)。

为什么必须写成“IS NULL”,而不是“= NULL”

DROP TABLE IF EXISTS t_sample_null;
CREATE TABLE t_sample_null (
    id INT(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    name VARCHAR(50) NOT NULL COMMENT '名称',
    remark VARCHAR(500) COMMENT '备注',
    primary key(id)
) COMMENT 'NULL样例';

INSERT INTO t_sample_null(name, remark)
VALUES('zhangsan', '张三'),('李四', NULL);

我们要查询备注为 NULL 的记录(为 NULL 这种叫法本身是不对的,只是我们日常中已经叫习惯了,具体往下看),怎么查,很多新手会写出这样的 SQL

--SQL 不报错,但查不出结果
SELECT * FROM t_sample_null WHERE remark = NULL;

神奇的 SQL 之温柔的陷阱 → 三值逻辑 与 NULL !第1张

执行时不报错,但是查不出我们想要的结果, 这是为什么了 ? 这个问题我们先放着,我们往下看

三值逻辑

这个三值逻辑不是三目运算,指的是三个逻辑值,有人可能有疑问了,逻辑值不是只有真(true)和假(false)吗,哪来的第三个? 说这话时我们需要注意所处的环境,在主流的编程语言中(C、JAVA、Python、JS等)中,逻辑值确实只有 2 个,但在 SQL 中却存在第三个逻辑值:unknown。这有点类似于我们平时所说的:对、错、不知道。

逻辑值 unknown 和作为 NULL 的一种的 UNKNOWN (未知)是不同的东西。前者是明确的布尔型的逻辑值,后者既不是值也不是变量。为了便于区分,前者采用小写字母 unknown ,后者用大写字母 UNKNOWN 来表示。为了让大家理解两者的不同,我们来看一个 x=x 这样的简单等式。x 是逻辑值 unknown 时,x=x 被判断为 true ,而 x 是 UNKNOWN 时被判断为 unknown

--这个是明确的逻辑值的比较
unknown = unknown → true

-- 这个相当于NULL =NULL
UNKNOWN = UNKNOWN → unknown

三值逻辑的逻辑值表

NOT

神奇的 SQL 之温柔的陷阱 → 三值逻辑 与 NULL !第2张

AND

神奇的 SQL 之温柔的陷阱 → 三值逻辑 与 NULL !第3张

OR

神奇的 SQL 之温柔的陷阱 → 三值逻辑 与 NULL !第4张

图中蓝色部分是三值逻辑中独有的运算,这在二值逻辑中是没有的。其余的 SQL 谓词全部都能由这三个逻辑运算组合而来。从这个意义上讲,这个几个逻辑表可以说是 SQL 的母体(matrix)。
NOT 的话,因为逻辑值表比较简单,所以很好记;但是对于 AND 和 OR,因为组合出来的逻辑值较多,所以全部记住非常困难。为了便于记忆,请注意这三个逻辑值之间有下面这样的优先级顺序。
AND 的情况:false > unknown > true
OR 的情况:true > unknown > false

优先级高的逻辑值会决定计算结果。例如 true AND unknown ,因为 unknown 的优先级更高,所以结果是 unknown 。而 true OR unknown 的话,因为 true 优先级更高,所以结果是 true 。记住这个顺序后就能更方便地进行三值逻辑运算了。特别需要记住的是,当 AND 运算中包含 unknown 时,结果肯定不会是 true (反之,如果AND 运算结果为 true ,则参与运算的双方必须都为 true )。

-- 假设 a = 2, b = 5, c =NULL,下列表达式的逻辑值如下

a < b AND b >c  → unknown
a > b OR b <c   → unknown
a < b OR b < c   → trueNOT (b <> c)     → unknown

  “IS NULL” 而非 “= NULL”

--以下的式子都会被判为 unknown
=NULL
>NULL
<NULL
<>NULL
NULL = NULL

那么,为什么对 NULL 使用比较谓词后得到的结果永远不可能为真呢?这是因为,NULL 既不是值也不是变量。NULL 只是一个表示“没有值”的标记,而比较谓词只适用于值。因此,对并非值的 NULL 使用比较谓词本来就是没有意义的。“列的值为 NULL ”、“NULL 值” 这样的说法本身就是错误的。因为 NULL不是值,所以不在定义域(domain)中。相反,如果有人认为 NULL 是值,那么我们可以倒过来想一下:它是什么类型的值?关系数据库中存在的值必然属于某种类型,比如字符型或数值型等。所以,假如 NULL 是值,那么它就必须属于某种类型。

NULL 容易被认为是值的原因有两个。第一个是高级编程语言里面,NULL 被定义为了一个常量(很多语言将其定义为了整数0),这导致了我们的混淆。但是,SQL 里的 NULL 和其他编程语言里的 NULL 是完全不同的东西。第二个原因是,IS NULL 这样的谓词是由两个单词构成的,所以我们容易把 IS 当作谓词,而把 NULL 当作值。特别是 SQL 里还有 IS TRUE 、IS FALSE 这样的谓词,我们由此类推,从而这样认为也不是没有道理。但是正如讲解标准 SQL 的书里提醒人们注意的那样,我们应该把IS NULL 看作是一个谓词。因此,写成 IS_NULL 这样也许更合适。

 比较谓词和 NULL

排中律不成立

排中律指同一个思维过程中,两个相互矛盾的思想不能同假,必有一真,即“要么A要么非A”

假设我们有学生表:t_student

DROP TABLE IF EXISTS t_student;
CREATE TABLE t_student (
    id INT(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    name VARCHAR(50) NOT NULL COMMENT '名称',
    age INT(3) COMMENT '年龄',
    remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注',
    primary key(id)
) COMMENT '学生信息';

INSERT INTO t_student(name, age)
VALUE('zhangsan', 25),('wangwu', 60),('bruce', 32),('yzb', NULL),('boss', 18);

SELECT * FROM t_student;

表中数据 yzb 的 age 是 NULL,也就是说 yzb 的年龄未知。在现实世界里,yzb 是 20 岁,或者不是 20 岁,二者必居其一,这毫无疑问是一个真命题。那么在 SQL 的世界里了,排中律还适用吗? 我们来看一个 SQL

SELECT *FROM t_student
WHERE age = 20 OR age <> 20;

神奇的 SQL 之温柔的陷阱 → 三值逻辑 与 NULL !第5张

yzb 没查出来,这是为什么了?我们来分析下,yzb 的 age 是 NULL,那么这条记录的判断步骤如下

-- 1. 约翰年龄是 NULL (未知的 NULL !)
SELECT *FROM t_student
WHERE age =NULL
OR age <>NULL;

-- 2. 对 NULL 使用比较谓词后,结果为unknown
SELECT *FROM t_student
WHERE unknown
OR unknown;

-- 3.unknown OR unknown 的结果是unknown (参考三值逻辑的逻辑值表)
SELECT *FROM t_student
WHERE unknown;

SQL 语句的查询结果里只有判断结果为 true 的行。要想让 yzb 出现在结果里,需要添加下面这样的 “第 3 个条件”

-- 添加 3个条件:年龄是20 岁,或者不是20 岁,或者年龄未知
SELECT *FROM t_student
WHERE age = 20OR age <> 20OR age IS NULL;

CASE 表达式和 NULL

简单 CASE 表达式如下

CASE col_1
    WHEN = 1 THEN 'o'WHEN NULL THEN 'x'END

这个 CASE 表达式一定不会返回 ×。这是因为,第二个 WHEN 子句是 col_1 = NULL 的缩写形式。正如我们所知,这个式子的逻辑值永远是 unknown ,而且 CASE 表达式的判断方法与 WHERE 子句一样,只认可逻辑值为 true 的条件。正确的写法是像下面这样使用搜索 CASE 表达式

CASE WHEN col_1 = 1 THEN 'o'WHEN col_1 IS NULL THEN 'x'END

  NOT IN 和 NOT EXISTS 不是等价的

我们在对 SQL 语句进行性能优化时,经常用到的一个技巧是将 IN 改写成 EXISTS ,这是等价改写,并没有什么问题。但是,将 NOT IN 改写成 NOT EXISTS 时,结果未必一样。

我们来看个例子,我们有如下两张表:t_student_A 和 t_student_B,分别表示 A 班学生与 B 班学生

DROP TABLE IF EXISTS t_student_A;
CREATE TABLE t_student_A (
    id INT(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    name VARCHAR(50) NOT NULL COMMENT '名称',
    age INT(3) COMMENT '年龄',
    city VARCHAR(50) NOT NULL COMMENT '城市',
    remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注',
    primary key(id)
) COMMENT '学生信息';

INSERT INTO t_student_A(name, age, city)
VALUE
('zhangsan', 25,'深圳市'),('wangwu', 60, '广州市'),
('bruce', 32, '北京市'),('yzb', NULL, '深圳市'),
('boss', 43, '深圳市');

DROP TABLE IF EXISTS t_student_B;
CREATE TABLE t_student_B (
    id INT(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
    name VARCHAR(50) NOT NULL COMMENT '名称',
    age INT(3) COMMENT '年龄',
    city VARCHAR(50) NOT NULL COMMENT '城市',
    remark VARCHAR(500) NOT NULL DEFAULT '' COMMENT '备注',
    primary key(id)
) COMMENT '学生信息';

INSERT INTO t_student_B(name, age, city)
VALUE
('马化腾', 45, '深圳市'),('马三', 25, '深圳市'),
('马云', 43, '杭州市'),('李彦宏', 41, '深圳市'),
('年轻人', 25, '深圳市');

SELECT *FROM t_student_A;
SELECT * FROM t_student_B;

需求:查询与A 班住在深圳的学生年龄不同的 B 班学生,也就说查询出 :马化腾 和 李彦宏,这个 SQL 该如何写,像这样?

-- 查询与 A  班住在深圳的学生年龄不同的 B 班学生 ?SELECT *FROM t_student_B
WHERE age NOT IN (
    SELECT age FROM t_student_A 
    WHERE city = '深圳市');

神奇的 SQL 之温柔的陷阱 → 三值逻辑 与 NULL !第6张

我们发现结果是空,查询不到任何数据,这是为什么了 ?这里 NULL 又开始作怪了,我们一步一步来看看究竟发生了什么

-- 1. 执行子查询,获取年龄列表
SELECT *FROM t_student
WHERE age NOT IN(43, NULL, 25);

-- 2. 用NOT 和IN 等价改写NOT IN
SELECT *FROM t_student
WHERE NOT age IN (43, NULL, 25);

-- 3. 用OR 等价改写谓词IN
SELECT *FROM t_student
WHERE NOT ( (age = 43) OR (age = NULL) OR (age = 25) );

-- 4. 使用德· 摩根定律等价改写
SELECT *FROM t_student
WHERE NOT (age = 43) AND NOT(age = NULL) AND NOT (age = 25);

-- 5. 用<> 等价改写 NOT 和 =SELECT *FROM t_student
WHERE (age <> 43) AND (age <> NULL) AND (age <> 25);

-- 6. 对NULL 使用<>后,结果为 unknown
SELECT *FROM t_student
WHERE (age <> 43) AND unknown AND (age <> 25);

-- 7.如果 AND 运算里包含 unknown,则结果不为true(参考三值逻辑的逻辑值表)
SELECT *FROM t_student
WHERE false 或 unknown;

可以看出,在进行了一系列的转换后,没有一条记录在 WHERE 子句里被判断为 true 。也就是说,如果 NOT IN 子查询中用到的表里被选择的列中存在 NULL ,则 SQL 语句整体的查询结果永远是空。这是很可怕的现象!

为了得到正确的结果,我们需要使用 EXISTS 谓词

--正确的SQL 语句:马化腾和李彦宏将被查询到
SELECT *FROM t_student_B B
WHERE NOT EXISTS ( 
    SELECT *FROM t_student_A A
    WHERE B.age =A.age
    AND A.city = '深圳市');

神奇的 SQL 之温柔的陷阱 → 三值逻辑 与 NULL !第7张

同样地,我们再来一步一步地看看这段 SQL 是如何处理年龄为 NULL 的行的

-- 1. 在子查询里和 NULL 进行比较运算,此时 A.age 是 NULL
SELECT *FROM t_student_B B
WHERE NOT EXISTS ( 
    SELECT *FROM t_student_A A
    WHERE B.age =NULL
    AND A.city = '深圳市');

-- 2. 对NULL 使用“=”后,结果为 unknown
SELECT *FROM t_student_B B
WHERE NOT EXISTS ( 
    SELECT *FROM t_student_A A
    WHERE unknown
    AND A.city = '深圳市');

-- 3. 如果AND 运算里包含 unknown,结果不会是true
SELECT *FROM t_student_B B
WHERE NOT EXISTS ( 
    SELECT *FROM t_student_A A
    WHERE false或 unknown
);

-- 4. 子查询没有返回结果,因此相反地,NOT EXISTS 为 trueSELECT *FROM t_student_B B
WHERE true;

也就是说,yzb 被作为 “与任何人的年龄都不同的人” 来处理了。EXISTS 只会返回 true 或者false,永远不会返回 unknown。因此就有了IN 和 EXISTS可以互相替换使用,而 NOT IN和 NOT EXISTS 却不可以互相替换的混乱现象。

还有一些其他的陷阱,比如:限定谓词和 NULL、限定谓词和极值函数不是等价的、聚合函数和 NULL 等等。

总结

1、NULL 用于表示缺失的值或遗漏的未知数据,不是某种具体类型的值,不能对其使用谓词

2、对 NULL 使用谓词后的结果是 unknown,unknown 参与到逻辑运算时,SQL 的运行会和预想的不一样

3、 IS NULL 整个是一个谓词,而不是:IS 是谓词,NULL 是值;类似的还有 ISTRUE、IS FALSE

4、要想解决 NULL 带来的各种问题,最佳方法应该是往表里添加 NOT NULL 约束来尽力排除 NULL

我的项目中有个硬性规定:所有字段必须是 NOT NULL,建表的时候就加上此约束

免责声明:文章转载自《神奇的 SQL 之温柔的陷阱 → 三值逻辑 与 NULL !》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Loadrunner中进行md5加密攻防世界 — Web进阶题(第11下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

日记账导入API

PROCEDURE IMPORT_GL_INTERFACE IS CURSOR CUR_GL_TEMP IS SELECT GL.REFERENCE1, --批名 GL.REFERENCE2, --批说明 GL.GL_NAME, --日记账 GL.DESCRIPTION, --日记账说明 GL.SET_BOOK_NAME, --分类账套 GL.PERIO...

展开BOM并使用最终用量的算法(转载)

本文系转载子ITPUB,如果有侵犯您权益的地方,烦请及时的告知与我,我即刻将停止侵权行为: 网址:http://www.itpub.net/thread-1020586-1-1.html http://www.itpub.net/thread-1020772-3-1.html http://www.itpub.net/thread-1020712-1-1....

Sql Server的艺术(七) SQL 数据插入操作

--用INSERT插入单行数据    在SQL中,可以通过INSERT...VALUES语句直接向数据库表中插入数据。可以整行,也可以部分列。 基本语法: INSERT INTO table_name [column1,column2...] VALUES (values1,values2...

mysql性能监控相关

如果是root帐号,你能看到所有用户的当前连接。如果是其它普通帐号,只能看到自己占用的连接 怎么进入mysql命令行呢? mysql的安装目录下面有个bin目录,先用命令行进入该目录,然后用 mysql -uroot -p123456 来登录(注意:用户名和密码不用包含“”) 命令: show processlist; 如果是root帐号,你能看到所有用户...

利用epoll实现异步IO

  之前异步IO一直没搞明白,大致的理解就是在一个大的循环中,有两部分:第一部分是监听事件;第二部分是处理事件(通过添加回调函数的方式)。就拿网络通信来说,可以先通过调用 select 模块中的 select 监听各个 socket 。当 socket 有事件到来时,针对相应的事件做出处理,就这么一直循环下去。所以异步IO也被称为事件驱动IO。原理其实我说...

MySQL之增删改查

前言:以下是MySQL最基本的增删改查语句,很多IT工作者都必须要会的命令,也是IT行业面试最常考的知识点,由于是入门级基础命令,所有所有操作都建立在单表上,未涉及多表操作。 前提:在进行“增删改查”的操作之前,先建立一个包含数据表student的数据库(具体操作可以见MySQL之最基本命令): 1、“增”——添加数据 1.1 为表中所有字段添加数据 1...