uuid插入导致b+树频繁页分裂,因其随机性使新记录常插入页中间,触发innodb页分裂、增加随机i/o和索引碎片;即便转为binary(16)或mysql 8.0+的uuid_to_bin(uuid,1),若未重排字节序仍难改善。

UUID插入导致B+树频繁页分裂
MySQL InnoDB用主键构建聚簇索引,数据行直接存放在B+树叶子节点里。页大小固定为16KB,当一页快满时,新记录必须“找位置”插入——自增ID总追加到末尾,而UUID是完全随机的字符串,每次插入都可能落在任意页的中间位置。
结果就是:原本填满80%的页,因一个UUID插进来触发Page Split(页分裂),InnoDB被迫把原页约一半数据挪到新页,再调整父节点指针。这个过程不仅耗CPU,还产生大量随机磁盘I/O。
- 实测100万行后,每插入1条
UUID主键记录,平均引发0.3次页分裂;自增ID则基本为0 - 页分裂后,原页利用率常跌至50%以下,形成索引碎片,
SHOW TABLE STATUS里Data_free值会明显升高 - 分裂不是孤立事件:父节点也可能因子节点增多而分裂,形成级联效应
为什么字符串UUID比BINARY(16)更糟
很多人以为把UUID存成BINARY(16)就能解决性能问题,其实只是缓解了存储开销,没动根本——无序性还在。
UUID作为字符串(如'550e8400-e29b-41d4-a716-446655440000')存入CHAR(36)或VARCHAR(36),比较逻辑是字典序,B+树内部节点排序、范围扫描全受拖累;换成BINARY(16)后,虽然比较变快了,但二进制值仍是时间无关的随机分布,页分裂照旧。
-
CHAR(36)占36字节,BINARY(16)占16字节,但两者在B+树中插入位置的随机性一致 - 二级索引叶子节点存的是主键值,
BINARY(16)让二级索引体积缩小约55%,但回表时仍要按随机地址读取数据页 - 真正起作用的是顺序性,不是字节长度——所以
UUID_TO_BIN()本身不解决问题,必须配合ORDER BY重排(MySQL 8.0+支持)
有序UUID能彻底避免页分裂吗
MySQL 8.0引入UUID_TO_BIN(uuid, true),把时间戳高位移到前缀,生成“时间有序”的二进制UUID。这确实大幅减少页分裂,但不是零分裂。
因为UUIDv1/v6的时间精度是100纳秒,同一毫秒内生成多个UUID时,后缀(clock sequence + node ID)仍是随机的。如果单机高并发写入(比如Web服务器每毫秒生成上百ID),这些“同毫秒ID”依然会集中插入到同一数据页,撑满后照样分裂。
- 测试显示:有序UUID在10万TPS写入下,页分裂率比普通UUID低90%,但仍比自增ID高3~5倍
- 它对范围查询有帮助(比如
WHERE id > BIN_TO_UUID(...)可走索引),但无法像自增ID那样保证物理连续 - 依赖MySQL 8.0+,低版本无法用;且应用层需统一调用
UUID_TO_BIN(),否则混用会导致索引乱序
自增ID的“顺序性”本质是什么
关键不在“自增”,而在“单调递增+无间隙预期”。InnoDB只要确认下一个id一定大于所有现存值,就能把新行无条件追加到B+树最右叶子节点末尾。
这个行为带来三个硬性优势:页填充率高(常达15/16)、无需查找插入点(省掉树遍历)、磁盘写入是顺序IO(OS预读生效)。而雪花ID虽是LONG型且含时间戳,但工作节点ID和序列号组合后,跨机器写入时ID会跳跃(比如节点A刚写完123456789012345678,节点B下一条可能是123456789098765432),这种跳跃仍会引发少量页分裂。
- 真正安全的“顺序”只存在于单点生成场景;分布式ID必须接受轻微分裂代价
- 如果你的业务允许主键暴露增长趋势(比如订单号被用户看到),自增ID仍是首选
- 若必须全局唯一,优先考虑带时间前缀的雪花ID,而非UUID——至少它不拖慢字符串比较
innodb_buffer_pool_size和innodb_log_file_size来得实在。











