
2026/10/24 1:39
UUIDv7 は、あなたが思い描いている特性を持っていません。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
UUIDv7 は埋め込まれたタイムスタンプを有する一意の識別子を提案することで前回の標準を大幅に向上させましたが、1970 年代から知られているクロック同期の本質的な限界により、すべての実装で厳密な単調増加性とタイムスタンプ精度を欠いており、これは致命的です。RFC 9562 に従い、カウンタのオーバーフローやシステムクロックのジャンプに対応するために、実装ではタイムスタンプフィールドを恣意的に変更することが許可され、場合によっては要求されます。特に、Python の標準ライブラリの
uuid.uuid7() では、負荷が〜1ID/msec を超える際にクロックが後方にジャンプした場合にタイムスタンプを任意に進める競合が発生し得ます。さらに、ジェネレータは必ずしもタイムスタンプを永続化しないため、クロック調整後のプロセス再起動では順序を乱した ID シーケンスが生じる可能性があります。したがって、開発者は単一の一意性や時間精度の仮定に基づくのではなく、特定のライブラリに対して厳格な監査を行う必要があります。これらのニュアンスを見逃すことで、アカウント作成メタデータを URL 経由で露見させるリスクや分散システムにおけるロジックエラーを招く恐れがあります。UUIDv7 の実装を選択する際には、RFC 準拠性、保証される特性の限界、クロック精度要件、および ID の推測可能性を確認し、時間に基づく順序付けに関する仮定を崩さないようにする必要があります。本文
シリコンの欠片の上に築かれたもの:UUIDv7 の真実と限界
この記事は「UUID」に関する一連の考察の一部です。私は UUIDv7 を**「奇妙な仕様」**と呼びます。多くのブログ記事では UUIDv7 の利点として以下が強調されています。
- モノトニックに増加する ID
- 作成時刻を示す有用なタイムスタンプ
- 推測困難な ID(カレントン)
しかし、これらの特性はすべて厳密には保証されていません。RFC に準拠した UUIDv7 の実装の中には、これらの特性を欠くものさえあります。UUIDv7 が唯一確実に提供できるのは**「一意性」**(衝突回避)のみです。推測可能性については以前議論しましたが、本稿では他の特性について探求します。
タイムスタンプは本当に有用か?
一見すると、ID 内にタイムスタンプを埋め込むことでデータベースの別の列を追跡する必要がなくなり、効率的だと考えられます。特に作成時刻順でのソートが必要な場合、プライマリインデックスから挙動を得られ、追加インデックスの必要性を除くメリットがあります。
しかし、UUIDv7 の ID に埋め込まれたタイムスタンプは、必ずしも実際のオブジェクトの作成時刻と一致しない可能性があります。
仕様の不確実性
PostgreSQL の公式ドキュメントにある
uuid_extract_timestamp 関数に関するヘルプでは、「抽出されたタイムスタンプが UUID が生成された時刻と厳密に等しいとは限らない」と注意されています。RFC 9562 では、実装側が以下の行為を許可しています。
- システムクロックから直接導出されない値を使用する
- タイムスタンプフィールドを変更する
これは、UUID のモノトニック性とタイムスタンプの精度との間の緊張関係を反映しています。特定の状況下では、埋め込まれたタイムスタンプと現在時刻との差は任意に大きくなれます。
Implementations MAY alter the actual timestamp.
— RFC 9562 §6.1この仕様では、クロック値が実際の時刻にどの程度近い必要があるかについて、何ら要求も保証もしていません。
— RFC 9562 §6.1……実装は任意で、現在の時刻よりも前の未来のタイムスタンプを増分することもできます……
— RFC 9562 §6.2
クOUNTER オーバーフローと加速問題
システムクロックよりも前の未来のタイムスタンプを先に増分することは危険です。生成元が非常に高い負荷に晒されており、カウンターフィールドが溢れている場合、タイムスタンプは将来へと加速してしまいかねません。
- 推奨サイズ: RFC ではカウンターの最小サイズとして**「12 ビット以上」**を推奨しています。
- オーバーフローの閾値: カウンターのオーバーフローには、ノードあたり1 ミリ秒に 4,096 個以上の IDを生成する激しいレートが必要です。多くのシステムではこの限界を超えないため、通常は問題ありません。
- 実質的なリスク: タイムスタンプが「止まる」場合、オーバーフローの問題は現実的に重要になります。
Python の uuid.uuid7()
による暴走の例
uuid.uuid7()タイムスタンプを先走らせる代表的な実装として、Python 標準ライブラリの UUIDv7 が挙げられます。この実装では、増分操作が特定の条件(カウンター溢れなど)のみで行われます。
しかし、システムクロックが逆方向に跳ね返った際に生成元が「増分サイクル」に陥り、タイムスタンプがシステム時刻を遥かに超えるように先行して進んでしまう可能性があります。
コード例
以下は、ユーザーが時計を 1 時間遡らせた場合の挙動を示すコードです。
import uuid from datetime import datetime # ジェネレータを現在の時刻で初期化する t = uuid.uuid7().time now = datetime.now() delta = (datetime.fromtimestamp(t/1000) - now).total_seconds() print("Initial delta: ", delta) # ユーザーに時計を 1 時間遡るよう促す input("Set clock back one hour") while True: for _ in range(100000): uuid.uuid7() t = uuid.uuid7().time now = datetime.now() delta = (datetime.fromtimestamp(t/1000) - now).total_seconds() print("New delta: ", delta)
実行結果(Python 3.14)
システムクロックを戻した際、期待される「差分が減少する」挙動ではなく、差分が無限に増加し続ける現象が発生しました。
$ uv run --python 3.14 main.py Initial delta: -0.000748 Set clock back one hour New delta: 3695.277581 New delta: 3795.042411 New delta: 3894.80756 New delta: 3994.573106 ^C
現象の解説
- 期待される挙動: 実装が埋め込みタイムスタンプを過去に戻さないので、差分は 3,600 秒(1 時間)となり、システムクロックが遅延したタイムスタンプに追いつくにつれて減少すべきです。
- 実際の挙動: ID の生成レートが1 ミリ秒あたり 1 個を超えると、差分は無限に増加し続けます。
- 低負荷の場合: システムクロックが遅れたタイムスタンプに追いつくまで時間がかかりますが、暴走効果(runaway behavior)は観察されません。
Python の開発者たちも GitHub イシューで「これは RFC に準拠しており、現状の欠陥のあるシステムに対して改善策を用意できる状況ではない」と述べています。しかし、既存の実装がシステムクロックの逆戻りに対し暴走しないように対応できれば理想的です。実際にはタイムゾーン変更や電源断などにより、時計が数十年もズレたまま放置されるケースもあります。
タイムスタンプを漏洩させることは危険か?
ID 内にタイムスタンプを組み込んでいると、ID を共有する際にタイムスタンプも同時に共有してしまいます。 例えば、ユーザープロフィールページでアカウント作成日を公開しないにもかかわらず、URL に含まれる UUIDv7 から高精度のタイムスタンプが漏洩してしまうケースなどが考えられます。
- 現状の評価: 一般的にセキュリティ監査においてより深刻な問題が見出されるため、この問題は優先度が低いとされています。
- 例外: それでも重要な文脈ではあり得ます。エンジニアリングリソースを割いて対応する必要がある場合があります。
- 注意: UUIDv7 を使用する際は、メタデータを漏洩させるリスクに注意してください。
UUIDv7 はモノトニックですか?
このセクションは細かい議論を含みますが、結論から述べますと:UUIDv7 の ID は厳密な順序保証を提供しません。
仕様上の矛盾
RFC 9562 を読む際に驚くのは、「異なるノード間でもモノトニックに整列された ID を生成する方法」が記述されている点です。1970 年代には不可能と証明されており、これはベクタークロックなどの技術開発につながりました。
しかし、根本的な問題は**「システムクロックを十分な精度で同期できない」**ことにあります。
- 時刻に基づいて順序付けるのは、「正確な時刻を知っている場合のみ有効」です。
- 時刻の不確実性は、イベント順序の不確実性を生み出します。
- 結論: 「時刻が分かっている限り時系列順に並べられるが、時刻が分からない限り時系列順には並ばない」。
類似の ID フォーマットは「ほぼソート済み」と主張しますが、UUIDv7 はより強い主張を行いつつもそれを満たしていない点が奇妙です。少なくともシステム間でのクロック同期の問題は、ミリ秒乃至サブミリ秒単位のタイムスタンプに対する疑念を高めるものです。
単一ノードでも厳密なモノトニック性は保証されない
単一のノード上で単一の実装を用いても、UUIDv7 の ID は厳密にはモノトニックではありません。 最も簡単な順序崩壊の方法は、カウンターを持たないミリ秒単位タイムスタンプを使用する実装を採用することです。単純なループ内で順序を逆にした ID を容易に生成してしまいます。
- UUIDv7 仕様自体が ID を厳密にモノトニック増加させることを保証するものではありません。
- サブミリ秒 タイムスタンプやカウンター、良好に同期されたクロックなどによって優位な結果を出す可能性はありますが、1970 年代からこの問題は完全に解決不可能であることは知られています。
- 有用性: UUIDv7 の利点として、大まかな順序を持つ ID は依然として非常に有用です。したがって、すぐにシステムから排除する必要はありませんが、前提条件には注意が必要です。
UUIDv7 ジェネレータの再起動とクロック逆戻りへの対応
RFC では、時刻が逆戻りする際の取扱いについて議論があります。クロックが順に動く限りタイムスタンプは時系列順になりますが、逆戻りの場合は特別な注意が必要です。
永続化ストレージの欠落
対策の一つとして、「生成された最後のタイムスタンプを追跡し、以前のものを使わないようにする」ことが挙げられます。これを安全に行うには、システム再起動またはプロセス再始動後に読み取れるよう、最後に使用したタイムスタンプを永続化ストレージに書き出す必要があります。
- 現状: 私が確認した複数の UUIDv7 実装のうち、永続化ストレージにタイムスタンプを記録するものは一つも見つかりませんでした。
- 結果: 実装者はこの問題への対処に関心がなく、システムクロックが逆戻りに続いてプロセスが再起動された際、順序を逆にした ID を生成してしまいます。
クロックの未来設定による誤動作
現実世界のクロック同期の問題を見ると、クロックを将来の日付に設定してしまう例が多くあります。こうした場合:
- 逆戻りによってタイムスタンプが実際の時刻と一致します。
- 「最後の使用済みタイムスタンプ」を防ぐガードがあるため、間違ったタイムスタンプを使い続けます。
- システムクロックが追いつくまで、誤った値を維持し続けます。
- その間にカウンターオーバーフロー(繰り上がり)が発生する可能性があります。
UUIDv7 とは何か?結論
私の理解では、UUIDv7 は様々なアイデアを組み合わせたものであり、各実装が自由に合わせて設計できるものです。提供される特性の幅は広く、その確信度には違いがあります。
- 自分で実装を開発する場合は、必要な特性を持つものを作れます。
- 市販版や標準ライブラリ(Python など)を使用する場合、他の誰かの設計決定に委ねられることになります。
- 実装は時代とともに変化し、RFC に準拠しつつも挙動の大規模な変更が行われることがあります。
分散システムではサブシステムごとに異なる特性を持つ複数の実装が存在することがあります。本当に期待できるのは、衝突回避の強力な保証と ID の一意性のみです。
実装を選択する際のチェックリスト
UUIDv7 実装を選ぶ際は、以下の点を必ず確認してください。
- 当該実装はRFC に準拠しているか?
- この実装が追加で保証する特性とは何か?
- その保証が成り立つのはどのような限界の下か?
- これらの特性が成立するために必要な前提条件はあるか(例:正確なシステムクロックが必要か)?
- タイムスタンプを変更させるのはどのような条件下で行われるか?
- 生成された ID の推測難易度はいくらか?
残念ながら、このような分析を行うことは、RFC に準拠したジェネレータを構築するよりもはるかに困難です。UUIDv7 ジェネレータの作成者は、これらの質問に対する答えを知らない場合があり、RFC がそれらの問題を既に解決済みであることを自らも驚いているかもしれません。
もしこの記事を通じて有益だと感じ、著者の執筆活動への支援をご希望の場合は、以下のリンクを通じてお気持ちを伝えることができます。 ※記述内容は個人の意見であり、雇用主の立場や見解を表すものではありません。