データベースのテーブルのIDの形式は一見単純に見えますが、実際には深い洞察が求められます。
特に最近は、オートインクリメントを用いるケースが減少し、他の方法を採用する動きが見られます。
複数の項目でレコードを特定できるような場合でも、複合主キーではなく別途IDを付与することが多いです。
しかし、IDの形式には様々な選択肢が存在します。そこで、どの形式を選ぶべきかについてまとめてみました。
概論
先に簡単な結論です。
基本的には、順序が保証されるULIDを使用するのがよいです。(UUID v7はバージョニングの関係でULIDの下位互換的になっているので特別な理由がなければULIDを使用する方が良いでしょう。)
シャーディングを行う要件があり、IDによるデータ分散が重要な場合(例えばデータベースにTiDBを採用しているなどのケース)では、順序がランダムになるUUID v4が適しています。
また、BtoBのシステムで顧客ごとにデータベースを分けている場合などでは、Auto Incrementを使用すると顧客Aと顧客BでIDが重複するリスクがあります。しかし、UUIDやULIDを使用すれば、この問題は回避できます。
フロントのJSでIDを生成するような要件であればCUID2が有力です。
Auto Incrementにはメリットも存在しますが、スケールするまでは重要性が低いのに、システムがスケールするにつれてULIDやUUIDの利点が大きくなりやすいです。そのため、具体的な要件を慎重に考慮し、Auto Incrementが適していると判断された場合のみ、Auto Incrementを採用することをおすすめします。
IDのパターン
Auto Increment
Auto Increment(オートインクリメント)は非常に一般的な形式で、多くのRDBMS(MySQL、PostgreSQL、SQL Serverなど)がサポートしています。
該当のテーブルにデータが新規に追加される度に自動的にIDが生成され、1から始まる連番が自動的に振られていきます。
ほとんどのデータベースにこの機能が存在し、IDを指定せずにデータをINSERTできる代表的な手段となっています。
Auto Incrementは連番であるため、保存に必要な領域は少なく、データが自然な順序で保存されることからデータベースのパフォーマンスを発揮しやすいという利点があります。
一方で、連番であるが故にID自体が意味を持ってしまうという問題点があります。「新規登録したらID100だったから100ユーザー目だ」「書いた記事のIDが1000だったから全部で1000記事しかない」といった情報が一般的なユーザーにも推測可能になります。さらに、「URLのID部分を1ずつ増やしてスクレイピングすれば全パターン網羅できる」などといった列挙型攻撃ができてしまうなどセキュリティ的な懸念もあります。
また、BtoB(企業などを対象とした)サービスで、企業ごとにデータベースを分離する場合、それぞれのデータベースで連番が振られるため、同じIDのレコードが生成されてしまう可能性があります。その結果、データを合算する際にはID以外の値を組み合わせて一意性を確保しなければならない状況が生じることがあります。
さらに、データをINSERTするまでIDが確定しないため、複数のテーブル間での依存関係がある場合など、事前にIDを知る必要がある状況では不便さを感じることがあります。
UUID v4
UUID(Universally Unique Identifier)は一意性を確保するために広く使用され、バージョン4のUUIDは乱数により生成されます。
Laravelではバージョン9.31からサポートされています。MySQLでもUUID_TO_BIN()などの専用関数があり、16ビットのバイナリで扱えます。
各UUIDは128ビットの数値で、例えば"31ebb21b-604e-9e35-f409-86cf2b5d1cbc"のような形式で16進数に変換されて36文字で表現されます。
6ビットはバリアントとバージョンビットのために固定されており、122ビットがランダム部分となっています。
UUIDの値は非常に大きな範囲からランダムに選ばれるため、他のUUIDと衝突する可能性は極めて低いと言えます。
UUIDは大規模な分散システムやマイクロサービスのような環境で頻繁に利用されますが、そのランダム性がデータベースのインデックス性能に影響を及ぼすことがあります。
一方で、ランダムに分散されることからシャーディングに利用でき、TiDBなど利用するデータベースによっては最適解となります。
なお、理論的にはUUIDが衝突する可能性が存在しますが、その確率は極めて低いです。
確率論的な問題で、衝突する事例がまったくないわけではありませんが、
実際に衝突が生じるケースは偏ったランダム数の生成方法によるものが多いです。そのため、UUIDの生成には信頼性の高いライブラリを使用することを推奨します。
UUIDがどのくらいぶつからないかに関する具体的な確率は多くの情報源で提供されており、検索すると色々出てきますが例として230京回UUIDを生成するとぶつかるそうです。
同じテーブル内で衝突が発生すると問題となるため、一つのテーブルに230京行以上のデータが存在することを想定している場合、UUID以外の一意性を確保する方法を検討することが推奨されます(これはもちろん、ほとんどのアプリケーションでは想像を超える数です)。
1億行のテーブルを1億個持っていても、その合計は1京行であり、この比較からもUUIDの衝突確率の低さが理解できます。
GUID
UUIDの実装のうちの一つです。主にマイクロソフトの実装によるものを指すことが多いですが、UUIDの別名として用いられることもあります。
参考:Microsoft:GUID 構造体
ULID
ULID(Universally Unique Lexicographically Sortable Identifier)は、一意性とソート可能性を両立した識別子です。
ULIDはUUIDの一部の問題、特にソートができない問題と文字列表現が長すぎるという問題を解消するために設計されました。
LaravelではUUIDと共にバージョン9.31からサポートされています。
ULIDはUUIDと同じく128ビットの値ですが、前半の48ビットで表されるミリ秒単位のタイムスタンプと、後半の80ビットで表されるランダムな値から構成されます。
これにより、ミリ秒単位で同時に生成されたULIDでも、ランダムな部分により一意性を保つことができます。
タイムスタンプ部分は西暦10889年まで表現可能で、まだ8千年以上の猶予があります。
ULIDの例は、"01ARZ3NDEKTSV4RRFFQ69G5FAV"のようになります。これらはBase32でエンコードされ、その結果としてアルファベットと数字のみで構成された26文字になります。大文字と小文字は区別されませんが、一般的には大文字で表現されます。
UUIDと比較して、ULIDは時系列順にソート可能で、これによりデータベースのレコードの順序付けが容易になり、挿入順に従ってデータを保存するデータベース(一般的な多くのRDBMS)では、パフォーマンス低下を防ぐことができます。ULIDは時間に基づいて部分的に生成されるため、UUIDとは異なり同じミリ秒に生成されたULID間のみ衝突の可能性があります。しかし、その確率も極めて低いです。
先に引用したUUIDの衝突確率に関する計算式をULIDにも適用すると、280/2=1.0995116e+12回≒1.1∗1012となり、期待値として同一ミリ秒内に約1兆回ULIDを生成すれば衝突することになります。
しかし、1秒よりはるかに短い時間で約1兆回ものレコードを生成することは、UUIDが衝突するよりも先にDBのストレージが満杯になるでしょう。
CUID
CUIDはクライアント(フロント)での生成を中心に考慮されたIDです。
ブラウザ上で実行されるJSは、乱数生成などがどうしてもブラウザの実装に依存します。そのためフロントでIDを生成しようとするとブラウザによっては偏りがあり衝突しやすい場合があるなど様々な問題がありました。それらを解消するために開発されたのがCUIDです。
現在はver1はDeprecatedになっていてCUID2の利用が推奨されています。(そのため以下はCUID2のことをメインに記載しています。)
ホストのフィンガープリントやセッションカウンターを利用してIDを生成します。形式はアルファベットの小文字と数字のみで、公式に記載がある例として"tz4a98xxat96iws9zmbrgj3a"などがあります。コンフィグにより長さなどを調整することもできます。
JSでの利用を中心に考えられているので、Reactなどが中心でそちらでIDを生成するようなプロジェクトでは有力な選択肢となるでしょう。
ソート可能な形式ではないので、データベースによってはデータ量が増えてきたときに速度低下が発生します。
CUID2のREADMEに他IDとの比較が色々書いているので採用の参考にしやすいです。ただし「ULIDやnanoidは高速すぎてセキュアじゃなくてちょうどいい速さはCUIDだ」など根拠の記載なく記載されている部分もあり一部宗教戦争の域に入っている印象を受ける部分もあります。
個人的にはフロントなどJS優位のプロジェクトなら有力な選択肢ですが、そうでなければまだ利用実績が十分でなく長期プロジェクトでの採用は懸念が残る印象です。
実際に最初のものはDeprecatedとしてCUID2になっており、CUID2のinitial commitは2022/12/06です。
その他パターン
大手の有名なサービスでは、独自のID生成方式を採用していることがあります。
しかし、これらの方法は大量のデータを処理することを前提に最適化されていることが多く、一般的なシステムでの使用には適さない場合があります。
また、独自の方式を実装するには時間やサーバー等のコストがかかるため、世界的な大規模サービスでない限りは参考にする必要性は低いでしょう。
Twitter: snowflake
一時期オープンソースとして公開されていたSnowflakeは、その設計を参考にしたプロジェクトを多く見ることができます。
UUIDなどは便利ですが、衝突を避けるために十分なビット数を必要とします。UUIDは128ビットを使用しますが、対してSnowflakeはその半分の64ビットしか使用しません。
この問題を解決するために、Snowflakeは専用のID採番サーバーを設置し、IDの生成と管理を効率化しています。
タイムスタンプ部分については、システムが稼働を開始してから約69年間使用可能です。
この仕組みについて詳しく解説されているスライドがありましたのでリンクしておきます。
まとめ
まとめとして、IDの形式選択は極めて重要であり、それぞれの形式が特有の利点と欠点を持つことを理解することが求められます。Auto Increment、UUID、ULIDのどれを選択するかは、システムの要件、スケールの大小、シャーディングの必要性など、システムの具体的な状況に強く依存します。
最後に、これらの識別子形式はあくまでツールの一部です。システムのニーズに最適な形式を選択し、その選択がシステム全体のパフォーマンスやメンテナンス性にどのように影響するかを理解することが重要です。
設定項目のあるIDも出てきていますが、個人的には特別な理由がない限りIDのコンフィグはあまり触らない方がいいと考えています。
というのも、世界中の博識な人が集まってもまだIDの正解が出ておらずこれだけ多様なIDがあるわけなので、衝突確率など詳しいことを把握せずになんとなくで設定を変えてしまうと返って要件を満たさなくなる可能性があるので注意しましょう。
本記事があなたの選択を補助する一助となれば幸いです。また何か疑問や新しい視点が生まれましたら、ぜひ共有していただければと思います。次回の記事でお会いしましょう。