唯一索引和主键索引的区别?
结论:主键索引是「唯一 + 非空 + 聚簇 + 全表仅一个」的索引,它决定了数据在磁盘上的物理顺序;唯一索引是二级索引上的唯一约束,可以建多个、允许 NULL(多个 NULL 不算重复)。一句话:主键是表的身份,唯一索引是业务规则。
一、结构上的位置不同
┌─────────────────────────────┐
聚簇索引 │ 主键 B+Tree │
(只能一个) │ 叶子 = 主键值 + 完整行数据 │
└─────────────────────────────┘
▲
┌──────────┴──────────────────┐
二级索引 │ 唯一索引 uk_phone (phone) │
(可以多个) │ 叶子 = phone + 主键值 │
└─────────────────────────────┘
查 WHERE phone = ? 走唯一索引拿到主键,再回表;查 WHERE id = ? 一次 B+Tree 就结束。这是两者在性能上最本质的差别。
二、逐项对照
| 维度 | 主键索引 | 唯一索引 |
|---|---|---|
| 数量 | 每表最多一个 | 可以有多个 |
| NULL | 不允许 | 允许,且多行 NULL 不冲突 |
| 索引类型 | 聚簇索引 | 二级索引(需回表) |
| 默认创建 | 有主键自动创建;无主键则用首个非空唯一索引 | 需显式 UNIQUE KEY |
| 外键引用 | 可被引用 | 也可被引用(需唯一) |
| 值可否修改 | 可以,但代价极高 | 可以,只影响本索引 |
| 冲突错误 | 1062 | 1062 |
| 典型用途 | 行标识、聚簇组织 | 手机号、邮箱、订单号去重 |
三、NULL 语义:唯一索引的常见陷阱
CREATE TABLE t_user (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
phone VARCHAR(20) DEFAULT NULL,
email VARCHAR(64) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_phone (phone)
) ENGINE=InnoDB;
-- 两行都能插入成功:NULL 不参与唯一性比较
INSERT INTO t_user (phone) VALUES (NULL), (NULL);
-- 这一行会失败:非 NULL 值重复
INSERT INTO t_user (phone) VALUES ('13800000000');
INSERT INTO t_user (phone) VALUES ('13800000000');
-- ERROR 1062 (23000): Duplicate entry '13800000000' for key 't_user.uk_phone'
业务上如果要求「手机号必须唯一且必填」,就该把它定义成 NOT NULL,否则唯一约束会被 NULL 绕开。
四、查询性能与写入代价
- 等值查询:主键命中即返回整行;唯一索引命中后还要一次回表。若查询列全在索引内,唯一索引也能做覆盖索引。
- 范围查询:主键范围扫描是聚簇索引顺序读,页内连续;唯一索引范围扫描先扫索引再逐行回表,随机 I/O 明显更多。
- 写入:每多一个唯一索引,插入就要多维护一棵 B+Tree,并做一次唯一性检查。唯一索引越多,写入越慢、死锁概率越高。
- 并发插入:唯一索引上的并发插入在 RR 隔离级别下依赖间隙锁保证唯一性,重复键冲突时会加 next-key lock,热点键容易形成锁等待。
-- 查看表上所有索引及唯一性
SHOW INDEX FROM t_user;
-- 或
SELECT index_name, non_unique, column_name, seq_in_index
FROM information_schema.statistics
WHERE table_schema = 'demo' AND table_name = 't_user';
五、主键选型的三条经验
- 用与业务无关的自增或雪花 ID:业务键(手机号、身份证)一旦变更,聚簇索引要整表重排,所有二级索引存的主键值也要跟着改。
- 主键尽量短:每个二级索引叶子都冗余主键值,
BIGINT比CHAR(32)的 UUID 省下的空间会乘以索引数量。 - 自增主键让插入集中在 B+Tree 右端,页分裂少;随机主键会在中间反复分裂,碎片率高。
常见追问
- 没有主键的表会怎样? InnoDB 会选第一个非空唯一索引做聚簇索引,都没有就生成隐藏的 6 字节
ROW_ID,此时无法用主键定位行。 - 唯一索引能当主键用吗? 可以
ALTER TABLE t ADD PRIMARY KEY (col),前提是该列非空且当前无重复;它会把表重建为以该列聚簇。 - 主键和唯一索引哪个查询更快? 单行等值、需要整行时主键更快;只要索引列时两者都能覆盖,差距很小。
INSERT IGNORE和ON DUPLICATE KEY UPDATE的区别? 前者静默跳过冲突行,后者转为更新;两者都依赖唯一性约束识别冲突,且都会消耗自增值。- 唯一索引能保证「先查后插」的并发安全吗? 能,靠数据库的唯一约束兜底;应用层「先 SELECT 再 INSERT」在并发下必然漏判,必须依赖约束或分布式锁。
小结
主键索引决定物理存储,唯一索引只增加一条业务规则。选主键看「稳定、短、单调」,加唯一索引看「是否真的需要数据库兜底去重」,因为每一个唯一索引都在为写入路径增加成本。