1. 概念解析:什么是唯一约束和唯一索引?
在PostgreSQL中,唯一约束(UNIQUE CONSTRAINT)和唯一索引(UNIQUE INDEX)都是用于确保表中数据唯一性的机制,但它们的实现方式和应用场景存在本质区别。我们先从基础概念入手。
1.1 唯一约束的本质
唯一约束是表级别的完整性约束,它通过逻辑规则限制列或列组合的值必须唯一。当你在表上创建唯一约束时,PostgreSQL实际上会在后台自动创建一个唯一索引来实现这个约束。这是数据库设计中的声明式方法——你只需要告诉数据库"这些列需要保持唯一",而不需要关心具体如何实现。
例如,创建用户表时要求邮箱必须唯一:
sql复制CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE, -- 这就是唯一约束
username VARCHAR(50)
);
1.2 唯一索引的实质
唯一索引则是物理层面的数据结构,它直接在存储引擎层创建一个索引结构来加速查询并强制唯一性。创建唯一索引是一种命令式操作,你需要明确指定创建索引的指令:
sql复制CREATE UNIQUE INDEX idx_user_email ON users(email);
虽然两者都能保证数据唯一性,但唯一索引提供了更多底层控制选项,比如可以指定索引类型(B-tree、Hash等)、存储参数和部分索引条件。
注意:在PostgreSQL中,唯一约束本质上是通过唯一索引实现的,这就是为什么很多人容易混淆两者的原因。但它们在语义和使用方式上有重要区别。
2. 功能对比:核心差异详解
2.1 创建方式的差异
唯一约束只能作为表定义的一部分创建(CREATE TABLE或ALTER TABLE),而唯一索引可以独立创建。这是两者最直观的区别:
唯一约束的创建方式:
sql复制-- 建表时定义
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
sku VARCHAR(50) UNIQUE,
name VARCHAR(100)
);
-- 通过ALTER TABLE添加
ALTER TABLE products ADD CONSTRAINT uniq_product_sku UNIQUE (sku);
唯一索引的创建方式:
sql复制-- 独立创建
CREATE UNIQUE INDEX idx_product_sku ON products(sku);
-- 可以包含WHERE条件创建部分索引
CREATE UNIQUE INDEX idx_product_active_sku ON products(sku) WHERE is_active = true;
2.2 空值处理的区别
唯一约束和唯一索引对NULL值的处理方式存在微妙但重要的差异:
- 唯一约束:认为所有NULL值都是不相等的,允许多个NULL值存在
- 唯一索引:默认行为与约束相同,但可以通过特殊语法改变
sql复制-- 测试唯一约束
ALTER TABLE users ADD CONSTRAINT uniq_user_phone UNIQUE (phone);
INSERT INTO users (phone) VALUES (NULL); -- 成功
INSERT INTO users (phone) VALUES (NULL); -- 仍然成功
-- 测试唯一索引
CREATE UNIQUE INDEX idx_user_phone ON users(phone);
INSERT INTO users (phone) VALUES (NULL); -- 成功
INSERT INTO users (phone) VALUES (NULL); -- 仍然成功
-- 创建不允许NULL重复的唯一索引
CREATE UNIQUE INDEX idx_user_phone_not_null ON users(phone) WHERE phone IS NOT NULL;
2.3 外键引用的能力
这是唯一约束独有的特性——只有唯一约束(或主键)才能被其他表的外键引用:
sql复制CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id), -- 引用主键
product_sku VARCHAR(50) REFERENCES products(sku) -- 引用唯一约束
);
-- 尝试引用唯一索引会失败
CREATE TABLE order_items (
item_id SERIAL PRIMARY KEY,
product_sku VARCHAR(50) REFERENCES products(sku_idx) -- 错误!不能引用索引
);
2.4 延迟检查特性
唯一约束支持延迟验证(DEFERRABLE),这在事务处理中非常有用:
sql复制-- 创建可延迟的约束
ALTER TABLE invoices ADD CONSTRAINT uniq_invoice_number
UNIQUE (invoice_number) DEFERRABLE INITIALLY DEFERRED;
-- 在事务中临时违反约束
BEGIN;
INSERT INTO invoices (invoice_number) VALUES ('INV-001');
UPDATE invoices SET invoice_number = 'INV-002' WHERE id = 1; -- 正常执行
COMMIT; -- 只在提交时检查唯一性
唯一索引则不支持这种延迟检查机制,任何违反唯一性的操作都会立即报错。
3. 性能与实现细节
3.1 底层实现机制
虽然唯一约束是通过唯一索引实现的,但PostgreSQL对它们的处理方式有所不同:
- 唯一约束:自动创建的索引名称由系统生成(如
users_email_key),且不能直接操作这个索引 - 唯一索引:显式创建,可以自定义名称,并且可以随时删除或重建
查看自动创建的索引:
sql复制SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'users';
3.2 并发创建的性能差异
在大型表上创建唯一约束和唯一索引时,性能表现可能不同:
- 唯一约束:通过ALTER TABLE添加时,会锁定表以防止并发修改
- 唯一索引:可以使用CONCURRENTLY选项在线创建,减少锁争用
sql复制-- 非阻塞创建索引(需要PostgreSQL 12+)
CREATE UNIQUE INDEX CONCURRENTLY idx_user_email ON users(email);
-- 添加约束时无法使用CONCURRENTLY
ALTER TABLE users ADD CONSTRAINT uniq_user_email UNIQUE (email); -- 会锁定表
3.3 部分索引的灵活性
唯一索引支持创建部分索引(partial index),只对满足条件的行实施唯一性约束:
sql复制-- 只对活跃用户强制邮箱唯一
CREATE UNIQUE INDEX idx_active_user_email ON users(email) WHERE is_active = true;
-- 这样是允许的
INSERT INTO users (email, is_active) VALUES ('a@test.com', true);
INSERT INTO users (email, is_active) VALUES ('a@test.com', false); -- 允许
INSERT INTO users (email, is_active) VALUES ('a@test.com', false); -- 允许
唯一约束无法实现这种灵活的条件约束。
4. 实践应用场景指南
4.1 何时使用唯一约束
以下场景适合使用唯一约束:
- 业务逻辑要求:当某列或列组合在业务上必须唯一时(如用户邮箱、产品SKU)
- 需要外键引用:当其他表需要通过外键引用这些唯一列时
- 需要延迟检查:在复杂事务中需要临时违反唯一性时
- 声明式风格:当你想强调"这些列应该唯一"的业务规则而非实现细节时
4.2 何时使用唯一索引
以下场景适合使用唯一索引:
- 性能优化:当需要显式控制索引类型或参数时
- 条件唯一:当只需要对部分行实施唯一性约束时
- 已有数据验证:创建CONCURRENTLY索引可以先检查唯一性而不立即生效
- 函数或表达式:当需要对列的计算结果实施唯一性时
sql复制-- 对邮箱的小写形式创建唯一索引
CREATE UNIQUE INDEX idx_user_email_lower ON users(LOWER(email));
4.3 实际案例对比
案例1:用户注册系统
sql复制-- 使用唯一约束确保邮箱和用户名唯一
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE,
email VARCHAR(255) UNIQUE,
password_hash VARCHAR(255)
);
-- 添加可延迟的约束以备复杂操作
ALTER TABLE users ADD CONSTRAINT uniq_user_email
UNIQUE (email) DEFERRABLE INITIALLY IMMEDIATE;
案例2:产品库存系统
sql复制-- 使用唯一索引实现条件唯一
CREATE TABLE products (
id SERIAL PRIMARY KEY,
sku VARCHAR(50),
is_active BOOLEAN DEFAULT true
);
-- 只对活跃产品强制SKU唯一
CREATE UNIQUE INDEX idx_product_active_sku ON products(sku) WHERE is_active;
5. 常见问题与解决方案
5.1 如何判断表上存在唯一约束还是唯一索引?
使用以下查询可以区分:
sql复制SELECT
tc.constraint_name,
tc.constraint_type,
ccu.column_name,
i.indexname
FROM
information_schema.table_constraints tc
LEFT JOIN
information_schema.constraint_column_usage ccu
ON tc.constraint_name = ccu.constraint_name
LEFT JOIN
pg_indexes i
ON i.tablename = tc.table_name
AND i.indexname LIKE '%' || tc.constraint_name || '%'
WHERE
tc.table_name = 'your_table'
AND tc.constraint_type = 'UNIQUE';
5.2 如何将唯一索引转换为唯一约束?
PostgreSQL没有直接命令转换,但可以通过以下步骤实现:
sql复制-- 1. 删除原索引
DROP INDEX IF EXISTS idx_user_email;
-- 2. 添加约束(会自动创建新索引)
ALTER TABLE users ADD CONSTRAINT uniq_user_email UNIQUE (email);
5.3 如何处理批量导入时的唯一性冲突?
对于唯一约束,可以使用ON CONFLICT子句:
sql复制INSERT INTO users (email) VALUES ('a@test.com')
ON CONFLICT (email) DO UPDATE SET last_seen = NOW();
对于唯一索引,需要确保冲突处理与索引条件匹配:
sql复制-- 对于部分索引
INSERT INTO products (sku, is_active) VALUES ('SKU001', true)
ON CONFLICT (sku) WHERE is_active DO NOTHING;
5.4 如何优化大型表的唯一性检查?
对于千万级以上的表,添加唯一约束或索引可能很耗时。建议:
- 先在非生产环境测试操作时间
- 使用CONCURRENTLY创建唯一索引(PostgreSQL 12+)
- 在业务低峰期执行
- 考虑先创建普通索引,再转换为唯一索引
sql复制-- 分步操作更安全
CREATE INDEX CONCURRENTLY idx_temp ON big_table(email);
CREATE UNIQUE INDEX CONCURRENTLY idx_email ON big_table(email);
DROP INDEX idx_temp;
6. 高级技巧与最佳实践
6.1 多列组合的唯一性
唯一约束和索引都支持多列组合:
sql复制-- 部门内员工编号必须唯一
CREATE TABLE employees (
id SERIAL PRIMARY KEY,
department_id INTEGER,
employee_number INTEGER,
CONSTRAINT uniq_dept_emp_num UNIQUE (department_id, employee_number)
);
-- 等效的唯一索引
CREATE UNIQUE INDEX idx_dept_emp_num ON employees(department_id, employee_number);
注意:多列唯一性中NULL值的处理很特殊——只要有一列为NULL,该行就会被认为满足唯一性条件。
6.2 表达式索引实现复杂唯一性
唯一索引支持使用表达式实现更复杂的唯一性规则:
sql复制-- 确保同一用户不同时拥有两个活跃订阅
CREATE UNIQUE INDEX idx_user_active_sub ON subscriptions(user_id)
WHERE status = 'active';
-- 确保产品名称和规格的组合唯一(忽略大小写和空格)
CREATE UNIQUE INDEX idx_product_name_spec ON products(
LOWER(TRIM(name)),
LOWER(TRIM(specification))
);
6.3 在分区表上的应用
在PostgreSQL的分区表上,唯一约束和索引的行为有所不同:
- 唯一约束:必须包含分区键列,因为约束是在每个分区上独立执行的
- 唯一索引:可以创建不包含分区键的全局唯一索引(需要额外配置)
sql复制-- 分区表示例
CREATE TABLE measurements (
logdate DATE NOT NULL,
device_id INTEGER,
value FLOAT,
PRIMARY KEY (logdate, device_id)
) PARTITION BY RANGE (logdate);
-- 必须包含分区键的唯一约束
ALTER TABLE measurements ADD CONSTRAINT uniq_measurement
UNIQUE (logdate, device_id);
-- 全局唯一索引需要额外处理(如使用外部工具或触发器)
6.4 监控和维护建议
定期检查唯一约束和索引的使用情况:
sql复制-- 查看未使用的唯一索引
SELECT
indexname,
tablename
FROM
pg_stat_user_indexes
WHERE
idx_scan = 0
AND indexname LIKE '%uniq%' OR indexname LIKE '%unique%';
-- 检查索引大小
SELECT
indexname,
pg_size_pretty(pg_relation_size(indexname::regclass))
FROM
pg_indexes
WHERE
tablename = 'your_table';
对于频繁更新的表,唯一约束/索引可能会导致写性能下降。在这种情况下,可以考虑:
- 评估是否真的需要数据库级别的唯一性保证
- 在应用层实现部分唯一性检查
- 使用部分索引减少维护开销