【问题标题】:Unique index by another column for postgresqlpostgresql 的另一列的唯一索引
【发布时间】:2026-02-08 17:15:02
【问题描述】:

例如,我有这张表(postgresql):

CREATE TABLE t(
 a TEXT,
 b TEXT
);
CREATE UNIQUE INDEX t_a_uniq_idx ON t(a);

我想为 ba 列创建唯一约束/索引。但不简单ADD CONSTRAINT t_ab UNIQUE (a, b)。我想要独特的b a

INSERT INTO t(a,b) VALUES('123', null); -- this is ok
INSERT INTO t(a,b) VALUES('456', '123'); -- this is not ok, because duplicate '123'

我该怎么做?

编辑:

我为什么需要这个?例如,如果我有users 表并且我想创建电子邮件更改功能,我需要这样的结构:

CREATE TABLE users(
 email TEXT,
 unconfirmed_email TEXT
 -- some other data
);
CREATE UNIQUE INDEX unq_users_email_idx ON users(email);

用户可以在unconfirmed_email 列中设置值,但前提是该值未在email 列中使用。

【问题讨论】:

  • 需要更多的例子和解释。
  • @GordonLinoff 更新问题
  • 您的更新仍然有解释的余地​​。如果用户将他的email 设置为unconfirmed_email 中已经存在的值怎么办?允许email = unconfirmed_email 在同一行吗? unconfirmed_email UNIQUE?
  • 不,不允许。每封电子邮件必须有两列是唯一的。

标签: sql postgresql


【解决方案1】:

如果两个列都需要唯一性,我认为你的数据模型有误。您应该有两个表,而不是将对存储在一行中:

create table pairs (
    pairid int generated always as identity,
    . . .   -- more information about the pair, if needed
);

create table pairElements (
    pairElementId int generated always as identity,
    pairId int references pairs(pairid),
    which int check (which in (1, 2)),
    value text,
    unique (pairid, which)
);

那么条件很简单:

create constraint unq_pairelements_value unique pairelements(value);

【讨论】:

  • 是的,这可行。但我不想为每个搜索请求加入两个表。没有更简单的解决方案吗?
【解决方案2】:

虽然这会导致一个有趣的问题,但我同意可以更好地对数据进行建模 - 具体而言,unconfirmed_email 列可以被视为结合了两个属性:地址和用户之间的关联,它与email专栏;以及该地址是否被确认的状态,这取决于用户和地址的组合,而不是两者之一。

这意味着应该从user_email_addresses 中提取一个新表:

  • user_id - 用户的外键
  • 电子邮件 - 不可为空
  • is_confirmed 布尔值

有趣的是,事实证明,这个提取的表包含可以添加的自然数据:

  • 地址是什么时候添加的?
  • 什么时候确认的?
  • 发送给用户的验证码是什么?
  • 如果允许用户使用多个地址,哪个是主要地址,或者哪个用于特定目的?

我们现在可以对该表的各种约束建模(在某些情况下使用唯一索引,因为您不能在唯一约束上指定 Where):

  • 每个用户只能与一个特定的电子邮件地址有一个关联(无论是否确认):Constraint Unique ( user_id, email )
  • 一个电子邮件地址只能为一个用户确认:Unique Index On user_emails ( email ) Where is_confirmed Is True;
  • 每个用户只能有一个确认地址:Unique Index On user_emails ( user_id ) Where is_confirmed Is True;。您可能需要对此进行调整,以允许用户确认多个地址,但只有一个“主”地址。
  • 每个用户只能有一个未确认地址:Unique Index On user_emails ( user_id ) Where is_confirmed Is False;。您当前的设计中暗示了这一点,但实际上可能没有必要。

这给我们留下了原始问题的改写版本:我们如何禁止 unconfirmed 行与 confirmed 行具有相同的email,但允许多个相同的未确认行。

一种方法是对email 匹配但is_confirmed 匹配的行使用Exclude 约束。强制转换为 int 是必要的,因为在 boolean 上创建 gist 索引失败。

Alter Table user_emails
Add Constraint unconfirmed_must_not_match_confirmed
Exclude Using gist ( 
    email With =,
    Cast(is_confirmed as Int) With <>
);

就其本身而言,这将允许email 的多个副本,只要它们都具有相同的is_confirmed 值。但是由于我们已经限制了is_confirmed Is True 所在的多行,因此剩下的唯一重复项将是所有匹配行上的is_confirmed Is False


这是一个演示上述设计的 dbfiddle:https://dbfiddle.uk/?rdbms=postgres_12&fiddle=fd8e4e6a4cce79d9bc6bf07111e68df9

【讨论】:

    【解决方案3】:

    据我了解,您需要 UNIQUE 索引而不是 ab 组合。
    您的更新缩小了范围(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。

    这似乎很难实现,所以不要屏住呼吸。

    如果 abinteger 列,我们可以使它与整数数组一起工作:

    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 列中添加一个帮助表以存储来自 ab 的值:

    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 中的UPDATEDELETE 级联,你仍然还需要将UPDATEDELETE 投影到帮助表中(或实现更多触发器)。棘手的极端情况,但有解决方案。在我使用hashtext() 找到上述带有排除约束的解决方案之后,不再赘述...

    相关:

    【讨论】:

    • 哇,感谢您的详细回答!我也在考虑排除约束。太糟糕了,我们不能将数组用于字符串。
    • @Kroid:我们可以让它与整数数组一起工作。考虑添加的解决方案。 :)