根本原因是UUID与InnoDB聚簇索引机制冲突:其随机性导致频繁页分裂、存储膨胀及Buffer Pool命中率下降,实测每千行插入触发1.7次分裂,页填充率常低于70%。

MySQL 主键不建议用 UUID,根本原因不是它“不唯一”,而是它和 InnoDB 的聚簇索引机制严重冲突:随机写入导致页分裂频繁、存储膨胀、查询变慢——这些问题在数据量过百万后会快速暴露。
UUID 主键为什么让 INSERT 变慢?看页分裂真实发生过程
InnoDB 表数据按主键物理排序存储,新行必须插入到 B+ 树的正确位置。自增 ID 总是追加到末尾,而 UUID() 生成的是完全随机的 16 字节序列(比如 0x550e8400e29b41d4a716446655440000),每次插入都大概率落在中间页。一旦目标页已满(默认 16KB),就必须做页分裂:
- 原页一半数据复制到新页,更新父节点指针,产生额外 I/O 和锁等待
- 实测:每插入 1000 行,
UUID()平均触发 1.7 次页分裂;自增 ID 基本为 0 -
SHOW TABLE STATUS中Data_free持续增长、innodb_buffer_pool_reads飙升,就是页碎片化的直接信号
CHAR(36) 和 BINARY(16) 都救不了无序性
把主键从 CHAR(36) 改成 BINARY(16) 能省 20 字节,但解决不了核心问题——B+ 树比较的是字节序列,而标准 UUID v4 的原始二进制顺序仍是时间戳+随机段混排,高位不单调。例如两个 UUID:
1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d → 0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d<br>6d5c5a5e-5c5a-4e5a-8d5a-5a5e5c5a5e5c → 0x6d5c5a5e5c5a4e5a8d5a5a5e5c5a5e5c
首字节 0x1a vs 0x6d 就决定了排序位置,后续插入毫无局部性。真正起作用的是:
-
UUID_TO_BIN(UUID(), 1):把时间戳高位前置,实现近似递增(v1/v7 才适用) - 客户端禁用
UUID.randomUUID()(Java)或crypto.randomUUID()(Node.js)——它们默认生成 v4,纯随机 -
BINARY(16)必须配合有序 UUID 使用,否则只是“更紧凑的随机”
比“优化 UUID”更靠谱的替代方案
如果业务允许改主键类型,优先考虑这些经过千万级表验证的方案:
-
ULID:前 6 字节毫秒时间戳 + 后 10 字节随机数,字符串形式(如
01ARZ3NDEKTSV4RRFFQZFSM5BG)天然字典序可排序,存为CHAR(26)或BINARY(16),兼容 UUID 语义且性能接近自增 -
Snowflake / Leaf-segment ID:64 位整数,
BIGINT UNSIGNED存储,二级索引体积小;注意校准服务器时钟,避免回退导致重复 -
自增主键 + UUID 字段组合:主键用
id BIGINT AUTO_INCREMENT,另设external_id CHAR(36) UNIQUE NOT NULL作对外暴露 ID(API 返回、订单号),兼顾性能与防遍历
真正容易被忽略的点是:页分裂不是“偶尔慢一下”,而是持续污染 Buffer Pool、降低页填充率(实测 UUID 表页填充率常低于 70%),且这种劣化会随数据量线性放大。哪怕你今天只写 1000 条,上线半年后查一条 ORDER BY id LIMIT 10 就可能变成全表扫描——因为物理存储早已乱序。



















