据我了解,您需要 UNIQUE 索引而不是 a 和 b 组合。
您的更新缩小了范围(b 不应存在于a 中)。这种解决方案更严格。
解决方案
在尝试和调查了很多之后(见下文!)我想出了这个:
ALTER TABLE tbl ADD CONSTRAINT a_not_equal_b CHECK (a <> b);
ALTER TABLE tbl ADD CONSTRAINT ab_unique
EXCLUDE USING gist ((ARRAY[hashtext(COALESCE(a, ''))
, hashtext(COALESCE(b, ''))]) gist__int_ops WITH &&);
db小提琴here
由于目前(第 12 页)排除约束不适用于 text[],因此我使用 int4[] 的哈希值。 hashtext() 是内置哈希函数,也用于哈希分区(以及其他用途)。看起来很适合这份工作。
运算符类gist__int_ops 由附加模块intarray 提供,每个数据库必须安装一次。它是可选的,该解决方案也适用于默认数组运算符类。只需删除gist__int_ops 即可退回。但是 intarray 更快。相关:
注意事项
-
int4 可能不够大,不足以充分排除哈希冲突。您可能想改用bigint。但这更昂贵,并且不能使用gist__int_ops 运算符类来提高性能。您的来电。
-
Unicode 有一个令人沮丧的特性,即相等的字符串可以用不同的方式编码。如果您使用 Unicode(典型编码 UTF8)并使用非 ASCII 字符(这对您很重要),请比较规范化形式以排除此类重复。即将推出的Postgres 13 adds the function normalize() 就是为了这个目的。这是字符类型重复的一般警告,但并非针对我的解决方案。
-
NULL 值是允许的,但与空字符串 ('') 冲突。我宁愿使用NOT NULL 列并从表达式中删除COALESCE()。
排除约束的障碍
我的第一个想法是:exclusion constraint。但它失败了:
ALTER TABLE tbl ADD CONSTRAINT ab_unique EXCLUDE USING gist ((ARRAY[a,b]) WITH &&);
ERROR: data type text[] has no default operator class for access method "gist"
HINT: You must specify an operator class for the index or define a default operator class for the data type.
为此有一个open TODO item。相关:
但是我们不能为text[] 使用 GIN 索引吗?唉,没有:
ALTER TABLE tbl ADD CONSTRAINT ab_unique EXCLUDE USING gin ((ARRAY[a,b]) WITH &&);
ERROR: access method "gin" does not support exclusion constraints
为什么? The manual:
访问方式必须支持amgettuple(见Chapter 61);目前这意味着不能使用 GIN。
这似乎很难实现,所以不要屏住呼吸。
如果 a 和 b 是 integer 列,我们可以使它与整数数组一起工作:
ALTER TABLE tbl ADD CONSTRAINT ab_unique EXCLUDE USING gist ((ARRAY[a,b]) WITH &&);
或者使用附加模块intarray 中的gist__int_ops 运算符类(通常更快):
ALTER TABLE tbl ADD CONSTRAINT ab_unique EXCLUDE USING gist ((ARRAY[a,b]) gist__int_ops WITH &&);
要同时禁止在同一行内重复,请添加CHECK 约束:
ALTER TABLE tbl ADD CONSTRAINT a_not_equal_b CHECK (a <> b);
剩余问题:是否适用于 NULL 值。
解决方法
在 one 列中添加一个帮助表以存储来自 a 和 b 的值:
CREATE TABLE tbl_ab(ab text PRIMARY KEY);
主表,就像你拥有的那样,加上 FK 约束。
CREATE TABLE tbl (
a text REFERENCES tbl_ab ON UPDATE CASCADE ON DELETE CASCADE
, b text REFERENCES tbl_ab ON UPDATE CASCADE ON DELETE CASCADE
);
使用这样的函数来INSERT:
CREATE OR REPLACE FUNCTION f_tbl_insert(_a text, _b text)
RETURNS void
LANGUAGE sql AS
$func$
WITH ins_ab AS (
INSERT INTO tbl_ab(ab)
SELECT _a WHERE _a IS NOT NULL -- NULL is allowed (?)
UNION ALL
SELECT _b WHERE _b IS NOT NULL
)
INSERT INTO tbl(a,b)
VALUES (_a, _b);
$func$;
db小提琴here
或者实现一个触发器在后台处理它。
CREATE OR REPLACE FUNCTION trg_tbl_insbef()
RETURNS trigger AS
$func$
BEGIN
INSERT INTO tbl_ab(ab)
SELECT NEW.a WHERE NEW.a IS NOT NULL -- NULL is allowed (?)
UNION ALL
SELECT NEW.b WHERE NEW.b IS NOT NULL;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER tbl_insbef
BEFORE INSERT ON tbl
FOR EACH ROW EXECUTE PROCEDURE trg_tbl_insbef();
db小提琴here
NULL 处理可以根据需要更改。
无论哪种方式,虽然添加的(可选)FK 约束强制我们不能回避辅助表tbl_ab,并允许tbl_ab 中的UPDATE 和DELETE 级联,你仍然还需要将UPDATE 和DELETE 投影到帮助表中(或实现更多触发器)。棘手的极端情况,但有解决方案。在我使用hashtext() 找到上述带有排除约束的解决方案之后,不再赘述...
相关: