「主キーに何を使うか?」というのは、データベース設計における永遠のテーマですよね。
駆け出しの頃は「とりあえず `SERIAL` か `BIGSERIAL` でいいや」となりがちですが、システムが大きくなって、分散環境での運用や、IDの推測を防ぐ必要が出てくると、そうも言っていられなくなります。
そこで頼りになるのが UUID (Universally Unique Identifier) です。今日は、PostgreSQLでUUIDを賢く使うための勘所を、現場の実践的な視点から解説します。
—
なぜ「連番」から「UUID」に移行するのか
連番(`SERIAL`)には、致命的な弱点が2つあります。
1. 推測が容易: `…/users/123` というURLがあれば、次は `124` だと誰でも分かります。これはセキュリティ的に非常に危険です。
2. マージが困難: 複数のDBサーバーや、オフラインで生成したデータをマージする際、IDの衝突を避けるための調整が地獄になります。
UUIDなら、中央集権的な管理なしに、世界中でほぼ確実に重複しないIDを生成できます。これが分散システムやマイクロサービスにおいて、UUIDが標準的に使われる理由です。
PostgreSQLでのUUID活用術
PostgreSQLでUUIDを使うのは、驚くほど簡単です。`pgcrypto` 拡張(PostgreSQL 13以降は標準機能でOK)を使い、データ型として `uuid` を指定するだけです。
1. テーブル定義の例
まずは、シンプルにUUIDを主キーにするテーブルの定義を見てみましょう。
CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
username text NOT NULL,
created_at timestamptz DEFAULT now()
);
見ての通り、`DEFAULT gen_random_uuid()` を指定するだけで、レコード挿入時に自動的にランダムなIDが割り振られます。アプリケーション側でIDを生成して渡す必要すらありません。
2. パフォーマンスの落とし穴:インデックス問題
ここで一つ、ベテランとしてアドバイスしておきたいことがあります。
UUIDを主キーにすると、インデックス(B-tree)の断片化が起こりやすいんです。`gen_random_uuid()` で生成されるUUID(バージョン4)は完全にランダムなので、インデックスの「新しいノード」がランダムな位置に書き込まれます。これによってページ分割が頻発し、挿入性能がガクンと落ちることがあります。
もし、「IDの生成順序」と「挿入順序」をある程度一致させたいなら、UUID v7 の利用を検討してください。
- UUID v4: 完全ランダム。セキュリティ重視。
- UUID v7: タイムスタンプを含む。ソート可能でインデックス効率が良い。
PostgreSQL 17以降であれば `gen_random_uuid()` が賢くなっており、状況によってはv7相当の動作を選択することも可能ですが、そうでなければ拡張ライブラリ(`pg_uuidv7`など)の導入を検討する価値は十分にあります。
現場でよくある「うっかり」
最後に、実務でよく見る失敗をいくつか共有します。
- 文字列型 (`text` や `varchar`) で保存してしまう:
UUIDは必ず `uuid` 型を使ってください。`text` 型で保存すると、インデックスサイズが倍以上に膨れ上がり、クエリ速度が目に見えて低下します。「見かけは文字でも、内部では128ビットの数値」として扱うのがPostgreSQLの流儀です。
- クエリの可読性:
IDが `550e8400-e29b-41d4-a716-446655440000` となると、デバッグ時に「これどのユーザーだっけ?」となりがちです。開発環境では、デバッグツールでIDからユーザー名を引けるようにしておくなど、運用でカバーする仕組みを作っておくとチームの生産性が上がります。
—
結び
UUIDは「とりあえず使っておけば安心」な万能ツールではありません。インデックスの特性や、IDとしての性質(ランダムか、ソート可能か)を理解して使い分けることで、初めてデータベースの真価を発揮します。
「何となく `BIGSERIAL`」から卒業して、システムに最適なID設計ができるようになると、設計の自由度がグッと広がりますよ。
もし「UUIDのインデックスサイズをさらに小さくしたい!」なんていうニッチな相談があれば、またいつでも聞いてください。データベースの深いところまで、一緒に掘り下げていきましょう。
コメント