自增id插入几乎不触发页分裂,因新记录总追加到b+树最右叶子页末尾;uuid则因随机性导致90%以上插入落在中间页,页满即分裂,引发i/o放大、锁竞争加剧及二级索引体积翻倍。

自增ID插入时几乎不触发页分裂
InnoDB的聚簇索引就是数据本身,B+树叶子节点按主键值顺序存放真实行记录。AUTO_INCREMENT主键每次插入都落在最右端,新记录直接追加到当前页末尾;只要页有空闲空间,就不需要挪动任何数据。实测百万级写入后,SHOW TABLE STATUS LIKE 'users'中Data_free通常低于100KB,页利用率稳定在80%以上。
而UUID插入必须先搜索定位——从根节点逐层下探,90%以上概率落在中间页。一旦目标页已满(InnoDB默认填充因子15/16),就强制触发page split:搬出约50%键值、新建页、更新父节点指针。这不只是CPU开销,还伴随随机I/O——被挤出的页可能已刷盘,又要重新读回Buffer Pool。
- 高并发下多个事务同时往不同中间页插入,锁竞争加剧,
innodb_row_lock_waits明显上升 -
information_schema.INNODB_METRICS里innodb_page_splits指标飙升,是直观信号 - 别指望调优能绕过这点:这是B+树物理结构和插入局部性决定的硬约束,不是配置问题
UUID主键让二级索引体积翻倍以上
所有二级索引的叶子节点都存完整主键值用于回表。BIGINT主键只占8字节,CHAR(36)则存36字节字符串(含横杠),BINARY(16)虽省到16字节,但仍是BIGINT的2倍。
这意味着:同样1000万行数据,二级索引体积直接膨胀30%–50%,Buffer Pool缓存命中率暴跌,磁盘随机I/O被迫介入。压测中Innodb_buffer_pool_reads持续上升,就是典型表现。
-
CHAR(36)字段定义下,应用传参若漏横杠或大小写混用(如'9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d'),索引完全失效,EXPLAIN显示type: ALL -
BINARY(16)+UUID_TO_BIN(UUID(), TRUE)只能缓解,不能根治——插入耗时仍比自增ID高2–3倍 - 联合索引如
(user_id, created_at),若user_id是UUID,整个索引的范围扫描能力基本归零
ALTER TABLE改主键不是改个类型那么简单
线上表想把UUID主键切回自增ID,ALTER TABLE users MODIFY id BIGINT AUTO_INCREMENT大概率失败。InnoDB要重建整个聚簇索引,千万级表锁表时间常超20分钟;更麻烦的是外键约束——如果其他表有FOREIGN KEY (order_id) REFERENCES users(id),而order_id还是VARCHAR(36),直接报错ERROR 1832 (HY000)。
- 安全迁移路径:新增
id_new BIGINT AUTO_INCREMENT列 →INSERT SELECT批量生成 → 停写 → 切换应用逻辑 → 删除旧id列 - MySQL 8.0+可启用
innodb_autoinc_lock_mode = 2(交错模式),但要求binlog_format = ROW,否则主从不一致 - 别把
AUTO_INCREMENT当业务序号用——回滚或批量失败会导致跳号,前端对不上
真正棘手的不是“能不能用UUID”,而是它一旦成为主键就锁死
UUID主键的问题不在“用不用”,而在“用错位置”。它天然适合做业务唯一标识(比如external_id),但绝不该直接顶替聚簇索引主键。很多团队后期想换回自增ID,才发现外键、复制延迟、二级索引统计信息全被牵连,重构成本远超预期。
如果业务真需要分布式唯一性,优先考虑ULID或KSUID——它们时间有序,聚簇性接近自增ID,又保留全局唯一;或者用号段服务生成整数ID,避免单点瓶颈。别拿UUID当默认选项,除非你已接受性能折损并做好长期维护准备。











