SQLite UUID 主鍵的效能陷阱:B-Tree 碎片化與寫入放大
andersmurphy.com · 2026-06-05
一篇在 Lobsters 獲得高關注的技術文章詳細分析在 SQLite 中使用 UUID 作為主鍵的效能代價,結論是 UUID v4 作為主鍵會導致 B-Tree 結構嚴重碎片化,在寫入密集場景下產生顯著的效能退化,作者以 Clojure + SQLite 應用為背景提供了具體測量數據與解決方案。
問題根源:隨機插入導致 B-Tree 分裂
SQLite 的主鍵索引是一棵 B-Tree,頁面大小預設 4 KB。UUID v4 是隨機生成的 128 位元值,新記錄的主鍵幾乎必然落在已存在記錄的隨機位置,觸發 B-Tree 中間頁的分裂與重新平衡。對比之下,自增整數(ROWID)永遠追加到 B-Tree 最右側葉子節點,不觸發已有頁面的修改。
隨機插入的直接後果有三:(1)每次插入平均需要讀取並寫回更多 B-Tree 頁面(寫入放大);(2)頁面填充率下降(B-Tree 分裂後兩個子節點各約半滿),導致相同資料量需要更多頁面,增加讀取時的 I/O;(3)SQLite 的 WAL(Write-Ahead Log)需要記錄更多頁面修改,checkpoint 時的同步工作增加。
測量結果
文章以 100 萬筆記錄的插入測試比較:
- ROWID(自增整數):插入耗時基準值
- UUID v4:插入耗時約 ROWID 的 4–6 倍,資料庫文件大小約 ROWID 的 1.5–2 倍(頁面碎片化)
- UUID v7(時間排序 UUID):插入耗時接近 ROWID,因為 UUID v7 的高位元是 Unix 毫秒時間戳記,保證新值大於既有值,不觸發隨機分裂
解決方案
UUID v7 是在需要全域唯一識別符的情況下,保留 SQLite 插入效能的最佳選擇。UUID v7 格式(RFC 9562)的前 48 位元為 Unix 毫秒時間戳記,後 74 位元為隨機值,保證在相同毫秒內多個節點生成的 UUID 幾乎嚴格遞增(偶爾的毫秒衝突由隨機位元部分緩解),讓 B-Tree 退化為近似追加模式。對於不需要跨系統唯一性的場景,SQLite 原生的 INTEGER PRIMARY KEY AUTOINCREMENT 仍是效能最優選項。
原始來源:andersmurphy.com
Redis 8.8:Array 資料型別、INCREX 限速器與 Stream 效能改進
Redis Blog · 2026-06-04
Redis 發布 8.8 版本,引入三個主要新特性:原生 Array 資料型別、INCREX 帶過期時間的增量指令,以及 XNACK 的 Stream pending entry 批次確認。8.8 延續 Redis 8.x 系列的高效能方向,同時提升 MGET/MSET 與 Stream 操作的吞吐量。
Array 資料型別
Redis 新增 ARRAY 型別,支援有序、支援下標存取的可變長序列,區別於現有的 List(雙向鏈結串列,O(n) 隨機存取)。Array 型別使用緊湊的連續記憶體佈局,提供 O(1) 下標讀寫,代價是插入(非追加端)仍為 O(n) 移位。新指令集:
ASET key index value:設定指定下標的元素AGET key index:讀取指定下標的元素APUSH key value [value ...]:追加元素ALEN key:回傳長度ARANGE key start stop:範圍讀取(類似 LRANGE)
INCREX:帶過期時間的原子增量
INCREX key increment milliseconds 在單一原子操作中執行增量並設定毫秒級過期時間,解決限速器(rate limiter)常見的 INCR + PEXPIRE 兩步驟競態條件。傳統做法使用 Lua 腳本保證原子性,INCREX 將此模式提升為原生指令,消除腳本呼叫的開銷與維護負擔。
效能改進
8.8 的效能改進集中在兩個面向:MGET/MSET 在大批次下的吞吐量提升——透過 pipeline 批次路徑最佳化,減少每個鍵的平均處理指令數;以及 Stream 消費者群組(consumer group)的 XACK 批次處理——XNACK 指令允許在一次呼叫中確認多個 pending entry,降低高吞吐 Stream 消費場景的往返次數。基準測試顯示在 100 個鍵的 MGET 批次上有 15–23% 的吞吐量提升。
原始來源:Redis Blog、Array 資料型別深入解析