uuid()作主键必然导致b+树页分裂,因字典序随机插入引发频繁页分裂、填充率低、buffer pool压力大;改binary(16)仅省空间不改随机性;uuid_to_bin(uuid,1)仅对v1/v6有效且非根治;最优解是自增主键+独立uuid字段。

MySQL 的 UUID() 函数生成的值是随机的,直接用作主键时,B+树索引必然碎片化——这不是配置或版本问题,而是插入位置完全不可预测导致的结构性恶化。
为什么UUID()插入会强制页分裂
InnoDB 聚簇索引的数据就存于 B+ 树叶子节点,页大小固定为 16KB。自增 ID 总追加到最右页末尾;而 UUID() 返回的字符串(如 '550e8400-e29b-41d4-a716-446655440000')在字典序上纯随机,每次 INSERT 都得二分查找插入点,99% 概率落在已有页中间。
- 目标页已满 → 立即触发 page split:复制约一半记录到新页,更新父节点指针,写磁盘
- 实测每插入 1000 行,平均引发 1.7 次分裂;分裂后两页填充率常低于 50%
-
Data_free持续升高、innodb_buffer_pool_reads明显高于innodb_buffer_pool_read_requests是典型信号 - 分裂不是孤立事件:父节点可能因子节点增多再次分裂,形成级联
CHAR(36) 改成 BINARY(16) 并不能解决问题
很多人把 id CHAR(36) 改成 id BINARY(16) 就以为优化完成了,其实只是省了 20 字节存储空间,B+ 树插入逻辑依然随机。
-
UUID()原生二进制仍是时间戳 + 随机段混排,高位不单调,首字节差异(如0x1avs0x6d)就能让两个 UUID 在树中相隔几十层 - 二级索引叶子节点存的是完整主键值,
BINARY(16)让每个索引项比BIGINT多占 8 字节,进一步挤占 Buffer Pool -
ORDER BY id查询大概率退化为全表扫描——物理存储乱序,B+ 树无法跳跃遍历
MySQL 8.0+ 的 UUID_TO_BIN(uuid, 1) 是必要但不充分的补救
UUID_TO_BIN(uuid, 1)(第二个参数为 1)会把时间戳高位左移到字节序最前,生成“近似递增”的二进制 UUID,能大幅降低分裂频率(实测比普通 UUID 低 90%),但不是根治方案。
- 它只对 UUID v1/v6 有效;v4 不含时间戳,
swap_flag=1无意义 - 同一毫秒内生成多个 ID 时,后缀(clock sequence + node ID)仍是随机的,高并发下仍会集中插入同一数据页,撑满后照样分裂
- 建表必须用
BINARY(16),插入必须统一走UUID_TO_BIN('xxx-xxx', 1);若旧数据是用UUID_TO_BIN(uuid, 0)存的,混用会导致排序错乱 - 即便如此,10 万 TPS 下页分裂率仍比自增 ID 高 3~5 倍
真正该关注的,是插入局部性是否被破坏
所有优化手段——包括 innodb_fill_factor 调整、批量插入、OPTIMIZE TABLE——都只是缓解表象。只要主键不具备时间趋势,页分裂和随机 I/O 就是持续发生的底层事实。业务上若强依赖 UUID 语义,更稳妥的做法是用自增主键 + 单独 uuid 字段,而非在聚簇索引上硬扛随机性。











