根本原因是v4 uuid完全随机,与innodb聚簇索引要求主键有序插入的机制严重错配;其字节序无时间局部性,导致新记录总插入b+树中间页,频繁触发页分裂、填充率低(常<65%)、索引膨胀、字符串比较慢。

UUID() 生成的 ID 在 InnoDB 聚簇索引下效率低,根本原因不是 UUID 本身“不好”,而是它和 InnoDB 的物理存储机制发生了严重错配:v4 UUID 完全随机,而聚簇索引要求主键有序插入。
为什么UUID()默认生成的是 v4 随机值?
UUID() 函数在 MySQL 中默认生成 v1 UUID(含时间戳),但实际行为取决于服务器配置和版本;多数生产环境(尤其使用 ORM 或应用层调用)最终落地的是 v4 —— 即 128 位完全随机数。它不带时间信息、无序、不可预测。
- v1 虽含时间戳,但 MAC 地址部分引入随机扰动,且跨服务器时易冲突,实际很少被信任用于主键
- v4 是安全首选,但代价是彻底失去局部性:两个相邻插入的
UUID()值在字节序上可能天差地别 - MySQL 不会对
UUID()输出做任何重排序,默认存为CHAR(36)字符串,比较和索引都走字典序
CHAR(36) 存储让问题雪上加霜
把 16 字节的 UUID 存成 36 字节字符串,不只是浪费空间 —— 它直接放大所有底层性能缺陷:
- 主键变大 → 所有二级索引都冗余这个 36 字节键 → 索引体积膨胀近 3 倍(实测
CHAR(36)表比BINARY(16)大 55%+) - 字符串比较比整数或二进制慢一个数量级,
WHERE id = ?、JOIN、ORDER BY全受影响 -
CHAR(36)无法利用 B+ 树的数值局部性,哪怕你手写WHERE id > 'a0ee...',也几乎无法跳过大量无效页
页分裂不是“偶尔发生”,而是持续恶化
你不会在第一次插入时看到明显卡顿,但随着数据增长,SHOW TABLE STATUS 中的 Data_free 值会稳步上升,innodb_buffer_pool_reads 持续高于 innodb_buffer_pool_read_requests —— 这说明碎片已开始反噬缓存效率。
- 新记录大概率落在 B+ 树中间页,而非末尾;页满即分裂,旧页拆成两个半页,复制数据、更新指针、刷盘
- 实测百万行表,页平均填充率常低于 65%,远低于自增主键的 85%+;这意味着每读一页,有效数据不到 2/3
- 分裂不是一次性事件:它引发连锁反应——父节点也可能满,触发向上分裂,树高增加,查询路径变长
别指望靠 OPTIMIZE TABLE 救命
有人试过定期跑 OPTIMIZE TABLE,发现短期有效,但很快又退回原状。这不是维护疏忽,而是治标不治本:
-
OPTIMIZE TABLE在 InnoDB 上本质是重建表(ALGORITHM=COPY),锁表、IO 压力大,线上不敢频繁执行 - 即使重建完成,只要继续用
UUID()插入,新的随机值立刻开始新一轮分裂 -
ALTER TABLE ... ALGORITHM=INPLACE不重排主键顺序,只整理页内碎片,对聚簇索引无实质改善
真正要解决的,不是“怎么修碎片”,而是“怎么让新值天然落在末尾附近”。这需要从生成、存储、比较三个环节一起动刀 —— 尤其注意 UUID_TO_BIN(uuid, 1) 的 1 参数不能漏,否则等于白改。











