12k
All articles

UUID v4 と v7: どちらを使うべきか?

データベースの主キーでUUID v4とv7を比較。v7が挿入性能を改善する理由、v4が時刻の秘匿に向く場面、生成方法まで解説。

OpenReplay Team
OpenReplay Team
UUID v4 と v7: どちらを使うべきか?

2026年に新しくデータベースの主キーを設計するなら、デフォルトは UUID v7 とし、識別子が公開されるうえに作成時刻を秘匿する必要がある場合にのみ v4 を選ぶ、というのが基本方針です。

負荷の高いテーブルで INSERT のレイテンシがじわじわと悪化していくのを目にし、その原因が「毎回インデックスの別々の場所に着地する主キー」にあると突き止めた経験があるなら、この問いには馴染みがあるはずです。実は解決策は、スキーマ移行よりもはるかに小さな変更で済みます。どちらの形式も同じ RFC で標準化された 128 ビットの UUID なので、選択の論点は互換性でも衝突安全性でもありません。争点はただ一つ、ID が作成順にソートされるかどうかです。v7 の時系列順序性は、ランダムな v4 キーが書き込み負荷の高いテーブルで引き起こすインデックスの断片化を解消します。その代償として、すべての値に読み取り可能なタイムスタンプが埋め込まれます。本稿では、構造上の違い、v7 の書き込み性能を支えるデータベースインデックスの仕組み、プライバシー面のトレードオフ、現在のライブラリサポート状況、そして決定的なデフォルト方針を整理します。

要点

  • UUID v4 と v7 はいずれも RFC 9562(2024年5月) で標準化された 128 ビット・16 バイトの識別子です。v7 は先頭 48 ビットを Unix ミリ秒タイムスタンプに置き換えているため値が作成順にソートされますが、v4 は完全にランダムです。
  • ランダムな v4 キーは B-tree インデックス全体に散らばり、ページ分割、キャッシュの入れ替わり、断片化を引き起こします。v7 の時系列順プレフィックスにより INSERT はインデックス末尾付近への追記となり、シーケンシャルキーに近い挙動になります。
  • v7 の ID は自身の作成時刻をミリ秒精度で漏らしますが、v4 は漏らしません。公開識別子、招待リンク、あるいは成長率を秘匿しなければならない箇所には v4 を使ってください。
  • v7 のサポートは一級品です。PostgreSQL 18 はネイティブの uuidv7() を搭載し、Python 3.14uuid.uuid7() を追加、JavaScript の uuid パッケージ(v14.x)は v7() をエクスポートしています。
  • UUID は秘密情報ではありません。認証には専用の 256 ビットのランダムトークンを使い、UUID は識別用途に限定してください。

UUID v4 と v7 の違いは何か?

UUID v4 と v7 はいずれも RFC 9562 で標準化された 128 ビット・16 バイトの識別子です。RFC 9562 は 2024 年 5 月に Standards Track 文書として公開され、旧来の RFC 4122 を廃止しました。構造上の唯一の違いは、ビットの出どころです。UUID v4 は 122 ビットのランダム値に、バージョンとバリアントのマーカー用に 6 ビットを確保した構成で、完全にランダムかつ順序を持ちません。UUID v7 は先頭 48 ビットを Unix ミリ秒タイムスタンプに置き換え、残り約 74 ビットをランダム値(およびバージョン・バリアントのマーカー)で埋めます。そのため v7 の値は辞書順でもバイト順でも作成順にソートされますが、v4 の値はそうなりません。

UUID v4:  [ 122 random bits ...................... ] + version/variant
UUID v7:  [ 48-bit ms timestamp ][ ~74 random bits ] + version/variant

この一点の違いが、判断のすべてです。ランダム部分に由来する衝突確率の計算は同じ、格納するカラムも同じ、そして両者とも同一の標準でカバーされています。

なぜデータベースキーには UUID v7 が有利なのか?

v7 が主キーとして v4 を上回るのは、タイムスタンプのプレフィックスが、ランダムキーでは失われるインデックス局所性を INSERT にもたらすからです。ランダムな v4 キーは B-tree インデックス内のランダムな位置に着地し、各 INSERT がツリーの異なる部分を対象とするため、ページ分割、キャッシュの入れ替わり、断片化を引き起こします。これはまさに RFC 9562 が v7 を定義した理由として挙げている問題です。識別子が時系列順序を持たない場合、新しい行はそのランダム値が偶然落ちた場所に書き込まれるほかありませんが、時系列順のスキームで次々と生成された値はインデックス上で隣り合うことになります。v7 の単調増加プレフィックスにより新しい行はインデックス末尾付近に追記されるため、ページ分割はまれになり、INSERT スループットはシーケンシャルな整数キーに近づきます。しかも UUID としての衝突安全性と分散生成の利点はそのまま保たれます。

このペナルティが最も深刻なのはクラスタ化インデックスのエンジンです。MySQL InnoDB と SQL Server ではテーブルが主キー順に物理配置されるため、ランダムな INSERT は構造全体にわたってページを書き換えます。PostgreSQL は行をヒープに格納しインデックスを分離しているためキーのランダム性の影響を受けにくいものの、それでもランダムな v4 キーではインデックスのキャッシュ局所性が失われます。影響の大きさはエンジン、ワークロード、ハードウェアによって変わるため、出典の不明なブログのベンチマークに載っている具体的なパーセンテージは懐疑的に扱い、自分のテーブルで実測してください。ただし、メカニズム自体に議論の余地はありません。

時系列順序性はバースト INSERT の下でも維持されます。実装側でサブミリ秒カウンタを追加し、同一ミリ秒内に生成された ID でも正しくソートされるようにしています。Python の uuid.uuid7() は 42 ビットをカウンタとして確保し、1 ミリ秒内に生成された値の順序を保ちます。PostgreSQL 18 の uuidv7() は、ミリ秒単位の Unix タイムスタンプ、サブミリ秒の端数、そしてランダムビットから各値を構成します。

それでも UUID v4 が正解となる場面

ID が外部に公開され、かつその作成時刻が機微な情報である場合は v4 を選んでください。v7 の ID は自身の作成時刻をミリ秒精度で埋め込んでいるためです。ID を読める者は誰でもレコードの作成時刻を読み取れるため、v7 は外部公開される識別子には不向きです。Aiven のチームも同じ結論に達しています。主キーが外部アプリや API を通じてエンドユーザーに渡された時点で、識別子がレコードの作成時刻を明かしてしまうため、v7 は妥当な選択ではなくなる、というものです。招待トークン、共有リンク、あるいは競合が ID の範囲から成長率を推測できてしまうような箇所には v4 を使ってください。

両バージョンに共通する注意点が一つあります。UUID はセキュリティトークンではありません。v7 の非ランダム部分は予測可能であり、v4 のランダム性の品質は実装依存です。したがって、どちらも認証の関門として使うべきではありません。シークレットには最低 256 ビットの暗号論的乱数文字列を別途生成し、UUID は識別用途にのみ使用してください。

今日 v7 を生成する方法と、段階的な移行

v7 のサポートはデータベースと言語の双方で広がっていますが、正確なバージョンが重要です。

プラットフォームv7 の生成バージョン
PostgreSQLuuidv7()(ネイティブ)PostgreSQL 18
Pythonuuid.uuid7()(標準ライブラリ)Python 3.14
JavaScript / Nodeuuidv7()uuid v14.x

PostgreSQL 18uuidv7() をネイティブ関数として搭載し、既存の gen_random_uuid() に対する uuidv4() エイリアスも追加しました。PostgreSQL 17 以前では拡張機能かアプリケーション側のライブラリが必要です。Python が uuid.uuid7() を標準ライブラリに追加したのは 3.14 であり、3.12 ではありません(3.12 で呼び出すと AttributeError になります)。JavaScript では、uuid パッケージが ESM の名前付きインポート経由で v7 を公開しており、変更履歴によれば同パッケージは v12 で CommonJS のサポートを終了しています。

// Node.js, uuid v14.x
import { v7 as uuidv7 } from "uuid";
const id = uuidv7();
-- PostgreSQL 18
SELECT uuidv7();

カラム型を確定させる前に違いを目で確かめたい場合は、それぞれをまとめて生成して並べてみてください。OpenReplay の UUID ジェネレーターはブラウザ内で v4 または v7 の値を一度に最大 500 件生成でき、大文字化、ハイフンなし、引用符付き出力のトグルも備えているため、SQL や JSON のフィクスチャにそのまま貼り付けられます。v7 の値を並べてソートすれば作成順に出てきますが、v4 で同じことをすると散らばります。値は自分のタブ内の Web Crypto API から生成されるため、どこにも送信されません。

移行に書き換えは不要です。v4 と v7 は同じ 16 バイトの uuid カラム型を共有しているため、既存の v4 の行はそのまま残しつつ、同一テーブル内で新しい行を v7 として生成できます。バージョンは値の中にエンコードされており、バックフィルも不要です。新しい INSERT はインデックス末尾に集まり、ページが時間をかけて書き換えられるにつれて断片化は徐々に緩和されます。これは即座のデフラグではなく、漸進的な効果です。

結論: どちらを使うべきか

新しいデータベースの主キー、ログ、イベントストリームには UUID v7 をデフォルトとし、予測不可能性が重要な場面では v4 を選んでください。v7 はシーケンシャルキー並みの書き込み性能と、UUID の衝突安全性・分散生成の両方をもたらし、しかも多くのチームがすでに運用しているプラットフォームでネイティブサポートされています。v4 は、公開されるうえに作成時刻を秘匿する必要のある識別子のために取っておきましょう。

判断を補完する選択肢が二つあります。ULID は同じ「タイムスタンプ + ランダム」の考え方を Crockford base32 でエンコードし、26 文字の URL に適した短い文字列を提供しますが、IETF 標準ではなくネイティブの uuid カラム型もありません。素朴な bigint の自動採番は、分散 ID 生成を必要としない小規模な単一ノードシステムにとって、依然として最も小さく高速な選択肢です。

今日これから新しいテーブルを立ち上げようとしていて、v4 キーによるインデックスの痛みを感じているなら、新規 INSERT を v7 に切り替え、既存の行はそのまま残し、インデックスが落ち着くのを待ってください。移行コストをほとんどかけずに得られる勝ちです。

FAQ

UUID v7 の値から作成タイムスタンプを取り出せますか?

はい。UUID v7 は先頭ビットに 48 ビットの Unix ミリ秒タイムスタンプを格納しているため、その値がいつ生成されたかをデコードできます。PostgreSQL 18 はまさにこの用途のために uuid_extract_timestamp() を提供しており、バージョン 7 の値をサポートするよう拡張されました。これはデバッグや時間範囲クエリにとっては有用な機能ですが、同時に v7 が作成時刻を漏らす理由でもあり、そのタイミングが機微となる公開識別子には使うべきではありません。

UUID v7 と v4 の衝突リスクは同じですか?

同じではありませんが、実務上その差は無視できます。UUID v4 は 122 ビットのランダム値を持つのに対し、v7 はタイムスタンプ用の 48 ビットに加えてバージョンとバリアントのマーカーを確保した後、およそ 74 ビットのランダム値が残ります。v7 のランダムビットは少ないものの、衝突が起こり得るのは同一ミリ秒内に生成された ID どうしに限られ、実装側はそのウィンドウ内で単調増加カウンタを追加しています。現実的なワークロードにおいては、どちらも現実的な生成レートの範囲で実質的に衝突しません。

UUID v7 は読み取りクエリの性能も改善しますか、それとも INSERT だけですか?

v7 が主に効くのは書き込み側の性能と範囲スキャンで、行をインデックス上で互いに近い位置に並べることでページ分割を減らし、キャッシュ局所性を改善します。大幅な読み取り高速化を v7 単独の効果と見なさないでください。PostgreSQL 18 が公式に掲げる「最大 3 倍」の読み取り性能向上は、新しい非同期 I/O サブシステムによるもので、uuidv7() とは独立した機能です。v7 の性能上の恩恵として正確に言えるのは、インデックス局所性と INSERT スループットです。

既存の UUID v4 主キーを v7 に移行すべきですか?

通常、全面的な移行は不要です。v4 と v7 は同じ 16 バイトの uuid カラム型を共有し、バージョンは値の中にエンコードされているため、既存の v4 の行はそのままにして、同一テーブル内で新しい行を v7 として生成でき、バックフィルも必要ありません。新しい INSERT はインデックス末尾に集まり、ページが書き換えられるにつれて断片化は徐々に緩和されます。古い行の書き換えが見合うのは、断片化がすでに実測できるレベルの問題を引き起こしている場合だけです。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.