コンテンツへスキップ
Toova
すべてのツール

UUID v4 vs v7 — 違いとどちらをいつ使うか

Toova

UUID v4 は 10 年以上にわたって分散主キーのデフォルト選択肢でした。ランダムな 128 ビット値を生成し、8 つの 16 進グループとしてフォーマットして、完了。調整不要、事実上ゼロの衝突確率、どこでも動作します。では、2024 年に RFC 9562 で標準化された UUID v7 が、2026 年に本番システムで急速に広がっているのはなぜでしょうか?

短い答え: データベースのパフォーマンス。UUID v4 は B-tree インデックスの局所性を破壊します。UUID v7 は、開発者が UUID について愛するすべてを保ちながら、それを修正します。この記事では、各バージョンが実際に何であるか、なぜ違いが大規模で重要なのか、いつどちらを選ぶか、そしてダウンタイムなしで移行する方法を説明します。

今すぐ UUID を生成する必要がありますか?Toova UUID ジェネレーターをお試しください。これは v4 と v7 の両方を一括でサポートします。他のランダム識別子には、ランダム文字列ジェネレーターパスワードジェネレーターが短いトークンをカバーします。

UUID v4 — 純粋なランダム性

UUID バージョン 4 は、暗号的に安全な擬似乱数ジェネレーター (CSPRNG) を使って 128 ビットのうち 122 ビットを埋めます。残りの 6 ビットは固定です: 4 ビットがバージョン (0100) をエンコードし、2 ビットがバリアント (10) をエンコードします。結果は次のようになります:

f47ac10b-58cc-4372-a567-0e02b2c3d479
              ^^^^
              バージョン = 4 (ランダム)

ランダム性が機能です。2 つの独立したシステムが調整なしで UUID を生成でき、決して衝突しません — 2.71 京の v4 UUID のセットでの衝突確率は約 50% で、これは実用的なアプリケーションではリスクが無視できることを意味します。集中型 ID サーバー、データベースシーケンス、または分散ロックは必要ありません。

v4 の生成方法

import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();
// => 'f47ac10b-58cc-4372-a567-0e02b2c3d479'

uuid ライブラリ (JavaScript)、Python の uuid.uuid4()、Go の google/uuid、すべての主要言語ランタイムは OS CSPRNG に委任します — Linux の /dev/urandom、Windows の CryptGenRandom。生成コストは事実上ゼロです。

問題: ランダム挿入が B-tree インデックスを殺す

B-tree インデックスはデータをソートして保持します。新しい行を挿入すると、データベースはソートされた順序で新しいキーが収まる場所を見つけ、そこに配置します。すべての新しいキーがランダムであれば、それはインデックスのランダムな位置に着陸します — これは、すべての挿入が異なるページをディスクからバッファプールにロードする必要があることを意味します。低ボリュームでは、これは目に見えません。高ボリューム (数百万行、高い INSERT レート) では、インデックスの断片化と呼ばれるパターンを作成します: 各挿入が異なる場所に当たるため、インデックスは半分空のページで満たされ、ホットなワーキングセットが予測可能な最近のスライスではなくインデックス全体にまたがるため、バッファプールが絶えず変動します。

本番での症状: INSERT レイテンシが増加し、autovacuum オーバーヘッドが上昇し (PostgreSQL)、チェックポイントの圧力が増加し、最近のレコードの読み取りパフォーマンスが低下します。「最近の」がインデックスの局所性にもはやマッピングされないためです。これは仮説ではありません — UUID v4 主キーを持つ大規模な PostgreSQL と MySQL テーブルでよく文書化された痛みのポイントです。

UUID v7 — 時間順序付けされたランダム性

UUID v7 は、B-tree 断片化問題を解決するために明示的に設計されました。最上位ビットに 48 ビット Unix ミリ秒タイムスタンプをエンコードし、続いてバージョンビット、12 ビットのランダム、バリアントビット、さらに 62 ビットのランダム。総ランダム性: 74 ビット — 衝突を防ぐために必要なものよりはるかに多い。

018f4e6b-a23c-7d45-9abc-0e02b2c3d479
 ^^^^^^^^^^^^^^
 48 ビット Unix タイムスタンプ (ミリ秒精度)
                   ^
                   バージョン = 7

タイムスタンプが上位ビットを占めるため、後で生成された UUID は早く生成された UUID の後にソートされます。挿入は常にインデックスの右端に追加されます。データベースは、インデックス全体ではなく、バッファプール内で最新のインデックスページのみをホットに保つ必要があります。高い INSERT レートでは、これだけで書き込みレイテンシを 30〜60% 削減し、数千万行のテーブルで I/O を桁違いに削減できます。

v7 の生成方法

import { v7 as uuidv7 } from 'uuid';
const id = uuidv7();
// => '018f4e6b-a23c-7d45-9abc-0e02b2c3d479'

同じ uuid ライブラリがバージョン 10 で v7 サポートを追加しました。Python の標準ライブラリは 3.14 で追加しました。PostgreSQL 17 には組み込み関数として uuidv7() が含まれています。古いスタックを使用している場合、いくつかの小さなライブラリが依存関係なしで v7 生成を提供します。

サブミリ秒の単調性

同じミリ秒内で 2 つの v7 UUID が生成されると何が起こるでしょうか?RFC 9562 は、実装がランダムビットに単調カウンターをインクリメントして同じミリ秒内の順序を保証することを許可します。uuid ライブラリはデフォルトでこれを行います。結果: 1 ミリ秒で 10,000 個の ID が生成されても、それらは依然として正しくソートされます。

B-Tree インデックス断片化問題の詳細

これが重要な理由を理解するには、1 億の既存行を持つテーブル上の UUID v4 主キーで毎秒 10,000 挿入で何が起こるかを考えてみてください:

  • 各挿入は、1 億の既存エントリの中でランダムな位置に落ちるランダムな 128 ビットキーを生成します。
  • データベースは、その位置を含む特定の B-tree ページをバッファプールにロードする必要があります。
  • 1 億行と 8 KB ページで、インデックスは約 100,000 ページに広がります。毎秒、10,000 の異なるページが必要です — このインデックスだけのために典型的な 8 GB の shared_buffers が保持できるものよりはるかに多い。
  • すべてのキャッシュミスはディスク読み取りをもたらします。毎秒 10,000 挿入で、これは大規模なテーブルで SSD でさえ飽和させる可能性のある、毎秒数千のランダムディスク読み取りを生成できます。

UUID v7 では、毎秒 10,000 のすべての挿入が最も右のリーフページ (または最近のいくつかのページ) に着陸します。バッファプールはそれらの少数のページのみをホットに保つ必要があります。書き込みのキャッシュヒット率は 100% に近づきます。書き込みの増幅が劇的に低下します。

同じ恩恵が範囲スキャンにも適用されます: v4 テーブルの WHERE created_at BETWEEN x AND y は、完全なインデックススキャンまたは別のタイムスタンプインデックスを必要とします。v7 テーブルでは、主キー自体がタイムスタンプインデックスです — クエリは正しい範囲に直接シークできます。

PostgreSQL での UUID v7 の使い方

-- PostgreSQL 16+ で UUID 型でネイティブに動作
CREATE TABLE events (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- v4 (ランダム)
  -- 拡張機能またはアプリケーション側生成で UUIDv7 に切り替え
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- UUIDv7 では: タイムスタンプがすでに埋め込まれているため、
-- この別の created_at 列はしばしば冗長です。

PostgreSQL 17 はネイティブに uuidv7() を出荷します。PostgreSQL 14–16 の場合、pg_uuidv7 拡張機能が同じ関数を提供します。UUID データ型は v4 と v7 を同じように保存します — 16 バイト、オーバーヘッドなし。唯一の違いは、ソート順を決定するビットパターンです。

1 つの有用な結果: タイムスタンプが埋め込まれているため、多くのテーブルでは順序付けや表示のために別の created_at 列が不要になります。1 回の関数呼び出しで v7 UUID からタイムスタンプを抽出できます。これにより、スキーマの複雑さが軽減され、挿入ごとの 1 書き込みが排除されます。

UUID v4 vs v7 — トレードオフ

特性 UUID v4 UUID v7
ランダム性ビット 122 74
ソート順 ランダム 時系列順
B-tree INSERT パフォーマンス 低 (断片化) 優秀 (順次)
タイムスタンプ漏洩 なし あり (ms 精度)
RFC 標準 RFC 4122 (2005)、RFC 9562 (2024) RFC 9562 (2024)
ライブラリサポート 普遍的 急速に成長 (2024–2026)
埋め込み created_at なし あり

移行ガイド — v4 から v7

既存のテーブルを UUID v4 から v7 主キーに移行することは、複数ステップの操作です。重要な制約: 外部キーによって参照される主キーの値を、すべての参照テーブルも更新せずに変更することはできません。メンテナンスウィンドウを計画するか、デュアル書き込みアプローチを使用してください。

-- 1. 新しい列を追加
ALTER TABLE orders ADD COLUMN id_v7 UUID;

-- 2. 既存の行をバックフィル (元の created_at を
--    タイムスタンプソースとして保持; アプリで UUIDv7 ライブラリ関数を使用)
UPDATE orders SET id_v7 = generate_uuidv7(created_at);

-- 3. NULL が残っていないことを検証
SELECT COUNT(*) FROM orders WHERE id_v7 IS NULL;

-- 4. 列を入れ替え (短いメンテナンスウィンドウが必要)
ALTER TABLE orders ALTER COLUMN id_v7 SET NOT NULL;
ALTER TABLE orders ALTER COLUMN id_v7 SET DEFAULT generate_uuidv7(now());
ALTER TABLE orders RENAME COLUMN id TO id_v4_old;
ALTER TABLE orders RENAME COLUMN id_v7 TO id;
ALTER TABLE orders ADD PRIMARY KEY (id);

高トラフィックテーブルでのゼロダウンタイム移行のための推奨アプローチは:

  1. 今後 v7 UUID を生成するサーバーデフォルトで新しい id_v7 列を追加します。
  2. 低トラフィック期間中にバッチで古い行をバックフィルし、既存の created_at タイムスタンプを v7 タイムスタンプ部分のシードとして使用します。
  3. 参照テーブルのすべての外部キー列を更新して id_v7 を指すようにします。
  4. 短いメンテナンスウィンドウ中に列の名前を変更し、古い主キー制約を削除します。

バックフィルステップが最も長いです。バッチ間で 50 ms スリープで 1 バッチあたり 10,000 行で、1 億行のテーブルは約 8 時間かかります。早めに開始してください。

実世界の影響

複数のエンジニアリングチームが、本番サイズのデータセットで UUID v4 と v7 を比較するベンチマークを公開しています。一貫した発見:

  • INSERT スループット: 5,000 万行以上のテーブルで v4 から v7 に切り替えると 2〜5 倍の改善、テーブルサイズが大きくなるにつれてゲインが増加します。
  • 書き込みレイテンシ p99: 同じハードウェアで、数百ミリ秒 (v4、負荷下) から 1 桁のミリ秒 (v7) に低下します。
  • インデックスサイズ: 断片化が半分空のページを少なく残すため、v7 列上の B-tree インデックスは、同じデータ上の同等の v4 インデックスよりも 15〜30% 小さくなります。
  • バッファプール効率: 主キーインデックスの shared buffers ヒット率が、〜40% (v4、大きなテーブル) から〜99% (v7) になります。最近のページのみがホットに留まる必要があるためです。

およそ 100 万行未満ではゲインは無視できます。テーブルが小さいままなら、シンプルさのために v4 にとどまります。意味のある INSERT レートを持つ 1,000 万行以上では、v7 がより良いデフォルトです。

どちらをいつ使うか

UUID v7 を使うべき場合:

  • 新しいスキーマを設計しており、テーブルが大きく成長する (1,000 万行以上)。
  • テーブルが高い INSERT レートを持つ — イベント、ログ、注文、メッセージ、通知。
  • 作成タイムスタンプとしても機能する主キーが欲しい (列を排除)。
  • PostgreSQL 17、MySQL 8.0+、または v7 生成をサポートする現代の ORM を使用している。
  • ID でのソートが作成時刻でのソートと意味的に同等 — ほとんどの追加が多いテーブルでそうなる。

UUID v4 を使うべき場合:

  • 識別子がユーザーに公開され、作成時刻を明らかにしてはならない (招待コード、共有リンク、請求ハンドル)。
  • テーブルが小さく安定している — パフォーマンス上の恩恵が移行コストを正当化しない。
  • 作成タイムスタンプが機密のコンテキストで ID を生成している (成長指標で競争する製品のプライベートユーザー記録)。
  • ワンタイム資格情報やトークンとして必要 — 任意の UUID バージョンではなく、専用の秘密ジェネレーターを使用します。

まとめ

UUID v4 はランダムで、プライベートで、普遍的にサポートされています。UUID v7 は時間順序付けされ、データベースに優しく、主要なライブラリとデータベースで今や標準です。大きなまたは急成長するテーブルを持つ新しいスキーマには、v7 がより良いデフォルトです。タイムスタンプ漏洩が懸念のユーザーに公開される ID には、v4 が引き続き正しい選択肢です。

Toova UUID ジェネレーターで両方のバージョンを即座に生成します — アカウント不要。短いトークンには、ランダム文字列ジェネレーターが任意の長さの英数字 ID を生成します。