在PostgreSQL的表设计环节,唯一约束和唯一索引大概是让初学者最容易犯迷糊的一对概念。你在建表语句里写上一个UNIQUE约束,然后用\d命令查看表结构时,会发现数据库悄悄多了一个同名索引;反过来,如果你手动创建一个唯一索引,表定义里却不会显示任何约束信息。这两者到底是什么关系?能不能只建其一?带着这些问题,我们从头梳理一遍。

添加唯一约束时数据库内部做了什么
先看一个最简单的例子,创建一张带唯一约束的表:
CREATE TABLE users (
id bigserial PRIMARY KEY,
email text NOT NULL,
phone varchar(20),
CONSTRAINT uq_users_email UNIQUE (email)
);执行完这条语句后,用\d users查看表结构,你会看到Indexes部分多了一个名为uq_users_email的索引,索引类型是btree。这不是巧合,而是PostgreSQL的固定行为:唯一约束的检查逻辑本质上依赖一个唯一索引来实现。当插入或更新一行数据时,数据库在索引中查找待写入的键值,如果发现已存在相同键值,立即报错回滚。
换句话说,唯一约束是逻辑规则,唯一索引是物理支撑。约束定义在系统目录pg_constraint中,索引定义在pg_class和pg_index中,两者通过pg_constraint.conindid字段关联起来。可以用下面这条查询验证这种关联:
SELECT conname AS constraint_name,
conindid::regclass AS backing_index
FROM pg_constraint
WHERE conrelid = 'users'::regclass
AND contype = 'u';理解了这层依赖关系,很多现象就说得通了。比如你不能单独删除约束背后的那个索引,必须先删约束;数据库会保证两者同生同灭,不会出现约束还在而索引没了的悬空状态。
唯一约束与手动建唯一索引的差异对比
既然约束会自动生成唯一索引,那直接用CREATE UNIQUE INDEX是不是完全等价?从重复校验的效果看,两者确实一样,都能阻止重复值写入,错误提示也都是duplicate key violates unique constraint这类信息。但在细节上存在几处值得注意的差异。
第一处差异是元数据层面。通过约束创建的索引在pg_index中indisprimary为false、但在约束目录中有对应记录,而手动创建的唯一索引不会有约束记录。这影响到某些工具的行为,例如一些ORM框架在反向生成模型时,只会识别真正的约束而忽略裸索引;某些逻辑复制和CDC工具也会区分对待。第二处差异是可移植性,标准SQL只定义了约束语法,CREATE UNIQUE INDEX是PostgreSQL的扩展能力,跨数据库迁移时约束写法更稳妥。第三处是命名与错误信息,约束可以显式命名,报错时提示约束名,排查问题更方便。
还有一点容易被忽视:ON CONFLICT子句的兼容性。虽然它既可以匹配约束也可以匹配唯一索引,但写法略有讲究:
-- 匹配命名约束(推荐,语义清晰)
INSERT INTO users (email, phone) VALUES ('a@ipipp.com', '123')
ON CONFLICT ON CONSTRAINT uq_users_email DO NOTHING;
-- 按推断列匹配,约束或唯一索引均可
INSERT INTO users (email, phone) VALUES ('a@ipipp.com', '123')
ON CONFLICT (email) DO NOTHING;如果一个唯一索引建立在表达式或函数上,ON CONFLICT (email)这种按列推断的写法就失效了,必须显式指定索引推断条件,这也是裸索引和约束在实战中行为差异的典型例子。
NULL值、部分索引与进阶用法
PostgreSQL的唯一索引对NULL值的处理遵循SQL标准:NULL不等于任何值,包括NULL自身,因此唯一约束的列可以插入多个NULL。这一点和某些数据库默认行为不同,从Oracle或SQL Server迁移过来的同学要特别注意。如果业务上要求NULL也唯一,可以用部分唯一索引解决:
-- 只对非空值强制唯一,允许多个NULL CREATE UNIQUE INDEX uq_users_phone ON users (phone) WHERE phone IS NOT NULL;
这是唯一约束做不到的事情:约束不能带WHERE条件,而部分唯一索引可以。典型场景如订单表,草稿状态的订单编号允许重复,只有正式订单才要求唯一:
CREATE UNIQUE INDEX uq_order_no_active ON orders (order_no) WHERE status = 'active';
表达式唯一索引则是另一项约束无法实现的能力。比如希望邮箱存储时忽略大小写唯一:
CREATE UNIQUE INDEX uq_users_email_lower ON users (lower(email));
注意使用该索引后,ON CONFLICT的写法也要跟着调整为ON CONFLICT (lower(email))。这些高级特性在用户名去重、软删除记录唯一性、时区无关的时间唯一性等场景中非常实用。
生产环境的选型建议与常见坑
综合以上分析,选型原则可以概括为一句话:如果规则是业务语义的一部分、希望它出现在表定义中并被各种工具识别,就用唯一约束;如果需要条件唯一、表达式唯一,或者只需要索引级别的去重保护,就用唯一唯一索引。
有几个常见的坑值得列出。首先是主键与唯一约束重复建设,主键本身就隐含唯一索引,再加一个相同列的唯一约束纯属浪费存储和写入开销。其次,给已有大表添加唯一约束时,PostgreSQL会扫描全表构建索引并校验数据,期间持有排他锁,建议加CONCURRENTLY选项,但注意约束语法不能直接带这个选项,需要分两步走:
-- 第一步:并发创建唯一索引,不阻塞写入
CREATE UNIQUE INDEX CONCURRENTLY uq_users_email
ON users (email);
-- 第二步:将索引挂载为约束
ALTER TABLE users
ADD CONSTRAINT uq_users_email UNIQUE
USING INDEX uq_users_email;最后提醒一点,如果表中已存在重复数据,无论是建约束还是建索引都会失败,需要先用窗口函数或分组查询清理重复行。理清唯一约束与唯一索引的关系,不仅能写出更规范的建表语句,也能在处理去重、冲突写入、数据迁移时少走很多弯路。
PostgreSQL唯一约束唯一索引修改时间:2026-09-03 01:44:47