UUIDv7をPHPで自力で作るの巻調査編
突然ですがデータベースのテーブルで、これまでとは違う方法でユニークなIDを作りたくなりました。
IDといえばAUTO INCREMENTでしょとよく考えずに当たり前のようにしてきましたが、「2025年の今ユニークなIDと言えば〇〇」のようなあるんじゃないかと思い探してみたところ、様々な技術系記事で取り上げられていたのがこのUUIDv7です。
UUIDv7 とは
RFC9562で2024年5月に定義されています。
以下原文。
UUIDv7 features a time-ordered value field derived from the widely implemented and well-known Unix Epoch timestamp source, the number of milliseconds since midnight 1 Jan 1970 UTC, leap seconds excluded. Generally, UUIDv7 has improved entropy characteristics over UUIDv1 (Section 5.1) or UUIDv6 (Section 5.6).
引用: https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7
そしてその自動翻訳が以下。
UUIDv7 は、広く実装され、よく知られている Unix エポック タイムスタンプ ソースから派生した時間順の値フィールド (UTC 1970 年 1 月 1 日の午前 0 時からのミリ秒数 (閏秒は除く)) を備えています。一般的に、UUIDv7 は UUIDv1 (セクション 5.1 ) や UUIDv6 (セクション 5.6 )よりもエントロピー特性が向上しています
UUIDにはバージョンがあり、過去バージョンではデータベースのインデックスのパフォーマンスが極度に悪かったり、MACアドレスを利用して作成しているバージョンもありプライバシーやセキュリティの問題があったようです。
IDの昇順/降順ソートは当たり前のように行うのでソートのパフォーマンスが悪いのは困りますね。
そういったデータベースのユニークキーで扱う場合の問題が解決されたバージョンがUUIDv7であると。
昨年RFCで公開されたということもあり、UUIDv7は2025年らしいユニークなIDの作り方ではないでしょうか。
どうやったら使えるUUIDv7
MySQLに備えついていたりしないものかと考えまして、あれこれ調査しましたがそういうものではないようです。
各々の言語でライブラリとして実装されていたり、フレームワーク側で実装が追加されたり、各々エンジニアが自作しているという状況だと把握しました。
Cでの実装例があったので見ていきましょう。
C言語での実装例
まずUUIDv7の構成はこのように説明されています。
unix_ts_ms:
48-bit big-endian unsigned number of the Unix Epoch timestamp in milliseconds as per Section 6.1. Occupies bits 0 through 47 (octets 0-5).
ver:
The 4-bit version field as defined by Section 4.2, set to 0b0111 (7). Occupies bits 48 through 51 of octet 6.
rand_a:
12 bits of pseudorandom data to provide uniqueness as per Section 6.9 and/or optional constructs to guarantee additional monotonicity as per Section 6.2. Occupies bits 52 through 63 (octets 6-7).
var:
The 2-bit variant field as defined by Section 4.1, set to 0b10. Occupies bits 64 and 65 of octet 8.
rand_b:
The final 62 bits of pseudorandom data to provide uniqueness as per Section 6.9 and/or an optional counter to guarantee additional monotonicity as per Section 6.2. Occupies bits 66 through 127 (octets 8-15).
なるほどとっても簡単ですね。
とはなりませんので、IETFでドラフト版にC言語での実装例があったためそちらも参考にしていきます。
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
// ...
// csprng data source
FILE *rndf;
rndf = fopen("/dev/urandom", "r");
if (rndf == 0) {
printf("fopen /dev/urandom error\n");
return 1;
}
// ...
// generate one UUIDv7E
uint8_t u[16];
struct timespec ts;
int ret;
ret = clock_gettime(CLOCK_REALTIME, &ts);
if (ret != 0) {
printf("clock_gettime error: %d\n", ret);
return 1;
}
uint64_t tms;
tms = ((uint64_t)ts.tv_sec) * 1000;
tms += ((uint64_t)ts.tv_nsec) / 1000000;
memset(u, 0, 16);
fread(&u[6], 10, 1, rndf); // fill everything after the timestamp with random bytes
*((uint64_t*)(u)) |= htonll(tms << 16); // shift time into first 48 bits and OR into place
u[8] = 0x80 | (u[8] & 0x3F); // set variant field, top two bits are 1, 0
u[6] = 0x70 | (u[6] & 0x0F); // set version field, top four bits are 0, 1, 1, 1
これで全容が分かってきました。
例えば /bin/urandom
で乱数を生成するということが分かりましたし、
timespec
は、 秒単位とナノ秒単位で表現された時間間隔を保持する型です。この値が今回UUIDv7を構成する時間順の値フィールドで利用するものでしょうね。
PHPエンジニアとしては、PHPで実装する場合極力この timespec
を利用している機能から結果を得たいなと考えたわけですが、hrtime
がまさに利用していることがわかりました。https://github.com/php/php-src/blob/33c4ca36e43cf03d7aa8eccf4493d84a6a5714eb/Zend/zend_hrtime.h#L94
PHPバージョンごとに実装がどうなっているか確認できていませんが、hrtime
を使うで間違いないでしょう。
以降のコードは、C言語が見慣れていないこともあって理解できていないところもありますが、パッと見なんとかなるんじゃないかと思いました。
(もし何とかならなかった場合、既に色々な方が公開されているPHPでのUUIDv7の生成方法を参考にしてみましょう..。)
ここまで分かればあとはPHPでの実装にトライしていくだけです。ということで調査は時間切れのためここまで。
Discussion