UUID・ULID・Nano ID・オートインクリメントIDの比較

よく使われる5つのID方式を、実務で効いてくる観点で比較します。サイズ、並び順、衝突確率、データベースでの挙動、そしてIDから何が読み取れるかです。

ツールで試す: UUID v7生成 UUID v4生成

ID形式の選択は、3つの問いに集約されます。IDを作成時刻順に並べる必要があるか。IDがURLや公開APIに現れるか。そしてデータベースに128ビットのネイティブ型で保存するのか、テキストで保存するのか。2024年5月にRFC 9562で標準化されたUUID v7は、多くのプロジェクトでこの答えを変えました。ULIDを人気にした時刻順の並びを、あらゆるUUIDカラムとライブラリがすでに受け付ける形式で実現しているからです。

一覧で比較

オートインクリメントUUID v4UUID v7ULIDNano ID
例1042f47ac10b-58cc-4372-a567-0e02b2c3d47901926f3a-7c2b-7d4e-9a1f-3b5c8d2e6f1001J9QKMZ1BFN79M7SVBJ6JWVRGV1StGXR8_Z5jdHi6B-myT
バイナリサイズ4または8バイト16バイト16バイト16バイトテキストとして保存
テキストの長さ最大10桁または19桁363626既定で21
ランダムビットなし122最大7480既定で126
作成時刻順に並ぶはいいいえはい(ミリ秒単位)はい(ミリ秒単位)いいえ
データベースなしで生成できるいいえはいはいはいはい
仕様データベースの機能RFC 9562RFC 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 v41222.7 × 10^189.4 × 10^-20
Nano ID(21文字)1261.1 × 10^195.9 × 10^-21
Nano ID(10文字)601.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を持たせる方法です。

よくある質問

ULIDはUUID v7より優れていますか?

両者の構造は同じで、48ビットのミリ秒タイムスタンプの後に乱数ビットが続き、合計128ビットです。UUID v7はIETFの標準で、UUIDカラムやバリデーターにそのまま適合します。ULIDはテキスト表現が短く大文字小文字を区別せず、乱数ビットが最大74ビットではなく80ビットあります。

Nano IDはUUIDと同じくらい一意ですか?

既定の21文字なら、はい。UUID v4の122ビットに対して126ビットの乱数を持ちます。短いNano IDではその強みがすぐに失われ、10文字では約13億個で重複の確率が50パーセントに達します。

UUID v7の値は衝突しますか?

衝突し得るのは、独立した生成器が同じミリ秒に作ったIDどうしだけです。1ミリ秒に1,000個ある場合でも確率は約2.6 × 10^-17なので、形式よりも健全な乱数源のほうがはるかに重要です。

ランダムなUUIDを主キーにすると遅くなるのはなぜですか?

新しいキーがB-treeインデックスのランダムな位置に入るため、ページ分割や半分空いたページが生じ、大きなテーブルではディスク読み込みが増えます。UUID v7やULIDのような時刻順のIDは、代わりに末尾へ追加されます。
ツールで試す: UUID v7生成 UUID v4生成