- 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。
继续学习
需要测试、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%覆盖率仍然会错过错误,以及合理的目标。
阅读文章 →数据库与 SQLSQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们
DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。
阅读文章 →数据库与 SQLSQL 视图解释:创建、使用和避免陷阱(附示例)
SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。
阅读文章 →软件开发TypeScript 接口与类型:真正的差异,使用编译器进行测试
TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。
阅读文章 →软件开发Python *args 和 **kwargs 解释:灵活的函数和清晰的示例
Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。
阅读文章 →软件开发Python Lambda 函数:语法、实际用途以及何时不使用它们
Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。
阅读文章 →人工智能什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环
什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。
阅读文章 →DevOps监控与可观察性:真正的区别,包括日志、指标和跟踪
监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。
阅读文章 →DevOpsDocker Volumes 与 Bind Mounts:以正确的方式保留容器数据
Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。
阅读文章 →测试自动化TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式
TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。
阅读文章 →软件测试用户验收测试 (UAT):流程、清单和真实示例
用户验收测试解释:谁运行它、它与 QA 有何不同、七步流程、UAT 测试用例模板、清单、签核标准和常见错误。
阅读文章 →数据库与 SQLSQL NULL 值解释:IS NULL、COALESCE、NULLIF 和要避免的陷阱
通过我们运行的查询解释了 SQL NULL 值:三值逻辑、IS NULL、COALESCE、NULLIF,聚合、连接和排序如何处理 NULL,以及它悄悄引起的错误。
阅读文章 →数据库与 SQLSQL 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 个示例以及如何计划测试。
阅读文章 →软件测试测试用例与测试场景:主要区别、示例以及何时使用它们
测试用例与测试场景的解释:清晰的定义、并排的表格、从场景到案例的密码重置功能、模板以及何时使用每个功能。
阅读文章 →数据库与 SQLSQL 查询优化:读取执行计划并修复慢速查询(带计时)
对一百万行进行 SQL 查询优化测试:读取执行计划、添加正确的索引、修复不可控制的过滤器、使用复合索引和键集分页。
阅读文章 →数据库与 SQLSQL 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 比较:架构、等待、浏览器和语言支持、调试、两种工具中的相同测试、迁移建议以及如何选择。
阅读文章 →软件测试等价划分:通过实例解释测试设计技术
等效分区解释:如何找到有效和无效分区,年龄、密码和优惠券的工作示例,我们运行的测试,以及为什么将其与边界配对。
阅读文章 →软件测试软件测试中的严重性与优先级:差异、矩阵和实例
软件测试中的严重性与优先级解释:清晰的定义、带有示例的四向矩阵、级别尺度、谁设置每个级别以及如何运行错误分类。
阅读文章 →数据库与 SQLSQL 子查询教程:类型、相关子查询和 EXISTS 以及示例
SQL 子查询教程,其中包含我们运行的查询:标量、IN、FROM 和 SELECT 子查询、相关子查询、EXISTS、NOT IN NULL 陷阱以及子查询与 JOIN 与 CTE。
阅读文章 →数据库与 SQLSQL 事务和 ACID 解释:COMMIT、ROLLBACK 和隔离级别
SQL 事务和 ACID 通过我们运行的银行转账进行了解释:COMMIT、ROLLBACK、保存点、锁、隔离级别、脏读、死锁和最佳实践。
阅读文章 →软件开发Git merge 与 rebase 的区别:示例及如何选择
用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。
阅读文章 →软件开发Python 列表推导式:语法、示例与何时避免使用
Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。
阅读文章 →数据库与 SQLSQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)
通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。
阅读文章 →