GVATECHブログ
🎭

なぜMySQLでUUIDv4を使わないほうがいいのか

に公開

はじめに

webサービスを設計・運用していくうえで(個人的に)最も苦労するのはデータベースの設計だと思っています。
DB自体の選定やスキーマ設計などは後から変更するのが難しく、ここでミスると負債を残したままになるので特に重要な部分だと思っています。
特に問題となるケースが、サービスのユーザーが増えて負荷が上がってきたときに問題になるパターンです。スケールアップで対応できるのは限界があり、ここで根本の設計の差が出てくるかなと思います。

いくつかポイントがあるのですが、今回はプライマリーキーにUUIDv4を使うのはバットプラクティスな理由をインデックスの仕組みを交えて解説していきたいと思います。

インデックスの仕組み

本題に入る前に、まずインデックスの仕組みについて軽く確認しておきます。
インデックスとはテーブルのデータを取得しやすい形で並べたものです。
SQL自体は柔軟なデータ表現が可能な言語ですが、実際のアプリケーションが発行するSQLは特定のクエリを実行することが多いです。
このとき、非インデックスのカラムをスキャンすると実テーブルの全てのレコードを走査することになってしまい、レコード数が多いととても負荷がかかってしまいます。
ここが厄介なところで、実装直後の新規テーブルはレコードが少ないので問題が出ることはないですが、レコードが溜まっていくと次第にレイテンシーが劣化していき(だいたい1万レコード超えたあたりから)、最終的には障害に繋がることになります。
そこでインデックスにデータ取得を代替させることにより、フルスキャンを避けることができるようになります。

と、ここまではインデックスの一般的な説明なのですが、ではインデックスを使うとなぜ速くなるのでしょうか?

今回はMySQLを例にして挙げますが、MySQLの場合アルゴリズム構造はB+Treeインデックス構造になっています(ドキュメントとか他の記事ではB-Treeとか書かれてたりしますがアルゴリズム的にはB+Treeになります)
簡易的には下の図のようになります。数字はレコードのPKです。

二分探索木アルゴリズムにのっとり、ノードの最短経路でデータを取得しにいける仕組みがあるのでどのデータに対しても必要最小限の試行回数でデータを特定できるのです。
これがテーブルスキャンだと目的のデータが見つかるまで順次スキャンになるので(スキャンはPK順)最悪の場合レコードの一番最後までスキャンすることになり、レコード数によって指数関数的に負荷が高まってしまいます。
リーフノードには実データへのポインタが格納されており、そのポインタ経由でテーブルにアクセスするので1度の読み取りで完了する仕組みです。

ページファイルとバッファプール

MySQL(ストレージエンジンがInnoDBの場合)にはバッファプールという仕組みがあります。
これはレコードを格納したページファイルという単位でメモリ上にキャッシュをする仕組みで、キャッシュヒットしているレコードに関してはディスクI/Oが発生しないというものです。
ちなみにInnoDBの場合、ページファイルのデフォルトサイズは16KBになってます(変更可能)

このページファイルはリーフノードに格納される一つのファイル。上記の図で言うと1~25という箇所に格納されます。
ページの概念はドキュメントに詳しく書かれています。
https://dev.mysql.com/doc/refman/8.0/ja/glossary.html#glos_page

UUIDv4をプライマリーキーに使った時の挙動

MySQLでは基本的にauto incrementによる採番をPKとして使うケースが多いと思いますが、ランダム性のあるIDをPKとして使いたいというユースケースもあるかと思います。
例えばauto incrementの採番は連続性を持たせるために非同期で発番ができないので、大量のinsertが発生した場合に待ちが発生してパフォーマンスの劣化につながります。
また、パスパラメータに数字のIDを入れてAPIに渡してそれをPKとしてテーブルから引っ張ってくる場合、数字だと類推されやすいのでセキュリティー的な問題もあるかなと思います。

ランダム性の高いUUIDv4をPKとして使った場合、以下のようにデータが格納されます。

連番の数字をPKにしてる場合は端から端までノードが昇順で並んでいてinsert時には一番端のノードに追加するだけで完了します。
しかしUUIDv4の場合は完全にランダムな数字を格納することになるので、どこのノードにデータが追加されるかが予測できません。
ページファイルが上限を超えると分割が行われ親ノードの再構成が走りますが、このコストが大きいのでパフォーマンスが劣化することになります。

また読み取り時のパフォーマンスも劣化する可能性があります。
インデックスが効くレコード数が多いテーブルは大抵の場合、時系列でデータが追加されていくケースが多いと思います。
このとき、新しいデータほどアクセスが多いケース(ニュースやお知らせなどは新しいものが取得率が高い)の場合ページファイルがキャッシュに乗りやすくなりパフォーマンスが向上します。
しかしUUIDv4の場合はランダムなのでキャッシュヒット率が下がってしまい、結果として読み取りパフォーマンスが劣化します。

ここで挙げられてるケースはいずれもレコード数が多くなった場合なので、小さいテーブルに関していうとそこまで差はありません。
ですがサービスがグロースしてデータが増えてきたときに効果を発揮するので、普段からこういった仕組みの部分を意識しているとよいでしょう。

UUIDv7を使おう

ちなみに先頭部分にタイムスタンプが埋め込まれているUUIDv7を使えば上記の問題はほぼ解決します。
どうしてもランダム採番したPKを使いたい場合はUUIDv7を使う方針が良いかなと思います。

GVATECHブログ
GVATECHブログ

Discussion