MySQL用UUID()直接作主键必然导致性能下降,因其生成v1 UUID后按字符串字典序存储,时间戳高位被连字符和字节序打乱,插入位置随机,频繁触发B+树页分裂;必须改用UUID_TO_BIN(UUID(), 1)配合BINARY(16)存储才能实现时间局部性。

MySQL 用 UUID() 函数直接作主键,性能下降不是“可能”,而是几乎必然——根本原因在于它生成的是 v1 UUID,但 MySQL 默认按字符串字典序存储,时间戳高位被压在中间,插入仍是随机的。不改存储方式和生成逻辑,光靠加索引或调参没用。
为什么 UUID() 函数本身就不适合当主键
MySQL 的 UUID() 返回的是标准 v1 格式(如 550e8400-e29b-41d4-a716-446655440000),含时间戳 + MAC 地址 + 序列号,但字符串形式下连字符和字节顺序导致字典序 ≠ 时间序。InnoDB 按 CHAR(36) 比较,结果就是:新值大概率插在索引中间,触发页分裂。实测 300 万行后,Data_free 值常超 1GB,缓冲池命中率跌破 60%。
- 不要用
DEFAULT (UUID())直接建表,这是最常见也是最危险的写法 -
UUID()在 MySQL 8.0 以下版本无法自动重排字节顺序,swap_flag 参数无效 - 即使你手动去掉连字符(
REPLACE(UUID(), '-', '')),仍只是 32 位十六进制字符串,比较慢、占空间、无序
MySQL 8.0+ 必须用 UUID_TO_BIN(uuid(), 1)
MySQL 8.0.1 起,UUID_TO_BIN() 的第二个参数 swap_flag = 1 会把时间戳高位(前 6 字节)移到最前面,并交换字节序,让二进制值具备时间局部性。这才是真正能让 B+ 树“接受”的有序 UUID。
- 建表语句必须写成:
id BINARY(16) PRIMARY KEY DEFAULT (UUID_TO_BIN(UUID(), 1)) - 查询时还原可读格式:用
BIN_TO_UUID(id, 1),第二个参数必须为1,否则乱序 - 不能用
UUID_TO_BIN(UUID())(漏掉1),那样等价于随机插入,白忙活 - 应用层若需预生成 ID,必须统一走
UUID_TO_BIN('xxx-xxx', 1),不能只传字符串
旧版本 MySQL(
没有 swap_flag 支持,UUID_TO_BIN() 默认输出仍是随机序,强行用等于自欺欺人。这时有两条现实路径:
- 用
UUID_SHORT():返回 8 字节整数,基于启动时间 + server_id + 计数器,天然有序;但跨实例不唯一,仅限单机或严格分库场景;需配合UNIQUE约束防重 - 改用 ULID 或 Snowflake:ULID(如
01ARZ3NDEKTSV4RRFFQZFSM5BG)前 48 位是毫秒时间戳,字符串天然可排序,存为CHAR(26)或BINARY(16)都行;Snowflake 用BIGINT UNSIGNED存,更省空间,但要注意时钟回拨
就算用了有序 UUID,也得配对优化存储格式
有序只是第一步,如果还存成 CHAR(36),性能依然打七折。核心压缩动作不能少:
- 绝对不用
CHAR(36)或VARCHAR(36),哪怕加了UUID_TO_BIN()也白搭 - 主键字段类型必须是
BINARY(16),从 36 字节压到 16 字节,二级索引体积直降 55%+ - 所有应用层代码读写该字段,必须统一走二进制流转,避免隐式转换(比如拼 SQL 时 toString() 出来再塞进去)
- 如果必须对外暴露字符串格式,用
BIN_TO_UUID(id, 1)在查询层转换,别在业务逻辑里反复编解码
最容易被忽略的一点:有序 UUID 的“序”只在单实例内有效。如果你用分库分表且依赖全局唯一性,ULID 或 Snowflake 的节点位设计比单纯依赖 MySQL 内置函数更可控;而 UUID_TO_BIN(..., 1) 的可靠性,完全绑定在 MySQL 版本和字段定义是否严丝合缝——漏一个 1,整张表就退回随机插入状态。



















