700 * 10 ^ 6行のテーブルからランダムレコードをサンプリングする

私たちの何人が実際にWeb開発者として普通の会社で働いて、この空想的な「ビッグデータ」という言葉に出くわしましたか? むしろ、あなたは私たちと同じように、同じCMS上で毎日同じサイトを開発します。多くの場合、そのパフォーマンスについて考えることさえしません。







しかし、Web開発者の生活の中で、そのような日は、顧客が興味深いタスクで到着します。 コーヒーを注ぎ、キーボードから猫を遠ざけ、インスピレーションを得てデザインを始めます。







これは、2人の意欲的なWeb開発者が「ビッグデータ」を処理するという課題に最初に直面した経緯に関する物語です。







画像









それで、顧客は何を望んでいますか



ログインの有限セットがあります。 新しいユーザーごとに、このセットからランダムログインを生成する必要があります。 ログインは各ユーザーに対して一意である必要があります。テンプレートXX999999に従って形成されます。Xは英語のアルファベットの文字、9は0〜9の数字です。







この問題は、Apache(PHP 5.6)およびMySQLデータベースで実行される既存のサイトで解決されました。







災害の大きさ



まず、ログインを生成するアルゴリズムを作成し、災害の規模を評価する必要がありました。

生成アルゴリズム自体は非常に単純です。







$alphabet = range('A', 'Z'); $alphabetLength = count($alphabet); for ($i = 0; $i < $alphabetLength; $i++) { for ($j = 0; $j < $alphabetLength; $j++) { $arLogins = []; for ($k = 0; $k < 1000000; $k++) { $k = strval($k); $arLogins[] = '("' . $alphabet[$i] . $alphabet[$j] . str_pad($k, 6, '0', STR_PAD_LEFT) . '")'; } // insert 1 000 000 by single query $strSql = "INSERT INTO logins VALUES " . implode(',', $arLogins); $DB->Query($strSql); } }
      
      





実際、約7億回のログインが発生することが判明しました。 したがって、それらをオンザフライで生成するオプションはここでは機能しません。







オンザフライ生成アルゴリズム

最も簡単なアルゴリズムは、特定のテンプレートを使用してランダムログインを生成し、そのログインを持つユーザーが既に存在するかどうかを確認することです。 存在する場合は、無料ログインが見つかるまで、次のものを生成します。

このアルゴリズムには、大量のデータに関する明らかな問題があります。







  • ユーザー数は増加しており、無料ログインの数は減少しています。 これは、サーバーが応答を受信する時間とともに、無料ログインを選択するための反復回数が増加することを意味します
  • データベースへのクエリの数も絶えず増加します。


そのため、それらをどこかに覚えておく必要があります。この目的には、データベース内の別のテーブルが最適です。 祝うために、テーブルを作成し、そこでログインを生成し、単一のフィールドPRIMARY



作成しました。 結果は、このような単純なテーブルです。







価値
AA000000
AA000001
AA000002
...


そして、ユーザー名はランダムに選択する必要があることを思い出しました。







最初のステップ



もちろん、最初に試したのは、よく知られているORDER BY RAND() LIMIT 1



です。 結果は待ち遠しく、サーバーにいつまでも別れを告げることができました。 この場合、インデックスの存在はまったく役に立ちませんでした。







 SELECT `value` FROM `logins` ORDER BY RAND() LIMIT 1;
      
      





どうする



「何をすべきか」という質問に対する答えをGoogleから見つけ出すときです。







Googleが最初に提供するのはORDER BY



使用した最適化方法ですが、データベースに数千のエントリがある場合にのみクールで生産的であるため、これは適切ではありません。 JOIN



とサブクエリを使用した最適化メソッドがいくつかありましたが、同じ理由で機能しませんでした。







これらのメソッドはすべて、クエリの実行時間を500ミリ秒から50ミリ秒に最適化する場合を対象としています。この例では、リクエストの実行中に10分でサーバーを削除しませんでした。







ただし、これはすべて正直に試行され、stackoverflowは最初から最後までテストされましたが、リクエストが実行されるまで待たなかったため、パフォーマンスの向上を判断できません:)







最初のリンクは、すべてのランダム化作業をPHPサーバーの側に置くことを提案しています-最小および最大IDを選択し、それらの間で乱数を生成し、出来上がりです-ランダムなレコードIDがあります。







 SELECT MAX(`id`) FROM `logins`; SELECT `value` FROM `logins` WHERE `id` = <random id>;
      
      





素晴らしいオプションであり、完全に高速に動作するはずです。 ただし、各レコードに整数IDを追加する機能はありません。テーブルは既に20 GBを超えており、サーバーリソースはゴムではありません。 はい。また、そのような機会があったとしても、ログインは一意でなければなりません。つまり、ユーザーに別のログインを与えると、テーブルからすぐに削除する必要があります。 すでに存在しないログインを見つけたらどうなりますか? 膨大な数のサイクルでオプションに戻ります。







次に試行されるオプションは、ランダムなOFFSET



およびLIMIT 1



です。 OFFSET



値はPHPサーバーを生成し、それをリクエストに置き換えます。 MySQL側ではソートとランダム化は行われていないように見えますが、 OFFSET



自体はそれほど単純ではありません。 オフセットに大きな値を生成する場合、MySQLは最初にすべての行をオフセットまで反復してから、目的の行のみを返します。 並べ替えは少し改善されますが、一般的には、結果を非常に長い時間待つことができます。 また、レコード数の選択はそれほど迅速な操作ではありません。







 SELECT COUNT(*) FROM `logins`; SELECT `value` FROM `logins` OFFSET 123456789 LIMIT 1;
      
      





新しいアプローチ



記載されているすべての方法は、すべての新規ユーザー登録で機能するはずでした。登録が遅くなり、穏やかに言えば、肯定的なユーザーエクスペリエンスが完全に失われました。 一度だけ動作し、ユーザーのパフォーマンスに影響を与えない方法の方向に考える価値がありました。 オフセットとソートなしでテーブルの最初の行をフェッチするとすぐに動作します。データが最初にランダムな順序でテーブルに保存された場合、結果はタスクの必要に応じてランダムになります。 それらをどのようにミックスするかを決めるのは残っています。







データベース側とPHP側の2つのオプションがすべて思い浮かびます。







「さて、MySQLでランダムソートを実行できますが、ユーザーは待たないでしょう」と私たちは考えました。 テーブルの空のコピーを作成し、クエリを実行します。







 INSERT INTO `new_logins` (SELECT * FROM `logins` ORDER BY RAND());
      
      





しかし、そこにはありませんでした。 すべてのレコードをランダムな順序でソートするために、MySQLはそれらをすべてRAMにアンロードし、その後でのみソートします。すでに説明したように、サーバーリソースは制限されています。 はい。このようなリクエストがある稼働中のサーバーで8時間のベースを使用したくありません。

次に、PHPで並べ替えを試してくださいset_time_limit(0)



+ consoleを試してみてください。 生成アルゴリズムは、データベースへの1クエリあたり100万レコードの挿入を意味しました。







メモリはそれほど占有しません。100万件のレコードをランダムにソートし、この順序でリクエストに挿入できます。 しかし、ここで私たちは完璧主義に屈しました-分布は均一とはほど遠いでしょう。 さて、さらに見てみましょう、完全な絶望の場合にはこれを残してください:)







NoSQL氏



NoSQLに向かって考え始めました。 しかし、他の商用プロジェクトと同様に、実装の時間は悲惨なほど短く、テストの時間はさらに短かった。 NoSQLリポジトリを使用した実際的な経験はありませんでしたが、どれほどパフォーマンスが向上したとしても、推測することしかできませんでした。 クエリの実行時間が10分から1分に短縮されたことを確認するのは、締め切りの日に不快です。 したがって、この考えは捨てられなければなりませんでした。







誰かが同等のデータ量でNoSQLストレージを使用した経験がある場合は、コメントを共有してください。







トンネルの終わりの光



長い実験と検索を通じて、解決策が見つかりました。 MySQLから何かを達成しようとすることは役に立たず、NoSQLを使用することは不可能であることが明らかになりました。







しかし、PHPサーバー側で値をランダム化してオプションに戻るとどうなりますか? varchar(8)



フィールドとPRIMARY



インデックスがあります。 ランダムログインを生成してデータベースから選択することはできません。これは、「ホール」(すでに削除されたログイン)に陥る可能性とその後のループのためですが、文字列を比較することはできます。 LIMIT 1



を追加しながら、ランダムなログインを生成し、それより大きいものを選択してみませんか? ここのインデックスは選択をスピードアップするのに役立ちます。 試してみます-今度は結果がすぐに届き、1秒もかからずに目的のレコードを取得します。 PHPサーバーが生成したログインがテーブルの最後である場合-極端な場合を1つ除外するだけです。 次に、空の結果を取得し、1つの追加リクエストで最初の順番のログインをテーブルから選択します。







 function generateRandomLogin() { $alphabet = range('A', 'Z'); $firstLetter = $alphabet[mt_rand(0, count($alphabet) - 1)]; $secondLetter = $alphabet[mt_rand(0, count($alphabet) - 1)]; $number = mt_rand(0, 999999); return $firstLetter . $secondLetter . $number; } function createLogin() { $randomLogin = generateRandomLogin(); $newLogin = $DB->Query('SELECT * FROM `logins` WHERE value > "' . $randomLogin . '" LIMIT 1')->Fetch(); if ($newLogin) { // if login was found delete it from database $DB->Query('DELETE FROM `logins` WHERE `value`="' . $newLogin['value'] . '"'); return $newLogin['value']; } // if login was last in table, select first $newLogin = $DB->Query('SELECT * FROM `logins` LIMIT 1')->Fetch(); if (!$newLogin) { throw new \Exception('All logins are already used'); } $DB->Query('DELETE FROM `logins` WHERE `value`="' . $newLogin['value'] . '"'); return $newLogin['value']; }
      
      





おわりに



そのような場合に常に起こるように、今見つかった解決策は明らかであるように見えます、あなたの多くにとってそれは最初からそう見えるでしょう。 しかし、初めて同様の問題に直面したとき、この方法でこの決定に到達する必要がありました。








All Articles