データベースの主キーにUUID v4を採用しているシステムで、レコード数が数百万件を超えたあたりからINSERTの遅延やインデックス肥大化に悩むケースは少なくありません。原因はUUID v4の「完全ランダム性」がB-treeインデックスと相性が悪いことにあります。UUID v7は、先頭48ビットにUnixタイムスタンプを埋め込むことでこの問題を根本的に解消し、時系列順の挿入を実現します。
2024年5月に正式公開されたRFC 9562でUUID v7は国際標準となり、2025年にはPostgreSQL 18がネイティブの uuidv7() 関数を搭載しました。本番環境でUUID v4からv7へ切り替えたBuildkite社はWAL(先行書き込みログ)生成量が50%削減されたと報告しています(出典: Buildkite Engineering Blog)。
UUID v7がDB性能を改善する仕組み
B-treeインデックスとランダムIDの問題
リレーショナルデータベースの主キーインデックスにはB-tree構造が広く使われています。B-treeはソート済みデータの検索・挿入に最適化されており、連番IDのように単調増加する値であれば、新しいレコードは常にインデックスの末尾ページに追記されます。
UUID v4は128ビット中122ビットが乱数で構成されるため、生成されるIDにはまったく順序性がありません。これをB-treeインデックスの主キーにすると、次の3つの性能劣化が発生します。
| 問題 | 原因 | 影響 |
|---|---|---|
| ページスプリットの頻発 | ランダムな位置へ挿入するため、既存リーフページが満杯だと分割が必要になる | INSERT時のディスクI/O増加、WAL書き込み量の増大 |
| バッファキャッシュの非効率化 | アクセスするページが分散するため、キャッシュヒット率が低下する | SELECTでもディスク読み込みが増える |
| インデックスの肥大化 | ページスプリットでリーフページの充填率が下がる | ストレージ消費とメモリ使用量の増加 |
UUID v7の時系列ソート性が解決する理由
UUID v7は先頭48ビットにミリ秒精度のUnixタイムスタンプを格納します。そのため、新しいIDは常に以前のIDより大きくなり、B-treeインデックスの末尾への追記パターンが成立します。これは連番IDと同等のインデックス効率を、分散ID生成の利便性と両立させる設計です。
UUID v7のビット構造(RFC 9562準拠)
RFC 9562(出典: IETF)で定義されたUUID v7の128ビット構造は次のとおりです。
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms (48 bit) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms | ver | rand_a |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| フィールド | ビット数 | 内容 |
|---|---|---|
| unix_ts_ms | 48 | Unixエポックからのミリ秒タイムスタンプ |
| ver | 4 | バージョン番号(0111 = 7) |
| rand_a | 12 | ランダムまたは単調増加カウンタ |
| var | 2 | バリアント(10) |
| rand_b | 62 | ランダムビット |
UUID v4は122ビットがランダムであるのに対し、UUID v7のランダム部分は74ビット(rand_a 12ビット + rand_b 62ビット)です。ランダムビットが48ビット分少ないものの、タイムスタンプによるミリ秒単位の分離があるため、同一ミリ秒内でも2^74(約1.9×10^22)通りの衝突耐性を持ちます。
パフォーマンス比較 — 実測ベンチマークデータ
INSERT性能
credativ社がPostgreSQL 18で実施したベンチマーク(出典: credativ)では、空テーブルへの5,000万行INSERTにおいて以下の差が測定されています。
| ID方式 | 5,000万行INSERT所要時間 |
|---|---|
| UUID v7 | 約1分46秒 |
| UUID v4 | 約20分 |
UUID v7はv4の約10倍の速度でINSERTを完了しています。2バッチ目(追加5,000万行)ではさらに差が広がったと報告されています。
フューチャー技術ブログの検証(出典: フューチャー技術ブログ)でも、PostgreSQL 18上でアプリ側からUUID v7を生成してINSERTした場合、v4と比較して**約20%**処理時間が短縮されたと報告されています。Macbook Air M3上で100万行の検証を実施し、3回平均を取った結果です。
SELECT性能
同じcredativ社の検証では、ORDER BY idクエリでUUID v7がv4の約3倍高速でした。これはインデックスのリーフページが物理的に連続配置されるため、シーケンシャルリードが効くためです。
フューチャー技術ブログの検証でも、100万行のうち直近5%(5万件)の範囲からランダムに20レコードを検索するクエリを1万回実行した場合、UUID v7は約10%高速という結果が報告されています。直近1%の範囲に限定するとさらに10%程度速くなる傾向が見られたとのことです。
インデックスサイズと断片化
credativ社のベンチマークでは、UUID v7の主キーインデックスはv4より26〜27%小さく、リーフページの平均充填密度も高い結果が得られています。
インデックスのリーフページの物理的な連続性にも大きな差が出ています。
| 指標 | UUID v4 | UUID v7 |
|---|---|---|
| 連続リンク数 | 0 | 3,812 |
| 非連続リンク数 | 4,860 | 19 |
UUID v4ではリーフページがディスク上に完全に分散している(連続リンク0)のに対し、UUID v7ではほぼ全てが連続配置されています。
WAL(先行書き込みログ)への影響
Buildkite社はUUID v4からv7への切り替え後、プライマリデータベースのWAL生成量が50%削減されたと報告しています(出典: Buildkite Engineering Blog)。書き込みI/Oも同程度に減少しており、クラウド環境でのIOPSコスト削減にも直結します。
ページスプリットが発生するたびにWALへの追加書き込みが必要になるため、UUID v7による順次挿入パターンがWAL生成を大幅に抑制しているのです。
データベース別のUUID v7対応状況
| データベース | UUID v7対応 | 備考 |
|---|---|---|
| PostgreSQL 18+ | ネイティブ対応 | uuidv7() 関数で生成可能 |
| PostgreSQL 17以前 | 拡張で対応 | pg_uuidv7 拡張を利用(出典: PGXN) |
| MySQL (Community) | 非対応 | アプリ側で生成してBINARY(16)カラムに格納する運用が主流 |
| Percona Server for MySQL | コンポーネントで対応 | UUID_VX コンポーネントでv7生成・タイムスタンプ抽出が可能(出典: Percona) |
| MariaDB 11.7+ | ネイティブ対応 | UUID_v7() 関数を搭載(出典: MariaDB) |
| SQLite | 非対応 | アプリ側で生成してBLOBまたはTEXT型に格納 |
PostgreSQL 18でのUUID v7生成
-- UUID v7を生成
SELECT uuidv7();
-- 結果例: 01938f6e-88c0-7f2a-b93c-4a1d8e5f9c70
-- テーブル定義での使用
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT uuidv7(),
customer_name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
PostgreSQL 17以前でも pg_uuidv7 拡張を導入すれば同様の機能を利用できます。
-- PostgreSQL 17以前
CREATE EXTENSION IF NOT EXISTS pg_uuidv7;
SELECT uuid_generate_v7();
MySQLでの運用パターン
MySQL Community Editionには現時点でUUID v7のネイティブサポートがありません。アプリケーション層で生成した値を格納する方法が一般的です。
-- MySQLでのテーブル定義(アプリ側でUUID v7を生成して格納)
CREATE TABLE orders (
id BINARY(16) PRIMARY KEY,
customer_name VARCHAR(255) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
);
-- BINARY(16)にはハイフンなしの16バイト値を格納
-- 検索時はBIN_TO_UUID()で可読形式に変換
SELECT BIN_TO_UUID(id) AS id, customer_name FROM orders;
MySQLでUUID v4の代わりにv7を使う場合、BINARY(16) 型で格納するとインデックス効率はv4と比較して大幅に改善されます。InnoDB(MySQLのデフォルトストレージエンジン)はクラスタードインデックスを採用しており、主キーの物理的な並び順でデータを格納するため、時系列順のUUID v7は連番IDに近いI/Oパターンとなります。
各言語でのUUID v7生成コード例
Python(3.14以降)
Python 3.14ではuuidモジュールに uuid7() が標準追加されました。
import uuid
# UUID v7を生成
new_id = uuid.uuid7()
print(new_id)
# 出力例: 01938f6e-88c0-7f2a-b93c-4a1d8e5f9c70
# タイムスタンプ部分の抽出(ミリ秒)
timestamp_ms = new_id.int >> 80
print(f"生成時刻(ms): {timestamp_ms}")
Python 3.13以前では uuid7 パッケージ(pip install uuid7)を利用します。
JavaScript / TypeScript(Node.js)
import { v7 as uuidv7 } from 'uuid';
// UUID v7を生成
const id = uuidv7();
console.log(id);
// 出力例: 01938f6e-88c0-7f2a-b93c-4a1d8e5f9c70
uuid パッケージ v10.0以降でv7がサポートされています(npm install uuid)。
Go
package main
import (
"fmt"
"github.com/google/uuid"
)
func main() {
// UUID v7を生成
id, err := uuid.NewV7()
if err != nil {
panic(err)
}
fmt.Println(id)
}
github.com/google/uuid パッケージ v1.5以降で NewV7() が利用可能です。
Rust
use uuid::Uuid;
fn main() {
// UUID v7を生成
let id = Uuid::now_v7();
println!("{}", id);
}
uuid クレート v1.2以降で Uuid::now_v7() を利用します。Cargo.toml には uuid = { version = "1", features = ["v7"] } を追加してください。
v4からv7への移行で考慮すべきポイント
タイムスタンプの情報漏洩リスク
UUID v7の先頭48ビットからレコードのおおよその生成時刻が推定できます。外部APIレスポンスやURLパスにIDを含めるシステムでは、生成時刻の露出が問題になるか評価が必要です。
対策として、外部公開するIDは別途ランダムトークンを用意し、主キーのUUID v7は内部識別子として運用する設計が有効です。
既存テーブルとの混在
既存テーブルのUUID v4カラムにUUID v7の値を格納しても、UUID型のバリデーションやストレージサイズには影響しません。ただし、v4とv7が混在するカラムではインデックスの局所性が部分的にしか得られません。新規レコードのみv7にする場合、v7レコードが大多数を占めるようになるまでは性能改善効果は限定的です。
クロック同期の重要性
分散システムで複数ノードがUUID v7を生成する場合、ノード間のクロックがずれているとID順序とイベント発生順序が一致しなくなります。NTPによるクロック同期を適切に運用する必要があります。
ただし、UUID v7のタイムスタンプはあくまで「おおよその生成順」を保証するものであり、厳密なイベント順序の保証には別途論理クロックやシーケンス番号が必要です。
UUID v7とULIDの選択基準
UUID v7と類似の時系列IDとしてULIDがあります。両者の主な違いは以下のとおりです。
| 項目 | UUID v7 | ULID |
|---|---|---|
| 標準化 | RFC 9562(IETF標準) | コミュニティ仕様 |
| サイズ | 128ビット(36文字表記) | 128ビット(26文字Crockford Base32表記) |
| タイムスタンプ精度 | ミリ秒 | ミリ秒 |
| ランダム部分 | 74ビット | 80ビット |
| DB互換性 | UUID型にそのまま格納可能 | VARCHAR/BINARY型での格納が必要 |
| バージョン/バリアント | 6ビットを使用 | なし |
PostgreSQLやMariaDBのようにUUID型をネイティブサポートするDBを使用している場合、UUID v7はカラム型・インデックス・ユーティリティ関数との互換性で優位です。ULIDはUUID型カラムに直接格納できないため、文字列型での運用が必要になりストレージ効率が低下します。
まとめ
UUID v7はRFC 9562で標準化された時系列ソート可能なUUID仕様です。先頭48ビットのUnixタイムスタンプにより、B-treeインデックスへの順次挿入パターンが成立し、UUID v4と比較して以下のパフォーマンス改善が実測されています。
- INSERT速度: 5,000万行で最大約10倍高速(credativ社ベンチマーク)
- SELECT速度: ORDER BYクエリで約3倍高速
- インデックスサイズ: 26〜27%削減
- WAL生成量: 本番環境で50%削減(Buildkite社実績)
PostgreSQL 18とMariaDB 11.7以降ではネイティブ関数で生成でき、MySQL環境でもPercona ServerのUUID_VXコンポーネントやアプリ層での生成で導入が可能です。Python 3.14、Node.js(uuidパッケージ v10+)、Go、Rustなど主要言語のライブラリも対応済みのため、新規プロジェクトの主キーとしてUUID v7を採用する障壁は低くなっています。
