LASSIC Media らしくメディア
UUIDのv4とv7の違い、ULIDとの使い分け
監修・編集責任者:牛尾 昭昌(株式会社LASSIC 執行役員)
この記事の結論
- UUIDは128ビットの識別子で、現行の仕様はRFC 4122を置き換えたRFC 9562です。
- v4は122ビットがほぼ乱数、v7は先頭48ビットがUnixミリ秒なので、v7は作った順に並びます。
- ULIDも先頭48ビットが時刻で、違いは26文字の表記と80ビットの乱数、標準化の有無です。
※ 本記事は2026年10月時点の公式情報(省庁・公的機関、および製品やサービスを提供する事業者が公開している資料)に基づきます。
新しいテーブルの主キーをUUIDにする、と決めたあとで迷うのが「どのUUIDにするか」です。乱数だけで作るv4、時刻を先頭に置くv7、似た目的で使われるULIDと、選択肢がいくつもあります。UUIDとは、128ビットの値で物やデータを一意に見分けるための識別子で、2024年に仕様が改訂され、時刻順に並ぶv7が正式に加わりました。
本記事では、システムの開発・運用を担当する方に向けて、UUIDの構造、v4とv7の違い、ULIDとの使い分けを整理します。Pythonでv7を実際に作り、先頭48ビットがUnixミリ秒の時刻になっていることを分解して確かめるほか、PostgreSQLとMySQLでの扱い方、主キーに使うときのつまずきやすい点も取り上げます。
目次
UUIDとは
UUID(Universally Unique IDentifier)は、128ビットの長さを持つ識別子です。現行の仕様はIETFが2024年5月に発行したRFC 9562で、従来のRFC 4122はこの文書で置き換えられました。*1 GUIDとも呼ばれ、文字列では 8-4-4-4-12 桁に区切った32桁の16進数、ハイフンを含めて36文字で書きます。
連番の主キーは、1つのデータベースの中でしか一意になりません。PostgreSQLのマニュアルも、分散システムではシーケンスによる採番より一意性の保証が強いとUUIDを説明しています。*2 RFC 9562は、ネットワーク越しに連番を調整する手間が負担になる場面で、調整なしに各所で作れる点をUUIDの利点に挙げています。 主キーそのものの考え方は「主キーと外部キー」で扱っています。UUIDは、業務上の意味を持たないサロゲートキーの作り方の1つです。
128ビットのうち、ビット48〜51の4ビットがバージョン、ビット64と65の2ビットがバリアントを表します。 文字列で見ると、3つ目の塊の先頭の1文字がバージョンの数字で、4つ目の塊の先頭は8・9・a・bのどれかになります。RFC 9562が定めるバージョンは次のとおりです。
| バージョン | 中身 |
|---|---|
| v1 | 1582年10月15日からの100ナノ秒単位の時刻(60ビット)、クロックシーケンス、ノード(MACアドレスなど) |
| v2 | DCEセキュリティ用に予約 |
| v3・v5 | 名前空間と名前のハッシュ値(v3はMD5、v5はSHA-1) |
| v4 | 乱数 |
| v6 | v1の時刻を上位の桁から並べ直したもの |
| v7 | Unixミリ秒の時刻(48ビット)と乱数 |
| v8 | 独自形式・実験用 |
v4とv7の違い
v4は、乱数でUUIDを作る方式です。128ビットからバージョンとバリアントの6ビットを除いた122ビットを乱数で埋めます。*1 RFCは、予測しにくく衝突しにくい値にするため、暗号用の疑似乱数生成器(CSPRNG)を使うよう求めています。Pythonの uuid4() も、暗号用の乱数で生成すると説明されています。*3
v7は、先頭48ビットに1970年1月1日0時(UTC)からのミリ秒数を、うるう秒を除いて符号なし整数で入れ、残りの74ビットを乱数で埋めます。*1 乱数の代わりに、1ミリ秒未満の時刻(12ビットまで)や、同じミリ秒の中で増えるカウンターを入れることも認められています。時刻が先頭にあるので、作った順にバイト列でも文字列でも並びます。48ビットの時刻は10889年まで表せます。
v1との違いも押さえておきます。v1は時刻の下位の桁を先頭に置くため、値を並べ替えても作った順になりません。ノード欄にMACアドレスを入れると、機器の情報が値から読み取られるおそれもあります。v6はv1の時刻を上位の桁から並べ直したものですが、RFCはv1を使っていないシステムではv7を使うべきだとしています。
衝突の確率は、乱数が理想的に作られる前提で、誕生日問題の近似式で見積もれます。v4の乱数部分は122ビットで、とり得る値は2の122乗、約5.3×10の36乗通りです。1兆個のv4を作ったときに1組でも重なる確率は、この近似で約9.4×10のマイナス14乗になります(筆者の計算)。現実の衝突は、乱数の作り方の不具合から起きると考えたほうがよいでしょう。
具体例:Pythonでv7を分解する
Pythonの標準ライブラリ uuid では、uuid7() はPython 3.14で追加されました。*3 手元の環境はPython 3.12で uuid7() が無いため、RFC 9562の手順どおりに作る関数を書き、v4と並べて生成しました。
import os, time, uuid, datetime
def uuid7(): # RFC 9562 5.7 の手順(Python 3.14 未満向け)
ms = time.time_ns() // 1_000_000
b = bytearray(ms.to_bytes(6, "big") + os.urandom(10))
b[6] = (b[6] & 0x0F) | 0x70 # バージョン 7
b[8] = (b[8] & 0x3F) | 0x80 # バリアント 0b10
return uuid.UUID(bytes=bytes(b))
u4, u7 = uuid.uuid4(), uuid7()
print(u4, u4.version) # 8443f6f0-70c6-42c7-b584-d1022c8c1910 4
print(u7, u7.version) # 01a0f74e-30cb-7dea-9ec6-f958384262bf 7
ms = int.from_bytes(u7.bytes[:6], "big") # 先頭48ビット
print(ms) # 1790855491787
print(datetime.datetime.fromtimestamp(ms / 1000, datetime.timezone.utc))
# 2026-10-01 11:51:31.787000+00:00
v7の先頭6バイト(48ビット)を整数に戻すと 1790855491787 になり、日時に直すと生成した時刻の2026年10月1日11時51分31.787秒(UTC)でした。文字列の先頭12桁 01a0f74e30cb が、この値の16進数です。RFC付録の例 017F22E2-79B0-7CC3-98C4-DC0C0C07398F も同じ手順で分解すると 1645557742000、つまり2022年2月22日19時22分22秒(UTC)になり、RFCが示す時刻と一致します。*1
v4の値を同じように分解しても、意味のある時刻は出てきません。どちらのバージョンかは version 属性か、3つ目の塊の先頭の数字で見分けます。Python 3.14の uuid7() は、48ビットの時刻に加えて42ビットのカウンターを持ち、同じミリ秒の中でも値が増えていくようにしています。
ULIDとの違い
ULIDは、時刻順に並ぶ128ビットの識別子として、GitHub上の仕様書で定められた方式です。先頭48ビットがUnixミリ秒の時刻、残り80ビットが乱数で、CrockfordのBase32で26文字の文字列にします。*4 Base32の文字からは、見間違えやすいI・L・O・Uを除いています。大文字と小文字を区別せず、URLにそのまま使える文字だけで書けます。
時刻の部分はv7と同じ48ビットのUnixミリ秒です。違いは、ULIDにはバージョンとバリアントのビットが無く乱数が80ビットあること、表記が26文字であることです。同じミリ秒の中で作るときは乱数部分を1ずつ増やす方式が仕様書に書かれており、増やしきれなくなると生成は失敗します。 RFC 9562は、改訂にあたって分析した16の実装の1つにULIDを挙げています。
| 項目 | UUID v4 | UUID v7 | ULID |
|---|---|---|---|
| 定めている文書 | RFC 9562 | RFC 9562 | GitHubの仕様書 |
| 時刻 | なし | 先頭48ビット(Unixミリ秒) | 先頭48ビット(Unixミリ秒) |
| 乱数 | 122ビット | 74ビット(カウンターも可) | 80ビット |
| 文字列 | 36文字(16進数とハイフン) | 36文字(16進数とハイフン) | 26文字(Base32) |
| 作った順の並び | 並ばない | 並ぶ | 並ぶ |
使い分けの目安を挙げます。データベースのuuid型や、UUIDを前提にしたライブラリ・APIを使うなら、v7が扱いやすいでしょう。すでにULIDで運用しているシステムや、短い文字列をURLに載せたい場面ではULIDが向いています。作った順の並びが要らず、値から生成時刻を知られたくない場合はv4を選びます。
データベースでの使いどころ
PostgreSQLのuuid型は、バージョンを問わずどのUUIDでも保存できます。*2 v4を作る gen_random_uuid() に加え、PostgreSQL 18で uuidv7() と、別名の uuidv4() が追加されました。*5 18の uuidv7() は、ミリ秒の時刻に1ミリ秒未満の時刻と乱数を組み合わせて作ります。*6 値から時刻を取り出す uuid_extract_timestamp() は、17ではv1だけが対象で、18でv7にも使えるようになっています。
-- PostgreSQL 18:主キーの既定値を v7 にする
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT uuidv7(),
ordered_at timestamptz NOT NULL DEFAULT now()
);
SELECT uuid_extract_timestamp('019535d9-3df7-79fb-b466-fa907fa17f9e'::uuid);
-- 2025-02-23 21:46:24.503-05(公式マニュアルの例)
-- MySQL 8.4:文字列のUUIDを16バイトにして保存する
SELECT HEX(UUID_TO_BIN('6ccd780c-baba-1026-9564-5b8c656024db', 1));
-- 1026BABA6CCD780C95645B8C656024DB(公式マニュアルの例)
SELECT BIN_TO_UUID(UUID_TO_BIN(@uuid, 1), 1); -- 元の文字列に戻す
マニュアルの例の 019535d9-3df7 を先ほどのPythonの手順で分解すると、UTCの2025年2月24日2時46分24.503秒になり、示された時刻(UTCより5時間遅い表記)と合います。
MySQLの UUID() は、マニュアルによるとRFC 4122のバージョン1の値を36文字の文字列で返します。UUID_TO_BIN() は文字列を16バイトの VARBINARY(16) に変換し、2つ目の引数に1を渡すと、1つ目と3つ目の塊を入れ替えて変化の速い部分を後ろに回します。索引の効率を上げるための指定で、v1の値を前提にしています。*7 RFC 9562も、文字列で持つと128ビットの値に288ビットを使うため、可能なら128ビットのバイナリで保存すべきだとしています。*1
主キーにv4を使うときの索引
RFC 9562は、時刻順でないv4は索引の局所性が悪く、続けて作った値が索引の中で離れた位置に入るため、B木とその派生の構造では性能への悪影響が大きくなり得ると書いています。*1 時刻順の値と乱数の値の差は、1桁以上になることもあるとしています。B木の構造そのものは「B木の仕組み」で扱っています。ここでは、値を作った順に整列済みの列へ挿入したとき、どの位置に入るかを数えてみます。
import bisect, time, uuid # uuid7 は前のコードの関数
def stats(gen, n, wait=0):
keys, pos, tail = [], 0, 0
for _ in range(n):
k = gen().bytes
i = bisect.bisect(keys, k) # 整列を保つ挿入位置
if keys: pos += i / len(keys) # 0=先頭、1=末尾
tail += (i == len(keys))
keys.insert(i, k)
if wait: time.sleep(wait)
return round(pos / (n - 1), 4), tail / n
print(stats(uuid.uuid4, 10000)) # (0.501, 0.0008)
print(stats(uuid7, 2000, 0.001)) # (1.0, 1.0)
print(stats(uuid7, 10000)) # (0.9302, 0.0128)
v4は、挿入位置の平均が列のちょうど中ほどで、末尾に入ったのは1万件中8件でした。1ミリ秒以上の間隔を空けて作ったv7は、2,000件すべてが末尾に入りました。B木なら、新しい値が右端のページに集まる状態です。一方、間隔を空けずに1万件を一気に作ると、末尾に入ったv7は1.3%にとどまりました。同じミリ秒の中では、後ろの乱数の大小で順番が決まるためです。
つまずきやすい点
1つ目は、同じミリ秒の中の順序です。上の結果のとおり、乱数だけのv7は同じミリ秒の中では並びません。RFC 9562は、まとめて作る場合もカウンターなどで作成順を保つよう勧めています。 Python 3.14はカウンター、PostgreSQL 18は1ミリ秒未満の時刻でこれに対応しているので、自作するより標準の関数を使うほうが確実です。
2つ目は、v7とULIDの値から作成時刻が読めることです。注文や会員のIDを画面やURLに出すと、いつ作られたかが分かります。RFCは、UUIDを推測しにくいものと見なさず、持っているだけで操作できる鍵のように使ってはならないとしています。セキュリティに関わる用途ではv4を使うべきだとも書いています。*1 UUIDを知っていれば見られる、という作りにはせず、権限の確認を別に置きます。
3つ目は、MySQLの入れ替えの指定をv7に使うことです。入れ替えはv1を前提にしています。 1ミリ秒違いの2つのv7で試すと、入れ替え後は後に作った値のほうが小さくなり、並びが崩れました。v7は先頭から時刻が並んでいるので、入れ替えずに保存します。
4つ目は、時計の巻き戻りです。NTPの補正などでシステムの時計が戻ると、後に作ったv7が前の値より小さくなることがあります。RFCは、こうした場合の扱いを実装ごとに決めておくよう求めています。
外部に委託するときに確認しておきたい点
識別子の方式は、いったんデータが貯まると変えにくい設計事項です。開発や保守を外部に頼むときは、次の点を設計書やレビューで確かめておきます。
- 主キーに使う識別子の方式(連番・UUID v4・UUID v7・ULID)と、選んだ理由が書かれているか
- UUIDをアプリケーションとデータベースのどちらで作るか、ライブラリと版が決まっているか
- 保存の型が文字列ではなく、uuid型や16バイトのバイナリになっているか
- 画面やAPIに出すIDから作成時刻が読めてよいか、検討されているか
- IDを知っていれば参照できる作りになっておらず、権限の確認があるか
- バージョンの数字や並び順を確かめる自動テストがあるか
複数のシステムで同じIDを受け渡す構成では、方式と表記をどこで決めるかを先にそろえておきます。片方がULIDの26文字、もう片方がUUIDの36文字を前提にしていると、受け渡しの段階で変換の処理が要ります。
まとめ:UUIDで確かめておきたい3つの点
UUIDを使ううえで確かめておきたい点は、3つに整理できます。第一に、v4・v7・ULIDのどれを使うかを、並び順が要るか、作成時刻を読まれてよいかで決めること。第二に、データベースでは文字列ではなくuuid型や16バイトのバイナリで持ち、主キーに使うなら時刻順のv7を候補にすること。第三に、同じミリ秒の中の順序や時計の巻き戻りの扱いを、標準の関数やライブラリの仕様で確かめることです。この3点を押さえておけば、後から主キーの方式を変える手戻りを避けやすくなります。識別子の設計の見直しに迷いがあれば、外部の手を借りるのも一つの選択肢です。
よくある質問
UUIDとGUIDは違うものですか
同じものを指す呼び方です。RFC 9562も、UUIDはGUIDとしても知られていると書いています。ただし、MicrosoftのCOMのGUIDはバイナリで保存するときのバイト順が違うことがRFCで触れられているので、バイナリで受け渡す場合は順序を確かめます。
v4の主キーをv7に切り替える必要はありますか
すぐに切り替える必要はありません。v4とv7はどちらも同じuuid型に保存でき、バージョンの4ビットが違うので両者の値が重なることもありません。挿入の性能や索引の大きさが問題になっている場合に、新しく作る値からv7に変える方法を検討します。
UUIDの生成は、アプリケーションとデータベースのどちらで行うのがよいですか
どちらでも作れます。RFC 9562は、1つのデータベースで完結する構成なら、データベースで作るほうが値の単調な増加を保ちやすいとしています。複数のサービスで作る場合は、各所で使うライブラリと版をそろえておきます。
主キーと識別子の設計のご相談
元請(プライムベンダー)として、識別子とテーブル設計の見直しからシステムの保守・運用までご提案します。
Remoguとリラシクなら、データベースの設計や移行に加わるITエンジニアも探せます。
Remoguは、リモート前提で全国から即戦力のITプロ人材を調達するサービスです。リラシクは、扱う求人がすべてリモートワークのITエンジニア専門転職エージェントです。どちらもLASSICが運営しています。
出典
- *1 参考:IETF「RFC 9562 Universally Unique IDentifiers (UUIDs)」(https://www.rfc-editor.org/rfc/rfc9562)。出典:2024年5月発行でRFC 4122を置き換えたこと、128ビットの形式とバージョン・バリアントの位置、v1・v4・v6・v7の構造、v7は10889年まで表せること、v1を使っていないシステムはv7を使うべきこと、索引の局所性、バイナリでの保存、単調性とカウンター、セキュリティ上の注意、付録A.6のv7の例を参照(2026年10月確認)
- *2 参考:PostgreSQL Global Development Group「PostgreSQL 18 Documentation 8.12. UUID Type」(https://www.postgresql.org/docs/current/datatype-uuid.html)。出典:uuid型がRFC 9562のUUIDをバージョンを問わず保存できること、分散システムではシーケンスより一意性の保証が強いことを参照(2026年10月確認)
- *3 参考:Python Software Foundation「uuid — UUID objects according to RFC 9562」(Python 3.14)(https://docs.python.org/3/library/uuid.html)。出典:uuid4() が暗号用の乱数で生成すること、uuid7() がPython 3.14で追加され48ビットの時刻と42ビットのカウンターを持つことを参照(2026年10月確認)
- *4 参考:ulid/spec「Universally Unique Lexicographically Sortable Identifier」(https://github.com/ulid/spec)。出典:48ビットのUnixミリ秒と80ビットの乱数、CrockfordのBase32による26文字の表記とI・L・O・Uの除外、大文字小文字を区別しないこと、同じミリ秒内の単調な生成と失敗の条件を参照(2026年10月確認)
- *5 参考:PostgreSQL Global Development Group「PostgreSQL 18 Documentation E.6. Release 18」(https://www.postgresql.org/docs/18/release-18.html)。出典:PostgreSQL 18で uuidv7() と別名の uuidv4() が追加されたことを参照(2026年10月確認)
- *6 参考:PostgreSQL Global Development Group「PostgreSQL 18 Documentation 9.14. UUID Functions」(https://www.postgresql.org/docs/current/functions-uuid.html)。出典:gen_random_uuid()・uuidv4()・uuidv7() の説明、uuidv7() がミリ秒の時刻と1ミリ秒未満の時刻と乱数で作ること、uuid_extract_timestamp() の対象と例を参照。17の同じ章(docs/17/functions-uuid.html)で uuid_extract_timestamp() がv1のみ対象であることも確認(2026年10月確認)
- *7 参考:Oracle「MySQL 8.4 Reference Manual 14.23 Miscellaneous Functions」(https://dev.mysql.com/doc/refman/8.4/en/miscellaneous-functions.html)。出典:UUID() がRFC 4122のバージョン1の値を返すこと、UUID_TO_BIN() がVARBINARY(16)を返し、2つ目の引数が1なら1つ目と3つ目の塊を入れ替えること、入れ替えがバージョン1を前提にしていること、マニュアルの例を参照(2026年10月確認)