アプリを作っていると、データひとつひとつに名前をつける場面が必ずやってきます。ユーザー、注文、アップロードされたファイル。あとから見分けられるように、重複しないIDが要ります。
多くの人はまず、1から順番に数える連番を思いつきます。それでうまくいく場面も多いのですが、規模が大きくなると急に扱いにくくなります。
そこで登場するのがUUIDです。Pythonにはuuidという標準ライブラリが最初から入っていて、importするだけで使えます。
この記事では、UUIDが重複しない理由から、Python 3.14で新しく加わったuuid7の使いどころまでを順番に見ていきます。
連番のIDは、どこで行き詰まるのか¶
まず、連番の何が困るのかをはっきりさせておきます。連番はデータベースが採番するので、番号が決まるのは保存が終わったあとです。
つまり、保存する前にIDを使いたい処理が書けません。ファイル名を先に決めたいときや、関連する複数のテーブルへまとめて書き込みたいときに、ここで手が止まります。
もうひとつが、番号から中身が透けて見えることです。URLに/users/1024と出ていれば、利用者がおよそ千人規模だと外から推測できてしまいます。
そして最後が、システムを分けたときの衝突です。サーバーを2台に増やした瞬間、どちらも1番から採番を始めてしまいます。
私は10年ほどエンジニアとして開発に関わってきましたが、別々に動いていた2つのシステムを統合する案件で、連番IDがまるごとぶつかって移行スクリプトを書き直したことがあります。あのときにUUIDのありがたみを痛感しました。
UUIDは、ぶつからないことを前提にした128ビットの値¶
UUIDはUniversally Unique IDentifierの略で、日本語にすると汎用一意識別子です。世界のどこで誰が作っても重ならないことを狙った、128ビットの値になります。
見た目は8桁、4桁、4桁、4桁、12桁のハイフン区切りです。ハイフンを含めてちょうど36文字になります。
36文字の中身を分解してみる¶
数字と英字がただ並んでいるように見えますが、位置ごとに意味が決まっています。おおまかな内訳を表にまとめました。
| 見え方 | 中身 |
|---|---|
| 全体 | 128ビット、つまり16バイトの数 |
| 文字列 | 16進数32文字とハイフン4本で36文字 |
| 13文字目 | バージョン番号。4なら乱数から、7なら時刻から作られた |
| 17文字目 | バリアント。RFC 9562の仕様に従っていることを示す |
バージョンという言葉が出てきましたが、これはUUIDの新旧ではなく作り方の種類を指します。乱数で作るのか、時刻から作るのかという違いです。
Pythonではこの種類ごとに関数が分かれています。順番に触っていきましょう。
まずはuuid4を動かしてみる¶
前置きが長くなりました。追加のインストールは要らないので、そのまま試せます。
いちばんよく使うのが、乱数から作るuuid4()です。
import uuid
new_id = uuid.uuid4()
print(new_id)
# 163fabb8-b507-4002-adc6-3da95320eefb
print(type(new_id)) # <class 'uuid.UUID'>
print(new_id.version) # 4
print(len(str(new_id))) # 36
print(len(new_id.bytes)) # 16
# 文字列からUUIDに戻すこともできる
same = uuid.UUID("163fabb8-b507-4002-adc6-3da95320eefb")
print(same == new_id) # True
実行するたびに違う値が返ります。上のコメントに書いた出力は手元で出た一例なので、あなたの環境では別の値になります。
uuid4()が返すのは文字列ではなくUUID型のオブジェクトです。データベースやJSONへ渡すときは、str()で文字列にするか.bytesでバイト列にします。
なぜ乱数なのに重複しないのか¶
ここが最初に引っかかるところだと思います。乱数なら、たまたま同じ値が出ることもあるのではないかという疑問です。
理屈のうえでは重複します。ただ128ビットのうち122ビットが乱数なので、取りうる値の数が現実離れして多いのです。
このランダム性を支えているのが、OSが用意している暗号用の乱数です。uuid4()は内部でos.urandom()を呼んでいて、randomモジュールが返す乱数とは性質がまったく違います。
その違いが気になる方は、こちらで詳しく扱っています。【関連記事】Pythonのrandomとは?サイコロやシャッフルの書き方と、パスワードに使ってはいけない理由を解説
同じ入力から同じIDが欲しいときはuuid5¶
毎回違う値が出ては困る場面もあります。同じメールアドレスからは、いつでも同じIDを作りたいというような要件です。
そのために用意されているのがuuid3()とuuid5()です。名前空間と名前を渡すと、ハッシュを通して決まった値を返してくれます。
import uuid
# 同じ名前空間・同じ名前なら、いつ実行しても結果は変わらない
print(uuid.uuid5(uuid.NAMESPACE_DNS, "python-webacademy.com"))
# cf4d99af-d46a-540a-ab98-cf6f773de4c4
print(uuid.uuid5(uuid.NAMESPACE_DNS, "python-webacademy.com"))
# cf4d99af-d46a-540a-ab98-cf6f773de4c4
uuid3()はMD5を、uuid5()はSHA-1を使います。新しく書くならuuid5()のほうを選んでおくのが無難です。
注意点もあります。元にした文字列が推測できるなら、UUIDのほうも作り直せてしまいます。
秘密にしておきたい値を材料にするのは避けてください。ハッシュそのものの性質は、こちらで整理しています。【関連記事】Pythonのhashlibとは?パスワードをそのまま保存してはいけない理由とハッシュの基本を初心者向けに解説
Python 3.14で加わったuuid7¶
ここからが新しい話です。Python 3.14でuuid6()、uuid7()、uuid8()の3つが追加されました。
根拠になっているのが2024年5月のRFC 9562で、これは長らく使われてきたRFC 4122を置き換える仕様です。同じタイミングで、Nil UUIDとMax UUIDを表すuuid.NILとuuid.MAXも使えるようになりました。
なかでも実務で話題になりやすいのがuuid7()です。先頭の48ビットにミリ秒単位のUnix時刻を置き、残りを乱数などで埋めます。
時刻が先頭に来ると何がうれしいのか。作った順番が、そのまま値の大小として並ぶという性質が手に入ります。
import uuid
ids = [uuid.uuid7() for _ in range(3)] # Python 3.14以降で使える
for one in ids:
print(one)
# 01a088f8-c580-7691-b57f-aa6efe7a71cf
# 01a088f8-c581-7bbf-9f43-9b03911a7ccb
# 01a088f8-c582-7d53-81ca-2109dc8f4435
# 生成した順のまま、文字列としてもソート済みになっている
print([str(i) for i in ids] == sorted(str(i) for i in ids)) # True
3つとも先頭が同じ文字で始まっているのは、ほぼ同じ時刻に作ったからです。さきほどのuuid4の出力と見比べると、性格の違いが一目でわかります。
時刻順に並ぶと、データベースが楽になる¶
なぜ順番がそれほど大事なのかというと、主キーのインデックスに効いてくるからです。
ランダムな値を主キーにすると、新しい行が毎回インデックスのばらばらな位置へ挿し込まれます。あちこちのページが書き換わり、キャッシュも効きにくくなります。
時刻順に並ぶIDなら、追加される場所はいつも末尾の近くです。書き込みが集中するテーブルほど、uuid7の効きめは大きくなります。
この流れはPythonだけの話ではありません。PostgreSQL 18でもuuidv7()関数が追加され、データベース側で同じ形のIDを作れるようになりました。
ORM越しに主キーを扱っているなら、生成をアプリ側とDB側のどちらでやるかを先に決めておくと迷いません。【関連記事】PythonのSQLAlchemyとは?SQLを書かずにデータベースを操作するORMの基本を初心者向けに解説
同じミリ秒に大量に作っても順番が崩れない¶
記録するのがミリ秒までなら、1ミリ秒の間に作ったIDは同着になってしまいそうです。ここは私も最初に気になりました。
CPythonの実装は、この隙間を埋めるために42ビットのカウンタを用意しています。同じミリ秒の中ではカウンタを1ずつ増やし、順番を守る仕組みです。
ループの中で連続して作っても並びが保たれるのは、このおかげです。ミリ秒より細かい時計を持たない環境でも同じように動きます。
uuid4とuuid7、結局どちらを選ぶか¶
種類が4つも5つもあると、結局どれを使えばいいのか迷います。実際の判断基準は、そう多くありません。
| 関数 | 作り方 | 向いている場面 |
|---|---|---|
uuid4() |
122ビットの乱数 | 迷ったときの既定。順序を知られたくないID |
uuid7() |
時刻48ビットと乱数 | データベースの主キー。並び順が欲しいログやイベント |
uuid5() |
名前空間と名前のハッシュ | 同じ入力からいつも同じIDを作りたいとき |
uuid1() |
時刻とMACアドレス | 新規採用は避けたい。機器の情報が混ざる |
uuid7には、作られた時刻をIDから読み取れるという裏返しの性質があります。公開URLに出すIDなら、そこを許容できるかを先に考えてください。
もうひとつ、uuid7()を使うにはPython 3.14以降が必要です。3.13以前で同じことをしたいなら、外部ライブラリを入れるかデータベース側の関数に任せることになります。
3.14で何が変わったのかをまとめて知りたい方は、こちらもどうぞ。【関連記事】Python 3.14ベータが始動!新機能について解説します。
判断に迷ったら、まずuuid4()で始めて構いません。主キーの性能が問題になってからuuid7()へ移る、という順番で十分に間に合います。
保存するときに気をつけたいこと¶
最後に、実際にデータベースへ入れるときの話をひとつ。持ち方しだいでテーブルの重さが変わります。
36文字の文字列としてそのまま入れると、1件あたり36バイトを使います。16バイトのバイナリで持てば半分以下です。
PostgreSQLのように専用のUUID型があるなら、それを使うのがいちばん素直です。sqlite3のように専用の型がない場合は、文字列で持つか.bytesでバイナリにするかを最初に決めておきます。
私が過去に苦労したのは、ある画面は大文字、別の画面は小文字でUUIDを保存していたケースです。同じIDのはずなのに文字列の比較で一致しないという、地味なのに見つけにくいバグになりました。
PythonのUUID型は、このあたりを吸収してくれます。大文字でも小文字でもuuid.UUID()に通せば同じオブジェクトになり、str()で出せば必ず小文字にそろいます。
小さなテーブルで実際に出し入れしてみると、感覚がつかみやすくなります。【関連記事】Pythonからデータベースを操作するsqlite3の使い方を解説
まとめ¶
uuidは、重複しないIDを作るための標準ライブラリです。importするだけで使えて、uuid4()を呼べば1行でIDが手に入ります。
用途によって選ぶ関数は変わります。ふつうのIDならuuid4()、同じ入力から同じ値が欲しいならuuid5()、主キーの性能を気にするならuuid7()という使い分けです。
そしてuuid7()は、Python 3.14から標準で使えるようになった新顔です。時刻順に並ぶという一点だけで、データベースの相性がぐっと良くなります。
まずは手元でuuid.uuid4()を何度か呼んで、値が毎回変わる様子を眺めてみてください。IDを自分で作るという感覚が、そこからつかめてくるはずです。
参考情報¶
- uuid — UUID objects according to RFC 9562(Python公式ドキュメント)
- What's New In Python 3.14(Python公式ドキュメント)
- RFC 9562: Universally Unique IDentifiers (UUIDs)(RFC Editor)
- PostgreSQL 18.0 Release Notes(PostgreSQL公式ドキュメント)
ここまでお読みいただきありがとうございました。