为什么我的 UNIQUE 约束没有触发?

use*_*174 14 postgresql null unique-constraint

我有这个UNIQUE限制:

ALTER TABLE table ADD CONSTRAINT "abc123" UNIQUE
("col1", "col2", "col3", "col4", "col5", "col6", "col7", "col8");
Run Code Online (Sandbox Code Playgroud)

然后我这样做:

INSERT INTO table ("col1", "col2", "col3", "col4", "col5", "col6", "col7", "col8") 
VALUES ('a', 'b', 'c', 'd', 'e', 'f', null, true);
INSERT INTO table ("col1", "col2", "col3", "col4", "col5", "col6", "col7", "col8") 
VALUES ('a', 'b', 'c', 'd', 'e', 'f', null, true);
Run Code Online (Sandbox Code Playgroud)

两者都有效。两行已添加到表中。从逻辑上讲,第二个应该失败。但事实并非如此。

我究竟做错了什么?这让我发疯。

注意:如果这是我自己的数据,我将拥有一个真正独特的列,而不是这个“疯狂”的UNIQUE约束。问题是这个表保存了我的银行帐户的记录,而且他们愚蠢地在 CSV 转储中没有真正的“唯一”列,我可以用它来实际确保不插入重复的行,所以我有提出一个组合整个表中所有列以确定唯一性的方法。

Erw*_*ter 24

NULL是罪魁祸首,因为UNIQUE根据 SQL 标准,两个 NULL 值在约束中被认为是不同的。

Postgres 15 或更高版本

Postgres 15 添加了一个选项来更改此行为,从而提供了一个简单的解决方案:

ALTER TABLE table ADD CONSTRAINT "abc123" UNIQUE NULLS NOT DISTINCT 
(col1, col2, col3, col4, col5, col6, col7, col8);
Run Code Online (Sandbox Code Playgroud)

看:

现在可以开箱即用。然而,底层的唯一索引对于许多和/或宽列来说很大且效率低下。我仍然会考虑使用哈希值索引,如下所述。

替代解决方案(原始答案)

您是否需要所有列才能使行唯一?通常,只需组合几个就足够了。银行数据应该有大量的非空列......

为了使其工作包括单个可为空的列,您可以使用部分索引,如下所述:

但如果有多个可为空的列,这种做法很快就会变得不切实际。

对于多个可为空的列,一个简单的解决方案是使用如下的唯一表达式索引COALESCE

CREATE UNIQUE INDEX bank_uni_idx ON bank
(col1, col2, COALESCE(col3, ''), col4, col5, col6, COALESCE(col7, ''), col8);
Run Code Online (Sandbox Code Playgroud)

假设col3&col7是可为空的字符串类型列,其中空字符串 ( '') 和NULL在语义上是等效的。

显然,同样的方法也可以用于单个可为空的列。
您需要一个安全的替换,NULL它不会与其他合法值冲突(在我的示例中为空字符串)。

到目前为止,所有解决方案(包括原始解决方案)的缺点是许多列上的大型索引。可以让它变得相当昂贵。这让我得出了我真正想给出的答案:

高效的解决方案

UNIQUE基于行的廉价且足够唯一的哈希值(简化为定义列)创建索引或约束。

Postgres 14

带有一个内置的记录哈希函数(包括匿名记录!),它比我下面的自定义函数便宜得多。

hash_record_extended(record, bigint) --> bigint
Run Code Online (Sandbox Code Playgroud)

看:

它属于与(详细信息如下)相同的函数系列hashtextextended()。现在,表达式索引似乎比生成列更有吸引力。所以就:

CREATE UNIQUE INDEX bank_hash_uni ON bank (hash_record_extended((col1, col2, col3, col4, col5, col6, col7, col8),0));
Run Code Online (Sandbox Code Playgroud)

就这样。以下大部分内容仍然适用。

Postgres 13(原始答案)

将哈希值存储在生成的列中并UNIQUE对其创建约束。看:

假设所有text列。

CREATE OR REPLACE FUNCTION public.f_bank_bighash(col1 text, col2 text, col3 text, col4 text
                                               , col5 text, col6 text, col7 text, col8 text)
  RETURNS bigint 
  LANGUAGE sql IMMUTABLE COST 25 PARALLEL SAFE AS 
'SELECT hashtextextended(textin(record_out(($1,$2,$3,$4,$5,$6,$7,$8))), 0)';

COMMENT ON FUNCTION public.f_bank_bighash(text, text, text, text, text, text, text, text)
IS 'Fast, practically unique signature for the set of defining columns in table bank.
IMMUTABLE for use in index. "record_out"() is only stable, but with only text input it is effectively immutable.';

ALTER TABLE bank
  ADD COLUMN bank_bighash bigint NOT NULL GENERATED ALWAYS AS (public.f_bank_bighash(col1, col2, col3, col4, col5, col6, col7, col8)) STORED  -- appends column in last position
, ADD CONSTRAINT bank_bighash_uni UNIQUE (bank_bighash);
Run Code Online (Sandbox Code Playgroud)

db<>在这里摆弄

与价值观一起工作NULL

需要Postgres 12或更高版本,其中添加了扩展哈希函数和生成列。

hashtextextended()以及hastext()用于对散列分区或散列索引进行快速可靠的散列的内部函数。他们没有证件但他们不会消失。正如Tom Lane 指出的
那样,它们在不同的硬件平台上可能不稳定。将数据库集群从小端系统移动到大端系统后重新创建哈希(如果发生类似的情况)。

第二个参数hashtextextended()是哈希值的盐。使用任何bigint常量,只要确保在各处使用相同的常量即可。坚持下去0,除非你更了解。

此外,虽然在巨大的 bigint 键空间下哈希冲突极不可能发生,但理论上的可能性始终存在。如果发生这种情况,您将收到两个不同行的唯一违规。如果对此感到不舒服,请md5()改为使用并存uuid储值。看:

16 个字节,uuid而不是 8 个字节bigint。计算、存储和比较的成本要高一些。理论上碰撞仍然是可能的,但你必须保持偏执。

旧的(或任何)版本可以对hashtext()返回integer. 使碰撞更有可能发生。达到几千个条目仍然不太可能。
并使用触发器使哈希列保持最新,或使用表达式的唯一索引而不是生成列的约束。

哈希冲突的概率?

TL;DR:最多几百万行非常安全。

您可以使用“生日问题”的数学公式计算实际概率。假设有一个完美的哈希函数,bigint 哈希的数字(2^64 - 1四舍五入为2^64不同的值)为:

SELECT sqrt(2^65 * ln(1/(1 - 0.1)))::int      AS p10     -- 1971577271
     , sqrt(2^65 * ln(1/(1 - 0.01)))::int     AS p1      --  608926881
     , sqrt(2^65 * ln(1/(1 - 0.001)))::int    AS p01     --  192124822
     , sqrt(2^65 * ln(1/(1 - 0.0001)))::int   AS p001    --   60741529
     , sqrt(2^65 * ln(1/(1 - 0.00001)))::int  AS p0001   --   19207726
     , sqrt(2^65 * ln(1/(1 - 0.000001)))::int AS p00001  --    6074003
Run Code Online (Sandbox Code Playgroud)

读取最后的计算p00001
对于大约 600 万个条目,至少单个哈希冲突的概率低于 0.000001 (= 0.0001 %)。

IOW,当应用于一百万个表(每个表有 6M 行)时,我们可以预期单个表会遇到哈希冲突。
对于大约 6 亿个条目 ( p1),至少发生单个哈希冲突的概率为 0.01。

具有 16 字节密钥空间(不同值)的md5()/的计算:uuid2^128

SELECT sqrt(2^129 * ln(1/(1 - 0.000001)))::int8 AS p00001  -- 26087642172564964
Run Code Online (Sandbox Code Playgroud)

阅读:
在大约 26 万亿行时,碰撞的几率变为 0.000001。


小智 10

您的问题源于 NULL 值。

来自文档强调

一般来说,如果表中有多于一行的约束中包含的所有列的值都相等,则违反了唯一约束。但是,在此比较中,两个空值永远不会被视为相等。这意味着即使存在唯一约束,也可以在至少一个受约束列中存储包含空值的重复行。此行为符合 SQL 标准,但我们听说其他 SQL 数据库可能不遵循此规则。因此,在开发可移植的应用程序时要小心。