てぶら

UUIDとは?v4とv7の違いとデータベースの主キーに使うときの注意点

公開日:

「550e8400-e29b-41d4-a716-446655440000」のような形式のIDを、APIやデータベースで見かけることは多いでしょう。これがUUIDです。中央の採番サーバーなしに、どこで生成しても実質的に重複しないIDを作れるため広く使われていますが、バージョンによって性質が大きく異なります。この記事では、UUIDの構造から、よく使われるv4と新しいv7の違い、主キーとして使うときの注意点までを解説します。

UUIDとは: 128ビットの識別子

UUID(Universally Unique Identifier)は128ビットの識別子で、16進数32桁を「8-4-4-4-12」の5つのグループにハイフンで区切った36文字の文字列で表すのが一般的です。Microsoftの製品ではGUID(Globally Unique Identifier)とも呼ばれますが、実質的に同じものです。

UUIDの仕様は長らくRFC 4122で定められていましたが、2024年に公開されたRFC 9562で置き換えられ、新しいバージョンのv6・v7・v8が追加されました。RFC 9562では、16進数の英字は小文字で出力し、入力時は大文字・小文字を区別しないことが推奨されています。すべてのビットが0の「00000000-0000-0000-0000-000000000000」はNil UUID、すべてが1(f)のものはMax UUIDと呼ばれる特別な値です。

UUIDの最大の利点は、データベースの連番のように1か所で採番しなくても、各サーバーやクライアントが独立して生成したIDが実質的に重複しない点です。複数のデータベースのデータを統合したり、オフラインで作成したデータを後から同期したりする場面で特に役立ちます。

UUIDの構造: バージョンとバリアントの読み方

UUIDの文字列には、どのバージョンで生成されたかを示す情報が含まれています。3つ目のグループの先頭1文字がバージョン、4つ目のグループの先頭1文字がバリアントです。

text
550e8400-e29b-41d4-a716-446655440000
              ^    ^
              |    └ バリアント: 8・9・a・b のいずれか(RFC 9562 の UUID)
              └ バージョン: 4

主なバージョンは次のとおりです。

  • v1: 生成時刻とMACアドレス(ノードID)をもとにする。生成した機器や時刻がわかってしまう
  • v3・v5: 名前空間と名前をハッシュ化して作る(v3はMD5、v5はSHA-1)。同じ入力からは常に同じUUIDになる
  • v4: ほぼすべてのビットが乱数。最も広く使われている
  • v6: v1のビットの並びを時刻順にソートできるよう入れ替えたもの
  • v7: UNIXタイムスタンプ(ミリ秒)と乱数を組み合わせたもの。時刻順に並ぶ
  • v8: 実装者が独自の形式を定義するためのもの

新しく設計するシステムでは、RFC 9562でも、v1・v6よりv7を使うことが推奨されています。名前から決まったIDを作りたい場合はv5、それ以外ではv4かv7を選ぶのが一般的です。

v4: ランダムなUUIDと衝突する確率

v4は、128ビットのうちバージョンとバリアントを表す6ビットを除いた122ビットがランダムな値です。とりうる値はおよそ5.3×10^36通りもあります。

誕生日のパラドックスの考え方を使うと、n個のv4 UUIDの中に重複が1組でも含まれる確率は、およそ n² ÷ (2 × 2^122) で見積もれます。

  • 1兆(10^12)個生成した場合: 約10^-13(10兆分の1程度)
  • 約103兆個生成した場合: 約10億分の1
  • 重複する確率が50%になるのは: 約2.7×10^18個(270京個)生成したとき

つまり、正しく生成されたv4 UUIDが偶然衝突することは、現実的には心配しなくてよいレベルです。実際に問題になるのは、数学的な確率よりも乱数の質です。暗号論的に安全でない乱数(JavaScriptのMath.random()など)で作った自作のUUIDや、仮想マシンのイメージを複製したときに乱数の状態まで複製されてしまうケースでは、重複が起きることがあります。

js
// ブラウザ(HTTPS または localhost)・Node.js 19 以降で利用できる
crypto.randomUUID(); // 例: "3b241101-e2bb-4255-8caf-4136c566a962"

このサイトのUUID生成ツールは、ブラウザの crypto.randomUUID() を使ってv4 UUIDを生成します。一度に1〜20件まで作成でき、それぞれコピーできます。この関数は安全なコンテキストでのみ使えるため、HTTPSまたはlocalhost以外で開いた場合は生成できません。

v7: 時刻順に並ぶUUID

v7は、先頭の48ビットにUNIXタイムスタンプ(ミリ秒)を、残りに乱数を入れたUUIDです。時刻が上位のビットにあるため、文字列としてソートするだけで生成順(ミリ秒単位)に並びます。

text
018cc251-f400-7a3c-9b2e-5d41c8e7f0a6
└──────────┘ └ バージョン 7
 先頭12桁 = 0x018cc251f400 = 1704067200000(2024-01-01T00:00:00Z のミリ秒)

先頭12桁の16進数を10進数に直すと、生成した時刻のミリ秒が得られます。逆にいえば、v7のUUIDを公開すると、そのデータが作られた時刻も推測できるということです。作成日時を知られたくない用途(例えば会員の登録順や登録時期を隠したい場合)では、v4を使うほうが適しています。

js
// v7 UUID から生成時刻を取り出す
const uuid = "018cc251-f400-7a3c-9b2e-5d41c8e7f0a6";
const ms = parseInt(uuid.slice(0, 8) + uuid.slice(9, 13), 16);
new Date(ms).toISOString(); // "2024-01-01T00:00:00.000Z"

同じミリ秒の中で複数のUUIDを生成したときの並び順は、実装によって異なります。RFC 9562では、乱数部分の一部をカウンターとして使うなど、同じミリ秒内でも単調に増加させる方法がいくつか示されています。v7の生成には、利用している言語やデータベースが対応しているかを確認し、対応していない場合は実績のあるライブラリを使いましょう。

データベースの主キーに使うときの注意点

UUIDを主キーにすると、IDから件数や連番を推測されにくくなり、アプリケーション側でIDを先に決めてから保存できるといった利点があります。一方で、連番の整数と比べて注意すべき点もあります。

  • サイズ: 文字列として保存すると36バイト、バイナリなら16バイト。8バイトのBIGINTより大きく、インデックスも大きくなる
  • 挿入性能: v4は値が完全にランダムなため、B-treeインデックスのあちこちに挿入が発生し、データが増えるとキャッシュ効率が落ちやすい
  • クラスタ化インデックス: MySQL(InnoDB)は主キーの順に行を格納するため、ランダムなv4の影響を特に受けやすい

挿入性能の問題は、時刻順に増加するv7を使うことで大きく改善できます。新しい行は常にインデックスの末尾付近に追加されるため、連番に近い挙動になるからです。

保存形式も重要です。PostgreSQLには専用のuuid型があり、16バイトで保存されます。MySQLでは CHAR(36) より BINARY(16) が効率的で、UUID_TO_BIN() と BIN_TO_UUID() で変換できます。UUID_TO_BIN() の第2引数(swap_flag)はv1の時刻部分を並べ替えるためのもので、v7に使うと時刻順が崩れるため指定しないでください。

sql
-- PostgreSQL(13 以降は拡張なしで gen_random_uuid() が使える)
CREATE TABLE users (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name text NOT NULL
);

-- MySQL: UUID() が返すのは v1
INSERT INTO users (id, name) VALUES (UUID_TO_BIN(UUID()), 'tanaka');

なお、RFC 9562では、UUIDが推測困難であることを前提にしないよう注意を促しています。パスワードリセットのトークンやセッションIDのように「知っていることが権限になる」値には、UUIDではなく、十分な長さの暗号論的に安全な乱数を使いましょう。v5のように名前からハッシュで作るUUIDの仕組みに興味がある場合は、ハッシュ生成ツールでSHA-1などのハッシュ値が入力によってどう変わるかを確かめてみると理解が深まります。