UUID は衝突しないのか - 間違った運用と実装で重複を招くパターン

· 更新日: · · UUID, 識別子, 分散システム, データ設計, 実装

更新履歴(8件・最終更新 2026年08月02日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
一意制約の棚卸しSQLで、インデックス側の判定が2通りの誤検知を出していたのを直しました。1つ目は`INCLUDE`列を鍵と数えていた点で、`CREATE UNIQUE INDEX ... ON orders(id) INCLUDE (uuid)`の`indkey`には鍵でない`uuid`も並ぶため、`id`に張った索引が「`uuid`の重複を防いでいる」ことになっていました。鍵は`indkey`の先頭`indnkeyatts`個だけなので、そこに限って照合するようにしました(`indkey`の添字は0始まりです)。2つ目は部分インデックスを全体の保証と数えていた点で、`WHERE`条件の外では同じUUIDが共存できます。`indpred IS NULL`を判定に加えました。あわせて列名を`single_col`から`prevents_dup`へ改め、読み方の説明も足しています。
チェックリスト5番の監査SQLを、UUIDを入れている列を指定して調べる形に直しました。テーブル単位で制約とインデックスを数えると、連番の`id`にある主キーや無関係な列の一意インデックスまで返るため、UUID列が無制約でも「最終防衛線あり」と誤って報告します。対象列を鍵に含むものだけを拾い、その列だけで一意かを`single_col`列で示すようにしました(UUIDを含む複合キーでは単独の重複を防げないためです)。
チェックリスト5番の監査SQLが`pg_constraint`しか見ておらず、`CREATE UNIQUE INDEX`で張った一意インデックスを見落としていました。単独の索引は`pg_index`にしか現れないため、既に一意性があるテーブルでも0行が返り、「最終防衛線が無い」と誤って報告してしまいます。制約と一意インデックスの両方を拾うSQLに差し替え、結果の読み方も直しました。
UUIDv7のカウンタについて、12ビットだから4096件使えると読める書き方をしていたのを直しました。RFC 9562はtickごとにカウンタをランダムな値で初期化することを求めているため、1tick内で使えるのは「4096 − 初期値」です。上位ビットを予約する初期化の方法と、確保できる件数がカウンタ幅ではなく初期化の許容範囲で決まることを明記し、目安の表も直しました。
自作PRNGでの生成とuuid5の誤用について、悪い例と良い例のコードを追加しました。確認コストの低い順に並べた診断フロー図、用語表、発番頻度の目安(rand_aの12ビットで毎ミリ秒4096値)の表を追加し、本文の主張にRFC 9562のセクション番号を併記しました。チェックリストは確認方法と合格ラインの列を持つ表に整理しました。
本文中の関連記事へのリンクの文言が、リンク先の現在のタイトルと食い違っていたのを、実際のタイトルに揃えました。本文の内容は変えていません。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589728)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「UUID は衝突しないのか - 間違った運用と実装で重複を招くパターン」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589728 https://staging.comcomponent.com/blog/2026/03/24/001-uuid-collision-bad-implementation-patterns/

DOI(最新版)
10.5281/zenodo.21589728
DOI(この版)
10.5281/zenodo.21732746

UUID を主キーにしていたのに、ある日 duplicate key が出る。 この瞬間、かなりの確率で「UUID って結局ぶつかるのでは」という話になります。

ただ、実務で起きる UUID 重複の多くは、UUID という規格そのものの問題 というより、規格が前提にしている生成条件を実装や運用で壊している ケースです。RFC 9562 では UUIDv4 は 122 ビットのランダム領域を持ち、UUIDv7 もタイムスタンプ以外の 74 ビットを一意性のための乱数やカウンタに使う前提で定義されています。一方で、UUIDv8 は「実装依存であり、一意性を前提にしてはいけない」と明記されています。123 また、Python 標準ライブラリでも uuid4() は暗号学的に安全な方法で生成されると説明されており、少なくとも「ちゃんとした実装を普通に使う」限り、UUID 側の前提はかなり強いです。4

この記事では、間違った運用や実装で UUID が衝突してしまう典型パターン を、再発防止策とセットで整理します。 内容は 2026 年 3 月時点 で確認できる RFC 9562、Python 公式ドキュメント、PostgreSQL 公式ドキュメントをもとにしています。546

この記事で使う用語

以降の章で、英語のまま出てくる術語を先に一行ずつ訳しておきます。

用語 一行での意味
PRNG pseudo-random number generator、疑似乱数生成器。seed から決まった系列を作るので、同じ seed なら同じ列が再現します
CSPRNG cryptographically secure PRNG、暗号学的に安全な疑似乱数生成器。次の値を予測しにくいことまで設計目標にしたものです
generator state 生成器の状態。乱数の内部状態、clock sequence、カウンタなど、次の UUID を決める材料のことです
carefully seeded counter 慎重に初期化されたカウンタ。UUIDv7 で、同一ミリ秒内の連番を作るために使う領域を指します
clock rollback 時刻の巻き戻り。NTP 補正や手動変更で、システム時刻が前の値へ戻ることです
counter rollover カウンタの桁あふれ。カウンタが最大値を超えて 0 へ戻ることです
monotonicity 単調性。あとから作った UUID のほうが必ず大きい値になる性質です
namespace 名前空間。UUIDv3 / v5 で、名前を解釈する文脈を決めるほうの UUID です
canonicalization 正規化。同じ対象を指す文字列を、大文字小文字や末尾のスラッシュまで含めて 1 つの形にそろえる処理です

1. まず結論

先に短くまとめると、危ないのはこのパターンです。

パターン 何が起きるか まずやるべき対策
固定 seed や弱い PRNG で UUIDv4 っぽい値を自作する 別プロセスや別ノードで同じ系列が再現される OS / ランタイム標準の UUID API を使う
fork、VM snapshot、コンテナ複製後に生成状態をそのまま引き継ぐ 乱数やカウンタの状態が巻き戻り、重複が出る fork 後の再 seed、clone 後の再初期化、永続状態の扱いを見直す
UUIDv3 / v5 を「毎回新しい ID」と誤解して使う 同じ namespace と same name から同じ UUID が再生成される 決定論的 ID だと理解し、用途を限定する
UUIDv1 / v6 / v7 / v8 を自前実装し、clock rollback や node/counter を雑に扱う 高頻度生成や複数ノードで重複しやすくなる 既存ライブラリを使い、独自生成器を減らす
UUID を途中で切り詰めたり、別形式に潰したりする 元の 128 ビットの一意性を自分で捨てる 保存・比較はフル長で行う
DB 側に UNIQUE / PRIMARY KEY を置かない 重複が静かに混入し、原因調査が遅れる ストレージ層で一意制約を持つ

要するに、UUID が衝突した というより、UUID に期待していた一意性を、途中の設計で削っている ことが多いです。

この記事の知識マップ

UUIDの衝突事故の多くは規格そのものではなく、生成や運用が前提を壊すことで起きると整理する記事です。RFC 9562が定めるUUIDv4はCSPRNGによる122ビットの乱数領域を、UUIDv7はカウンタによる単調性の設計を前提としており、固定seedの弱いPRNGでの自作やfork・snapshot後の生成器状態の巻き戻り、UUIDv3/v5を新規採番用途に誤用すること、UUIDv8の実装依存な一意性への過信、128ビットを削る切り詰めがいずれも重複を招きます。RFC自身が真のglobal uniquenessを絶対保証しないとしている以上、DB側のUNIQUE制約が重複挿入を防ぐ最終防衛線になります。

UUIDの衝突を招く実装パターンの知識マップUUIDv4・UUIDv7・UUIDv3/v5・UUIDv8・UUIDv1/v6それぞれがRFC 9562で定義されること、弱いPRNGでの自作・生成器の状態の巻き戻り・name-based UUIDの新規採番への誤用・カウンタの桁あふれ・時刻の巻き戻り・切り詰めがいずれもUUIDの衝突を招くこと、CSPRNGの利用とfork後の再seedとDB側の一意制約がその対策になることを示す図推奨される対応用いるのは非推奨用いるのは非推奨用いるのは非推奨原因になり得る原因になり得る原因になり得る原因になり得る原因になり得る原因になり得る軽減する軽減する防止する利用する両立しない両立しない利用する前提とするで確認できるで確認できるで確認できるで確認できるで確認できる原因になり得るで確認できるUUIDRFC 9562CSPRNGUUIDv4弱いPRNGによるUUID自作新規採番UUIDv3/v5UUIDv8UUIDの衝突生成器の状態カウンタの桁あふれ時刻の巻き戻りUUIDの切り詰めDB側の一意制約fork後の再seedUUIDv7単調性namespace正規化UUIDv1/v6

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全25件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

2. まず疑うべきは「UUID の数学」ではなく「生成と運用」

UUID の話がややこしくなるのは、バージョンごとに性質が違うからです。

  • UUIDv4 はランダムベースです。RFC 9562 では version / variant を除く 122 ビットが乱数で埋まります(Section 5.4)。1
  • UUIDv7 は時系列ソートしやすい構造で、Unix ミリ秒タイムスタンプに加えて、残りを乱数や carefully seeded counter で構成します(Section 5.7)。2
  • UUIDv3 / v5 は name-based です。同じ namespace と同じ canonical name なら、同じ UUID が出るのが正しい挙動です(Section 6.5)。7
  • UUIDv8 は実験用・ベンダー独自用であり、一意性は実装依存です。RFC は「一意性を前提にしてはいけない」としています(Section 5.8)。3

つまり、「UUID を使っている」と言っても、その中身が

  • 標準ライブラリの uuid4() なのか
  • 自前の timestamp + random なのか
  • uuid5(namespace, name) なのか
  • 見た目だけ UUIDv8 の独自フォーマットなのか

で、話がまったく変わります。

UUID 重複が見つかる本当にどこで同じ値になったか生成器が弱い状態が巻き戻ったname-based UUID の誤用保存時に切り詰めたDB 側で一意制約がない実装ミス

実務では、この図の右側から見ていくほうが早いです。

もう少し手順として書くなら、確認コストが低いところから順に潰す のが実際的です。次の順番だと、必要な道具が「SQL を 1 本流す」「コードを grep する」「運用手順を確認する」と段階的に増えていくので、途中で原因が分かればそこで止まれます。

ないある切り詰めている維持している自前実装標準 API引き継いでいる問題なしduplicate key や重複データを検知1. DB に UNIQUE / PRIMARY KEY はあるか(8章)制約なしで静かに混入していた。まず制約を張り、重複の入口を止める2. 保存と比較でフル長 128 ビットを維持しているか(7章)prefix 比較、64 ビットへの潰し込み、カラム長不足による末尾切れを疑う3. 生成は標準 API か(3章・5章)弱い PRNG での自作、またはname-based UUID の採番用途への誤用を疑う4. fork / snapshot / clone 後に生成器の状態を引き継いでいないか(4章)生成器の状態が巻き戻っている残るのは独自の時刻系実装やUUIDv8 の設計そのもの(6章)

3. パターン1: UUIDv4 を名乗りながら、実際は弱い PRNG を使っている

一番ありがちなのはこれです。

  • Math.random() 相当の一般用途 PRNG で 128 ビット分作る
  • 起動時に time() や PID で seed を入れる
  • 「UUID 形式っぽい 32 hex 桁」を自前で組み立てる

見た目は UUID でも、乱数源が弱ければ、同じ系列が別プロセスや別ノードで再現 されます。

コードにすると分かりやすいです。次は Python 3 の標準ライブラリだけで動く、やってはいけないほうの例 です。

# 悪い例: 一般用途の PRNG を seed 固定で回し、UUID 形式の文字列を自前で組み立てる
import random


def make_pseudo_uuid(seed: int) -> str:
    rng = random.Random(seed)          # 同じ seed なら毎回まったく同じ系列になる
    value = rng.getrandbits(128)
    hex_digits = f"{value:032x}"
    return "-".join([
        hex_digits[0:8],
        hex_digits[8:12],
        hex_digits[12:16],
        hex_digits[16:20],
        hex_digits[20:32],
    ])


first = make_pseudo_uuid(12345)
second = make_pseudo_uuid(12345)
print(first == second)   # True: 別プロセスでも別ノードでも、seed が同じなら同じ値が出る

この例には、実は問題が 2 つあります。

  1. random.Random は一般用途の PRNG なので、seed が同じなら系列がそのまま再現します。起動時刻や PID を seed にしても、同時起動や clone で衝突しえます。
  2. version / variant のビットを一切設定していないので、そもそも RFC 9562 の UUIDv4 ではありません。UUID の形をした 128 ビットの数 でしかない、というのが正確なところです。

対して 良いほうの例 は、拍子抜けするほど短くなります。

# 良い例: 標準 API をそのまま使う。seed も版数も自分で触らない
import uuid

new_id = uuid.uuid4()
print(new_id.version)    # 4: version ビットは API 側が正しく埋めてくれる
print(uuid.uuid4() != uuid.uuid4())   # True: 呼ぶたびに別の値になる

RFC 9562 は、UUID の一意性と予測困難性の両方のために、CSPRNG を使うべき としています(Section 6.9 Unguessability)。これは推奨要件(SHOULD)であり、用途によっては例外設計もあり得ますが、一般用途 PRNG で UUID を自作するならその理由を説明できる状態にしておくべきです。さらに、process fork のような状態変化時には CSPRNG 状態を適切に再 seed すべき と書いています。8 Python の uuid.uuid4() も、暗号学的に安全な方法でランダム UUID を生成すると説明しています。4

ここでの実務上の結論は単純です。

  • UUID を自作しない
  • 乱数 seed を手でいじらない
  • 標準ライブラリか、広く使われた実装をそのまま使う

「軽いから」「昔から使っているから」で独自生成器を持ち続けると、あとで最も高くつきます。

4. パターン2: fork、snapshot、clone で生成状態を巻き戻す

二番目に危ないのは、生成器の状態が複製・巻き戻しされる運用 です。

RFC 9562 は、fork 後の再 seed を明示的に勧めていますし(Section 6.9)、stable storage を持たない実装は clock sequence、counter、random data の生成頻度が増え、重複確率が上がる と説明しています(Section 6.3 UUID Generator States)。89

ここから自然に出てくる実務上の推論があります。

  • VM snapshot 取得後に同じイメージを複数復元する
  • コンテナイメージ起動時に同じ初期状態から独自生成器が立ち上がる
  • worker fork 後に PRNG 状態やカウンタ状態を共有してしまう

こうした運用では、UUID の生成系列が意図せず再現 されえます。 これは RFC がそのまま「snapshot は危険」と書いているわけではありませんが、fork 後の再 seed と generator state の扱いに関する注意から導ける、かなり実務的な注意点です。89

対策はこのあたりです。

  • 独自の UUID 生成状態を長く持たない
  • fork / clone / restore の直後に再初期化する
  • 可能なら OS 由来の乱数を毎回利用する実装に寄せる
  • 高頻度生成器なら、状態管理と再 seed の仕様を明文化する

5. パターン3: UUIDv3 / v5 を「毎回新しい ID」と誤解する

UUIDv3 / v5 は collision しにくいランダム ID ではありません。 同じ名前から同じ ID を再生成できる決定論的 ID です。

RFC 9562 では、同じ canonical format の same name を same namespace で生成した UUID は等しくなければならない と書かれています(Section 6.5 Name-Based UUID Generation)。7 なので、こういう使い方をすると、重複は事故ではなく仕様どおりの動きです。

  • uuid5(NAMESPACE_URL, "https://example.com/users/42") を毎回「新規採番」として使う
  • tenant を namespace に入れず、全顧客共通 namespace + email で発番する
  • 同じ論理名を retry ごとに再発番しても、別 ID になると思い込む

これも短いコードで確認できます。Python 3 の標準ライブラリだけで動きます。

# 誤用例: uuid5 を「新規採番」だと思って使う
import uuid

url = "https://example.com/users/42"

first = uuid.uuid5(uuid.NAMESPACE_URL, url)
second = uuid.uuid5(uuid.NAMESPACE_URL, url)
print(first == second)   # True: 何回呼んでも同じ。これは仕様どおりの動き

# しかも、name の正規化がぶれると今度は逆に別 ID になる
with_slash = uuid.uuid5(uuid.NAMESPACE_URL, url + "/")
print(first == with_slash)   # False: 末尾スラッシュ 1 個で別 UUID

# 新規採番がしたいなら、そもそも版が違う
print(uuid.uuid4() != uuid.uuid4())   # True

uuid5 を使うなら、name をどう正規化してから渡すか を先に決めて、その正規化関数を 1 か所に集めるのが安全です。

# 良い例: canonicalization を関数に切り出し、必ずそれを通してから uuid5 に渡す
import uuid

# tenant ごとに namespace を分ける。この値は仕様として固定し、あとから変えない
TENANT_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_DNS, "tenant-a.example.com")


def canonical_user_url(user_id: int) -> str:
    # スキーム、ホスト、末尾スラッシュの有無まで含めて 1 つの形に決める
    return f"https://example.com/users/{user_id}"


def user_uuid(user_id: int) -> uuid.UUID:
    return uuid.uuid5(TENANT_NAMESPACE, canonical_user_url(user_id))


print(user_uuid(42) == user_uuid(42))   # True: 同じ対象なら必ず同じ ID

逆に、name の canonicalization がぶれていると、同じ対象なのに別 UUID になります。RFC も canonical representation の扱いを繰り返し強調しています(Section 5.5、Section 6.5)。710

この系統で大事なのは、

  • UUIDv3 / v5 は「重複しない採番」ではなく「同じ入力なら同じ ID」
  • namespace 設計を曖昧にしない
  • name の canonicalization を仕様化する

の 3 点です。

6. パターン4: 時刻系 UUID や UUIDv8 を自前実装している

UUIDv1 / v6 / v7 / v8 は、見た目だけ真似すると危ない です。

6.1 UUIDv1 / v6 で node や clock sequence を雑に扱う

RFC 9562 では、UUIDv6 は DB locality 改善のために UUIDv1 を並べ替えたもので、clock sequence や node を扱います(Section 5.6)。さらに、分散環境の node collision resistance(Section 6.4)や state 保持(Section 6.3)について、いくつも注意があります。11912

しかも RFC は、仮想マシンやコンテナの登場により、MAC address の一意性はもはや保証されない とまで書いています。5

なので、

  • MAC アドレスだから一意だろうと決め打ちする
  • node ID をイメージ焼き込みで複製する
  • clock sequence を再起動ごとに固定値へ戻す

のような設計は危険です。

6.2 UUIDv7 を自作して counter rollover や clock rollback を放置する

UUIDv7 はかなり実用的ですが、RFC は高頻度生成時の monotonicity と counter handling を丁寧に書いています(Section 6.2 Monotonicity and Counters)。clock rollback や counter rollover で重複を knowingly return してはいけない とも明示されています。213

ということは、

  • 同一ミリ秒内で大量発番するのに counter 設計がない
  • 時刻が戻ったときに何もせず生成を続ける
  • 複数プロセスが同じ internal counter を別々に初期化する

といった実装は危ないです。

どのくらいの発番頻度から気にするべきか

「自分のシステムは該当するのか」を判断するには、ビットの割り当てを見るのが早いです。

RFC 9562 の UUIDv7 は、48 ビットのミリ秒タイムスタンプのあとに rand_a(12 ビット)、さらに variant のあとに rand_b(62 ビット)が続く構造です。2 そして Section 6.2 では、単調性を保つ方法として、rand_a の 12 ビットを専用カウンタにする方法(Method 1)と、rand_b 側を「ランダムに初期化されたカウンタ」として使う方法(Method 2)、rand_a の最大 12 ビットをミリ秒より細かい時刻精度に置き換える方法(Method 3)が示されています。13

ここから、目安はこう読めます。

ここで 12 ビット = 4096 件まで使える、と単純に読まないでください。RFC 9562 は、カウンタを予測されにくくするために tick ごとにランダムな値で初期化することを求めています。13 初期値が 4000 なら、その tick で使えるのは残り 96 件しかありません。1 tick 内で使える件数は「4096 − 初期値」であって、4096 ではありません。

RFC はこの対策として、カウンタの上位ビットを 0 にしておき、初期化するのは下位側だけにする方法を挙げています。13 たとえば最上位 1 ビットを 0 に固定して下位 11 ビットだけ乱数にすれば、初期値は最大でも 2047 なので、どの tick でも 2048 件は必ず確保できます。確保できる件数は、カウンタ幅ではなく初期化の許容範囲で決まります。

これを踏まえると、目安はこうなります。

1 生成器・1 ミリ秒あたりの発番数 どう考えるか
数件〜数十件 上位ビットを予約する初期化にしておけば、1 tick 内で使い切りません
数百件 初期化の仕方によっては、ここで一周します。初期値の上限を決め、rollover 時の挙動(タイムスタンプを進める / 待つ)を仕様として書いておく水準です
1000 件以上 上位ビットを予約しても余裕が乏しくなります。rand_b 側も使う Method 2 や、タイムスタンプを進める設計を前提にしてください

「毎秒数百万件を出さないから rollover は無関係」とは言えません。上限は初期化の設計で決まるので、発番頻度が中程度でも、初期値を 12 ビット全域から取っていれば一周し得ます。rollover したときに何をするかを決めないまま出荷すると、そこで重複が出ます。まず初期化の許容範囲を決め、次に rollover 時の挙動を決める、という順番で設計してください。

ただし、ここで安心して終わらせないほうがよい点が 2 つあります。

  • clock rollback は発番頻度と無関係に起きます。 NTP 補正、VM のサスペンド復帰、手動の時刻変更で時刻は普通に戻ります。毎秒 10 件しか作らないシステムでも対策は要ります。
  • 「1 生成器あたり」なので、プロセス数を掛ける必要があります。 100 プロセスがそれぞれ独立にカウンタを持ち、しかも同じ初期値から始めるなら、1 プロセスあたりの発番数が少なくても系列は重なります。

6.3 UUIDv8 を「新しい UUID 規格」くらいの軽い気持ちで使う

UUIDv8 は便利そうに見えますが、RFC 9562 はかなりはっきりしていて、UUIDv8 の一意性は実装依存であり、前提にしてはいけない としています(Section 5.8)。3

なので、

  • timestamp を埋め込む
  • shard id を埋め込む
  • 何か業務意味を埋め込む
  • 残りは適当に random を入れる

という「自社独自 UUID」は、その設計書が UUID の一意性仕様そのもの です。 レビューなしで入れるには危険すぎます。

7. パターン5: UUID を途中で短くしてしまう

生成までは正しくても、保存や比較の段階で壊してしまうことがあります。

典型例を挙げます。

  • 先頭 8 文字だけを外部キー代わりに使う
  • 128 ビット UUID を 64 ビット整数へ潰す
  • 文字列カラム長が足りず、末尾が切れる
  • ログや画面表示の短縮表現を、そのまま一意キー扱いする

ここで大事なのは、表現を変えること自体が悪いわけではない ことです。

  • ハイフンを外す
  • 小文字 / 大文字をそろえる
  • バイナリ 16 バイトで持つ

のように、128 ビットを落とさない変換 は問題ありません。 危ないのは、一意性の材料そのものを削る変換 です。

特に「人が見やすい短縮 ID」を別途作ったのに、それがいつのまにか本来の UUID より優先される設計は事故になりやすいです。

8. パターン6: DB 側に一意制約がない

そして、これが特に重要です。

UUID が十分衝突しにくいとしても、本当に重複を許容できないなら、保存先でも一意制約を持つべき です。

PostgreSQL の公式ドキュメントでは、unique constraint は列や列集合の値が表全体で一意であることを保証し、primary key は unique かつ not null な行識別子になると説明されています。6

RFC 9562 も、UUID は実装上十分な一意性を提供できる一方で、真の global uniqueness を絶対保証することはできない としています。さらに collision impact が高い用途では、より強い対策を取るべきとしています(Section 6.7 Collision Resistance、Section 6.8 Global and Local Uniqueness)。14

実務では、この組み合わせが基本になります。

  • UUID は衝突しにくい ID として使う
  • DB は UNIQUE / PRIMARY KEY で最終防衛線を持つ
  • 重複時の retry / idempotency / incident logging を設計する

UUID を使うことと、一意制約を置かないことは、同義ではありません。

9. 実務向けチェックリスト

最後に、導入や監査でそのまま使いやすい形にまとめます。 1 章の結論表が「何が危ないか」の一覧だったのに対して、こちらは どうやって自分のシステムを確認するか の一覧です。

# 確認すること 確認方法 合格ライン
1 UUID を自前生成していないか リポジトリ全体で getrandbitsMath.randomnew Random(%032x のような自前組み立ての痕跡を grep する UUID 生成は uuid4() / uuid7() などの標準 API 経由だけになっている
2 UUID の version を仕様として決めているか 保存済みデータから version を数える。PostgreSQL なら substring(id::text from 15 for 1) が version の桁 使ってよい version が文書化され、実データもそれと一致している
3 seed と generator state の扱いを棚卸ししたか 起動スクリプト、Dockerfile、snapshot / clone 手順書に「再初期化」の記述があるか読む。worker を fork する箇所を grep する fork、worker 再起動、snapshot、clone の直後に生成器を作り直す手順が明記されている
4 保存時にフル長を維持しているか カラム定義を確認する。加えてコード側で [:8]substring(Left(ToString("N").Substring のような切り詰めを grep する 保存も比較も 128 ビットのまま。短縮表現は表示専用に限定されている
5 DB に UNIQUE / PRIMARY KEY があるか 下の SQL で、UUID を入れている列名を指定して、制約と一意インデックスの両方を一覧する(テーブル単位で数えると、連番の主キーを数えてしまう) その列について single_coltrue の行が1つ以上ある
6 重複を観測できるか duplicate key 相当の例外を握りつぶしている箇所を grep する。ログに generator / node / deployment が出るか実物を見る 重複時に例外が記録され、どこで出たか追える

5 番の確認は、PostgreSQL なら次の 1 本で済みます。

-- public.orders の uuid 列に一意性が張られているかを一覧する。
-- テーブル名と列名は対象に合わせて置き換える
WITH target AS (
    SELECT attrelid, attnum
    FROM   pg_attribute
    WHERE  attrelid = 'public.orders'::regclass
      AND  attname  = 'uuid'                    -- ← 調べたい列
      AND  NOT attisdropped
)
SELECT c.conname                     AS name,
       c.contype::text               AS kind,      -- p = 主キー / u = 一意制約
       array_length(c.conkey, 1) = 1 AS prevents_dup, -- その列だけで重複を防げるか
       pg_get_constraintdef(c.oid)   AS definition
FROM   pg_constraint AS c JOIN target AS t ON c.conrelid = t.attrelid
WHERE  c.contype IN ('p', 'u')
  AND  t.attnum = ANY (c.conkey)                -- その列を鍵に含むものだけ
UNION ALL
SELECT i.relname                     AS name,
       'i'                           AS kind,      -- i = 制約を伴わない一意インデックス
       -- 部分インデックス(indpred が非NULL)は条件に合う行の中でしか一意を保証しない
       x.indnkeyatts = 1 AND x.indpred IS NULL AS prevents_dup,
       pg_get_indexdef(x.indexrelid) AS definition
FROM   pg_index AS x
JOIN   pg_class AS i ON i.oid = x.indexrelid
JOIN   target AS t ON x.indrelid = t.attrelid
WHERE  x.indisunique
  AND  EXISTS (                                 -- INCLUDE 列は鍵ではないので数えない。
         SELECT 1                               -- indkey の先頭 indnkeyatts 個だけを見る
         FROM   generate_series(0, x.indnkeyatts - 1) AS k(i)  -- indkey は 0 始まり
         WHERE  x.indkey[k.i] = t.attnum)
  AND  NOT EXISTS (                             -- 制約に紐づく索引は上で出している
         SELECT 1 FROM pg_constraint AS c2 WHERE c2.conindid = x.indexrelid);

読み方は2段階です。

  • kind は主キーが p、一意制約が uCREATE UNIQUE INDEX で張った制約を伴わない一意インデックスが i です。15
  • prevents_duptrue の行が1つも無ければ、その列の重複は防げていません。

「テーブルに一意制約があるか」で判定しないでください。主キーが連番の id にあるだけで UUID 列は無制約、という設計はごく普通にあります。テーブル単位で数えると、その状態を「最終防衛線あり」と誤って報告します。同じ理由で、UUID を含む複合キーがあっても UUID 単独の重複は通るので、prevents_dup を見る必要があります。

もう1つ、pg_constraint だけを見ないでください。一意性を CREATE UNIQUE INDEX で入れている設計も普通にあり、そちらは pg_index にしか現れません。制約だけ数えて「最終防衛線がない」と報告すると、既にある索引を見落として不要なスキーマ変更へ話が進みます。上の1本なら、どちらの形でも拾えます(indnkeyatts は PostgreSQL 11 以降です)。

インデックス側の判定は、単純に「indkey にその列が入っているか」で書くと2通りの誤検知を出します。どちらも「防げている」と誤って報告する側に倒れるので、そのまま合格印を押すことになります。

  • INCLUDE 列を鍵と数えてしまう。CREATE UNIQUE INDEX ... ON orders(id) INCLUDE (uuid)indkey には、鍵ではない uuid も並びます。15 一方 indnkeyatts は鍵の本数(この例では1)なので、indnkeyatts = 1uuid を含むことが同時に成立し、id に張った索引が「uuid の重複を防いでいる」ことになります。鍵は indkey先頭 indnkeyattsだけなので、そこに限って照合します(indkey の添字は 0 始まりです15)
  • 部分インデックスを全体の保証と数えてしまう。CREATE UNIQUE INDEX ... ON orders(uuid) WHERE active は、条件に合う行の中でしか一意を保証しません。active でない行どうし、あるいは索引に載る行と載らない行のあいだで、同じ UUID が普通に共存できます。indpred が非NULLならそれが部分インデックスなので、15 全体の重複防止としては数えません(索引そのものは一覧に残るので、definitionWHERE 句を見て条件付きの防衛線として扱えます)

10. まとめ

UUID の衝突事故は、たいてい UUID が弱い のではなく、UUID の前提を実装や運用で壊している ところから始まります。

  • 弱い乱数で自作する
  • fork や snapshot 後の状態を巻き戻す
  • name-based UUID を採番用途に使う
  • v7 や v8 を軽く自前実装する
  • 途中で短縮して一意性を捨てる
  • DB 側の一意制約を外す

このあたりをやってしまうと、こちらから衝突しやすい状況を作りに行っている のとほとんど変わりません。

重複を見つけたら、まず疑うべきは UUID の数学より、生成器、状態管理、保存形式、制約設計 です。 その順で見れば、たいてい原因はかなり絞れます。

11. 関連記事

12. 参考資料

  1. IETF RFC 9562, Section 5.4 UUID Version 4. UUIDv4 の 122 ビット乱数領域について。  2

  2. IETF RFC 9562, Section 5.7 UUID Version 7. UUIDv7 の timestamp、random bits、counter の考え方について。  2 3 4

  3. IETF RFC 9562, Section 5.8 UUID Version 8. UUIDv8 の一意性は実装依存で、前提にしてはいけないことについて。  2 3

  4. Python 3.14 documentation, uuid module. uuid4() の cryptographically-secure generation、uuid5() の deterministic behavior、uuid7() / uuid8() の性質について。  2 3

  5. IETF RFC 9562, Universally Unique IDentifiers (UUIDs). UUID の形式、各 version、best practices 全体の基準文書です。  2

  6. PostgreSQL documentation, Constraints. UNIQUE 制約と PRIMARY KEY による一意性担保について。  2

  7. IETF RFC 9562, Section 6.5 Name-Based UUID Generation. same namespace + same name が同じ UUID になること、canonicalization の重要性について。  2 3

  8. IETF RFC 9562, Section 6.9 Unguessability. CSPRNG 利用と fork 後の再 seed について。  2 3

  9. IETF RFC 9562, Section 6.3 UUID Generator States. stable storage や generator state の扱いについて。  2 3

  10. IETF RFC 9562, Section 5.5 UUID Version 5. namespace + canonical name に基づく name-based UUID の仕様について。 

  11. IETF RFC 9562, Section 5.6 UUID Version 6. UUIDv6 の node / clock sequence / DB locality について。 

  12. IETF RFC 9562, Section 6.4 Distributed UUID Generation. 分散環境での node collision resistance について。 

  13. IETF RFC 9562, Section 6.2 Monotonicity and Counters. clock rollback、counter rollover、batch generation 時の注意について。  2 3 4

  14. IETF RFC 9562, Sections 6.7 and 6.8. collision resistance と global uniqueness の考え方について。 

  15. PostgreSQL documentation, pg_constraint. contypep は primary key、u は unique constraint を表すこと、conrelid が制約の対象テーブルを指すこと、conindid が制約を支える索引を指すことについて。制約を伴わない一意インデックスは pg_index 側にのみ現れる(indrelid が対象テーブル、indisunique が一意かどうか)。  2 3 4

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

UUIDは衝突しないのですか?
普通に使えば十分衝突しにくいです。RFC 9562 では UUIDv4 は 122 ビットのランダム領域を持ち、UUIDv7 もタイムスタンプ以外の 74 ビットを一意性のための乱数やカウンタに使う前提で定義されています。Python の uuid4() のように、ちゃんとした実装を普通に使う限り前提はかなり強いです。ただし RFC 9562 自身が、UUID は真の global uniqueness を絶対保証することはできないとしており、衝突の影響が大きい用途ではより強い対策を取るべきとしています。だからこそ DB 側の一意制約が最終防衛線になります。
UUIDの重複はなぜ起きるのですか?
実務で起きる UUID 重複の多くは、規格そのものの問題ではなく、規格が前提にしている生成条件を実装や運用で壊しているケースです。典型パターンは、固定 seed や弱い PRNG で UUID を自作する、fork・VM snapshot・コンテナ複製後に生成器の状態を巻き戻す、UUIDv3 / v5 を「毎回新しい ID」と誤解して使う、時刻系 UUID や UUIDv8 を自前実装して clock rollback や counter を雑に扱う、UUID を途中で切り詰めて一意性を捨てる、DB に一意制約を置かず重複が静かに混入する、の 6 つです。
UUIDv3やUUIDv5を使うと重複するのですか?
UUIDv3 / v5 は衝突しにくいランダム ID ではなく、同じ名前から同じ ID を再生成できる決定論的 ID です。RFC 9562 では、同じ namespace と同じ canonical name から生成した UUID は等しくなければならないと定められているため、同じ入力から同じ UUID が出るのは事故ではなく仕様どおりの動きです。したがって新規採番用途に使うのは誤用です。逆に name の canonicalization がぶれると、同じ対象なのに別 UUID になります。namespace 設計と name の正規化ルールを仕様として明文化することが重要です。
UUIDの重複を防ぐには何をすればよいですか?
まず UUID を自前生成せず、uuid4() / uuid7() のような標準 API か広く使われた実装に寄せます。使う UUID の version を仕様として決め、fork・worker 再起動・snapshot・clone 後に生成器の状態を引き継がないようにします。保存や比較はフル長の 128 ビットを維持し、prefix 比較や短縮表示を本来のキーとして使わないようにします。そのうえで、本当に重複を許容できないなら DB に UNIQUE / PRIMARY KEY 制約を置き、duplicate key を握りつぶさずにどの generator や node で出たか追えるようにしておきます。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る