概略
まずはじめに訂正します。以前に投稿した「ダングリングDNSとドメインの終活【後編】」の中で「Qiitaにおいても、投稿記事は識別子(UUID)で管理されており」と記しましたが、UUIDは誤りです。UUIDの長さとは異なり別の方式で生成されたIDとみられます。本稿では誤りの反省を込めて、一意な識別子を生成する仕組みについて改めて整理したいと思います。
本稿に記載した内容は、個人的な見解を示したものであり、筆者の所属する企業・団体の公式見解ではありません。
なぜユニークIDが必要なのか
システム開発において「ユニークID(Unique Identifier)」は、データを区別するための“しるし”として欠かせません。例えば、ユーザーアカウント、記事、取引履歴、IoT機器のデータなど――同じ形式のレコードが膨大に存在する場面では、それらを一意に識別できる仕組みが必要になります。もし識別子が重複してしまうと、別々のデータが混同され、整合性の崩壊やセキュリティ上の問題につながりかねません。
もっとも単純な方法は、1から順番に番号を振っていく連番方式です。単一のデータベースの中で管理する限りは十分に機能しますが、分散システムや複数のデータベースが混在する環境では、異なる場所で同じ番号が発行される可能性を避けることができません。
また、外部との連携や公開APIで内部の連番をそのまま利用すると、容易に推測されて不正アクセスにつながる危険性もあります。さらに、ログやイベントを一意に識別する識別子を付与する場合には、監査証跡として誰がどの操作を行ったのかを確実に結びつけるために識別子の衝突がないことが求められます。
このように、単純な連番では解決できない要件が数多く存在するために様々な識別子の方式が考案されてきました。
UUID
UUID(Universally Unique Identifier)は、分散環境で一意な識別子を確実に生成するために考案され、最初は OSF DCE(Distributed Computing Environment)の一部として利用されました。その後 IETF によって標準化が進められ、RFC4122が広く使われる仕様となります。
RFC では UUID の生成方法に応じてバージョンが定義されています。
UUIDの識別
どの方式のUUIDなのかは、UUID内にあるバリアント(variant)」と「バージョン(version)」で識別することができます。
UUIDは128ビット、すなわち16オクテットで構成されています。見た目は32桁の16進数を「8-4-4-4-12」という区切りで並べた文字列形式(例:550e8400-e29b-41d4-a716-446655440000)ですが、この中には単なるランダム値ではなく、どの方式で生成されたかを示す情報が埋め込まれています。
UUIDの「variant」は、どの標準仕様に従っているかを示すフィールドで第4ブロックの先頭1文字によって判別でき、8~B の範囲で始まるものが一般的に使われているUUIDです。
UUIDの「version」は、規定されたどのバージョンに則っているかを示すフィールドで第3ブロックの先頭1文字によって判別できます。
550e8400-e29b-41d4-a716-446655440000
| |
| └─────────── バリアント
└──────────────── バージョン
| バリアント | 値 | 意味・仕様 |
|---|---|---|
| Variant 0 | 0~7 | Network Computing System互換形式。古い仕様 |
| Variant 1 | 8~B | RFC4122 / RFC9562 に準拠。最も一般的なUUID |
| Variant 2 | C~D | Microsoft GUID形式 など、一部実装で利用 |
| Variant 3 | E~F | 予約領域(将来用)。通常は使用されない |
UUIDのバージョン変遷と問題点
UUIDv1 はタイムスタンプとノードIDを組み合わせた方式です。100ナノ秒単位の時間を基準にし、同じ時刻に複数生成されても衝突しないようクロックシーケンスが付加されます。RFC4122ではノードIDはIEEE802の MACアドレスを用いることになっており、生成されたUUIDからネットワークカードのベンダーがわかります。
UUIDv2 は DCE セキュリティに関連した特殊な用途向けに定義され、ユーザーIDやグループIDといったローカル情報を組み込む方式です。ただし利用範囲は限られており、実装例も少なく、事実上ほとんど使われていません。
UUIDv3 は名前ベースの UUID で、名前空間 UUID と任意の文字列を組み合わせ、それを MD5 ハッシュにかけて生成します。決定性があり、同じ入力から常に同じ UUID を得られる点が特徴ですが、基盤となる MD5 の安全性が脆弱化したことが課題となりました。
UUIDv4 は最も一般的に利用されている方式で、ほぼ全ビットを乱数で埋めるランダム型 UUID です。衝突する可能性は非常に低く、実装も容易であるため、プログラミング言語やライブラリで広くサポートされています。しかし、完全にランダムであるため生成時刻や順序性がまったく反映されず、データベースのインデックスに使うとランダム挿入によって性能劣化が起こりやすいという問題があります。また、乱数生成器の品質に依存するため、乱数が偏ると衝突リスクが増す懸念もあります。
UUIDv5 は v3 と同じ名前ベースですが、ハッシュ関数に SHA-1 を採用しています。MD5 よりは安全性が高まったものの、SHA-1 自体も衝突攻撃が実現されたことで長期的には不安が残ります。さらに、名前ベース UUID は時間情報を持たないため、生成順序の管理には向きません。
このように、UUID v1〜v5 にはそれぞれ設計思想と利点がある一方で、プライバシー、乱数依存、順序性の欠如、ハッシュ関数の安全性といった問題がつきまといました。こうした課題を踏まえて、後に ULID や KSUID、さらに RFC9562で定義された UUID v6/v7/v8 といった改良版が登場していきます。
問題点を改良したバージョン
UUIDの初期仕様では、プライバシーや順序性、データベース性能などの問題が明らかになりました。これらの課題を解消するため、後継世代として ULID(Universally Unique Lexicographically Sortable Identifier) や、新たに標準化された UUID v6 / v7 / v8(RFC9562) が登場します。
ULIDは、UUIDの「ランダムで順序性がない」という弱点を克服するために2016年に提案された方式です。128ビット構造を保ちながら、最初の48ビットをミリ秒単位のUnix時刻、残りの80ビットをランダム値として構成しています。これにより、生成順にソートできる特性(lexicographical order)を持ち、同時に十分な一意性も確保しています。さらに文字列表現には Crockford’s Base32 が使われ、人間にも読みやすく誤記しにくい設計が採用されています。たとえば 01HZX6TP7YH0AK4R3Q5G6D6Q9F のような形式で、UUIDのようなハイフン区切りではなく、アルファベットと数字を混ぜた26文字で表されます。ULIDは標準化こそされていないものの、多くの言語・フレームワークでライブラリが提供され、UUID v4 の代替として実務でも広く利用されています。
このULIDの考え方は後の UUID v7 に引き継がれました。RFC9562では、従来のUUIDを改良する形で v6, v7, v8 が定義されています。v6 は v1(時刻+MACアドレス)をベースに、時刻ビットの配置を変更してソート可能性を高めた形式です。v7 は ULIDに近い発想で、Unix時刻(ミリ秒単位)とランダム値を組み合わせることで、時系列ソート可能かつ高い一意性を両立しています。v8 は拡張用の自由形式で、アプリケーション固有のデータ埋め込みなどに利用できる柔軟な仕様になっています。
これらの新しいUUID仕様は、過去の問題点を解消しつつ既存システムとの互換性も維持することを目的としており、UUID v4 の「乱数だけの識別子」から「時間を含む識別子」への進化を示しています。ULIDとUUID v7は生成順に並べても自然に時系列が維持されるため、データベースインデックスの最適化や監査ログの管理など、実務的な利点も大きいです。
その他のユニークID生成方式
UUIDやULID以外にも、分散環境で一意な識別子を生成する仕組みはいくつか存在します。代表的なものとして、Twitter Snowflake、MongoDB ObjectId、Segment KSUID などが挙げられます。
Snowflakeは、Twitterが2010年ごろに開発した64ビット整数のID生成方式です。最上位ビットを符号に使い、41ビットをミリ秒単位のタイムスタンプ、10ビットをマシンID、12ビットをシーケンス番号として構成しています。これにより、分散環境でも重複せず、時刻順に並ぶIDを効率的に生成できます。整数型のままデータベースキーとして扱える点も大きな利点です。
ObjectIdは、MongoDBが採用している96ビット(12バイト)の識別子です。上位4バイトが秒単位の時刻、次の5バイトがマシン+プロセスID、残り3バイトがカウンターとして機能します。生成順に並べるとおおよその時系列を反映するため、挿入順にソート可能です。UUIDよりもコンパクトで、可読性の高い16進数文字列(24桁)で表現されます。
KSUID(K-Sortable Unique ID) は、Segment社によって2017年に設計されたULIDと似た仕組みです。全体は160ビット(20バイト)で、最初の4バイトをUnix時刻(秒単位)、残り16バイトをランダム値として構成します。ULIDとの違いは、時間の精度が秒単位であることと、文字表現がBase62(英数字のみ)である点です。こちらも時系列順にソート可能で、クラウドログやイベントトラッキングなどの用途に適しています。
これらの方式はいずれも「一意性」「時系列順」「分散生成可能性」という点で共通しており、UUIDの進化系といえる存在です。実装の容易さ、データ型(整数型か文字列か)、ソート性などの要件によって最適な方式を選択することが重要になります。たとえば、UUIDのような標準化と互換性を重視するなら UUIDv7、軽量でソート可能な整数IDを求めるなら Snowflake、文字列で可搬性を優先するなら ULID や KSUID などが選択肢となるでしょう。
最後に
これまで見てきたように、UUIDやULID、Snowflakeなど、ユニークな識別子を生成する方式にはそれぞれ異なる設計思想があります。UUIDは標準化と互換性を重視し、ULIDやKSUIDは時間順の整列性を加え、SnowflakeやObjectIdはデータベース効率を追求しました。これらの仕組みはいずれも、分散環境で重複しないIDを安全に生成し、膨大なデータを秩序立てて扱うための工夫の結晶といえます。
一方で、Qiitaの記事IDがどのような方式で生成されているのか、公式の技術アナウンスなどからは確認することができませんでした。Qiita API v2ドキュメントにも item_id の定義はありますが、「文字列で一意に識別されるID」とのみ記され、生成方式には触れられていません。もしも、Qiitaの記事IDの生成方法について定義されたドキュメントがあるようでしたらご教示ください。
具体的な生成方式をあえて公開しない設計は、桁数の増減や使用する文字種の変更などが柔軟に行えるという利点があります。公開仕様に準拠するUUIDとは異なり、独自形式のIDは「不透明だが安定している」こと自体が価値になることもあると考えます。