先月、シニアエンジニアが遅いクエリのデバッグに2日費やすのを見た。犯人?5,000万行のテーブルにUUIDを主キーとして使っていた。インデックスは必要な2倍のサイズで、レンジスキャンは痛いほど遅かった。
UUIDは多くのチームにとってデフォルトの選択になっている。でもデフォルトは疑うべきだ。
UUIDは16バイト。整数は4-8バイト。些細に聞こえるが、計算するとそうでもない。
5つの外部キーを持つ1億行のテーブル:
ストレージ2倍。メモリ圧力2倍。I/O 2倍。
でも大きな問題はインデックスの断片化だ。UUIDv4はランダムなので、挿入がB-treeのあちこちに着地する。ページ分割が絶えず起きる。
シーケンシャルBIGINT挿入:
[1] [2] [3] [4] [5] [6] [7] [8] → 末尾に追加
B-treeはコンパクトのまま。ページは左から右に埋まる。
ページ分割なし。素晴らしいキャッシュローカリティ。
ランダムUUIDv4挿入:
[a7f...] [3b2...] [f91...] [0d4...] → あちこちに散乱
B-treeが断片化。ページが絶えず分割。
ランダムI/O。悪いキャッシュローカリティ。
数百万行の本番PostgreSQLセットアップで、BIGINTの挿入はUUIDv4より一貫して30-40%速かった。レンジスキャン?BIGINTは劇的に速かった。UUIDv7は書き込みでBIGINTに近く、それが重要な理由だ。
UUIDは特定の問題を解決する:
分散システム — 10個のデータベースシャード間でオートインクリメントを調整できない。UUIDは調整なしでどこでも生成できる。
パブリックAPI — シーケンシャルIDは情報を漏らす。GET /api/users/1000で、攻撃者は約1000人のユーザーがいることを知り、/api/users/1001、/api/users/1002を試す... 典型的なIDOR脆弱性。GET /api/users/550e8400-e29b-...では、何もわからず他のユーザーを列挙できない。
マージ衝突 — データベース間でデータを同期?シーケンシャルIDは衝突する。UUIDはしない。
これらがユースケースに当てはまらないなら、おそらくUUIDは必要ない。
本番で実際に効くのはこれ:両方使う。
内部クエリは高速な整数を使う。APIは安全なUUIDを公開。両方の良いとこ取り。
UUIDv7は時間順序付き。最初の48ビットはタイムスタンプなので、挿入は整数のようにシーケンシャルだがUUIDのようにグローバルユニーク。
UUIDv4 (550e8400-e29b-41d4-a716-446655440000): ランダムビット、順序なし。挿入ごとにB-treeインデックスを断片化。
UUIDv7 (019006f3-2e47-7000-8000-000000000001): 最初の48ビットがUnixタイムスタンプ(ms)、自然にソート。挿入は整数のようにインデックスの末尾に。
ベンチマークでは、UUIDv7はユニーク性保証を保ちながら挿入パフォーマンスのほとんどを回復する。
挿入パフォーマンス(100万行): BIGINTがベースラインで100%。UUIDv7は92% — ほぼ同等。UUIDv4は68% — 大きなペナルティ。
UUIDが必要なら、v7を使え。PostgreSQL 17+はネイティブサポート。古いバージョンは、アプリケーションコードで生成するかpg_uuidv7エクステンションを使う。
キーはどちらが「良い」かではない。何をトレードオフしているか理解することだ。意図的に選択しろ。
普遍的なIDはない。アクセスパターンに適したものだけがある。
— blanho
また間違ったものを最適化した。パフォーマンスを推測しても絶対うまくいかない理由がこれ。
2つのサーバー、1つのリソース、ゼロの調整。こうやって壊れる。
1,000万件の保存検索がある。新しいアイテムが来る。1,000万件のクエリを実行せずにすべてのマッチをどう見つける?
CREATE TABLE orders (
-- 内部: 高速ルックアップ、コンパクトなインデックス
id BIGSERIAL PRIMARY KEY,
-- 外部: 公開しても安全、列挙不可
public_id UUID DEFAULT gen_random_uuid() UNIQUE,
customer_id BIGINT REFERENCES customers(id), -- FKはBIGINT
created_at TIMESTAMPTZ DEFAULT now()
);
-- APIルックアップ用インデックス
CREATE INDEX idx_orders_public_id ON orders(public_id);
-- 内部クエリ(速い、整数PKを使用):
SELECT * FROM orders WHERE id = 12345;
-- APIクエリ(安全、UUIDを使用):
SELECT * FROM orders WHERE public_id = '550e8400-e29b-...'::uuid;