SQL NULL 值解释:IS NULL、COALESCE、NULLIF 和要避免的陷阱

  1. 首页
  2. 博客
  3. 数据库和 SQL

数据库与 SQL

SQL NULL 值解释:IS NULL、COALESCE、NULLIF 和要避免的陷阱

NULL 会导致 SQL 中一些最安静的错误,查询运行时没有错误,但会丢失行或平均偏差。通过经过测试的示例了解 NULL 的实际行为,以及如何安全地处理它。

阅读约 9 分钟
SQL NULL 值解释:IS NULL、COALESCE、NULLIF 以及要避免的陷阱

要点速览

  • NULL 表示缺失或未知值。它不为零,也不是空字符串。
  • 使用 IS NULL 进行测试,因为 = NULL 永远不会匹配任何内容。
  • 大多数使用 NULL 的计算都会给出 NULL,并且 WHERE 只保留绝对正确的行。
  • 聚合会跳过 NULL,这会悄悄改变平均值。

查询运行时没有错误并返回看起来合理的数字,但缺少行且平均值错误。 SQL NULL 值 通常是原因。 NULL 的行为与任何正常值不同,这让初学者和经验丰富的开发人员都感到困惑。

本指南准确展示了 NULL 的行为方式。我们在 SQLite 3.50 中运行每个查询,显示的结果来自该运行。大多数这种行为是标准 SQL,我们会注意到数据库的不同之处,例如 NULL 排序顺序以及 Oracle 对空字符串的处理。

什么是NULL?

NULL 是一个标记,表示值丢失或未知。它不是通常意义上的值,因此这些都是不同的东西:

价值 含义 电话列示例
空值 未知或未提供 我们不知道号码
'' 已知的空文本值 提交为空白
0 数字零 与文本无关,但与金额相关

这种差异对于金额尤其重要。奖金为 0 意味着该人什么也没收到。空奖金意味着还没有人知道。

样本表

CREATE TABLE employees (
  id INT, name TEXT, phone TEXT, bonus INT, manager_id INT
);

INSERT INTO employees VALUES
 (1,'Meera','555-0101',500,NULL),
 (2,'Arjun',NULL,300,1),
 (3,'Sara','',NULL,1),
 (4,'Ravi',NULL,NULL,2);

请注意,Sara 的电话号码为空字符串,而 Arjun 和 Ravi 的电话号码为 NULL。两者的待遇截然不同。

测试 NULL

这是几乎每个人一开始都会犯的错误:

SELECT name FROM employees WHERE phone = NULL;
-- (no rows)

SELECT name FROM employees WHERE phone IS NULL;
-- Arjun
-- Ravi

SELECT name FROM employees WHERE phone IS NOT NULL;
-- Meera
-- Sara

第一个查询是有效的 SQL 并且没有给出错误。它只是不返回任何内容,因为将任何内容与 NULL 进行比较都不会成立。 Sara 显示在 IS NOT NULL 下,因为空字符串是实值。但在 Oracle 中,空字符串被视为 NULL,这是一个值得注意的例外。

三值逻辑

在大多数编程语言中,条件是 true 或 false。 SQL 添加第三个结果:未知。如果您不知道某个值,则无法判断它是否等于 5。并且不知道两个未知值是否相等,这就是 NULL = NULL 不成立的原因。在我们的测试中,所有这些都返回 NULL:

SELECT NULL = NULL, NULL <> 1, 1 + NULL, 'a' || NULL;

-- NULL | NULL | NULL | NULL

组合条件遵循仔细的规则。以下是我们的运行结果,其中 1 为 true,0 为 false:

表达 结果 为什么
真与空 空值 取决于未知部分
假和空 错误 无论未知是什么都是假的
真或空 真实 无论未知是什么,都是真实的
假或空 空值 取决于未知部分
非空 空值 未知的反义词是未知

从 WHERE 消失的行

WHERE 仅当条件为 true 时才保留行。结果未知的行以及错误的行都会被删除。这导致了令人惊讶的结果:

SELECT name FROM employees WHERE phone <> '555-0101';

-- Sara

您可能会期待阿琼和拉维出现,因为他们的电话肯定不是那个号码。但他们的电话不详,所以结果未知,他们就消失了。要包含它们,请明确说明:WHERE phone <> '555-0101' OR phone IS NULL。否定也无济于事:

SELECT name FROM employees WHERE NOT (bonus > 400);

-- Arjun

Sara和Ravi没有奖金,所以他们既没有出现在bonus > 400中,也没有出现在它的对立面中。这是要记住的关键一点:条件及其否定并不总是覆盖每一行。

COALESCE:提供后备

COALESCE 返回其列表中的第一个非 NULL 值,并且它适用于所有主要数据库:

SELECT COALESCE(NULL, NULL, 'third', 'fourth');
-- third

SELECT name, COALESCE(phone, 'no phone') FROM employees;
-- Meera | 555-0101
-- Arjun | no phone
-- Sara  |
-- Ravi  | no phone

Sara 仍然显示为空白,因为空字符串不为 NULL。类似的功能也存在,例如 MySQL 和 SQLite 中的 IFNULL、SQL Server 中的 ISNULL 以及 Oracle 中的 NVL。 COALESCE 是便携式的选择。我们之前关于计算的观点也适用于此。 bonus + 100 对于没有奖金的任何人都给出 NULL,因此如果将缺失的奖金视为零对于您的业务来说是正确的,请填写 COALESCE(bonus, 0) + 100。

NULLIF:将一个值变成NULL

如果两个值相等,NULLIF(a, b) 返回 NULL,否则返回 a。有两种用途特别有用。它可以清理占位符值:

SELECT name, NULLIF(phone, '') FROM employees WHERE id = 3;
-- Sara | NULL

SELECT name FROM employees
WHERE COALESCE(NULLIF(phone, ''), '') = '';
-- Arjun
-- Sara
-- Ravi

第二个查询是查找“缺失或空白”值的便捷方法。它还可以防止被零除,这会在许多数据库中引发错误:

SELECT total / NULLIF(quantity, 0) FROM sales;

当 quantity 为 0 时,这会给出 NULL 而不是失败。在我们的测试中,SELECT 100 / NULLIF(0, 0) 返回 NULL。

聚合跳过 NULL

这是 NULL 对报告造成最大损害的地方:

SELECT COUNT(*), COUNT(bonus), SUM(bonus), AVG(bonus),
       AVG(COALESCE(bonus, 0))
FROM employees;

-- 4 | 2 | 800 | 400.0 | 200.0
表达 结果 这意味着什么
COUNT(*) 4 所有行
COUNT(bonus) 2 仅限有奖金的行
SUM(bonus) 800 NULL 被忽略
AVG(bonus) 400 800 除以 2,而不是除以 4
AVG(合并(奖金, 0)) 200 800除以4

那么平均奖金是多少,400还是200?这取决于 NULL 在数据中的含义。如果 NULL 表示“没有支付奖金”,那么 200 是正确的。如果它的意思是“尚未决定”,那么400可能是正确的。数据库无法为你做出这个决定,所以这个问题必须交给了解业务的人。

一个相关的陷阱:没有值的 SUM 是 NULL 而不是 0。当我们只对没有奖励的行求和时,我们得到 NULL,而 COUNT 返回 0。期望数字的应用程序代码可能会崩溃,所以将其包装:COALESCE(SUM(bonus), 0)。

提示: 当报告总计看起来不正常时,请将 COUNT(*) 与 COUNT(column) 进行比较。它们之间的任何间隙都会立即告诉您该列有多少个 NULL。

GROUP BY、排序和 DISTINCT

尽管 NULL 在比较中“不相等”,但这些操作将它们视为一组:

SELECT bonus, COUNT(*) FROM employees GROUP BY bonus ORDER BY bonus;
-- NULL | 2
--  300 | 1
--  500 | 1

SELECT DISTINCT bonus FROM employees;
-- 500, 300, NULL

有关分组的更多信息,请参阅 SQL GROUP BY 和 HAVING。排序顺序是数据库真正不同的地方。 SQLite和MySQL将NULL视为最小值,而PostgreSQL和Oracle将其视为最大。因此,相同的 ORDER BY bonus DESC 可以将 NULL 放在一个中的最后,而将 NULL 放在另一个中的第一个。 PostgreSQL、Oracle 和现代 SQLite 允许您使用 NULLS FIRST 或 NULLS LAST 直接说明这一点。

加入

使用 = 的连接条件永远不会匹配 NULL,因此具有 NULL 键的行会从内部连接中删除:

SELECT e.name FROM employees e
JOIN employees m ON m.id = e.manager_id;
-- Arjun, Sara, Ravi    (Meera is gone)

SELECT e.name, m.name FROM employees e
LEFT JOIN employees m ON m.id = e.manager_id;
-- Meera | NULL
-- Arjun | Meera
-- Sara  | Meera
-- Ravi  | Arjun

Meera 没有经理,因此内部联接会默默地删除该行。 A LEFT JOIN 保留它,并且它还为缺失的一侧创建自己的 NULL。请阅读 SQL 连接解释 来了解更多信息。

NOT IN 陷阱

NOT IN 列表中的单个 NULL 会清除所有结果:

SELECT name FROM employees WHERE manager_id NOT IN (1);
-- Ravi

SELECT name FROM employees WHERE manager_id NOT IN (1, NULL);
-- (no rows)

第二种形式意味着manager_id <> 1 AND manager_id <> NULL,而最后一部分永远不可能是真的。这对于子查询尤其危险,因为 NULL 可能会在不被注意的情况下到达。请改用 NOT EXISTS,如我们的 SQL 子查询教程 中所述。第一个结果中也缺少 Meera,因为该行的 manager_id 为 NULL。

使用 NULL 进行设计

  • 默认使用NOT NULL。 仅当值确实可能缺失时才允许 NULL。
  • 添加合理的默认值。例如,quantity INT NOT NULL DEFAULT 0。
  • 请勿使用魔法值。 日期 1900-01-01 或代表“未知”的年龄 -1 会破坏您的计算。
  • 了解 UNIQUE 行为。 大多数数据库允许在 UNIQUE 列中存在多个 NULL,并且我们的测试插入了两个 NULL,没有错误。 SQL Server 是一个例外,因为它通常允许这样做。
  • 请记住主键不能为 NULL。 请参阅主键与外键。
  • 使用 NULL 进行测试。 不使用 NULL 的测试数据隐藏了这些错误。有意将它们包括在内,就像其他边缘情况一样。

最后的话

将 NULL 视为“未知”,它的大部分行为开始变得有意义:未知加一是未知,并且未知等于未知不知道是否为真。本周审核你自己的一份报告。使用 COUNT(*) 对照 COUNT(column) 检查可空列,查找任何 NOT IN,并确定每个平均值中 NULL 的含义。

常见问题

SQL中的NULL是什么?

NULL 是缺失值或未知值的标记。它不同于零和空字符串,并且在比较时需要特殊处理。

为什么 = NULL 不起作用?

将任何内容与 NULL 进行比较都会给出未知而不是 true,因此 WHERE 会丢弃该行。使用 IS NULL 或 IS NOT NULL 来测试它。

COALESCE 和 ISNULL 有什么区别?

COALESCE 是标准 SQL,接受许多参数。 ISNULL 特定于 SQL Server,需要两个。对于可移植代码,首选 COALESCE。

COUNT 是否包含 NULL 值?

COUNT(*) 对所有行进行计数,包括具有 NULL 的行。 COUNT(column) 仅计算该列不为 NULL 的行,这可以给出较小的数字。

NULL 和空字符串一样吗?

不,在大多数数据库中。空字符串是没有字符的已知值,NULL 表示未知。 Oracle 是一个值得注意的例外,因为它将空字符串视为 NULL。

继续学习

与Stack Quora合作

需要测试、WordPress 或 SEO 方面的帮助?

我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。

继续阅读

更多指南 测试、人工智能和现代开发

喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。

人工智能

人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它

人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。

阅读文章 →
DevOps

环境变量解释:.env 文件、秘密和配置正确完成

使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。

阅读文章 →
软件开发

API 速率限制解释:算法、标头和处理 429 错误

API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。

阅读文章 →
测试自动化

Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误

使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。

阅读文章 →
软件测试

代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义

用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。

阅读文章 →
数据库与 SQL

SQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们

DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。

阅读文章 →
数据库与 SQL

SQL 视图解释:创建、使用和避免陷阱(附示例)

SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。

阅读文章 →
软件开发

TypeScript 接口与类型:真正的差异,使用编译器进行测试

TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。

阅读文章 →
软件开发

Python *args 和 **kwargs 解释:灵活的函数和清晰的示例

Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。

阅读文章 →
软件开发

Python Lambda 函数:语法、实际用途以及何时不使用它们

Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。

阅读文章 →
人工智能

什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环

什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。

阅读文章 →
DevOps

监控与可观察性:真正的区别,包括日志、指标和跟踪

监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。

阅读文章 →
DevOps

Docker Volumes 与 Bind Mounts:以正确的方式保留容器数据

Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。

阅读文章 →
测试自动化

TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式

TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。

阅读文章 →
软件测试

用户验收测试 (UAT):流程、清单和真实示例

用户验收测试解释:谁运行它、它与 QA 有何不同、七步流程、UAT 测试用例模板、清单、签核标准和常见错误。

阅读文章 →
数据库与 SQL

SQL NULL 值解释:IS NULL、COALESCE、NULLIF 和要避免的陷阱

通过我们运行的查询解释了 SQL NULL 值:三值逻辑、IS NULL、COALESCE、NULLIF,聚合、连接和排序如何处理 NULL,以及它悄悄引起的错误。

阅读文章 →
数据库与 SQL

SQL CASE WHEN 语句:语法、实例和常见错误

SQL CASE WHEN 解释了我们运行的查询:简单和搜索表单、条件顺序、SUM 透视、COUNT 技巧、自定义排序、更新和要避免的 NULL 陷阱。

阅读文章 →
软件开发

什么是智威汤逊?通过工作示例解释 JSON Web 令牌

什么是智威汤逊? JSON Web Tokens 解释:这三个部分,在我们运行的代码中构建和篡改的令牌,JWT 与会话,存储,刷新令牌和安全错误。

阅读文章 →
软件开发

JavaScript 事件循环解释:调用堆栈、微任务和计时器以及示例

JavaScript 事件循环用我们运行的代码进行了解释:调用堆栈、任务和微任务队列、为什么 setTimeout 0 不是立即执行、阻塞、饥饿和面试测验。

阅读文章 →
软件开发

Python 生成器和 Yield 解释:通过真实示例节省内存

Python 生成器和yield 用我们运行的代码进行了解释:它们如何暂停和恢复、测量内存节省、管道、无限序列、yield from、发送和常见陷阱。

阅读文章 →
人工智能

什么是嵌入?向量和余弦相似度简单解释

什么是嵌入?关于向量、余弦相似度和语义搜索的简单语言指南,其中包含我们运行的一个小示例、常见用途、选择模型和已知限制。

阅读文章 →
DevOps

蓝绿部署与金丝雀部署:差异、权衡以及如何选择

蓝绿部署与金丝雀部署解释:每种部署的工作原理、成本、风险和回滚、我们测试的稳定用户分桶、数据库迁移、滚动更新以及如何选择。

阅读文章 →
测试自动化

不稳定的测试:造成这些问题的原因以及如何彻底解决它们

我们运行的演示解释了不稳定的测试:主要原因、如何确认和诊断、正确的时间修复、共享状态和日期以及隔离过程。

阅读文章 →
软件测试

负载测试与压力测试:差异、指标和 k6 示例

负载测试与压力测试的解释:各自证明什么、峰值和浸泡测试、重要的指标、为什么平均值会误导、k6 个示例以及如何计划测试。

阅读文章 →
软件测试

测试用例与测试场景:主要区别、示例以及何时使用它们

测试用例与测试场景的解释:清晰的定义、并排的表格、从场景到案例的密码重置功能、模板以及何时使用每个功能。

阅读文章 →
数据库与 SQL

SQL 查询优化:读取执行计划并修复慢速查询(带计时)

对一百万行进行 SQL 查询优化测试:读取执行计划、添加正确的索引、修复不可控制的过滤器、使用复合索引和键集分页。

阅读文章 →
数据库与 SQL

SQL UNION 与 UNION ALL:差异、示例和性能测试

SQL UNION 与 UNION ALL 通过我们运行的查询进行了解释:重复、都遵循的规则、真实的数据错误、测量的性能、INTERSECT 和 EXCEPT 以及何时使用它们。

阅读文章 →
软件开发

JavaScript Fetch API 教程:GET、POST、错误处理和超时

针对本地服务器测试的 JavaScript Fetch API 教程:GET 和 POST、为什么不抛出 404、可重用的帮助程序、查询字符串、超时、CORS 和常见错误。

阅读文章 →
软件开发

Python 文件处理:读取、写入、CSV、JSON 和 pathlib 以及示例

使用我们运行的代码解释了 Python 文件处理:打开模式、使用、读取大文件、CSV、JSON、pathlib、二进制文件、安全写入、错误处理和常见错误。

阅读文章 →
软件开发

Python 类和对象:带有实际示例的 OOP 教程

用我们运行的代码解释 Python 类和对象:__init__、方法、类与实例属性、继承、属性、dunder 方法、数据类和常见错误。

阅读文章 →
人工智能

什么是大语言模型?法学硕士如何运作,在没有炒作的情况下进行解释

什么是大语言模型?法学硕士用简单的语言解释:标记、嵌入、注意力、训练、温度和上下文窗口、一个微小的下一个单词演示和真正的限制。

阅读文章 →
DevOps

适合初学者的 Linux 命令:带有示例的基本终端指南

适合初学者的 Linux 命令,按任务分组:导航、文件、查看日志、grep、管道、权限、进程和档案,并附有实用的日志分析示例。

阅读文章 →
测试自动化

Selenium 与 Playwright:主要区别、代码比较以及选择

Selenium 与 Playwright 比较:架构、等待、浏览器和语言支持、调试、两种工具中的相同测试、迁移建议以及如何选择。

阅读文章 →
软件测试

等价划分:通过实例解释测试设计技术

等效分区解释:如何找到有效和无效分区,年龄、密码和优惠券的工作示例,我们运行的测试,以及为什么将其与边界配对。

阅读文章 →
软件测试

软件测试中的严重性与优先级:差异、矩阵和实例

软件测试中的严重性与优先级解释:清晰的定义、带有示例的四向矩阵、级别尺度、谁设置每个级别以及如何运行错误分类。

阅读文章 →
数据库与 SQL

SQL 子查询教程:类型、相关子查询和 EXISTS 以及示例

SQL 子查询教程,其中包含我们运行的查询:标量、IN、FROM 和 SELECT 子查询、相关子查询、EXISTS、NOT IN NULL 陷阱以及子查询与 JOIN 与 CTE。

阅读文章 →
数据库与 SQL

SQL 事务和 ACID 解释:COMMIT、ROLLBACK 和隔离级别

SQL 事务和 ACID 通过我们运行的银行转账进行了解释:COMMIT、ROLLBACK、保存点、锁、隔离级别、脏读、死锁和最佳实践。

阅读文章 →
软件开发

Git merge 与 rebase 的区别:示例及如何选择

用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。

阅读文章 →
软件开发

Python 列表推导式:语法、示例与何时避免使用

Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。

阅读文章 →
数据库与 SQL

SQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)

通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。

阅读文章 →