SQLite中UUID主键的危险性
2026年6月5日,随机UUID作为数据库中的主键是很常见的做法。随机UUID的已知缺点之一是其无序特性(UUID4)可能导致聚集索引产生大量额外的分页,因为您是随机插入行到B树中,必须重平衡它。本文试图帮助我们更深入地理解额外分页的性能成本。虽然这篇文章是专门针对SQLite的,但随机UUID的问题也扩展到其他使用聚集索引的数据库。什么是聚集索引?聚集索引决定了表中行的物理存储顺序。表的数据行存储在索引的叶子页中,按索引键排序。因此:每个表只能有一个聚集索引(行只能以一种方式物理排序)。聚集索引就是表本身。叶子节点包含完整的行数据,而非聚集索引则仅存储索引列和指向实际行数据的指针,这些数据存储在其他地方。Rowid每个普通SQLite表都有一个隐式的64位整数主键,称为rowid。表的数据按rowid顺序存储在B树中,这实际上是SQLite的聚集索引。行的物理存储顺序遵循rowid顺序。没有rowid的SQLite还支持WITHOUT ROWID表。这些表没有隐式的rowid。相反,您声明的主键成为聚集索引。基准让我们建立一个带有普通rowid整型主键的性能基准。我们将以100万的批次插入1000万行。(d/q writer ["CREATE TABLE IF NOT EXISTS event ( id INT PRIMARY KEY, data BLOB ) "]) (dotimes [_ 100] (时间 (d/with-write-tx [db writer] (dotimes [_ 1000000] (d/q db ["INSERT INTO event ( data ) values ( ? ) " data] ))))))结果:总行数 时间(毫秒)10000000 120820000000 110230000000 117740000000 113850000000 108660000000 110170000000 107080000000 106910000000 107910000000 1081每秒大约插入一百万。UUID4现在让我们试试UUID4。(d/q writer ["CREATE TABLE IF NOT EXISTS event ( id BLOB PRIMARY KEY, data BLOB ) WITHOUT ROWID"]) (dotimes [_ 10] (时间 (d/with-write-tx [db writer] (dotimes [_ 1000000] (d/q db ["INSERT INTO event ( id, data ) values ( ?, ? ) " (random-uuid4-bytes) data] ))))))结果:总行数 时间(毫秒)10000000 264920000000 564430000000 713740000000 835250000000 935960000000 981770000000 104900000000 111300000000 116680000000 12586哦不!这里发生了什么 10-12倍慢?!性能分析这差别很大。但当我们可以进行性能分析时,不要猜测。下面是一个标准化的差异图。差异图比较两个性能快照(在这种情况下是INT与UUID4)并以火焰图结构显示差异。与显示绝对变化的常规差异图不同,标准化视图将两个被比较性能之间的样本总数调整为相同。这意味着我们可以看到相对差异的百分比。这很重要,因为我们的性能分析会运行不同的时间。颜色表示变化的方向:蓝色框表示在第二个性能(UUID4)中,花在这个函数上的时间比第一个(INT)少;红色框意味着在第二个性能中花费更多时间。颜色强度表示框本身的样本数量的相对变化(自耗时变化)。我们可以从差异图中看到,我们花了更多时间来平衡树、读取和写入。这是因为UUID4的无序特性意味着它们是随机排序的,这迫使SQLite不断重平衡B树。UUID7理论上,我们可以用UUID7解决这个问题,UUID7是按时间排序的,消除了UUID4的排序问题。让我们看看这是否改善了情况。(d/q writer ["CREATE TABLE IF NOT EXISTS event ( id BLOB PRIMARY KEY, data BLOB ) WITHOUT ROWID"]) (dotimes [_ 10] (时间 (d/with-write-tx [db writer] (dotimes [_ 1000000] (d/q db ["INSERT INTO event ( id, data ) values ( ?, ? ) " (random-uuid7-bytes) data] ))))))结果:总行数 时间(毫秒)10000000 137220000000 128030000000 136540000000 125050000000 125620000000 127070000000 124680000000 125780000000 124510000000 1258回到更合理的数字上。稍微慢于我们的基准。UUID blob主键为16字节,而int主键为8字节。结论希望这篇文章能帮助说明在SQLite中使用UUID主键的一些陷阱以及如何应对它们。完整的基准测试代码可以在这里找到。如果你喜欢这篇文章,你可能也会喜欢这篇 "SQLite的100000 TPS" 进一步阅读聚集索引聚集索引与WITHOUT ROWID优化clj-async-profiler探索火焰图差异图感谢所有在Datastar Discord上阅读草稿并给我反馈的人。
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡