uuid主键触发频繁页分裂,因innodb聚簇索引按主键物理排序,而uuid()生成的随机128位字符串插入时大概率落在b+树中间页,页满即分裂,导致i/o放大、buffer pool污染、页利用率下降;改用uuid_to_bin(uuid(), 1)可将时间戳高位前置实现近似递增。

为什么UUID主键会触发频繁页分裂
因为InnoDB的聚簇索引按主键值物理排序存储数据,而UUID()生成的是随机128位字符串(如'550e8400-e29b-41d4-a716-446655440000'),每次插入都得在B+树中二分查找插入位置——大概率落在已有页中间,而非末尾。一旦目标页已满,就必须分裂:原页一半数据搬出、新建页、更新父节点指针。这个过程不是“慢一点”,而是直接放大I/O、污染Buffer Pool、降低页利用率。
常见现象包括:SHOW TABLE STATUS里Data_free持续增长;innodb_buffer_pool_reads飙升;写入吞吐随时间明显下滑。
- 自增ID插入:总追加到B+树叶节点末尾,页分裂极少
- UUID插入:实测每插入1000行,平均触发1.7次页分裂
- 页内碎片化后,16KB页原本可存约100条自增记录,UUID可能只剩30–40条
CHAR(36) vs BINARY(16):存储格式没解决根本问题
把id CHAR(36)改成id BINARY(16)能省20字节,但无法改变“随机性”这个核心缺陷。B+树比较的是字节序列,而标准UUID的16字节原始顺序仍是时间戳+随机段混排,高位不单调,插入位置依然不可预测。
例如两个UUID:1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d和6d5c5a5e-5c5a-4e5a-8d5a-5a5e5c5a5e5c,首字节0x1a vs 0x6d就决定排序位置,后续插入完全无局部性。
-
UUID_TO_BIN(UUID())(无swap_flag)仍是随机序,仅节省空间 - 必须用
UUID_TO_BIN(UUID(), 1)才把时间戳高位前置,实现近似递增 - MySQL swap_flag=1,硬套该写法无效
ORDER BY id变全表扫描?是的,而且很真实
当主键是UUID时,SELECT * FROM t ORDER BY id LIMIT 10几乎必然退化为全表扫描。原因不是SQL写错了,而是物理存储乱序:InnoDB必须遍历所有叶子页才能凑够10条“逻辑上最小”的记录,无法利用B+树的有序链表特性做跳跃式遍历。
对比自增ID:同一查询只需访问1–2个连续页,磁盘I/O次数差一个数量级。
- 非聚簇索引的叶子节点存的是主键值,UUID越长,单页能存的索引项越少,B+树层级越高
- 8字节
BIGINT主键下,二级索引页通常能存300+条引用;36字节CHAR下可能只剩80条左右 - 树高每+1,范围查询或多条件JOIN的I/O成本就翻倍
真要分布式唯一ID?别硬扛UUID主键
如果业务确实需要全局唯一且不依赖中心发号器,优先考虑更适配InnoDB的方案,而不是强行用UUID顶主键。
-
UUID_SHORT():8字节整型、天然有序、单机安全,适合管理后台/日志表;注意服务器重启会重置计数器 - 时间前缀+自增序列:
CONCAT(DATE_FORMAT(NOW(), '%y%m%d%H%i%s'), LPAD(@cnt:=@cnt+1, 4, '0')),需配合应用层并发控制 - MySQL 8.0+ 推荐建表语句:
id BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID(), 1)) - 已有UUID表想止损?
OPTIMIZE TABLE可临时清理碎片,但锁表严重;ALTER TABLE t ENGINE=InnoDB ALGORITHM=INPLACE只整理页内空洞,不重排主键顺序
最常被忽略的一点:页分裂不是一次性损耗,而是随写入持续恶化。上线半年后性能掉30%,往往就始于第一行INSERT INTO t (id) VALUES (UUID())。











