UUIDとは、中央の採番サーバを置かずに、どの環境で作っても衝突しないことを狙って設計された128ビットの識別子です。文字列としては 8-4-4-4-12 に区切った36文字で表され、現行の仕様は2024年5月発行のRFC 9562になります。この記事で扱うのは、128ビットの内訳とversion・variantビットの読み取り位置、v1からv8までの生成方式の違い、Python 3.14とPostgreSQL 18で実際に生成して確かめる手順、衝突確率を自分で計算する方法、主キーへ採用するかどうかの線引きです。v7の内部構造とデータベース性能の実測はUUIDv7とUUIDv4の違いを扱った記事に譲り、ここではUUID全体の地図と判断基準を置きます。
まとめ|UUID採用の可否とv4かv7かの選択に対する実務上の結論
結論から置きます。新規に識別子を設計するなら、外部へIDを出す・複数のデータベースへ書き込む・オフライン端末で採番する、のいずれかに当てはまる時だけUUIDを選んでください。単一のデータベースで完結し、IDを画面やURLに出さない社内システムなら、8バイトの連番で足ります。UUIDは16バイトを消費し、索引も太る選択です。
採用すると決めた後のバージョンは2択です。時刻の順序が要らないならv4、主キーに置いて挿入位置を末尾へ寄せたいならv7。v1とv2は新規採用しません。v1はMACアドレスを埋め込む設計で、機器情報が識別子に残ります。v3とv5は目的がまったく別で、同じ入力から常に同じUUIDを作りたい場面の道具です。
UUIDは秘密の値ではありません。RFC 9562は、実装がUUIDを推測困難だと仮定してはならないと明記しています。認証トークンや、推測されると困るURLに流用する設計は採りません。
RFC 9562が定める128ビット構造とバージョン判別ビットの読み取り手順
UUIDの中身は、バージョンごとにレイアウトが違います。ただし全バージョンで共通の座標が2つあり、そこを読めば手元の値の素性が分かる仕組みです。
8-4-4-4-12の36文字表記と128ビットへの対応関係の内訳
16進数1文字は4ビットを表します。UUIDは128ビットなので16進数では32文字になり、そこへハイフンを4本入れて 8-4-4-4-12 に区切った36文字が標準の表記です。a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 のような形になります。ハイフンは人が読むための区切りで、値そのものには含まれません。
PostgreSQLの公式ドキュメントは、この標準形に加えて、大文字表記・中括弧で囲んだ形・ハイフンを省いた形も入力として受け付け、出力は常に標準形へ揃えると記載しています。uuid型のリファレンスにある通り、格納される実体は128ビット=16バイトです。
6オクテット目上位4ビットに置かれるversionフィールドの位置
RFC 9562の §4 は、バージョン番号の位置を「octet 6 の上位4ビット、UUIDのビット48から51」と定義しています。文字列表記では、3番目のグループの先頭1文字がそこにあたります。冒頭に挙げた a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 なら、3グループ目 4ef8 の先頭が 4 なのでv4です。
この1文字を見るだけで、受け取った識別子がランダム生成なのか時刻ベースなのかを判定できます。ログに残ったUUIDから生成方式を逆算したい時、外部システムから渡された値の素性を確かめたい時に使う読み方です。
variantが10xxで固定される理由とNil UUID・Max UUIDの扱い
もう1つの共通座標がvariantです。RFC 9562 §4 は、variantフィールドを octet 8 の上位ビットに置き、UUIDのビット64と65をそれぞれ 1 と 0 に設定しなければならないと規定しています。上位2ビットが 10 であることが、この仕様に従うUUIDである印。文字列では4グループ目の先頭1文字が 8・9・a・b のいずれかになります。
variantが分かれているのは、Microsoftの旧GUID実装やNCS互換の値と区別するためです。variantが違えばversionフィールドの解釈も成り立たないため、バージョン判別より先にvariantを見る順序になります。仕様には特別な値も2つあり、128ビットすべてが0の Nil UUID と、すべてが1の Max UUID が §5.9・§5.10 に定義されています。前者を「値が無い」の番兵に使う設計は可能ですが、NOT NULL制約を張った列へ未設定の意味で入れると、後からNULLとの区別がつきません。
UUIDバージョン1から8までの生成方式と用途別に見た適性の比較表
RFC 9562 の §5 は8つのバージョンを定義しています。数字が大きいほど新しく優れている、という並びではありません。決定論的に作るv3とv5は、目的そのものが別です。
| 版 | 生成方式 | 時刻順の並び | 新規採用の適性 |
|---|---|---|---|
| v1 | 時刻+MACアドレス | 並ばない | 採用しない |
| v2 | DCE Security | 並ばない | 採用しない |
| v3 | MD5の名前ベース | なし | 決定論的な採番のみ |
| v4 | 乱数122ビット | なし | 汎用の一意ID |
| v5 | SHA-1の名前ベース | なし | 決定論的な採番のみ |
| v6 | v1の時刻順並べ替え | 並ぶ | v1からの移行時のみ |
| v7 | Unix時刻48ビット+乱数 | 並ぶ | データベース主キー |
| v8 | 実装が独自に定義 | 実装依存 | 実験・独自要件 |
v1のMACアドレス埋め込みとv6による時刻順並び替えの設計差
v1は、グレゴリオ暦の時刻とノードIDを組み合わせて作ります。ノードIDにはネットワークインターフェースのMACアドレスを使うのが本来の設計で、Pythonの uuid.getnode() も公式ドキュメントの記載どおりハードウェアアドレスを48ビット整数として返します。取得に失敗した時だけ、第1オクテットの最下位ビットを1にしたランダム値へ切り替わる仕様です。
問題は、識別子から機器が特定できてしまう点にあります。RFC 9562 もMACアドレスの利用にプライバシー上のリスクが内在すると述べています。公開APIのレスポンスにv1を載せれば、サーバの台数構成が外から推定できる状態です。v6はv1の時刻フィールドを並べ替えて辞書順と時刻順を一致させた版で、既存のv1資産を持つシステムの移行先にあたります。
v3とv5の名前ベース生成が持つ決定論的な識別子としての用途
v3とv5は、名前空間UUIDと名前文字列をハッシュして作ります。v3がMD5、v5がSHA-1です。同じ名前空間と同じ名前を与えれば、いつどの言語で実行しても必ず同じUUIDが返ります。この性質が用途を決めています。
使いどころは、外部システムのキーを自システムのUUIDへ写像する場面です。取引先コードや商品コードのような自然キーから決定論的にUUIDを作れば、同期処理を何度流しても同じ行に着地します。採番テーブルを持たずに冪等な取り込みを組めるため、冪等性を担保する実装と相性の良い組み合わせになります。ただしハッシュ元が推測できる値だとUUIDも復元できるため、秘匿したい情報を名前に入れてはいけません。
PythonとPostgreSQLで実行して確かめるUUID生成と検証の具体手順
仕様を読んだら、手元で生成して確かめます。ここ2年で標準ライブラリとデータベース本体の対応が進み、外部パッケージなしでv7まで作れる環境が揃いました。
Python 3.14で追加されたuuid6・uuid7・uuid8の実行例
Python公式ドキュメントは uuid6()・uuid7()・uuid8() のいずれにも「バージョン 3.14 で追加」と付記しています。3.13以前は uuid1・uuid3・uuid4・uuid5 の4つだけで、v7が要る場合は外部パッケージを入れる必要がありました。UUID.version 属性も3.14でバージョン6・7・8に対応しています。
# Python 3.14 以降。uuid6 / uuid7 / uuid8 が標準ライブラリへ入った
import uuid
u4 = uuid.uuid4() # ランダム122ビット
u7 = uuid.uuid7() # Unix時刻48ビット + 乱数
u5 = uuid.uuid5(uuid.NAMESPACE_DNS, "issoh.co.jp") # 名前ベース・決定論的
print(u4, u4.version, u4.variant)
print(u7, u7.version)
print(u5) # 同じ引数なら何度実行しても同じ値が返る
print(uuid.NIL) # 00000000-0000-0000-0000-000000000000
手元のPython 3.14.6で実行すると、u4.variant は specified in RFC 4122 という文字列を返します。RFC 4122 はRFC 9562に置き換えられていますが、後方互換のため定数名と表示はそのまま維持されている、と公式ドキュメントに明記されています。
PostgreSQL 18のuuidv7と抽出関数で確認するバージョン判定
データベース側も同じ流れです。PostgreSQL 18のUUID関数リファレンスには uuidv4() と uuidv7([shift interval]) が並んでいます。17系のリファレンスを照合すると、そこには gen_random_uuid・uuid_extract_timestamp・uuid_extract_version の3つしかなく、uuidv7() は載っていません。ネイティブのv7生成は18で入った機能です。
-- PostgreSQL 18 以降
SELECT uuidv4();
SELECT uuidv7();
-- 受け取った値の素性を関数で確かめる
SELECT uuid_extract_version('019535d9-3df7-79fb-b466-fa907fa17f9e'::uuid); -- 7 が返る
SELECT uuid_extract_timestamp('019535d9-3df7-79fb-b466-fa907fa17f9e'::uuid); -- 生成時刻
-- 主キーの既定値として置く
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT uuidv7(),
ordered_at timestamptz NOT NULL DEFAULT now()
);
uuid_extract_timestamp() が時刻を返すのはv1とv7だけで、他のバージョンにはnullが返ります。同リファレンスは、取り出した時刻が生成した瞬間と厳密に一致するとは限らず、生成した実装に依存する、とも注意書きを添えています。監査で厳密な時刻が要る列には、この抽出値ではなく専用のタイムスタンプ列を置いてください。
生成したUUIDをアプリ側とDB側のどちらで採番するかの判断分岐
生成関数が両側に揃うと、どちらで採番するかが設計判断になります。分岐の軸は「INSERTより前にIDが要るか」の一点です。要るならアプリ側で作ります。複数テーブルへ親子関係を持つ行をまとめて書き込む時、親のIDが確定していないと子を組み立てられません。要らないならデータベース側の DEFAULT uuidv7() に任せ、生成漏れを構造的に防ぎます。両方に置くと、経路によってバージョンが混在する事故のもとです。どちらかに決めて、コードレビューの観点に加えてください。
UUIDの衝突確率の実数計算と一意性が崩れる現実的な条件の切り分け
「絶対に重複しないのか」は必ず出る問いです。答えは、確率的にほぼ起きないが保証ではない、になります。他所の数字を引き写さず、自分で計算できる形にしておきます。
1秒に10億個を86年続けて50%に達する衝突確率の計算根拠
v4のレイアウトは、RFC 9562 §5.4 で random_a 48ビット・version 4ビット・random_b 12ビット・variant 2ビット・random_c 62ビットと定義されています。乱数部分は 48+12+62 で122ビット。取りうる値は 2の122乗、およそ5.317×10の36乗通りになります。
衝突確率は誕生日問題の近似式で求まります。50%に達する生成数は n ≒ 1.1774×√N なので、N にこの値を入れると n ≒ 2.715×10の18乗個。毎秒10億個を生成し続ける前提で割ると 2.715×10の9乗秒、年に直して約86.0年です。世界のどこかで重複が起きるかではなく、自分のシステムが何個作るかで判断してください。1日100万件の発行なら、100年動かしても n より8桁小さい水準にとどまります。
疑似乱数の使用でv4が一意性を失う実装上の代表的な失敗パターン
この確率は、乱数が本当にランダムであることが前提です。壊れる時は確率ではなく実装が原因になります。
- 暗号論的に安全でない疑似乱数を使い、シードが時刻由来で同時起動のプロセス間で一致する
- コンテナイメージへ乱数状態を焼き込み、同一イメージから起動した全インスタンスが同じ系列を生成する
- 自前実装でversionビットとvariantビットを固定し忘れ、122ビットのつもりが実質のばらつきを削っている
いずれも確率の桁を数十桁単位で崩します。標準ライブラリの uuid4() は暗号論的に安全な乱数源を使うと公式ドキュメントに明記されているので、自前実装に置き換える理由がない限り標準の関数を呼びます。生成器を自作したなら、上位ビットの分布に偏りがないことを検証してから本番へ載せてください。
主キーにUUIDを採用する条件と連番を選ぶべき場面の判断基準
ここからは判断です。UUIDは万能の主キーではありません。要件を確かめずに「分散に強いから」と既定値にすると、性能とディスク容量の両方で後悔します。
B-treeインデックスの断片化とストレージ量の増加という代償
代償は2つあります。1つは容量です。UUIDは16バイト、BIGINTは8バイトなので主キーだけで倍。さらに外部キーとして参照する子テーブルの列と、その索引にも同じ差が乗ります。1000万行の親と3つの子テーブルを持つ構成なら、索引を含めて数百MB単位の差になります。
もう1つは書き込み位置です。v4は完全なランダム値なので、B-treeの葉ページ全体に挿入が散らばります。ページ分割が頻発し、キャッシュに乗せたい領域も広がります。v7が時刻順に並ぶよう設計されたのは、この挿入位置を末尾へ寄せるためです。具体的な測定値と各言語での生成方法はUUIDv7の性能とDB設計を扱った記事にまとめてあります。
連番を選ぶべき単一DB構成とUUIDが必要な分散環境の明確な線引き
線引きを条件で書きます。次の3つのいずれにも当てはまらないなら、UUIDは採用しません。BIGINTの連番で組みます。
- 複数のデータベースやシャードへ書き込み、採番の一意性を中央で取れない
- オフラインのモバイル端末や店舗端末が先にIDを作り、後から同期する
- IDをURLや帳票に出すため、連番だと総件数や受注ペースが外部に推定される
見送るべき場面も明示します。社内の1データベースで完結する在庫管理や勤怠管理で、画面にIDを出さない設計なら、UUIDは容量と索引の負担だけを増やす選択です。将来の分散化を理由に先回りするのも採りません。行数が積み上がってからの型変更は確かに重い作業ですが、来ない分散のために全テーブルの索引を太らせる判断のほうが、期待値では損をします。3番目の条件だけが理由なら、内部主キーは連番のまま外部公開用の列にUUIDを持たせる二本立てが実務的な解です。識別子の設計や既存システムの分散化で判断に迷うなら、API開発・システム連携の相談先として設計段階から入る形も選べます。
推測不能なトークンとしてUUIDを使う設計の誤りと代替手段の選定
最後に、実装として最も危ないUUIDの使い方を潰します。
RFC 9562がsecurity capabilityとしての利用を禁じる根拠
RFC 9562 のSecurity Considerationsは、実装がUUIDを推測困難だと仮定すべきではないと述べ、security capability(所持しているだけでアクセス権が得られる識別子)として使ってはならないと明記しています。前身のRFC 4122から引き継がれ、より強い表現になった項目です。
禁じられているのは具体的にこういう設計です。パスワードリセットのリンクにUUIDを埋めてそれだけで本人確認とする、招待URLにUUIDを付けて認可の代わりにする、ダウンロードURLをUUIDにして権限チェックを省く。いずれも「知っていること」を「権限があること」と同一視しています。UUIDは一意性のための識別子で、秘匿性を仕様として保証していません。バージョンによっては大半のビットが予測可能な値で埋まっている点も見落とせません。
v7の時刻可読性が招く情報漏えいと推測不能トークン生成の代替案
v7には固有の注意があります。先頭48ビットがミリ秒精度のUnix時刻なので、値を1つ手に入れれば生成時刻がそのまま読めてしまう構造です。PostgreSQLが uuid_extract_timestamp() という関数を持つこと自体が、その裏返しになります。会員IDや注文IDをv7にしてURLへ出すと、登録日時や受注時刻が外部から丸見えです。連番を避けた目的が、別の形で崩れる構図になります。
トークンが要る場面の代替は単純です。用途を分けるだけです。データの一意性にはUUID、認証や認可には署名付きトークンか、乱数から作った十分な長さのランダム文字列と、サーバ側での有効期限と権限の照合をセットで置きます。REST APIの設計原則に沿うなら、URLに載るのはリソースの識別子であって認可情報ではない、という切り分けになります。識別子と鍵を1つの値に兼ねさせない。この原則だけ守れば、ここで挙げた事故はまとめて防げます。
よくある質問
UUIDの実装で問い合わせの多い論点を5つまとめます。仕様の該当箇所と、実務でどちらを選ぶかまで含めて答えます。
UUIDとGUIDは同じ規格のものとして扱ってよいのでしょうか?
実務ではほぼ同じものとして扱えます。GUIDはMicrosoftが用いる呼称で、値の構造は同じ128ビットです。ただし歴史的にvariantが異なる実装が存在し、Pythonの UUID.variant にも RESERVED_MICROSOFT という区分が残っています。古いWindows系システムから受け取った値は、variantを確認してから解釈してください。
UUIDの文字数は何文字で、DBには何バイトで保存されますか?
標準の文字列表記は、ハイフン4本を含めて36文字です。ハイフンを除くと16進数32文字になります。データベースの専用型に格納する場合は128ビット=16バイトです。専用型を持たないミドルウェアで文字列として保存すると36バイト以上を消費し、比較も文字列比較になるため遅くなります。
UUIDv4とUUIDv7はどちらを選べばよいのでしょうか?
データベースの主キーに置くならv7、それ以外の一意なラベル用途ならv4です。v7は先頭48ビットがUnix時刻なので挿入位置が末尾へ寄り、索引の断片化を抑えられます。反面、生成時刻が値から読めるため、外部に出す識別子には向きません。v7の内部構造と各言語での実装は、末尾の関連記事にある専用の解説を参照してください。
UUIDのバージョンは文字列のどの位置を見れば判別できますか?
3番目のグループの先頭1文字です。RFC 9562 は version フィールドを octet 6 の上位4ビット(ビット48〜51)と定めており、これが文字列表記の15文字目にあたります。4ef8 で始まるグループなら4でv4、79fb なら7でv7です。この読み方が成立するのは variant が 10xx の値だけなので、4番目のグループ先頭が 8・9・a・b のいずれかであることを先に確かめます。
UUIDを短縮してURLに使った場合も一意性は保たれますか?
Base64やBase32で表記を短縮するだけなら、128ビットの情報量は変わらないため一意性も保たれます。22文字前後まで縮む計算です。一方、末尾を切り落として桁数を減らす短縮は別物で、削ったビット数のぶん衝突確率が跳ね上がります。8文字に切れば残りは32ビットで、約8万件の生成で50%衝突する水準です。切り捨てではなく基数変換を選びます。
関連記事
- UUIDv7とは|UUIDv4との違い・生成方法・DB性能と各言語の実装を解説:v7の内部構造とDB性能の実測はこちらです
- 冪等性とは?読み方・意味からAPI・IaCでの担保方法まで実装者向けに解説:名前ベースUUIDで冪等な取り込みを組む前提です
- RESTとは?REST APIの仕組みと6原則・SOAP/GraphQLとの違いを実装目線で解説:URLへ識別子を載せる際の設計原則を扱っています
- 非同期処理とは?同期処理との違いから実装方式まで実装者目線で解説:オフライン採番と後追い同期の周辺知識です