はじめに
今回は、業務でWebアプリケーションを構築する際や、外部サービスのAPI等に利用されるIDの種類と、それらをどう選ぶかについて考えてみます。
そもそもIDとは
そもそもIDとはなんでしょうか?
日本語だと「識別子」であり、リソースが一意であることを保証するためのラベルです。
Webアプリケーションでは自動で採番された連番のINT型の値を思い浮かべる方も多いと思います。
本来は他の識別子と違うこと以外の意味を持つべきではない値であるという主張から、本記事では「IDは文字列型を採用すべき」という立場から考えてみます。
INT型のIDの自動採番における問題点
INT型のIDの自動採番は、主に以下の問題点があります。
- スケールアウトの阻害
- DBMSにレコードを作成する際に自動採番する場合、オートインクリメントは1台のDBに依存します。
- これは水平分割時に重複していないことを担保するのが難しくなります。
- ホットスポット
- オートインクリメントの場合、最新レコードへ書き込みが集中します。
- B+Treeの末尾にあるノードが過度にロックされる可能性があります。
- リークしやすい連番
- ビジネスロジックとして連番に意味がなくても、数が推測できるため、サービスの規模が露見してしまいます。
- 連番であることに依存したビジネスロジックを実装してしまう可能性があります。
また、設計的なデメリットもあります。
DDDのような責務を分離した設計を行っていくと、DB上にレコードを保存しないとIDを採番できない事による不都合が生じます。
DDDではエンティティが作成されるタイミングでIDを持つ必要があります。しかし、連番は「DBへINSERTされるタイミング」でIDが確定するため、エンティティの生成タイミングとトランザクションの境界がねじれることになります。
文字列型のIDの利点
文字列型のIDの利点は主に以下の点です。
- 一意性の保証がアプリ層で完結する
- 例えば、RFC 9562で定義されているUUID v4/v7ではIDが衝突する確率は天文学的に低く、DBMSに依存しないため、DBが分散したり、DB自体の移行の際にもIDポリシーを維持しやすいです。
- 順序性を担保できる実装が増えた
- INT型を採用する際の大きな利点として順序性が担保できることによるインデックス性能の向上がありました
- UUIDv7やULIDといったIDの生成アルゴリズムでは、「時刻 + 乱数」構成を用いれば、挿入順のままインデックスを構築することができます
- 連番ではないので必要な情報以外を切り離せる
- 連番によるデメリットである規模の推定や攻撃への利用を防ぐ事ができます
- 多言語/他のDBとの互換性
- 多言語化する際に、IDを文字列型にすることで、IDの意味を変更する必要がなく、PostgreSQL/MySQLなど他のDBとの互換性を保ちやすいです。
INT型の方が速い?
多くの場合、DBMS目線ではたしかにINT型が最適な選択肢であるという記述が多く見つかります。
PostgreSQL: Documentation: 17: 8.1. Numeric Types
しかし、業務で作成するアプリケーション目線だと連番を採用することによるメリットとデメリットのバランスを考慮する必要があります。
基本的にサービスを作成、運用、保守するという立場から考えるとDBとしての最高性能にこだわることより、分散可能かつ生成タイミングを限定されないことによる変更容易性のほうが重要だと感じています。
IDの選択による性能差が問題になる規模になればマイクロサービスによる整理や分散DBによる負荷分散などを行うほうが有利です。
ちなみに、2022年時点で下記のIDに関する記事が公開されていますが、数年経った2025年時点でも同じ結論になるかなと思います。
Postgres と MySQL における id, created_at, updated_at に関するベストプラクティス
加えて、2024年の5月にはUUIDの仕様がRFC 9562で正式化されました。
RFC 9562 - Universally Unique IDentifiers (UUIDs) 日本語訳
UUIDの選択肢
UUIDの選択肢は主に以下の2つです。
UUIDv4
UUIDv7
UUIDv4と比べるとページ局所性に優れます。
このページ局所性とは、データベース内部で「連続するレコードが同じディスク/メモリページ上にまとめて格納される性質」のことです。
ページ局所性があることで、下記のようなメリットがあります。
- キャッシュ効率の向上
- 同じページ内に連続レコードがまとまっていると、一度ページを読み込むだけで複数レコードをまとめてキャッシュできます。つまり、メモリ/ディスクのI/O回数が減るので、全体のクエリ応答性が向上します。
- ランダムI/Oの抑制
- ランダムに分散したIDだと、新規挿入時にレコードが書き込まれるページが分散するため、ディスクのシークが増加し、性能が落ちやすくなります。
- インデックスの木構造最適化
- B-Tree系のクラスタ化インデックスでは、キー順にリーフページが配置されるため、ページ局所性を保つことで木構造の高さを抑え、検索性能を向上させることができます。
MySQLで利用されているB−Treeについての解説は下記の記事を参考にしてください。
ページ局所性に関していうと 連番
> UUIDv1,v6,v7
のような時間成分を含むもの > UUIDv4のような完全ランダムのもの
まとめると下記のようになります。
バージョン | 生成方法 | 特徴 | 典型的用途 |
---|---|---|---|
v4 | 暗号学的乱数 122 bit | 衝突確率が低く広くサポート | 分散トランザクション ID など順序不要なケース |
v7 | Unix epoch(ms) + 74 bit 乱数 | 時系列ソート可能・2024 RFC で正式化 | RDB 主キー、ログのキー、メッセージストリーム |
UUID v7のライブラリ対応状況
UUID自体が2024年のRFCで正式化されたため、それまではライブラリの対応がまちまちでしたが、記事執筆時点の2025年では十分利用できるくらいサポートされてきたなという印象があります。
- Laravel
モデルに対するHasUuidsトレイトで利用されるIDがUUIDv7に変更されました。
[12.x] モデルを UUID v7 に切り替える by staudenmeir · プルリクエスト #52433 · laravel/framework
また、ID生成に関して時系列情報が使用されるため、プライバシーの観点からUUIDv7を採用するかどうかの議論があり、最終的にモデルのみUUIDv7を採用する方針を採択し、Str::uuid()はUUIDv4を採用する方針となりました。
- npm
npmパッケージとして利用されるuuidパッケージは、2024年の6月にv7の対応がマージされています。
uuid v7 by pmccarren · プルリクエスト #681 · uuidjs/uuid
これにより、ドメイン駆動的にフロントエンドでエンティティを作成したいケースや、バックエンドでTypeScriptを利用する場合でも、簡単に利用できるようになりました。
その他のID
- NanoID
- 軽量かつ高い衝突耐性を持つID。URLに利用できる文字で構成され、UUIDv4同等くらいの衝突確率を持ちます。ID長がデフォルトだと21文字で、短い文字数なので利用しやすいです。
- ULID
- 時系列情報を先頭に配置し、26文字で構成されるIDです。ハイフン無しの短い文字列で、大文字小文字を区別しない設計になっています。
MySQLを使う場合の選択肢
MySQLの場合はDBMSの特性上主キーの順序性がない場合は著しく性能が悪化します。 そのため、下記の様な選択肢を検討する必要があります。
観点 | INT | UUID v4 | UUID v7 / ULID |
---|---|---|---|
主キーサイズ | 4–8 B | 16 B | 16 B / 26 chars |
インデックス局所性 | ◎(末尾集中) | △(ランダム分散) | ○(半順序) |
生成コスト | DB 依存 (軽) | アプリ層 (暗号 RNG) | アプリ層 (RNG + 時刻) |
スケールアウト適性 | △(シーケンス競合) | ◎ | ◎ |
情報漏えいリスク | × | ◎ | ◎ |
結論
基本的にはUUIDv7を採用する
INT型を採用するケースは下記に当てはまり、かつMySQLを利用する場合くらいにとどまりそう
- スケールする可能性が低い
- 将来的な拡張性より今シンプルであるほうが価値が高い
所感
INT型を利用することが性能的にプラスに働くことは多いですが、分散不可能というデメリットや、設計的なデメリットが大きいため、多くの場合の最適解としては文字列IDを採用する方針をとるべきだと思います。 RFCでの採択があり、UUIDv7の採用をしやすくなりました。 既存システムからの移行は強い動機がないと難しいですが、新規プロダクトの作成時においては、文字列IDを採用するとより柔軟な設計を実現することができると思います。