UUIDはほとんどのアプリケーションのデフォルトの一意識別子です。しかしUUID v4は——ランダム設計上——順序付けされていません。データベースに挿入された行は挿入順に保存されません。インデックスが断片化します。クエリが遅くなります。ULIDはまさにこの問題を解決するために設計されました。

ULIDを生成する →

ULIDとは何か

ULIDはUniversally Unique Lexicographically Sortable Identifier(汎用一意辞書順ソート可能識別子)の略です。2016年にAlizain Feerastaによって作成され、ULID仕様で定義されています。

ULIDはこのようなものです:

01HQ7V5J3Z2QK8MNRPXTYW9E4B

26文字、大文字、Crockfordのbase32アルファベットを使用(視覚的な曖昧さを避けるためI、L、O、Uを除外)。

構造

 01HQ7V5J3Z  2QK8MNRPXTYW9E4B
|-----------|-----------------|
  タイムスタンプ    ランダム性
  48ビット         80ビット
  • タイムスタンプ(48ビット):Unixエポックからのミリ秒。仕様では西暦10889年まで使えるとしています(2⁴⁸ − 1 ミリ秒は 10889-08-02 UTC)。
  • ランダム性(80ビット):仕様は可能な限り暗号論的に安全な乱数源を使うよう求めています。1ミリ秒あたり約1.2×10²⁴の可能な値。

ULID vs UUID:主な違い

特徴UUID v4ULID
長さ36文字(ハイフンあり)26文字
ソート可能いいえはい(辞書順)
タイムスタンプ埋め込みいいえはい
衝突確率極めて低い極めて低い
URLセーフはい(16進数字とハイフンは RFC 3986 の非予約文字)はい
大文字小文字区別しない区別しない(仕様)
仕様RFC 9562(RFC 4122 を廃止)ULID仕様

重要な違い:ULIDは作成時刻でソートされます。ULIDキーのテーブルに1000行挿入すると、プライマリキーインデックスがシーケンシャルに保たれます。これが重要な場面:

  • Bツリーインデックス:シーケンシャルな挿入は同じページにヒットし、断片化を減らす。
  • ページネーション:別のcreated_atカラムなしに効率的なカーソルとしてWHERE id > :cursorが使える。
  • ログファイル:ファイル名でソート = 時刻でソート。
  • メッセージキュー:別のシーケンス番号なしに自然な順序付け。

ULID vs UUID v7

UUID v7(RFC 9562、2024年5月発行)も最初の48ビットにミリ秒タイムスタンプを埋め込んでいます——ソート可能にするために。RFC 9562 の 2.1 節は、新しいバージョンの検討で分析した時刻順 ID 方式の一つとして ULID を挙げています。

特徴ULIDUUID v7
フォーマット26文字Base3236文字16進数(ハイフンあり)
RFC標準いいえはい(RFC 9562)
DBネイティブ型TEXTまたはBINARYUUID型(PostgreSQL)

データベースにネイティブのUUIDサポートがある場合(PostgreSQLのuuid型)、UUID v7の方が便利かもしれません。文字列ベースのシステムで作業する場合や短いIDが必要な場合、ULIDは依然として良い選択です。

同一ミリ秒内のモノトニシティ

同一ミリ秒内に複数のULIDが生成された場合、タイムスタンプ部分は同じになります。ULID仕様はモノトニシティ拡張を定義しています:同一ミリ秒内の後続のULIDごとにランダムコンポーネントを1ずつインクリメントします。これで同じ生成器から出たIDは順序が保たれます。1ミリ秒内にランダム部分が桁あふれすると生成は失敗します。別プロセスが同じミリ秒に生成したIDの順序は、ランダム部分で決まるだけです。

ZeroTool のジェネレーターは ulid パッケージの monotonicFactory() と同じくこの拡張を使います。同じミリ秒内では直前のULIDのランダム部分に 1 を足すため、バッチは辞書順に並び、ランダム部分が桁あふれしそうになるとエラーで止まります。

コードでのULID生成

JavaScript / TypeScript

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

const id = ulid();
// "01HQ7V5J3Z2QK8MNRPXTYW9E4B"

// カスタムタイムスタンプを使用
const id2 = ulid(Date.now());

Go

go get github.com/oklog/ulid/v2
import (
    "crypto/rand"
    "fmt"

    "github.com/oklog/ulid/v2"
)

id := ulid.MustNew(ulid.Now(), rand.Reader)
fmt.Println(id) // "01HQ7V5J3Z2QK8MNRPXTYW9E4B"

ulid.Make()(v2.1 以降)はより短く書け、同一ミリ秒内で単調増加しますが、既定のエントロピーは時刻をシードにした math/rand です。推測されにくいIDが必要なら crypto/rand を渡してください。

Python

pip install python-ulid
from ulid import ULID

id = ULID()
print(str(id))  # "01HQ7V5J3Z2QK8MNRPXTYW9E4B"
print(id.timestamp)  # Unix秒(float)
print(id.datetime)   # UTCのdatetimeオブジェクト

データベースへのULIDの保存

PostgreSQL

-- テキストとして(26バイト)
CREATE TABLE events (
  id TEXT PRIMARY KEY,
  ...
);

PostgreSQLはULIDをネイティブに理解しませんが、TEXTまたはBYTEAとして保存できます。多くのチームはわかりやすさのためにTEXTを使用します。26バイトのコストはクエリのシンプルさと比べると無視できます。

MySQL / SQLite

同じアプローチ——CHAR(26)またはVARCHAR(26)として保存します。固定幅文字列なのでCHAR(26)の方がわずかに効率的です。

ULIDのデコード

タイムスタンプは最初の10文字に埋め込まれています。抽出できます:

import { decodeTime } from 'ulid';

const ts = decodeTime('01HQ7V5J3Z2QK8MNRPXTYW9E4B');
console.log(new Date(ts).toISOString()); // 2024-02-22T07:23:36.959Z

これはデバッグに有用です:created_atを照会せずにIDだけからレコードがいつ作成されたかを正確に確認できます。

ULIDをいつ使うか

  • 高挿入レートのテーブルでインデックス断片化が懸念される場合
  • イベントログ、監査証跡、または追記が多いワークロード
  • 別のタイムスタンプカラムなしに時刻順のIDが必要なシステム
  • UUIDより短いURL安全で人間が読めるIDが必要なAPI

ULIDを今すぐ生成する →

関連ツール