UUID v4 と UUID v7 はどちらも 128 ビットの識別子ですが、共通点はそこまでです。v4 は 122 ビットをランダム性に費やします。v7 は先頭に 48 ビットの Unix ミリ秒タイムスタンプを置き、続いて 4 ビットのバージョン、74 ビットのランダム値、2 ビットのバリアントを配置します。バージョン番号は優劣の指標ではありません。識別子が何を漏らしてよいか、どのようにソートされるかを示す宣言です。
どちらも 2024 年に RFC 4122 を置き換えた RFC 9562 で定義されています。実用上の選択は 3 つの問いに集約されます。識別子を作成時刻でソートする必要があるか、B-tree の主キーに格納されるか、値を保持する誰もが作成時刻を読めることを許容できるか。本ガイドは経験則ではなくバイトレイアウトでこれらに答えます。
128 ビットのレイアウトを並べて比較
UUID はすべて 16 バイトで、各フィールドはテキスト上のハイフンではなく位置で定義されます。バイト 6 の上位ニブルがバージョン、バイト 8 の上位 2 ビットがバリアントです。それ以外はバージョンによって異なります。正規文字列では、13 番目の 16 進文字がバージョン、17 番目がバリアントです。
v4 では、バージョンとバリアント以外の 122 ビットがランダムです。v7 では、先頭 48 ビットがビッグエンディアンの Unix ミリ秒タイムスタンプであり、そのため v7 の値は生バイトとして作成順にソートされます。残りの 74 ビットは既定でランダムですが、RFC 9562 はミリ秒内の単調性のためにその一部をカウンタとして使うことを許容しています。
| フィールド | ビット | v4 | v7 |
|---|---|---|---|
| バイト 0-5 | 48 | ランダム | unix_ts_ms、ビッグエンディアン |
| バイト 6 上位ニブル | 4 | 0100 (バージョン 4) | 0111 (バージョン 7) |
| バイト 6 下位ニブルとバイト 7 | 12 | ランダム | rand_a |
| バイト 8 上位ビット | 2 | 10x バリアント | 10x バリアント |
| バイト 8-15 の残り | 62 | ランダム | rand_b |
| ランダムビット合計 | 122 | 122 | 74 |
v4 の詳細
v4 UUID は 6 ビットが固定された乱数です。バイト 6 の上位ニブルは 0100 なので、正規文字列の 13 番目の文字は常に 4 です。バイト 8 の上位 2 ビットは 10 なので、17 番目の文字は常に 8、9、a、b のいずれかです。予測できるのはこれらだけで、残りの 30 桁の 16 進数字はランダムです。
このランダム性がセキュリティ特性です。v4 の値にはタイムスタンプ、シーケンス、マシン識別子が含まれないため、いつどこで作成されたかについて何も明かしません。そのため、列挙されてはならない公開識別子に適した選択肢となります。ただし、暗号学的に安全な乱数源で生成されることが前提です。122 ビットの空間では衝突は無視できるほど小さく、10 億個の値でも衝突確率はおよそ 2^-63 です。
- バージョンニブル: 正規文字列の 13 番目は常に 4
- バリアント: 17 番目は常に 8、9、a、b のいずれか
- ランダムビット: 122、CSPRNG から生成
- 意味のある順序を持たず、順序はランダム
- 作成時刻もシーケンスも明かさない
v7 の詳細
v7 UUID は時刻順です。バイト 0 から 5 に Unix エポックからのミリ秒数をビッグエンディアンで格納します。バイト 6 の上位ニブルは 0111 です。バイト 6 の下位ニブルとバイト 7 全体が rand_a を形成し、バイト 8 の上位 2 ビットがバリアント、残りの 62 ビットが rand_b です。つまり 74 ビットのランダム値に加えて、目視で読めるタイムスタンプがあります。
タイムスタンプは 48 ビットで、西暦 10889 年までのミリ秒を表現できるため、オーバーフローは実用上の問題ではありません。同じミリ秒に作成された 2 つの v7 値はランダムビットまたはカウンタビットだけが異なり、時計が巻き戻らない限り、後から作成された値は常にバイト列として先の値より大きくなります。この順序性が要点です。
| バイト | 内容 | 例 |
|---|---|---|
| 0-5 | Unix ミリ秒、ビッグエンディアン | 0192f3c4a1b2 |
| 6 上位ニブル | バージョン 7 | 7 |
| 6 下位ニブル + 7 | rand_a、12 ビット | a1b |
| 8 上位 2 ビット | バリアント 10 | 8、9、a、b のいずれか |
| 8 (下位 6) から 15 | rand_b、62 ビット | ランダムまたはカウンタ |
ミリ秒内のソートと単調性
正規テキストの辞書順ソートはバイト順と一致します。ハイフンが固定位置にあり、小文字の 16 進数字は値と同じ順序でソートされるためです。テキスト形式でも 16 バイトでもソートすれば作成順になります。これはマシン間で調整なしに成り立つため、v7 はイベントログやソートキーの一部となる識別子に魅力的です。
問題は単一ミリ秒内です。既定では、同じミリ秒に作成された 2 つの値はランダムビットで順序付けられ、作成順にはなりません。RFC 9562 は、rand_a フィールドまたは左端のランダムビットを、タイムスタンプの刻みごとにランダムにシードしたカウンタとして使う方法を説明しています。この方法なら実用上ミリ秒内で単調になりますが、ランダム性が少し減り、共有カウンタを並行する生成側で慎重に扱う必要があります。
- ミリ秒をまたぐ場合: 時計が単調である限り順序は保証される
- ミリ秒内: カウンタを使わない限りランダム順
- カウンタ方式: 刻みごとにランダムにシードし、値ごとにインクリメント
- 時計の巻き戻し: 順序が壊れるため、単調な時計源を使う
- 並行性: カウンタをアトミックにインクリメントしないと特性を失う
B-tree のインデックスコスト
ランダムな v4 主キーはインデックスのランダムな位置に挿入されます。B-tree ではほぼ毎回の挿入で異なるリーフページに触れ、バッファプールからページが追い出され、ページが中央で分割されて半分しか埋まらない状態になります。テーブルが大きくなるにつれてインデックスは肥大化し、書き込み増幅が増え、キャッシュヒット率が下がります。
v7 キーは右端付近に挿入されるため、ワーキングセットは小さく保たれ、ページ分割は安価な末尾で発生します。トレードオフは競合です。すべての書き込み側が同じ右端のページを求めるためです。単一データベースで毎秒数百書き込み程度なら通常は問題ありませんが、非常に高い並行性ではホットスポットになります。一般的な緩和策はバッチ処理、接続レベルのインターリーブ、上位ビットへの少量のジッタの付与です。
| 特性 | ランダムな v4 キー | 時刻順の v7 キー |
|---|---|---|
| 挿入位置 | ランダムなリーフページ | 右端のリーフページ |
| ページ分割 | 頻繁、ページ中央 | まれ、末尾 |
| バッファプールの局所性 | 大規模インデックスでは悪い | 良い |
| インデックスサイズ | 大きい、充填率が低い | コンパクト |
| 書き込みホットスポット | ツリー全体に分散 | 右端に集中 |
| 時刻による範囲スキャン | 別カラムが必要 | ネイティブ |
プライバシーとタイムスタンプの漏洩
v7 の値は、それを保持する誰にでも、作成されたおおよその時刻をミリ秒単位で示し、したがってその前後に何件のレコードが作成されたかも示します。内部キーやイベント ID には問題ありません。登録数、注文率、アカウントの経過時間を明かすべきでない公開識別子には適しません。
順序性と秘匿性の両方が必要な場合の選択肢は、v7 を内部に留めて別のランダムな公開 ID を公開するか、システム外に出るものには v4 を使うことです。どちらのバージョンでも UUID をケイパビリティトークンとして扱わないでください。どちらも識別子であり、認可ではありません。v4 の ID は実際上推測不可能ですが、それでもすべてのリクエストでアクセス制御に対して検証する必要があります。
- v7 は作成時刻をミリ秒単位で明かす
- 近傍の v7 値を列挙するとレコード量が明らかになる
- v7 は内部に留め、v4 または別の公開 ID を公開する
- どちらのバージョンもアクセストークンや秘密情報ではない
- ランダム化された v7 というものはない。タイムスタンプは必須
ストレージとエンコードサイズ
ストレージの答えはどちらのバージョンも 16 バイトです。正規テキスト形式は 36 文字で、うち 32 文字が 16 進数字、4 文字がハイフンです。テキスト形式で保存するとサイズが倍になり比較も遅くなるため、ネイティブの UUID 型があればそれを使い、なければ BINARY(16) を使い、境界でのみフォーマットしてください。
SQL Server にはテストする価値のある注意点があります。uniqueidentifier 型は正規文字列とは異なるバイト順でソートされ、内部のバイト順も RFC 4122 と異なります。v7 の順序に依存するコードは、そのプラットフォームでソート挙動を前提とせず検証する必要があります。コンパクトなテキスト形式が必要な場合、パディングなしの base64url は 22 文字になりますが、標準の UUID 表現ではないため、正規形式を期待するツールを混乱させます。
| 表現 | サイズ | 備考 |
|---|---|---|
| バイナリ | 16 バイト | ネイティブ保存、最速の比較 |
| 正規テキスト | 36 文字 | 8-4-4-4-12、ハイフン付き |
| ハイフンなし 16 進 | 32 文字 | 一部の API やログで使用 |
| base64url、パディングなし | 22 文字 | 標準の UUID 形式ではない |
| Base32 | 26 文字 | まれ、一部のチケットシステムで使用 |
選択、移行、テスト
識別子がデータベースキーである場合、2 つ目のカラムなしで作成時刻でソートしたい場合、または追記の多いテーブルを構築する場合は v7 を選びます。値が公開される場合、作成時刻が機密である場合、またはシステムがすでにタイムスタンプカラムを保存していて順序性から何も得られない場合は v4 を選びます。どちらも異なる制約に対する正しい選択であり、1 つのテーブルで混在させるのは通常は誤りです。
テストでは値ではなく構造を検証する必要があります。パース後にバージョンニブル、バリアントニブル、バイト長を確認します。v7 では、埋め込まれたタイムスタンプがテスト期間内に収まること、順に作成された 2 つの値が正しく比較されることを検証します。テストでリテラルの UUID をスナップショットしないでください。また、生成器がカウンタを使わない限り、v7 が前の値より厳密に大きいと検証しないでください。カウンタがなければ、同じミリ秒内の順序はランダムです。
| 状況 | 推奨 | 理由 |
|---|---|---|
| 大規模テーブルの主キー | v7 | 局所性と小さいインデックス |
| 公開向け識別子 | v4 | タイムスタンプを開示しない |
| 時刻でソートするイベントログ | v7 | カラムなしでネイティブな順序 |
| セッションまたはリセットトークン | どちらでもない | 専用のランダムトークンを使う |
| 時刻カラムを持つ既存テーブル | v4 | 順序性はすでに利用可能 |
| システム間の交換 | どちらでもよい | どちらも標準の 128 ビット UUID |
よくある質問
- はい。どちらも 128 ビット、16 バイト、正規テキストで 36 文字です。異なるのは内部レイアウトだけです。
- ミリ秒をまたぐ場合は、生バイトとしても正規テキストとしても正しくソートされます。単一ミリ秒内では、生成器がカウンタを使わない限り既定の順序はランダムです。
- v7 です。挿入が B-tree 全体に散らばらず右端付近に留まるためです。トレードオフは、非常に高い並行性で最終ページに書き込みホットスポットが生じることです。
- 作成時刻をミリ秒単位で明かし、近傍の値からレコード量も明らかになります。内部に留めるか、システム外に出る識別子には v4 を使ってください。
- いいえ。UUID は識別子です。v4 の値でもすべてのリクエストでアクセス制御に対して検証する必要があり、トークンは独自の有効期間ルールを持つ専用の生成器から発行すべきです。
UUID v4 と v7 は同じサイズですか?+
UUID v7 は正しくソートされますか?+
データベースの主キーにはどちらが適していますか?+
UUID v7 は情報を漏洩しますか?+
UUID をアクセストークンとして使えますか?+
例は合成データです。生成された住所を本人確認、アカウント登録、配送に使用しないでください。