SQLite 中 UUID 主键的危害

在配置了 WITHOUT ROWID 的 SQLite 表中使用随机 UUID(特别是 UUIDv4)作为主键,会导致严重的性能下降,其插入速度与整数主键相比最高可能下降 14-16 倍。这是因为随机 UUID 会迫使数据库不断重新平衡聚簇索引 B-tree,从而导致过度的分页和磁盘 I/O。

聚簇索引对性能的影响

在聚簇索引中,行的物理存储顺序由索引键决定。由于行只能以一种方式进行物理排序,因此一个表只能有一个聚簇索引;实际上,聚簇索引 就是 表本身。

在 SQLite 中,默认行为是使用一个名为 rowid 的隐式 64 位整数主键。表的数据使用此 rowid 作为键存储在 B-tree 中,使其成为聚簇索引。当使用 WITHOUT ROWID 优化创建表时,声明的主键将取代隐式的 rowid 成为聚簇索引。

基准测试结果:整数 vs. UUIDv4 vs. UUIDv7

通过以 100 万行为一组进行分批插入 1000 万行数据的基准测试,展示了 SQLite 中不同主键策略的性能成本:

1. 基准:整数主键 (Standard rowid)

插入效率极高,每秒可达到约一百万次插入。随着表规模的增长,每百万行的耗时保持一致。

2. UUIDv4 WITHOUT ROWID

WITHOUT ROWID 表中使用随机 UUIDv4 作为主键时,性能显著下降。插入后续每 100 万行数据的耗时随着表规模的增长而增加(例如,从第一个 100 万行的 2,649ms 增加到第 1 亿行的 12,586ms)。这种性能退化是由 UUIDv4 的无序特性引起的,它迫使 SQLite 将行随机插入到 B-tree 中,从而触发不断的重新平衡和增加的分页。

3. UUIDv7 WITHOUT ROWID

使用具有时间顺序的 UUIDv7——这在很大程度上解决了性能问题。插入时间恢复到了合理的水平(平均每百万行约 1,250ms),尽管它们仍然比整数基准稍慢。这归因于 UUID 字节块(16 字节)比整数主键(8 字节)更大。

4. UUIDv4 WITH ROWID

使用 UUIDv4 作为主键但保留隐式 rowid(默认表类型)的结果比 WITHOUT ROWID 性能更好,因为聚簇索引仍然是顺序的 (rowid)。然而,它仍然比 UUIDv7 慢,因为数据库必须仍为 UUID 主键维护一个非聚簇索引,这仍然会受到随机插入开销的影响。

主键策略总结

| 策略 | 聚簇索引 | 性能 | 权衡 | | :--- | :--- |" :

Sources