自增id插入性能远优于uuid:自增id仅需定位最右叶子页,路径短、分裂少、单页存100条;uuid随机插入导致搜索路径长3–4倍、分裂多37%、单页仅存30条、延迟高6倍、吞吐量差4倍。

自增ID插入只找最右叶子页,UUID每次都要从根节点搜
InnoDB 的 B+ 树聚簇索引要求数据按主键物理排序存储。自增 AUTO_INCREMENT 插入时,新记录总追加到索引最右侧的叶子页,InnoDB 只需定位到最后一页、判断是否满、决定是否分裂——路径极短,CPU 比较少,缓存预取有效。
而 UUID 是 128 位随机值(如 '550e8400-e29b-41d4-a716-446655440000'),插入位置完全不可预测:必须从根节点开始逐层向下搜索,最终落在任意中间页。实测显示,16 线程并发写入百万行时,innodb_page_splits 指标比自增高 37%,B+ 树搜索路径平均长 3–4 倍。
- 自增 ID:插入点固定在“末尾”,几乎不触发页分裂
- UUID v4:99% 插入落在中间区域,强制拆页、重连指针、刷盘次数翻倍
- UUID v7(MySQL 8.0+)能缓解但不消除问题:时间戳前置提升局部性,插入耗时仍比自增高 2–3 倍
单页存 100 条自增记录 vs 30 条 UUID 记录
innodb_page_size=16K 下,页内要存页头、记录头、空闲空间等元信息。主键越长,每页能塞下的行数就越少。
一个 BIGINT 主键占 8 字节,加上其他字段后,16K 页通常存约 100 条;而 CHAR(36) 存标准格式 UUID(含横杠),实际占 36 字节 + 长度前缀 + 填充碎片,同一页往往只剩 30–40 条可用空间——单页容量直接跌掉 60%。
- 二级索引也跟着膨胀:每个二级索引项都存完整主键值,
CHAR(36)让所有二级索引体积翻 3 倍以上 - 别用
UUID()直插:它返回带横杠字符串,多占 4 字节且排序慢;至少用REPLACE(UUID(), '-', '')转成CHAR(32) - MySQL 8.0+ 推荐
UUID_TO_BIN(UUID(), TRUE)存为BINARY(16),但无法解决根本的随机性
INSERT 延迟从 0.3ms 跳到 1.8ms 不是偶然
真实业务中,每秒写入 500+ 行的订单表,用 BIGINT AUTO_INCREMENT 时 Query_time 稳定在 0.2–0.5ms;换成 VARCHAR(36) UUID 后,慢日志里频繁出现 Query_time: 3.845123 这类记录,高峰期甚至触发行锁等待。
- 批量插入 100 万行:自增耗时 12.3 秒,UUID v4 耗时 47.8 秒(吞吐量差 4 倍)
- 单条插入延迟:自增均值 0.3ms,UUID v4 均值 1.8ms(6 倍差距)
- 不是“够用就行”的问题——延迟跳变会直接暴露在监控和用户感知里
外键、索引、应用代码全得跟着改,不是 ALTER TABLE 就完事
线上表想从 UUID 主键切回自增,ALTER TABLE users MODIFY id BIGINT AUTO_INCREMENT 会失败:主键变更要重建整个聚簇索引,千万级表锁表常超 20 分钟,且外键、二级索引、触发器全受影响。
- 先查
SHOW CREATE TABLE:确认有没有联合索引以主键开头,避免 DROP PRIMARY KEY 顺手干掉关键查询路径 - 检查外键依赖:如果
order_items.order_id是VARCHAR(36),改users.id会直接报错ERROR 1832 (HY000) - 应用层 WHERE 参数类型必须严格匹配:ORM 把数字当字符串传(如
WHERE id = '12345')查BIGINT主键还能隐式转换,但查CHAR(36)主键若漏横杠或大小写混用,就彻底命中不了索引
Query_time、监控里跳变的 P99 延迟、磁盘 I/O 指标里飙升的随机读。选 UUID 前,先问清楚——你是不是真的在分库分表且没上 Snowflake,或者正在合并多个数据库?否则,就是拿确定的性能折损,换一个其实并不需要的“全局唯一”。











