Auto increment, UUID, ULID をざっくり比較
新しく開発するアプリケーションにおいて Primary Key を何にするか?という問題を検討する必要が出てきたため、改めて勉強しなおした。
Primary key の条件
Primary key に設定されたカラムは、以下の制約が設定される。
- UNIQUE 制約
- NOT NULL 制約
また、基本的には Primary key は不変でなければならない。つまり、一度決定したらその値を変えないことを前提に、その何を Primary key にするか検討する必要がある。
Natural Key と Surrogate key
まず Natural Key と Surrogate key という分類の仕方があることを知る。
- Natural Key(ナチュラルキー)
- 人間がその値を見て意味を理解できる key
- 例えば e-mail, tel, user_id など
- 先述の通り、 Primary key は不変でなければいけないため、 email や user_name などを Primary key に設定することは避けた方が良い
- Surrogate key(サロゲートキー)
- 一定の条件に従って自動で付与される key
- Auto increment, UUID, ULID など
- 特段の事情がなければこちらを使うべきだろう
比較
各種 Surrogate key の特徴とメリット・デメリットをまとめていく。
Auto increment (id)
特徴
- DBが標準で備える機能を利用する、1から自動で連番が振られる仕組み
- Laravel や Ruby on Rails ではこれがデフォルトである
メリット
- Auto increment そのものが 生成日時のソートとして使用可能
- DB1台で運用するなら余計なことを考えなくて良いので楽
デメリット
- Primary key の発行が RDB に依存する。これにより次の問題が生じる
- アプリケーション側では Primary key を null安全な型として定義・運用しなければならない
- 将来的にDBを分散する構成を取る場合に、複数のDB間で一意性の保証が難しい(各DBから参照されるAuto increment table を作るのか?)
- URL形式に id を用いることに場合( https://example.com/{id} などのように) Primary key が連番であることが分かってしまう
- これにより、以下の懸念が発生する
- id の部分に
1
からインクリメントしてアクセスしていくことで簡単にスクレイピングされてしまう - id =
22
と分かれば これまでに 21回 しか投稿されていないサービスなのだと検討がついてしうまう(閑古鳥が鳴いているサービスだとバレる。冗談のようでユーザが定着するまでは結構つらい問題)
- id の部分に
- これにより、以下の懸念が発生する
UUID (v4)
- 128bitの数値で構成され、 16進数32桁(
8+4+4+4+12
)で表現される - format:
RRRRRRRR-RRRR-4RRR-rRRR-RRRRRRRRRRRR
- 4 = UUID v4 であることを表す固定値。 この値は v3 なら 3、 v5 なら 5 の固定値となる
- r = バリアント (8, 9, a, b のいずれかが割り当てられる)
- R = ランダム英数値(1-9A-F)
- DBを分散する構成を取っても Key の一意性が保証される(理論上は)
メリット
- ランダムに生成されるにもかかわらず衝突しないものとして扱うことができる(理論上は)
- 国際的な標準規格である (
ISO/IEC 11578:1996
およびRFC 4122
)- これは以下のことを意味する
- UUIDが実装されている言語・フレームワークは(ULIDと比較して相対的に)多く、特別な手順を踏まなくても利用できることが多い
- 仕様が変更になるリスクがほとんどない(絶対ではない)
- 仕様が廃止になるリスクも相対的に少ない(絶対ではない)
- これは以下のことを意味する
- 自分に振られたUUIDや他人のUUIDを知ったところで、有用な情報は推測できない
- 生成日時も既存レコード数もMacアドレスも分からない
- UUID を推測してスクレイピングすることも非常に効率が悪い
デメリット
- UUID でソートしても生成順に並ばないため、UUID によるソートで意味のある結果を得られない
- 作成日時でソートしたい場合、created_at と UUID を併用してソートしないと 冪等性が確保されない
- created_at だけでソートする場合、同一日時のレコードがあると検索結果が安定しない
- ページ跨ぎの際に表示されないレコードが発生してしまう可能性がある
- created_at だけでソートする場合、同一日時のレコードがあると検索結果が安定しない
- 文字列型であるため、数値型である Auto increment よりはどうしてもパフォーマンスでは劣っていまう
- そしてこれはレコード数が増大になるほど顕著になる
ULID
UUID の弱点である「生成日時のソートに利用できない」という欠点を補う形で公開されたもの。
- 公式の仕様書: https://github.com/ulid/spec
特徴
- format:
ttttttttttrrrrrrrrrrrrrrrr
- T = timestamp
- R = ランダム英数値(A-Z1-7)
- example:
01ARZ3NDEKTSV4RRFFQ69G5FAV
- 128bitの数値、 32進数 26桁の英数値
-
以下の通り計算すると辻褄があう
- 2進数5桁でULID桁、これを125bit分行うことで ULID25桁を構成
- 余った3bit で 最後のULID1桁 を構成
-
公式ドキュメントにも
5 bits per character
の記述がある -
2進数換算で1桁余る?
-
先頭48bit が タイムスタンプ
- 10進数に直すと Unix time になる
- つまり 先頭10桁だけ抽出してソートすると生成日時順になる
- 西暦10889年までオーバーフローしない
- 10進数に直すと Unix time になる
-
残りの80bit が ランダムに付与される値
-
01AN4Z07BY 79KA1307SR9X4MV3
|----------| |----------------|
Timestamp Randomness
48bits 80bits
メリット
- ULID だけで 生成日時によるソートが可能(created_at を併用しなくても冪等性が保証される)
- UUIDのメリットは基本的に全て継承している
デメリット
- ULIDからそのデータを作成した日付は簡単にバレる(先頭10桁を Unixtime に直すだけでいいため)
- 変更不可能な生成日時が公開情報になることがサービスを利用する上で利用者を遠ざける要素になるのか、また運営者として困ることがあるかは一度考えたほうがいいかも
- 文字列型であるため、数値型である Auto increment よりはどうしてもパフォーマンスでは劣っていまう
- そしてこれはレコード数が増大になるほど顕著になる
- これに関しては UUID も ULID も一緒(ULIDに優位性はない)
- 国際的な標準規格ではなく、ULID とは 有志によって提示された設計(アルゴリズム)に過ぎない(各言語・フレームワークでは この設計に基づいて各コミュニティごとに実装が行われている)
- これは以下のことを意味する
- 仕様が変更になる可能性が(UUIDと比較して相対的に)ある
- 仕様が廃止になる可能性が(UUIDと比較して相対的に)ある
- これは以下のことを意味する
以上を踏まえて
スペックだけ見るとULIDに飛びついてしまいたくなるのだが、やっぱり将来的な仕様変更・廃止のリスクを考えると二の足を踏んでしまう。
要件によるというのが大前提だが、 UUID がやっぱり無難かなと思った。