UUID主键总在中间页分裂,因InnoDB聚簇索引按主键字节序物理排序,v4 UUID是完全随机的16字节序列,插入时99%以上落在已有页中间,页满即触发分裂;UUID_TO_BIN(uuid, 1)将时间戳高位左移,使分裂频率降低90%,但无法根治,ULID或Snowflake才是更优替代方案。

为什么UUID主键总在中间页分裂
因为InnoDB的聚簇索引按主键字节序物理排序,而UUID()生成的v4 UUID是完全随机的16字节序列。每次插入都要二分查找位置,99%以上落在已有页中间——页一满就触发Pages split due to insert,复制一半数据、更新父节点、写磁盘。这不是“偶尔慢”,而是每千行插入平均引发1.7次分裂(实测),分裂后两页填充率常低于50%,Data_free持续上涨。
用UUID_TO_BIN(uuid, 1)重排时间戳高位
MySQL 8.0.31+支持第二个参数为1的UUID_TO_BIN(),它会把UUIDv1/v4中隐含的时间戳高位左移到字节序最前,让二进制值具备近似单调性。这比默认的UUID_TO_BIN(uuid, 0)(仅去连字符)效果强得多,实测分裂频率降低90%。
- 建表必须用
BINARY(16),不能用CHAR(36)或UUID_TO_BIN(uuid, 0)存的数据混用,否则排序错乱 - 插入统一走
UUID_TO_BIN('xxx-xxx', 1),应用层无需改逻辑,但所有入口(包括批量导入、迁移脚本)都得强制调用 - 查询时用
BIN_TO_UUID(id, 1)转回可读格式,注意第二个参数必须一致 - 已存在的旧数据若用
UUID_TO_BIN(uuid, 0)存的,不能直接ALTER COLUMN,需导出→转换→重导入
别只盯着主键,二级索引也在分裂
就算主键用了UUID_TO_BIN(uuid, 1),只要二级索引字段(比如status、created_at)本身无序,它的B+树照样频繁分裂。尤其危险的是UPDATE t SET name = 'new' WHERE id = 123——它要同时修改聚簇索引行和name二级索引页,两棵树都可能分裂。
- 把高频更新的字段从联合索引里挪出来,比如把
(user_id, status, created_at)改成(user_id, created_at),状态变更改用覆盖查询+应用层过滤 - 对写多读少的二级索引,考虑设
innodb_fill_factor = 70预留空间,但别设太高(如99),否则“刚写满就分裂”反而更糟 - 监控
Innodb_page_splits和Handler_read_rnd_next,后者飙升说明碎片已影响查询局部性
真正治本:换ID类型,不是打补丁
靠UUID_TO_BIN(uuid, 1)只能压低分裂频率,无法根除——同一毫秒内生成的多个ID后缀仍是随机的,高并发下仍会集中撑满单页。ULID和Snowflake才是适配InnoDB的天然选择。
- ULID:48位毫秒时间戳 + 80位随机数,字符串形式字典序可排序,存为
CHAR(26)或BINARY(16),兼容UUID语义 - Snowflake:64位整数,必须用
BIGINT UNSIGNED存储,禁止前端直传,须由服务端或DB代理层生成,避免时钟回拨导致重复 - 若业务强依赖对外暴露UUID,就用自增主键+独立
uuid CHAR(36) UNIQUE字段组合,主键走AUTO_INCREMENT,uuid仅用于API返回
页分裂从来不是单点问题,它是主键顺序性、二级索引设计、innodb_fill_factor配置、写入节奏四者耦合的结果。改ID类型只是第一步,漏掉任意一环,分裂照旧发生。


















