Postgres唯一约束 vs 索引

266

据我理解,文档表明以下定义是等价的:

create table foo (
    id serial primary key,
    code integer,
    label text,
    constraint foo_uq unique (code, label));

create table foo (
    id serial primary key,
    code integer,
    label text);
create unique index foo_idx on foo using btree (code, label);    

然而,Postgres 9.4的手册中有一条注释:

向表中添加唯一约束的首选方式是使用ALTER TABLE ... ADD CONSTRAINT。考虑到实现细节不应该被直接访问,使用索引来强制执行唯一约束可能被认为是一种实现细节。

(编辑:此注释在Postgres 9.5中已被删除。)

这只是好的编程习惯吗?选择其中一种方法的实际后果是什么(例如在性能方面)?


34
唯一的实际区别是你可以创建一个外键到唯一约束,但不能创建到唯一索引。 - user330315
36
另一个问题中提出的相反的优点(最近在此提出)是,你可以拥有一个“部分”唯一索引,例如“Unique ( foo ) Where bar Is Null”。据我所知,使用约束条件无法实现这一点。 - IMSoP
4
@a_horse_with_no_name,我不确定这是何时发生的,但现在看起来似乎不再正确。这个SQL fiddle允许外键引用唯一索引:http://sqlfiddle.com/#!17/20ee9;编辑:向唯一索引添加“筛选器”将导致它停止工作(如预期)。 - user1935361
4
PostgreSQL在为表定义唯一约束或主键时,会自动创建一个唯一索引。 - maggu
2
其中一个区别是可延迟的行为 - 约束支持它,索引不支持。https://www.postgresql.org/docs/current/sql-set-constraints.html - Nikolai
显示剩余6条评论
11个回答

214

对于这个基础但重要的问题,我有一些疑问,所以我决定通过实例来学习。

让我们创建一个测试表master,它有两列:con_id带有唯一约束条件,ind_id通过唯一索引进行索引。

create table master (
    con_id integer unique,
    ind_id integer
);
create unique index master_unique_idx on master (ind_id);

    Table "public.master"
 Column |  Type   | Modifiers
--------+---------+-----------
 con_id | integer |
 ind_id | integer |
Indexes:
    "master_con_id_key" UNIQUE CONSTRAINT, btree (con_id)
    "master_unique_idx" UNIQUE, btree (ind_id)

在表的描述(使用psql中的\d命令)中,你可以从唯一约束和唯一索引中区分出来。

唯一性

为了确保唯一性,让我们进行检查。

test=# insert into master values (0, 0);
INSERT 0 1
test=# insert into master values (0, 1);
ERROR:  duplicate key value violates unique constraint "master_con_id_key"
DETAIL:  Key (con_id)=(0) already exists.
test=# insert into master values (1, 0);
ERROR:  duplicate key value violates unique constraint "master_unique_idx"
DETAIL:  Key (ind_id)=(0) already exists.
test=#

它按照预期工作!

外键

现在我们将定义一个detail表,其中包含两个外键,分别引用我们在master中的两个列。

create table detail (
    con_id integer,
    ind_id integer,
    constraint detail_fk1 foreign key (con_id) references master(con_id),
    constraint detail_fk2 foreign key (ind_id) references master(ind_id)
);

    Table "public.detail"
 Column |  Type   | Modifiers
--------+---------+-----------
 con_id | integer |
 ind_id | integer |
Foreign-key constraints:
    "detail_fk1" FOREIGN KEY (con_id) REFERENCES master(con_id)
    "detail_fk2" FOREIGN KEY (ind_id) REFERENCES master(ind_id)

好的,没有错误。让我们确保它可以正常工作。

test=# insert into detail values (0, 0);
INSERT 0 1
test=# insert into detail values (1, 0);
ERROR:  insert or update on table "detail" violates foreign key constraint "detail_fk1"
DETAIL:  Key (con_id)=(1) is not present in table "master".
test=# insert into detail values (0, 1);
ERROR:  insert or update on table "detail" violates foreign key constraint "detail_fk2"
DETAIL:  Key (ind_id)=(1) is not present in table "master".
test=#

两个列均可在外键中引用。

使用索引的约束

您可以使用现有的唯一索引添加表约束。

alter table master add constraint master_ind_id_key unique using index master_unique_idx;

    Table "public.master"
 Column |  Type   | Modifiers
--------+---------+-----------
 con_id | integer |
 ind_id | integer |
Indexes:
    "master_con_id_key" UNIQUE CONSTRAINT, btree (con_id)
    "master_ind_id_key" UNIQUE CONSTRAINT, btree (ind_id)
Referenced by:
    TABLE "detail" CONSTRAINT "detail_fk1" FOREIGN KEY (con_id) REFERENCES master(con_id)
    TABLE "detail" CONSTRAINT "detail_fk2" FOREIGN KEY (ind_id) REFERENCES master(ind_id)

现在列约束的描述没有区别。

部分索引

在表约束声明中,您不能创建部分索引。这直接来自于create table ...定义。在唯一索引声明中,您可以设置WHERE子句以创建部分索引。您还可以创建基于表达式(而不仅仅是列)的索引,并定义一些其他参数(排序规则,空值放置位置)。

无法使用部分索引添加表约束。

alter table master add column part_id integer;
create unique index master_partial_idx on master (part_id) where part_id is not null;

alter table master add constraint master_part_id_key unique using index master_partial_idx;
ERROR:  "master_partial_idx" is a partial index
LINE 1: alter table master add constraint master_part_id_key unique ...
                               ^
DETAIL:  Cannot create a primary key or unique constraint using such an index.

这是实际信息吗?特别是关于部分索引的内容。 - anatol
1
@anatol - 是的,没错。 - klin
@klin 关于最后一部分,您也不能将自定义排序索引作为约束添加 - undefined

49
使用UNIQUE INDEX相对于UNIQUE CONSTRAINT的另一个优点是,你可以轻松地使用CONCURRENTLY删除/创建索引,而使用约束则不行。

7
据我所知,不可能同时删除唯一索引。在https://www.postgresql.org/docs/9.3/static/sql-dropindex.html中提到:“在使用此选项时需要注意几个警告事项。只能指定一个索引名称,并且不支持CASCADE选项。(因此,支持UNIQUE或PRIMARY KEY约束的索引不能以这种方式被删除。)” - Rafał Cieślak
在Postgres 12.0中,当尝试并发删除唯一索引时,我没有收到任何错误;运行命令后的表定义显示该索引确实已被删除。DROP INDEX CONCURRENTLY IF EXISTS idx_name - Ruben Murray

33

唯一性是一种约束条件。它通过创建唯一索引来实现,因为索引可以快速搜索所有现有的值,以确定给定的值是否已经存在。

从概念上讲,索引是一种实现细节,唯一性应该仅与约束条件相关联。

完整文本

因此,速度表现应该是相同的。


从那句话中我读到,索引的速度性能更快而且不同。我认为这就是索引存在的全部原因。 - Zaffer
@Zaffer:可能你被“quickly”这个词所迷惑了,它并不等同于“quicker”。引用中说索引用于检查值是否已经存在。而这个任务可以很快地完成。同时,它还说当你在底层使用“唯一约束”时,它会使用“索引”。因此性能是相同的。 - Eugen Konkov

21

鉴于多人已经提供了唯一索引比唯一约束更优的优势,这里介绍一个缺点:唯一约束可以被延迟(只在事务结束时检查),而唯一索引则无法延迟。


这怎么可能呢?毕竟所有唯一约束都有唯一的索引。 - Chris
2
由于索引没有延迟API,只有约束条件才有,因此虽然延迟机制存在于支持唯一约束条件的背后,但无法声明索引为可延迟或推迟它。 - Masklinn
有趣的观察。因此,在使用约束(而不是索引)时,可以在事务期间拥有冲突数据,但如果最终结果没问题,那么它将成功吗?据我所知,索引会更早失败。 - Tvaroh
1
这是正确的,假设约束是可延迟和延迟的。还要注意,并非所有约束都是可延迟的:NOT NULLCHECK 约束始终是立即执行的。 - Masklinn
@Masklinn 用户需要做些什么才能使它们可延迟/延迟?谈论唯一性约束。更新:找到了这个https://hashrocket.com/blog/posts/deferring-database-constraints - Tvaroh
你需要在第一时间将约束条件创建为“DEFERRABLE”(默认为“NOT DEFERRABLE”),并且可以将其创建为“DEFERRABLE INITIALLY DEFERRED”(在这种情况下,默认情况下始终被延迟),或者在特定事务中使用“SET CONSTRAINTS <constraint_name> DEFERRED”来延迟约束条件的检查。哪种方法最好取决于你的具体用例,例如你是否总是希望推迟约束条件,或者只有少数几个地方需要这样做。 - Masklinn

16

我遇到的另一件事是,您可以在唯一索引中使用SQL表达式,但是不能在约束条件中使用。

因此,以下内容不起作用:

CREATE TABLE users (
    name text,
    UNIQUE (lower(name))
);

但是以下工作。

CREATE TABLE users (
    name text
);
CREATE UNIQUE INDEX uq_name on users (lower(name));

我会使用 citext 扩展。 - ceving
@ceving 这取决于使用情况。有时您希望保留大小写,同时确保不区分大小写的唯一性。 - Sampson Crowley
1
我也遇到了这个问题。我担心无法添加约束会成为一个问题,但似乎一切都正常工作。我尝试执行以下操作:ALTER TABLE films ADD CONSTRAINT unique_file_title UNIQUE USING INDEX lower_title_idx; 但是出现错误,无法使用此类索引创建主键或唯一约束。索引包含表达式。我尝试插入不区分大小写的数据,即使没有约束,它似乎也可以正常工作。 - RcoderNY
我也需要这个特定的功能,而且我可以像描述的那样使用函数索引来实现它,而约束条件则没有帮助。 - Rafs

15

使用约束而不是索引可以完成的一件小事是使用ON CONFLICT ON CONSTRAINT子句(也可以参考这个问题)。

以下方法不可行:

CREATE TABLE T (a INT PRIMARY KEY, b INT, c INT);
CREATE UNIQUE INDEX u ON t(b);

INSERT INTO T (a, b, c)
VALUES (1, 2, 3)
ON CONFLICT ON CONSTRAINT u
DO UPDATE SET c = 4
RETURNING *;

它会产生:
[42704]: ERROR: constraint "u" for table "t" does not exist

将索引转换为约束:
DROP INDEX u;
ALTER TABLE t ADD CONSTRAINT u UNIQUE (b);

现在,INSERT语句可以正常工作。


6
但在唯一索引上,您仍然可以通过在大括号中列出字段来执行操作:ON CONFLICT(b)DO UPDATE SET - BIOHAZARD

5

锁定操作有所不同。
添加索引不会阻止对表的读取访问。
添加约束将会加入一个表级别的锁定(因为它是通过ALTER TABLE添加的),所以所有的查询都会被阻塞。

最初的回答:

锁定操作有所不同。
添加索引不会阻止对表的读取访问。
添加约束将会加入一个表级别的锁定(因为它是通过ALTER TABLE添加的),所以所有的查询都会被阻塞。


这是一个很好的观点,我认为。 - tim

1
除了其他答案之外,还有一个话题,即唯一约束是否也用于加速查询,就像索引一样。
显然,约束实际上用于索引扫描,如EXPLAIN所示:
ALTER TABLE mytable
    ADD CONSTRAINT mytable_uc UNIQUE (other_id, name);

explain select * from mytable
    where name = 'name' and other_id = 154

结果:

Index Scan using mytable_uc on mytable  (cost=0.28..2.29 rows=1 width=101)
  Index Cond: ((other_id = 154) AND ((name)::text = 'name'::text))

1
截至PostgreSQL 16.1版本,无论是哪种方式,最终都会以相同的方式强制唯一性,但在命令底层索引的方式上并不相等。还有第三种实现unique的方式,即排除约束(自9.0版本开始,但下面的矩阵是基于16.1版本的状态):
控制权 索引 约束 索引作为约束 排除约束
排序规则,方向,null 排序
自定义相等性
包含载荷列
索引存储参数
表空间
部分(WHERE
多列 是* (通过复合/数组/JSON/范围等模拟)
延迟验证
空值不区分 是* (通过 coalesce() 或需要 not distinct from 运算符模拟)
并发创建 (没有等效的索引语法来启用作为约束的添加)
总的来说,排除约束的灵活性的代价是性能

如果所有指定的操作符都测试相等性,那么这等同于一个唯一约束,尽管一个普通的唯一约束会更快。然而,排除约束可以指定比简单相等性更一般的约束。

对于多个字段来说,设置起来也更困难,因为你需要以某种方式将它们合并成一个,这进一步复杂化了配置,可能会带来更高的性能损失。

在db<>fiddle上的演示


限制条件:
create table t (
    id bigserial primary key,
    val1 text,
    val2 text,
    unique /*concurrently*/
        nulls not distinct 
       ( val1 
       /*collate unicode 
         text_ops 
         desc nulls last
         with operator(pg_catalog.=)*/) 
        include(val2)
        with(fillfactor=100)
        using index tablespace pg_default
        --where (id%2=1)
        deferrable initially deferred
);

索引:

create table t2 (
    id bigserial primary key,
    val1 text,
    val2 text
);
create unique index concurrently 
  on t2 (val1 
         collate unicode 
         text_ops 
         desc nulls last
         /*with operator(pg_catalog.=)*/) 
  include(val2)
  nulls not distinct 
  with(fillfactor=100)
  tablespace pg_default
  where id%2=1
  /*deferrable initially deferred*/;

索引作为约束添加:

create unique index concurrently uidx
  on t3 (val1 
         /*collate unicode 
         text_ops 
         desc nulls last
         with operator(pg_catalog.=)*/) 
  include(val2)
  nulls not distinct 
  with(fillfactor=100)
  tablespace pg_default
  /*where id%2=1*/;
alter table t3 
  add constraint ucns 
  unique using index uidx 
  deferrable initially deferred;

排除:
create table t4 (
    id bigserial primary key,
    val1 text check(val1<>'reserved'),
    val2 text,
    exclude /*concurrently*/
            (coalesce(val1,'reserved') 
             collate unicode 
             text_ops 
             desc nulls last 
             with operator(pg_catalog.=))
        include(val2)
        with(fillfactor=100)
        using index tablespace pg_default
        where (id%2=1)
        deferrable initially deferred
);

0
我在文档中读到了这段内容:

ADD table_constraint [ NOT VALID ]

使用与CREATE TABLE相同的语法,加上选项NOT VALID,该形式向表中添加新的约束条件。目前只允许外键约束条件使用。如果约束条件标记为NOT VALID,则将跳过验证所有行是否符合约束条件的潜在长时间初始检查。约束条件仍将强制执行对于随后的插入或更新(也就是说,除非在引用表中有匹配的行,否则它们将失败)。但是数据库不会假设约束条件对于表中的所有行都是成立的,除非使用VALIDATE CONSTRAINT选项进行验证。

所以我认为通过添加约束条件可以实现“部分唯一性”。
关于如何确保唯一性:
添加唯一约束将自动在列或列组中创建唯一的B树索引。仅覆盖某些行的唯一性限制无法编写为唯一约束,但可以通过创建唯一部分索引来强制执行这种限制。
注意:向表添加唯一约束的首选方法是ALTER TABLE … ADD CONSTRAINT。使用索引来强制执行唯一约束可能被视为不应直接访问的实现细节。然而,应该意识到,在唯一列上手动创建索引是没有必要的;这样做只会复制自动创建的索引。
因此,我们应该添加约束,以创建索引,以确保唯一性。
我如何看待这个问题?
“约束”旨在从语法上确保该列应该是唯一的,它建立了一条规则,一条法律;而“索引”是关于“如何实现、如何实现唯一性、在实现时唯一性意味着什么”的语义化内容。因此,Postgresql实现的方式非常合理:首先,您声明一个列应该是唯一的,然后,Postgresql为您添加了添加唯一索引的实现。

1
所以我认为通过添加约束条件可以实现“部分唯一性”,索引只能应用于通过where子句定义的记录的明确定义的子集,因此您可以定义仅当它们满足某些条件时记录是唯一的。这仅会禁用未定义的记录集上的约束条件,这些记录在创建约束条件之前存在。这完全不同,而且后者显然不太有用,尽管对于渐进式迁移来说很方便。 - Masklinn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接