SQLiteにおけるUUID主キーの危険性

WITHOUT ROWIDで構成されたSQLiteテーブルにおいて、ランダムなUUID(特にUUIDv4)を主キーとして使用すると、深刻なパフォーマンス低下を招く可能性があります。整数主キーと比較して、挿入速度が最大14〜16倍も低下することがあります。これは、ランダムなUUIDがデータベースにクラスター化インデックスのB-treeを常に再構成させることを強制し、過剰なページングとディスクI/Oを引き起こすためです。

クラスター化インデックスがパフォーマンスに与える影響

クラスター化インデックスでは、行の物理的な格納順序がインデックスキーによって決定されます。行は物理的に1つの方法でしかソートできないため、テーブルは1つのクラスター化インデックスしか持つことができません。実質的に、クラスター化インデックステーブルそのものです。

SQLiteでは、デフォルトの動作としてrowidと呼ばれる暗黙的な64ビット整数主キーを使用します。テーブルのデータはこのrowidをキーとしてB-treeに格納されるため、これがクラスター化インデックスとなります。WITHOUT ROWID最適化を使用してテーブルを作成すると、宣言された主キーが暗黙のrowidに代わってクラスター化インデックスになります。

ベンチマーク結果:整数 vs. UUIDv4 vs. UUIDv7

100万行ずつのバッチで1,000万行を挿入するベンチマークは、SQLiteにおける異なる主キー戦略のパフォーマンスコストを示しています。

1. ベースライン:整数主キー (標準のrowid)

挿入は非常に効率的で、毎秒約100万件の挿入に達します。テーブルのサイズが大きくなっても、100万行あたりの所要時間は一貫しています。

2. UUIDv4 WITHOUT ROWID

WITHOUT ROWIDテーブルでランダムなUUIDv4を主キーとして使用すると、パフォーマンスが大幅に低下します。テーブルのサイズが大きくなるにつれて、後続の100万行の挿入にかかる時間が増加します(例:最初の100万行では2,649ms、1億行目では12,586ms)。この低下は、UUIDv4の順序のない性質によって、SQLiteがB-tree内にランダムに行を挿入することを強制され、絶え間ない再構成とページングの増加を引き起こすためです。

3. UUIDv7 WITHOUT ROWID

時間順序のUUIDv7を使用すると、パフォーマンスの問題は大幅に解決されます。挿入時間は妥当なレベルに戻ります(100万行あたり平均約1,250ms)。ただし、整数ベースラインよりもわずかに遅くなります。これは、整数主キー(8バイト)と比較して、UUIDblob(16バイト)のサイズが大きいためです。

4. UUIDv4 WITH ROWID

UUIDv4を主キーとして使用しつつ、暗黙のrowidを維持する(デフォルトのテーブルタイプ)場合、WITHOUT ROWIDよりも優れたパフォーマンスが得られます。これは、クラスター化インデックスがシーケンシャルなrowidのままであるためです。しかし、UUIDv7よりも遅くなります。なぜなら、データベースはUUID主キーのための非クラスター化インデックスを維持する必要があり、そこでのランダム挿入のオーバーヘッドが依然として発生するためです。

主キー戦略のまとめ

| 戦略 | クラスター化インデックス | パフォーマンス | トレードオフ | | :--- | :--- | | :--- | :--- | | | Integer PK | シーケンシャルなrowid | 最速速 | 行数や隣接するIDの露出 | | UUIDv7 (WITHOUT ROWID) | 時間順序のUUID | 高速 | 作成タイムスタンプの露出 | | UUIDv4 (WITH ROWID) | シーケンシャルなrowid | 中程度 | 書き込み増幅(2つのインデックス) | | UUIDv4 (WITHOUT ROWID) | ランダムなUUID | 最低速 | 深刻なB-tree再構成オーバーヘッド |

コミュニティの洞察と代替案

これらのベンチマークに関する技術的な議論では、UUIDを主キーとして使用することに対するいくつかのアーキテクチャ上の代替案が強調されています。

  • ハイブリッドアプローチ: 多くの開発者は、結合(join)や検索(lookup)のための内部主キーとしてシーケンシャルな整数を使用し、公開用識別子として別途UUID列を維持することを推奨しています。これにより、ランダムなキーによるパフォーマンスの落とし穴を回避しつつ、API向けに不透明なIDを提供できます。
  • 分散システムにおけるニーズ: 整数は単一インスタンスのデータベースにおける優先事項ですが、UUIDv7は、複数のアプリケーションインスタンス間でデータをマージしたり、デバイス間で衝突なしに同期したりする必要があるシステムにおいて、重要なツールと見なされています。
  • セキュリティ上の考慮事項: 挿入パフォーマンスが主要な制約ではない場合、作成時間や行数が厳密に隠蔽される必要がある場合には、UUIDv4が依然として有用であると主張する人もいます。

"ランダムIDの主キーは、UUの種類であれ、SQの種類であれ、あるいは他の種類であれ、とにかく悪いアイデアです。私のDB知識の範囲では、この種のIDはすべてのツリーアルゴリズムを破壊します..."

"UUIDv7とシーケンシャルな整数は、かなり似ています。シーケンシャルな整数は行数や隣接数IDを露出させますが、UUIDv7はタイムスタンプを露出させます。"

結論

SQLiteユーザーにとって、最もパフォーマンスの高いUUIDの実装方法は、WITHOUT ROWIDテーブルで時間順序のUUIDv7を使用することです。最高のパフォーマンスが必要な場合は、標準の整数rowidを使用してください。ランダムなUUIDv4をクラスター化インデックスとして使用することは避けてください。データセットのサイズが大きくなるにつれて、パフォーマンスのペナルティが線形に増加するためです。

Sources