MySQL主键不建议用UUID,根本原因是其随机性破坏InnoDB聚簇索引物理排序逻辑,导致频繁页分裂(每千行插入平均触发1.7次)、索引膨胀及ORDER BY查询退化为全表扫描,即便转为BINARY(16)或UUID_TO_BIN()也难改无序本质。

MySQL 主键不建议用 UUID,根本不是因为“不够唯一”或“生成慢”,而是它直接破坏 InnoDB 聚簇索引的物理组织逻辑——写入变慢、索引膨胀、查询退化,三者叠加后,数据量一过百万就明显卡顿。
UUID 插入引发频繁页分裂,自增ID几乎不触发
InnoDB 的主键即聚簇索引,数据行直接存于 B+ 树叶子节点。页大小固定为 16KB,插入位置决定性能:
-
AUTO_INCREMENT总追加到末尾,页填满后自然分配新页,分裂极少,实测百万行后页分裂接近 0 次 -
UUID()是随机字符串(如'550e8400-e29b-41d4-a716-446655440000'),每次插入都要二分查找落点,大概率落在已有页中间 - 页已满时必须分裂:搬一半数据、建新页、更新父节点指针——单次插入常牵连 3 个以上页,CPU 和随机 I/O 暴涨
- 实测每插入 1000 行
UUID主键,平均触发 1.7 次页分裂;SHOW TABLE STATUS中Data_free值会持续升高,就是碎片证据
BINARY(16) 或 UUID_TO_BIN() 只省空间,不改无序本质
把 CHAR(36) 改成 BINARY(16) 或用 UUID_TO_BIN(uuid),只是压缩存储体积,对插入性能改善微乎其微:
-
BINARY(16)占 16 字节,比CHAR(36)少 20 字节,二级索引体积缩小约 55%,但插入位置仍是随机的 -
UUID_TO_BIN(uuid)默认不重排字节序(即swap_flag = 0),时间戳仍埋在中间,首字节还是随机值,B+ 树排序和定位不受益 - 真正有效的是
UUID_TO_BIN(uuid, 1)(MySQL 8.0+),把时间戳高位前置,形成近似递增序列,页分裂率可降 90%,但仍无法达到自增 ID 水平 - 同一毫秒内高并发生成多个 UUIDv1/v6,后缀仍随机,这些 ID 会集中挤爆同一数据页
ORDER BY id 查询退化为全表扫描
主键乱序导致最基础的范围查询失效:
-
SELECT * FROM t ORDER BY id LIMIT 10在UUID主键下,InnoDB 必须遍历所有叶子页才能凑出逻辑最小的 10 条记录 - 无法利用 B+ 树有序链表做跳跃式遍历,等效于全表扫描,响应时间随数据量线性增长
- 即使加了
WHERE id > ?,若 ? 不是物理连续段起点,依然可能回溯多个分散页,I/O 效率远低于自增 ID 的顺序读取 - 二级索引叶子节点存的是主键值,
BINARY(16)虽减小索引体积,但回表时仍要按随机地址读取数据页,缓存命中率低
真正容易被忽略的是:问题不在 UUID 本身,而在你是否控制得了它的字节序和生成节奏。哪怕用了 UUID_TO_BIN(uuid, 1),如果应用层每毫秒批量生成上百 ID,InnoDB 依然扛不住。自增 ID 简单粗暴但高效,雪花 ID 需严格校准时钟且避免单点压测——选哪个,得看你的写入吞吐、分布式粒度和运维能力边界在哪。


















