唯一索引强制绕过Change Buffer,每次插入都触发磁盘随机读;普通索引可将变更暂存于Change Buffer,避免立即读盘,显著提升写性能。

唯一索引强制绕过 Change Buffer,每次插入都触发磁盘随机读
普通索引写入时,若目标索引页不在 buffer_pool 中,InnoDB 会把变更暂存到 change_buffer,等页后续被读入或后台线程空闲时再合并——零次磁盘随机读。而唯一索引必须校验唯一性,就得先把对应索引页从磁盘读进来,哪怕只插一条记录、哪怕该页之后永远不被访问,这次 innodb_buffer_pool_reads 也逃不掉。
常见错误现象:innodb_buffer_pool_reads 在批量导入时陡增;SSD 上延迟不明显,但高并发离散写(如 UUID 主键)下 IOPS 仍会打满;SHOW ENGINE INNODB STATUS 里频繁看到 waiting for table metadata lock 或 read view 等待。
-
innodb_change_buffering对唯一索引完全无效,设成inserts或all都没区别 - 机械硬盘(HDD)场景下,单次插入延迟可能从 0.1ms 跳到 8ms+
- 如果业务已用分布式锁 + 幂等表确保不重复,数据库层加
UNIQUE INDEX就是纯 IO 浪费
唯一索引插入要走“查 + 写”两步,普通索引是“盲写”
普通索引插入:直接修改聚簇索引,再把二级索引变更记进 change_buffer(或直接改内存页),不查重、不判断、不回滚。
唯一索引插入:先在 B+ 树里搜索目标值是否存在 → 若存在,报 Duplicate entry 'xxx' for key 'uk_xxx';若不存在,才继续写入聚簇索引和唯一索引树。这多出的一次完整 B+ 树查找,CPU 和逻辑读都翻倍。
- 即使值确定不存在(比如新生成的 UUID),引擎仍要走完整搜索路径,锁定
[prev_key, next_key)区间 - 高并发下多个事务搜索相邻间隙,极易触发
*** DEADLOCK FOUND - 监控重点不是慢日志,而是
Handler_read_rnd_next和Innodb_buffer_pool_wait_free
唯一索引越多,插入时锁竞争和页分裂越失控
每多一个唯一索引,INSERT 就得多维护一棵 B+ 树:各自加锁、各自分裂、各自刷脏页。更麻烦的是,这些操作不是串行完成的——InnoDB 为每个索引单独加锁,多个索引更新之间可能形成锁依赖链。
实测数据:某订单流水表从 1 个唯一索引增加到 4 个后,批量导入吞吐量下降 62%,innodb_data_writes 上升 3.8 倍,Buffer pool hit rate 从 99.2% 掉到 87.1%。
- 联合唯一索引(如
UNIQUE KEY uk_order (user_id, order_no))比单列更重,因为要同时定位两个维度的叶子节点 -
VARCHAR(255)字段建唯一索引,实际值长会导致索引页膨胀快、分裂更频繁 - 低区分度字段(如
status)放在联合唯一索引最前,会加深树高、拉长搜索路径
NULL 值让唯一索引的“唯一性”失效,但校验开销照旧
MySQL 规定 UNIQUE 索引允许任意多个 NULL,因为 NULL != NULL。结果就是:你加了 UNIQUE INDEX uk_email (email),却能成功插入 100 条 email IS NULL 的记录——业务以为字段必填且唯一,实际约束形同虚设,而每次插入仍要走完整查重流程。
- 检查方式:
SHOW CREATE TABLE t,确认约束是否含UNIQUE NOT NULL - 如果字段本就允许为空,又不需要强唯一语义,优先用普通索引 + 应用层兜底
- 已有普通索引想升级为唯一索引,务必先跑:
SELECT COUNT(*) - COUNT(DISTINCT IFNULL(col, '')) FROM t,避免ALTER TABLE因隐性重复失败
真正卡住高并发写入的,往往不是 SQL 写得差,而是那一堆看似“安全”的 UNIQUE INDEX —— 它们不声不响地把每次插入变成一次磁盘读 + 一次 B+ 树搜索 + 一次间隙锁 + 一次跨索引协调。删掉非必要唯一索引,比调优查询语句对吞吐量的提升更直接。



















