【検証】UUIDv4・v7・bigint、結局どれを選ぶ?プロジェクト別ID設計の実験
「UUIDv7はv4の上位互換」は本当か。PostgreSQL 18とPython 3.14でInsert性能を実測し、プロジェクト規模・テーブル用途別に最適なID戦略を整理した。
ラボ
kkm
Backend Engineer / AWS / Django
「UUIDv7はv4の上位互換」は本当か。PostgreSQL 18とPython 3.14でInsert性能を実測し、プロジェクト規模・テーブル用途別に最適なID戦略を整理した。
UUIDv7の中身を覗いてみた
UUIDv7には生成日時が埋まっている。Pythonで実際に取り出してみます。
import uuid
from datetime import datetime, timezone
# Python 3.14で追加されたuuid7()
id = uuid.uuid7()
print(id) # 019648a0-7c5f-7def-b321-4a7e8f3c1b09
# 上位48bitからタイムスタンプを取り出す
timestamp_ms = id.int >> 80
dt = datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc)
print(dt) # 2026-04-13 05:23:17.631000+00:00たった3行で「このIDが2026年4月13日の午前5時23分に生成された」とわかります。ミリ秒精度です。オンラインツールにペーストするだけでも同じ結果が得られます。
UUIDv4ではこれができません。128bitのうち122bitが完全ランダムだからです。
「UUIDv7はv4の上位互換」という言説をよく見かけます。DB性能は確かに上がる。でも、セキュリティ特性はむしろ後退しています。この記事では、UUIDv4・v7・bigintそれぞれの「得意なこと」と「やってはいけないこと」を、実測データとともに整理します。
UUIDv4とv7、何が違うのか
UUID(Universally Unique Identifier)は128ビットの識別子で、分散システムで重複しないIDを生成するために使います。2024年5月に正式化されたRFC 9562でv7が標準化されました。
| 項目 | UUIDv4 | UUIDv7 | bigint auto-increment |
|---|---|---|---|
| 構造 | 122bitランダム | 48bitタイムスタンプ + 74bitランダム/カウンタ | 64bit連番 |
| サイズ | 16バイト(36文字) | 16バイト(36文字) | 8バイト |
| 時系列ソート | 不可 | 可能 | 可能 |
| DB問い合わせなしの ID生成 | 可能 | 可能 | 不可(DB採番) |
| ランダム性 | 122bit | 74bit(仕様上) 32bit(Python 3.14実装) | 0(完全に予測可能) |
| B-TreeのInsert性能 (大量データ時) | ページスプリット多発 | 末尾追記で高速 | 末尾追記で最速 |
| 生成日時の漏洩 | なし | ミリ秒精度で漏洩 | 順序から推測可能 |
v7の最大の特徴は時間順にソートできるUUIDであること。これがB-Treeインデックスと相性がいい理由です。v4は完全ランダムなのでインデックスのあちこちにInsertが散らばり、ページスプリット(インデックスページの分割)が頻発します。v7は時間順に末尾へ追記されるので、これが起きにくい。
ただし、ここで見落とされがちな事実が2つあります。v7のランダム部分はv4より大幅に少ない(122bit → 74bit、Python実装ではわずか32bit)こと。そして、「末尾に追記される」は高並行環境では逆にボトルネックになりうること。後で検証します。
URLにIDを出すとき、何が問題になるのか
「主キーは外部に公開すべきでない」という主張をよく見かけます。でも、この主張には2つの異なる論点が混ざっています。整理しましょう。
論点1: ID形式の問題(列挙リスク)
/user/42というURLを見たとき、/user/43や/user/1が存在するかもしれない、と推測できます。これが列挙攻撃(IDOR: Insecure Direct Object Reference)のリスクです。auto-incrementのbigintをURLに出すと、このリスクが生まれます。
ただし、これはID形式の問題であって、「主キーだから」の問題ではありません。主キーでなくても連番のカラムをURLに出せば同じリスクがあるし、主キーがUUIDv4なら列挙は現実的に不可能です。
論点2: 主キーと外部公開IDを分離すべきか
これはセキュリティの話ではなく、設計の柔軟性の話です。主キーをURLに使うと、主キーの変更がURL(= API契約)の破壊的変更になります。たとえば「後からbigintをUUIDに変えたい」と思っても、URLが変わるとクライアント全員に影響が出る。
外部公開用のIDを別カラムで持てば、内部の主キーを自由に変更できます。URLには短いnanoidを使い、内部ではUUIDv7で性能を最適化する、といった柔軟な設計が可能になります。
OWASP API Security Top 10(2023年版)の第1位は「Broken Object Level Authorization(BOLA)」ですが、対策として明示されているのは「認可チェックを必ず実施せよ」です。「UUIDを使え」とは書かれていません。UUIDは列挙リスクを下げる補助手段であって、認可チェックの代替にはなりません。
じゃあbigintをクエリパラメータに出していいのか?
結論からいうと、auto-incrementのbigintをそのままURLやクエリパラメータに出すのは推奨しません。理由は3つです。
- 1. 列挙が容易。
?order=1482を見れば、?order=1483を試すのは誰でもできます。認可チェックがあれば不正アクセスは防げますが、「レコードが存在するか」の情報自体が漏れます(404 vs 403の差) - 2. ビジネスメトリクスの漏洩。注文IDが連番なら「昨日の注文が1,482で今日が1,519だから、1日37件か」と競合に推測されます
- 3. 後から変えにくい。URLスキームは公開APIの契約です。一度
/api/orders/1482で公開したものを/api/orders/019648a0-7c5f...に変えるには、全クライアントにバージョン移行を強いることになります
ではUUIDなら何でもいいのか? UUIDv4なら列挙は実質不可能なので、URLに出しても「列挙リスク」は解消されます。ただしUUIDv7をURLに出すと、今度はタイムスタンプが漏れる。冒頭で見た通り、生成日時がミリ秒単位でわかってしまいます。
ちなみにTwitterのSnowflake IDはタイムスタンプ入りですが、ツイートIDとして全世界に公開されています。「タイムスタンプが漏れること」がどの程度のリスクかは、ビジネスの文脈次第です。ツイートの投稿日時は元々公開情報なので問題にならない。しかし、非公開APIの内部リソースIDなら話が違います。
まとめ: URLにIDを出すときに考えるべきは「主キーかどうか」ではなく、「そのID形式が列挙可能か」「情報を漏洩しないか」「将来変更する可能性があるか」の3点です。主キーと外部公開IDを分離するのは、セキュリティ対策というより設計の保険です。
PostgreSQL 18でInsert性能を比べてみた
理屈はわかった。では実際にどれくらい差が出るのか。PostgreSQL 18(2025年9月GA)のネイティブuuidv7()関数とPython 3.14のuuid.uuid7()を使って計測しました。
テスト環境
| 項目 | 内容 |
|---|---|
| DB | PostgreSQL 18 |
| 主キー型 | UUIDv4 / UUIDv7 / bigint GENERATED ALWAYS AS IDENTITY |
| インデックス | 主キーのB-Treeインデックスのみ |
| 計測 | 1万行 / 10万行 / 100万行のバルクInsert |
バルクInsert結果(単一セッション)
まず単一セッション(1クライアントから連続Insert)の結果です。検証コードはGitHubリポジトリで公開しています。
| 行数 | bigint | UUIDv7 | UUIDv4 |
|---|---|---|---|
| 1万行 | 0.02秒 | 0.02秒 | 0.02秒 |
| 10万行 | 0.17秒 | 0.20秒 | 0.26秒 |
| 100万行 | 1.63秒 | 2.16秒 | 2.98秒 |
1万行では差がまったくありません。3つとも0.02秒。1万行程度なら、ID形式は何を選んでも体感ゼロです。10万行で微差が見え始め、100万行になるとv7はv4より約28%速い。bigintはさらにその上です。
ここから2つのことが読み取れます。数万行規模のプロジェクトでは、ID形式の選択がInsert性能に影響することはまずありません。性能差が意味を持つのは数十万行以上のスケールです。
インデックスの内部構造も見てみた
PostgreSQLのpgstattuple拡張を使うと、B-Treeインデックスの内部状態を覗けます。100万行Insert後のインデックスを比較しました。
| 指標 | UUIDv7 | UUIDv4 |
|---|---|---|
| リーフページ数 | 3,832 | 4,839(約26%増) |
| インデックスサイズ | 30.1 MB | 38.0 MB(約26%増) |
| 平均リーフ密度 | 90.0% | 71.3% |
v4のインデックスはリーフページ数がv7より約26%多い。ランダムなIDがインデックス全体に散らばるため、ページスプリットで空き領域が生まれ続けた結果です。平均リーフ密度はv7が90.0%に対しv4は71.3%。v7は時間順に末尾へ追記されるので、ページが隙間なく詰まります。インデックスサイズもv4の方が約26%大きくなっています。
ただし、この差が読み取り性能に影響するかはワークロード次第です。範囲スキャン(例: 「直近1時間のレコード」)ならv7が圧倒的に有利ですが、ランダムな単一行ルックアップなら差は限定的です。
実用的なスキーマでは差が縮まる
上記のベンチマークはid + payload textの2カラムだけのテーブルです。実運用のテーブルはもっとカラムが多いし、外部キーもあります。条件を変えて再計測しました。
今度は13カラム + 外部キー制約つきの「ECサイトの商品テーブル」を想定したスキーマで、Faker(テストデータ生成ライブラリ)で生成したリアルに近い値を使いました。INSERT以外にSELECT・JOIN・UPDATEも計測しています。
-- 親テーブル: カテゴリマスタ(20件)
CREATE TABLE categories (
id uuid PRIMARY KEY DEFAULT uuidv7(), -- bigint / uuidv4 / uuidv7 で切替
name varchar(100) NOT NULL,
description text,
created_at timestamptz DEFAULT now()
);
-- 子テーブル: 商品(100万件)
CREATE TABLE products (
id uuid PRIMARY KEY DEFAULT uuidv7(), -- ← 検証対象の主キー
category_id uuid NOT NULL REFERENCES categories(id), -- 外部キー
name varchar(200) NOT NULL,
description text,
price numeric(10,2) NOT NULL,
stock integer NOT NULL DEFAULT 0,
sku varchar(50),
weight_kg numeric(6,2),
is_active boolean DEFAULT true,
rating numeric(3,2),
tags text[],
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
-- 外部キーインデックス(実運用では普通つける)
CREATE INDEX idx_products_category ON products(category_id);各カラムにはFakerで生成したリアルに近い値が入っています。実際のデータはこんな感じです。
| カラム | 型 | サンプル値 | 生成方法 |
|---|---|---|---|
| name | varchar(200) | Innovative asymmetric hub | fake.catch_phrase() |
| description | text | Method pick entire check...(最大300文字) | fake.text(300) |
| price | numeric(10,2) | 3847.29 | random(1.0, 9999.99) |
| stock | integer | 4821 | random(0, 10000) |
| sku | varchar(50) | QWE-8432-PLM | fake.bothify('???-####-???') |
| weight_kg | numeric(6,2) | 12.45 | random(0.01, 50.0) |
| is_active | boolean | true | 75% true / 25% false |
| rating | numeric(3,2) | 4.23 | random(1.0, 5.0) |
| tags | text[] | {nature,color,east} | fake.words(1〜4) |
実アプリに近い行サイズ(text含めて数百バイト〜)になるよう意図しています。ミニマルベンチの「100文字固定payload」とは条件が大きく異なります。
| 操作(100万行) | bigint | UUIDv7 | UUIDv4 | v4 vs bigint |
|---|---|---|---|---|
| INSERT(全行) | 76.5秒 | 77.0秒 | 77.8秒 | 1.02x |
| SELECT by PK(1000回) | 0.075秒 | 0.063秒 | 0.077秒 | 1.03x |
| SELECT 範囲 by FK(100回) | 0.025秒 | 0.023秒 | 0.039秒 | 1.54x |
| JOIN集計(100回) | 6.93秒 | 7.21秒 | 7.31秒 | 1.05x |
| UPDATE by PK(1000回) | 0.081秒 | 0.089秒 | 0.082秒 | 1.01x |
INSERTの差がほぼ消えました。2カラムのミニマルテーブルでは100万行でbigint 1.63秒 vs v4 2.98秒(83%増)だったのが、13カラムの実用テーブルでは76.5秒 vs 77.8秒(2%増)。行データのI/Oがインデックス更新コストを飲み込んでいます。
一方、範囲クエリでは差が顕著です。外部キーによるカテゴリ内商品一覧の取得で、v4はbigintより54%遅い。v7はbigintとほぼ同等。v4のインデックスはリーフページが散在しているため、範囲スキャンで不利になります。
主キーによる単一行SELECT・UPDATE・JOIN集計では3タイプの差はほぼ誤差の範囲です。
| ストレージ(100万行) | bigint | UUIDv7 | UUIDv4 |
|---|---|---|---|
| テーブル | 430.9 MB | 447.0 MB | 447.2 MB |
| インデックス | 27.7 MB | 36.4 MB(31%増) | 44.0 MB(59%増) |
| 合計 | 458.7 MB | 483.6 MB(5%増) | 491.3 MB(7%増) |
ストレージはインデックスサイズに差が出ます。UUIDは16バイト、bigintは8バイトなので、主キーインデックス+外部キーインデックスの合計でv4は59%大きくなります。ただしテーブル本体のサイズが支配的なので、トータルでは7%増にとどまります。
つまり: 「UUIDはInsertが遅い」は2カラムのベンチマーク特有の話で、実用的なスキーマでは差がほぼ消えます。むしろ本番で効いてくるのは範囲クエリの性能とインデックスサイズです。v7はこの両方でv4より優れています。
高い並行度ではv7が逆転負けする
ここまでの結果は「単一セッションのバルクInsert」です。実運用ではWebアプリケーションの複数リクエストが同時にInsertを実行します。この条件ではv7の特性が裏目に出ることがあります。
v7は時系列順なので、全てのInsertがB-Treeインデックスの末尾のリーフページに集中します。これを「rightmost leaf contention」と呼びます。1つのページに対して複数セッションが同時に書き込もうとすると、ページレベルのロック競合が発生します。
実際に、Spring Boot + PostgreSQLの事例では、v4からv7に切り替えた結果、12,000 inserts/secから3,000以下に激減した報告があります。インフラは何も変えていない。主キーの型を変えただけです。
v4はどうか。ランダムなIDがインデックス全体に分散するので、異なるInsertが別々のリーフページに書き込む確率が高い。並行度が高いOLTPでは、このランダム性がむしろロック競合を減らす方向に働きます。
つまり: v7はバルクロードや単一セッションでは速い。でも、数百〜数千の同時接続があるOLTPでは、v4の方が安定する場合があります。「v7はInsertが速い」は常に正しいわけではありません。
対策としては、PostgreSQLのパーティショニングやfillfactorの調整でcontentionを分散できます。AWSのMarc Brooker氏(Distinguished Engineer)は、タイムスタンプ部をXOR演算で拡散する改変を提案しています。ただし、どちらも追加の設計・運用コストがかかります。
Python 3.14のuuid7()のランダム部は32bitしかない
これはセキュリティ上、見逃せない事実です。
Python 3.14のuuid.uuid7()は、RFC 9562のMethod 1(単調カウンタ方式)を採用しています。128bitの内訳は以下の通りです。
| フィールド | ビット数 | 用途 |
|---|---|---|
| unix_ts_ms | 48bit | Unixタイムスタンプ(ミリ秒) |
| ver | 4bit | バージョン(固定値 0111) |
| counter_hi | 12bit | 単調カウンタ(上位) |
| var | 2bit | バリアント(固定値 10) |
| counter_lo | 30bit | 単調カウンタ(下位) |
| random | 32bit | ランダム |
42bitのカウンタは同一ミリ秒内での単調増加を保証するために使われます。その代償として、ランダム部分はわずか32bit(約43億通り)に減ります。v4の122bit(約5.3 × 1036通り)と比べると、予測可能な空間が約290倍狭い。
何が問題か。同一ミリ秒内に生成されたIDが1つ漏れると、カウンタは+1ずつ増えるだけなので、隣接するIDを高確率で推測できます。
import uuid
# 同じミリ秒内で連続生成
ids = [uuid.uuid7() for _ in range(5)]
for id in ids:
# 下位32bitだけが変わり、カウンタは+1ずつ増加
print(id)これがパスワードリセットトークンだったら? 攻撃者が自分のアカウントでリセットを実行し、自分のトークン(UUIDv7)を取得します。被害者のリセットが同じミリ秒内に実行されていれば、カウンタを±数個ずらすだけで被害者のトークンにたどり着ける可能性があります。UUIDv1で実証されているSandwich Attackと同じ原理です。
鉄則: パスワードリセットトークン、APIキー、招待リンクなど、推測不能性が求められるIDにUUIDv7を使ってはいけません。UUIDv4か、secrets.token_urlsafe()などの専用関数を使ってください。
やってはいけない6パターン
ここまでの検証と調査で見つかった、ID設計の「地雷」をまとめます。
① パスワードリセットトークンにUUIDv7
前述の通り、Python 3.14実装ではランダム部が32bitしかなく、カウンタから隣接トークンを推測できます。セキュリティトークンにはUUIDv4またはsecretsモジュールを使ってください。
② 全テーブル一律bigintで始めて「後からUUIDに変える」
主キーの型変更は、その主キーを参照する全ての外部キーの型変更を伴います。深い関連構造を持つシステム(ORMのポリモーフィック関連、ページツリー構造など)では、事実上不可能に近い作業量になります。外部APIとして公開していれば、URLの破壊的変更も加わります。「後からで大丈夫」は、後になるほど大丈夫でなくなります。
③ Google SpannerやDynamoDBの主キーにUUIDv7
Spannerはキー範囲でデータをsplitに分散します。時系列順のUUIDv7は全ての書き込みが末尾splitに集中し、ホットスポットを生みます。Google公式ドキュメントが「主キーの先頭は非順序値にせよ」と明言しており、UUIDv4を推奨しています。単一splitの上限は約3,500 writes/secで、それを超えるとスケーラビリティが破綻します。
④ 注文IDにauto-incrementを使ってURLに公開
/orders/1482を見れば、競合は「1日に何件注文があるか」を推定できます。さらに、/orders/1483を叩けば他人の注文の有無がわかります。認可チェックで中身は守れますが、レコードの存在情報自体が漏れます。
⑤ 既存のuuidv7()ポリフィルのままPostgreSQL 18にアップグレード
PG18ではuuidv7()がネイティブ関数として追加されました。それ以前にSQL関数や拡張として独自にuuidv7()を定義していたアプリは、名前衝突でマイグレーションが停止します。AIプラットフォームのDifyで実際に発生しており、マネージドDB環境ではスーパーユーザー権限がないため手動でdropもできず、解決が困難でした。
⑥ 高並行OLTPの全テーブルをUUIDv7に統一
前述のrightmost leaf contentionにより、数千ops/secの並行Insertでは性能が大幅に劣化する可能性があります。書き込みが集中するテーブル(ログ、イベント、セッション等)では、v4を使うか、テーブルパーティショニングで書き込み先を分散させる設計を検討してください。
プロジェクトの性質で最適解は変わる
「結局どれを選べばいいのか」を判断するには、まずプロジェクトの性質を見ます。ここが最も重要なポイントです。
| プロジェクトの性質 | 推奨ID | 理由 |
|---|---|---|
| 単一サービス・単一DB (社内ツール、個人開発等) | bigint | 分散ID不要。8バイトで済む。JOIN・WHERE句も高速。 UUIDにする技術的メリットがない |
| 外部APIを持つWebサービス (SaaS、マーケットプレイス等) | UUID(v4 or v7) | IDがクライアントに露出する。後からbigint→UUIDへの 移行は全APIの破壊的変更になる |
| マイクロサービス構成 | UUIDv7推奨 | 各サービスが独立にID生成。時系列ソートで サービス間の因果関係を追跡しやすい |
| オフラインファースト (モバイルアプリ同期等) | UUIDv4推奨 | クライアント側でID生成→後でサーバーと同期。 bigintは衝突する。v7はクライアント時刻が信用できない |
| 分散DB (Spanner、CockroachDB等) | UUIDv4 | 時系列順IDは書き込みホットスポットを生む。 Google公式がv4を推奨 |
| 大規模イベントストリーミング (ログ基盤、IoT等) | UUIDv7 | 時系列順+ユニーク性が必須。created_atカラムの 代替にもなり、カラム数を削減できる |
「単一サービスならbigintで十分」は正しい。でも「外部APIをいずれ提供する予定がある」「マイクロサービス化を見据えている」なら、初期設計でUUIDを選んでおく方が合理的です。主キーの型変更は後から行うと外部キーとAPI全体に波及する大手術になるため、初期設計の時点で判断すべきです。
テーブルの役割でも使い分ける
同じプロジェクト内でも、テーブルの役割によって最適なIDは変わります。全テーブル同じID体系にする必要はありません。
| テーブルの性質 | 推奨ID | やってはいけないこと |
|---|---|---|
| ユーザー・注文・請求書 (APIで外部露出) | 内部: UUIDv7 外部: prefixed ID or nanoid | bigintで公開 → 列挙攻撃。 後からID体系変更 → APIの破壊的変更 |
| パスワードリセット APIキー・招待トークン | UUIDv4 orsecrets.token_urlsafe() | UUIDv7 → 隣接IDを推測される (Sandwich Attack) |
| 設定・マスタデータ カテゴリ・権限ロール | bigint or 自然キー(文字列コード) | UUIDは過剰。レコード数が少なく 変更頻度も低いテーブルにUUIDは不要 |
| 監査ログ・イベント履歴 | UUIDv7 | bigint → シャーディング時に衝突。 UUIDv4 → 時系列ソートできず運用で困る |
| シャーディング予定のテーブル | UUID(v4 or v7) | bigintでシャード → マージ時にID衝突。 回収不可能な技術負債になる |
| 高書き込み頻度テーブル (セッション、キャッシュ等) | UUIDv4 or bigint (並行度による) | UUIDv7を安易に選択 → rightmost leaf contentionで性能劣化 |
ポイントは「全テーブル同じにしなくていい」ということです。外部に露出するリソーステーブルはUUIDv7 + 公開用nanoid、セキュリティトークンはUUIDv4、内部の設定テーブルはbigint。こうした使い分けは、StripeやGitHubなど大手のAPIでも実際に行われています。
大手テック企業は実際にどうしているか
「一流企業のベストプラクティス」を見てみましょう。ただし、彼らの設計判断はそのまま小規模プロジェクトに適用できるわけではありません。規模と文脈が全然違います。参考として見てください。
Stripe: prefixed ID
Stripeはcus_NffrFeUfNV2Hib(顧客)、pi_3MtwBwLkdIwHu7ix28a3tqPa(決済)のようなprefixed IDを使っています。プレフィックスでリソースの種類が即座にわかるため、サポートログを見たときに「これは顧客IDだな」と一目瞭然です。内部の主キーとは別に生成されており、APIの公開IDとして機能しています。
目的はセキュリティではありません。人間可読性と多型ルックアップ(IDを見ただけでどのテーブルを引けばいいかわかる)のためです。
Twitter/X: Snowflake ID
Twitter(現X)は独自のSnowflake IDを使っています。64bitの整数で、タイムスタンプ(41bit)+ データセンターID(5bit)+ ワーカーID(5bit)+ シーケンス番号(12bit)という構成です。
タイムスタンプが含まれていますが、ツイートIDとして全世界に公開されています。ツイートの投稿日時は元々公開情報なので、タイムスタンプの漏洩が問題にならないケースです。Discordも同じSnowflake方式を採用しています。
Buildkite: 連番→UUIDv7への移行
CI/CDサービスのBuildkiteは、連番整数からUUIDv7への移行を実施しました。結果、WAL(Write Ahead Log)レートが50%削減され、Write I/Oも同等の削減を達成しています。
移行の動機はPostgreSQLのシャーディング計画でした。分散環境では連番整数のユニーク性を保証するのが現実的でないため、UUIDv7に統一して「連番PK + UUID外部ID」の二重管理を解消しています。逆に言えば、シャーディングの予定がなければこの移行は必要なかったとも言えます。
Google Spanner: UUIDv4を推奨
GoogleのCloud Spannerは、前述の通りUUIDv7がアンチパターンになるDBです。公式ドキュメントで「主キーの先頭は非順序値にせよ」と明記しており、UUIDv4またはビット反転した連番を推奨しています。
共通点: 大手は「1つのID体系に統一」していません。用途に応じて使い分けています。そして「外部公開IDと内部主キーの分離」は、セキュリティ以上に人間可読性と保守性のために行われています。
UUID以外の選択肢も知っておく
UUID以外にも時系列ソート可能なID形式があります。
| ID形式 | 長さ | 時系列ソート | RFC標準 | 主な用途 |
|---|---|---|---|---|
| UUIDv7 | 36文字 | 可能 | RFC 9562 | RDBの主キー(B-Tree最適化) |
| ULID | 26文字 | 可能 | なし | UUIDv7と機能的にほぼ同等。Base32で短い |
| KSUID | 27文字 | 可能 | なし | Segment社開発。Stripe的なprefixed IDと相性良 |
| nanoid | 21文字 | 不可 | なし | URL用の短いID。主キーには不向き |
| CUID2 | 可変 | 不可 | なし | 衝突耐性重視。クライアントサイド生成向け |
| Snowflake ID | 64bit整数 | 可能 | なし | Twitter/Discord採用。中央採番サーバー必要 |
ULIDとUUIDv7は機能的にほぼ同等ですが、UUIDv7はRFC標準であることが決定的な差です。DB側のネイティブUUID型にそのまま格納でき、各言語の標準ライブラリでサポートされつつあります。新規プロジェクトならUUIDv7を選ぶ方が互換性の面で有利です。既存でULIDを使っているなら、無理に移行する必要はありません。
nanoidは「主キーの代替」ではなく「外部公開用IDの追加」として併用するのが最も合理的です。UUIDv7の主キーでDB性能を最適化しつつ、URLにはnanoidの短いIDを出す構成です。
2026年4月時点のエコシステム対応状況
「使いたいけど、自分の環境で使えるのか」。2026年4月時点の対応状況をまとめました。
言語・ランタイム
| 環境 | 対応状況 | 備考 |
|---|---|---|
| Python 3.14 | ✅ 標準ライブラリ | uuid.uuid7()。ただしランダム部32bit |
| Go | ✅ google/uuid | uuid.NewV7() |
| Rust | ✅ uuid クレート | Uuid::now_v7() |
| Node.js | ❌ 標準未対応 | npmのuuidパッケージまたはBunの Bun.randomUUIDv7()が必要 |
| Java | ❌ JDK標準未対応 | java-uuid-generator等のライブラリ必要 |
データベース
| DB | 対応状況 | 備考 |
|---|---|---|
| PostgreSQL 18 | ✅ ネイティブ | uuidv7()関数。既存ポリフィルとの名前衝突に注意 |
| MariaDB 11.7 | ✅ サポート追加 | v4/v7サポート。timestamp抽出は非対応 |
| MySQL | ❌ v1のみ | v7はアプリケーション層で生成が必要 |
フレームワーク/ORM
| フレームワーク | 対応状況 | 備考 |
|---|---|---|
| Laravel 9.30+ | ✅ デフォルトv7 | HasUuidsトレイトで自動生成 |
| Django 5.2 | ✅ Python 3.14 + PG18前提 | UUIDFieldでdefault=uuid.uuid7 |
| Rails | ⚠️ 手動設定 | デフォルトはv4。gem + コールバックが必要 |
| Spring Boot | ⚠️ ライブラリ必要 | JDK未対応のためライブラリ頼み |
Python + PostgreSQLの組み合わせが最も手軽です。両方とも標準でv7をサポートしており、ライブラリ追加なしで使えます。Node.js + MySQLの組み合わせは、両側でライブラリが必要なので、採用する場合はそのコストを織り込んでください。
注意: UUIDv7は2024年5月にRFC 9562として正式化されましたが、AWSのMarc Brooker氏が設計上の4つの課題(情報漏洩、エントロピー低下、相関行動、UI表示)を指摘し修正を提案しています。標準化されたとはいえ、まだ進化の途上にある規格です。この記事の情報は2026年4月時点のスナップショットとして読んでください。
判断フローチャート
ここまでの内容を1枚のフローチャートにまとめます。初期設計時にこの順番で考えてください。
加えて、テーブル単位では以下を確認してください。
- → セキュリティトークン(パスワードリセット、APIキー等)→ UUIDv4または
secrets.token_urlsafe() - → 設定・マスタデータ(レコード数が少ない)→ bigintで十分
- → 監査ログ・イベント履歴 → UUIDv7(時系列ソート+ユニーク性)
- → 高書き込み頻度テーブル → UUIDv4(rightmost leaf contention回避)
- → シャーディング予定 → UUID(bigintはマージ時にID衝突)
まとめ: 上位互換ではなく、用途特化の正統進化
UUIDv7はいい技術です。B-Tree系RDBのInsert性能を改善し、時系列ソートを可能にし、分散環境でユニークなIDを生成できる。RFC標準にもなった。
でも「上位互換」ではありません。
- • ランダム性が求められるセキュリティトークンにはv4が適切(v7のランダム部はPython実装で32bit)
- • 分散DB(Spanner等)ではv7はアンチパターン(ホットスポット)
- • 高並行OLTPではv7がv4より遅くなることがある(rightmost leaf contention)
- • 数万行規模では、そもそもbigintとの性能差がない
- • URLにIDを出す際の安全性は、ID形式ではなく認可チェックで担保する
「とりあえず全部UUIDv7にしておけばOK」は、「とりあえず全部Reactで書いておけばOK」と同じくらい雑なアドバイスです。技術選定には文脈が要る。プロジェクトの規模、テーブルの役割、将来のスケール計画。それを見ないで形式だけ真似しても、過剰設計か設計ミスのどちらかにしかなりません。
この記事で整理した判断基準を使えば、「なぜこのIDを選んだか」を説明できるようになるはずです。それが「脊髄反射でUUIDv4」や「UUIDv7最強」から一歩先に進むための出発点だと思います。
なお、UUIDv7自体がまだ進化中の規格です。AWSのDistinguished Engineerが設計上の課題を指摘し修正を提案しているように、今後もベストプラクティスは変わる可能性があります。この記事は2026年4月時点のスナップショットとして読んでいただければと思います。
参照元
- • RFC 9562: Universally Unique IDentifiers (UUIDs)(2024年5月正式化)
- • Python 3.14 uuid モジュール(uuid7() のリファレンス)
- • UUIDv7 Comes to PostgreSQL 18(The Nile)
- • PostgreSQL UUID Performance Benchmarking(DEV Community)
- • Fixing UUIDv7 (for database use-cases)(Marc Brooker, AWS Distinguished Engineer)
- • UUID v7 in PostgreSQL: The Tiny Choice That Quietly Destroys Insert Performance(Medium)
- • OWASP API Security Top 10 - API1:2023 Broken Object Level Authorization
- • Sandwich Attack UUID v1(IBM PTC Security)
- • Designing APIs for Humans: Object IDs(Stripe ID設計解説)
- • Goodbye to sequential integers, hello UUIDv7!(Buildkite)
- • Spanner Schema Design Best Practices(Google Cloud)
- • Understanding UUIDv7 and Its Impact on Cloud Spanner(Google Cloud Blog)
- • Dify: Migration fails on PostgreSQL 18 due to uuidv7 function conflict(GitHub Issue)
- • How to use UUIDv7 in Python, Django and PostgreSQL
- • Hacker News: UUIDv7 timing attack discussion
- • 本記事の検証コード(GitHub)