页分裂是B+树对无序插入的必然反应,因UUID等随机主键导致插入点分散、页满即分裂,虽UUID_TO_BIN(uuid, 1)可降90%分裂频率,但同毫秒ID仍扎堆引发分裂,根本解法是采用自增ID或ULID/Snowflake等有序ID。

页分裂不是异常,而是B+树对无序插入的必然反应
MySQL InnoDB 的聚簇索引就是数据本身,B+树叶子节点按主键值物理排序存放行记录。自增ID总追加到最右页末尾,而UUID()、UUID_TO_BIN(uuid, 0) 或乱序字符串(如 'ORD20261104001')生成的值,在字节序或字典序上完全不可预测——每次 INSERT 都得二分查找插入点,99%以上概率落在已有页中间而非末尾。
一旦目标页已满(默认16KB),InnoDB立刻触发页分裂:复制约一半键值对到新页、更新父节点指针、写入 doublewrite buffer。这不是“偶尔慢一点”,而是确定性开销:
-
SHOW ENGINE INNODB STATUS中频繁出现"Pages split due to insert" -
innodb_page_splits监控指标持续飙升 -
SHOW TABLE STATUS显示Data_free值不断增长且远高于预期(比如超 30%)
CHAR(36) 改成 BINARY(16) 并不能解决根本问题
很多人把 CHAR(36) 主键改成 BINARY(16) 就以为优化完成了,其实只是省了存储空间——v4 UUID 的原始二进制仍是时间戳+随机段混排,高位不单调,插入位置依然不可预测。
例如两个 UUID:'1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d' 和 '6d5c5a5e-5c5a-4e5a-8d5a-5a5e5c5a5e5c',首字节 0x1a vs 0x6d 就决定它们在 B+ 树里可能相隔几十层。后续插入毫无局部性,Buffer Pool 预读失效,innodb_buffer_pool_reads 明显升高。
更关键的是:二级索引叶子节点存的是完整主键值。BINARY(16) 让每个索引条目比 BIGINT 多占 8 字节,进一步挤占 Buffer Pool。
UUID_TO_BIN(uuid, 1) 能缓解但不能根治
MySQL 8.0.31+ 支持 UUID_TO_BIN(uuid, 1),第二个参数为 1 时会把时间戳高位左移到字节序最前,生成“近似递增”的二进制 UUID。实测比普通 UUID 分裂频率低 90%,但它仍不是零分裂。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
原因在于:UUIDv1/v6 时间精度是 100 纳秒,同一毫秒内生成多个 ID 时,后缀(clock sequence + node ID)仍是随机的。单机高并发写入(比如 Web 服务每毫秒生成上百 ID),这些“同毫秒 ID”会集中插入同一数据页,撑满后照样分裂。
使用时必须注意两点:
- 必须显式调用
UUID_TO_BIN(uuid, 1),它不是默认行为 - 字段类型必须是
BINARY(16),若存回CHAR(36),转换白做
真正有效的解法是从源头控制插入顺序
页分裂是聚簇索引 + 写入节奏 + 缓冲池管理共同作用的结果。任何“换主键类型”的方案都只能缓解,不能根治。最直接有效的方式是让 INSERT 始终追加到最右页:
- 优先用
BIGINT UNSIGNED AUTO_INCREMENT,哪怕只作隐藏聚集键,业务主键设为唯一非聚集索引 - 大表改造需分步:
ALTER TABLE t ADD COLUMN new_id BIGINT UNSIGNED AUTO_INCREMENT FIRST,再ADD PRIMARY KEY (new_id),不能直接MODIFY id - 若必须保留 UUID 语义,应用层应统一生成 ULID 或 Snowflake,并强制校验时钟偏移,避免 worker_id 冲突和回拨重复
真正容易被忽略的是:即使你用了 UUID_TO_BIN(uuid, 1),也没法绕过 InnoDB 对“同一毫秒内多写”的物理限制——那批 ID 在字节序上仍会扎堆,只要写入节奏快于页填充速度,分裂就躲不掉。










