3 列上的唯一索引,其中 NULL 与一列中的所有其他值冲突

问题描述 投票:0回答:2

我有一张桌子:

CREATE TABLE "users" (
  id serial PRIMARY KEY,
  town text NOT NULL,
  street text not null,
  building text
);

我希望能够通过“城镇”、“街道”和“建筑物”三列来存储独特的条目。如果第三行“建筑物”为空,则无论“建筑物”列中是什么,都应该不可能存储具有相同“城镇”或“街道”列的任何新其他行。所以这应该有效:

---- example 1:
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', 8);
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', 9);
---- example 2: 
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', null);

但这不是:

---- example 1:
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', 8);
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', 9);
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', null);
---- example 2:
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', 8);
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', 8);

我尝试为此使用两个部分索引:

CREATE UNIQUE INDEX "two_cols" ON "users" ("town", "street") WHERE "building" IS NULL; 
CREATE UNIQUE INDEX "three_cols" ON "users" ("town", "street",  "building") WHERE "building" IS NOT NULL;

但是问题是您只能为当前索引设置过滤器,因此第一个索引仅检查其查询内部,这允许将空值与其他值一起存储,这不是我需要的。删除第一个索引上的过滤器不允许存储具有相同两列但不同第三列的两行。有办法解决这个问题吗?

sql postgresql database-design unique-constraint exclusion-constraint
2个回答
0
投票

您的第一次插入具有相同的数据,失败了,因为两者都被视为一个事务,并且发生回滚,这会删除两个插入。

您可以看到第二个插入效果完美。 8 号楼不存在。

当你让每个插入都有自己的事务时,它将进入该行,第二个没有问题,因为它在自己的事务中,当它回滚时,已经保存了 7 个。

因此在短期内使用交易

CREATE TABLE "users" (
  "id" SERIAL PRIMARY KEY,
  "town" varchar(255) NOT NULL,
  "street" varchar(255) not null,
  "building" varchar(255) 
);

CREATE TABLE
ALTER TABLE "users"
ADD CONSTRAINT users_uni UNIQUE NULLS NOT DISTINCT ("town", "street", "building");

ALTER TABLE
---- example 2:
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', 8);
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', 8);

INSERT 0 1
ERROR:  duplicate key value violates unique constraint "users_uni"
DETAIL:  Key (town, street, building)=(t, s, 8) already exists.
---- example 1:
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', 8);
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', 9);
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', null);

INSERT 0 1
INSERT 0 1
INSERT 0 1
---- example 2:
bEGIN;
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', 7);
Commit;
bEGIN;
INSERT INTO "users" ("town", "street", "building") VALUES ('t', 's', 7);
Commit;

BEGIN
INSERT 0 1
COMMIT
BEGIN
ERROR:  duplicate key value violates unique constraint "users_uni"
DETAIL:  Key (town, street, building)=(t, s, 7) already exists.

小提琴


0
投票

您希望

NULL
与 notnull 值冲突。但不同的 notnull 值不应相互冲突。

尽你所能,你不会用

UNIQUE
约束(或索引)来覆盖它。

排除约束提供了所需的功能。但它不适用于

text
值。因此,我散列
text
并从中构造一个
int8range
值。努力将对存储和性能的影响降至最低。

另外,保持简单的

UNIQUE
约束,即使这对于强制执行您的要求来说是多余的。隐式 B 树索引通常有助于提高性能。

正是您所要求的:

-- requires additional module btree_gist
CREATE EXTENSION btree_gist;

-- auxiliary function
CREATE OR REPLACE FUNCTION texthash_int8range(text)
  RETURNS int8range
  LANGUAGE sql IMMUTABLE PARALLEL SAFE
RETURN int8range(hashtextextended($1, 0), hashtextextended($1, 0), '[]');

CREATE TABLE users (
  id serial PRIMARY KEY
, town text NOT NULL
, street text NOT NULL
, building text
, CONSTRAINT address_uni UNIQUE NULLS NOT DISTINCT (town, street, building)
, CONSTRAINT address_with_null_building EXCLUDE USING gist(hash_record_extended((town, street), 0) WITH =, texthash_int8range(building) WITH &&)
);

小提琴

现在,您会想知道它到底是如何工作的,以及为什么它是可靠的。确实如此。
但解释一切比解决方案本身花费的时间要多得多。我稍后会再回来讨论这个问题。

这些相关答案涵盖了解决方案的所有特殊方面:

© www.soinside.com 2019 - 2024. All rights reserved.