主キー設計入門 — AUTO_INCREMENT と UUID をどう選ぶか
概要
テーブルを作るときに必ず決めるのが「主キー(Primary Key)をどう採番するか」です。最も一般的な選択肢は次の2つです。
- AUTO_INCREMENT(連番):
1, 2, 3, ...とDBが自動で振る整数キー - UUID:
550e8400-e29b-41d4-a716-446655440000のような128bitの一意な識別子
どちらも「行を一意に識別する」という役割は同じですが、性能・分散のしやすさ・セキュリティ面で性質が大きく異なります。本記事では入門者向けに、両者の仕組み・トレードオフ・現実的な選び方を整理します。
それぞれの基本
AUTO_INCREMENT(連番)
-- MySQL CREATE TABLE users ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, PRIMARY KEY (id) );
-- PostgreSQL(IDENTITY 列。SERIAL より推奨) CREATE TABLE users ( id BIGINT GENERATED ALWAYS AS IDENTITY, name VARCHAR(255) NOT NULL, PRIMARY KEY (id) );
DBがカウンタを持ち、INSERT のたびに単調増加する整数を割り当てます。
UUID
-- PostgreSQL(v4 = ランダム) CREATE TABLE users ( id UUID NOT NULL DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, PRIMARY KEY (id) );
-- MySQL 8.0 では BINARY(16) 格納が定石 CREATE TABLE users ( id BINARY(16) NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY (id) );
アプリ側でもDB側でも生成でき、事前にDBへ問い合わせなくても一意なIDを作れるのが特徴です。
比較表
| 観点 | AUTO_INCREMENT | UUID |
|---|---|---|
| サイズ | 8バイト(BIGINT) | 16バイト(BINARY(16)) |
| 一意性の保証 | 単一DB内 | 実質グローバル |
| 事前採番 | 不可(INSERT後に確定) | 可能(クライアント側で生成可) |
| 推測しにくさ | 連番なので推測容易 | 推測困難 |
| INSERT時の挿入位置 | 常に末尾 | ランダム(v4) |
| ソート・範囲検索 | 作成順に近い | v4は不可 / v7は可能 |
| 分散DB・シャーディング | 衝突しやすい | 衝突しにくい |
AUTO_INCREMENT のメリット・デメリット
メリット
- サイズが小さくインデックス効率が良い
- 単調増加するため、クラスタインデックス(InnoDB)末尾に追記され、ページ分割が起きにくく書き込みが速い
- 人間が読みやすくデバッグしやすい
デメリット
- URLに
/users/1234のように出すと、件数やIDが推測される(列挙攻撃・情報漏洩のリスク) - 複数DBを統合・シャーディングするときに採番が衝突する
- INSERT するまで値が確定しない
UUID のメリット・デメリット
メリット
- アプリ側で採番でき、DBへ問い合わせる前にIDを確定できる(バルクINSERT・イベント駆動と好相性)
- グローバルに一意で、分散システム・オフライン生成・複数サービス統合に強い
- 連番でないため外部露出しても件数を推測されにくい
デメリット(特に v4)
- サイズが大きい(文字列で持つと36バイトでさらに悪化)
- ランダムな v4 はクラスタインデックス上で挿入位置がバラつき、ページ分割とインデックス肥大化を招く
- 作成順にソートできない
v4 の性能問題と UUID v7
UUID v4 の「ランダムゆえにインデックス末尾に追記できない」問題を解決するのが UUID v7 です。v7 は先頭にミリ秒精度のタイムスタンプを持つため、生成順にほぼ単調増加します。
v4: 550e8400-e29b-41d4-a716-446655440000 ← ランダム、順序性なし v7: 018f3e7a-9c2b-7xxx-xxxx-xxxxxxxxxxxx ← 先頭がタイムスタンプ、生成順に増加
これにより「UUIDの分散・推測困難性」と「連番のインデックス効率」を両立でき、時系列ソートも可能です。新規設計でUUIDを採用するなら、v4よりv7を検討する価値があります。
実装例:MySQL で UUID を効率よく持つ
MySQL では CHAR(36) の文字列ではなく BINARY(16) に変換して格納するのが定石です。
INSERT INTO users (id, name) VALUES (UUID_TO_BIN(UUID(), 1), 'Alice'); -- 第2引数 1 でタイムスタンプ部分を先頭に並べ替え、インデックス効率を改善 SELECT BIN_TO_UUID(id, 1) AS id, name FROM users;
CHAR(36) で持つとサイズは倍以上になり、インデックスも重くなります。
現実的な選び方
- 単一DB・性能とシンプルさ優先 → AUTO_INCREMENT
- URLやAPIにIDを露出し推測されたくない → UUID(または連番の難読化)
- 分散・シャーディング・複数サービス統合・オフライン生成 → UUID(v7推奨)
- 迷ったら: 内部結合は連番、外部公開はUUIDの「ハイブリッド」
CREATE TABLE orders ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, -- 内部結合用 public_id BINARY(16) NOT NULL, -- 外部公開用 PRIMARY KEY (id), UNIQUE KEY uk_public_id (public_id) );
確認方法
-- MySQL: テーブル/インデックスサイズの確認
SELECT table_name,
ROUND(data_length/1024/1024, 1) AS data_mb,
ROUND(index_length/1024/1024, 1) AS index_mb
FROM information_schema.tables
WHERE table_schema = DATABASE();
同じ件数を AUTO_INCREMENT テーブルと UUID v4 テーブルに投入し、index_mb を比較すると v4 の肥大化が確認できます。
注意点
- 「UUIDは遅い」は主に v4 × ランダム挿入 × クラスタインデックス の話。v7や BINARY(16) 格納で大きく改善する
- UUIDを
VARCHAR/CHAR(36)で持つのはサイズ・性能の両面で不利 - 主キーは後から変えづらい。外部露出の有無・分散の予定を初期に見極める
まとめ
- AUTO_INCREMENT は小さく速くシンプル。単一DBのデフォルト候補
- UUID はグローバル一意・事前採番・推測困難が強み。分散や外部露出に強い
- v4の性能問題は v7 と BINARY(16) 格納で緩和できる
- 「内部は連番・外部はUUID」のハイブリッドも実務では有力
主キー設計は後から直しにくい土台です。要件(外部露出・分散・書き込み量)を先に見極めて選びましょう。
