在 Oracle, SQL Server, MySQL 和 SQLite 之间迁移 NULL 与空字符串语义
通过解决四个关键移植点(NULL 测试方式、比较结果、布尔上下文强制转换以及空字符串定义)来在 Oracle, SQL Server, MySQL 和 SQLite 之间移植 NULL 处理。Oracle 将零长度字符视为 NULL 但警告此行为可能更改;SQL Server 依赖 ANSI_NULLS 设置;MySQL 在布尔上下文中将 0 和 NULL 强制转换为 false;SQLite 的 IS 和 IS NOT 语法属于扩展。
本文内容
核心观点
要在 Oracle, SQL Server, MySQL 和 SQLite 之间移植 NULL 处理,必须解决四个移植点:如何测试 NULL、与 NULL 比较的返回值、布尔上下文是否强制转换 NULL 以及空字符串的含义。风险最高的是 Oracle,它目前将长度为零的字符值视为 NULL,但官方文档警告未来版本可能不再如此,并建议不要将空字符串与 NULL 等同视之。测试时应仅使用 IS NULL 和 IS NOT NULL,因为涉及 NULL 的任何其他条件都会评估为 UNKNOWN;UNKNOWN 在 WHERE 子句中表现类似于 FALSE,但 NOT UNKNOWN 仍为 UNKNOWN。SQL Server 存在配置权衡:当 SET ANSI_NULLS 为 ON 时,一个或两个 NULL 操作数产生 UNKNOWN;而当 ANSI_NULLS 为 OFF 时,等于和不等于运算符将 NULL 视为一个已知值(与其他 NULL 相等),仅返回 TRUE 或 FALSE,这会导致过滤结果随会话设置而变化。MySQL 的 NULL 比较依然返回 NULL,但其布尔上下文将 0 或 NULL 视为 false,其他视为 true;此外,MySQL 在 GROUP BY 中认为两个 NULL 相等,且在 ASC 排序时将 NULL 置前,DESC 排序时置后。SQLite 提供了简洁的 IS 和 IS NOT 运算符,始终返回 1 或 0 而非 NULL,但文档将其定义为 SQLite 扩展,标准 SQL 则要求使用 IS NOT DISTINCT FROM 和 IS DISTINCT FROM。这种权衡在于一致性与可移植性:特定引擎的语法在本地简洁易读,而 IS NULL / IS NOT NULL 结合显式的 COALESCE 式处理是唯一在所有环境下行为可预测的写法。建议将这四个要点为每个引擎记录为书面矩阵,而非假设标准 SQL 行为通用,并根据 PostgreSQL 官方文档将其作为一行添加到矩阵中。
上下文与建议
在 Oracle, SQL Server, MySQL 和 SQLite 之间移植查询或共享 SQL 模块之前,应建立一个显式的 NULL 可移植性检查清单:(1) 如何测试 NULL,(2) 与 NULL 比较的返回值,(3) 布尔上下文是否强制转换 NULL,(4) 空字符串的含义。最高风险项是 Oracle,它目前将长度为零的字符值视为 NULL,但明确建议不要依赖空字符串与 NULL 的等价性。应将这四个要点为每个引擎记录为书面矩阵,而不是假设标准 SQL 行为可以无缝迁移。由于此处没有 PostgreSQL 源码,请参考其官方文档将其作为矩阵的一行。
核心可移植性规则是仅使用 IS NULL 和 IS NOT NULL 来测试 NULL。Oracle 指出,IS NULL 和 IS NOT NULL 是处理 NULL 的唯一比较方式,任何其他涉及 NULL 的条件都会评估为 UNKNOWN。UNKNOWN 在 WHERE 子句中几乎像 FALSE 一样工作,但 NOT UNKNOWN 依然是 UNKNOWN,而不会变成 TRUE。SQL Server 存在真实的配置权衡:当 SET ANSI_NULLS 为 ON 时,一个或两个 NULL 操作数产生 UNKNOWN;而当 ANSI_NULLS 为 OFF 时,等于 (=) 和不等于 (<>) 运算符将 NULL 视为一个已知值,且与其他 NULL 相等,仅返回 TRUE 或 FALSE,这意味着过滤结果取决于会话设置。
MySQL 的 NULL 比较返回 NULL,但其布尔上下文将 0 或 NULL 视为 false,其他所有值视为 true。在 GROUP BY 中,MySQL 将两个 NULL 视为相等,且在 ASC 排序时将 NULL 放在最前,DESC 排序时放在最后。SQLite 提供了简洁的 IS 和 IS NOT 运算符,它们始终返回 1 或 0 且绝不返回 NULL,但文档将其标注为 SQLite 扩展,标准 SQL 则要求使用 IS NOT DISTINCT FROM 和 IS DISTINCT FROM。
这是一种一致性与可移植性之间的权衡:特定引擎的写法在本地简洁易读,而 IS NULL / IS NOT NULL 结合显式的 COALESCE 式处理是唯一在所有环境下行为可预测的拼写方式。不要假设为某个引擎编写的查询可以在另一个引擎上原样运行,也不要假设空字符串与 NULL 是可以互换的。矩阵应捕捉每个引擎的这四个要点以及可能影响结果的会话级设置,并将其作为代码审查的检查清单,而非一次性审计。
-- portable across the engines covered here
SELECT
'' IS NULL AS empty_is_null,
'' IS NOT NULL AS empty_is_not_null,
1 = NULL AS eq_null,
1 <> NULL AS ne_null;
-- run the same probe twice on SQL Server: once with ANSI_NULLS ON, once OFF
SET ANSI_NULLS ON;
SELECT 1 = NULL;
SET ANSI_NULLS OFF;
SELECT 1 = NULL;
-- row filtering: only this form is portable
SELECT * FROM t WHERE col IS NOT NULL;推理与权衡
各引擎在核心规则上达成一致,但在破坏可移植性的边缘情况上存在分歧。Oracle 强调 IS NULL 和 IS NOT NULL 是唯一正确的比较方式,其他涉及 NULL 的条件评估为 UNKNOWN,且 NOT UNKNOWN 保持 UNKNOWN。SQL Server 的 ANSI_NULLS 设置会导致 = 和 <> 的行为在会话级别发生变化,从而改变过滤结果。MySQL 在布尔上下文中将 0 和 NULL 强制转换为 false,但这不适用于 = 和 <> 比较(后者仍返回 NULL),因此在 WHERE 子句中有效的查询可能在 CASE 表达式或函数参数中失败。SQLite 的 IS/IS NOT 语法虽简洁,但属于扩展,在仅支持 IS NOT DISTINCT FROM 的引擎上无法运行。
权衡点在于:使用特定引擎的语法虽然高效,但缺乏通用性。例如,Oracle 的空字符串即 NULL 行为虽然简洁,但具有版本敏感性,官方建议不要依赖此特性。SQL Server 的 ANSI_NULLS 设置可能会静默地改变查询含义。因此,矩阵不仅应记录探测的字面结果,还应记录影响结果的会话设置。对于 SQL Server,在运行探测前应读取实际的 ANSI_NULLS 设置,因为默认值可能与文档描述不符。对于 Oracle,应记录版本并在升级后重新测试,因为空字符串规则可能在未来版本中更改。
对于 MySQL,必须区分布尔上下文与比较运算符,并记住 ORDER BY 对 NULL 的排序规则。对于 SQLite,应意识到 IS 和 IS NOT 是扩展,而标准 SQL 需要 IS NOT DISTINCT FROM。矩阵还应记录引擎是否支持 IS NOT DISTINCT FROM,因为这是标准 SQL 中处理 NULL 相等性的可移植写法。
最终建议是:编写查询时,仅使用 IS NULL 和 IS NOT NULL 进行测试,并对可能为 NULL 的值使用显式的 COALESCE 或等效处理。这是唯一能确保在所有环境下行为一致的方案,因为它不依赖于引擎对空字符串的处理、会话设置或布尔上下文。如果需要区分空字符串和 NULL,应将其存储在不同列中或使用哨兵值,不要依赖引擎的隐式转换。
具体示例
在每个目标引擎上运行相同的微小探测,并将字面输出记录为可移植性证据:SELECT '' IS NULL, '' IS NOT NULL, 1 = NULL, 1 <> NULL, 0 = NULL;在支持布尔位置的引擎中运行 SELECT NOT NULL。在 Oracle 中,空字符串表现为 NULL,但需注意文档警告;MySQL 中 '' IS NULL 为 0,'' IS NOT NULL 为 1,且 1 = NULL 和 1 <> NULL 返回 NULL;SQLite 的 IS 和 IS NOT 返回 1 或 0 而非 NULL。随后使用行过滤探测(WHERE value <> NULL 和 WHERE value IS NOT NULL)来验证实际返回的行。将探测值保存在固定装置表中,以显式揭示存储的空字符串与 NULL 之间的差异。
固定装置表应包含一个 NULL、一个空字符串、一个零和一个普通值。在 Oracle 中,空字符串被存储为 NULL,因此 '' IS NULL 应为 1,'' IS NOT NULL 应为 0,而 1 = NULL 和 1 <> NULL 应返回 NULL。在 MySQL 中,'' IS NULL 为 0,'' IS NOT NULL 为 1,且 1 = NULL, 1 <> NULL, 1 < NULL, 1 > NULL 均返回 NULL。在 SQLite 中,'' IS NULL 为 0,'' IS NOT NULL 为 1,1 = NULL 和 1 <> NULL 返回 NULL,且 IS/IS NOT 始终返回 1 或 0。在 SQL Server 中,探测需运行两次(ANSI_NULLS 分别为 ON 和 OFF),因为 = 和 <> 在 ON 时返回 UNKNOWN,在 OFF 时返回 TRUE 或 FALSE。
行过滤探测将揭示实际行为。在 Oracle, MySQL 和 SQLite 中,WHERE value <> NULL 不应返回任何行,因为比较结果为 NULL,而评估为 UNKNOWN 的条件在 WHERE 中表现类似于 FALSE。WHERE value IS NOT NULL 是唯一可移植的形式,能返回所有非 NULL 行。在 SQL Server 中,如果 ANSI_NULLS 为 OFF,WHERE value <> NULL 可能会返回行,因为此时运算符将 NULL 视为已知值。布尔上下文探测应显示 NOT NULL 在遵循三值逻辑的引擎中评估为 UNKNOWN,而将 UNKNOWN 强制转换为 FALSE 的布尔上下文则表现不同。
将每个探测的字面输出记录在矩阵中,并包含 SQL Server 的会话设置。矩阵还应记录引擎是否支持 IS NOT DISTINCT FROM,以及空字符串为 NULL 的规则是否可能更改。这些结果是可移植性检查清单的证据,用于决定在共享 SQL 模块中使用哪种拼写方式。可移植的拼写是 IS NULL 和 IS NOT NULL 结合显式的 COALESCE 处理。
适用性限制
本建议仅涵盖 NULL 比较和空字符串语义,不涉及 NULL 的聚合计数、索引行为或性能。由于源码不包含 PostgreSQL 文档,因此此处未对 PostgreSQL 的 NULL 或空字符串行为做出断言,在扩展矩阵前必须查阅 PostgreSQL 官方文档。Oracle 的零长度字符规则被记录为当前行为但可能更改,因此应将其视为版本敏感的假设,在升级时重新测试,而非永久保证。SQL Server 的结果取决于 ANSI_NULLS 设置,因此应探测实际会话配置而非假设默认值。
MySQL 将 0 和 NULL 强制转换为 false 仅适用于布尔上下文,不适用于 = 和 <> 比较(后者仍返回 NULL)。SQLite 的简洁 IS 和 IS NOT 形式是扩展,使用它们的查询在仅接受 IS NOT DISTINCT FROM 的引擎上无法原样运行。这些限制至关重要,因为矩阵是一个检查清单而非绝对保证。Oracle 的规则可能在未来版本中失效,SQL Server 的设置可能静默改变查询结果,MySQL 的强制转换可能导致 WHERE 子句有效的查询在 CASE 表达式中失败。
矩阵应记录引擎是否支持 IS NOT DISTINCT FROM,因为这是标准 SQL 中 NULL 相等性的可移植写法。如果不支持,则可移植的替代方案是 IS NULL / IS NOT NULL 结合 COALESCE。不要尝试从一个引擎推断另一个引擎的行为,也不要假设空字符串与 NULL 是可互换的。探测结果应作为证据,指导在共享 SQL 模块中选择正确的拼写方式。
再次强调,由于缺乏 PostgreSQL 源码,必须独立验证其行为。矩阵应作为代码审查的检查清单,并在引擎版本更新导致行为变更时及时更新。可移植的拼写方式(IS NULL/IS NOT NULL + COALESCE)不依赖于空字符串处理、会话设置或布尔上下文,因此是唯一推荐的方案。
适用条件
- 目标引擎是否将 '' IS NULL 视为 0 且 '' IS NOT NULL 视为 1,还是将空字符串折叠为 NULL?
- 像 1 = NULL 这样的比较是返回 NULL, UNKNOWN,还是根据会话设置返回 TRUE/FALSE?
- 像 WHERE NOT NULL 或 IF(NOT NULL) 这样的布尔上下文是将 NULL 强制转换为 FALSE,还是保留 UNKNOWN?
- 引擎的 ORDER BY 在 ASC 时是否将 NULL 置前,在 DESC 时是否将 NULL 置后,或者遵循其他规则?
- GROUP BY 是否将两个 NULL 视为相等,且引擎是否支持 IS NOT DISTINCT FROM?
- 空字符串视为 NULL 的规则是否被记录为当前行为且未来版本可能会更改?
- 简洁的 IS 和 IS NOT 语法是否被记录为扩展而非标准 SQL?
- 结果是否受 ANSI_NULLS 等会话级设置影响,且在运行探测前能否读取该设置?
- 探测固定装置是否能区分存储的 NULL 与存储的空字符串,还是两者被混淆?
- 在将矩阵扩展到 PostgreSQL 之前,是否已对照其官方文档进行了验证?
适用范围
本建议仅涵盖 NULL 比较和空字符串语义,不涉及 NULL 的聚合计数、索引行为或性能。由于源码不包含 PostgreSQL 文档,因此此处未对 PostgreSQL 的 NULL 或空字符串行为做出断言,在扩展矩阵前必须查阅 PostgreSQL 官方文档。Oracle 的零长度字符规则被记录为当前行为但可能更改,因此应将其视为版本敏感的假设,在升级时重新测试。SQL Server 的结果取决于 ANSI_NULLS 设置,因此应探测实际会话配置而非假设默认值。MySQL 将 0 和 NULL 强制转换为 false 仅适用于布尔上下文,不适用于 = 和 <> 比较。SQLite 的简洁 IS 和 IS NOT 形式是扩展,在仅接受 IS NOT DISTINCT FROM 的引擎上无法原样运行。