❤️

連番IDを使うと会社が潰れる。(訳: 連番とUUIDのベンチマークを取ってみた❤️)

2024/10/06に公開2

大いなる流れには逆らえない

あるAI研究者が言っていた、私の仕事もいつか AI に奪われるという言葉が非常に印象的だった。
私は一時期自分のキャリアに危機感を覚えAIに関する情報を集めていた。そのとき見つけたYoutube動画でこのようなことが語られていたのである。
ではなぜ彼らは研究を続けるのかと思うかもしれないが、個人や一団体がそれを放棄したところで世の中のイノベーションの流れを止めることは不可能だろう。
平和を望む国々も兵器開発をやめられないのと似たようなものだ。

私がこの記事のタイトルを思いついたとき、つい溜息が出た。あまり楽しくない思い出があるからだ。
ただ、思いついてしまった以上これを世に出さないわけにもいかず、血の涙を流しながらこの記事を書いている。
私というちっぽけな存在では、この大宇宙の大いなる流れには逆らえないのだ。

申し遅れました。私、YadaYadaKonnanYadaと申します。以後お見知り置きを。
よかったらZenn, Twitterのフォローよろしくお願いします。
https://x.com/uncode_jp

本題

さて、Youtube といえば、最近 Twitter であるYoutube動画(ムーザルチャンネル様)が少し話題になっていた。
UUID を主キーにすると遅い?
このような話題は昔から頻繁に議論されており、結論 UUID の方が遅くなるのだが、実際にどの程度遅くなるのか実際のベンチマークを見ると大変興味深かった。
(この動画では図やグラフを交えながら視覚的に RDBMS の仕組みについて解説されているので大変わかりやすい。)
そしてその流れであるツイートを目にした

要約すると

データベース設計の現代的アプローチ:

  • 主キー:連番整数
  • 副キー:適度な長さの乱数文字列(例:12-16 文字)
  • 外部公開用には UUID より短い副キーを使用(主にセキュリティの問題で連番は使わない)
  • UUID は空間効率が悪く、多くの場合オーバーキル
  • 副キーの長さは想定レコード数に基づいて選択

このアプローチはパフォーマンス、スケーラビリティ、使いやすさのバランスが良い。

そして私は上の動画とこのツイートを見て素朴にある疑問が浮かんだ。

「連番IDを使うと会社が潰れるのではないか?」(タイトル回収)

まあ本当のところは

  1. UUID v4 を前提としているが、v7 の場合はどうか。
  2. 連番主キー とランダム文字列(Unique index)はUUIDを主キーとして使用する場合と比べてどの程度パフォーマンスに差があるのか

今回はこれらについて実際に検証していく。
結論を先に言うと、大変恐れ多いがこのツイートとは少し異なる結論に至った。(何かケチをつけようとかそういう意図は無いです。)
ただし、このツイートの前提がわからないのと、この選択の違いが実際に問題になるのはそれなりに大規模になってからなので、多くの人にとってはあまり影響が無いかもしれない。実際このツイートのようなことをしているサービスは沢山あるだろう。
あくまで、2024年現在で最も効率的なのはどれかという話をしたい。   
ちなみに私はDatabaseのパフォーマンス計測に関して特に詳しいわけではないので、検証結果を見ても何が影響しているのか正直よくわからない部分があった。

以下は今回記事を書くにあたって参考にした記事
https://zenn.dev/mpyw/articles/rdb-ids-and-timestamps-best-practices
https://zenn.dev/s3i7h/articles/12f25ca61fe02a
二番目はTwitter相互フォローの方の記事です。大変ありがとうございます。
より高度なことが知りたければこれらを読むといいと思います。

IDにまつわる議論

ID関係には色々な議論がある。
例えば、主キーは(UUIDであっても)外部に晒すべきではないという意見がある。
これはシステムの移行等で主キーを変えたくなる場合に困るということ、また、攻撃者にヒントを与えるというセキュリティ上の理由もあるらしい。
これに関しては、連番の場合はその通りだが、UUIDには当てはまらないのではないかと思っている。
システム移行に関しては連番はDB依存で重複の可能性があるが、UUIDはそうではない。
セキュリティに関しては調べても強い根拠を見つけられなかった。
また、emailやTwitterのID等のあとから変更される可能性のあるものはたとえユニークでも主キーにすべきでない。

前提知識の説明

詳細はムーザルチャンネルの動画に譲るが、今回のベンチ結果を読むにあたって最低限知っておくことを説明する。

単調増加IDは速い

プライマリキーに単調増加の値を使うとDBの各場所で偏りが生じる。
偏りというと良くなさそうだが、RDBMSにおいては有利に働く。
以下AIにまとめさせたものから重要なものを抜粋した。

  1. インデックスの効率性:
    B-treeインデックスが最適に機能します。
    新しい値が常に最後に追加されるため、インデックスの再構築が最小限に抑えられます。
  2. データの物理的な配置:
    新しいレコードが常にテーブルの末尾に追加されるため、ディスクI/Oが最小化されます。
    これにより、データの断片化が減少し、読み取り性能が向上します。
  3. キャッシュの効率:
    最近挿入されたデータは通常最も頻繁にアクセスされます。
    単調増加のIDを使用すると、これらのデータがキャッシュに残りやすくなります。

単調増加IDに該当するものとして、

  1. 連番(Serial型)
  2. UUID v7
  3. Snowflake ID

等がある。UUIDv4は完全にランダムなので均一に分布するが、v7,Snowflakeは先頭にタイムスタンプを含むのでソート可能。
また、Snowflakeは8byteなので16byteのUUIDよりも小さいが、衝突確率は十分小さい。
(今回大量のバルクインサートを行ったので割と衝突が発生した。ミリ秒時間が一致するとまあまあ衝突するっぽい。)

実際にやってみた

検証に使ったリポジトリはこちら
https://github.com/ydydsnyd/postgresql-uuidv7-bench

今回は、PostgreSQLで大量のレコードをInsertした場合、パフォーマンスにどの程度差が出るのかを検証した。Select等は今回は検証しない。
また、差が出やすいように、共有バッファサイズを32MBに制限して意図的にDiskIOが発生しやすいようにした。
ちなみにPostgreSQLはUUID型をネイティブでサポートしているが、MySQLはそうではないので結論が変わる可能性がある。
Serialと書いているがなるべく条件を揃えるため実際はBigSerialを使った。

今回検証したのは

  1. Serial(8byte)
  2. Serial(8byte)とVarchar(21)(88byte,Unique)
  3. UUIDv4(16byte)
  4. UUIDv4(16byte)とTimestamp(8byte,Index)
  5. UUIDv7(16byte)
  6. Snowflake(8byte)

の6パターン
まず前提として、Webサービスではセキュリティの関係で連番はURLに使いたくない。
また、webサービスだと作成順でソートすることがよくあるので、これらを踏まえて実用的な設計パターンをいくつか用意してどの程度差がでるのか比較したかった。(Varcharを21文字にしたのは3000万件規模だと何度も衝突が起きたため)
そうすると、2,4,5,6が実用的だと考えられる。1,3は参考のため計測した。

これらのパターンで、100万件ずつ30回のバルクインサートを行い計3000万行を挿入し、ミリ秒単位で計測してみた。
すごく大雑把なやり方だがやってみると有意な差が生まれたのでとりあえずOKとしたが、問題があれば指摘していただきたい。

仮説

直感的に速い順に以下の順序になると考えた

  • Serial = Snowflake < UIIDv7 << UUIDv4 << Serial+文字列 < UUIDv4+時間

シンプルに、単調増加、サイズが小さいものが有利なはずと考えた。
記号の意味は、<が一つの場合は実用上あまり問題にならない程度の差で、2つだと場合によっては問題になる程度という意味。
恐ろしく曖昧な表現だが、カジュアルな検証なので許して。

結果

レコード数 Serial Serial+文字列 UUIDv4 UUID4+時間 UUIDv7 Snowflake
1000000 1996 5182 2975 3644 1558 1445
5000000 1883 12344 8520 9366 1523 1751
10000000 1817 16766 10892 11120 1841 2193
20000000 1906 38472 23369 28802 1845 2391
30000000 1828 54026 40352 41730 2460 2268

本来は30件あったが見づらいしカジュアルな検証なので途中の行を省略した。
まず順位は

  • 仮説: Serial = Snowflake < UIIDv7 << UUIDv4 << Serial+文字列 < UUIDv4+時間
  • 結果: UUIDv7 < Serial < Snowflake << UUIDv4 < UUIDv4+時間 < Serial+文字列

となった
大雑把に言えば大体予想通りだが、UUIDv7が一番だったのが意外だった。
計測方法に問題があったのだろうか?あまり有意な差ではない?理由が気になる。
とはいえ、上位3つは大きな性能劣化も見られないので、多くの場合実用上は問題ない程度の差しかないだろう。
そしてvarcharのインデックスは性能劣化がかなり大きい。とはいえ、これも問題になるかは規模次第だが。

この先は君の目で確かめてくれ!

正直よくわからない部分もあったが、結論としてUUIDv7は超優秀でした。
たしかにUUIDは長く多少扱いづらいが、Base64Urlエンコードすれば22文字まで短縮可能(わざわざやるかは置いといて)だし、そこまで問題にならない気がする。
ランダム文字列を別で用意するのは連番のパフォーマンス上のメリットを打ち消してしまい、冗長なカラムが増えるのも微妙かなと思った。
みなさんはどう考えますか?よかったらコメントください。

ただし繰り返しになるが、この選択の違いが実際に問題になるのはそれなりに大規模になってからなので、多くの人にとってはあまり意味をなさないかもしれない。
実際、連番+ランダム文字列を用いているサービスは沢山あるだろう。UUIDv7が比較的新しくまだそこまで普及してないのもあると思うが。
この検証ではあくまで、2024年現在で実用上最も効率的なのはどれかという話をしたかった。

もし興味があったらみなさんも是非計測してみてください❤️

Discussion

ゆうたゆうた

直感的に速い順に以下の順序になると考えた
Serial = Snowflake < UIIDv7 << UUIDv4 << Serial+文字列 < UUIDv4+時間

速い順と書いていて< 記号で表現されると右側に行くほど速いと表現しているのか、左側に行くほど速いのか分かりづらい気がしました。

「処理時間が短いという意味」と「優位であるという意味」が逆の意味として捉えられる気がするので。不等号が逆向きであれば違和感は無い気がしました。

Otogawa Katsutoshi(oto)Otogawa Katsutoshi(oto)

serial+文字列のDB処理は仕事で見かけてもかなり遅いなぁって思ってたが、やっぱかなり遅かった。
採用するメリットそんなあったんだろうか?
これ。