ラボまとめコラムニュース
ブログ/記事一覧/【検証】UUIDv4・v7・bigint、結局どれを選ぶ?プロジェクト別ID設計の実験
uuid-v4-v7-bigint-primary-key-design-cover-ja

【検証】UUIDv4・v7・bigint、結局どれを選ぶ?プロジェクト別ID設計の実験

「UUIDv7はv4の上位互換」は本当か。PostgreSQL 18とPython 3.14でInsert性能を実測し、プロジェクト規模・テーブル用途別に最適なID戦略を整理した。

ラボ
kkm-horikawa

kkm

Backend Engineer / AWS / Django

2026.04.1318 min8 views
この記事のポイント

「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が標準化されました。

項目UUIDv4UUIDv7bigint auto-increment
構造122bitランダム48bitタイムスタンプ
+ 74bitランダム/カウンタ
64bit連番
サイズ16バイト(36文字)16バイト(36文字)8バイト
時系列ソート不可可能可能
DB問い合わせなしの
ID生成
可能可能不可(DB採番)
ランダム性122bit74bit(仕様上)
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()を使って計測しました。

テスト環境

項目内容
DBPostgreSQL 18
主キー型UUIDv4 / UUIDv7 / bigint GENERATED ALWAYS AS IDENTITY
インデックス主キーのB-Treeインデックスのみ
計測1万行 / 10万行 / 100万行のバルクInsert

バルクInsert結果(単一セッション)

まず単一セッション(1クライアントから連続Insert)の結果です。検証コードはGitHubリポジトリで公開しています。

行数bigintUUIDv7UUIDv4
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後のインデックスを比較しました。

指標UUIDv7UUIDv4
リーフページ数3,8324,839(約26%増)
インデックスサイズ30.1 MB38.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で生成したリアルに近い値が入っています。実際のデータはこんな感じです。

カラムサンプル値生成方法
namevarchar(200)Innovative asymmetric hubfake.catch_phrase()
descriptiontextMethod pick entire check...(最大300文字)fake.text(300)
pricenumeric(10,2)3847.29random(1.0, 9999.99)
stockinteger4821random(0, 10000)
skuvarchar(50)QWE-8432-PLMfake.bothify('???-####-???')
weight_kgnumeric(6,2)12.45random(0.01, 50.0)
is_activebooleantrue75% true / 25% false
ratingnumeric(3,2)4.23random(1.0, 5.0)
tagstext[]{nature,color,east}fake.words(1〜4)

実アプリに近い行サイズ(text含めて数百バイト〜)になるよう意図しています。ミニマルベンチの「100文字固定payload」とは条件が大きく異なります。

操作(100万行)bigintUUIDv7UUIDv4v4 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万行)bigintUUIDv7UUIDv4
テーブル430.9 MB447.0 MB447.2 MB
インデックス27.7 MB36.4 MB(31%増)44.0 MB(59%増)
合計458.7 MB483.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_ms48bitUnixタイムスタンプ(ミリ秒)
ver4bitバージョン(固定値 0111)
counter_hi12bit単調カウンタ(上位)
var2bitバリアント(固定値 10)
counter_lo30bit単調カウンタ(下位)
random32bitランダム

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 or
secrets.token_urlsafe()
UUIDv7 → 隣接IDを推測される
(Sandwich Attack)
設定・マスタデータ
カテゴリ・権限ロール
bigint or
自然キー(文字列コード)
UUIDは過剰。レコード数が少なく
変更頻度も低いテーブルにUUIDは不要
監査ログ・イベント履歴UUIDv7bigint → シャーディング時に衝突。
UUIDv4 → 時系列ソートできず運用で困る
シャーディング予定のテーブルUUID(v4 or v7)bigintでシャード → マージ時にID衝突。
回収不可能な技術負債になる
高書き込み頻度テーブル
(セッション、キャッシュ等)
UUIDv4 or bigint
(並行度による)
UUIDv7を安易に選択 →
rightmost leaf contentionで性能劣化

ポイントは「全テーブル同じにしなくていい」ということです。外部に露出するリソーステーブルはUUIDv7 + 公開用nanoid、セキュリティトークンはUUIDv4、内部の設定テーブルはbigint。こうした使い分けは、StripeやGitHubなど大手のAPIでも実際に行われています。

大手テック企業は実際にどうしているか

「一流企業のベストプラクティス」を見てみましょう。ただし、彼らの設計判断はそのまま小規模プロジェクトに適用できるわけではありません。規模と文脈が全然違います。参考として見てください。

Stripe: prefixed ID

Stripecus_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標準主な用途
UUIDv736文字可能RFC 9562RDBの主キー(B-Tree最適化)
ULID26文字可能なしUUIDv7と機能的にほぼ同等。Base32で短い
KSUID27文字可能なしSegment社開発。Stripe的なprefixed IDと相性良
nanoid21文字不可なしURL用の短いID。主キーには不向き
CUID2可変不可なし衝突耐性重視。クライアントサイド生成向け
Snowflake ID64bit整数可能なし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/uuiduuid.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+✅ デフォルトv7HasUuidsトレイトで自動生成
Django 5.2✅ Python 3.14 + PG18前提UUIDFielddefault=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枚のフローチャートにまとめます。初期設計時にこの順番で考えてください。

新規プロジェクト?No既存ID体系を維持必要箇所だけ追加Yes分散ID生成が必要?マイクロサービス / マルチDB / クライアント生成NobigintYesDBの種類は?分散DB(Spanner等)UUIDv4B-Tree系RDB高並行Write?数千 ops/sec 以上YesUUIDv4 orパーティションNoUUIDv7 を主キーに外部APIにID公開?NoUUIDv7のままYes内部: UUIDv7(主キー)外部: prefixed ID or nanoidテーブル単位の例外:セキュリティトークン → UUIDv4  設定・マスタ → bigint  監査ログ → UUIDv7  高書き込み頻度 → UUIDv4シャーディング予定 → UUID(bigintはマージ時にID衝突)

加えて、テーブル単位では以下を確認してください。

  • セキュリティトークン(パスワードリセット、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月時点のスナップショットとして読んでいただければと思います。

参照元