なぜ今さらUUIDなのか?:PostgreSQLにおける「主キーの最適解」を再考する
DB設計の初期段階で、誰もが一度は直面する問いがあります。「主キーを何にするか」。
かつては連番の `SERIAL` や `BIGSERIAL` が黄金律でした。しかし、システムが分散化し、マイクロサービスが当たり前になった現代において、これらの「単調増加する整数」は、時にスケーラビリティの足枷となります。
今回は、PostgreSQLにおける `UUID` 型を、単なる「便利なID」としてではなく、DBの内部構造とパフォーマンスの観点から深掘りしてみましょう。
—
1. なぜUUIDなのか:分散システムの必然
`BIGSERIAL` を使うと、マスタDBが一つである前提が必要になります。複数のノードで並行してレコードを生成する場合、IDの衝突を避けるためのシーケンス管理がボトルネックになります。
UUID(特にv4やv7)を使えば、各アプリケーションノードが独立して一意なIDを生成できます。DB側に主キー生成を委ねず、クライアント側で完結できる。この「疎結合」こそが、現代の分散システムにおける最大のメリットです。
2. 内部構造と「断片化」の罠
PostgreSQLにおいて `UUID` 型は16バイトの固定長です。`BIGINT` が8バイトであることを考えると、インデックスサイズは単純計算で2倍に膨らみます。
ここで熟練エンジニアが注意すべきは、B-treeインデックスの断片化(Fragmentation) です。
- ランダムなUUID(v4など): 挿入位置が予測不能なため、インデックスページが頻繁に分割(Page Split)されます。これにより、インデックスの密度が下がり、物理的なI/O効率が悪化します。
- シーケンシャルなUUID(v7など): 時間軸でソートされるため、インデックスの末尾に追記されます。これは物理的な局所性を高め、キャッシュ効率を劇的に向上させます。
もし現在UUIDv4を使っていて「書き込みが増えるにつれてパフォーマンスが落ちてきた」と感じているなら、それはインデックスの断片化が原因かもしれません。
3. パフォーマンス・チューニングの鉄則
UUIDを採用する際、避けては通れない最適化のポイントをいくつか共有します。
a. `pgcrypto` の拡張機能に頼りすぎない
`gen_random_uuid()` は便利ですが、DB側で生成するとその分だけCPU負荷がかかります。可能であれば、アプリケーション層(GoやRustなど)で生成し、主キーとしてDBに渡す構成にしましょう。DBは「保存と整合性維持」に集中させるのが、高負荷環境での鉄則です。
b. インデックスのサイズを意識する
16バイトのUUIDは、整数型に比べてキャッシュに載りにくい性質があります。もしUUIDを外部インターフェース(APIのレスポンスなど)として使いつつ、内部的な結合やアクセス効率を重視するなら、「内部的にはBIGSERIAL、外部公開用にはUUID」というハイブリッド戦略も検討の価値があります。
c. `UUID v7` への移行
もしPostgreSQL 17以降、あるいは最新の拡張を利用できる環境であれば、強く `UUID v7` を推奨します。これは時間順序を含むため、v4が抱えていたインデックスの断片化問題と、シーケンシャルなIDの衝突問題を両立させた、まさに「現代の最適解」です。
4. 最後に:銀の弾丸はない
UUIDは強力ですが、万能ではありません。
- 「IDの順序から作成日時を推測されたくない」ならv4
- 「インデックス効率と分散生成を両立させたい」ならv7
これらを用途に合わせて使い分けるのが、熟練のエンジニアの流儀というものです。
データベースの設計は、常に「トレードオフの連続」です。16バイトというサイズをコストと捉えるか、あるいは分散システムの恩恵を受けるための必要経費と捉えるか。その判断を、あなたのシステムの特性に合わせて行ってみてください。
PostgreSQLは、その判断に応えてくれるだけの懐の深さを持っています。さて、次はどのインデックス戦略について語りましょうか。
コメント