---
slug: "ulid-vs-uuidv7-django-primary-key"
title: "ULID と UUIDv7 の違い"
description: "128bit の時系列ソート可能 ID として ULID と UUIDv7 を比較する。RFC 9562 のビット配置・標準化レベル・Python/TypeScript/Rust の最新ライブラリ事情、そして Django 5.2 + Python 3.14 + PostgreSQL 18 で UUIDv7 を主キーに採用する具体的な書き方までまとめる。"
url: "https://www.ytyng.com/blog/ulid-vs-uuidv7-django-primary-key"
publish_date: "2026-05-09T03:00:26.279Z"
created: "2026-05-09T03:00:26.287Z"
updated: "2026-05-09T03:15:12.613Z"
categories: []
keywords: ""
featured_image_url: "https://media.ytyng.com/resize/20260509/a9fbe34b32bd4dac8a8f14f11d4829e9.png.webp?width=768"
has_video: false
has_music: false
video_urls: []
music_urls: []
lang: "ja"
---

# ULID と UUIDv7 の違い

128bit の時系列ソート可能 ID として ULID と UUIDv7 の二つがある。どちらも「単調増加するランダム ID」という同じ問題を解いているが、設計思想・標準化のレベル・エコシステムが違う。本記事では両者の違いを整理し、Python / TypeScript / Rust それぞれの最新ライブラリ事情、そして Django で主キーに採用するときの具体的な書き方までまとめる。

## TL;DR

- **128bit、先頭48bit に Unix time (ms)** という根幹は両者共通。文字列が時系列順にソート可能。
- **UUIDv7 は IETF 標準 (RFC 9562, 2024年5月公開)**。ULID は GitHub 上の単独仕様 (2016年〜)。標準化のレベルで明確に差がついた。
- **Python 3.14 (2025年10月リリース) で `uuid.uuid7()` が標準ライブラリ入り**。PostgreSQL 18 (2025年9月25日リリース) で `uuidv7()` 関数がネイティブ提供。Django 5.2 + Python 3.14 + Postgres 18 という組み合わせで、外部ライブラリなしに UUIDv7 主キーが書けるようになった。
- **私の結論: ユーザーに紐づく永続データの主キーは UUIDv7**。文字列を見たときに「ID とわかる」「v7 とわかる」可読性が ULID の26文字 Base32 文字列に勝る。ULID は内部識別子・URL に出すスラッグ用途のほうが向いている。

## 1. UUIDv4 と ULID / UUIDv7 の大きな違いは「ソート可能性」

まず、ULID と UUIDv7 の違いの前に、 UUIDv4 と ULID / UUIDv7 の違いについて。

UUIDv4 は 128bit のうち version/variant の 6bit を除いた 122bit を完全ランダムで埋める。

- 分散環境で調整なしに一意な ID を生成できる → これは v7/ULID も同じ。
- 時系列情報を一切含まない → **生成順とソート順が無関係**。
- セキュアなトークン用途には別物 (`secrets.token_urlsafe()` など) を使うべきで、UUIDv4 は本来「衝突しない ID 」のためのもの。

「**分散一意性は欲しいが、生成順でソートも効かせたい**」というニーズに応えるのが ULID と UUIDv7 の共通動機。逆に「ソート不要、ランダム性こそ欲しい」(例: 推測されたくない公開トークンの代用、ランダム配置のシャーディングキー) なら UUIDv4 のほうが適切な場面もある。

両者とも:

| 共通仕様 | 内容 |
|---|---|
| 全体長 | 128bit (16 bytes) |
| 先頭フィールド | 48bit Unix timestamp (ミリ秒精度) |
| 残り 80bit | ランダム + (実装による) 単調性保証 |
| バイナリ表現 | 同じ 16 バイト、相互変換可能 |

つまり **ストレージ的には完全に等価** で、違いはエンコード形式・標準化・周辺エコシステムにある。

## 2. 歴史的背景

### ULID (2016年)

- 作者: Alizain Feerasta。
- リポジトリ: [`ulid/spec`](https://github.com/ulid/spec)
- 動機: UUIDv4 の「文字列が長い・ソート不可」という不満への対処。
- 仕様: 128bit を Crockford Base32 で 26 文字にエンコード。
- 標準化: **IETF 標準ではなく、GitHub 上の単独仕様**。後から「ULID Spec v1」として整理されているが、RFC ではない。
- 各種言語で40以上の実装がある (Go の oklog/ulid が事実上のリファレンス実装)。

### UUIDv6, v7, v8 (2024年5月)

- [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562.html) として IETF が標準化。RFC 4122 の改訂版。
- v6: 既存の v1 (MAC + 時刻) を時系列ソート可能に並べ替えたもの。後方互換のための救済策。
- **v7: ULID 的なアプローチを UUID フォーマットに取り込んだもの**。これが事実上のメインプレイヤー。
- v8: 完全カスタム実装用の枠 (アプリ固有のビット配置を許容)。

UUIDv7 は ULID の問題提起を IETF が標準として吸収した形になっている。歴史的には ULID が先行し、その実績を踏まえて UUIDv7 が後発で標準化された。

## 3. ビット配置の差分

### ULID

```
 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      32_bit_uint_time_high                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     16_bit_uint_time_low      |       16_bit_uint_random      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       32_bit_uint_random                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       32_bit_uint_random                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```

- 先頭 48bit: Unix time (ms)
- 残り 80bit: 全部ランダム
- 単調性: 同じ ms 内で複数生成されたら **ランダム部の最下位ビットを +1** して保証する (monotonic mode)。これは仕様の MUST ではなく実装の責務。

### UUIDv7 (RFC 9562)

```
 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                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          unix_ts_ms           |  ver  |       rand_a          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var|                        rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            rand_b                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```

- 先頭 48bit: Unix time (ms)
- 4bit: version (固定で `0111` = 7)
- 12bit: `rand_a` (RFC 推奨では sub-ms カウンタやランダムに使用)
- 2bit: variant (`10`)
- 62bit: `rand_b`

### 重要な差

UUIDv7 は version/variant ビットを必ず固定で持つ。ランダムに使えるのは合計 74bit。ULID は完全に 80bit 全部ランダムに使える。衝突確率は ULID のほうがわずかに低いが、74bit でも十分に実用的なので実害はない。

### サブミリ秒の単調性 (RFC 9562 の Method 1, Method 2, Method 3)

RFC 9562 はサブミリ秒の単調性保証について複数の方式を許容している:

- **Method 1 (Fixed-Length Dedicated Counter Bits)**: `rand_a` の一部 (または `rand_b` の上位ビット) を固定長のカウンタとして専有する。同じ ms 内ではカウンタを +1 し、新しい ms に入ったらリセット。
- **Method 2 (Monotonic Random)**: 同じ ms 内では前回生成値のランダム部を「ランダムにシードされたカウンタ」とみなして +1 する。固定カウンタ領域を作らず、ランダム領域全体を単調増加に使う。
- **Method 3 (Replace Leftmost Random Bits with Increased Clock Precision)**: `rand_a` の 12bit にミリ秒をさらに分割した sub-ms 精度の小数 (1/4096ms 単位) を入れる。**PostgreSQL 18 はこの Method 3 採用**。

→ 実装ライブラリによってどの Method を使うかが違うので、**「同じセッション内で生成順が UUIDv7 文字列のソート順と一致するか」は実装依存**。

## 4. 文字列表現の差

### ULID

```
01ARZ3NDEKTSV4RRFFQ69G5FAV
```

- Crockford Base32 (5bit / 文字 × 26 文字 = 130bit、先頭文字の上位 2bit が常に 0 になる制約で実質 128bit。先頭文字は `0`–`7` のみ)
- 大文字のみ。`I` `L` `O` `U` を除外 (誤読・卑語回避)
- ハイフンなし、26文字
- ぱっと見は **「ランダムなトークン」っぽい**

### UUIDv7

```
0190b3a1-7c4b-7abc-89de-f0123456789a
              ^
              先頭が 7 → version 7
```

- 16進、ハイフン区切り、36 文字 (`8-4-4-4-12`)
- 3 グループ目の先頭 (ハイフン込み 15 文字目 / hex 13 文字目) が `7` で固定 → **目視で「これ UUIDv7 だ」とわかる**
- 4 グループ目の先頭 (ハイフン込み 20 文字目 / hex 17 文字目) が `8`/`9`/`a`/`b` のいずれか → variant `10`

### 私の主観的な使い勝手評価

UUIDv7 のほうが「人間が見たときに ID だとわかる」。`0190b3a1-7c4b-7abc-...` を見ればハイフンの区切り方で誰しも UUID と認識できるし、3 グループ目先頭の `7` で v7 と即座にわかる。これが API レスポンスや管理画面の URL に出ていても違和感がない。

ULID の `01ARZ3NDEKTSV4RRFFQ69G5FAV` は、JWT やランダムトークンと **目視で区別がつかない**。サーバーレスポンスにこれが入っていると「これ機密トークン入っちゃってない?」と一瞬警戒する。慣れの問題ではあるが、コードレビューで他人のコードを読むときの認知負荷が違う。

→ **ユーザーに紐づくデータベースの主キーには UUIDv7 を採用する**、というのが私の方針。

## 5. 各言語での使い方

### Python

#### UUIDv7

##### Python 3.14+ (2025年10月リリース)

標準ライブラリ `uuid` モジュールに `uuid6`, `uuid7`, `uuid8` が追加された。

```python
import uuid

new_id = uuid.uuid7()
print(new_id)
# UUID('0190b3a1-7c4b-7abc-89de-f0123456789a')

# 上位 48bit を Unix 時刻 (ms) として取り出す
ts_ms = (new_id.int >> 80) & 0xFFFFFFFFFFFF
```

##### Python 3.13 以前

標準ライブラリにないので `uuid6` パッケージを使う。

```bash
pip install uuid6
```

```python
from uuid6 import uuid7

new_id = uuid7()
```

#### ULID

標準ライブラリにはない。`python-ulid` が事実上のスタンダード。

```bash
pip install python-ulid
```

```python
from ulid import ULID

new_id = ULID()
print(str(new_id))           # '01ARZ3NDEKTSV4RRFFQ69G5FAV'
print(new_id.timestamp)      # datetime
print(new_id.to_uuid())      # 同じ 128bit を UUID として表現
```

`python-ulid` は ULID と UUID の相互変換 API を持っている。バイト列としては等価なので変換コスト 0。

### TypeScript / JavaScript

#### UUIDv7

```bash
npm install uuid          # v11+ で TypeScript ネイティブ対応・v7 サポート
```

```ts
import { v7 as uuidv7 } from 'uuid';

const id = uuidv7();
// '0190b3a1-7c4b-7abc-89de-f0123456789a'
```

`uuid` パッケージ v11 以降は完全 TypeScript 化されており、`v7` がエクスポートされる。
高機能版が必要なら `uuidv7` パッケージ (LiosK 製) もある。これは Method 2 (Monotonic Random) の参照実装で、生成順の単調性が厳密。

```bash
npm install uuidv7
```

```ts
import { uuidv7 } from 'uuidv7';

const id = uuidv7();
```

#### ULID

`ulid` パッケージ。

```bash
npm install ulid
```

```ts
import { ulid, monotonicFactory } from 'ulid';

ulid();  // '01ARZ3NDEKTSV4RRFFQ69G5FAV'

// 単調性を保証したい場合
const monotonic = monotonicFactory();
monotonic();
monotonic();  // 必ず前回より大きい
```

ブラウザ / Node.js / Deno / Bun 全部で動く。

### Rust

#### UUIDv7

`uuid` クレートが本家対応済み。`v7` feature を有効化する。

```toml
# Cargo.toml
[dependencies]
uuid = { version = "1", features = ["v7"] }
```

```rust
use uuid::Uuid;

let id = Uuid::now_v7();
println!("{}", id);
// 0190b3a1-7c4b-7abc-89de-f0123456789a
```

`Uuid::new_v7(timestamp)` で任意のタイムスタンプを指定することもできる。

#### ULID

`ulid` クレート。

```toml
[dependencies]
ulid = "1"
```

```rust
use ulid::Ulid;

let id = Ulid::new();
println!("{}", id);
// 01ARZ3NDEKTSV4RRFFQ69G5FAV

// UUID 互換変換
let as_uuid: uuid::Uuid = id.into();
```

`ulid` クレートは内部表現が `u128` なのでコピー・比較が高速。`Into<Uuid>` も実装されている。

## 6. Django でプライマリキーに採用する方法

### 6.1 既存パターン (uuid6 ライブラリ + UUIDField)

私が運用している複数の Django プロジェクトでは、Python 3.14 リリース前に立ち上がった経緯から、PyPI の `uuid6` パッケージを使っている。

```toml
# pyproject.toml
dependencies = [
    "uuid6>=2025.0.1",
]
```

```python
# models.py
from django.db import models
from uuid6 import uuid7


class User(AbstractUser):
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    email = models.EmailField(unique=True)


class LoginCode(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
    email = models.EmailField()
    code = models.CharField(max_length=8)
```

#### ポイント

- `default=uuid7` のように **関数自体を渡す** (関数呼び出し `uuid7()` ではない)。Django が INSERT 時に毎回呼んで新しい UUID を生成する。
- `editable=False` で Django Admin の編集フォームから外す。
- `models.UUIDField` は Postgres では `uuid` 型、SQLite では `char(32)` にマッピングされる。

### 6.2 新しいパターン (Python 3.14 標準ライブラリ)

Python 3.14 以降のプロジェクトなら、依存ライブラリすら不要。

```python
import uuid
from django.db import models


class Item(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid7, editable=False)
```

`uuid6` パッケージへの依存を削れる。新規プロジェクトはこれが推奨。

### 6.3 PostgreSQL 18 + Django 5.2 ネイティブパターン (最も新しい)

PostgreSQL 18 (2025-09-25 リリース) は `uuidv7()` 関数を組み込みで持つ。Django 5.2 の `db_default` を使うと、**アプリケーションではなく DB 側で UUID を生成** できる。

```python
from django.db import models


class UUIDv7(models.Func):
    function = "uuidv7"
    output_field = models.UUIDField()


class UUIDExtractTimestamp(models.Func):
    function = "uuid_extract_timestamp"
    output_field = models.DateTimeField()


class Record(models.Model):
    uuid = models.UUIDField(db_default=UUIDv7(), primary_key=True)
    creation_time = models.GeneratedField(
        expression=UUIDExtractTimestamp("uuid"),
        output_field=models.DateTimeField(),
        db_persist=True,
    )
```

#### 生成される DDL

```sql
CREATE TABLE "items_record" (
    "uuid" uuid DEFAULT (uuidv7()) NOT NULL PRIMARY KEY,
    "creation_time" timestamp GENERATED ALWAYS AS
        (uuid_extract_timestamp("uuid")) STORED
);
```

#### メリット

- **複数アプリサーバーが同時に書いても DB が一元的に発番** する → クロックドリフトの影響を受けない。
- `creation_time` は UUID から自動抽出するので、別途タイムスタンプカラムを INSERT に含める必要がない。
- PostgreSQL 18 の Method 3 (sub-ms fraction) で同一セッション内の単調性が強く保証される。

### 6.4 どれを選ぶか

| 状況 | 推奨パターン |
|---|---|
| 既存プロジェクト (Python ≤ 3.13) | `uuid6` パッケージ + `default=uuid7` |
| 新規プロジェクト (Python 3.14+) | 標準ライブラリ `uuid.uuid7` + `default` |
| Postgres 18 + Django 5.2 確定 | `db_default=UUIDv7()` でネイティブ生成 |
| SQLite/MySQL 主体 | アプリ側生成 (`default=uuid.uuid7`) |

私の現状は 6.1 のパターンが主力。新規プロジェクトからは 6.3 を採用していく予定。

## 7. ULID と UUIDv7 の弱点比較

### ULID の弱点

1. **IETF 標準ではない**。実装ごとに「ms 越え時の clock 戻り処理」「monotonic mode の挙動」が微妙に違う。Python の `python-ulid` と JavaScript の `ulid` で挙動が一致するとは限らない。
2. **PostgreSQL 18 のようなネイティブ DB サポートがない**。`UUID` 型カラムに変換して入れるか、`BINARY(16)` / `BYTEA` で持つかの選択。後者は `WHERE id = '01ARZ...'` と書けず、`bytea_to_ulid()` 的な変換関数を作る羽目になる。
3. **文字列が一見ただのトークンに見える**。コードレビューで「これ機密じゃない?」と毎回確認される。

### UUIDv7 の弱点

1. **Hex 文字列にすると長い (36文字)**。URL に直接出すと不格好。slug用途なら ULID か Base62 短縮の方が良い。
2. **UUIDv4 と目視で区別しづらい**。version ビットを見れば確実だが、慣れていないと「ハイフン込み 36 文字」=「v4」と誤解されがち。
3. **RFC 9562 の Method 選択がライブラリ依存**。アプリ A が Method 1、アプリ B が Method 2 のときに、同一 ms 内のソート順が一致しない可能性がある。**「UUIDv7 だから単調」ではなく「実装が単調保証している UUIDv7 だから単調」**。
4. **74bit ランダム** (ULID は 80bit) なので衝突確率は理論上わずかに高い。実用上問題にはならないが。

## 8. 参考リンク

- [RFC 9562: Universally Unique IDentifiers (UUIDs)](https://www.rfc-editor.org/rfc/rfc9562.html)
- [ULID Specification (ulid/spec)](https://github.com/ulid/spec)
- [Python 3.14 uuid module documentation](https://docs.python.org/3/library/uuid.html)
- [PostgreSQL 18 Release Notes](https://www.postgresql.org/docs/current/release-18.html)
- [How to use UUIDv7 in Python, Django and PostgreSQL — Paolo Melchiorre](https://www.paulox.net/2025/11/14/how-to-use-uuidv7-in-python-django-and-postgresql/)
- [UUIDv7 Comes to PostgreSQL 18 — Nile](https://www.thenile.dev/blog/uuidv7)
- [npm: uuid](https://www.npmjs.com/package/uuid) / [npm: uuidv7](https://www.npmjs.com/package/uuidv7) / [npm: ulid](https://www.npmjs.com/package/ulid)
- [crates.io: uuid](https://crates.io/crates/uuid) / [crates.io: ulid](https://crates.io/crates/ulid)
- [PyPI: uuid6](https://pypi.org/project/uuid6/) / [PyPI: python-ulid](https://pypi.org/project/python-ulid/)
