
2025/12/02 5:45
ULID: Universally Unique Lexicographically Sortable Identifier
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
ULID(Universally Unique Lexicographically Sortable Identifier)は、URLセーフで大文字小文字を区別せず、PostgreSQL の UUID カラムに直接使用できるため、UUID に対する優れた代替案として提示されています。
ULID は 48 ビットのタイムスタンプとその後に 80 ビットの暗号学的ランダム性をエンコードし、合計で 128 ビット(≈ 26 文字の Base32)となります。時間プレフィックスにより、新しい ID が常に古いものより辞書式順序で大きくなるため、ランダム UUID v4 によるインデックス断片化が排除されます。
パッケージを用いた実際の Go の例では、ULID がoklog/ulidとdatabase/sql/driver.Valuerを実装しているため、UUID プライマリキーを持つテーブルに直接挿入できることが示されています。同じコードで UUID v4 も比較用に挿入し、変換なしで互換性があることを確認しています。encoding.TextMarshalerULID は 1.21 × 10²⁴ 個のユニーク ID を毎ミリ秒生成でき、ほぼすべてのアプリケーションに十分です。その短さは URL をクリーンに保ちます(例:
)。/users/01KANDQMV608PBSMF7TM9T1WR4ULID注意点として、高書き込みシステムでは多くの ID が同じミリ秒タイムスタンププレフィックスを共有するため、潜在的に「ホットスポット」遅延が発生する可能性があります。CUID や NanoID などの代替案もありますが、ULID のソート性・一意性・UUID 互換性の組み合わせが採用を促進しています。
新興の UUID v7 標準は、古い UUID が抱えるパフォーマンス問題に対処するために、同様の時間順序構造を採用しています。
本文
UUID形式はユニーク識別子として非常に広く使われている標準です。
しかし、その普及度と裏腹に、いくつかの固有制限から多くの一般的なユースケースでは最適とは言えません:
- 文字数が多く、人間が読み取りやすいわけでもありません。
- UUID v1/v2 は環境によっては実用性が低く、MACアドレスを取得できる安定したネットワークカードが必要です。
- UUID v3/v5 では一意のシード値が必須になります。
- UUID v4 は真にランダムであるため、B‑Tree 等のデータ構造上でインデックスの断片化を招き、書込み性能を低下させることがあります。
私が携わったプロジェクトでは ULID(Universally Unique Lexicographically Sortable Identifier)を採用し、大変満足しています。Go で Postgres を使うケースに焦点を当てつつ、同じ原理は他言語・他データベースでも応用可能です。
仕様全体はここで確認できます:
https://github.com/ulid/spec
ULID が従来の UUID バージョンの欠点を解消する理由
ULID は次の四つの特性に重点を置いています:
- 辞書順ソート可能 – ID をそのまま並べ替えられるため、データベースインデックスで最大のメリットとなります。
- 大文字小文字非区別 – 特殊文字を含まず URL でも安全に使用できます。
- UUID と互換性 – Postgres の
カラム型とそのまま使えるため、スキーマ変更は不要です。UUID
構造
ULID は UUID 同様 128 ビットですが、機能別に構成されています:
| 48ビット(タイムスタンプ) | 80ビット(暗号学的に安全な乱数) | |----------| |----------------|
例:
01AN4Z07BY79KA1307SR9X4MV3
デモ:Go + Postgres + pgx
ドライバ
pgx以下のコードは、稼働中の PostgreSQL に接続し、主キーを
UUID 型にしたテーブルを作成します。その後、標準 UUID v4 と ULID の両方でレコードを挿入します。
package main import ( "context" "fmt" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/oklog/ulid/v2" ) func main() { ctx := context.Background() conn, err := pgx.Connect(ctx, "postgres://...") if err != nil { panic(err) } defer conn.Close(ctx) _, err = conn.Exec(ctx, ` CREATE TABLE IF NOT EXISTS ulid_test ( id UUID PRIMARY KEY, kind TEXT NOT NULL, value TEXT NOT NULL );`) if err != nil { panic(err) } for i := 1; i <= 5; i++ { insertUUID(ctx, conn, fmt.Sprint(i)) } for i := 1; i <= 5; i++ { insertULID(ctx, conn, fmt.Sprint(i)) } } func insertUUID(ctx context.Context, conn *pgx.Conn, value string) { id := uuid.New() conn.Exec(ctx, "INSERT INTO ulid_test (id, value, kind) VALUES ($1, $2, 'uuid')", id, value) fmt.Printf("Inserted UUID: %s\n", id.String()) } func insertULID(ctx context.Context, conn *pgx.Conn, value string) { id := ulid.Make() // ULID は文字列化せずそのまま使えます。 conn.Exec(ctx, "INSERT INTO ulid_test (id, value, kind) VALUES ($1, $2, 'ulid')", id, value) fmt.Printf("Inserted ULID: %s\n", id.String()) }
oklog/ulid パッケージは database/sql/driver.Valuer と encoding.TextMarshaler を実装しているため、Postgres の UUID カラムに自動で変換されます。これにより、スキーマを変更せずに ULID のソート利点を活用できます。
挿入時刻順のソート
タイムスタンプ付きプレフィックスのおかげで、新しい ULID は常に古いものより大きくなり、インデックス内では後に挿入されたレコードが末尾に物理的に配置されます。対照的に UUID v4 のランダム性はインデックス全体に分散します。
例:
SELECT * FROM ulid_test WHERE kind = 'ulid' ORDER BY id;
結果:
| id | kind | value |
|---|---|---|
| 019aaae4-be9c-d307-238f-be1692b3e8d7 | ulid | 1 |
| 019aaae4-be9d-011f-b82e-b870ca2abe9d | ulid | 2 |
| 019aaae4-be9f-e9d7-6efc-5b298ecc572b | ulid | 3 |
| 019aaae4-bea0-deae-6408-d89e7e3ce030 | ulid | 4 |
| 019aaae4-bea1-8ed2-c2f5-144bb1ffedde | ulid | 5 |
レコードは挿入順と同じ順序で返されます。
URL にも最適
ULID は URL に組み込みやすく、短く洗練された表記です:
/users/01KANDQMV608PBSMF7TM9T1WR4
1 ミリ秒あたり約 1.21 × 10²⁴ 通りの一意 ID を生成できるため、多くのアプリケーションに十分な容量があります。
制限事項
極端に高頻度で書き込みを行うシステムでは、現在時刻周辺に集中する書き込みがホットスポットとなり、特定のインデックスブロックで遅延や性能低下が起こる可能性があります。
代替案と将来
CUID や NanoID 等も存在しますが、ULID の利点はユニーク識別子標準として大きな影響力を持っています。最新の提案では UUID v7 が登場し、時間順序構造を採用して旧バージョンのソート性と性能問題を解決することを目指しています。
YouTube チャンネルでさらにデモをご覧ください。