UUID v7 は、先頭 48 ビットにミリ秒精度の Unix 時刻を置く UUID です。値そのものがおおむね生成順に並ぶため、データベースの主キーのように「順番に増えてほしい」場所で使われます。本記事では、なぜランダムな v4 を主キーにすると不利になるのか、v7 が何を解決するのか、そして導入前に知っておくべき落とし穴を整理します。
1. v7 のビット配置 — 何が時刻順を作っているのか
UUID は 128 ビットの値です。v7 ではその先頭 48 ビットに Unix エポックからのミリ秒が入ります。続いて 4 ビットのバージョン(7)、2 ビットのバリアント、残りが乱数です。
| 領域 | ビット数 | 中身 |
|---|---|---|
| unix_ts_ms | 48 | Unix エポックからの経過ミリ秒 |
| ver | 4 | バージョン(v7 なので 7) |
| rand_a / rand_b | 74 | 乱数(一部は単調性のために使うことができる) |
| var | 2 | バリアント |
16 進数表記は v4 と同じ 8-4-4-4-12 の 36 文字です。先頭が時刻なので、文字列としてソートしても時系列にほぼ一致する点が実務上いちばん効きます。バイト列で比較しても同じ順序になります。
UUID の仕様は RFC 4122 が長く使われてきましたが、2024 年の RFC 9562 がこれを置き換え、v6・v7・v8 を追加しました。v7 はこの新しい仕様で定義されたものです。
2. なぜ v4 を主キーにすると不利になるのか
v4 は 122 ビットが乱数で、値の並びに規則性がありません。これが主キーになると、多くの RDBMS が採用する B-Tree インデックスとの相性で不利になります。
- 書き込み位置が散らばる — B-Tree は近い値を同じページにまとめます。挿入キーがランダムだと、書き込みがインデックス全体に散らばります。
- キャッシュに乗りにくい — 直近の挿入先が毎回違うため、更新対象のページがメモリ上に残っていない確率が上がります。
- ページ分割が増える — 満杯のページの途中に挿入すると分割が起き、その分の書き込みと空き領域が発生します。
一方、時刻順に増えるキーなら挿入は常にインデックスの末尾付近に集まります。触るページが局所化するのでキャッシュが効き、分割も末尾でのみ起きます。v7 が解決しているのはこの一点です。
ただし、これは規模が出て初めて差になる話でもあります。数万行のテーブルで v4 から v7 へ移す価値はほとんどありません。移行はスキーマと既存データに影響するので、必ず自分の環境で計測してから判断してください。
3. 落とし穴1: 生成時刻が読める
v7 の先頭 48 ビットは加工されていないミリ秒時刻です。つまり ID を見れば、そのレコードがいつ作られたかが分かります。16 進数の先頭 12 桁を 10 進に直すだけで復元できます。
連番の整数 ID のように「全体で何件あるか」が漏れることはありませんが、時刻そのものが業務上の機密になる場面はあります。たとえば申込の受付時刻、問い合わせの発生時刻、内部処理のタイミングなどです。公開する ID と内部の主キーを分けるのが素直な対処になります。
4. 落とし穴2: 同一ミリ秒内の順序は保証されない
時刻が同じ 1 ミリ秒の中では、後ろの乱数によって順序が決まります。つまり同一ミリ秒に生成した値どうしの並びはランダムです。高頻度で生成する処理では、これが「作成順に並ばない」形で表面化します。
RFC 9562 は、乱数領域の一部をカウンタとして使う単調性の追加方式を示しています。厳密な順序が要るなら、その方式に対応した実装を選ぶか、順序専用の列(シーケンスや挿入時刻)を別に持たせてください。「v7 だから作成順に並ぶ」と仕様書のように扱わないことが要点です。
5. 落とし穴3: 時計に依存する
v7 の値は生成した機械の時計を反映します。したがって、時計がずれているホストが混ざると順序も崩れます。NTP による時刻同期が前提になりますし、時計が巻き戻ったときの挙動は実装によって異なります(同じミリ秒に留める、カウンタで吸収するなど)。
複数のホストで採番する構成では、ホスト間の時計のずれがそのまま順序のずれになります。ミリ秒単位の正確な順序を業務要件にしている場合は、UUID ではなく中央で採番する仕組みを検討したほうが確実です。
6. v4 / v7 / ULID の使い分け
| UUID v4 | UUID v7 | ULID | |
|---|---|---|---|
| 並び順 | ランダム | ほぼ時刻順 | ほぼ時刻順 |
| 表記 | 36文字の16進 | 36文字の16進 | 26文字(Crockford Base32) |
| 時刻の露出 | 無し | あり(ミリ秒) | あり(ミリ秒) |
| 標準 | RFC 4122 / 9562 | RFC 9562 | 仕様は公開だがRFCではない |
- v4 — 用途を選ばない一般的な一意識別子。順序が要らないならこれで十分です。
- v7 — 主キーやイベントログなど、生成順に並んでほしい場所。UUID の型・関数がそのまま使えるのが利点です。
- ULID — 26 文字で短く、URL に載せやすい。ただし DB の UUID 型には収まらないため、格納方法を決める必要があります。
どれを選ぶ場合も、乱数部分は暗号論的に安全な乱数(CSPRNG)で作ることが前提です。ブラウザなら crypto.getRandomValues()、Node.js なら crypto モジュールが該当します。Math.random() は予測可能なので使ってはいけません。
よくある質問(FAQ)
UUID v7 は必ず時刻順に並びますか?
同じミリ秒の中では保証されません。v7 が時刻順に並ぶのは先頭48ビットのミリ秒タイムスタンプによるもので、その後ろは乱数です。したがって、同一ミリ秒に生成した複数の値の並びはランダムになります。ミリ秒単位より細かい順序が必要な場合は、RFC 9562 が示す単調性の追加方式を実装するか、別途シーケンス列を持たせてください。
v4 の主キーは本当に性能問題になりますか?
テーブルの規模と挿入頻度によります。B-Tree インデックスは近い値をまとめて格納するため、挿入キーがランダムだと書き込み位置がインデックス全体に散らばり、キャッシュに乗りにくくなってページ分割も増えます。小規模なテーブルでは体感できませんが、行数が増えるほど差が出ます。まず自分の環境で計測し、問題が確認できてから移行するのが妥当です。
UUID v7 を公開APIのIDに使っても安全ですか?
生成時刻が読み取れることを許容できるかで判断してください。v7 の先頭48ビットはミリ秒のUnix時刻そのもので、ID を見た第三者がレコードの作成時刻を復元できます。連番のように件数を推測されることはありませんが、時刻が業務上の機密になる場合(申込の受付時刻など)は、公開用に別の識別子を持たせるほうが安全です。