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,问题就解决了。

我的建议:业务主键和数据库主键尽量分离。用自增 ID 或雪花 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)
);
注意:唯一索引和普通索引的性能差异不大,但唯一索引在插入时需要额外检查唯一性,写入性能会略低于普通索引。高并发写入场景下,这个差异会被放大。我曾经优化过一个秒杀系统,把唯一索引改成了普通索引+应用层去重,写入吞吐量提升了 30%。

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 核心逻辑:一张图看懂

下面这张图展示了三种约束在数据库中的关系与层级:

唯一性约束体系结构 唯一性约束 主键约束 唯一索引 联合唯一约束 特性 • 每表仅1个 • 不允许 NULL • 聚簇索引 特性 • 可多个 • 允许 NULL • 二级索引 特性 • 多字段组合 • 字段顺序重要 • 复合索引 设计建议 1. 每张表必须有主键,推荐自增 ID 2. 业务唯一标识用唯一索引,与主键分离 3. 联合约束字段不超过 4 个,高区分度字段放前面

2.6 避坑指南:我踩过的那些坑

做数据治理这些年,我在唯一性约束上栽过不少跟头。分享几个典型的:

  • 主键用业务字段:我曾经把身份证号设为主键,结果业务调整后,身份证号允许修改了——改主键?那简直是噩梦。从那以后,我再也不用业务字段做主键。
  • 唯一索引字段过长:有次给一个文本字段建唯一索引,字段长度 2000 字符。MySQL 对索引长度有限制,InnoDB 单列索引最长 767 字节。后来只能改成前缀索引,但唯一性又打了折扣。嗯,这是个两难的选择。
  • 联合约束字段顺序搞反:查询条件经常只查商品ID,但联合约束把用户ID放前面了。结果每次查询都全表扫描,性能惨不忍睹。后来调整了字段顺序,查询效率翻了好几倍。
一个小技巧:在 MySQL 中,可以用 SHOW INDEX FROM table_name 查看索引信息。我每次上线前都会跑一遍这个命令,确认索引字段、顺序、类型都符合预期。别嫌麻烦,这一步能省掉很多线上故障。

好了,唯一性约束的基础就聊到这儿。记住一句话:约束设计得越合理,后面的数据治理就越省心。别等到数据冲突了才想起来补约束,那时候代价就大了。