ULID と UUIDv7 の違い

2026-05-09 03:00 (36 days ago)
Hex Badge
この記事をテーマにした曲を再生

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
  • 動機: UUIDv4 の「文字列が長い・ソート不可」という不満への対処。
  • 仕様: 128bit を Crockford Base32 で 26 文字にエンコード。
  • 標準化: IETF 標準ではなく、GitHub 上の単独仕様。後から「ULID Spec v1」として整理されているが、RFC ではない。
  • 各種言語で40以上の実装がある (Go の oklog/ulid が事実上のリファレンス実装)。

UUIDv6, v7, v8 (2024年5月)

  • RFC 9562 として 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。先頭文字は 07 のみ)
  • 大文字のみ。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 が追加された。

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 パッケージを使う。

pip install uuid6
from uuid6 import uuid7

new_id = uuid7()

ULID

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

pip install python-ulid
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

npm install uuid          # v11+ で TypeScript ネイティブ対応・v7 サポート
import { v7 as uuidv7 } from 'uuid';

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

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

npm install uuidv7
import { uuidv7 } from 'uuidv7';

const id = uuidv7();

ULID

ulid パッケージ。

npm install ulid
import { ulid, monotonicFactory } from 'ulid';

ulid();  // '01ARZ3NDEKTSV4RRFFQ69G5FAV'

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

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

Rust

UUIDv7

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

# Cargo.toml
[dependencies]
uuid = { version = "1", features = ["v7"] }
use uuid::Uuid;

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

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

ULID

ulid クレート。

[dependencies]
ulid = "1"
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 パッケージを使っている。

# pyproject.toml
dependencies = [
    "uuid6>=2025.0.1",
]
# 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 以降のプロジェクトなら、依存ライブラリすら不要。

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 を生成 できる。

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

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. 参考リンク

評価をお願いします (会員登録・ログイン不要)
まだ評価がありません
著者は、アプリケーション開発会社 Cyberneura を運営しています。
開発相談をお待ちしています。

アーカイブ