- ブログ
データベースの主キーのデータ型えらび(Serial vs UUID vs ULID)
2024.03.14 Thu

飼っている犬(ポメコギ)の体重が体感値で8kgを超えました。つなまよです。重いです。
先日、初めてDB設計を担う機会があり、その際にデータベースのプライマリキー(PK)の形式をどうするか悩んだので、PKの形式として一般的なSerial、UUID、ULIDがそれぞれ何が違うのか調査した結果をまとめたいと思います。
概要
プライマリキー(以下、PK)とは、データベースにおいて、レコードを他のレコードと区別するために使用する、ユニークなデータです。
その形式は「連番(Serial)」か「文字列」という二つに大別されており、それぞれ用途、特徴が異なります。
例えば、連番は1,2,3などの増加する整数です。一方「文字列」形式にはUUIDやULIDなどのフォーマットがあり、その中でもバージョンによって文字列に含まれる情報には差異があります。
今回は、あくまでwebアプリケーションのデータベースに使用するなら、という視点でまとめます。
結論
それぞれの形式の特徴を抽象化し、比べた表が以下です。
◎,○は観点に対してポジティブ、✕は観点に対してネガティブであると定義します。
| Serial | UUIDv4 | UUIDv7 | ULID | |
| データ型(MySQL) | BIGINT | VARCHAR(36) | VARCHAR(36) | VARCHAR(26) |
| 生成速度・アクセス速度 | ◎ | ○ | ○ | ○ |
| ソート可能か | ○ | ✕ | ○ | ○ |
| 推測難易度(衝突可能性) | ✕ | ◎ | ○ | ○ |
| 特殊文字を含まないか | ○ | ✕ | ○ | ○ |
実務上は個別の要件に合った形式を選択することを前提として、目安で大別すると以下の結論になりました。
ユーザー側に露出されない → Serial
ユーザー側に露出される → ULID 、UUIDv7
UUIDv4も広く使用されている形式ですが、PKとして使用した場合、DBのパフォーマンスが低下することが指摘されています。
そのため、UUIDv4を使用する場合はPKとしてではなく、Serialなレコードをユーザー側に露出する必要が生じた場合に、Serialと組み合わせて使用するといった用途が考えられます。
その使用方法において、UUIDv4はランダム部が長いため、衝突可能性が今回比較した形式の中では最も低いという利点があります。
Serial vs UUID
本来、UUIDとは文字列型のPKのフォーマットの一つですが、この比較においては以下のように定義して比較します。
Serial:連続性のある整数
UUID:ランダム性のある文字列のデータ
生成速度(Serial:○ , UUID:△)
UUIDの生成においては乱数ジェネレータへのアクセスが必要なため、Serialの方が速いです。 しかし、数万件単位の生成でやっと有意な差が見られる程度であり、実務上は誤差の範囲でしょう
データサイズ(Serial:○ , UUID:△)
単一のデータサイズは以下のようになります。
Serial:最大8バイト(64bit)
UUID:常に16バイト(128bit)
数万件のレコードが保存される場合や、数万件のレコードに同時にアクセスする場合には有意な差があると言えます。
推測難易度(Serial:× , UUID:○)
SerialとUUIDの比較において最も明確な差は推測難易度と言えます。
Serialは連続性があるため、前後との関係性が想像できたり、文字列として独自性が低いため、総当たり的な悪意のある攻撃を受けた場合に一致する可能性が高いと言えます。
一方、UUIDのようなランダムを含む文字列の場合、総当たり的な攻撃を受けた場合に一致する可能性を軽減することができるでしょう。
UUIDのバージョンごとの違い
UUIDは現在、バージョン1から8まで存在します。
バージョン1から4はRFC4122に定義されており、5から8はドラフト状態となっています。
ここではそれぞれのバージョンごとの違いをざっくり紹介します。
詳しく知りたい方は以下を参照してください。
ver1〜4(定義済み)
RFC4122:https://www.rfc-editor.org/rfc/rfc4122
ver1〜8(ドラフト状態)
IETF Datatracker:https://datatracker.ietf.org/doc/draft-ietf-uuidrev-rfc4122bis/14/
v1 タイムスタンプ + クロックシーケンス(3文字) + MACアドレス
v2 UnixUID + タイムスタンプ + クロックシーケンス(1文字) + MACアドレス
MACアドレスを使用するため同一機器による生成では分散しにくい。タイムスタンプを使用しているが、秒:分:時のように保持しているのでソートできない(しづらい)
v3 MD5ハッシュを用いて生成。ランダム要素はなく同じ入力からは同じ出力がされる。
v5 SHA-1ハッシュを切り詰めたものを用いて生成。v3同様ランダム要素はなし。
文字列をハッシュ化することで生成するため、同じ入力値からは同じ出力が得られる。ユニークであることが大事なプライマリキーの生成方法としては不向きに思える。
ハッシュ化に使用するアルゴリズムとしてはMD5、SHA-1のどちらも脆弱性が指摘されており、暗号強度として信頼性は高くない。
v4 完全にランダムに生成。アルゴリズムに指定はなし。
完全にランダムなため理論上衝突しうるが、現実的には実現不可能な試行回数が必要。
v6 v1にグレゴリオ暦ベースのタイムスタンプを加えた形式
v7 Unix Time Stamp + ランダム部
ソート可能なタイムスタンプを持つバージョン。
v6はv1のフィールド構成を見直し、ソート可能にしたもので、v1を想定して構築したシステムの移行目的での使用で推奨されている。それ以外の用途では基本的にはv7が使用される。
v8 ベンダー固有のユースケースに合わせてカスタム可能な形式
現在、UUIDでは、以下の二つのバージョンが一般的に使用されているようです。
ソート不可で推測難易度が高い:v4
ソート可能で推測難易度も十分高い:v7
v1, v2, v6ではMACアドレスが使用されており、MACアドレスは世界的に一意であることが【推奨】されているだけなので、一意性が担保されているものではありません。
また、同一のマシンで生成している場合、MACアドレスの部分は変化しないため、推測難易度の観点からも使用される機会が少ないようです。
※ クロックシーケンスについて
クロックシーケンスはCPUのクロックごとにインクリメントすることが”推奨”されているランダムな文字列で、同じタイムスタンプ、同じMACアドレスからUUIDを生成した場合の衝突可能性を軽減するための情報です。
UUID v7 vs ULID
「ソート可能」な「文字列型」で「ランダム要素を含む」PKの生成方法であるUUID v7とULIDの違いに注目して比較します。
文字数(UUIDv7:ハイフンを含む36字, ULID:特殊文字を含まない26文字)
UUIDv7とULIDの両者は生成において128bitの情報として生成されます。8-4-4-4-12の合計32文字が生成され、UUIDv7はそれらをハイフンで繋ぎ36字とします。
ULIDは生成された32文字をBase32エンコードされ26文字となります。
ランダム部(UUIDv7:12bit + 62bit, ULID:80bit)
UUIDv7はUnix Epochタイムスタンプ + バージョン情報 + ランダム部で構成されており、ULIDに比べてランダム部が短くなっています。
ULIDはUnix Epochタイムスタンプ + ランダム部で構成されています。また、同じulidインスタンス内でタイムスタンプが衝突した場合、ランダムビットの最下位がインクリメントされ衝突を回避します(2の80乗回まで)。
どちらもタイムスタンプが衝突した場合ソート性は保証されません。
特殊文字の使用(UUIDv7:ハイフンを含む, ULID:特殊文字を含まない)
使用用途によっては影響することがあるかも。
UUIDv7とULIDの違いは以上です。 大きく異なるのは文字数でしょう。どちらも生成段階は4bitずつの16進数で作られますが、ULIDは生成された文字列をBase32エンコードすることで文字数を減らしています。
総括
今回の調査によってUUIDの設計思想やULIDの設計思想に触れることができ、PKに限らずデータに何を含め、どう保存するのかという観点を学ぶことができました。
PKの形式の選び方についての結論としては、第一にSerialという基本形。
そこから「推測を困難にしたい」「他のテーブルやサービスのPKと比較しても唯一性のあるPKにしたい」などの要件に応じてUUIDやULIDを選択する
という考え方に至りました。
UUID、ULIDの定義を探すところから、それぞれの項目がどういった意図で設計されたものなのかまで追って調べていたので、大学時代のゼミのレポート課題を思い出しました。
次回はもう少しライトなテーマだといいな、と願ってやみません。
以上、つなまよでした。