那些年我们踩过的坑,SQL 中的空值陷阱!
文章目录
- NULL 即是空
- 三值逻辑
- 空值比较
- NOT IN 与空值
- 函数与空值
- DISTINCT、GROUP BY、UNION 与空值
- ORDER BY 与空值
- 空值处理函数
- 字段约束与空值
SQL 是一种声明式的语言,我们只需要描述想要的结果(WHAT),而不关心数据库如何实现(HOW);虽然 SQL 比较容易学习,但是仍然有一些容易混淆和出错的概念。
今天我们就来说说 SQL 中的空值陷阱和避坑方法,涉及的数据库包括 MySQL、Oracle、SQL Server、PostgreSQL 以及 SQLite。还是老规矩,结论先行:
NULL 特性 | MySQL | Oracle | SQL Server | PostgreSQL | SQLite |
---|---|---|---|---|---|
三值逻辑 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
空值比较 | IS [NOT] NULL expr <=> NULL |
IS [NOT] NULL '' IS NULL |
IS [NOT] NULL | IS [NOT] NULL IS [NOT] DISTINCT FROM NULL |
IS [NOT] NULL |
NOT IN (NULL) | 不返回结果 | 不返回结果 | 不返回结果 | 不返回结果 | 不返回结果 |
函数/表达式 NULL 参数 | 结果为 NULL | 结果为 NULL CONCAT 函数和 || 例外 |
结果为 NULL CONCAT 函数例外 |
结果为 NULL CONCAT 函数例外 |
结果为 NULL |
聚合函数 | 忽略 NULL 数据 COUNT(*) 除外 |
忽略 NULL 数据 COUNT(*) 除外 |
忽略 NULL 数据 COUNT(*) 除外 |
忽略 NULL 数据 COUNT(*) 除外 |
忽略 NULL 数据 COUNT(*) 除外 |
DISTINCT GROUP BY PARTITION BY UNION |
所有空值分为一组 | 所有空值分为一组 | 所有空值分为一组 | 所有空值分为一组 | 所有空值分为一组 |
ORDER BY | 默认空值最小 | 默认空值最大 支持 NULLS FIRST | LAST |
默认空值最小 | 默认空值最大 支持 NULLS FIRST | LAST |
默认空值最小 支持 NULLS FIRST | LAST |
COALESCE 函数 NULLIF 函数 |
✔️ IFNULL(expr1, expr2) IF(expr1, expr2, expr3) |
✔️ NVL(expr1, expr2) NVL2(expr1, expr2, expr3) |
✔️ ISNULL(expr1, expr2) |
✔️ | ✔️ |
唯一约束允许多个空值 | ✔️ | ✔️ | ❌ | ✔️ | ✔️ |
检查约束允许插入空值 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
本文使用的示例数据可以点击链接《SQL 入门教程》示例数据库下载。
NULL 即是空
在数据库中,空值(NULL)是一个特殊的值,通常用于表示缺失值或者不适用的值。比如,填写问卷时不愿意透露某些信息会导致录入项的缺失,在公司的组织结构中总会有一个人(董事长/总经理)没有上级领导。
首先一点,空值与数字 0 并不相同。假如我问你:你的钱包里有多少钱?如果你知道里面没有钱,可以说是零;如果你不确定,那么就是未知,但不能说没有。当我们需要创建一个表来存储这个信息的时候,应该是 NULL;除非我们能够确定钱包里面没有钱或者有多少钱。
另外,空值与空字符串(''
)也不相同,原因和上面类似。但是 Oracle 是一个例外,我们会在下文具体讨论。
在大多数编程语言中,访问 null 值通常会导致错误;但是 SQL 不会出错,只是会影响到运算的结果而已。
三值逻辑
在大多数编程语言中,逻辑运算的结果只有两种情况,不是真(True)就是假(False)。但是对于 SQL 而言,逻辑运算还可能是未知(Unknown):
引入三值逻辑主要是为了支持 NULL,因为 NULL 代表的是未知数据。因此,SQL 中的逻辑运算与(AND)、或(OR)以及非(NOT)的结果如下:
AND | 真 | 假 | 未知 |
---|---|---|---|
真 | 真 | 假 | 未知 |
假 | 假 | 假 | 假 |
未知 | 未知 | 假 | 未知 |
OR | 真 | 假 | 未知 |
---|---|---|---|
真 | 真 | 真 | 真 |
假 | 真 | 假 | 未知 |
未知 | 真 | 未知 | 未知 |
NOT | 结果 |
---|---|
真 | 假 |
假 | 真 |
未知 | 未知 |
对于 AND 运算符而言,真和未知的与运算有可能是真,也有可能是假;因此,最终的结果是未知。
?SQL 中的 WHERE、HAVING 以及 CASE WHEN 子句只返回逻辑运算结果为真的数据,不返回结果为假或未知的数据。
空值比较
当我们使用比较运算符(=、<>、<、> 等)与 NULL 进行比较时,结果既不是真也不是假,而是未知;因为 NULL 表示未知,也就意味着可能是任何值。以下运算的结果都是未知:
NULL = 0
NULL <> 0
NULL <= 0
NULL = NULL
NULL != NULL
NULL 与任何值都不相等,甚至两个 NULL 也不想等;因为我们不能说两个未知的值相同,也不能说它们不相同。
⚠️对于比较运算而言,NULL 和 NULL 不相同;但是某些 SQL 子句中的 NULL 值被看作相同的值,例如 GROUP BY。具体参考下文。
那么,如何判断一个值是否是 NULL 呢?为此,SQL 引入了两个谓词(WHERE 子句):IS NULL
和IS NOT NULL
。以下示例用于查找 manager 为空的员工:
-- 使用比较运算符判断空值
SELECT employee_id, first_name, last_name, manager_id
FROM employees
WHERE manager_id = NULL;
employee_id|first_name|last_name|manager_id|
-----------|----------|---------|----------|
-- 使用 IS NULL 判断空值
SELECT employee_id, first_name, last_name, manager_id
FROM employees
WHERE manager_id IS NULL;
employee_id|first_name|last_name|manager_id|
-----------|----------|---------|----------|
100|Steven |King | |
其中,第一个查询使用比较运算符判断空值,不会返回任何结果;第二个查询使用 IS NULL 判断空值,返回了正确的结果。
除了标准的IS [NOT] NULL
之外,还有一些数据库扩展的运算符可以用于空值比较:
-- MySQL
SELECT employee_id, first_name, last_name, manager_id
FROM employees
WHERE manager_id <=> NULL;
employee_id|first_name|last_name|manager_id|
-----------|----------|---------|----------|
100|Steven |King | |
-- PostgreSQL
SELECT employee_id, first_name, last_name, manager_id
FROM employees
WHERE manager_id IS NOT DISTINCT FROM NULL;
employee_id|first_name|last_name|manager_id|
-----------|----------|---------|----------|
100|Steven |King | |
MySQL 中的<=>
可以用于等值比较,支持两个 NULL 值;PostgreSQL 中的IS [NOT] DISTINCT FROM
可以用于等值比较,支持两个 NULL 值。
以下查询的结果也不会返回任何结果:
SELECT employee_id, first_name, last_name, manager_id
FROM employees
WHERE (1 = NULL) OR (1 != NULL);
因为根据上面的三值逻辑,两个未知结果的 OR 运算最终还是未知。
前文我们说过,空字符串不是 NULL;但是 Oracle 中的空字符串被看作 NULL。例如:
-- Oracle
SELECT 1
FROM dual
WHERE '' IS NULL;
VAL|
---|
1|
-- 其他数据库
SELECT 1 AS val
WHERE '' IS NULL;
val|
---|
当然,我们如果使用等值(=)运算符判断空字符串与 NULL,结果仍然为空。
NOT IN 与空值
对于 WHERE 条件中的 IN 和 NOT IN 运算符,使用的是等值比较。所以如果 NOT IN 碰到了 NULL 值,永远不会返回任何结果。例如:
SELECT employee_id, first_name, last_name, manager_id
FROM employees
WHERE 1 NOT IN (NULL, 2);
因为上面的条件实际上等价于:
SELECT employee_id, first_name, last_name, manager_id
FROM employees
WHERE 1 != NULL AND 1 != 2;
1 不等于 NULL 的结果是未知,1 不等于 2 的结果是真,未知和真的 AND 运算结果还是未知。
⚠️如果使用 NOT IN,一定要确保括号中的值不会出现 NULL;或者尽量使用 NOT EXISTS。
函数与空值
一般来说,函数和表达式的参数中如果存在 NULL,其结果也是 NULL。当然也有一些例外,比如聚合函数。
以下查询返回的都是 NULL:
SELECT ABS(NULL), 1 + NULL
FROM employees
WHERE employee_id = 100;
ABS(NULL)|1 + NULL|
---------|--------|
[NULL]| [NULL]|
一个未知值的绝对值仍然未知,1 加上一个未知值结果还是未知。
但是一个常见的例外是字符串与 NULL 的连接:
-- Oracle、SQL Server、PostgreSQL
SELECT CONCAT('Hello', NULL)
FROM employees
WHERE employee_id = 100;
CONCAT('HELLO',NULL)|
--------------------|
Hello |
-- MySQL
SELECT CONCAT('Hello', NULL)
FROM employees
WHERE employee_id = 100;
CONCAT('Hello', NULL)|
---------------------|
[NULL]|
Oracle 将 NULL 看作空字符串,所以查询结果为“Hello”;SQL Server 和 PostgreSQL 虽然区分了 NULL 和空字符串,但是 CONCAT 函数中这两者等价;MySQL 中 NULL 参数导致 CONCAT 函数结果为 NULL;SQLite 没有提供 CONCAT 函数。
另外,Oracle 中的 || 也将 NULL 看作空字符串;其他数据库 || 中的 NULL 将参数会产生 NULL 结果;SQL Server 中使用 + 连接字符串,NULL 参数将会产生 NULL 结果。
聚合函数(SUM、COUNT、AVG 等)通常会在进行计算之前删除 NULL 数据:
SELECT SUM(salary + commission_pct) sum1,
SUM(salary) + SUM(commission_pct) sum2,
COUNT(salary),
COUNT(commission_pct)
FROM employees;
SUM1 |SUM2 |COUNT(SALARY)|COUNT(COMMISSION_PCT)|
--------|--------|-------------|---------------------|
311507.8|691423.8| 107| 35|
第一个 SUM 函数返回的是 salary 和 commission_pct 都不为空的数据总和;第而个 SUM 函数返回的是 salary 不为空的数据总和加上 commission_pct 不为空的数据总和,所以比第一个数据大;COUNT 函数结果显示 salary 有 107 条记录不为空,commission_pct 只有 35 条记录不为空。
如果输入数据都是 NULL 值,除了 COUNT 函数之外的其他聚合函数返回 NULL:
SELECT COUNT(*), COUNT(commission_pct), AVG(commission_pct), SUM(commission_pct)
FROM employees
WHERE commission_pct IS NULL;
COUNT(*)|COUNT(COMMISSION_PCT)|AVG(COMMISSION_PCT)|SUM(COMMISSION_PCT)|
--------|---------------------|-------------------|-------------------|
72| 0| [NULL]| [NULL]|
COUNT(*) 总是返回数据的行数,不受空值的影响;COUNT(commission_pct) 返回了零;AVG 和 SUM 返回了 NULL。
DISTINCT、GROUP BY、UNION 与空值
SQL 中的分组操作将所有的 NULL 值分到同一个组,包括 DISTINCT、GROUP BY 以及窗口函数中的 PARTITION BY。例如:
SELECT DISTINCT commission_pct
FROM employees;
commission_pct|
--------------|
[NULL]|
0.40|
0.30|
0.20|
0.25|
0.15|
0.35|
0.10|
SELECT commission_pct
FROM employees
GROUP BY commission_pct;
commission_pct|
--------------|
[NULL]|
0.40|
0.30|
0.20|
0.25|
0.15|
0.35|
0.10|
从上面的示例可以看出,commission_pct 为空的数据有 72 条,但是分组之后只有一个 NULL 组。
除此之外,UNION 操作符也将所有的 NULL 看作相同值:
SELECT manager_id
FROM employees
WHERE manager_id IS NULL
UNION
SELECT manager_id
FROM employees
WHERE manager_id IS NULL;
manager_id|
----------|
[NULL]|
如果将 UNION 换成 UNION ALL,查询结果将会保留 2 个 NULL 值。
ORDER BY 与空值
SQL 标准没有定义 NULL 值的排序顺序,但是为 ORDER BY 定义了 NULLS FIRST 和 NULLS LAST 选项,用于明确指定空值排在其他数据之前或者之后。
不同数据库对此提供了不同的实现:
SELECT employee_id, manager_id
FROM employees
WHERE employee_id IN (100, 101, 102)
ORDER BY manager_id;
-- Oracle、PostgreSQL
EMPLOYEE_ID|MANAGER_ID|
-----------|----------|
101| 100|
102| 100|
100| [NULL]|
-- MySQL、SQL Server、SQLite
employee_id|manager_id|
-----------|----------|
100| [NULL]|
101| 100|
102| 100|
其中,Oracle 和 PostgreSQL 默认将 NULL 作为最大值,升序时排在最后;MySQL、SQL Server 和 SQLite 默认将 NULL 作为最小值,升序时排在最前。
另外,Oracle、PostgreSQL 和 SQLite 提供了扩展的 NULLS FIRST 和 NULLS LAST 选项:
-- Oracle、PostgreSQL 和 SQLite
SELECT employee_id, manager_id
FROM employees
WHERE employee_id IN (100, 101, 102)
ORDER BY manager_id NULLS FIRST;
employee_id|manager_id|
-----------|----------|
100| [NULL]|
101| 100|
102| 100|
我们也可以使用 CASE 表达式实现类似的效果。以下示例与 NULLS LAST 作用相同,而且所有数据库都可以使用:
SELECT employee_id, manager_id
FROM employees
WHERE employee_id IN (100, 101, 102)
ORDER BY CASE WHEN manager_id IS NULL THEN 1
ELSE 0
END,
manager_id;
employee_id|manager_id|
-----------|----------|
101| 100|
102| 100|
100| [NULL]|
首先,CASE 表达式将 manager_id 为空的数据转换为 1,非空的数据转换为 0,所以空值排在其他数据之后;第二个排序字段 manager_id 确保了非空的数据从小到大排序。
空值处理函数
由于空值的特殊性,我们在分析数据时经常需要进行空值和其他值的转换。为此,SQL 提供了两个标准的空值函数:COALESCE 和 NULLIF。
COALESCE(exp1, exp2, …) 函数用于将 NULL 转换为其他值。当 exp1 不为空时返回 exp1,否则检查 exp2;如果 exp2 不为空时返回 exp2,依次类推。例如:
SELECT COALESCE(NULL, NULL, 3)
那些年我们踩过的坑,SQL 中的空值陷阱!的更多相关文章
- 关于生sql中的空值
在数据库中的空值无非就是两种形式的表示 一种是什么都没有,一种是以NuLL显示的 , 那么在C# 读取出来怎么判断呢, DtStatus.Rows[0]["FetchCode"]. ...
- 那些年我们一起踩过的坑(javascript常见的陷阱)
1.object最后一个逗号 定义object直接量或json,最后一个逗号多写了,在ie下会报错,高级浏览器则不会,给只使用chrome调试的同学敲个警钟.踩了无数次这个坑了. 2.自动加分号 ...
- 那些年我们踩过的坑之表单reset
开发者往往是在一个又一个的坑中成长起来的,自学的开发者尤其如此,刚刚填完一个坑,转身又掉进另一个坑.有些坑很容易就跳出来了,也有些坑能整了一天都没头绪,第二天早上一来发现后面就有一架通往坑外的梯子,坑 ...
- 那些年我们踩过的坑-NSTimer
昨天下午工作的时候遇见一个这样的需求,网络请求失败后把请求数据保存到本地,并自动重发3次,时间间隔是10秒,如果3次后还失败的话,下一次启动这个接口的时候,把新数据和保存在本地的数据都要发送,刚开始以 ...
- SQL中空值与NULL区别
很多人都有过这样的问题吧 在SQL中填充空值与NULL有什么区别 现在我以一个实例给大家分享一下自己的想法 恳请大家给予批评也指正 谢谢 创建一个监时表 CREATE TABLE #temp ( ...
- 项目中踩过的坑之-sessionStorage
总想写点什么,却不知道从何写起,那就从项目中踩过的坑开始吧,希望能给可能碰到相同问题的小伙伴一点帮助. 项目情景: 有一个id,要求通过当前网页打开一个新页面(不是当前页面),并把id传给打开的新页面 ...
- 使用ffmpeg视频编码过程中踩的一个坑
今天说说使用ffmpeg在写视频编码程序中踩的一个坑,这个坑让我花了好多时间,回头想想,非常多时候一旦思维定势真的挺难突破的.以下是不对的编码结果: ...
- git工作中常用命令-工作中踩过的坑
踩坑篇又来啦,这是我在工作中从git小白进化到现在工作中运用自如的过程中,踩过的坑,以及解决办法. 1.基于远程develop分支,建一个本地task分支,并切换到该task分支 git checko ...
- ng-zorro-antd中踩过的坑
ng-zorro-antd中踩过的坑 前端项目中,我们经常会使用阿里开源的组件库:ant-design,其提供的组件已经足以满足多数的需求,拿来就能直接用,十分方便,当然了,有些公司会对组件库进行二次 ...
随机推荐
- window.onresize事件
定义和用法 onresize 事件会在窗口或框架被调整大小时发生. 语法 In HTML: <element onresize="SomeJavaScriptCode"> ...
- Java JDK 1.5 1.6 1.7 新特性整理
Java JDK 1.5的新特性 1.泛型 List<String> strs = new ArrayList<String>();//给集合指定存入类型,上面这个集合在存入数 ...
- Scrapy 命令
Scrapy提供了两种类型的命令.一种必须在Scrapy项目中运行(针对项目(Project-specific)的命令),另外一种则不需要(全局命令).全局命令在项目中运行时的表现可能会与在非项目中运 ...
- Jmeter_选项_函数助手_RandomString的用法
1.用处:测试账户注册可以通过随机生成数实现,而不需要Excel手动输入, 缺点:随机生成数可能会重复 优点:不需要使用CSV config 或者excel ,txt格式 2.举例:之前我们通过CSV ...
- django登陆界面报错InternalError at /admin/polls/question/add/
登陆界面报错信息如下: 改为输入英文,正确
- C语言:将形参s所指字符串中所有ASCII码值小于97的字符存入形参t所指字符数组中,
//将形参s所指字符串中所有ASCII码值小于97的字符存入形参t所指字符数组中,形成一个新串,并统计出符合条件的字符个数返回. //关注点:使用*(t+n)的方式可以不改变指针的指向,像数组一样处理 ...
- 大数据计算引擎之Flink Flink CEP复杂事件编程
原文地址: 大数据计算引擎之Flink Flink CEP复杂事件编程 复杂事件编程(CEP)是一种基于流处理的技术,将系统数据看作不同类型的事件,通过分析事件之间的关系,建立不同的时事件系序列库,并 ...
- Linux - 软硬链接,hard link and symbolic link
- Python:函数基础
概念 一段代码,集中到一起,起一个名字,下一次可以使用这个名字调用这个代码块,就是函数的功能 作用: 方便代码的重用 分解任务,简化程序逻辑 使代码更加模块化 函数的分类 内建函数 第三方函数 自定义 ...
- Windows下MySQL5.7版本中修改编码为utf-8
我们新安装的MySQL数据库默认的字符是 latin1 ,所以每次新建数据库都要修改字符,非常麻烦.所以我们必须将它改成UTF8字符的. 修改方法如下: 一.修改MySQL的my.ini 首先在 \P ...