2026 年 6 月 5 日在数据库中用随机 UUID 作主键很常见。不过随机 UUID如 UUID4有个已知缺点其无序性会让聚集索引产生大量额外分页操作因为是随机将行插入 B 树还得重新平衡。本文能帮我们直观了解这些额外分页操作带来的性能成本。虽说本文主要针对 SQLite但随机 UUID 的问题在其他用聚集索引的数据库中同样存在。什么是聚集索引聚集索引决定了表中数据行的物理存储顺序。表的数据行存于索引的叶子页按索引键排序。所以每个表只能有一个聚集索引行只能按一种方式物理排序聚集索引就是表本身叶子节点含完整的行数据相比之下非聚集索引只存索引列及指向实际行数据的指针实际行数据存于其他地方。Rowid每个普通的 SQLite 表都有个隐式的 64 位整数主键叫 rowid。表的数据存于按 rowid 排序的 B 树中这其实就是 SQLite 的聚集索引。行的物理存储顺序遵循 rowid 序列。无 Rowid 表SQLite 也支持无 Rowid 表。这些表没有隐式的 rowid声明的主键会成为聚集索引。基准测试我们以常规的 rowid 整数主键建立性能基准分批次插入 1000 万行数据每次插 100 万行。(d/q writer [CREATE TABLE IF NOT EXISTS event(id INT PRIMARY KEY, data BLOB)]) (dotimes [_ 100] (time (d/with-write-tx [db writer] (dotimes [_ 1000000] (d/q db [INSERT INTO event (data) values (?), data])))))结果如下总行数时间毫秒1000000012082000000011023000000011774000000011385000000010866000000011017000000010708000000010699000000010791000000001081大约每秒插入 100 万行。UUID4现在试试用 UUID4。(d/q writer [CREATE TABLE IF NOT EXISTS event(id BLOB PRIMARY KEY, data BLOB) WITHOUT ROWID]) (dotimes [_ 10] (time (d/with-write-tx [db writer] (dotimes [_ 1000000] (d/q db [INSERT INTO event (id, data) values (?, ?), (random-uuid4-bytes), data])))))结果如下总行数时间毫秒10000000264920000000564430000000713740000000835250000000935960000000981770000000104908000000011130900000001166810000000012586哦不速度慢了 10 - 12 倍性能分析差距不小。不过我们能进行性能分析而非凭空猜测。下面是归一化的差异图diffgraph。差异图用于比较两个性能分析快照本例中是整数主键和 UUID4 主键并以火焰图结构显示差异。与显示绝对变化的常规差异图不同归一化视图会调整两个比较配置文件的样本总数使其相同。这意味着我们能以百分比形式看到相对差异这很重要因为配置文件运行时间不同。颜色表示变化方向蓝色框表示在第二个配置文件UUID4中该函数花费时间比第一个整数主键少红色框表示在第二个配置文件中花费时间更多。颜色强度表示该框样本数量的相对变化自身时间增量。从差异图能看到在平衡树、读写操作上花费时间更多。这是因为 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] (time (d/with-write-tx [db writer] (dotimes [_ 1000000] (d/q db [INSERT INTO event (id, data) values (?, ?), (random-uuid7-bytes), data])))))结果如下总行数时间毫秒1000000013722000000012803000000013654000000012505000000012566000000012707000000012468000000012579000000012451000000001258速度回到较合理水平比基准测试稍慢。这是因为 UUID 二进制主键为 16 字节而整数主键为 8 字节。结论希望本文能帮你了解 SQLite 中使用 UUID 主键的陷阱及应对办法。完整的基准测试代码可[点击此处](https://github.com/andersmurphy/clj-cookbook/tree/master/sqlite-perils-of-uuid)查看。如果你喜欢这篇文章可能也会对[《使用 SQLite 实现 100000 TPS》](https://andersmurphy.com/2025/12/02/100000-tps-over-a-billion-rows-the-unreasonable-effectiveness-of-sqlite.html)感兴趣。延伸阅读聚集索引聚集索引与无 Rowid 优化clj-async-profiler探索火焰图差异图感谢 [Datastar Discord](https://discord.gg/bnRNgZjgPh) 上阅读本文草稿并提供反馈的每一个人。
SQLite 使用 UUID 主键风险大!UUID7 能否解决排序难题?
2026 年 6 月 5 日在数据库中用随机 UUID 作主键很常见。不过随机 UUID如 UUID4有个已知缺点其无序性会让聚集索引产生大量额外分页操作因为是随机将行插入 B 树还得重新平衡。本文能帮我们直观了解这些额外分页操作带来的性能成本。虽说本文主要针对 SQLite但随机 UUID 的问题在其他用聚集索引的数据库中同样存在。什么是聚集索引聚集索引决定了表中数据行的物理存储顺序。表的数据行存于索引的叶子页按索引键排序。所以每个表只能有一个聚集索引行只能按一种方式物理排序聚集索引就是表本身叶子节点含完整的行数据相比之下非聚集索引只存索引列及指向实际行数据的指针实际行数据存于其他地方。Rowid每个普通的 SQLite 表都有个隐式的 64 位整数主键叫 rowid。表的数据存于按 rowid 排序的 B 树中这其实就是 SQLite 的聚集索引。行的物理存储顺序遵循 rowid 序列。无 Rowid 表SQLite 也支持无 Rowid 表。这些表没有隐式的 rowid声明的主键会成为聚集索引。基准测试我们以常规的 rowid 整数主键建立性能基准分批次插入 1000 万行数据每次插 100 万行。(d/q writer [CREATE TABLE IF NOT EXISTS event(id INT PRIMARY KEY, data BLOB)]) (dotimes [_ 100] (time (d/with-write-tx [db writer] (dotimes [_ 1000000] (d/q db [INSERT INTO event (data) values (?), data])))))结果如下总行数时间毫秒1000000012082000000011023000000011774000000011385000000010866000000011017000000010708000000010699000000010791000000001081大约每秒插入 100 万行。UUID4现在试试用 UUID4。(d/q writer [CREATE TABLE IF NOT EXISTS event(id BLOB PRIMARY KEY, data BLOB) WITHOUT ROWID]) (dotimes [_ 10] (time (d/with-write-tx [db writer] (dotimes [_ 1000000] (d/q db [INSERT INTO event (id, data) values (?, ?), (random-uuid4-bytes), data])))))结果如下总行数时间毫秒10000000264920000000564430000000713740000000835250000000935960000000981770000000104908000000011130900000001166810000000012586哦不速度慢了 10 - 12 倍性能分析差距不小。不过我们能进行性能分析而非凭空猜测。下面是归一化的差异图diffgraph。差异图用于比较两个性能分析快照本例中是整数主键和 UUID4 主键并以火焰图结构显示差异。与显示绝对变化的常规差异图不同归一化视图会调整两个比较配置文件的样本总数使其相同。这意味着我们能以百分比形式看到相对差异这很重要因为配置文件运行时间不同。颜色表示变化方向蓝色框表示在第二个配置文件UUID4中该函数花费时间比第一个整数主键少红色框表示在第二个配置文件中花费时间更多。颜色强度表示该框样本数量的相对变化自身时间增量。从差异图能看到在平衡树、读写操作上花费时间更多。这是因为 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] (time (d/with-write-tx [db writer] (dotimes [_ 1000000] (d/q db [INSERT INTO event (id, data) values (?, ?), (random-uuid7-bytes), data])))))结果如下总行数时间毫秒1000000013722000000012803000000013654000000012505000000012566000000012707000000012468000000012579000000012451000000001258速度回到较合理水平比基准测试稍慢。这是因为 UUID 二进制主键为 16 字节而整数主键为 8 字节。结论希望本文能帮你了解 SQLite 中使用 UUID 主键的陷阱及应对办法。完整的基准测试代码可[点击此处](https://github.com/andersmurphy/clj-cookbook/tree/master/sqlite-perils-of-uuid)查看。如果你喜欢这篇文章可能也会对[《使用 SQLite 实现 100000 TPS》](https://andersmurphy.com/2025/12/02/100000-tps-over-a-billion-rows-the-unreasonable-effectiveness-of-sqlite.html)感兴趣。延伸阅读聚集索引聚集索引与无 Rowid 优化clj-async-profiler探索火焰图差异图感谢 [Datastar Discord](https://discord.gg/bnRNgZjgPh) 上阅读本文草稿并提供反馈的每一个人。