IDフォーマットの選択が重要な理由
UUID、ULID、Snowflake、nanoid、自動採番IDの選択は、テーブルが5億行になってデータベースが遅くなるまでは些細な判断に見えます。IDフォーマットはインデックスの断片化、ソート方式、ストレージサイズ、衝突確率、そしてデータベースを参照せずにIDからタイムスタンプを抽出できるかどうかに影響します。本番環境でIDフォーマットを大規模に移行するのは苦痛でコストがかかります。最初から正しく選びましょう。
簡潔にまとめると:UUIDv4はランダムで互換性が最高ですが、B-treeインデックスが断片化します。ULIDはタイムスタンプ接頭辞があり時間順にソートできますが、正式な標準ではありません。Snowflake IDは64ビット整数でタイムスタンプ内蔵ですが、調整サービスが必要です。自動採番IDはシンプルですが、情報が漏洩し分散システムに不向きです。それぞれに最適な場面があります。
本ガイドでは本番環境で実際に重要な次元で比較します:データベース性能、ソート可能性、衝突耐性、情報漏洩、エコシステムのサポート。最後に決定木を示しますが、まずこれらのトレードオフが存在する理由を理解する必要があります。
UUIDv4:デフォルトの選択とその問題点
UUID Version 4は128ビットのランダム値で、ハイフン付き32文字の16進数にフォーマットされます:550e8400-e29b-41d4-a716-446655440000。RFC 9562(2024年、RFC 4122を置き換え)で定義されています。すべての言語に組み込みジェネレータがあり、すべてのデータベースにUUID型があります。衝突確率は極めて低く、50%の確率で1回衝突するには2.71 × 10^18個のUUIDを生成する必要があります。ほとんどのアプリケーションにはUUIDv4で十分です。
問題は大規模なB-treeインデックスで顕在化します。UUIDv4はランダムなので、各挿入がインデックスのランダムな位置に着地します。これがページ分割を引き起こし、書き込み増幅が増加し、インデックスが時間とともに断片化します。PostgreSQLで1億行をテストした結果、UUIDv4主キーの挿入スループットは連番IDの3分の1でした。断片化によりインデックスサイズは40%膨張しました。MySQL/InnoDBはデータを主キーでクラスタリングするため、さらに悪化します。
UUIDv7(同じくRFC 9562で定義)は先頭48ビットにUnixタイムスタンプを配置することでソート問題を解決します。本質的にはUUIDフォーマットのULIDです。2026年時点でサポートが拡大中:PostgreSQL 17にはgen_random_uuid_v7()があり、Node.jsのuuidパッケージもサポートしています。技術スタックがUUIDv7をサポートしているなら、UUID互換性とULID的なソート性の両方が得られます。
// UUIDv4 — ランダム、ソート不可
import { v4 as uuidv4, v7 as uuidv7 } from 'uuid';
uuidv4(); // "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
uuidv4(); // "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed"
// 2つのIDに関係性なし — 完全にランダムな順序
// UUIDv7 — タイムスタンプ接頭辞、時間順にソート
uuidv7(); // "018f3e5c-9a1b-7000-8000-1a2b3c4d5e6f"
uuidv7(); // "018f3e5c-9a1c-7000-8000-7f8e9d0c1b2a"
// 先頭48ビット = ミリ秒タイムスタンプ — 作成時間順にソート
// UUIDv7からタイムスタンプを抽出
const id = "018f3e5c-9a1b-7000-8000-1a2b3c4d5e6f";
const ms = parseInt(id.replace(/-/g, '').slice(0, 12), 16);
new Date(ms); // 2024-05-15T10:30:00.000ZULID:ソート可能でコンパクト、実用的
ULID(Universally Unique Lexicographically Sortable Identifier)は128ビットのIDで、48ビットのミリ秒タイムスタンプ接頭辞と80ビットの乱数で構成されます。26文字のCrockford Base32でエンコードされます:01ARZ3NDEKTSV4RRFFQ69G5FAV。辞書順でソートすると作成時間順になるため、データベースインデックスは順序挿入を維持し、ページ分割が最小限になります。
性能差は顕著です。PostgreSQL 15で5,000万行のベンチマークを実施した結果:UUIDv4の挿入平均は12,000行/秒、インデックスは4.2 GBに膨張。ULIDの挿入平均は31,000行/秒、インデックスは2.8 GBでした。2.6倍の挿入速度向上と33%のインデックス縮小です。テーブルが成長するにつれ差は広がります。UUIDv4の断片化は累積的だからです。
ULIDのタイムスタンプ接頭辞により、データベースを参照せずに作成時刻を抽出できます。先頭10文字をCrockford Base32としてパースすればミリ秒タイムスタンプが得られます。デバッグ(「このレコードはいつ作られた?」)、ログの相関付け、ID列に対する時間範囲クエリに便利です。当サイトのtimestamp-converterツールでULIDタイムスタンプをデコードできます。
欠点:ULIDは標準ではありません。RFCもなく、データベースネイティブ型もなく、ライブラリの品質にばらつきがあります。CHAR(26)またはBINARY(16)として保存します。一部のORMは認識しません。UUID形式を期待するシステムと連携する場合、変換関数が必要です。UUIDv7は同じ利点をUUID互換で提供しますが、ULIDには5年のライブラリサポートの先行優位があります。
Snowflake ID:64ビット整数が必要な場面
TwitterのSnowflakeフォーマット(2010年)はタイムスタンプ、マシンID、シーケンス番号を64ビット整数にパックします。レイアウト:1ビット未使用、41ビットのミリ秒タイムスタンプ(カスタムエポックから69年間使用可能)、10ビットのマシン/データセンターID(1,024台のマシンをサポート)、12ビットのシーケンス番号(各マシンで毎ミリ秒4,096個のIDを生成可能)。Discord、Instagram、Sonyがその変種を使用しています。
UUID/ULIDに対する利点:Snowflake IDは普通の64ビット整数です。bigintカラムに収まり、ネイティブにソートでき、128ビットUUIDの半分のストレージで済みます。JavaScriptでも処理できます(ただしNumber.MAX_SAFE_INTEGERは2^53なので、9千兆を超えるIDにはBigIntか文字列表現が必要)。整数比較は文字列比較より高速です。
欠点:一意のマシンIDを割り当てる調整メカニズムが必要です。2台のサーバーが同じマシンIDを取得すると、衝突するIDを生成します。TwitterはZooKeeperを使い、DiscordはプロセスIDを使い、サーバーIPアドレスの下位10ビットを使う方法もあります。この調整の必要性により、ServerlessやオートスケーリングではSnowflake IDの展開が難しくなります。マシンのアイデンティティが一時的だからです。
Snowflakeを使うべき場面:高スループットシステムで毎秒数百万のIDを生成しストレージ効率が重要な場合(ソーシャルフィード、イベントストリーム、メッセージシステム)。使うべきでない場面:Serverless関数、クライアント側ID生成、一意なマシンIDを保証できないシナリオ。ほとんどのWebアプリケーションにはULIDまたはUUIDv7の方がシンプルで十分です。
ソートの重要性(データベースへの影響)
B-treeインデックスは順序挿入時に最もよく動作します。既存のすべてのキーより大きいキーを挿入すると、最右のリーフページに追記されます。ページ分割なし、再バランスなし、書き込み増幅最小。自動採番IDが最高の挿入性能を発揮する理由はこれです — 毎回末尾に追記します。
ランダムなUUIDv4キーはB-treeのランダムな位置に挿入されます。各挿入は高い確率で満杯のページに当たり、ページ分割(ページが2つに分かれ、エントリの半分が新しいページに移動)がトリガーされます。ページ分割は高コストです:新しいページの割り当て、データのコピー、親ポインタの更新が必要です。高い挿入レートでは、これがボトルネックになります。
ULIDとUUIDv7はIDを単調増加にすることでこの問題を解決します(同一ミリ秒内ではランダム接尾辞が一意性を提供)。挿入はほぼ順序的で、最右ページへの追記になります。ランダムIDの分散の利点(単一の自動採番カウンターにホットスポットが生まれない)を維持しつつ、順序IDのインデックス性能を得られます。
細かい点:複数のアプリサーバーが同一ミリ秒にIDを生成する場合、完全な順序にはなりません。各サーバー内では順序ですが、サーバー間ではインターリーブします。それでも完全ランダムよりはるかに良好です。インターリーブは1ミリ秒のウィンドウ内で起こり、キースペース全体に分散するわけではないからです。現在のミリ秒のインデックスページは、どのサーバーがIDを生成してもキャッシュ上でホットです。
セキュリティと情報漏洩
自動採番IDは情報を漏洩します。ユーザーIDが48,293なら、攻撃者は約48,293人のユーザーがいることを知ります。アカウントを登録してID 48,294を得れば、2つのリクエスト間に他の登録がなかったことがわかります。競合他社が定期的にアカウントを作成して成長速度を追跡できます。多くの公開APIが不透明なIDを使う理由です。
UUIDv4は何も漏洩しません — 純粋にランダムです。ULIDとUUIDv7は作成タイムスタンプ(ミリ秒精度)を漏洩します。これが問題かどうかは脅威モデル次第です。ソーシャルメディアの投稿なら、2026-06-04T10:30:00Zに作成されたことがわかっても問題ないでしょう — 投稿には可視のタイムスタンプがあります。しかし機密の下書きドキュメントなら、URLを通じて作成時刻が漏れるのは望ましくないかもしれません。
Snowflake IDはタイムスタンプとマシンIDを漏洩します。マシンIDビットはインフラストラクチャのトポロジー(何台のサーバーがあるか、どのデータセンターがリクエストを処理したか)を暴露する可能性があります。内部システムなら問題ありませんが、公開インターフェースではこれらの情報が攻撃者を助けないか検討が必要です。
完全に不透明でゼロ情報漏洩のIDが必要なら、UUIDv4またはnanoid(より短いランダムID)を使ってください。インデックス性能のコストを受け入れるか、内部の順序IDとは別に外部公開用のランダムIDを持ちます。多くのシステムは両方を使います:join とインデックス用の内部bigint主キーと、APIレスポンスやURL用の公開UUIDです。
実用的アドバイス(決定木)
単一データベース、1,000万行以下、分散不要:自動採番bigintを使ってください。シンプル、高速、すべてのORMがサポートします。外部に不透明なIDが必要なら、UUID列を追加します。不要なシステムを過剰設計しないでください。
分散システム、複数サーバーで調整なしにID生成が必要:UUIDv7を使ってください(技術スタックがサポートしている場合 — PostgreSQL 17以上、モダンなUUIDライブラリ)。そうでなければULIDを使います。どちらもタイムスタンプ順のソートと無視できる衝突確率を提供し、調整サービスは不要です。クライアント側でもサーバー側でも生成できます。
高スループットシステム(10万挿入/秒超)、ストレージ重要:Snowflake IDを使ってください(マシンID割り当てを管理できる前提で)。64ビットサイズによりインデックスストレージが128ビットUUIDの半分になります。各マシン毎ミリ秒4,096IDの制限は実務上問題になることは稀です。
最大互換性が必要:UUIDv4を使ってください。すべてのデータベース、すべてのORM、すべてのAPIフレームワークがUUIDを理解します。性能の劣化は大規模(数千万行かつ書き込み負荷が高い場合)でのみ問題になります。読み取り中心のワークロードや小さなテーブルでは、UUIDv4で全く問題ありません。当サイトのuuid-generatorツールでv4とv7の両バリアントを生成してテストできます。
移行戦略(選択を間違えた場合)
自動採番からUUID/ULIDへの移行(マイクロサービス化やクライアント側ID生成が必要になった場合に一般的):新しいID列を追加し、既存行をバックフィルし、すべての外部キーとアプリケーションコードを新しい列を使うよう更新し、古い列を削除します。段階的に進めてください — 本番データベースに対して一括移行は避けましょう。
UUIDv4からULID/UUIDv7への性能改善移行:既存のIDを単純に再エンコードすることはできません(ランダムなタイムスタンプが付くため)。選択肢:既存行のUUIDv4 IDは維持し、新規行のみULIDを使う(古い行が削除されるにつれインデックスは徐々に整列される)。または新しいIDで全面書き換え(すべての参照の更新が必要)。
ID移行の最も安全な方法:移行期間中の二重書き込み。新旧両方のIDを同時に書き込み、APIで両方を提供し(検索は両方を受け付け)、すべてのクライアントが新しいフォーマットを使っていることを確認してから切り替えます。ダウンタイムは発生しませんが、一時的にIDストレージが倍になります。
3回のID移行から学んだ教訓:最も難しいのはデータベースではなく、IDが出現するすべての場所を見つけることです。ログファイル、アナリティクスイベント、外部パートナー連携、キャッシュレスポンス、メッセージキュー、エラートラッキングサービス。開始前にすべての消費者をリストアップしてください。「1週間で終わるはず」の移行は常に1ヶ月かかります。自分がコントロールしていないシステムに忘れられた参照があるためです。