---
title: "UUIDv7とは？UUIDv4との違い・言語別の生成方法と主キー設計の注意点"
url: "https://www.issoh.co.jp/tech/details/11650/"
published: 2026-04-03
updated: 2026-10-23
categories: ["データベース"]
publisher: "株式会社一創"
---

# UUIDv7とは？UUIDv4との違い・言語別の生成方法と主キー設計の注意点

UUIDv7は、2024年5月公開の[RFC 9562](https://www.rfc-editor.org/rfc/rfc9562.html)で定められた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節](https://www.rfc-editor.org/rfc/rfc9562.html#section-5.7)で「Implementations SHOULD utilize UUIDv7 instead of UUIDv1 and UUIDv6 if possible.」と、時刻ベースならv7を使うよう求めています。バージョンごとの使い分けは[UUIDのv1〜v8を比較した記事](/tech/details/17545/)に譲り、ここでは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節](https://www.rfc-editor.org/rfc/rfc9562.html#section-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節](https://www.rfc-editor.org/rfc/rfc9562.html#section-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モジュール](https://docs.python.org/3/library/uuid.html)に`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](https://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/util/UUID.html)に`ofEpochMillis(long)`が追加されました。2026年9月公開のJava 27でもv7を作るメソッドはこれだけです。現行LTSのJava 25の[java.util.UUID](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/UUID.html)には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](https://nodejs.org/api/crypto.html)の`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()](https://bun.com/reference/bun/randomUUIDv7)を追加しており、同じ時刻で複数回呼ぶとカウンタを足して並ぶようにすると説明しています。

### Go・Rust・.NET・Kotlin・PHPの生成関数

Goは1.27で標準ライブラリに[uuidパッケージ](https://pkg.go.dev/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()](https://learn.microsoft.com/en-us/dotnet/api/system.guid.createversion7)が入りました。Kotlinは2.3.0で`Uuid.generateV7()`を追加し、[2.4.0](https://kotlinlang.org/docs/whatsnew24.html)で`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](https://www.postgresql.org/docs/18/functions-uuid.html)は組み込みの`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](https://dev.mysql.com/doc/refman/9.7/en/miscellaneous-functions.html)です。アプリ側で生成して`BINARY(16)`に入れます。MariaDBは11.7で[UUID\_v7()](https://mariadb.com/docs/server/reference/sql-functions/secondary-functions/miscellaneous-functions/uuid%5Fv7)を追加しています（MySQLとの差分は[MariaDBとMySQLの違いをまとめた記事](/tech/details/18657/)を参照）。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](https://docs.djangoproject.com/en/6.1/ref/models/database-functions/)はデータベース関数`UUID7`を追加しました。Rails 8.1は`id: :uuid`の`create_table`に既定値を渡さないと`gen_random_uuid()`（v4）を使い、Drizzleの`defaultRandom()`も`gen_random_uuid()`を既定値にするので、v7にするには既定値を明示します。HibernateのエンティティはJPAの`@Id`と組み合わせて書きます（書き方は[JPAのエンティティクラスの記事](/tech/details/3948/)を参照）。

```
# 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をつなぐ記事](/tech/details/17072/)を参照）。

## 同一ミリ秒内の生成順と実装別の単調性

UUIDv7が時刻順に並ぶのは、ミリ秒が異なる値どうしの話です。同じミリ秒に複数生成したときの並びについて、[RFC 9562の6.2節](https://www.rfc-editor.org/rfc/rfc9562.html#section-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をカーソルにした[ページネーション](/tech/details/16297/)や、イベントを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](/tech/details/17855/)などで保ち、ステップ補正で時計が戻らないように設定しておきます。

## 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の仕様](https://github.com/ulid/spec)には同じミリ秒で乱数部を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の比較の説明](https://learn.microsoft.com/en-us/sql/connect/ado-net/sql/compare-guid-uniqueidentifier-values?view=sql-server-ver17)は、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を作り、参照している表の[外部キー](/tech/details/18651/)は、制約に`ON UPDATE CASCADE`を付けるか、`DEFERRABLE`にして`SET CONSTRAINTS ALL DEFERRED`を実行したうえで同じトランザクションで更新する（既定のNO ACTIONのままでは、親と子のどちらを先に更新しても制約違反で止まる）

既存システムの主キーの見直しや、DBの移行に合わせたID設計の変更は、当社の[データベース設計・移行支援](/service/system/database/)でも扱っています。

## よくある質問

### 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や暗号論的乱数を使うライブラリで生成します。

## 関連記事

- [UUIDとは？RFC 9562の構造とv1〜v8の使い分け・実装判断を解説](/tech/details/17545/)
- [Object IDとは？MongoDBやAPIでの意味の違いとID設計の選び方](/column/details/2809/)
- [カーディナリティとは？意味・高低の見分け方とインデックス設計を実装目線で解説](/tech/details/14785/)
- [ページネーション設計とは？オフセットとカーソルの選び分けと実装を解説](/tech/details/16297/)
- [chronyとは：chrony.confの設定とchronycで同期を確かめる手順【4.9系】](/tech/details/17855/)

---

出典: [UUIDv7とは？UUIDv4との違い・言語別の生成方法と主キー設計の注意点](<https://www.issoh.co.jp/tech/details/11650/>)（株式会社一創）
