主キーの選び方 — UUID v4 が遅い理由と UUID v7 / BIGINT / 日付
ランダム UUID v4 が B-Tree を肥大化させる理由と、連番・UUID v7 が速い理由。
問題: ランダム 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 はアプリ側生成(各言語ライブラリ)。
-- 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 が使えない: ULID や KSUID など v7 相当の時系列 ID ライブラリで代替
既に UUID v4 を主キーに使っているテーブルの性能に悩んでいる場合は、インデックスを再構築(PostgreSQL: REINDEX, MySQL: OPTIMIZE TABLE)するだけでも一時的に改善することがあります。
