UUIDはほとんどのアプリケーションのデフォルトの一意識別子です。しかしUUID v4は——ランダム設計上——順序付けされていません。データベースに挿入された行は挿入順に保存されません。インデックスが断片化します。クエリが遅くなります。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 v4 | ULID |
|---|---|---|
| 長さ | 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 を挙げています。
| 特徴 | ULID | UUID v7 |
|---|---|---|
| フォーマット | 26文字Base32 | 36文字16進数(ハイフンあり) |
| RFC標準 | いいえ | はい(RFC 9562) |
| DBネイティブ型 | TEXTまたはBINARY | UUID型(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
関連ツール
- UUIDジェネレーター → — UUID v4を生成する
- タイムスタンプコンバーター → — Unix タイムスタンプを変換する