PHP Manual

UUIDと大規模アプリケーションのパフォーマンス

08. 11. 2019

データベースのサイズが数百万行を超えるようになったら、アプリケーションのスケーリングを開始し、データベースを複数の物理サーバーに分割することが推奨されます。

データベースを複数に分割した場合の最大の問題は、ユーザーが特定のデータを要求した場合のその後の同期である。

UUIDを使用する理由と、autoincrementと比較した場合の利点について

例えば、articlesというテーブルがあったとして、巨大なサイトであるため、上位には数千万件の記事があり、物理的に複数のマシンに分けて表示する必要があるとします。

もし、普通の整数をid`(主キー)として、autoincrementの設定で使うと、分散して異なるマシンでレコードを作成し、それを同期させるときに、idの衝突が起こり、複雑な方法でレコード番号を変更しなければならないことがすぐに分かってしまうでしょう。さらに、多くのセッションを他のテーブルに解決する場合、これは非常に複雑なオーバーヘッドとなり、ミスを犯しやすくなります。

そこで、数字の識別子の代わりに、複数のマシンで独立に生成しても一意になることが保証される複雑なアルゴリズムで生成されたテキスト文字列であるUUIDを生成することができるようにした。

メリット

  • 独立した複数のデータベースを同期させる場合、UUIDを使用することで、1つのIDがすべてのデータベースで一意になり、あなたがいる場所とそのIDが生成された場所だけでなく、すべてのデータベースで一意になることを意味します。1つのクラスタに統合しても、コンフリクトは発生しない。
  • 実際にデータベースにレコードを挿入する前に、「プライマリーキー」を知ることができるのです。これにより、SQLクエリの数が減り、トランザクションロジックが単純化され、レコードコレクションが存在する前に外部キーとして簡単に使用することができます。
  • UUIDは日付や配列の数などの情報を開示しないので、URLで使うには安全である。 例えば、自分がユーザー19010018であることがわかれば、ユーザー19010017なども存在することが容易に推測される。この攻撃はベクトル攻撃と呼ばれる。

新しいUUIDを生成する

UUIDは単純なSQLクエリ SELECT UUID(); によっても取得できるが、これではデータベースへの問い合わせ回数が増え、アプリケーションロジックで最初にまとめてデータを用意し、それを一気に書き込むということができなくなる。

そこで、良い解決策として、Composerで取得したramsey/uuidパッケージを使いたいのです。UUID自体にはいくつかのバージョンがあり、パッケージは必要に応じてあらゆる種類のものを遊び心で生成することができます。

そのため、使い勝手が良い。

require 'ベンダ/autoload.php';
use Ramsey\Uuid\Uuid;
// バージョン 1(時間軸)の UUID オブジェクトを生成する。
$uuid1 = Uuid::uuid1();
echo $uuid1->toString() . "\n"; // e4eaaaf2-d142-11e1-b3e4-080027620cdd
// バージョン 3 (名前ベースと MD5 のハッシュ化) の UUID オブジェクトを生成します。
$uuid3 = Uuid::uuid3(Uuid::NAMESPACE_DNS, 'ピーピーネット');
echo $uuid3->toString() . "\n"; // 11a38b9a-b3da-360f-9353-a5a725514269
// バージョン4の(ランダムな)UUIDオブジェクトを生成する
$uuid4 = Uuid::uuid4();
echo $uuid4->toString() . "\n"; // 25769c6c-d34d-4bfe-ba98-e0ee856f3e7a
// バージョン 5(名前ベース、SHA1 ハッシュ)の UUID オブジェクトを生成する
$uuid5 = Uuid::uuid5(Uuid::NAMESPACE_DNS, 'ピーピーネット');
echo $uuid5->toString() . "\n"; // c4a760a8-dbcf-5254-a0d9-6a4474bd1b62

Doctrine を使っている場合、ID をデータ型として直接生成する拡張機能 ramsey/uuid-doctrine があります。

データベース内の物理ストレージ

最初の試みでは、主キー(ID)として varchar(36) を使いましたが、それは全く良いアイデアではありません

内部ロジックの説明:

MySql データベース (そして他の多くのデータベース) では、varcharchar などの文字列を表すデータ型を主キーとして効率的に使用することができません。 いくつかのデータベースでは、UUID を直接保存するための GUID データ型が存在します。この型が使えない場合は、binary(16)という形で適当な代用品があります。

データベースを物理的に調べる場合、IDはHEX形式で表現されます(バイナリ形式は表示できないため)。素敵なID 726c67c4-e5eb-4a4c-8fcc-031da5d6f3c6 の代わりに、INSERTクエリの '?kYߟKg2c;' のように見える726C67CE5EB4A4C8FCC031DA5D6F3C6が表示されているだけでしょう。

元データを varchar(36) から binary(16) に変換する。

新しく設定されたIDをデータベースで表現している(または表現する予定)のでしょうね。

`id` binary(16) NOT NULL

ただし、データ型を変えるだけではうまくいかないので、次のようなものを。

SET FOREIGN_KEY_CHECKS=0;
ALTER TABLE article CHANGE id id BINARY(16) NOT NULL
SET FOREIGN_KEY_CHECKS=1;

理由は基本的に2つあります。

  • 主キーとそれに対するセッションは 同じデータ型でなければなりません。そのため、論文IDのデータ型と、例えば、論文と著者をマッチングするリレーショナルテーブルの両方を変更する必要があります。
  • バイナリ形式は、元の文字列とは少し異なるものを含んでいます。変換関数を使用する必要があります。

したがって、唯一の正しい解決策は、データをバックアップし(ただし、いずれにせよ各移行の前にこれを行う必要があります)、機能リレーションのある空のデータベースを準備し、移行によってそこにデータを再び置くことです。

以前、UUIDを変に生成してしまった場合は、何か順次取得する方法を選び、すべてのレコードの番号を変更した方がよいでしょう。理由は、シーケンシャルレイアウトによって、値の順序付けがうまくいき、 btree を作成できるため、bigint とほぼ同じ性能になるからです。

既存のデータベースを、複雑なマイグレーションを考案することなく、外部キーを保持したまま、UUIDをvarcharで保存したものからバイナリ形式に変換する良い方法をご存知でしたら、フィードバックをいただけると幸いです

Jan Barášek   Více o autorovi

Autor článku pracuje jako seniorní vývojář a software architekt v Praze. Navrhuje a spravuje velké webové aplikace, které znáte a používáte. Od roku 2009 nabral bohaté zkušenosti, které tímto webem předává dál.

Rád vám pomůžu:

Související články

1.
2.
Status:
All systems normal.
2024