🍺

ID生成を理解して使いこなす〜UUIDとGUIDとULIDとCUID〜

に公開

IDの生成方法はDBのパフォーマンスやスケーラビリティにも影響する重要な要素です。また2024年5月にUUIDの新標準であるRFC 9562が制定されたこともあり、備忘録も兼ねてメジャーなユニークIDについてまとめてみました。

UUID (Universally Unique Identifier)

おさらい

UUIDは RFC9562 で定義されているID生成方法です。128bit(4bit * 32文字)で構成されます。

例(ハイフン除く):11223344-5566-7788-99aa-bbccddeeff00

このとき、次のアルファベッドの箇所に意味があるのでそれぞれ説明します 11223344-5566-{M}788-{N}9aa-bbccddeeff00

Mはバージョンです。UUID v1 〜 UUIDv8 があり、バージョンに応じて1〜8が入ります。またNはバリアントと呼ばれる値で、現在のRFC仕様的には次の4つの値が入りえます。

  • 8(1000)
  • 9(1001)
  • A(1010)
  • B(1011)

バリアントは、RFCでは上位2ビットが2進数で 10 から始まることが定められています。
バージョンとバリアントで6ビットを使うので、識別子として利用できる部分は122bitの範囲となります。

デメリットとしては、数値型のIDと比べるとデータサイズが128bitと大きい点や、可読性の低さ、またRDBのインデックスとして採用した際のインデックス効率の悪さ(indexが隣接する可能性が低く、分散しやすいのでクラスタインデックス効率が悪くなりがち)などがあります。

ちなみにUUIDバージョンは現時点で1〜8までありますが、2024年に制定されたRFC 9562によると、一般の用途として推奨されるのは4と7と書いてあったので本記事では4と7の特徴をまとめていきます。

UUID v4

バージョン4のUUIDは、ランダム or 擬似ランダムに生成されます。ちなみに、UUID v3がMD5でUUIDを生成する仕様となっているが、MD5が脆弱なのでセキュリティが懸念される用途の場合非推奨となっている(RFC的には、そうでない場合は使っても良いっぽい?)。

基本的にUUID v4(バリアント1)の場合は、バリアント部に2bitしか使わないので、冒頭で記載した通りバージョン部とバリアント部で合計6ビットの占有で済みます。そのため122ビット(2の122乗)の範囲が識別子として使えます。

SQLServerなど、Microsoft系システムで採用されるバリアント2(GUID)の場合、バリアント部に3bit使うので、121bit(=バリアント1の半分)の範囲しか使えないケースがあります。しかし121bitでも十分に大きい範囲でもありますし、あまり意識するケースは多くないかなと思います。

近年は分散DBなどのインデックスでの採用が盛んですが、v4は並べ替えができないなどの点が指摘されがちですね。

UUID v7

一言で要約すると、自分は「高い一意性を実現しつつ UUID v4を時系列ソートできるようにしたもの」という捉え方をしています。

{タイムスタンプ部1}-{タイムスタンプ部2}-{バージョン}-{バリアント}-{ランダム値} というような形式です。

先頭48bitが生成時刻で、ミリ秒単位のUNIX timestampが入ります。バージョン部とバリアント部を除いたその他ランダム値セクションが、74bitあります。

値の例: 018e15ba-ff46-7023-540b-bffb6d3518e4 (2024-03-06 21:46:00.390 UTC)

特徴1. 先頭48bitの値が連続値になることで、MySQLのクラスタインデックスや分散DBでは効率的にインデクシングされやすい(ただしMySQLなどのRDBでは数値型のindexよりは劣ることが多そう)

特徴2. 先述のフォーマットから推測できる通り、v7はIDがわかると時刻が特定できます

なので、IDから時刻が特定されても問題ない場合v7にする、やパフォーマンス的な意味でv7にする、といった技術選定ができそうです。

GUID( Globally Unique IDentifier )

GUIDは、RFC9562では「UUIDと同じ」というように説明されています。これだと分かりづらいですが、GUIDはMicrosoftが提唱したもので、UUIDのMicrosoft版実装と考えるのが良いかなと思っています。実際Microsoft関連のシステム(例えばSQLServerなど)で採用されていることが多いですが、普通のWeb開発で意識するケースは少ないかと思います。

UUIDと違う点は、GUIDの方ではMixed-Endianとして解釈する点です。

例えば 00112233-4455-6677-8899-aabbccddeeff というUUIDがあった時、

これをGUIDとして解釈する場合は
33 22 11 00 55 44 77 66 88 99 aa bb cc dd ee ff
というオーダーになります。

GUIDでは最初の3コンポーネントをLittle-Endianとして解釈し、最後の2コンポーネントをBig-Endianとして解釈する。

ULID(Universally Unique Lexicographically Sortable Identifier)

ULIDはUUID v7と似ており、時系列ソート可能なIDです。先頭48bitがタイムスタンプで、UUID v7と同じく1ms単位でソート順が保証されます。UUID v7より少ない文字列(26文字)で表現できることが特徴です。またUUIDと互換性があります。

128bitのIDをCrockford's Base32でエンコードし、26文字に短縮したものがULIDです。例えばDB上でUUID v7を生成するときはCHAR(36)のサイズが必要ですが、ULIDの場合はCHAR(26)のサイズのみで済みます。

値の例 : 01GMZ2A8M08R8S5Q9D8Z7Z88H5 (2022-07-28T14:48:40.000Z)

CUID(Collision-Resistant Unique Identifier)

CUIDは、衝突耐性を持ちつつ、バイナリサーチによる検索効率の高いユニークIDの生成を目的に作られた新しいID生成方式です。UUID v7のように時系列ソートを可能にした上で、ULIDの改良版とも言われているらしい。ざっくりいうと、ULIDより短い文字数でUUID v7やULIDと同等 or 同等以上の衝突性、時系列ソート特性があるよ、みたいなもの。

ちなみにUUIDと違って、まだRFC等で標準化されていないため利用には注意が必要な可能性がある。

値の例 : ab12cdefgh34567ijk1234567

長さは20〜25文字(ULIDより短い)。タイムスタンプ、カウンター、ランダム値を組み合わせ生成され、UUID v7やULIDと同じく時系列ソートが可能。

IDのデータサイズに制約があるかつ、時系列ソートが必要だったり高い一意性が必要な場合で検討するケースがありそうです。一度業務でCUID v4を使ったシステム(内部で分散MySQLを利用)を組んだことはあり、パフォーマンス面や保守面でまだ困ったことはないですが、まだまだ採用事例が少ないので採用は慎重派ではあります。

まとめ

DBのインデックスやPKについて考えるケースでは、パフォーマンス面でいうとstringなUUID/GLIDより、基本的には数値のAutoIncrementの方が良いケースが多いと思います。ただやむをえずstringのユニークIDを利用する場合や、分散DBのシャード振り分けなどを考慮するときは各ユニークIDの特性を考慮して設計していきたいですね。

AutoIncrementによるID予測可能性を許容するか / UUID v4のランダム性による問題はないか(クラスタインデックス観点などで) / UUID v7よりもUULDの小さいデータサイズを重視した方が良いか、など考えられることが多そうです。

参考文献

https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding

https://tex2e.github.io/rfc-translater/html/rfc9562.html

Discussion