GraphQL APIはPHPとMySQLで作成します。 パート3:N + 1クエリの問題を解決する

画像



graphql-phpを使用してGraphQLサーバーを作成するこの3番目の記事では、N + 1クエリの問題に対処する方法について説明します。



まえがき



前の記事の最後に取得したコードを引き続き変更します。 Githubの記事リポジトリでも確認できます 。 以前の記事を読んでいない場合は、続行する前にそれらを読むことをお勧めします。



また、graphql.phpの2行をコメントアウトします。これらの行は、開発時にはあまり必要ではありませんが、デバッグの問題を引き起こす可能性があるため、リクエストの最大の深さと複雑さに制限を加えます。



// DocumentValidator::addRule('QueryComplexity', new QueryComplexity(6)); // DocumentValidator::addRule('QueryDepth', new QueryDepth(1));
      
      







問題N + 1クエリ



問題



N + 1クエリの問題を説明する最も簡単な方法は、例です。 記事とその著者のリストをリクエストする必要があるとします。 ためらうことなく、次のようにすることができます。



 $articles = DB::table('articles')->get(); foreach ($articles as &$article) { $article->author = DB::table('users')->where('id', $article->author_id)->first(); }
      
      





原則として、 DB::table('articles')->get()



最終的に次のような1つのリクエストをデータベースに送信します。



 SELECT * FROM articles;
      
      





そして、別のN個のクエリがループでデータベースに送信されます。



 SELECT * FROM users WHERE id = 1; SELECT * FROM users WHERE id = 2; SELECT * FROM users WHERE id = 3; SELECT * FROM users WHERE id = 4; SELECT * FROM users WHERE id = 5; ... SELECT * FROM users WHERE id = N;
      
      





Nは、最初のリクエストで受信した記事の数です。



たとえば、100件の記事を返すリクエストを1つ実行し、記事ごとに著者からもう1つのリクエストを実行します。 合計で、100 + 1 = 101のリクエストが取得されます。 これは、データベースサーバーの余分な負荷であり、N + 1クエリ問題と呼ばれます。



解決策



この問題を解決する最も一般的な方法は、クエリをグループ化することです。



クエリのグループ化を使用して同じ例を書き換えると、次のようになります。



 $articles = DB::table('articles')->get(); $authors_ids = get_authors_ids($articles); $authors = DB::table('users')->whereIn('id', $authors_ids)->get(); foreach ($articles as &$article) { $article->author = search_author_by_id($authors, $article->author_id); }
      
      





つまり、次のことを行います。



  1. 記事の配列をリクエストする
  2. これらの記事のすべての著者のIDを覚えておいてください
  3. これらのIDでユーザーの配列をリクエストします
  4. 多数のユーザーからの記事に著者を挿入する


同時に、リクエストする記事の数に関係なく、データベースに送信されるリクエストは2つだけです。



 SELECT * FROM articles; SELECT * FROM users WHERE id IN (1, 2, 3, 4, 5, ..., N);
      
      







GraphQLのN + 1クエリの問題



それでは、前回の記事の後の状態でGraphQLサーバーに戻り、ユーザーフレンドの数のクエリがどのように実装されているかに注目しましょう。



それぞれの友人の数を示すユーザーのリストをリクエストする場合、最初にGraphQLサーバーはuserテーブルからすべてのエントリをリクエストします:



 'allUsers' => [ 'type' => Types::listOf(Types::user()), 'description' => ' ', 'resolve' => function () { return DB::select('SELECT * from users'); } ]
      
      





そして、ユーザーごとに、友人の数をデータベースに照会します。



 'countFriends' => [ 'type' => Types::int(), 'description' => '  ', 'resolve' => function ($root) { return DB::affectingStatement("SELECT u.* FROM friendships f JOIN users u ON u.id = f.friend_id WHERE f.user_id = {$root->id}"); } ]
      
      





これが、N + 1クエリの問題が発生する場所です。



クエリをグループ化してこの問題を解決するために、 graphql-phpは、他のすべての(遅延されていない)フィールドが受信されるまでそのようなフィールドのリゾルバーの実行延期します。



アイデアは単純です。結果の代わりに、フィールドの「解決」関数は、GraphQL \ Deferredクラスのオブジェクトを返す必要があり、そのコンストラクターに関数を渡して結果を取得します。



つまり、Deferredクラスを接続できるようになりました。



 use GraphQL\Deferred;
      
      





そして、「countFriends」フィールドのリゾルバを次のように上書きして、実行を延期します。



 'countFriends' => [ 'type' => Types::int(), 'description' => '  ', 'resolve' => function ($root) { return new Deferred(function () use ($root) { return DB::affectingStatement("SELECT u.* FROM friendships f JOIN users u ON u.id = f.friend_id WHERE f.user_id = {$root->id}"); }); } ]
      
      





ただし、リクエストの実行を延期するだけで、N + 1の問題は解決しません。 したがって、友人の数を要求する必要があるすべてのユーザーのIDを蓄積するバッファーを作成する必要があり、将来的にはすべてのユーザーの結果を返すことができます。



これを行うには、3つの単純な静的メソッドを持つ小さなクラスを作成します。





また、このクラスを便利な方法で実装することもできます。特定の例のコードのみを示します。



アプリ/ Buffer.php
 <?php namespace App; /** * Class Buffer * *    * * @package App */ class Buffer { /** *  id  * * @var array */ private static $ids = array(); /** *        * * @var array */ private static $results = array(); /** *           */ public static function load() { //     ,     if (!empty(self::$results)) return; //          $rows = DB::select("SELECT u.id, COUNT(f.friend_id) AS count FROM users u LEFT JOIN friendships f ON f.user_id = u.id WHERE u.id IN (" . implode(',', self::$ids) . ") GROUP BY u.id"); foreach ($rows as $row) { self::$results[$row->id] = $row->count; } } /** *  id    * * @param int $id */ public static function add($id) { //   id    ,     if (in_array($id, self::$ids)) return; self::$ids[] = $id; } /** *       * * @param $id * @return int */ public static function get($id) { if (!isset(self::$results[$id])) return null; return self::$results[$id]; } }
      
      





バッファをUserType.phpに接続します。



 use App\Buffer;
      
      





そして再び、「countFriends」フィールドのリゾルバーを書き換えます。



 'countFriends' => [ 'type' => Types::int(), 'description' => '  ', 'resolve' => function ($root) { //  id    Buffer::add($root->id); return new Deferred(function () use ($root) { //       (     ) Buffer::load(); //       return Buffer::get($root->id); }); } ],
      
      





できた リクエストを実行するとき:



GraphQL:友人を持つユーザーのリクエスト



すべてのユーザーの友人の数は、データベースから一度だけ取得されます。 さらに、このようなクエリGraphQLを使用しても、友人の数に関するデータの要求は1回だけ実行されます。



GraphQL:フレンドの数を持つネストされたユーザークエリ



もちろん、この形式では、バッファは非常に特殊化されています。 別のフィールドでは、別のバッファを作成する必要があることがわかります。 しかし、これは単なる例であり、ユニバーサルバッファを作成することを妨げるものはありません。たとえば、キーによって異なるフィールドのデータを格納し、結果を取得する関数を引数として受け取ります。 同時に、バッファはデータベースからだけでなく、何らかのAPIからもデータを受信できます。



おわりに



以上です。 そのような問題を解決するためのオプションを提案し、問題が発生した場合は質問してください。



Githubの記事のソースコード



この記事の他の部分:



  1. インストール、回路図、クエリ
  2. 突然変異、変数、検証、安全性
  3. N + 1クエリの問題を解決する



All Articles