UUID・ULID・Nano ID・オートインクリメントIDの比較
よく使われる5つのID方式を、実務で効いてくる観点で比較します。サイズ、並び順、衝突確率、データベースでの挙動、そしてIDから何が読み取れるかです。
ID形式の選択は、3つの問いに集約されます。IDを作成時刻順に並べる必要があるか。IDがURLや公開APIに現れるか。そしてデータベースに128ビットのネイティブ型で保存するのか、テキストで保存するのか。2024年5月にRFC 9562で標準化されたUUID v7は、多くのプロジェクトでこの答えを変えました。ULIDを人気にした時刻順の並びを、あらゆるUUIDカラムとライブラリがすでに受け付ける形式で実現しているからです。
一覧で比較
| オートインクリメント | UUID v4 | UUID v7 | ULID | Nano ID | |
|---|---|---|---|---|---|
| 例 | 1042 | f47ac10b-58cc-4372-a567-0e02b2c3d479 | 01926f3a-7c2b-7d4e-9a1f-3b5c8d2e6f10 | 01J9QKMZ1BFN79M7SVBJ6JWVRG | V1StGXR8_Z5jdHi6B-myT |
| バイナリサイズ | 4または8バイト | 16バイト | 16バイト | 16バイト | テキストとして保存 |
| テキストの長さ | 最大10桁または19桁 | 36 | 36 | 26 | 既定で21 |
| ランダムビット | なし | 122 | 最大74 | 80 | 既定で126 |
| 作成時刻順に並ぶ | はい | いいえ | はい(ミリ秒単位) | はい(ミリ秒単位) | いいえ |
| データベースなしで生成できる | いいえ | はい | はい | はい | はい |
| 仕様 | データベースの機能 | RFC 9562 | RFC 9562 | コミュニティ仕様 | ライブラリ |
| 読み取れる情報 | 件数と順序 | なし | 作成時刻 | 作成時刻 | なし |
それぞれの構造
- オートインクリメントはデータベースが管理するカウンターです。符号付き32ビットの
INTは2,147,483,647で、符号付きのBIGINTは9,223,372,036,854,775,807で上限に達します。シーケンスから先に値を予約しない限り、アプリケーションがIDを知るのはINSERTの後です。 - UUID v4は、122ビットの乱数に、バージョンとバリアントを示す固定の6ビットを加えたものです。
- UUID v7は先頭がミリ秒単位の48ビットUnixタイムスタンプで、その後にバージョン、
rand_aと呼ばれる12ビット、バリアント、rand_bと呼ばれる62ビットが続きます。RFC 9562ではこれらのビットの一部をカウンターとして使うことが認められており、同じミリ秒内に作られたIDでも順番どおりに並びます。このサイトの生成ツールは、rand_aの12ビットでそれを行っています。 - ULIDは、ミリ秒単位の48ビットタイムスタンプと80ビットの乱数を、CrockfordのBase32で26文字に表したものです。時刻に10文字、乱数に16文字を使います。アルファベットにはI、L、O、Uが含まれず、大文字小文字を区別しません。単調増加(monotonic)モードでは、直前のULIDと同じミリ秒に作られたULIDは、直前のULIDの乱数部分に1を足したものになります。
- Nano IDはランダムな文字の並びで、既定では64文字のアルファベット
A-Z a-z 0-9 _ -から21文字を使います。1文字あたり6ビットなので、合計126ビットです。長さもアルファベットも変更でき、それに応じてランダムビット数も変わります。
ULIDとUUIDは、どちらも128ビットの数値を別の書き方で表したものです。01J9QKMZ1BFN79M7SVBJ6JWVRGと01926f3a-7c2b-7d4e-9a1f-3b5c8d2e6f10は同じ値を、Base32と16進数でそれぞれ書いたものです。だからULIDはPostgreSQLのuuidのようなネイティブのUUIDカラムに保存でき、そこでも並び順が保たれます。ただし、もともとv7 UUIDとして作られたのではないULIDは、バージョンとバリアントの位置に任意のビットを持つため、厳格なUUIDバリデーターに拒否されることがあります。
衝突確率を計算する
完全にランダムなIDには、誕生日問題の近似が使えます。N = 2のランダムビット数乗の空間からn個のIDを引いたとき、少なくとも1組が重複する確率は約 1 − e^(−n²/2N) で、n = 1.18 × √N 付近で50パーセントに達します。
| 方式 | ランダムビット | 重複の確率が50%になるID数 | 10億個のIDで重複が生じる確率 |
|---|---|---|---|
| UUID v4 | 122 | 2.7 × 10^18 | 9.4 × 10^-20 |
| Nano ID(21文字) | 126 | 1.1 × 10^19 | 5.9 × 10^-21 |
| Nano ID(10文字) | 60 | 1.3 × 10^9 | 約35% |
覚えておくべきは最後の行です。URLを見栄えよくするためにNano IDを短くすると、衝突耐性はあっという間に失われます。長さを決める前に、この計算をしておきましょう。
UUID v7とULIDでは、同じミリ秒を持つIDどうしでなければ衝突しません。したがってnは、カウンターを共有しない生成器が1ミリ秒の間に作るIDの数です。同じミリ秒にそうしたIDが1,000個あるとき、重複の確率はランダムビットが74ビットなら約2.6 × 10^-17、ULIDの80ビットなら約4.1 × 10^-19です。実際には、どの方式でもリスクは計算上の確率ではなく、Math.randomのような弱い乱数源にあります。
データベースのインデックスでの挙動
主キーは通常B-treeインデックスに格納されます。オートインクリメント、UUID v7、ULIDのように新しいキーが常に直前より大きい場合、INSERTはインデックスの右端に集中し、ページは隙間なく埋まり、最近書き込んだ行どうしが近くに置かれます。UUID v4やNano IDのようなランダムなキーはどこにでも入り込みます。ページ分割が起きて部分的に空いたページが残り、インデックスがメモリに収まらなくなると、INSERTのたびにランダムな読み込みが発生しかねません。
この影響は、行そのものを主キー順に格納するInnoDBを使ったMySQLやMariaDBでより大きくなり、ランダムなキーはインデックスだけでなくテーブル自体を散らばらせます。PostgreSQLは行を別のヒープに保存するため、違いはインデックスだけにとどまります。
方式と同じくらい、保存形式も重要です。
- UUIDやULIDは、ネイティブの16バイト型があればそれに保存します。PostgreSQLの
uuid、MariaDB 10.7以降のUUID、MySQLのBINARY(16)です。CHAR(36)は2倍以上の容量を使い、すべてのインデックスを大きくします。 - MySQLでは、v7の値に対して
UUID_TO_BIN(uuid, 1)のスワップフラグを渡さないでください。v1 UUIDのために時刻フィールドを並べ替える機能で、v7の並び順を壊してしまいます。 - SQL Serverの
uniqueidentifierは末尾の6バイトから比較するため、そこに保存したv7の値は時刻順に並びません。詳しくはUUIDとGUIDの違いを参照してください。 - Nano IDは文字列なので
CHARかVARCHARに保存し、大文字小文字を区別する必要があればバイナリ照合順序を指定します。
URLと、IDから漏れる情報
5つの形式はいずれも、パーセントエンコードなしでURLに使えます。16進数、ハイフン、CrockfordのBase32、Nano IDのアルファベットは、どれもRFC 3986の非予約文字だからです。違いは長さ(36文字、26文字、21文字)と大文字小文字の扱いです。UUIDとULIDは大文字小文字を区別しないので、大文字小文字を変えてしまうシステムを通っても問題ありませんが、Nano IDはそうはいきません。
決め手になりやすいのは、IDから何が読み取れるかです。
- オートインクリメントは量を明かします。今日の請求書番号が1042で、1週間後に1342なら、その会社はその週に約300件の請求書を発行したことになります。連番IDでは隣のレコードを試すのも簡単なので、アクセス権チェックの漏れがひとつあるだけでデータ漏えいにつながります。
- UUID v7とULIDは、レコードが作成された時刻をミリ秒単位で明かします。注文ならまず問題になりませんが、ユーザーアカウントや医療記録では問題になるかもしれません。v7 UUIDをUUID v7生成ツールの検査欄に貼り付けると、タイムスタンプが表示されます。
- UUID v4とNano IDは、いつどこで作られたかについて何も明かしません。
これらのIDはどれも秘密情報ではありません。推測できないIDは探りを入れにくくしますが、ユーザーがそのレコードを見てよいかを確認する処理の代わりにはなりません。
どれを選ぶか
- 単一データベース内の内部テーブルで、ユーザーに見せないもの:オートインクリメントの
BIGINT。最も小さく、最もシンプルなキーです。 - アプリケーション側やサービス間で作る主キーで、ネイティブのUUIDカラムが使える場合:UUID v7。
- 作成時刻を伏せておくべき公開識別子、相関ID、ファイル名:UUID v4。UUID v4生成ツールか
crypto.randomUUID()で作れます。 - 並び順が関係ないURL用の短いID:既定の長さか、それに近い長さのNano ID。
- ULID:すでに技術スタックで使っている場合や、より短く大文字小文字を区別しないテキスト表現が欲しい場合。新規プロジェクトなら、UUID v7が標準形式で同じ役割を果たします。
両方の長所を取るよくある組み合わせもあります。結合やインデックスにはBIGINTかUUID v7の主キーを使い、URLやAPI向けには一意インデックスを張った別のランダムな公開IDを持たせる方法です。