唯一索引直接拒绝重复插入,普通索引允许;唯一索引不支持change buffer,普通索引支持;唯一索引等值查询可能触发const/eq_ref执行计划;建唯一索引前须检查隐性重复及null处理。

唯一索引会直接拒绝重复插入,普通索引不会
这是最根本的分水岭。当你执行 INSERT INTO users (email) VALUES ('a@b.com') 时,如果 email 上建的是 UNIQUE INDEX,而表里已存在该值,MySQL 立即报错:ERROR 1062 (23000): Duplicate entry 'a@b.com' for key 'idx_email';如果是普通索引,语句照常成功,哪怕已有 100 条相同 email 的记录。
常见误判:以为“加了唯一索引就能防脏数据”,其实前提是业务没绕过数据库直写(比如用 LOAD DATA INFILE 或跳过约束的复制通道)。另外,NULL 值在唯一索引中允许多个——这不是 bug,是 SQL 标准行为,因为 NULL != NULL。
普通索引能用 change buffer,唯一索引不能
InnoDB 在写入时若目标数据页不在内存,普通索引可以把更新先缓存在 change buffer 中,等后续读取该页时再合并(merge),大幅减少随机磁盘 I/O;而唯一索引必须先加载数据页到内存,校验唯一性后才能写,无法走 change buffer 路径。
这意味着:在日志表、消息队列表这类写多读少的场景下,给 trace_id 或 event_time 加普通索引,性能通常比唯一索引高 20%–50%。但如果你刚插入就立刻 SELECT ... WHERE trace_id = ?,那 change buffer 反而会触发强制 merge,开销反而更大。
- 确认是否启用:
SHOW VARIABLES LIKE 'innodb_change_buffering';默认是all,但对唯一索引无效 - 监控效果:
SHOW STATUS LIKE 'Innodb_buffer_pool_read_ahead%';和Innodb_change_buffer_*相关指标
等值查询时,唯一索引可能触发 const/eq_ref 执行计划
当优化器确认查询条件命中唯一索引且为等值匹配(如 WHERE user_id = 123),它知道最多返回一行,于是可能跳过某些扫描步骤,在 EXPLAIN 中显示 type: const 或 type: eq_ref;普通索引通常只能到 type: ref,需额外判断是否还有下一条。
不过这个差异在绝大多数情况下可忽略——因为 InnoDB 按 16KB 数据页读取,查完第一条后,同页内下一条的指针查找成本极低。只有当索引字段是超长字符串(如 VARCHAR(2000))且数据页内记录稀疏时,才可能观察到微弱差距。
别为了这点理论优势强行加唯一索引。先看业务是否真需要去重保障:如果应用层已用分布式锁+幂等表保证不重复,数据库层用普通索引更轻量。
ALTER TABLE 加唯一索引前必须检查隐性重复
已有数据的表上执行 ALTER TABLE t ADD UNIQUE INDEX idx_col (col),只要 col 存在重复值,操作直接失败,报错信息是:ERROR 1062 (23000): Duplicate entry '' for key 'idx_col'(注意错误里可能显示空字符串,实际是重复值被截断或隐式转换了)。
安全做法是提前验证:
- 查重复:
SELECT col, COUNT(*) FROM t GROUP BY col HAVING COUNT(*) > 1; - 查 NULL 数量(确认是否真要保留多个 NULL):
SELECT COUNT(*) FROM t WHERE col IS NULL; - 若发现重复,先清理或归档,再建索引
最容易被忽略的一点:联合唯一索引中,全为 NULL 的行仍被视为可重复(因 (NULL, NULL) = (NULL, NULL) 不成立),所以 INSERT INTO t (a,b) VALUES (NULL,NULL), (NULL,NULL) 在 UNIQUE(a,b) 下是合法的——这点和很多人直觉相反,上线前务必实测。











