不同数据库对NULL值处理逻辑也不同,总结一下
PostgreSQL
默认情况NULL值是不同的值,也就是未知,未知和未知不相同。PostgreSQL15引入了 NULLS NOT DISTINCT on unique indexes / constraints对NULL的约束更严格了,不同的NULL可以看做是相同的值。
看例子
PostgreSQL 15 引入的 NULLS NOT DISTINCT 选项确实对唯一性约束中 NULL 值的处理方式带来了重要改变。在之前的版本中,NULL 值在唯一性约束中被视为互不相同的"未知"值,因此可以插入多个 NULL 而不违反约束。而新选项允许将多个 NULL 值视为相同的值,从而在唯一性约束中只允许存在一个 NULL。
以下是对这一特性的补充实例说明:
1. 单列唯一约束的对比实例
在 PostgreSQL 14 及更早版本中,即使对列设置了唯一约束,也能插入多个 NULL 值。
sql
-- PostgreSQL 14 及更早的行为(默认 NULLS DISTINCT)
CREATE TABLE users_old (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE -- 默认允许多个 NULL
);
INSERT INTO users_old (name) VALUES (NULL);
INSERT INTO users_old (name) VALUES (NULL); -- 成功插入,表中存在两行 name 为 NULL 的记录[5](@ref)
SQL
在 PostgreSQL 15 中,可以通过 NULLS NOT DISTINCT 明确禁止多个 NULL。
sql
-- PostgreSQL 15 新行为
CREATE TABLE users_new (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NULLS NOT DISTINCT -- 将 NULL 视为相同值
);
INSERT INTO users_new (name) VALUES (NULL);
INSERT INTO users_new (name) VALUES (NULL); -- 失败,违反唯一约束[5](@ref)
SQL
2. 多列复合唯一约束的实例
这个特性同样适用于多列组成的复合唯一约束。在旧风格下,只要组合中有一列为 NULL,该行就不会与其他行(即使其他列值完全相同)发生唯一性冲突。
sql
-- 传统风格:允许 (val1, val2) 组合中出现多个 val2 为 NULL 的行,只要 val1 相同
CREATE TABLE null_old_style (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
val1 TEXT NOT NULL,
val2 TEXT NULL,
CONSTRAINT uq_val1_val2 UNIQUE (val1, val2) -- 默认 NULLS DISTINCT
);
INSERT INTO null_old_style (val1, val2) VALUES ('Hello', NULL);
INSERT INTO null_old_style (val1, val2) VALUES ('Hello', NULL); -- 成功,可插入多行[1](@ref)
SQL
使用 NULLS NOT DISTINCT 后,复合唯一键会将 NULL 视为一个具体的值进行比对。
sql
-- 新风格:将 (val1, val2) 组合中的 NULL 视为确定值
CREATE TABLE null_new_style (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
val1 TEXT NOT NULL,
val2 TEXT NULL,
CONSTRAINT uq_val1_val2_new UNIQUE NULLS NOT DISTINCT (val1, val2)
);
INSERT INTO null_new_style (val1, val2) VALUES ('Hello', NULL);
INSERT INTO null_new_style (val1, val2) VALUES ('Hello', NULL); -- 失败,违反唯一约束[1](@ref)
INSERT INTO null_new_style (val1, val2) VALUES ('World', NULL); -- 成功,因为 val1 不同[1](@ref)
SQL
3. 在已有表上通过 ALTER TABLE 添加约束的实例
该选项也可以在修改表时使用。
sql
-- 创建一个表,不立即添加约束
CREATE TABLE t2 (
id INT,
c1 VARCHAR,
c2 VARCHAR
);
-- 添加一个将 NULL 视为非 distinct 的唯一约束
ALTER TABLE t2 ADD UNIQUE NULLS NOT DISTINCT (c1, c2);
-- 测试插入:第一个 NULL 成功,第二个相同组合的 NULL 失败
INSERT INTO t2 VALUES (1, 'c1', NULL);
INSERT INTO t2 VALUES (2, 'c1', NULL); -- 将报错:违反唯一约束[4](@ref)
SQL
4. 创建唯一索引时的应用实例
除了表约束,该语法也可用于创建唯一索引。
sql
CREATE TABLE unique_tbl (i INTEGER);
-- 创建允许 NULL 值重复的唯一索引(传统行为,默认)
CREATE UNIQUE INDEX unique_idx1 ON unique_tbl (i) NULLS DISTINCT;
-- 创建不允许 NULL 值重复的唯一索引
CREATE UNIQUE INDEX unique_idx2 ON unique_tbl (i) NULLS NOT DISTINCT;
SQL
当使用 NULLS NOT DISTINCT 索引后,向表中插入多个 i 列为 NULL 的值将会失败。
5. 需要注意的特殊场景与限制
虽然 NULLS NOT DISTINCT 加强了对 NULL 的唯一性限制,但它并不能直接满足某些更复杂的业务规则。例如,有这样一个需求:当列 b 为 NULL 时,列 a 的值必须唯一;但当 b 不为 NULL 时, a 可以重复。仅使用 UNIQUE NULLS NOT DISTINCT (a, b) 无法实现该逻辑,因为它会将 (a, NULL) 整体视为一个唯一键,允许 (a, 非NULL值) 同时存在,但也会允许 (a, NULL) 与 (a, 另一个非NULL值) 共存,这不符合"b为NULL时a必须唯一"的规则。实现这类复杂逻辑通常需要借助排除约束(Exclusion Constraints)或函数索引等其他机制。
此外PostgreSQL还支持create index xxx on xx where xx is null的语法,方便快速过滤null或者not null值。
Oracle
和PostgreSQL 15之前默认的逻辑相近。
MySQL
和PostgreSQL 15之前默认的逻辑相近。
总结
PostgreSQL 15 的 NULLS NOT DISTINCT 选项为数据库设计提供了更精细的唯一性控制手段,让开发者可以根据业务需求,选择是否将 NULL 视为一个可重复的"未知值",还是一个具有唯一性的确定值。在设计表结构时,应仔细考虑业务语义,以决定是否启用此选项。