Query Go
主キーの選び方 — UUID v4 が遅い理由と UUID v7 / BIGINT / 日付
チューニング

主キーの選び方 — UUID v4 が遅い理由と UUID v7 / BIGINT / 日付

ランダム UUID v4 が B-Tree を肥大化させる理由と、連番・UUID v7 が速い理由。

主キーの選び方 — UUID v4 が遅い理由と UUID v7 / BIGINT / 日付 diagram

問題: ランダム UUID はインデックスに優しくない

主キーに UUID v4(完全ランダム)を使うと、新しい行の挿入位置が毎回インデックス上でバラバラになります。B+Tree は「ソート済み」のデータ構造なので、バラバラな位置への挿入は:

  • ページの分割(page split)を多発させる
  • キャッシュヒット率が下がる(直近挿入したページが次の挿入では別物)
  • インデックスが肥大化する(断片化+空き領域)
  • ディスク I/O のランダム化で書き込みが遅くなる

一方、連番 BIGINT や時刻のような単調増加するキーなら、挿入は常にインデックスの右端のリーフページに集中します。同じページに連続で書き込むためキャッシュが有効に働き、ページ分割もほぼ発生しません。

なぜ数値・日付のキーは速いのか

自然数や created_at のような時系列カラムは、新しい値 = 大きい値という性質を持ちます。B+Tree は昇順で並ぶので、新規挿入は必ず「右端のリーフページ」に入ります。

結果として:

  • 挿入中のページは常にバッファプール上にある(ホットページ)
  • ページ分割は右端のページでしか起こらない(最小コスト
  • インデックスの物理順序 ≈ 論理順序なので、範囲スキャンも高速

さらにアプリ側でも、時系列キーは「最新 N 件」「日付範囲」のクエリと相性が良く、ORDER BY id DESC LIMIT 10 のようなクエリがそのままインデックスを使って処理できます。

UUID v7 — 時系列+ランダムのハイブリッド

UUID v7(RFC 9562, 2024年標準化)は、先頭 48bit にUnix ミリ秒タイムスタンプを埋め込み、残りをランダムにした UUID です。連番キーの「単調増加」と UUID の「分散生成可能 / 衝突しない」を両立します。

  • 挿入は v4 と違い時系列順に並ぶ → B+Tree の右端に集中
  • グローバルに一意、事前に ID 発行可能(分散システム向き)
  • 主キーから大まかな生成時刻が分かる(ソート可能)
  • 推測しにくさは v4 より弱い(タイムスタンプが見える)ので、URL に露出させる ID には不向き

実装: PostgreSQL は拡張 pg_uuidv7(17 以前)または組込み uuidv7()(18+)を利用。MySQL / SQLite はアプリ側生成(各言語ライブラリ)。

sql
-- PostgreSQL (pg_uuidv7 拡張)
CREATE EXTENSION pg_uuidv7;
CREATE TABLE orders (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- アプリ側生成 (例: Node.js)
-- import { v7 as uuidv7 } from 'uuid';
-- const id = uuidv7();

クラスタ化インデックスがあるとさらに影響が大きい

MySQL (InnoDB) は主キーが必ずクラスタ化インデックス(テーブルデータそのものが主キー順に物理配置される)になります。SQL Server は主キーがデフォルトでクラスタ化されますが、PRIMARY KEY NONCLUSTERED で非クラスタ化にも設計できます。クラスタ化されている状況では、ランダム UUID 主キーはテーブル本体の書き込みまでランダム化します。影響はインデックスだけでなくテーブルサイズ全体に及び、数倍のディスク容量差が出ることもあります。

PostgreSQL はヒープ構造(クラスタ化なし)なので影響は相対的に小さいですが、それでもインデックスの肥大化は避けられません。

実務での選び方

  • 単一 DB で完結するアプリ: BIGINT AUTO_INCREMENT / BIGSERIAL が最速で最小。迷ったらこれ
  • 分散システム / マイクロサービスで ID を事前生成したい: UUID v7。ランダム性能問題を回避しつつ分散生成可能
  • URL に露出する ID で推測不可にしたい: UUID v4 を使うが、主キーは別に BIGINT や UUID v7 を立てて、v4 は外部公開専用の別カラムにする(内部結合は高速なまま)
  • レガシー環境で v7 が使えない: ULIDKSUID など v7 相当の時系列 ID ライブラリで代替

既に UUID v4 を主キーに使っているテーブルの性能に悩んでいる場合は、インデックスを再構築(PostgreSQL: REINDEX, MySQL: OPTIMIZE TABLE)するだけでも一時的に改善することがあります。