はじめに
DB の主キー設計では、AUTO_INCREMENT と UUID のどちらを使うかで運用がかなり変わります。
どちらにも明確な向き不向きがあるので、メリットとデメリットを整理します。
この記事では、性能・運用・開発の観点から違いを整理し、どちらを選ぶべきかの判断材料をまとめます。
主キーに求めるもの
主キーはユニークであれば良いだけでなく、運用や開発フローに影響を与えます。
以下のどれを優先するかで、選択が変わります。
- 書き込み性能とインデックス効率
- 分散環境や複数システムとの統合のしやすさ
- 外部公開時の安全性
- 調査のしやすさと人間可読性
- テストや開発の扱いやすさ
AUTO_INCREMENT の特徴
メリット
- 値が短く、インデックス効率が良い
- 連番なので挿入時のページ分割が起きにくく、書き込み性能が安定しやすい
- 人間が扱いやすく、ログ調査や手動確認がしやすい
- 主キーが順序を持つため、作成順の追跡がしやすい
デメリット
- ID を推測しやすく、URL 直打ちで他データに到達しやすい
- 複数 DB や分散環境で重複回避の設計が必要になる
- 移行時やマージ時に採番戦略を揃える必要がある
- テストで採番の状態に依存しやすく、固定 ID 前提の検証が書きづらい
- 取り込み順やリトライで欠番が出ることがあり、連番前提の仕様にすると破綻しやすい
UUID の特徴
メリット
- 一意性が高く、分散環境でも採番しやすい
- 連番ではないため、外部公開時に件数や順序を推測されにくい
- 別システム間のデータ統合で衝突しにくい
- 発行場所を気にせず事前生成できるため、非同期処理と相性が良い
デメリット
- 値が長く、インデックスサイズが大きくなりやすい
- ランダム性の高い UUID は B-Tree インデックスが断片化しやすい
- 可読性が低く、調査時に扱いづらい
- ID がランダムなためテストで値を固定しづらく、テストデータの準備が面倒になる
- 文字列として扱うと比較や検索が遅くなりやすい
パフォーマンス観点での注意
UUID を使う場合は、文字列のまま保存するよりバイナリ形式の方が効率的です。
また、UUIDv4 のような完全ランダム型より、時系列性を持つ UUIDv7 の方がインデックス効率は改善しやすいです。
パフォーマンスの違いをもう少し具体的に
現在の一般的な DB 実装では、AUTO_INCREMENT は書き込みと検索の両面で有利になりやすい傾向があります。
理由は主に「連番によるインデックスの連続性」と「値の短さ」です。
AUTO_INCREMENT の場合
- B-Tree の末尾に順序よく挿入されるため、ページ分割が起きにくい
- インデックスが小さく収まるので、メモリキャッシュに乗りやすい
- クラスタ化インデックスを使う DB では、データの物理配置も連続しやすい
UUID の場合
- UUIDv4 のようなランダム値は、挿入位置が散らばりやすくページ分割が増える
- インデックスサイズが大きくなり、検索時の I/O が増えやすい
- 文字列保存だと比較コストが高くなり、二次インデックスの負担も増える
ただし、UUID でも性能を改善する手段があります。
- UUIDv7 のように時系列性を持つ形式を使う
- 文字列ではなくバイナリ形式で保存する
- 外部公開用にだけ UUID を持ち、内部主キーは AUTO_INCREMENT を使う
実務的には、読み書きが多いテーブルほど AUTO_INCREMENT が有利で、UUID は分散性と引き換えに性能を少し犠牲にする設計だと理解しておくと判断しやすいです。
UUID のパフォーマンス問題で実際に起きやすい例
実体験がなくても、構造的に起こりやすいパターンは説明できます。
ここでは実務で遭遇しやすいケースを具体化します。
UUID を VARCHAR(36) で保存してインデックスが肥大化する
InnoDB ではセカンダリインデックスに主キーが含まれます。
主キーが UUID 文字列だと、セカンダリインデックスの全てに長い主キーが入り、インデックス全体が肥大化します。
例として、以下のような差が出ます。
- BIGINT の主キーは 8byte
- UUID 文字列は 36byte で、実際はさらにオーバーヘッドがある
セカンダリインデックスが 5 本あるとすると、(36-8) × レコード数 × 5 本分の差が生まれます。
結果としてバッファプールに乗り切らず、I/O 増加と読み取りレイテンシ悪化に繋がることがあります。
UUIDv4 でページ分割が増える
UUIDv4 はランダム性が高いため、B-Tree の中間ページに挿入されやすくなります。
その結果、ページ分割が増え、書き込み I/O が増加します。
挿入位置のイメージは以下の通りです。
| 方式 | 挿入位置 |
|---|---|
| AUTO_INCREMENT | 常に末尾 |
| UUIDv4 | 木のどこか |
高頻度書き込みテーブルでは、AUTO_INCREMENT と比べて書き込み効率が落ちるケースが出やすいです。
文字列 UUID で JOIN コストが増える
文字列比較は整数比較より CPU コストが高く、キャッシュ効率も落ちやすくなります。
大量 JOIN や GROUP BY を多用するクエリでは、UUID 文字列をキーにすると一気に重くなることがあります。
なんとなく UUID が原因で遅くなる
分散採番や外部公開がないのに、なんとなく UUID 文字列を主キーにする例は珍しくありません。
その結果として、インデックスサイズが大きくなり、読み書きがじわじわ遅くなるケースがあります。
改善パターンの例
UUID を採用する場合でも、性能を落としにくい選択肢があります。
- UUID を BINARY(16) で保存する
- UUIDv7 のように時系列性のある形式を使う
- 内部主キーは BIGINT、外部公開だけ UUID を使う
DB で UUID 型が使える場合の扱い
UUID 型がある DB では、それを使うのが基本的に自然です。
ただし、DB によって状況が違うため、使い方は分けて考える必要があります。
PostgreSQL の場合
- UUID 型があり、内部は 16byte として扱われる
- 文字列より効率が良いので、UUID を使うなら UUID 型が第一候補
MySQL の場合
- UUID 型はなく、実体は CHAR(36) か BINARY(16) を選ぶ
- 文字列なら扱いやすいが、インデックス肥大化が起きやすい
- BINARY(16) は効率が良いが、可読性や運用の扱いには工夫が必要
どちらの DB でも、UUID 型や BINARY を使うと改善する余地はあります。
ただし、連番の AUTO_INCREMENT が持つ性能優位が完全に消えるわけではない点は押さえておくと判断しやすいです。
外部公開するなら ID は2つ持つのが安全
AUTO_INCREMENT をそのまま API や URL で公開すると、連番から他データを推測されやすくなります。
そのため実務では、内部用の主キーと外部公開用の識別子を分ける設計がよく使われます。
- 内部ID: AUTO_INCREMENT を主キーとして使う
- 外部ID: UUID など推測しにくい一意IDを別カラムで持つ
この構成にすると、DB 内部では連番の性能メリットを維持しつつ、外部公開時の安全性も確保できます。
開発とテストの観点
AUTO_INCREMENT は基本的に DB 側が値を決めるため、テスト時に ID が確定しないのが悩みになりがちです。
特に、固定 ID を前提にした検証やスナップショットテストとは相性が良くありません。
UUID もランダム性が高いため、テストで値を固定しづらいという意味では似た問題を抱えます。
ただし、生成をアプリ側で管理できる場合は、テスト中だけ固定値を生成する仕組みを作りやすいです。
どちらを使う場合でも、テストでは以下のような対策が有効です。
- 生成関数をラップして、テスト時に固定値を返す
- テストデータを明示的に投入して検証する
- ID に依存しないテスト設計に寄せる
運用上の小さな違い
AUTO_INCREMENT は障害調査や問い合わせ対応のときに、人間が ID を読み取って追いやすいのが利点です。
一方で、UUID はログや画面に出すと長くなり、見落としや打ち間違いが増えます。
逆に、UUID は複数環境をまたいだデータ連携や、事前に ID を発行するワークフローに向いています。
API が非同期で動く構成や、複数サービスが同じ ID を共有する構成だとメリットが大きくなります。
どう選ぶか
- 単一 DB で高頻度書き込みが中心なら AUTO_INCREMENT
- 分散システム連携や外部公開 ID が必要なら UUID
- 外部公開があるなら、内部主キーと公開用IDを分ける前提で設計する
- テストのしやすさと調査性を重視するなら AUTO_INCREMENT が無難
- 事前に ID を発行したい、複数サービスで ID を共有したいなら UUID が適している
まとめ
速度と運用の単純さを優先するなら AUTO_INCREMENT。
分散性と公開時の安全性を優先するなら UUID。
公開 API や URL を持つサービスでは、内部主キーと外部公開IDの2系統にしておくと運用が安定します。