Postgres 唯一约束与索引

Posted

技术标签:

【中文标题】Postgres 唯一约束与索引【英文标题】:Postgres unique constraint vs index 【发布时间】:2014-06-25 21:44:17 【问题描述】:

据我所知documentation 以下定义是等价的:

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);    

但是,the manual for Postgres 9.4 中的注释说:

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

(编辑:此注释已从 Postgres 9.5 的手册中删除。)

这只是风格的问题吗?选择这些变体之一的实际后果是什么(例如在性能方面)?

【问题讨论】:

(唯一的)实际区别是您可以为唯一约束创建外键,但不能为唯一索引创建外键。 另一个优势 (as came up in another question recently) 是您可以有一个 部分 唯一索引,例如“Unique ( foo ) Where bar Is Null”。 AFAIK,没有办法通过约束来做到这一点。 @a_horse_with_no_name 我不确定这是什么时候发生的,但这似乎不再是真的。此 SQL 小提琴允许外键引用唯一索引:sqlfiddle.com/#!17/20ee9;编辑:向唯一索引添加“过滤器”会导致它停止工作(如预期的那样) 来自 postgres 文档:当为表定义唯一约束或主键时,PostgreSQL 会自动创建唯一索引。 postgresql.org/docs/9.4/static/indexes-unique.html 我同意@user1935361,如果无法为唯一索引(至少 PG 10)创建外键,我很久以前就会遇到这个问题。 【参考方案1】:
SELECT a.phone_number,count(*) FROM public.users a
Group BY phone_number Having count(*)>1;

SELECT a.phone_number,count(*) FROM public.retailers a
Group BY phone_number Having count(*)>1;

select a.phone_number from users a inner join users b
on a.id <> b.id and a.phone_number = b.phone_number order by a.id;


select a.phone_number from retailers a inner join retailers b
on a.id <> b.id and a.phone_number = b.phone_number order by a.id
DELETE FROM
    users a
        USING users b
WHERE
    a.id > b.id
    AND a.phone_number = b.phone_number;
    
DELETE FROM
    retailers a
        USING retailers b
WHERE
    a.id > b.id
    AND a.phone_number = b.phone_number;
    
CREATE UNIQUE INDEX CONCURRENTLY users_phone_number 
ON users (phone_number);

验证:

insert into users(name,phone_number,created_at,updated_at) select name,phone_number,created_at,updated_at from users

【讨论】:

这个问题真的很老了,请注意新的。我认为您想将此粘贴到其他帖子,因为它是 OT。 如果您认为这个答案与给定的问题有关,请添加一些解释,以便其他人可以从中学习【参考方案2】:

使用ON CONFLICT ON CONSTRAINT 子句 (see also this question) 可以仅使用约束而不使用索引来完成。

这不起作用:

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 语句现在可以使用了。

【讨论】:

但是在唯一索引上,您仍然可以通过在大括号中列出字段来完成:ON CONFLICT (b) DO UPDATE SET【参考方案3】:

我对这个基本但重要的问题有些怀疑,所以我决定以身作则。

让我们创建一个包含两列的测试表 mastercon_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 ... 的definition。 在唯一索引声明中,您可以设置WHERE clause 来创建部分索引。 您还可以在表达式上create index(不仅在列上)并定义一些其他参数(排序规则、排序顺序、NULL 位置)。

不能使用部分索引添加表约束。

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 - 是的。【参考方案4】:

由于很多人都提供了唯一索引优于唯一约束的优点,这里有个缺点:唯一约束可以延迟(仅在事务结束时检查),唯一索引不能。

【讨论】:

鉴于所有唯一约束都有唯一索引,这怎么可能? 因为索引没有用于延迟的 API,只有约束有,所以虽然延迟机制存在于底层以支持唯一约束,但无法将索引声明为可延迟或延迟它. 有趣的观察。因此,当使用约束(与索引)时,在事务期间可能会有冲突的数据,但如果最后没问题,那么它会成功吗?据我了解,索引会更早失败。 这是正确的,假设约束是可延期的和延期的。另请注意,并非所有约束都是可延迟的:NOT NULLCHECK 约束始终是即时的。 @Masklinn 用户是否需要做任何事情来使它们可延期/延期?谈到唯一性约束。 UPD:找到这个hashrocket.com/blog/posts/deferring-database-constraints【参考方案5】:

锁定有所不同。 添加索引不会阻止对表的读取访问。 添加约束确实会设置表锁(因此所有选择都被阻止),因为它是通过 ALTER TABLE 添加的。

【讨论】:

【参考方案6】:

我在文档中读到了这个:

添加 table_constraint [ 无效 ]

此表单使用与CREATE TABLE 相同的语法向表添加新约束,加上选项NOT VALID,目前仅允许用于外键约束。如果约束标记为NOT VALID,则跳过验证表中所有行是否满足约束的可能冗长的初始检查。约束仍将针对后续插入或更新(也就是说,除非引用表中存在匹配行,否则它们将失败)。但是数据库不会假定约束适用于表中的所有行,直到使用 VALIDATE CONSTRAINT 选项对其进行验证。

所以我认为这就是你所说的通过添加约束的“部分唯一性”。

还有,关于如何保证唯一性:

添加唯一约束将自动在约束中列出的列或列组上创建唯一 B 树索引。仅涵盖某些行的唯一性限制不能写为唯一性约束,但可以通过创建唯一部分索引来强制执行此类限制。

注意:向表添加唯一约束的首选方法是 ALTER TABLE ... ADD CONSTRAINT。使用索引来强制执行唯一约束可以被认为是不应该直接访问的实现细节。然而,应该注意的是,没有必要在唯一列上手动创建索引。这样做只会复制自动创建的索引。

所以我们应该添加约束,它创建一个索引,以确保唯一性。

我怎么看这个问题?

“约束”旨在语法上确保此列应该是唯一的,它建立了法律,规则;而“索引”是语义,关于“如何实现,如何实现唯一性,唯一性在实现方面意味着什么”。所以,Postgresql 实现它的方式,是非常合乎逻辑的:首先,你声明一个列应该是唯一的,然后,Postgresql 为你添加添加唯一索引的实现

【讨论】:

“所以我认为这就是你所谓的“部分唯一性”,通过添加约束。”索引只能通过where 子句应用于定义明确的记录子集,因此您可以定义记录是唯一的,它们满足某些条件。这只是禁用了在创建约束之前未定义的记录集的约束。它完全不同,后者明显不太有用,尽管我猜它对渐进式迁移很方便。【参考方案7】:

我遇到的另一件事是您可以在唯一索引中使用 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 它取决于用例。有时您希望保留大小写同时确保不区分大小写的唯一性 我也遇到了这个问题。我担心无法添加约束会是一个问题,但它似乎工作正常。我试图这样做: ALTER TABLE 电影添加约束 unique_file_title UNIQUE USING INDEX lower_title_idx;但出现错误无法使用此类索引创建主键或唯一约束。索引包含表达式。我尝试插入不区分大小写的数据,即使没有约束它似乎也能工作。【参考方案8】:

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

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

The full text

所以速度表现应该是一样的

【讨论】:

【参考方案9】:

使用UNIQUE INDEXUNIQUE CONSTRAINT 相比,另一个优势是您可以轻松地DROP/CREATE 索引CONCURRENTLY,而使用约束则不能。

【讨论】:

AFAIK 不可能同时删除唯一索引。 postgresql.org/docs/9.3/static/sql-dropindex.html "使用此选项时需要注意几个注意事项。只能指定一个索引名称,并且不支持 CASCADE 选项。(因此,不能删除支持 UNIQUE 或 PRIMARY KEY 约束的索引方式。)”

以上是关于Postgres 唯一约束与索引的主要内容,如果未能解决你的问题,请参考以下文章

Oracle数据库主键约束与唯一索引有啥区别?

Mysql数据唯一约束与唯一索引案例总结及踩坑记(含NULL值与唯一约束唯一索引的搭配使用)

Oracle数据库主键约束与唯一索引有啥区别?

SQLServer 唯一键约束和唯一索引有啥区别

mongodb 索引唯一性约束

唯一性约束和唯一性索引的区别