- UUID v4 はランダム性が高く、インデックス効率の低下と過剰な I/O を引き起こし、PostgreSQLで主キーとして使用すると性能低下が発生する
- ランダムな挿入により ページ分割(page split) と インデックス断片化 が頻発し、WALログサイズの増加 と書き込み遅延が発生する
- UUIDは 16バイト のサイズで、bigintの2倍の領域を占有し、キャッシュヒット率の低下と メモリ浪費 につながる
- セキュアな識別子と誤解されがちだが、RFC 4122によれば UUIDは推測防止のためのセキュリティ手段ではない
- 新しいデータベースでは 整数シーケンスベースのキー を使い、やむを得ない場合は 時系列UUID v7 を使うことが推奨される
UUID v4の性能問題
- PostgreSQLで UUID v4主キー を使用したデータベースは、この10年間一貫して 性能低下と過剰な I/O を示してきた
- UUID v4は122ビットがランダムに生成されるため、インデックスの整列ができない
- 挿入時に連続したページへ保存されず ランダムアクセス が発生し、更新・削除時にも非効率な探索が必要になる
- B-Treeインデックス は整列されたデータを前提とするが、UUID v4には順序性がないため 挿入効率が低い
- 各挿入が任意のページに書き込まれることで 中間ページの分割 が頻繁に発生する
- これにより 書き込みレイテンシ と WALの増加 が発生する
UUIDの構造と代替案
- UUIDは128ビット(16バイト)の識別子であり、PostgreSQLでは binary uuid型 として保存される
- UUID v4 はランダムビットベース、UUID v7 は先頭48ビットに タイムスタンプ を含むためインデックス効率が高い
- PostgreSQL 18(2025年予定)ではUUID v7が標準サポートされる予定
- UUID v7 は時系列順のソートが可能なため、ページ密度とキャッシュ効率 が改善される
UUIDが選ばれる理由と限界
- 複数クライアントやマイクロサービス環境で 衝突のない識別子生成 が必要なときにUUIDが使われる
- 例: 複数のデータベースインスタンスで同時にIDを生成する場合
- しかしRFC 4122には「UUIDは推測が難しいと仮定してはならない」と明記されており、セキュアな識別子としては不適切
- 衝突確率は2.71×10¹⁸個生成時に50%であり、現実的には衝突可能性は低いが 性能コストが大きい
UUIDの領域とI/Oの非効率
- UUIDは bigint(8バイト) の2倍、int(4バイト) の4倍の領域を占める
- 大規模テーブルでは 保存領域の増加 と バックアップ・リストア時間の増加 につながる
- インデックスページ密度 の実験結果
- integerインデックス: 97.64%
- UUID v4インデックス: 79.06%
- UUID v7インデックス: 90.09%
- Cybertecのテストでは UUID v4インデックス参照時に850万件の追加ページアクセス、31229%の I/O増加 が確認された
- 同一条件でbigintインデックスは27,332バッファアクセス、UUID v4は8,562,960バッファアクセス
キャッシュとメモリへの影響
- UUIDはランダム分布のため バッファキャッシュヒット率(cache hit ratio) が低い
- より多くのページをキャッシュに読み込む必要があり、必要なページが頻繁に エビクション(eviction) される
- キャッシュ効率の低下によってクエリ遅延が発生し、メモリ使用量が増加 する
- 性能維持のため 定期的なインデックス再構築(REINDEX CONCURRENTLY) や pg_repack の利用が推奨される
性能を緩和する方法
- メモリ拡張: データベースサイズの4倍のRAMを確保することが推奨される(例: DB 25GB → メモリ 128GB)
- work_memの調整: ソート演算時により多くのメモリを割り当てることで性能改善が可能
- Rails環境 では
implicit_order_column の設定を通じて、UUIDの代わりに created_at など並び替え可能なフィールドを使う
- CLUSTERコマンド で並び替え可能なフィールドを基準にテーブルを再配置できるが、排他ロック が必要
整数型キーとシーケンスの推奨
- 新規データベースには 整数型シーケンスベースのキー の利用が推奨される
integer(4バイト)は約20億個、bigint(8バイト)はそれよりはるかに多くの一意な値を提供する
- ほとんどのビジネスアプリではintegerで十分であり、大規模サービスでは bigint の利用が適している
- UUID v4の代わりにUUID v7 または sequential_uuids拡張 を使うのが現実的な代替案である
まとめ
- UUID v4は ランダム性によるインデックス非効率、高い I/O、低いキャッシュ効率 を招く
- セキュアな識別子 としては使えず、領域の浪費 も大きい
- 整数型シーケンスキー がほとんどのアプリケーションにより適している
- やむを得ずUUIDを使うなら 時系列UUID v7 を選ぶべきである
- PostgreSQLで gen_random_uuid() を主キーとして使うのは避けるべきである
1件のコメント
Hacker Newsの意見
これは典型的な早すぎる最適化の例だと思う
永続識別子にデータを埋め込むのは、データ管理では禁じ手とされる
ノルウェーの住民番号のように生年月日をIDに入れると、後になって生年月日を誤って把握していた移民が出てきたり、1月1日生まれが多すぎて番号が足りなくなるといった問題が起きる
昔のカードカタログ時代なら検索コストを下げるためにデータと識別子を混ぜるのも理解できるが、今は強力なデータベースがあるので、わざわざそうする必要はない
分からない誕生日を1月1日にしたのが問題なのであって、日付をキーに入れたこと自体が本質的な問題ではない
もし00や99のような日付ではない値を使っていれば衝突は起きなかったはずだ
UUIDにタイムスタンプを入れるのは意味を持たせるためではなく、性能最適化が目的だ
時系列で増加するキーはB-treeの再編成コストを減らし、DBの挿入性能を高める
「永続識別子にデータを入れるな」というのは一般論にすぎず、状況によってはトレードオフを受け入れることもある
例えばmd5ハッシュをUUIDとして使ってインデックスを構成すれば断片化は起きるが、管理可能な水準ではある
ランダムUUIDか時間ベースUUIDかの選択で、ミリ秒単位ではなく秒単位の性能差が出ることもある
大規模DBではシャーディングと分散が必須なので、UUIDのほうがオートインクリメントよりうまく機能する
フォーマットがDDMMYYXXXXXなら10万人までカバーできるが、本当にそこまで集中し得るのか気になる
おそらく特定の年に難民が大量流入した場合のような特殊事情なのだろう
UUIDはセキュリティトークンのように使うべきではない
推測しにくいという理由だけでセキュリティ機能として使うのは危険だ
ランダム値の目的は推測防止だけでなく、連続したID同士の関係を隠すことにもある
DBの種類によってPK戦略は大きく変わる
PostgresではランダムPKは非効率だが、CockroachやSpannerのような分散DBでは、むしろ単調増加キーがホットシャード問題を引き起こす
UUIDv7は上位ビットがソート可能で下位ビットはランダムなので、ノード間の分散性とローカル保存効率を同時に得られる
一般的な非シャーディングDBではランダムキーがB-treeの断片化を引き起こす
ただし範囲検索(range query)が多いならランダムキーは不利になる
結局はワークロード特性に応じて選ぶべきだ
記事の主旨はUUIDv4をPKに使う欠点をよく指摘しているが、提示された整数難読化方式は本番サービスには不向きに見える
小規模DBならUUIDv7が妥当な折衷案だ
生成時刻が露出してほしくないからだ
データ量が多く、UUIDv4のランダム性が性能問題を引き起こさない限り、v4のほうが安全な選択だ
多少の情報露出はあるが、運用上は十分に曖昧だ
例えばAES-128で変換してbase64でエンコードすれば、YouTubeの動画IDのように見せられる
私は技術デューデリジェンスの中で多くの企業を見ているが、素早くシャーディングできる可能性が企業成長の鍵だ
すべてのテーブルにUUIDを持たせておけば、シャーディング時に構造変更なしで拡張できる
わずかな空間・時間の損失よりはるかに大きいスケーラビリティ上の利点がある
結局、複雑なデータモデルだったので、UUIDの有無よりマイグレーション自体が難しかった
私たちのアプリでは整数PKを暗号化してUUIDのように見せている
連番IDが露出すると顧客数の推定や辞書攻撃が可能になるからだ
暗号化されたIDなら、復号に失敗することでスキャンの試みを即座に検知できる
「20億あれば十分だ」という言い方は危険だ
すべてのDBAは、そうした決定から始まった悪夢のような事例を少なくとも1つは持っている
記事では「ランダム値はソートが非効率」としていたが、実際にはバイト順ソートは可能だ
ただしランダムキーは順次挿入ではないため、B-treeの再平衡が頻繁に起きて性能低下を招く
整数PKはインデックスがメモリに収まりやすく、UUIDv4はページアクセスが多くなってレイテンシが大きくなったとのことだ
この記事は問題よりも解決策が先に出た早すぎる最適化に見える
UUIDv4はたいていの場合十分に問題ない
性能問題は実際に起きたときに考えればよい
要するに、PostgresではUUIDv7はv4よりわずかに良い性能を示す
最新バージョンではプラグインなしでもUUIDv7をサポートできる
uuidv7()関数があるが、拡張機能のほうが多機能かどうかはまだ不明だ