搞数据库开发这些年,我最怕听到的一句话就是“系统跑着跑着,突然发现数据重复了”。之前接手一个订单系统,因为没设唯一约束,同一批库存被两次扣减的故障赔偿了不少钱。事后复盘,核心原因很简单:业务层做了重复校验,但接口被并发请求打穿,两层校验都没拦住。所以后来我养成了一个习惯:凡是业务上不允许重复的数据,数据库层面必须加一道 UNIQUE 约束,永远不要把唯一性完全托付给应用层。
这篇文章就想把 UNIQUE 约束从语法到原理、从创建到排障,整个讲透。不管你是刚学 SQL 的学生,还是写了几年业务代码但一直靠应用层做校验的后端工程师,看完全文能理解它背后那套规则,并且在自己项目里用得明明白白。
1. UNIQUE 约束是什么:一个最容易被低估的数据库骑士
1.1 一句话理解 UNIQUE 约束
UNIQUE 约束是 SQL 标准里用于保证字段或字段组合“值不重复”的一种声明式规则。建表的时候直接写在字段后面,或者通过独立约束语句定义,数据库就会在写入时自动检查:如果新插入或更新后的数据在指定列上已经存在相同值,就直接拒绝这次操作。
它不只是一个“检查规则”,更准确的定位是“数据完整性防线”。你可以把它当成数据库给我们免费提供的一个并发安全的重试锁——不管有多少个应用实例在同时写数据,数据库自身会保证同一值的唯一性。应用层几个人口检查都可能有并发缝隙,数据库这一层没有。
1.2 UNIQUE 约束到底解决了什么问题
先说最常见的一个:业务上的“唯一身份”字段。比如用户邮箱、手机号、身份证号、库存单号、优惠券码,这些字段天然就不允许重复。如果业务层没做校验或者校验逻辑有疏漏,数据库里涌入两条一模一样的记录,后面所有基于这个字段的查询都会出问题。
再比如防止重复提交。电商支付回调、消息队列消费者、积分兑换这类场景,同一笔订单很可能被系统以“重试”的方式处理多次。如果数据库里对“订单号 + 业务类型”加上一个唯一约束,第二次回调就会直接插入失败,重复处理的问题从根源解决。很多分布式场景下的“幂等表”,本质上就是在用唯一约束兜底。
所以 UNIQUE 约束解决的核心问题可以概括成两层:第一层是保障数据本身符合业务规则,第二层是给应用层提供可靠的幂等机制和异常发现机制。
1.3 UNIQUE 约束和 PRIMARY KEY 的边界关系
很多人一开始分不清 UNIQUE 和 PRIMARY KEY。简单讲:主键也是唯一,但唯一约束不一定是主键。主键最主要的职责是“行的唯一标识”,一张表只能有一个主键,而且主键列默认不允许为空;UNIQUE 约束主要职责是“列值的唯一性”,一张表可以有多个 UNIQUE 约束。
从实现层面看,两者的底层都会创建唯一索引,所以查询性能上都很强。但语义上主键更偏“实体标识”,UNIQUE 更偏“业务规则”。我自己的实践是:每张表只保留一个主键,其他业务上需要唯一的字段全部用 UNIQUE 约束。有些开发者习惯把手机号设成主键,短时间没毛病,但后期如果要支持多账号体系或者数据分片,主键的自增整数往往更稳定,业务唯一键交给 UNIQUE 约束去管,解耦更干净。
2. UNIQUE 约束的正确创建姿势
2.1 建表时声明约束的几种写法对比
第一种写法,直接在字段后面加 UNIQUE 关键字,适合单个字段唯一的情况:
CREATE TABLE users ( id INT PRIMARY KEY, email VARCHAR(255) UNIQUE, phone VARCHAR(20) UNIQUE );这段 SQL 的意思很直白:email 和 phone 各自不能有重复值。如果你只想限制手机号唯一、邮箱不唯一,那就只在 phone 后面加约束。
第二种写法,用表级约束,适合约束条件比较复杂的情况:
CREATE TABLE users ( id INT PRIMARY KEY, email VARCHAR(255), phone VARCHAR(20), UNIQUE (email) );表级约束的优势是可以给约束起名字,后期要删除或调整时能按名字精确操作。我强烈建议所有约束都显式命名,不然数据库自动生成的名字五花八门,排障时全靠猜。
推荐带命名方式:
CREATE TABLE users ( id INT PRIMARY KEY, email VARCHAR(255), phone VARCHAR(20), CONSTRAINT uk_users_email UNIQUE (email), CONSTRAINT uk_users_phone UNIQUE (phone) );这里 uk_users_email 是约束名,命名规则建议统一,比如“uk_表名_字段名”,方便后期维护。约束名在删除、禁用、查找约束元数据时非常有用。
2.2 已有表上追加约束:先想清楚存量数据
实际项目里很少有机会从建表时就规划好所有约束,更多情况是表已经跑了大半年、里面堆满数据,然后某天发现部分字段有重复,需要补约束。这时候顺序很重要,不能直接 ALTER,要先处理存量数据。
正确的追加方式:
ALTER TABLE users ADD CONSTRAINT uk_users_email UNIQUE (email);如果表里已经有重复的 email 值,这条语句会直接报错,提示存在重复键。这时先得把重复数据清理掉,具体清理思路我在第 4 章展开。这里要特别提醒一句:加约束之前一定要先对数据做一次去重和检查,否则 ALTER 语句一执行,数据库会立刻拒绝,留下一个半成品状态。
另外,往大表上加 UNIQUE 约束时会触发对表的扫描和索引创建,锁表时间可能很长。在生产环境操作前,建议评估表数据量和写入高峰,必要时用在线工具或低峰期执行。
2.3 多列联合唯一性:业务里真正有价值的用法
单列唯一约束很简单,但业务里更常见的是“两个字段合起来不能重复”。举几个典型场景:订单表和商品表中间的关系表里,同一订单不允许出现同一商品两行记录;用户和角色关联表里,同一用户不能重复绑定同一角色;签到表里,同一用户在一天内只能有一条签到记录。
这类需求要用的就是复合唯一约束:
CREATE TABLE order_items ( order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, CONSTRAINT uk_order_product UNIQUE (order_id, product_id) );这个约束表示 (order_id, product_id) 这一对值组合不能重复,但单独某一列里可以有重复值。比如订单 1001 可以包含商品 2001 和 2002,订单 1002 也可以包含商品 2001,两者互不冲突。
联合唯一约束在插入数据时有一个容易被忽视的细节:如果我们先插入了 (1001, 2001),再去插入 (2001, 1001),从值的角度看不重复,会被允许。也就是说组合唯一性跟字段顺序有关,联合索引的最左前缀法则在这里同样适用。
2.4 各种数据库的兼容性差异,最好一次搞清楚
UNIQUE 约束是 SQL 标准语法,但主流数据库在细节实现上有些差异。我自己实际用下来整理了一张对照表:
| 数据库 | 建表时创建 | 追加方式 | 对多个 NULL 的处理 |
|---|---|---|---|
| MySQL | 字段级/表级 | ALTER TABLE ADD CONSTRAINT | 允许存在多个 NULL |
| Oracle | 字段级/表级 | ALTER TABLE ADD CONSTRAINT | 允许存在多个 NULL |
| SQL Server | 字段级/表级 | ALTER TABLE ADD CONSTRAINT | 允许存在多个 NULL |
| PostgreSQL | 字段级/表级 | ALTER TABLE ADD CONSTRAINT | 允许存在多个 NULL |
这个“多个 NULL”的问题非常关键,后面单独讲。兼容性方面还有一个细节:MySQL 的 UNIQUE 约束默认对文本类型做整段索引,如果字段是很长的 VARCHAR,建议用前缀索引或哈希字段来替代,否则索引体积和写入效率都会受影响。其他数据库也各有变体,比如 PostgreSQL 可以直接为表达式建唯一索引,Oracle 还支持基于函数的唯一约束。原则是:如果只是常规字段,用标准写法就行;一旦涉及特殊类型或特殊规则,先查对应数据库的手册。
3. UNIQUE 约束和 NULL 值那点剪不断理还乱的关系
3.1 标准 SQL 的立场:NULL 不等于 NULL
这是 UNIQUE 约束里最容易踩坑的地方。按照 SQL 标准,NULL 表示“未知”,两个 NULL 之间并不被认为是相等的。因此,在绝大多数数据库里,UNIQUE 约束并不会阻止多个 NULL 值的存在。
比如这样一个表:
CREATE TABLE employees ( id INT PRIMARY KEY, badge_no VARCHAR(20) UNIQUE );这个 badge_no 是工号,理论上应该唯一。但如果某员工还没分配工号,或者有历史数据没补齐 badge_no,插入两次 NULL、三次 NULL 都是允许的。也就是说,表里可以出现多行 badge_no 为 NULL 的员工记录。
这么设计其实合理,因为“一个人没手机号”和“两个人共享一个手机号”是完全不同的场景。数据库没法表达“NULL 也应该唯一”这种业务规则,标准实现选择了放行。
3.2 MySQL 的行为细节,以及唯一索引和 NULL 的关系
MySQL 与标准一致,在 unique 约束字段上,NULL 可以出现多次。这带来一个非常典型的坑:一个字段如果不设 NOT NULL,哪怕加了 UNIQUE 约束,依然可能出现大量 NULL 行。很多新手以为“设为 UNIQUE 就是所有值不一样”,结果查出来多行 NULL,瞬间懵掉。
从索引原理上解释就是:普通二级索引中,每个索引项对应一行数据,但 NULL 在索引里属于特殊占位符, MySQL 默认不把 NULL 当成可比较的重复项。因此在 InnoDB 引擎下,允许若干个 NULL 存在。
如果业务要求“这个字段非空且唯一”,最佳实践是组合约束:先设置 NOT NULL,再加 UNIQUE。这样强制所有值存在,同时保证值本身不重复。
CREATE TABLE employees ( id INT PRIMARY KEY, badge_no VARCHAR(20) NOT NULL UNIQUE );3.3 PostgreSQL 和 Oracle 的坑,以及如何规避
PostgreSQL 行为跟 MySQL 一致,允许 NULL 重复。Oracle 稍有不同:Oracle 的普通唯一约束同样允许 NULL 重复,但如果使用基于函数的唯一索引,比如 UNIQUE (NVL(badge_no, 'EMPTY')),就可以把“NULL”当成一个具体值来限制重复。
我在另一个项目里在 PostgreSQL 上碰到一个类似需求:优惠券表里的核销记录,一个用户对一张券只能核销一次。业务上用户还没核销时,老库里会存一条 status 为待核销的记录,用户一旦核销就把这条记录的实付金额字段更新为具体值。这时候“实付金额”字段本来应该为 NULL,但业务规则是每个用户每张券最多只能有一条非空实付记录。网上常见的解决思路是用部分唯一索引:
CREATE UNIQUE INDEX idx_coupon_unique_used ON coupon_records (user_id, coupon_id) WHERE actual_amount IS NOT NULL;这种“条件唯一索引”在 PostgreSQL 里特别灵活,相当于把“只有满足条件的行才受唯一性约束”这件事交给数据库去判断。如果项目跑在 MySQL 上,想实现类似效果就得靠生成列或者稍微改造业务表结构,多两个字段来配合实现。
4. 实操阶段:从建约束到踩坑再到排障
4.1 一个完整 Demo:从零开始给订单表加唯一约束
直接上一个我近期整理过的例子。模拟项目 X 是某跨平台系统的订单模块,有一张订单表,表结构设计初并没有考虑到“同一用户同一商户的订单号必须唯一”这个规则。后期在联调时发现,两个用户在同一个商户下居然可以生成相同的商户订单号,于是开始补约束。
原始表结构简化后大致这样:
CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, merchant_id BIGINT NOT NULL, merchant_order_no VARCHAR(64) NOT NULL, amount DECIMAL(10,2), status TINYINT );需求是 user_id、merchant_id、merchant_order_no 三列组合不得重复。先检查存量数据:
SELECT user_id, merchant_id, merchant_order_no, COUNT(*) AS cnt FROM orders GROUP BY user_id, merchant_id, merchant_order_no HAVING COUNT(*) > 1;如果发现有重复,需要先决定保留哪一行。一般策略是保留最早创建或状态最全的一条,然后删除其他重复行。删除之前可以用 ROW_NUMBER() 窗口函数标记出要删的行:
WITH ranked AS ( SELECT id, ROW_NUMBER() OVER ( PARTITION BY user_id, merchant_id, merchant_order_no ORDER BY created_at ) AS rn FROM orders ) DELETE FROM orders WHERE id IN (SELECT id FROM ranked WHERE rn > 1);执行完确认查不到重复数据后,再追加联合唯一约束:
ALTER TABLE orders ADD CONSTRAINT uk_orders_user_merchant_no UNIQUE (user_id, merchant_id, merchant_order_no);这个流程看起来简单,但网上不少人只写了 ALTER TABLE 那一条,忽略了前置查询,结果直接报错。我个人的建议是:凡是给存量表加约束,宁可多花十分钟做数据探查,也别直接执行。
4.2 违反约束到底会发生什么?错误消息和业务层怎么处理
当一条插入或更新语句触发了 UNIQUE 约束冲突时,数据库会返回错误,具体错误信息因数据库而异。MySQL 是 Duplicate entry ‘xxx’ for key ‘uk_users_email’,PostgreSQL 是 duplicate key value violates unique constraint “uk_users_email”,Oracle 是 ORA-00001: unique constraint violated。这类错误本质上是数据完整性保护机制被触发,属于预期中的失败信号。
业务层的处理思路应该是:捕获这个错误,把它识别成“数据重复”,而不是直接当系统异常抛给用户。比如在注册接口里,注册时插入用户记录,如果捕获到唯一约束冲突,应该友好提示“该手机号已注册”。如果用 Spring Boot,可以通过异常类型映射来处理;若用原生 JDBC,则要判断 SQLState 或错误码。
这里顺便提一句我踩过的坑:在一个高并发的秒杀场景里,我在应用层先用 SELECT 判断用户是否已购买,再执行 INSERT。压测时发现存在重复订单,后来换成直接在 INSERT 时依赖唯一约束来拦截,程序收到约束冲突后返回“您已购买过该商品”,问题就解决了。应用层先判断的方式在低并发下没问题,高并发下有竞态窗口,数据库约束才是最靠谱的。
4.3 常见问题与排查技巧实录
给 UNIQUE 约束的日常运维做个速查表,平时排查问题照着看能省不少时间:
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| 加约束报错: Duplicate entry | 存量数据已存在重复值 | 用分组查询找出重复行,去重后再加 |
| 约束未生效,重复插入成功 | 字段含 NULL,NULL 可重复 | 检查是否为 NULL,加 NOT NULL 约束 |
| 联合约束无法满足需求 | 只加了单列约束 | 改写为多列复合唯一约束 |
| 想临时停用约束做数据修复 | 约束影响批量操作 | 先删除约束,修复完重新添加 |
| 历史数据没有唯一键 | 建表时未设计 | 按 4.1 节步骤补约束 |
还有一个很隐蔽的问题:某些数据库的约束删除语法并不统一。MySQL 里删除一个 UNIQUE 约束用的是 DROP INDEX 而非 DROP CONSTRAINT,因为 MySQL 将唯一约束处理成索引。PostgreSQL 和 Oracle 则用 DROP CONSTRAINT。如果使用通用 ORM 往不同库迁移,这个差异会直接让迁移脚本在不同环境上报错。
我自己的习惯是:每建一个约束,就把“约束名+字段+用途”记在设计文档里。后来排查问题时,拿约束名反向检索文档,比自己记忆全库几十个约束靠谱得多。
5. UNIQUE 约束与性能:索引红利与维护成本
5.1 约束为什么自动带索引
UNIQUE 约束的实现依赖唯一索引。数据库在执行插入时,需要快速判断“当前值是否已存在”,最直接的手段就是在该字段或字段组合上建立一个 B-Tree 索引,写入时按索引查找。同理,查询时这个索引也能直接加速。比如对 email 建了 UNIQUE 约束,之后用 email 做条件查询天然命中索引。
所以给某个字段加 UNIQUE 约束,相当于买一送一:拿数据完整性规则的同时,也顺手拿到了高性能索引。但反过来想一想,约束加得越多,索引也越多。每次插入、删除、更新时,数据库都要同步维护这些索引,写入压力会随之增加。
5.2 加约束之前的存量脏数据清洗
这个环节最容易被忽略,也最容易出事故。某次我给一个用户表加手机号唯一约束,表里有 3000 多万条数据。我先执行去重 SQL,删掉了约 2000 条重复记录,然后直接执行 ALTER TABLE,结果生产库锁了近 9 分钟,期间写入全部阻塞。当时还算好,业务低峰期,没造成事故,但也足够吓人。
从那以后,我对大表加约束的操作流程升级成三步:
- 第一步:执行只读的重复检测 SQL,定位重复数据并备份。
- 第二步:将重复数据归档到一张独立的历史表,保留审计痕迹,然后把原表相关行删除。
- 第三步:选择在业务低谷期执行 ALTER TABLE ADD CONSTRAINT,并在执行前观察主从延迟和磁盘 IO。
如果表数据量巨大、无法接受长时间锁表,可以考虑用在线索引创建的方式。MySQL 5.7 后 InnoDB 支持 ONLINE DDL,PostgreSQL 12 后也支持 CONCURRENTLY 创建唯一索引。用在线方式加索引先创建唯一索引,再通过 ALTER TABLE 附加约束关系,可以减少锁表时间,但要注意部分数据库对事务内和事务外的约束行为有别。
5.3 加约束之后的业务逻辑调整建议
一旦表上有了 UNIQUE 约束,业务代码也要同步调整。很多老代码的习惯是“插入前先查询一下”,这种写法在约束存在时不冲突,但属于冗余;更值得推荐的做法是“直接插入,捕获冲突后按业务规则处理”。前者一次操作要两个 SQL 往返,后者只需要一次写入操作,而且后者天然防并发。
不过有一点也要提醒:INSERT 撞约束时的失败本身会消耗一次数据库写操作的资源,如果业务上有大量必撞的插入尝试,让应用层先做一个快速过滤可以减少无效写入。比如注册场景中,明知手机会重复,与其每次让数据库报冲突,不如先查询一次。这属于工程权衡,没有绝对的对错,但凡是带“唯一判断”的地方,最终兜底一定是用数据库约束,而不是查询判断。
还有一点涉及容灾:主从架构下,如果唯一约束只建立在主库,从库读取的数据已经由主库保证,所以没问题;反过来,如果从库同步延迟,应用层立刻做读后写也会存在短暂窗口,唯一约束要两边一致,不能只在某个环境加。我们就有过一次测试环境和生产环境约束不一致的教训,测试库能插入重复数据,生产库却报错,排查了大半天才发现是两类数据库的 DDL 脚本不同步。
6. 一些关于 UNIQUE 约束的扩展玩法
6.1 用 UNIQUE 约束实现幂等表
分布式系统里最常用的幂等方案之一是建立一张幂等表,主键是业务唯一键,比如订单号。处理消息时先插入幂等表,插入成功才继续业务逻辑;插入冲突则说明这个请求之前已经处理过。这样即使消息队列重复投递,也不会影响业务结果。
CREATE TABLE idempotent_records ( biz_id VARCHAR(64) PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );这里的 PRIMARY KEY 就承担了唯一约束的职责。如果想基于多个维度做幂等,可以改成联合主键或联合唯一约束。这种设计比用 Redis 做分布式锁的方案更持久可靠,因为幂等记录是存在数据库里的,不会因为缓存过期而失效。
但要注意幂等表会一直增长,需要定期归档清理。业务上一般只保留最近 30 天或 90 天的记录,所以清理任务要提前准备好。如果因为清理而不小心删掉了正在使用的记录,可能导致重复请求穿透幂等层,所以清理条件要慎重设计,建议只清理创建时间远早于当前时刻的记录,并给清理任务留出足够余量。
6.2 用部分唯一索引实现“软删除后还能重新插入”
软删除场景里常常碰到这种问题:逻辑删除了一条记录后,按道理应该允许重新创建一条相同唯一键的记录,但硬约束直接挡住了。比如用户 ID 是 88,手机号是 13800000000,第一次注册后删除了,再去注册时如果手机号字段上直接设了 UNIQUE,插入就会失败。常见的解决办法是把“唯一性限定在未删除的数据上”。
PostgreSQL 可以用部分唯一索引:
CREATE UNIQUE INDEX uk_users_mobile_active ON users (mobile) WHERE deleted_at IS NULL;这样已软删除的数据不参与唯一性判断。MySQL 没有直接等价的语法,但可以在表中加一个生成列。比如定义一个字段“active_flag”,当 deleted_at 为 NULL 时该列值等于主键 ID,否则等于 NULL,然后对 (mobile, active_flag) 建唯一索引。这种方法有点绕,但确实能实现同样的效果。
Oracle 也有类似方法,使用函数索引并搭配 CASE WHEN 表达式,实现逻辑也大同小异。整体思路可以总结为一句话:让“空值/无效值”绕过唯一检查,让“有效值”接受唯一限制。
6.3 唯一约束失败的监控与告警
约束冲突错误不能完全靠应用日志去发现,建议把唯一约束冲突当成一类可以统计的错误单独上报到监控系统。比如在 Spring Boot 里,自定义一个异常处理器,将约束冲突错误累加到指标计数器中;或者在数据库层面对 ERROR 级别的日志做采集和聚合。
从经验看,唯一冲突往往能提前暴露业务问题:某接口突然出现大量重复插入报错,可能是前端重复提交、消息队列重复消费或者外部系统异常重试。有了这些指标,再结合业务链路日志,能快速定位问题源头。如果没有这类监控,用户投诉“系统卡顿”但后端日志翻半天也找不到有效线索,而且总有擦枪走火的排查成本在里面。
我在实际项目里会把唯一约束冲突的“约束名”也打进监控标签,这样哪张表频繁冲突一目了然。像 uk_users_mobile 频繁触发,大概率是注册流程被恶意刷;uk_orders_merchant_no 频繁触发,可能是上游回调重复投递。这类监控指标对排查分布式问题非常有用。
7. 一些容易被忽略的 UNIQUE 约束细节
7.1 约束命名与元数据查询
约束建立好后,想要在系统里查询它,不同数据库有不同的元数据表。MySQL 可以从 INFORMATION_SCHEMA.TABLE_CONSTRAINTS 查,PostgreSQL 可以查 pg_constraint,Oracle 查 ALL_CONSTRAINTS。实际工作中,定期把数据库里所有唯一约束导出来和业务文档做对照,是一个值得养成的习惯,可以有效防止“文档上写约束了,但生产库里根本没建”的情况发生。
-- MySQL 示例 SELECT table_name, constraint_name, constraint_type FROM information_schema.table_constraints WHERE table_schema = 'your_db' AND constraint_type = 'UNIQUE';PostgreSQL 的查询稍复杂一点,因为约束里关联的字段需要 join 到 pg_attribute 等系统表,但原理相同。这类元数据检查脚本在数据库迁移、架构评审时非常常用。
7.2 约束执行的时机与事务行为
UNIQUE 约束的检查发生在语句执行阶段,而不是事务提交阶段。也就是说,在同一个事务中,即使你后续会更新或删除那一行,只要当前语句触碰了唯一约束冲突,数据库立刻报错。但有一种情况需要留意:如果约束是 DEFERRABLE 的,比如 PostgreSQL 支持可延迟约束,检查可以推迟到事务提交时。这在批量导入场景下可能带来便利,但要注意如果事务提交时的数据仍违反约束,报错依然会发生,且整个事务会被回滚。
MySQL InnoDB 支持唯一约束检查时对索引加锁,在高并发下可能出现锁等待或死锁。尤其并发插入相同值时,多个事务都在等待索引锁,极端情况下会发生死锁并自动回滚其中一个事务。业务层要能处理这种死锁重试。如果经常在唯一键冲突时死锁,八成是业务设计有问题,最好考虑改成“先占用后更新”的模式减少竞争。
7.3 唯一约束外加唯一索引,多此一举吗
有些团队的代码里,既建了 UNIQUE 约束,又手工创建了一个同字段的唯一索引,纯属重复建设。数据库不会因为你两道防线就更安全,只是白白增加写入成本。要记住:UNIQUE 约束的底层就是唯一索引,二者高度重合。除非你要用部分唯一索引、表达式唯一索引这类“约束语法表达不了”的规则,否则不要同时建两个。这个理念我从一次事故里领悟到的:之前有位同学在表上既加了约束又加了唯一索引,结果数据量翻倍后写入变慢,排查半天才发现是指数级膨胀的额外索引占用了大量写入 IO。
8. 收尾之前,分享一个我自己的心得
UNIQUE 约束看起来是入门级别的内容,但真正用好它,需要理解索引原理、 NULL 语义、事务行为和不同数据库的差异。我遇到过不少开发多年的同行,写 CRUD 很熟练,却依然在重复数据上栽跟头,原因往往是不知道数据库自己能做“唯一性保证”。
如果你新做一个系统,建议在建表阶段就把所有业务唯一键列出来,逐个确认该不该加约束。如果你在维护老系统,先从表结构文档开始,查一遍哪些字段存在唯一需求但数据库里没有约束。一次性补全这些约束,比上线后再靠代码救火省心得多。
最后一个实用小技巧:加约束前,先用一条带条件的 SELECT 把可能冲突的数据打印出来,确认全部处理完之后再执行 ALTER。这看起来多了一步操作,实际上能替你挡住很大的麻烦。希望对正在搞数据库的你有点用。