我们有一个传统的数据库模式,它有一些有趣的设计决策。直到最近,我们只支持Oracle和SQL Server,但我们正在尝试添加对PostgreSQL的支持,这带来了一个有趣的问题。我搜索了Stack Overflow和互联网上的其他网站,我不相信这种特殊的情况是重复的。
对于唯一约束中的可空列,Oracle和SQL Server的行为相同,即在执行唯一性检查时基本上忽略为NULL的列。
假设我有下面的表和约束:
CREATE TABLE EXAMPLE
(
ID TEXT NOT NULL PRIMARY KEY,
FIELD1 TEXT NULL,
FIELD2 TEXT NULL,
FIELD3 TEXT NULL,
FIELD4 TEXT NULL,
FIELD5 TEXT NULL,
...
);
CREATE UNIQUE INDEX EXAMPLE_INDEX ON EXAMPLE
(
FIELD1 ASC,
FIELD2 ASC,
FIELD3 ASC,
FIELD4 ASC,
FIELD5 ASC
);
在Oracle和SQL Server上,保留任何可为空的列NULL
将导致仅对非空列执行唯一性检查。因此,以下插入只能执行一次:
INSERT INTO EXAMPLE VALUES ('1','FIELD1_DATA', NULL, NULL, NULL, NULL );
INSERT INTO EXAMPLE VALUES ('2','FIELD1_DATA','FIELD2_DATA', NULL, NULL,'FIELD5_DATA');
-- These will succeed when they should violate the unique constraint:
INSERT INTO EXAMPLE VALUES ('3','FIELD1_DATA', NULL, NULL, NULL, NULL );
INSERT INTO EXAMPLE VALUES ('4','FIELD1_DATA','FIELD2_DATA', NULL, NULL,'FIELD5_DATA');
然而,由于PostgreSQL(正确地)遵循SQL标准,这些插入(以及任何其他值的组合,只要其中一个为NULL)将不会抛出错误,并被正确地插入没有问题。不幸的是,由于我们的旧模式和支持代码,我们需要PostgreSQL的行为与SQL Server和Oracle相同。
我知道以下堆栈溢出问题及其答案:Create unique constraint with null columns.据我了解,有两种策略可以解决这个问题:
1.创建部分索引,用于描述可为空的列同时为NULL
和NOT NULL
时的索引(这将导致部分索引的数量呈指数增长)
1.在索引中可为空的列上使用带有标记值的COAELSCE
。
(1)的问题是,我们需要创建的部分索引的数量随着我们想添加到约束中的每一个额外的可空列而呈指数增长(如果我没有记错的话,是2^N);(2)的问题是,sentinel值减少了该列可用值的数量,以及所有潜在的性能问题。
我的问题:这是解决这个问题的唯一两种方法吗?如果是,对于这个特定的用例,它们之间的权衡是什么?一个好的答案是讨论每种方法的性能,可维护性,PostgreSQL如何在简单的SELECT
语句中使用这些索引,以及任何其他"陷阱"或需要注意的事情。请记住,5个可空列只是一个例子;我们的模式中有一些表,最多包含10个(是的,每次看到它我都会哭,但它就是这样)。
4条答案
按热度按时间u59ebvdq1#
Postgres 15添加了子句
NULLS NOT DISTINCT
参见:
现在的解决方案非常简单:
适用于14岁或以上的Postgres
您正在努力实现与现有Oracle和SQL Server实施的**兼容性。
因为Oracle在行存储中根本没有实现
NULL
值,所以它无法区分空字符串和NULL
,所以在Postgres中使用空字符串(''
)而不是NULL
值不是更谨慎吗?将唯一约束中包含的列定义为
NOT NULL DEFAULT ''
,问题解决:注解
您在问题中演示的是一个唯一 * 索引*:
不是你一直在说的"唯一"限制,而是有细微而重要的区别!
我把它变成了一个实际的约束,就像问题的标题一样。
关键字
ASC
只是噪声,因为这是默认的排序顺序。为简单起见,使用
serial
PK列,这完全是可选的,但通常比存储为text
的数字更可取。使用它
只需省略
INSERT
中的空/null字段:重复这些插入中的任何一个都将违反唯一约束。
INSERT
语句中有点反模式),或者,您可以编写一个触发器
BEFORE INSERT OR UPDATE
,将NULL
转换为''
。替代解决方案
如果您需要使用实际的NULL值,我建议使用唯一的 * index ,带有*
COALESCE
,就像您在选项(2)中提到的那样,并在最后一个示例中提供@wildplasser。像@Rudolfo这样的数组**上的索引很简单,但是开销要大得多。在Postgres中数组处理不是很便宜,并且有一个类似于行(24字节)的数组开销:
数组仅限于相同数据类型的列。如果某些列不是,则可以将所有列强制转换为
text
,但这通常会进一步增加存储要求。或者,可以对异类数据类型使用已知的行类型...一个极端案例:所有值均为NULL的数组(或行)类型被视为相等(!),因此只能有一行的所有相关列均为NULL。可以是,也可以不是。如果要禁止所有列均为NULL:
ajsxfq5m2#
第三种方法:使用
IS NOT DISTINCT FROM
而不是=
来比较键列。(这可以利用候选 natural 键上的现有索引)示例(查看最后一列)下一步就是把它放到一个触发器函数中,并在它上面放置一个触发器。(现在没有时间,也许以后会有)
下面是trigger-function(它还不完美,但看起来可以工作):
UPDATE:一个唯一的索引有时候可以被 Package 到一个约束中(参见postgres-9.4 docs, final example),你确实需要发明一个标记值;我在这里使用了空字符串
''
。但是
coalesce()
上的“函数”索引在这个构造中是不允许的,唯一索引(OP的选项2)仍然有效,尽管:gkl3eglg3#
这实际上对我很有效:
我不知道性能是如何受到影响的,但它应该接近理想状态(取决于postres中阵列的优化程度)
hc2pp10m4#
您可以创建一个规则,将ALL NULL值(而不是原始表)插入到分区(如partition_field1_nullable、partition_fiend2_nullable等)中。这样,您就可以仅在原始表中创建唯一索引(不带空值)。这将允许您仅向原始表插入非空值(具有唯一性),且不为空(并且相应地不唯一)值到“可空分区”。并且您可以仅对可空分区应用COALESCE或触发器方法,为了避免对原始表上的每个DML进行许多分散的部分索引和触发...