2. 唯一性约束基础:数据库主键约束、唯一索引、联合唯一约束的原理与实现
聊数据唯一性,绕不开数据库里的几个硬家伙——主键约束、唯一索引、联合唯一约束。说白了,它们就是数据库层面保证数据不重复的「守门员」。我做了这么多年数据治理,见过太多因为约束设计不合理导致的线上事故。今天咱们就把这几个基础概念掰开揉碎,讲清楚它们到底怎么工作,又该怎么用。
2.1 主键约束:数据的「身份证」
主键约束,英文叫 Primary Key。它是最严格的一种唯一性约束。一张表只能有一个主键,而且主键列不能为空。为什么?因为主键是用来唯一标识每一行数据的。
我记得刚入行时,有个同事设计用户表,把「姓名+手机号」设为主键。结果用户改手机号时,数据直接炸了。嗯,这就是典型的主键设计失误。
- 唯一性:主键列的值不能重复
- 非空性:主键列不能为 NULL
- 单表唯一:一张表只能有一个主键
- 物理存储:InnoDB 引擎中,主键就是聚簇索引的根
来看一个简单的建表语句:
CREATE TABLE users (
user_id INT AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100),
PRIMARY KEY (user_id)
);
这里 user_id 是自增主键。为什么用自增?因为 InnoDB 的聚簇索引是按主键顺序组织的,自增主键能避免页分裂,写入性能更好。我在项目中遇到过一张日志表,用了 UUID 做主键,结果插入性能直线下降——磁盘 IO 频繁,索引碎片严重。后来改成自增 ID,问题就解决了。
2.2 唯一索引:更灵活的「唯一性守卫」
唯一索引,Unique Index。它和主键很像,但更灵活。一张表可以有多个唯一索引,而且唯一索引允许 NULL 值存在。不过要注意,不同的数据库对 NULL 的处理方式不一样。
MySQL 里,唯一索引允许插入多个 NULL 值。因为 NULL 代表「未知」,两个未知值被认为是不相等的。但 Oracle 就不一样了,它认为 NULL 也参与唯一性比较。这个坑我踩过——有一次做数据迁移,MySQL 的表迁到 Oracle,结果唯一索引冲突,跑了一晚上的任务全挂了。
-- 创建唯一索引
CREATE UNIQUE INDEX idx_users_email ON users(email);
-- 或者在建表时直接定义
CREATE TABLE users (
user_id INT AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) UNIQUE,
PRIMARY KEY (user_id)
);
2.3 联合唯一约束:多字段的「组合锁」
联合唯一约束,也叫复合唯一约束。它是对多个字段的组合进行唯一性约束。比如订单表里,同一个用户不能对同一个商品重复下单,那就可以用「用户ID + 商品ID」做联合唯一约束。
你想想看,如果只用单字段唯一索引,根本解决不了这种场景。用户ID可以重复,商品ID也可以重复,但它们的组合必须唯一。这就是联合唯一约束的价值所在。
CREATE TABLE orders (
order_id INT AUTO_INCREMENT,
user_id INT NOT NULL,
product_id INT NOT NULL,
order_time DATETIME,
PRIMARY KEY (order_id),
UNIQUE KEY uk_user_product (user_id, product_id)
);
这里有个细节要注意:联合唯一约束中,字段的顺序很重要。MySQL 在构建 B+ 树索引时,会按照字段定义的顺序来组织。比如 UNIQUE KEY (user_id, product_id),索引会先按 user_id 排序,再按 product_id 排序。这意味着查询条件如果只带 product_id,是走不了这个索引的。
- 区分度高的字段放前面:比如用户ID比状态码的区分度高
- 查询频繁的字段放前面:能更好地利用索引
- 字段数量不宜过多:超过 3-4 个字段的联合约束,维护成本会急剧上升
2.4 三种约束的对比与选择
我把这三种约束的核心差异整理成了一张表,方便你对照参考:
| 特性 | 主键约束 | 唯一索引 | 联合唯一约束 |
|---|---|---|---|
| 每表数量 | 只能 1 个 | 可以多个 | 可以多个 |
| 允许 NULL | 不允许 | 允许(MySQL 允许多个 NULL) | 允许(各字段可独立为 NULL) |
| 物理存储 | 聚簇索引 | 二级索引 | 二级索引 |
| 适用场景 | 行唯一标识 | 单字段唯一性 | 多字段组合唯一性 |
| 性能影响 | 写入时维护聚簇索引 | 写入时额外维护索引 | 写入时维护复合索引 |
怎么选?我个人习惯是:
- 每张表必须有一个主键,这是底线
- 业务唯一标识用唯一索引,别往主键上堆
- 多字段组合唯一性,用联合唯一约束
2.5 核心逻辑:一张图看懂
下面这张图展示了三种约束在数据库中的关系与层级:
2.6 避坑指南:我踩过的那些坑
做数据治理这些年,我在唯一性约束上栽过不少跟头。分享几个典型的:
- 主键用业务字段:我曾经把身份证号设为主键,结果业务调整后,身份证号允许修改了——改主键?那简直是噩梦。从那以后,我再也不用业务字段做主键。
- 唯一索引字段过长:有次给一个文本字段建唯一索引,字段长度 2000 字符。MySQL 对索引长度有限制,InnoDB 单列索引最长 767 字节。后来只能改成前缀索引,但唯一性又打了折扣。嗯,这是个两难的选择。
- 联合约束字段顺序搞反:查询条件经常只查商品ID,但联合约束把用户ID放前面了。结果每次查询都全表扫描,性能惨不忍睹。后来调整了字段顺序,查询效率翻了好几倍。
SHOW INDEX FROM table_name 查看索引信息。我每次上线前都会跑一遍这个命令,确认索引字段、顺序、类型都符合预期。别嫌麻烦,这一步能省掉很多线上故障。
好了,唯一性约束的基础就聊到这儿。记住一句话:约束设计得越合理,后面的数据治理就越省心。别等到数据冲突了才想起来补约束,那时候代价就大了。