UUIDv7は「速いUUID」ではない|作成時刻を値に埋め込む代わりに得るものと、v4との使い分け
主キーに連番を使うか、UUIDを使うか。UUIDを使うなら、ランダムなv4か、時刻順のv7か。この選択は、よく「v7のほうがインデックスに優しい」という理由で語られます。
それ自体は間違いではありません。ただ、v7がインデックスに優しい理由は、値の先頭に作成時刻が入っているからです。つまりv4からv7への移行は、性能の判断であると同時に、作成時刻を識別子の表面に出すという判断でもあります。
この記事では、UUIDの仕様であるRFC 9562と、PostgreSQL 18のドキュメントを材料に、v7が得るものと差し出すもの、そのリスクの大きさを整理します。
30秒で要点
- UUIDv7は、上位48ビットにミリ秒単位のUnixタイムスタンプを置くUUID
- 時刻順なので、新しい値がインデックス上で近くに並び、ランダムなv4より局所性が高い(RFC 9562)
- その代わり、値からおおよその作成時刻が読み取れる。PostgreSQL 18には取り出す関数もある
- RFC 9562は、埋め込まれた時刻が生む攻撃面を「ごく小さい」と評価したうえで、セキュリティ上の用途にはv4を使うべきとしている
- どちらのバージョンでも、UUIDを「推測されない秘密の値」として使ってはならない
| 用語 | 意味 |
|---|---|
| UUID | データを識別するための値の形式の1つ。仕様はRFC 9562で定められ、いくつかのバージョン(v4、v7など)がある |
| 主キー | データベースの表で、1行を一意に特定するための列 |
| インデックス | データベースが検索を速くするために、値を並べて持っておく仕組み |
| 局所性 | 近いタイミングで扱う値が、保存場所でも近くにまとまっている度合い |
| Unixタイムスタンプ | 1970年1月1日0時(UTC)からの経過時間で時刻を表す方式 |
| ビット | データの最小単位。0か1のどちらかを表す |
RFC 9562とUUIDv7
RFC 9562「Universally Unique IDentifiers (UUIDs)」は、2024年5月に発行されたStandards Trackの文書で、それまでのUUIDの仕様だったRFC 4122を廃止(置き換え)しています。
このRFCのSection 5.7によれば、UUIDv7は、広く実装されているUnixエポック(1970年1月1日0時UTCからのミリ秒数、うるう秒を除く)に由来する、時刻順の値のフィールドを持ちます。具体的には、上位48ビットにミリ秒単位のUnixタイムスタンプを置き、バージョンとバリアント(UUIDの種類を示す固定のビット)を除く残りの74ビットを乱数で埋めます。なお、この74ビットの埋め方には別のやり方も認められているので、「必ず乱数」とは限りません。
v7がインデックスで有利になる理由
RFCは、v4のように時刻順でないUUIDは、データベースのインデックスの局所性が低いと説明しています。連続して作った値がインデックス上で近くにならないので、挿入がランダムな位置で行われることになるからです。
一方、時刻順で単調に増えるUUIDは、新しい値がインデックス上で互いに近くに並ぶので、局所性が高くなります。
ここまでが、「v7は速い」と言われるときの根拠です。ただし、実際にどれくらい差が出るかは環境によって変わりえます。本記事では性能の数値は扱いません。
その代わりに、値が作成時刻を語る
見落とされやすいのはここからです。v7がインデックスで有利なのは、値の先頭が作成時刻だからでした。ということは、値を見れば作成時刻がおおよそ分かります。
これは理屈の上だけの話ではありません。PostgreSQL 18のドキュメントには、次の組み込み関数が載っています。
uuidv4():version 4(ランダム)のUUIDを作るuuidv7([shift interval]):version 7(時刻順)のUUIDを作る。タイムスタンプは、ミリ秒精度のUNIXタイムスタンプ+ミリ秒未満のタイムスタンプ+乱数で計算され、任意のshift引数で計算上のタイムスタンプをずらせるuuid_extract_timestamp(uuid):version 1または7のUUIDからタイムスタンプを取り出す。ほかのバージョンではnullを返す
つまり、データベースの組み込み関数として「UUIDから時刻を読む」手段が用意されているわけです。
ただし同じドキュメントは、取り出した時刻は生成した実装によって決まり、UUIDを生成した時刻と必ずしも完全には一致しないと注記しています。shift引数でずらすこともできます。したがって「値から生成時刻が正確に復元できる」とまでは言えません。言えるのは、多くの場合、おおよその作成時刻が値から読み取れるということです。
そのリスクは、どのくらいの大きさか
作成時刻が読めると聞くと、危険に感じるかもしれません。ここでRFC自身の評価を見ておきます。
RFC 9562のSecurity Considerations(Section 8)は、UUIDに埋め込まれたタイムスタンプは「ごく小さな攻撃面」を生むと述べています。タイムスタンプ(と埋め込まれたカウンタ)は、そのUUIDと対応するデータの作成順序を示すが、データそのものやアプリケーション全体については何も定めない、という説明です。そのうえで、アプリケーション内で何らかの形のセキュリティ上の操作にUUIDが必要な場合は、UUIDv4を使うべき(SHOULD)としています。
同じ節には、バージョンを問わない注意もあります。
- UUIDが推測困難だと仮定すべきではない(SHOULD NOT)
- たとえば、持っているだけでアクセスが許される識別子として使ってはならない(MUST NOT)
また、Section 6.12は、UUIDを不必要に解析せず、できる限り中身を見ない値として扱うことを勧めています。アプリケーションの事情でタイムスタンプを調べる必要が出うることにも触れつつ、それを避けるよう助言しています。
まとめると、RFCの立場は「時刻が読めるリスクは小さい。ただしセキュリティに関わる用途ならv4を。そして、どのバージョンでも秘密の値としては使わない」です。本記事もこの評価を超えてリスクを大きく見積もることはしません。
なぜ「v7は速いv4」と受け取ってしまうのか
v7が「速いUUID」として受け取られやすい理由は、2つあると考えています。
1つ目に、UUIDはランダムなもの、という印象が先にあるから。UUIDと聞いてランダムなv4を思い浮かべる人が多いと考えられ、「UUID=中身に意味のない値」という前提で、v7も同じ種類のものの改良版として受け取りやすくなります。実際にはv7は、中身に時刻という意味を持つ値です。
2つ目に、移行の話がデータベースの中で完結するから。v7への移行は、インデックスの効率というデータベース内部の話として持ち込まれることが多く、その値がURLや画面、外部とのやり取りに出ていくかどうかは、同じ議論の中で確認されにくくなります。
ここから先は仮説です。たとえば会員IDや注文IDがv7でURLに出ている場合、外部の人が値から「このアカウントがいつ頃作られたか」を読める可能性があります。それが問題になるかどうかはサービスによって異なり、RFCもそこまでは述べていません。ただ、問題になるかどうかを判断する機会そのものが、「速いから」という理由での移行では飛ばされやすい、と筆者は考えています。
確かなことと、まだ確かではないこと
一次資料で確認したこと:
- UUIDv7は上位48ビットにミリ秒単位のUnixタイムスタンプを持つ(RFC 9562)
- 時刻順のUUIDはインデックスの局所性が高い(RFC 9562)
- 埋め込まれた時刻の攻撃面は「ごく小さい」が、セキュリティ上の用途にはv4を使うべき(RFC 9562)
- PostgreSQL 18には、v7を作る関数と、v1・v7から時刻を取り出す関数がある。取り出した時刻は生成時刻と完全に一致するとは限らない(PostgreSQL 18ドキュメント)
本記事では扱わないこと:
- v4からv7に変えたときの性能差の数値
- PostgreSQL 18以外のデータベースや、各言語のライブラリの対応状況
v4とv7を使い分けるための確認手順
UUIDを使っている表、またはこれからUUIDを使う表を1つ選び、次の順で確かめてください。
- その値は、データベースの外に出るか。URL、画面、外部サービスとの連携、ログの共有先などに出るかを書き出します。外に出ないなら、作成時刻が読めることの影響はほぼ社内に限られます
- 作成時刻が外から読めて困ることはあるか。1で外に出る場合、その値を見た人に「いつ作られたか」が分かって困るかを、サービスの事情で判断します
- その値を、セキュリティ上の用途に使っていないか。アクセス権の判定やパスワードの再設定用のリンクのように、「値を知っていること」を何かの根拠にしていないかを確かめます。使っているなら、RFCの記述に沿えばv7は避け、そもそもUUIDを秘密の値として使わない設計に直す必要があります
PostgreSQL 18を使っている場合は、既存の値がどのバージョンかを次のように確かめられます。
-- 既存のUUID列から数件取り出し、時刻を読めるか確かめる
SELECT id, uuid_extract_timestamp(id) FROM your_table LIMIT 5;
your_tableは自社の表の名前に置き換えます。結果がnullならv1・v7以外のバージョンで、時刻が返ってくるなら、その値はすでに作成時刻を表に出しています。この1行の結果を見てから、1〜3の確認に進むと判断がしやすくなります。
データベースの基本的な仕組みはデータベース入門やデータベースとは?超初心者向け完全ガイドで、UUIDを主キーにする実装の例はPrismaとSupabaseを使ったバックエンド構築の基礎で扱っています。
この記事で扱ったような判断を、自社の状況に照らして整理したい場合は、無料診断ツールで状況を言語化するところから始めることができます。
状況を整理する(15分)