在数据库迁移脚本、多次上线或手工修复数据时,有可能会不小心对同一列重复执行添加 UNIQUE 约束的操作。此时数据库表现并不一致:有些会直接成功并生成多个冗余索引,有些会因为约束名冲突而报错,还有些会复用已有索引。理解这一行为对表结构和写入性能很重要。

重复添加 UNIQUE 约束会直接成功吗?
以 MySQL 8.0 为例,建一张用户表并在 email 列上连续添加两个名称不同的唯一约束,执行以下语句:
CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, email VARCHAR(255) NOT NULL ); ALTER TABLE users ADD CONSTRAINT uq_users_email UNIQUE (email); ALTER TABLE users ADD CONSTRAINT uq_users_email_dup UNIQUE (email); SHOW INDEX FROM users;
执行后不会报错,说明 MySQL 并没有把同一列上的唯一约束视为冲突,而是按索引名进行区分。使用 SHOW INDEX FROM users 可以看到两条 Non_unique 为 0 的记录,索引名分别是 uq_users_email 和 uq_users_email_dup,列名均为 email。也就是说,MySQL 会为每个唯一约束创建一个独立的唯一索引,即使它们作用的列完全相同。
PostgreSQL 情况类似。只要约束名不同,重复添加唯一约束也会成功。与 MySQL 不同的是,PostgreSQL 中的约束是独立对象,每个唯一约束都使用一个唯一索引来支撑。即使两个约束作用的列完全相同,数据库也不会自动合并它们。如果使用相同的约束名再次添加,PostgreSQL 会报错:relation uq_users_email already exists。
由此可见,重复添加能否成功主要看约束名是否唯一。不同名称通常会成功,但代价是产生冗余的约束对象和索引对象;相同名称则直接失败。对于期待幂等执行的迁移脚本,这一点需要格外注意。
为什么唯一约束会变成冗余索引?
唯一约束的核心任务是保证一列或多列的值不会重复。大多数数据库在实现这个约束时,都会借助唯一索引。原因在于索引的本质是按键值排序存储,插入新数据时索引层会快速检查是否已有相同键值;一旦发现重复,就返回键冲突错误。因此唯一约束与唯一索引在实践中经常被混为一谈,但二者概念不同:约束是逻辑规则,索引是物理结构。
在 MySQL InnoDB 中,唯一约束直接表现为一个唯一二级索引。二级索引本身包含索引列和主键,占用磁盘空间,并且在插入、更新、删除时都需要同步维护。重复添加两个相同列的唯一约束,就等于创建了两个结构几乎一样的二级索引。它们都能阻止重复数据,但业务上只需要一个就足够。冗余索引不仅浪费空间,还会让每次写入多维护一份索引,延长事务时间。
可以用 SQL Server 的视角再印证。为列添加唯一约束时,SQL Server 会创建一个唯一非聚集索引来支撑该约束。约束名和索引名通常一致,删除约束时对应索引也会被一并删除。这说明重复约束并不是一个纯粹的元数据标记,它会真正落地到物理存储。即使某个数据库允许复用已有索引,也不能假设所有产品都这样做。
不同数据库的处理差异
MySQL 和 MariaDB 对重复的唯一约束非常宽松。它们不阻止同一列上存在多个唯一索引,只要索引名称不相同即可。MySQL 8.0 的 information_schema.statistics 可以查看这些索引。实际项目中如果迁移脚本多次执行,很容易遗漏这类重复索引,因为表结构看起来仍然有唯一约束,只是多了一份隐藏成本。
PostgreSQL 同样允许同一列上存在多个唯一约束,前提是约束名不同。每个约束对应一个唯一索引,查询 pg_constraint 可以看到多个 contype 为 u 的记录。PostgreSQL 不提供自动删除重复约束的功能。需要注意的是,如果第一个约束创建了索引,第二个约束不会自动复用第一个索引,除非在添加约束时显式使用 USING INDEX 指定已有索引。例如:
ALTER TABLE users ADD CONSTRAINT uq_users_email_dup UNIQUE USING INDEX uq_users_email;
Oracle 的规则略有不同。Oracle 在创建唯一约束时,如果目标列上已经存在唯一索引,会优先复用该索引,而不是再建一个新的。因此重复添加唯一约束时,可能不会增加索引数量,但约束本身仍然会重复存在。SQL Server 则介于两者之间,具体是否新建索引取决于已有索引能否复用,但用户通常不需要手动关心,数据库会做选择。
这些差异提示我们,跨数据库迁移时不能只依赖一种数据库的行为来设计幂等脚本。对于 MySQL 和 PostgreSQL 环境,重复 DDL 更容易产生冗余索引;对于 Oracle 环境,虽然索引可能复用,但重复约束仍然会让权限和依赖管理变复杂。
如何检查并清理重复约束
要发现重复的唯一约束,第一步是查询表上的约束和索引。以 MySQL 为例,可以从 information_schema.statistics 中找出同一张表、同一列上的多个索引。再结合 Non_unique 字段判断是否为唯一索引。如果看到 email 列对应两个 Non_unique 为 0 的记录,基本可以判定存在重复约束。
SELECT INDEX_NAME, NON_UNIQUE, COLUMN_NAME, SEQ_IN_INDEX FROM information_schema.statistics WHERE TABLE_SCHEMA = 'app_db' AND TABLE_NAME = 'users' ORDER BY INDEX_NAME, SEQ_IN_INDEX;
PostgreSQL 可以查询 pg_constraint 与 pg_index 的关联,或者直接查看 pg_indexes。下面语句列出 users 表上的所有唯一约束定义,方便对照是否重复。
SELECT conname, contype, pg_get_constraintdef(oid) FROM pg_constraint WHERE conrelid = 'users'::regclass AND contype = 'u';
清理时要先判断要删除的约束是否被其他数据库对象引用。比如外键可能引用唯一约束作为目标,直接删除会报错或破坏完整性。在 MySQL 中可以先执行 SHOW CREATE TABLE 查看外键;在 PostgreSQL 中可以查询 pg_constraint 中的 confrelid 依赖。确认没有依赖后,再执行删除语句:
-- MySQL 删除多余的唯一索引 ALTER TABLE users DROP INDEX uq_users_email_dup; -- PostgreSQL 删除多余的唯一约束 ALTER TABLE users DROP CONSTRAINT uq_users_email_dup;
清理完成后,再次检查表上的索引数量是否减少。对于生产环境,建议在低峰期执行,并使用在线 DDL 能力,避免长时间锁表。MySQL 8.0 的 ALTER TABLE 默认支持在线操作,PostgreSQL 的 CREATE INDEX CONCURRENTLY 可以在不阻塞写入的情况下创建索引,但删除约束也需要谨慎评估锁影响。
如何避免重复添加唯一约束
最有效的方法是在迁移脚本中加入存在性判断。比如在 MySQL 中,先查询 information_schema.statistics 判断索引是否已存在,再决定是否执行 ALTER TABLE。PostgreSQL 中可以查询 pg_constraint。这样可以保证脚本幂等,多次执行不会产生重复对象。
统一命名规范也很关键。推荐使用 uk_表名_列名 这样的格式给唯一约束命名。这样当有人再次添加同名约束时,数据库会直接报错,至少不会静默生成一个新的冗余索引。团队在代码评审时可以快速识别。
定期审计索引也是必要的。MySQL 8.0 提供 sys.schema_redundant_indexes 视图,可以辅助发现重复索引;PostgreSQL 则需要写自定义查询,比较 pg_index 中的列组合是否相同。把索引审计纳入发布流程,能够在冗余索引进入生产前被拦截。
总结一下,多次在同一列上添加 UNIQUE 约束,在不同数据库中表现不一,但相同约束名会报错,不同约束名通常会创建多个唯一约束或唯一索引。理解约束与索引的关系,结合查询和清理方法,可以有效避免这处容易忽略的性能隐患。

