UUID vs bigint 主キー — 性能差の実体と uuidv7

読了 4分

単一 DB で始まる普通のサービスなら、主キーは bigint GENERATED ALWAYS AS IDENTITY が既定値で、UUID が必要な要件(分散生成・外部露出)があるときだけ uuidv7 を使います。 基礎第 2 回で表 1 枚にまとめた結論ですが、この記事はその根拠、つまり「UUID は遅い」という話の実体が何で、uuidv7 が何を変えたのかを掘る深掘り編です。

bigint が既定値である理由 #

  • サイズ: bigint 8 バイト vs UUID 16 バイト。PK 自体より重要なのは波及です。PK はすべての二次インデックスに含まれ、FK カラムとして子テーブルごとに複製されます。PK が 2 倍ならインデックス全般が一緒に大きくなり、キャッシュに収まるインデックスの量がその分減ります。
  • 挿入の局所性: IDENTITY の採番は単調増加なので、新しい行がいつも B-tree の右端に付きます。書き込みが触るページが少数に保たれ、キャッシュヒットが高く、WAL も少なく済みます。
  • 可読性: ログと障害対応で id = 4213 は扱いやすいです。些細に見えても、運用で毎日体感する差です。

「UUID は遅い」の実体: v4 のランダム性 #

UUID 自体が遅いのではなく、UUID v4 のランダム性が遅いのです。v4 は完全ランダムなので、新しい行が B-tree のどこにでも刺さります。その結果が 3 つです。挿入のたびにインデックスの別々のページを触るのでキャッシュヒットが崩れ(インデックスがメモリより大きくなった瞬間、挿入のたびにディスク読み取りが混ざります)、ページ分割が頻発してインデックスが膨らみ、触ったページごとに記録が必要なので WAL の量が増えます。数百万行の規模から挿入スループットの差が実測で現れる理由です。範囲検索(「新規登録順」)を PK でできないことも、おまけで付いてくる損失です。

uuidv7: ランダム性の問題を時系列順で解消 #

uuidv7 は先頭 48 ビットにミリ秒のタイムスタンプを入れ、残りを乱数で埋めます。生成時刻順に並ぶ UUID なので、新しい行が bigint のようにインデックスの右端近くに固まって付きます。v4 のキャッシュ・ページ分割・WAL の問題はほとんど消え、PostgreSQL 18 からは組み込み関数で使えます。

orders テーブル作成
CREATE TABLE orders (
    id         uuid PRIMARY KEY DEFAULT uuidv7(),
    user_id    bigint NOT NULL REFERENCES users (id),
    created_at timestamptz NOT NULL DEFAULT now()
);

残るトレードオフも正確に知っておく必要があります。16 バイトというサイズはそのままなのでインデックス・FK への波及は変わらず、生成時刻が ID に露出します(v4 を選ぶ理由が「推測不可」だったなら、v7 は時刻の部分が推測可能になる折衷です。ほとんどのサービスでは問題になりませんが、生成時刻自体が機微なドメインでは考慮の対象です)。連番の露出で件数を推測される bigint の問題は、こちらにはありません。

判断基準 #

条件推奨
単一 DB、内部サービス、普通の規模bigint IDENTITY
クライアント・複数サービスで ID を先に生成uuidv7
URL などに露出する ID の推測防止uuidv7(または下の併用)
連番も時刻も露出不可UUID v4 + 性能コストの受け入れ

併用設計も実務の有力な答えです。内部の PK・FK・結合は bigint のままにして、外部露出用の public_id uuid DEFAULT uuidv7() UNIQUE カラムを別に持つ形です。結合性能と露出の安全を両方取る代わりに、カラムとインデックスが 1 つずつ増えるコストを払います。API のレスポンスと URL には public_id だけを出せば十分です。

最後によくある誤解を 1 つ。「いつかシャーディングするかもしれないから最初から UUID」は、ほとんどの場合過剰設計です。その時が来たらパーティショニングや併用設計への移行の道筋があり、来ないかもしれない未来のために現在のすべての結合とインデックスに税金を払うのは、順序が逆の決定です。

まとめ #

  • 既定値は bigint IDENTITY です。サイズの波及(すべてのインデックス・FK)と挿入の局所性が根拠です。
  • 「UUID は遅い」の実体は v4 のランダム挿入です。キャッシュの崩壊・ページ分割・WAL の増加という、実測できるメカニズムです。
  • uuidv7 は時系列順でその問題をほぼ解消し、PostgreSQL 18 に内蔵されました。16 バイトのサイズと生成時刻の露出は残る折衷です。
  • 分散生成・外部露出の要件があれば uuidv7、両方取りたければ内部 bigint + 外部 uuidv7 の併用設計です。
  • 未来のシャーディングのための先回り UUID はほとんど過剰です。現在のワークロードで決めてください。
X