前提,数据库(MySQL InnoDB 索引)最核心的数据结构是B + 树。
为什么不能用uuid当主键,而要用有序的id
InnoDB 的主键是聚簇索引:整张表的数据,全部存在主键 B + 树的叶子节点。
叶子节点的数据是按主键有序排列的,叶子节点之间靠双向链表串起来。
节点有容量上限(我们例子:叶子最多存 3 条记录),装满就要分裂。
我们先记住两个核心结论,后面用例子推演:
- 雪花 ID:递增有序主键 → 新数据永远往链表最后一个叶子节点追加,很少触发节点分裂。
- UUID:随机无序主键 → 新数据随机插入到 B + 树中间某个叶子节点,极易触发叶子节点分裂,性能很差。
示例 B + 树(聚簇索引)
[13 , 25] / | \ L1 L2 L3 (3,data) (13,data) (25,data) (7,data) (17,data) (30,data) 叶子链表:L1 ↔ L2 ↔ L3 叶子最多容纳3条记录。叶子链表顺序就是主键从小到大:3,7,13,17,25,30
场景 1:主键是【有序雪花 ID】(趋势递增)
雪花 ID 特点:新生成的 ID 永远比之前所有 ID 更大。 现在持续插入新数据: 已有最大主键 = 30,新插入 ID=31。
- 查找位置:根节点
[13,25],31≥25 →走到最右边叶子 L3 - L3 现在有两条记录:25、30,还能再塞 1 条(上限 3)
- 直接追加到 L3 里面,L3 变成
(25,30,31)✅,不需要分裂节点
再来插入 ID=32: L3 已经满(3 条记录),放不下。触发叶子节点分裂:
- L3 旧:
25,30,31 - 分裂:旧 L3 保留前一半
25,30;新建叶子 L4 放后一半31,32 - L4 最小 key=31,把 31 插入到根节点 根从
[13,25]→[13,25,31]叶子链表:L1 ↔ L2 ↔ L3 ↔ L4
👉 特点:插入永远只操作最右侧叶子节点,绝大多数情况直接追加;只有最右边叶子满了,才会分裂。 节点分裂的次数非常少。
节点分裂是昂贵操作:磁盘写新页、修改上层索引 key、修改链表指针,一次分裂多次 IO。
场景 2:主键是【UUID】
UUID随机无序,比如:550e8400-e29b-41d4-a716-446655440000
UUID 新 ID 不一定比旧 ID 大。 现有树:
[13 , 25] / | \ L1 L2 L3 (3,data) (13,data) (25,data) (7,data) (17,data) (30,data)现在插入一个随机 UUID,换算成数字举例:15
- 查找位置:根节点
[13,25],13 ≤15<25 →走到中间叶子 L2 - L2 现在已经有
13,17,还能塞一条,插入 15 → L2:13,15,17(刚好 3 条,满了)
再来插入下一个随机 UUID,换算成数字:16
- 定位到 L2,L2 现在
13,15,17,已经装满 3 条! - 必须分裂中间这个叶子 L2!
- L2 旧:
13,15,17 - 分裂:旧 L2 保留
13,15;新叶子 L4 放17,16(排序后16,17) - L4 最小 key=16,把 16 插入根节点 根变成
[13,16,25]链表:L1 ↔ L2 ↔ L4 ↔ L3
- L2 旧:
⚠️重点区别:
雪花 ID 只分裂最右边叶子;
UUID 会随机在树中间位置的叶子节点插入、触发分裂。
继续再来一个随机 UUID=9: 定位到 L1(3,7),插入 9,L1 变成3,7,9,满了。
再来随机 ID=8 →又要分裂左边 L1 节点。
UUID 带来的一连串问题(基于 B + 树原理)
- 频繁节点分裂每一次随机插入,都有可能命中一个已经快满的叶子,触发分裂。大量随机写入,分裂次数暴增,大量磁盘 IO,写入性能暴跌。
- 索引页碎片化叶子节点本来是连续的磁盘页。反复在中间分裂,新叶子页会落在磁盘不同位置。 原本连续的叶子链表,物理磁盘上变得零散。范围查询的时候,磁盘需要大量随机读,速度变慢。
- 非叶子节点大量维护开销每次叶子分裂,要把新叶子的最小 key 插入上层非叶子节点。随机插入会造成上层节点也频繁分裂,B + 树整体变高,查询时磁盘 IO 次数变多。
- UUID 本身更长(16 字节),雪花 ID 一般 8 字节 主键越长,非叶子节点能存放的 key 数量越少。同样大小的索引页,能存的分界 key 变少 →树高度更高,查询 IO 变多。