
UUID と ULID 入門 - 分散システムで衝突しない ID を、時系列に強く設計する
「主キーはとりあえず AUTO_INCREMENT の連番でいい」——単一のデータベースで完結するうちは、それで十分です。ところが、複数のサーバーやサービスが独立して ID を発行するようになると、連番はとたんに扱いづらくなります。この記事では、分散システムで衝突しない ID を設計するための定番である UUID と ULID を、RFC 9562 と ulid/spec を一次ソースに整理します。
なぜ連番 ID ではなく分散 ID なのか
連番の主キーには、いくつか運用上の弱点があります。
- 採番の一元化が必要: 次の値を決めるのは 1 つのデータベースだけ。複数ノードで独立して発行しづらく、シャーディングやマルチマスター構成で衝突しやすい
- 推測されやすい:
/users/1001の次は/users/1002。URL に連番 ID を露出すると、件数の推定や順番のなぞりができてしまう - マージがつらい: 別々に採番したデータを後から統合すると、ID がぶつかる
これらを避けるため、各ノードが単独で、事前調整なしに生成できる ID が求められます。その代表格が UUID です。
なお、ID を URL に含めてページを送る設計そのものについては、ページネーション設計(オフセット vs カーソル) も合わせて読むと、ソート可能な ID がなぜ効いてくるかが見えてきます。
UUID とは
UUID(Universally Unique Identifier)は、128 ビットの一意な識別子です。長らく RFC 4122 が仕様でしたが、これを置き換える RFC 9562 が 2024 年 5 月に発行され、新しく v6 / v7 / v8 が標準化されました。RFC 9562 は v1 から v8 までのバージョンと、Nil(全 0)・Max(全 1)の特殊な UUID を定義しています。
代表的なバージョンを挙げます。
- UUIDv1: タイムスタンプと MAC アドレスから生成。時刻順に近いが、MAC アドレス由来のため生成元がわかってしまう懸念がある
- UUIDv4: ほぼ全ビットがランダム。最も広く使われる
- UUIDv7: Unix ミリ秒タイムスタンプ + ランダム。時系列にソートできる新しい標準
文字列としての UUID は、550e8400-e29b-41d4-a716-446655440000 のように、ハイフン区切りの 36 文字(16 進 32 桁 + ハイフン 4 個)で表現します。
NOTE
RFC 9562 は RFC 4122 を「obsolete(廃止)」にしていますが、既存の v1 / v4 の生成方法が変わったわけではありません。v6 / v7 / v8 という選択肢が新たに標準へ加わった、と捉えるのが正確です。
UUIDv4 の課題 - ランダム性と DB インデックス
UUIDv4 は 128 ビットのうち、バージョン(4 ビット)とバリアント(2 ビット)を除いた 122 ビットがランダムです。RFC 9562 では、このランダム部が random_a(48 ビット)・random_b(12 ビット)・random_c(62 ビット)の 3 フィールドに分かれると定義されています。衝突確率は現実的に無視できるほど低く、事前調整なしにどのノードでも生成できます。
一方で、この「完全ランダム」がデータベースのインデックスと相性が悪いという副作用を持ちます。
多くの RDBMS の主キーインデックスは B-Tree(B+tree)で、キーをソート済みで保持します。連番や時刻順の値なら、新しい行はつねに「末尾(右端)」に追記され、挿入が局所的にまとまります。ところが UUIDv4 はランダムなので、挿入位置がインデックス全体に散らばります。その結果、次のような傾向が生じやすくなります。
- ページ分割(page split)が増える: 埋まったページの途中に割り込むため、ページを分割して詰め直す処理が起きやすい
- キャッシュ効率が落ちる: アクセスするページが毎回バラバラで、バッファに乗りにくい
- 断片化しやすい: とくに InnoDB のように主キーでテーブル本体を並べる(クラスタ化インデックス)方式では、行の物理配置まで散らばる
B-Tree そのものの仕組みは データベースインデックス入門 で解説しています。
WARNING
「UUIDv4 は遅い」と断言はできません。効果はテーブルサイズ・ストレージ・バッファ設定・書き込み量に強く依存し、環境依存です。ここでは「ランダムキーは挿入局所性が低い」という一般的な性質までを押さえてください。数値ベンチは条件次第で大きく変わります。
UUIDv7 の登場 - 時系列にソートできる UUID
この課題への標準的な答えが UUIDv7 です。RFC 9562 では、UUIDv7 を「よく知られた Unix エポックのタイムスタンプ(1970 年 1 月 1 日 UTC からのミリ秒数、うるう秒を除く)に基づく時刻順の値」と定義しています。ビット構成は次のとおりです。
- unix_ts_ms: 48 ビット(Unix ミリ秒タイムスタンプ)
- ver: 4 ビット(バージョン。
0111) - rand_a: 12 ビット(ランダム、または単調増加のための構造)
- var: 2 ビット(バリアント)
- rand_b: 62 ビット(ランダム、または単調性のためのカウンター)
先頭にミリ秒タイムスタンプが来るため、生成した順にほぼ単調増加し、バイト列としてそのままソートすると時系列順になります。RFC 9562 も「v6 / v7 はデータベースインデックスなどソートを要する用途向けに、不透明な生バイトとしてソートできるよう設計されている」と述べています。同一ミリ秒内の順序を保証したい実装では、rand_b の一部をカウンターとして使う「単調性」の仕組みも用意されています。
つまり UUIDv7 は、UUID の衝突耐性・分散生成のしやすさと、時系列キーの挿入局所性を両立させたバージョンだと言えます。新規設計で「UUID を使うが、主キーにもしたい」なら、まず v7 を検討する価値があります。
ULID とは
ULID(Universally Unique Lexicographically Sortable Identifier)は、UUIDv7 とよく似た発想を持つ、仕様が ulid/spec で公開されている識別子です。UUIDv7 が RFC で標準化される前から使われてきました。
ULID の構成は次のとおりで、合計 128 ビットです(UUID と互換のサイズ)。
- タイムスタンプ: 48 ビット(UNIX ミリ秒時刻)
- ランダム: 80 ビット
特徴を UUIDv7 と対比すると分かりやすくなります。
- 文字列表現: Crockford の Base32(1 文字あたり 5 ビット)で符号化し、26 文字で表現します。たとえば
01ARZ3NDEKTSV4RRFFQ69G5FAVのような見た目です。使う文字は0123456789ABCDEFGHJKMNPQRSTVWXYZで、紛らわしいILOUを除いてあります - ソート可能: 先頭 10 文字がタイムスタンプ、後半 16 文字がランダム。文字列としてそのまま辞書順(レキシコグラフィカル)に並べると時系列順になります
- URL セーフ: ハイフンや記号を含まないため、URL やファイル名にそのまま使いやすい
- 単調性: 同じミリ秒内で生成する場合、ランダム部を最下位ビットから 1 増やしていくことで、同一ミリ秒内でも順序を保てます
UUIDv7 との主な違いは、文字列表現と長さです。UUIDv7 は 36 文字のハイフン付き 16 進表現、ULID は 26 文字の Base32 表現で、より短く読みやすくなります。バイナリとしてはどちらも 128 ビットで、48 ビットのミリ秒タイムスタンプを先頭に置く点は共通です。
使い分けと実装例
まずは JavaScript(Node.js)での生成例です。UUIDv4 は追加ライブラリなしで生成できます。
// UUIDv4: 標準 Web Crypto API で生成
const id = crypto.randomUUID();
console.log(id);
// 例: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'crypto.randomUUID() が返すのは UUIDv4 です。UUIDv7 や ULID は、専用ライブラリを使うのが手軽です。
// UUIDv7: uuid パッケージ(v7 対応版)
import { v7 as uuidv7 } from 'uuid';
console.log(uuidv7());
// 例: '018f6f9c-5b3a-7c1e-8a2b-1f4d9c0e5a6b'
// 先頭がミリ秒タイムスタンプなので、時間が進むと文字列も増加する
// ULID: ulid パッケージ
import { ulid } from 'ulid';
console.log(ulid());
// 例: '01ARZ3NDEKTSV4RRFFQ69G5FAV'(26 文字の Crockford Base32)3 つを並べると、見た目と性質の違いがはっきりします。
UUIDv4 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d 36 文字 / ランダム(ソート不可)
UUIDv7 018f6f9c-5b3a-7c1e-8a2b-1f4d9c0e5a6b 36 文字 / 先頭が時刻(ソート可)
ULID 01ARZ3NDEKTSV4RRFFQ69G5FAV 26 文字 / 先頭が時刻(ソート可)データベース側での生成もできます。PostgreSQL 18 では、時系列ソート可能な UUID を生成する uuidv7() と、明示的に v4 を生成する uuidv4() が組み込み関数として追加されました。あわせて uuid_extract_timestamp() や uuid_extract_version() も追加され、生成値の検証がしやすくなっています。
-- PostgreSQL 18: 主キーを UUIDv7 で自動生成
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT uuidv7(),
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO orders DEFAULT VALUES;
SELECT id, uuid_extract_timestamp(id) FROM orders;
-- id は時刻順に増加し、主キーインデックスへの挿入が末尾側にまとまるNOTE
PostgreSQL 17 以前や他の RDBMS で v7 を使いたい場合は、拡張やアプリケーション側での生成に頼ります。MySQL には v7 の組み込み生成関数は標準では用意されていない(未確認の部分を含むため、利用時はバージョンごとの公式ドキュメントで確認してください)ため、アプリ側での生成が現実的です。
使い分けの目安は次のとおりです。
- とにかく一意で、順序も予測もされたくない: UUIDv4。トークンや外部公開する不透明 ID 向き
- 主キーにして挿入性能と時系列を両立したい: UUIDv7。標準(RFC)に沿う点も安心材料
- 短く読みやすい文字列で、URL やログにも使いたい: ULID。26 文字で扱いやすい
なお、時刻ベースの ID を扱うときは、生成側のタイムゾーンやクロックの扱いも影響します。日時の扱いは タイムゾーンと日時処理入門 も参考にしてください。
注意点 - セキュリティと運用
時系列 ID は便利ですが、その「時刻が埋まっている」性質が、そのままリスクにもなります。
- タイムスタンプの露出: UUIDv7 と ULID は先頭 48 ビットに生成ミリ秒時刻が入っています。ID を外部に見せると、レコードの作成時刻を第三者が復元できます。作成時刻を隠したい ID(招待トークン、パスワードリセットのキーなど)には向きません
- ランダム部からの推測: UUIDv7 / ULID のランダム部はあくまで「衝突回避」が主目的で、暗号学的な秘匿性の保証ではありません。秘密の URL やセッション ID には、専用のランダムトークン(十分なビット数の暗号乱数)を使ってください
- 件数・生成レートの漏洩: 連番ほど露骨ではないものの、時系列 ID は「いつ・どれくらいの間隔で作られたか」を示唆します。公開 API のリソース ID として使うなら、その情報が漏れて困らないか検討します
そのうえで、UUIDv7 / ULID は内部の主キーとしては優れた選択肢です。REST API のリソース設計で ID をどう扱うか(同じ操作を安全に繰り返せるか)は、REST API のべき等性設計 とあわせて検討すると設計が締まります。
まとめ
- UUIDは 128 ビットの一意な識別子で、仕様は 2024 年 5 月発行の RFC 9562(旧 RFC 4122 を置き換え)。v6 / v7 / v8 が新たに標準化されました
- UUIDv4は 122 ビットがランダムで衝突しにくい反面、ランダムキーは B-Tree インデックスへの挿入局所性が低く、ページ分割やキャッシュ効率の低下を招きやすい(効果は環境依存)
- UUIDv7は 48 ビットの Unix ミリ秒タイムスタンプ + ランダムで、時系列にソート可能。主キーとインデックスの相性がよい
- ULIDは 128 ビット(48 ビットタイムスタンプ + 80 ビットランダム)を Crockford Base32 の 26 文字で表現し、辞書順で時系列ソート可能。UUIDv7 とねらいは近く、より短い文字列が特徴
- 時系列 ID は作成時刻が露出するため、秘匿トークン用途には使わない。内部の主キーとして活かすのが基本方針です
連番の手軽さと、分散生成・順序性のバランスを取れるのが UUIDv7 と ULID です。新規設計では、まず「主キーに v7 / ULID、公開トークンは別途暗号乱数」という切り分けから始めるのがおすすめです。


