神奇的 SQL 之温柔的陷阱 → 三值逻辑 与 NULL !
前言
开心一刻
主持人问:“Who is your favorite singer ?”
翻译:”你最喜欢哪个歌手啊 ?”
小孩兴奋地回答:”Michael Jackson”
翻译转身对主持人说:”迈克尔-杰克逊”
主持人看着翻译:"你说什么 ?"
NULL
NULL 用于表示缺失的值或遗漏的未知数据,不是某种具体类型的值。数据表中的 NULL 值表示该值所处的字段为空,值为 NULL 的字段没有值,尤其要明白的是:NULL 值与 0 或者空字符串是不同的。
两种 NULL
这种说法大家可能会觉得很奇怪,因为 SQL 里只存在一种 NULL 。然而在讨论 NULL 时,我们一般都会将它分成两种类型来思考:“未知”(unknown)和“不适用”(not applicable,inapplicable)。
以“不知道戴墨镜的人眼睛是什么颜色”这种情况为例,这个人的眼睛肯定是有颜色的,但是如果他不摘掉眼镜,别人就不知道他的眼睛是什么颜色。这就叫作未知。而“不知道冰箱的眼睛是什么颜色”则属于“不适用”。因为冰箱根本就没有眼睛,所以“眼睛的颜色”这一属性并不适用于冰箱。“冰箱的眼睛的颜色”这种说法和“圆的体积”“男性的分娩次数”一样,都是没有意义的。平时,我们习惯了说“不知道”,但是“不知道”也分很多种。“不适用”这种情况下的 NULL ,在语义上更接近于“无意义”,而不是“不确定”。这里总结一下:“未知”指的是“虽然现在不知道,但加上某些条件后就可以知道”;而“不适用”指的是“无论怎么努力都无法知道”。
关系模型的发明者 E.F. Codd 最先给出了这种分类。下图是他对“丢失的信息”的分类
为什么必须写成“IS NULL”,而不是“= NULL”
我相信不少人有这样的困惑吧,尤其是相信刚学 SQL 的小伙伴。我们来看个具体的案例,假设我们有如下表以及数据
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;
执行时不报错,但是查不出我们想要的结果, 这是为什么了 ? 这个问题我们先放着,我们往下看
三值逻辑
这个三值逻辑不是三目运算,指的是三个逻辑值,有人可能有疑问了,逻辑值不是只有真(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
AND
OR
图中蓝色部分是三值逻辑中独有的运算,这在二值逻辑中是没有的。其余的 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 → true
NOT (b <> c) → unknown
“IS NULL” 而非 “= NULL”
我们再回到问题:为什么必须写成“IS NULL”,而不是“= NULL”
对 NULL 使用比较谓词后得到的结果总是 unknown 。而查询结果只会包含 WHERE 子句里的判断结果为 true 的行,不会包含判断结果为 false 和 unknown 的行。不只是等号,对 NULL 使用其他比较谓词,结果也都是一样的。所以无论 remark 是不是 NULL ,比较结果都是 unknown ,那么永远没有结果返回。以下的式子都会被判为 unknown
-- 以下的式子都会被判为 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;
咋一看,这不就是查询表中全部记录吗? 我们来看下实际结果
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 = 20
OR age <> 20
OR 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 = '深圳市'
);
我们来看下执行结果
我们发现结果是空,查询不到任何数据,这是为什么了 ?这里 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 的行的
-- 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 为 true
SELECT * 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 是值;类似的还有 IS TRUE、IS FALSE
4、要想解决 NULL 带来的各种问题,最佳方法应该是往表里添加 NOT NULL 约束来尽力排除 NULL
我的项目中有个硬性规定:所有字段必须是 NOT NULL,建表的时候就加上此约束
参考
《SQL进阶教程》
navicat
https://gitee.com/youzhibing/tools/blob/master/NavicatforMySQL.rar
神奇的 SQL 之温柔的陷阱 → 三值逻辑 与 NULL !的更多相关文章
- SQL进阶系列之3三值逻辑与NULL
写在前面 普通编程语言里的布尔型只有true和false两个值,这种逻辑体系被称为二值逻辑,而SQL语言里,还有第三个值unknown,因此SQL的逻辑体系被称为三值逻辑. Why SQL存在三值逻辑 ...
- 神奇的 SQL 之谓词 → 难理解的 EXISTS
前言 开心一刻 我要飞的更高,飞的更高,啊! 谓词 SQL 中的谓词指的是:返回值是逻辑值的函数.我们知道函数的返回值有可能是数字.字符串或者日期等等,但谓词的返回值全部是逻辑值(TRUE/FALSE ...
- 神奇的 SQL 之扑朔迷离 → ON 和 WHERE,好多细节!
开心一刻 楼主:心都让你吓出来了! 狮王:淡定,打个小喷嚏而已 前情回顾 神奇的 SQL 之 联表细节 → MySQL JOIN 的执行过程(一)中,我们讲到了 3 种联表算法:SNL.BNL 和 I ...
- 神奇的 SQL 之擦肩而过 → 真的用到索引了吗
开心一刻 今天下班,骑着青桔电动车高高兴兴的哼着曲回家,感觉整个世界都是我的 刚到家门口,还未下车,老妈就气冲冲的走过来对我说道:"你表哥就比你大一岁,人家都买了奔驰了,50 多万!&quo ...
- 那些年我们踩过的坑,SQL 中的空值陷阱!
文章目录 NULL 即是空 三值逻辑 空值比较 NOT IN 与空值 函数与空值 DISTINCT.GROUP BY.UNION 与空值 ORDER BY 与空值 空值处理函数 字段约束与空值 SQL ...
- 神奇的 SQL 之 联表细节 → MySQL JOIN 的执行过程(二)
开心一刻 一头母牛在吃草,突然一头公牛从远处狂奔而来说:“快跑啊!!楼主来了!” 母牛说:“楼主来了关我屁事啊?” 公牛急忙说:“楼主吹牛逼呀!” 母牛大惊,拔腿就跑,边跑边问:“你是公牛你怕什么啊? ...
- 神奇的 SQL 之 ICP → 索引条件下推
开心一刻 楼主:来,我们先排练一遍 小伙伴们:好 嘿.哈.嚯 楼主:非常好,就是这个节奏,我们开始吧 楼主:啊.啊.啊,疼 ! 你们是不是故意的 ? 回表与覆盖索引 正式讲 ICP 之前了,我们先将相 ...
- 神奇的 SQL 之 HAVING → 容易被轻视的主角
开心一刻 一天,楼主和隔壁小男孩一起坐电梯,中途进来一位高挑的美女,她牵着一条雪白的贵宾犬 小男孩看着这条雪白的贵宾犬,甚是喜欢,说道:阿姨,我能摸下这个狗狗吗? 美女:叫姐姐 小男孩低头看了下贵宾犬 ...
- 神奇的 SQL 之性能优化 → 让 SQL 飞起来
开心一刻 一天,一个男人去未婚妻家玩,晚上临走时下起了大雨 未婚妻劝他留下来过夜,说完便去准备被褥,准备就绪后发现未婚夫不见了 过了好久,全身淋的像只落汤鸡的未婚夫回来了 未婚妻吃惊的问:" ...
随机推荐
- kuangbin专题 专题一 简单搜索 Prime Path POJ - 3126
题目链接:https://vjudge.net/problem/POJ-3126 题意:给你两个四位的素数N,M,每次改变N四位数中的其中一位,如果能经过有限次数的替换变成四位数M,那么求出最少替换次 ...
- WordPress教程之如何创建博客内容
上两篇教程的链接: Wordpress教程之初识WordPress Wordpress教程之如何入门WordPress Hostwinds共享主机vps限时五折优惠链接 现在,你的 WordPress ...
- 哈工大计算机网络Week3-传输层
目录 学习目标 传输层服务概述 传输层服务和协议 传输层 vs. 网络层 Internet传输层协议 多路复用和多路分用 多路复用/分用 分用如何工作? 无连接分用(UDP) 面向连接的分用 面向连接 ...
- 记一次linux服务器入侵应急响应
近日接到客户求助,他们收到托管电信机房的信息,通知检测到他们的一台服务器有对外发送攻击流量的行为.希望我们能协助排查问题. 一.确认安全事件 情况紧急,首先要确认安全事件的真实性.经过和服务器运维人员 ...
- MySQL Explain查看执行计划
这篇博客写的还是蛮全的 http://www.cnblogs.com/songwenjie/p/9409852.html 在执行SQL的时候,经常有些SQL未正确使用到索引,导致程序变慢,通过使用ex ...
- 完全平方数(C语言实现)
一.题目 一个整数,它加上100后是一个完全平方数请问该数是多少? 二.程序分析 1.题目中没有限定这个整数的范围,因此,可以在代码中#define scope 10000,即使用scope变量定义一 ...
- python对Excel的读取
在python自动化中,经常会遇到对数据文件的操作,比如添加多名员工,但是直接将员工数据写在python文件中,不但工作量大,要是以后再次遇到类似批量数据操作还会写在python文件中吗? 应对这一问 ...
- nu.xom:Element
Element: 机翻 Element(Element element) :通过深复制,创建一个element Element(String name) :创建一个没有命名空间的element Ele ...
- 【UVA - 10006 】Carmichael Numbers (快速幂+素数筛法)
-->Carmichael Numbers Descriptions: 题目很长,基本没用,大致题意如下 给定一个数n,n是合数且对于任意的1 < a < n都有a的n次方模n等于 ...
- ASP.NET 前端数据绑定---<%#%>及Eval()的使用
ASP.NET 前端html代码中会经常出现的<%%>的代码,里面的文本其实就是不能直接输出到客户端浏览器的文本,是需要服务器解释的. 在ASP中,<%%>里面的文本是vbsc ...