PostgreSQL多列唯一约束和NULL值


93

我有一个如下表:

create table my_table (
    id   int8 not null,
    id_A int8 not null,
    id_B int8 not null,
    id_C int8 null,
    constraint pk_my_table primary key (id),
    constraint u_constrainte unique (id_A, id_B, id_C)
);

我想(id_A, id_B, id_C)在任何情况下都与众不同。因此,以下两个插入必须导致错误:

INSERT INTO my_table VALUES (1, 1, 2, NULL);
INSERT INTO my_table VALUES (2, 1, 2, NULL);

但这并没有达到预期的效果,因为根据文档,两个NULL值没有相互比较,因此两个插入均正确无误。

我怎么能保证我的唯一约束,即使id_C可以NULL在这种情况下?实际上,真正的问题是:我可以在“纯sql”中保证这种唯一性,还是必须在更高级别上实现(在我的情况下为java)?


因此,假设您有值(1,2,1)并且(1,2,2)(A,B,C)列中。是否(1,2,NULL)应允许添加?
ypercubeᵀᴹ

A和B不能为null,但C可以为null或任何正整数值。因此(1,2,3)和(2,4,null)是有效的,但(null,2,3)或(1,null,4)无效。并且[(1,2,null),(1,2,3)]不会破坏唯一约束,但是[[1,2,null),(1,2,null)]必须打破它。
Manuel Leduc

2
有没有永远不会出现在这些列中的值(例如负值?)
a_horse_with_no_name

您不必在pg中标记约束。它将自动生成名称。仅供参考。
埃文·卡罗尔

Answers:


93

您可以在纯SQL中执行此操作。除了您拥有的索引之外 ,还创建一个局部唯一索引

CREATE UNIQUE INDEX ab_c_null_idx ON my_table (id_A, id_B) WHERE id_C IS NULL;

这样,您可以(a, b, c)在表格中输入:

(1, 2, 1)
(1, 2, 2)
(1, 2, NULL)

但是这些都不是第二次。

或使用两个部分UNIQUE索引,不使用完整索引(或约束)。最佳解决方案取决于您的要求的详细信息。相比:

尽管这对于UNIQUE索引中的单个可为空的列非常有效,但它很快就失控了。讨论这个-以及如何使用带有部分索引的UPSERT:

阿西德斯

在PostgreSQL中,没有双引号的混合大小写标识符不可用。

可能会考虑serial作为主键或IDENTITY在Postgres的10或更高版本。有关:

所以:

CREATE TABLE my_table (
   my_table_id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY  -- for pg 10+
-- my_table_id bigserial PRIMARY KEY  -- for pg 9.6 or older
 , id_a int8 NOT NULL
 , id_b int8 NOT NULL
 , id_c int8
 , CONSTRAINT u_constraint UNIQUE (id_a, id_b, id_c)
);

如果您在表的整个生命周期(包括浪费的行和已删除的行)中不希望有超过20亿行(> 2147483647),请考虑使用integer(4个字节)而不是bigint(8个字节)。


1
文档提倡使用此方法,添加唯一约束将在约束中列出的列或一组列上自动创建唯一的B树索引。不能将仅覆盖某些行的唯一性限制写为唯一性约束,但是可以通过创建唯一的部分索引来实施这种限制。
埃文·卡洛尔

12

我遇到了同样的问题,并且发现了另一种在表中添加唯一NULL的方法。

CREATE UNIQUE INDEX index_name ON table_name( COALESCE( foreign_key_field, -1) )

在我的情况下,该字段foreign_key_field是一个正整数,永远不会为-1。

因此,要回答Manual Leduc,另一种解决方案可能是

CREATE UNIQUE INDEX  u_constrainte (COALESCE(id_a, -1), COALESCE(id_b,-1),COALESCE(id_c, -1) )

我认为id不会为-1。

创建部分索引有什么好处?
如果您没有NOT NULL子句id_a,则id_bid_c只能一起为NULL。
使用部分索引,这三个字段可能不止一次为NULL。


3
>创建部分索引有什么优势?完成此操作的方式COALESCE可以有效地限制重复项,但是索引在查询中并不是很有用,因为它的表达式索引可能与查询表达式不匹配。也就是说,除非您SELECT COALESCE(col, -1) ...不会达到该索引。
Bo Jeanes'8

@BoJeanes尚未针对性能问题创建索引。创建它是为了满足业务需求。
卢克M

8

Null可能意味着该行目前尚不知道值,但将来会在已知值后将其添加(例如FinishDate运行Project),或者该行没有值可应用(例如EscapeVelocity黑洞Star)。

我认为,通常最好通过消除所有Null来规范化表。

在您的情况下,您希望允许NULLs在您的列中,但只NULL允许一个。为什么?这两个表之间是什么样的关系?

也许您可以简单地将列更改为NOT NULL,而不是存储NULL一个-1永远不会出现的特殊值(如)。这将解决唯一性约束问题(但可能会产生其他可能不希望出现的副作用。例如,使用-1“未知/不适用”表示该列的任何总和或平均值计算都会出现偏差。或者所有此类计算都必须采用考虑特殊值并忽略它。)


2
在我的情况下,NULL实际上是NULL(例如id_C是table_c的外键,因此它不能具有-1值),这意味着它们在“ my_table”和“ table_c”之间没有关系。因此它具有功能上的含义。顺便说一下[[(1,1,1,null),(2,1,2,null),(3,2,4,null)]是插入数据的有效列表。
Manuel Leduc

1
它实际上不是SQL中使用的Null,因为在所有行中只需要一个。您可以通过将-1添加到table_c或通过添加另一个表(将是子类型table_c的超类型)来更改数据库模式。
ypercubeᵀᴹ

3
我只想向@Manuel指出,此答案中关于null的观点并不是普遍存在的,并且存在很多争议。像我一样,许多人认为null可以用于您想要的任何目的(但对于每个字段都只能表示件事,并且可以在字段名称或列注释中进行记录)
Jack Douglas

1
当列为FOREIGN KEY时,不能使用虚拟值。
Luc M

1
+1我和你在一起:如果我们希望某些列组合是唯一的,那么您需要考虑一个实体,其中该列组合是PK。OP的数据库架构可能应该更改为父表和子表。
AK
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.