UUID主键必然引发页分裂,因InnoDB聚簇索引按主键字节序物理排序,v4 UUID为128位完全随机二进制,插入99%以上落在B+树中间页,页满即分裂;UUID_TO_BIN(uuid, 1)可将时间戳高位左移实现近似递增,压降90%分裂但无法根治,ULID或Snowflake才是更优替代方案。

为什么UUID主键必然引发页分裂
不是“会不会”,而是“裂多少”——InnoDB聚簇索引按主键字节序物理排序,UUID()生成的v4 UUID是128位完全随机二进制,插入时99%以上概率落在B+树中间页而非末尾。一旦目标页(默认16KB)已满,InnoDB必须执行页分裂:复制约一半键值到新页、更新父节点指针、刷盘。实测百万行表中,Data_free持续升高,SHOW ENGINE INNODB STATUS频繁出现Pages split due to insert。
UUID_TO_BIN(uuid, 1) 是唯一有效的MySQL内置补救方案
仅把CHAR(36)改成BINARY(16)毫无意义——存储省了,但字节序仍是随机的。真正起作用的是UUID_TO_BIN(uuid, 1)(MySQL 8.0.31+),它把UUID里的时间戳高位左移到前6字节,让生成的BINARY(16)具备时间局部性。
- 建表必须用
BINARY(16),不能用CHAR(36)或VARCHAR(36) - 所有INSERT必须走
UUID_TO_BIN(UUID(), 1),应用层无需改ID生成逻辑,但入口要统一 - 查询需用
BIN_TO_UUID(id, 1)转回可读格式;若只做等值查询,直接用二进制比较更快 - 已有数据是
UUID_TO_BIN(uuid, 0)存的,混用会导致排序错乱,必须全量转换
比补丁更可靠:换ULID或Snowflake
靠UUID_TO_BIN(uuid, 1)能压降90%页分裂,但无法根治——同一毫秒内高并发生成的ULID/Snowflake后缀仍可能挤爆单页。它们才是为InnoDB设计的替代方案:
-
ULID:48位毫秒时间戳 + 80位随机数,字符串形式(如01ARZ3NDEKTSV4RRFFQZFSM5BG)天然字典序递增,存为CHAR(26)即可 -
Snowflake:64位整数,含时间戳+机器ID+序列号,必须用BIGINT UNSIGNED存储,禁止前端直传 - 若业务强依赖对外暴露UUID格式,可用
AUTO_INCREMENT主键 +uuid CHAR(36) UNIQUE组合,API返回uuid字段
配套InnoDB参数与写入节奏调整
光换ID类型不够,页分裂是聚簇索引结构、缓冲池管理、写入模式共同作用的结果:
- 设
innodb_fill_factor = 70:预留30%页空间减分裂频次,代价是磁盘占用上升 - 批量插入必须用
INSERT INTO t VALUES (), (), (),禁用innodb_autoinc_lock_mode = 0,改用= 2(交错模式)提升并发吞吐 - 绝对避免高频单行INSERT——尤其在UUID主键表上,这是最伤Buffer Pool的操作
真正容易被忽略的点:即使用了UUID_TO_BIN(uuid, 1),ORDER BY id依然大概率退化为全表扫描,因为物理存储无法做到自增ID那样的连续性。页分裂少了,但范围查询性能没本质改善。


















