UUIDv7は、2024年5月公開のRFC 9562で定められたUUIDの形式です。128ビットの先頭48ビットにUnix時刻(ミリ秒)を入れ、バージョンとバリアントの計6ビットを除く領域に乱数やカウンタを配置するため、生成した時刻の順におおむね並びます。表記は従来のUUIDと同じ36文字で、既存のuuid型の列にそのまま入ります。
2026年10月時点では、Python 3.14、Java 26、Node.js 26.1.0/24.16.0、Go 1.27、.NET 9、PostgreSQL 18が標準機能としてUUIDv7を生成できます。この記事では、UUIDv4との違い、言語・DB・ORMごとの生成方法、それらを実行して確かめた結果、主キーとして保存するときの落とし穴を順に説明します。
まとめ:UUIDv7の要点と選び方
- 構造は「48ビットのミリ秒時刻+バージョン4ビット+rand_a 12ビット+バリアント2ビット+rand_b 62ビット」。文字列は36文字、バイナリは16バイトで、UUIDv4と長さは同じ
- UUIDv4との差は並び順。PostgreSQL 18で共有バッファを16MBに絞って300万行を入れると、v4は共有バッファ外からの読み込みが約179万ページ・INSERT 119秒、v7は5ページ・42秒だった
- 生成時刻が先頭12桁から読み取れる。作成日時を外部に知られたくないIDや、認可に使う値にはUUIDv4か専用のトークンを使う
- Java 25 LTS・MySQL(2026年10月公開の26.10.0を含む)・SQLiteには生成関数が無い。ライブラリかアプリ側、ORMの設定で生成する
- 同じミリ秒内の順序は、Python・PostgreSQL・Go・Rust・npmのuuidでは保たれ、Java 26とNode.jsの標準APIでは約半数が逆転した。IDの大小を作成順として使うなら実装を選ぶ
- SQL Serverの
uniqueidentifierは末尾6バイトから比較するため、UUIDv7を入れても時刻順には並ばない
UUIDv7のビット配置とRFC 9562での定義
RFC 9562はRFC 4122を置き換えたUUIDの現行仕様で、UUIDv6・v7・v8を新たに定義しました。RFC自身も5.7節で「Implementations SHOULD utilize UUIDv7 instead of UUIDv1 and UUIDv6 if possible.」と、時刻ベースならv7を使うよう求めています。バージョンごとの使い分けはUUIDのv1〜v8を比較した記事に譲り、ここではv7の中身に絞ります。
128ビットの内訳と36文字表記の対応
| フィールド | ビット数 | 文字列での位置 | 中身 |
|---|---|---|---|
| unix_ts_ms | 48 | 先頭12桁 | 1970年からのミリ秒 |
| ver | 4 | 3つ目の区切りの先頭1桁 | 常に7 |
| rand_a | 12 | ver直後の3桁 | 乱数・カウンタ・サブミリ秒 |
| var | 2 | 第4グループ先頭桁の上位2ビット | 常に二進数10 |
| rand_b | 62 | 残り | 乱数(一部をカウンタに使う実装あり) |
文字列は16進32桁をハイフン4本で8-4-4-4-12に区切った36文字です。varの2ビットが二進数10なので、第4グループの先頭桁は必ず8・9・a・bのどれかになります。DBにバイナリで保存すれば16バイトで、RFC 9562の6.13節は、文字列で持つと128ビットの値に288ビットを費やすとして、可能ならバイナリで保存するよう勧めています。48ビットのミリ秒は西暦10889年まで表現できます。
先頭12桁から生成時刻を読み出すコード
UUIDv7は時刻が先頭にあるので、右に80ビットずらせばミリ秒が取り出せます。Python 3.14ではUUID.timeがv7に対してこのミリ秒を返します。
import uuid, datetime
u = uuid.UUID("01a0e0e4-747b-7238-9526-d2802dd7cd3a")
ms = u.int >> 80
print(u.version, ms, u.time)
print(datetime.datetime.fromtimestamp(ms / 1000, datetime.timezone.utc))
# 7 1790479463547 1790479463547
# 2026-09-27 03:24:23.547000+00:00
PostgreSQL 18ならuuid_extract_timestamp()がv7の時刻をtimestamptzで返し、v4を渡すとNULLになります。文字列がv7かどうかだけを確かめたいときは、ハイフンを除いて数えた13桁目が7、17桁目が8〜bであることを見る正規表現^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$で足ります(大文字を受けるなら大文字小文字を無視する指定を付けます)。
UUIDv4との違い:乱数122ビットと時刻付き74ビット
UUIDv4は、version・variantの6ビットを除く122ビットがすべて乱数です。UUIDv7は先頭48ビットを時刻に使うため、乱数は最大74ビットに減る代わりに、値の大小が生成時刻の順になります。
| 項目 | UUIDv4 | UUIDv7 |
|---|---|---|
| 中身 | 乱数122ビット | 時刻48ビット+乱数最大74ビット |
| 生成順の並び | 無し | ミリ秒単位で並ぶ |
| 生成時刻の読み取り | 不可 | 先頭12桁から可能 |
| B-tree索引への挿入 | ランダムな位置 | ほぼ末尾への追記 |
| 文字数・バイト数 | 36文字・16バイト | 36文字・16バイト |
乱数が74ビットに減った分、衝突までの余裕はv4より小さくなります(同じミリ秒の中での見積もりは末尾のよくある質問に記載)。
PostgreSQL 18で比べた索引サイズとキャッシュ外の読み込み
並び順の差は主キー索引の詰まり方と、索引がメモリに収まらないときの読み込み量に出ます。PostgreSQL 18.6で、主キーidと整数列padを持つ表にgen_random_uuid()(v4)とuuidv7()で行を入れ、EXPLAIN (ANALYZE, BUFFERS)とpgstattuple拡張で調べました。2回目は索引が共有バッファに収まらないよう、shared_buffersを16MBにしています。
-- postgres -c shared_buffers=16MB で起動したクラスタ
CREATE EXTENSION pgstattuple;
CREATE TABLE t4 (id uuid PRIMARY KEY, pad int);
CREATE TABLE t7 (id uuid PRIMARY KEY, pad int);
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF, COSTS OFF)
INSERT INTO t4 SELECT gen_random_uuid(), n FROM generate_series(1, 3000000) n;
EXPLAIN (ANALYZE, BUFFERS, TIMING OFF, COSTS OFF)
INSERT INTO t7 SELECT uuidv7(), n FROM generate_series(1, 3000000) n;
SELECT 't4', avg_leaf_density, leaf_fragmentation FROM pgstatindex('t4_pkey')
UNION ALL
SELECT 't7', avg_leaf_density, leaf_fragmentation FROM pgstatindex('t7_pkey');
| 指標 | v4(gen_random_uuid) | v7(uuidv7) |
|---|---|---|
| 100万行・既定設定の索引サイズ | 38MB | 30MB |
| 100万行・既定設定のINSERT | 12.2秒 | 8.0秒 |
| 300万行・16MBの索引サイズ | 120MB | 90MB |
| 300万行・16MBのINSERT | 119.2秒 | 41.7秒 |
| 同 shared read(8KBページ) | 1,788,033 | 5 |
| 同 shared written(8KBページ) | 1,799,622 | 41,826 |
| 300万行・16MBの葉ページ充填率 | 67.83% | 89.98% |
| 300万行・16MBの葉ページ断片化率 | 49.7% | 0% |
v7は新しい値が常に索引の右端に入るので、葉ページが既定のfillfactor(90)近くまで埋まり、断片化も起きません。v4は挿入先が索引全体に散るため、16MBのバッファでは書き込むたびに追い出したページを読み戻すことになり、読み込みが約179万ページ(8KB換算で約13.6GiB)に達しました。読み込みにはOSのファイルキャッシュに当たった分も含みます。表本体はどちらも149MBなので、差は主キー索引の挿入位置から来ています。測定は2026年9月27日(100万行)と10月23日(300万行)にMacBook Pro(Core i9-9880H・メモリ16GB)で各1回行ったもので、絶対値ではなく差の向きと桁を見る材料です。
UUIDv4を選ぶべき場面と外部公開用IDの分け方
RFC 9562の8節は、埋め込まれた時刻を「a very small attack surface」と認めたうえで、何らかのセキュリティ操作に使うならUUIDv4を使うべきだとしています。会員IDや注文IDをURLに出す場合、v7だと登録日時や注文日時が誰にでも読み取れ、一定期間の登録件数も推測しやすくなります。同じ節は、UUIDを持っているだけでアクセスを許すsecurity capabilityとして使うこと自体をMUST NOTとしているので、パスワード再設定用のトークンなどはUUIDではなくsecrets.token_urlsafe()のような専用の乱数で作ります。
実務では、DBの主キーをv7にして、URLやAPIに出す識別子は別の列(v4か、推測できない短い文字列)で持つ構成が扱いやすくなります。v7をBase62などで短く符号化して外に出す方法もありますが、符号化しても時刻は復号できるので、日時を隠す目的には使えません。
言語・DB別のUUIDv7生成方法(2026年10月時点)
| 環境 | 関数・メソッド | 提供元 | 同一ミリ秒内の順序 |
|---|---|---|---|
| Python 3.14以降 | uuid.uuid7() | 標準ライブラリ | 保たれる |
| Java 26以降 | UUID.ofEpochMillis(ms) | 標準ライブラリ | 保証なし |
| Java 25以前 | UuidCreator.getTimeOrderedEpoch() | uuid-creator 6.1.1 | 保たれる |
| Node.js 26.1.0/24.16.0以降 | crypto.randomUUIDv7() | 標準モジュール | 保証なし |
| Node.js・ブラウザ | v7() | npm uuid 14.0.3 | 保たれる |
| Bun 1.1.34以降 | Bun.randomUUIDv7() | 組み込み | 保たれる(カウンタ) |
| Go 1.27以降 | uuid.NewV7() | 標準ライブラリ | 保たれる |
| Go 1.26以前 | uuid.NewV7() | google/uuid v1.6.0 | 保たれる |
| Rust | Uuid::now_v7() | uuid 1.28.0(v7 feature) | 保たれる |
| .NET 9以降 | Guid.CreateVersion7() | 標準ライブラリ | 保証なし |
| Kotlin 2.3以降 | Uuid.generateV7() | 標準ライブラリ(実験的API) | アプリ内で保たれる |
| Ruby 3.3以降 | SecureRandom.uuid_v7 | 標準ライブラリ | 既定では保証なし |
| PostgreSQL 18以降 | uuidv7() | 組み込み関数 | セッション内で保たれる |
| MariaDB 11.7以降 | UUID_v7() | 組み込み関数 | - |
「同一ミリ秒内の順序」は、後述の実測とソース・公式ドキュメントの記述によるものです。
Python:3.14のuuid.uuid7()と3.13以前の代替
Python 3.14のuuidモジュールにuuid6()・uuid7()・uuid8()が加わりました。コマンドラインからもpython3.14 -m uuid -u uuid7で1件出せます。
import uuid
u = uuid.uuid7()
print(u, u.version)
# 01a0e0e4-747b-7238-9526-d2802dd7cd3a 7
Python 3.13以前では、PyPIのuuid6(2025.0.1)かuuid-utils(Rust実装)を入れます。uuid6.uuid7()は標準のuuid.UUIDの派生クラスを返しますが、uuid_utils.uuid7()が返す独自のUUID型はisinstance(x, uuid.UUID)が偽になります。標準型を前提にするORMへ渡すならuuid_utils.compat.uuid7()を使います(Python 3.13.14・uuid-utils 1.0.0と最新の1.1.0で確認)。
Java:Java 26のUUID.ofEpochMillisとJava 25以前のuuid-creator
Java 26でjava.util.UUIDにofEpochMillis(long)が追加されました。2026年9月公開のJava 27でもv7を作るメソッドはこれだけです。現行LTSのJava 25のjava.util.UUIDにはv7を作るメソッドが無いため、LTSで運用するならライブラリを使います。
// Java 26以降:時刻は呼び出し側が渡す
UUID a = UUID.ofEpochMillis(System.currentTimeMillis());
// Java 25以前:com.github.f4b6a3:uuid-creator:6.1.1
UUID b = UuidCreator.getTimeOrderedEpoch();
ofEpochMillisは渡されたミリ秒の後ろを乱数で埋めるだけです。Javadocの@apiNoteも、単調なUUIDv7が欲しいなら渡すtimestampの単調性を呼び出し側が保証するよう求めています。負の値や48ビットを超える値にはIllegalArgumentExceptionを投げます。
JavaScript・Node.js・Bun:randomUUIDv7とnpmのuuid
import { randomUUIDv7 } from 'node:crypto'; // Node.js 26.1.0 / 24.16.0以降
import { v7 } from 'uuid'; // npm uuid 14.0.3
console.log(randomUUIDv7());
console.log(v7());
Node.jsのcryptoのrandomUUIDv7()は26.1.0と24.16.0で追加されました。公式ドキュメントは、時刻の後ろを「cryptographically secure random bits」で埋め、埋め込む時刻も「is not guaranteed to be strictly increasing」と説明しています。ブラウザのcrypto.randomUUID()が返すのはv4だけなので、フロントエンドやNode.js 22以前ではnpmのuuidパッケージのv7()を使います。Bunは1.1.34でBun.randomUUIDv7()を追加しており、同じ時刻で複数回呼ぶとカウンタを足して並ぶようにすると説明しています。
Go・Rust・.NET・Kotlin・PHPの生成関数
Goは1.27で標準ライブラリにuuidパッケージが入り、uuid.NewV7()はエラーを返さずに値を返します。ドキュメントには「always returns UUIDs which sort in increasing order, except when the system clock moves backwards」とあります。uuid.New()は現時点でNewV4()と同じです。1.26以前ではgithub.com/google/uuidのv1.6.0がNewV7()を提供します(戻り値にerrorを含みます)。
Rustはuuidクレートをfeatures = ["v7"]付きで入れればUuid::now_v7()が使えます。.NETは9でGuid.CreateVersion7()が入りました。Kotlinは2.3.0でUuid.generateV7()を追加し、2.4.0でUuid型そのものは安定版になりましたが、v4・v7の生成関数は実験的APIのままでオプトインが要ります。PHPは標準関数が無く、symfony/uid(8.1.9)のUuid::v7()か、ramsey/uuid(4.9.4)のUuid::uuid7()を使います。symfony/uidはマイクロ秒までの時刻を入れたうえで、同じミリ秒の中では乱数部を24ビットの乱数分だけ増やす方式(RFC 9562のMethod 2)で順序を保ちます。
// Go 1.27以降(標準ライブラリの uuid)
id := uuid.NewV7()
// Go 1.26以前(github.com/google/uuid v1.6.0)
id, err := uuid.NewV7()
if err != nil {
return err
}
// Rust(Cargo.toml: uuid = { version = "1.28", features = ["v7"] })
let id = uuid::Uuid::now_v7();
// C#(.NET 9以降)
var id = Guid.CreateVersion7();
PostgreSQL・MySQL・SQLiteでの生成
PostgreSQL 18は組み込みのuuidv7()を持ち、列の既定値にそのまま書け、引数にintervalを渡すと時刻をずらせます。
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT uuidv7(),
created_at timestamptz NOT NULL DEFAULT now()
);
SELECT uuidv7(interval '-1 hour'), uuid_extract_timestamp(uuidv7());
PostgreSQL 17以前で使われてきたuuid-ossp拡張にはv7を作る関数がありません。PostgreSQL 18.6で拡張の関数を一覧すると、生成系はuuid_generate_v1・v1mc・v3・v4・v5だけでした。17以前ではアプリ側で生成するか、v7を作る別の拡張を入れます。
MySQLには生成関数がありません。9.7の次から版番号が年月形式に変わり、2026年10月20日に26.10.0が出ましたが、そのソース(sql/item_create.cc)に登録されているUUID系の関数は、生成用がUUID()・UUID_SHORT()、変換・判定用がUUID_TO_BIN()・BIN_TO_UUID()・IS_UUID()のままで、UUID()が返すのはバージョン1です。アプリ側で生成してBINARY(16)に入れます。MariaDBは11.7でUUID_v7()を追加しています(MySQLとの差分はMariaDBとMySQLの違いをまとめた記事を参照)。SQLiteは本体にUUID関数が無く、公式の拡張ext/misc/uuid.cのuuid()もv4だけを返します。
ORM・フレームワークでの指定方法
ORMでは、DBの関数に任せる指定とアプリ側で生成する指定があります。前者は同一ミリ秒の順序をDBに任せられ、後者はDBの版に依存しません。
| ORM・FW | 指定 | 生成する場所・前提 |
|---|---|---|
| Prisma 5.18.0以降 | @default(uuid(7)) | Prismaのクライアント側 |
| Hibernate ORM 7.0以降 | @UuidGenerator(style = VERSION_7) | アプリ側(Incubating) |
| Django 6.1以降 | db_default=UUID7() | PG18・MariaDB 11.7・SQLite+Python 3.14 |
| Django(Python 3.14) | default=uuid.uuid7 | アプリ側 |
| Laravel 12 | HasUuidsトレイト | アプリ側(Str::uuid7()) |
| Rails 8.1 | default: -> { "uuidv7()" } |
PostgreSQL 18 |
| Drizzle ORM | .default(sql`uuidv7()`) |
PostgreSQL 18 |
Django 6.1はデータベース関数UUID7を追加しました。Rails 8.1はid: :uuidのcreate_tableに既定値を渡さないとgen_random_uuid()(v4)を使い、DrizzleのdefaultRandom()もgen_random_uuid()を既定値にするので、v7にするには既定値を明示します。HibernateのエンティティはJPAの@Idと組み合わせて書きます(書き方はJPAのエンティティクラスの記事を参照)。
# Django 6.1(PostgreSQL 18など)
from django.db import models
from django.db.models.functions import UUID7
class Order(models.Model):
id = models.UUIDField(primary_key=True, db_default=UUID7())
# Rails 8.1(PostgreSQL 18)のマイグレーション
create_table :orders, id: :uuid, default: -> { "uuidv7()" } do |t|
t.timestamps
end
// Hibernate ORM 7.0以降
@Id
@UuidGenerator(style = UuidGenerator.Style.VERSION_7)
private UUID id;
SupabaseなどマネージドのPostgreSQLでは、先にSELECT version();で18以上かを確かめ、17以前ならアプリ側生成にします(接続の組み方はSupabaseにPrismaとDrizzleをつなぐ記事を参照)。
同一ミリ秒内の生成順と実装別の単調性
UUIDv7が時刻順に並ぶのは、ミリ秒が異なる値どうしの話です。同じミリ秒に複数生成したときの並びについて、RFC 9562の6.2節は3つの方法を示し、バッチで生成する実装にはどれかを使うよう求めています(SHOULD)。
- Method 1(Fixed Bit-Length Dedicated Counter):時刻の直後にカウンタを置き、同じミリ秒なら1ずつ増やす
- Method 2(Monotonic Random):乱数部分そのものを、同じミリ秒なら乱数分だけ増やす
- Method 3(Replace Leftmost Random Bits with Increased Clock Precision):rand_aの最大12ビットにミリ秒未満の時刻を入れる
どれも使わず、ミリ秒の後ろを毎回新しい乱数で埋める実装もRFC上は正しいUUIDv7です。その場合、同じミリ秒に作った2つの値の大小は乱数で決まります。
8つの実装で20万件を連続生成した逆転回数
各実装で20万件を連続生成し、直前の値より文字列として小さくなった回数を数えました(2026年9月27日、MacBook Pro・Core i9-9880H)。
| 実装 | 同一ミリ秒の扱い | 逆転回数/20万 |
|---|---|---|
| Python 3.14.6 uuid.uuid7() | 42ビットカウンタ(Method 1) | 0 |
| PostgreSQL 18.6 uuidv7() | 12ビットのサブミリ秒(Method 3) | 0 |
| Go 1.27.1 uuid.NewV7 | 時計が戻らない限り増加 | 0 |
| Go google/uuid v1.6.0 | 12ビットのサブミリ秒 | 0 |
| Rust uuid 1.26.1 | カウンタ | 0 |
| npm uuid 14.0.2 | カウンタ | 0 |
| Java 26.0.2.1 UUID.ofEpochMillis | 乱数のみ | 99,816 |
| Node.js 26.5.0 crypto.randomUUIDv7 | 乱数のみ | 99,819 |
乱数だけの実装は、同じミリ秒に入った値のほぼ半分が直前より小さくなります。一方でミリ秒単位では並んでいるので、B-tree索引の局所性はほとんど損なわれません。困るのは、IDの大小を作成順として使う処理です。IDをカーソルにしたページネーションや、イベントをID順に再生する処理では、同じミリ秒の行が入れ替わって見えます。こうした用途があるなら、Java 26とNode.jsの標準API、.NETのGuid.CreateVersion7()、既定引数のRubyのuuid_v7は避け、カウンタを持つ実装を選びます。
ただし、DB側のuuidv7()を含め、IDの順序はトランザクションのコミット順を保証しません。先に採番した行が遅れてコミットされれば、読み終えたカーソルより小さいIDが後から見えることがあります。イベントの再生には専用の連番を持たせ、カーソル型のページネーションでは作成日時とIDを組み合わせて並べるなど、ID単体の順序に頼らない設計にします。
カウンタ方式でも順序が保証されない範囲
カウンタやサブミリ秒で保たれる順序は、1つのプロセス(PostgreSQLなら1つのセッション)の中だけです。KotlinのUuid.generateV7()のドキュメントも、同じホストの別プロセスや別ホストで作った値には単調性の保証が無いと明記しています。複数のアプリサーバーが別々に採番すれば、ミリ秒単位の並びもサーバー間の時計のずれに左右されます。RFC 9562の6.13節は、単一のDBを使う構成ならDBでの生成が最も単調性を得やすいとしています。時計が巻き戻ったときの動きも実装で分かれ、Python 3.14は時計が追いつくまで、呼び出すたびに直前の値へ1ミリ秒を足して進めます。サーバーの時刻同期はchronyなどで保ち、ステップ補正で時計が戻らないように設定しておきます。
ULID・Snowflake IDとUUIDv7の違い
| 項目 | UUIDv7 | ULID | Snowflake ID |
|---|---|---|---|
| 長さ | 128ビット | 128ビット | 64ビット |
| 時刻部分 | 48ビット・Unixミリ秒 | 48ビット・Unixミリ秒 | 41ビット・独自起点のミリ秒 |
| 残り | version・variant+乱数 | 乱数80ビット | データセンター5+ワーカー5+連番12 |
| 文字列表記 | 16進36文字 | Crockford Base32の26文字 | 10進整数 |
| 仕様 | IETFのRFC 9562 | GitHub上のコミュニティ仕様 | Twitterの実装 |
ULIDは時刻48ビットの配置がUUIDv7と同じですが、version・variantのビットを持たないので、16バイトとしてuuid型に入れてもUUIDv7としては扱えません。ULIDの仕様には同じミリ秒で乱数部を1増やす単調生成もありますが、通常の生成では同一ミリ秒内の順序は保証されません。Snowflake IDはBIGINTに収まる代わりに、ワーカー番号を重複なく割り当てる運用が要ります(Twitterの実装の起点は2010年11月4日)。新規に選ぶなら、DB・言語・ORMの対応が揃ったUUIDv7を第一候補にします。IDを64ビットに収めたい場合や、ワーカー番号と連番による採番を管理できる場合は、Snowflake方式が候補に残ります。
主キーにUUIDv7を保存するときの注意点
MySQLとSQL Serverでの保存型と並び順
MySQLではCHAR(36)ではなくBINARY(16)に入れます。UUID_TO_BIN()の第2引数(swap_flag)はv1の時刻フィールドを入れ替えるためのもので、UUIDv7は先頭が既に時刻なので0のまま使います。1にすると並び順が崩れます。
SQL Serverのuniqueidentifierについて、MicrosoftのGUIDとuniqueidentifierの比較の説明は、SQL Server 2025(17.x)向けの版でも「the last six bytes of a value are most significant」としています。UUIDv7の時刻は先頭6バイトにあるので、uniqueidentifierの主キーに入れても挿入位置は時刻順になりません。SQL Serverで追記型の主キーが欲しいなら、NEWSEQUENTIALID()やBIGINTの連番を検討するか、BINARY(16)で持ちます(.NETから入れる場合はGuid.ToByteArray(true)でビッグエンディアンのバイト列にします)。
主キーの範囲だけで期間を絞り込むSQL
先頭がミリ秒なので、ある時刻のミリ秒を12桁の16進にして残りを最小値で埋めた値は、その時刻以降に生成されたUUIDv7の下限になります。created_atに索引が無い表でも、主キー索引だけで直近の行を拾えます。
CREATE FUNCTION uuidv7_floor(ts timestamptz) RETURNS uuid LANGUAGE sql STABLE AS
$$ SELECT (lpad(to_hex(floor(extract(epoch FROM ts) * 1000)::bigint), 12, '0')
|| '70008000000000000000')::uuid $$;
SELECT uuidv7_floor('2026-10-23 00:00:00+00');
-- 01a1660e-ac00-7000-8000-000000000000
SELECT count(*) FROM ev WHERE id >= uuidv7_floor(now() - interval '10 minutes');
PostgreSQL 18.6で、uuidv7(interval)を使って過去1時間に3.6ミリ秒間隔で散らした100万行を作り、直近10分を数えました。主キーの範囲で絞るとIndex Only Scan using ev_pkey、uuid_extract_timestamp(id) >= …で絞ると並列のSeq Scanになり、件数はどちらも163,816件で一致しました。関数を条件に書くと(式索引を作っていない限り)索引が使われないので、時刻の条件は境界値のUUIDに直してから比べます。ここで絞れるのはIDを生成した時刻で、行がコミットされた時刻ではありません。集計や課金のように正確さが要る期間指定は、created_atの列で行います。
UUIDv4の列をUUIDv7へ切り替える手順と確認点
型はどちらも同じUUIDなので、既存の列はスキーマを変えずに、新しく作る行からv7へ切り替えられます。切り替え後は次の2点を確認します。
- 既存のv4を含む集合ではID順を日時順とみなさない。日時順の表示には
created_atなどの日時列と一意な補助キーを使う - 過去データにもv7を振り直す場合は、PostgreSQL 18の
uuidv7(interval)などで作成日時に合わせたIDを作り、参照している表の外部キーは、制約にON UPDATE CASCADEを付けるか、DEFERRABLEにしてSET CONSTRAINTS ALL DEFERREDを実行したうえで同じトランザクションで更新する(既定のNO ACTIONのままでは、親と子のどちらを先に更新しても制約違反で止まる)
既存システムの主キーの見直しや、DBの移行に合わせたID設計の変更は、当社のデータベース設計・移行支援でも扱っています。
よくある質問
UUIDv7の文字数とバイト数は?
文字列はハイフン込みで36文字、ハイフンを除くと16進32桁、バイナリでは16バイト(128ビット)で、UUIDv4と同じです。
UUIDv7から生成日時を取り出せますか?
取り出せます。Python 3.14ならUUID.time、PostgreSQL 18ならuuid_extract_timestamp()で読めます。入っているのは生成したマシンの時計の値です。
Java 25でUUIDv7を生成するには?
Java 25のjava.util.UUIDにはv7を作るメソッドが無いため、uuid-creator(6.1.1)のUuidCreator.getTimeOrderedEpoch()などのライブラリを使います。標準APIのUUID.ofEpochMillis()はJava 26からです。
MySQLでUUIDv7は使えますか?
保存はできますが、MySQLには2026年10月公開の26.10.0時点でもUUIDv7を生成する関数がなく、UUID()が返すのはバージョン1です。アプリ側で生成した値をUUID_TO_BIN(値, 0)でBINARY(16)列に入れます。MariaDBなら11.7以降のUUID_v7()が使えます。
UUIDv7は衝突しませんか?
乱数部分が最大74ビットあり、すべて乱数で埋める実装なら、同じミリ秒の中で衝突確率が50%に達するのは約1,600億個を生成したときです。ただし乱数の生成元が弱いと前提が崩れるため、各言語の標準APIや暗号論的乱数を使うライブラリで生成します。