laravel

【laravel】N+1問題について考える

こんにちは、らいか(@hideiwa1)です。

今回は、DBを扱う上で注意するべき「N+1問題」について解説します。

N+1問題とは

リレーションを使用する場合、関連するモデルには「遅延読み込み」という特性があります。
「遅延読み込み」とはどういう事かというと、

プロパティにアクセスされるまで、データを取得しない

つまり、リレーションをしただけではクエリを実行しないという事です。

実際のコードで、どのような事が起きるのかを見てみましょう。

「User」と「Item」が「1対多」でリレーションしているとします。
このとき、すべてのuserとitemを取得すると、以下の通りになります。


  $user = User::all();

  foreach($users as $user)
  {
      $user->items;
  }

この時、どのようなクエリが実行されているかというと、


  select * form Users;
  
  select * from Items where Items.user_id = 1 and Items.user_id is not null;
  select * from Items where Items.user_id = 2 and Items.user_id is not null;
  …

このように、

・まず「users」テーブルより全件を取得
・次に、user_id毎に「Items」テーブルから一致するものを取得

という流れになっています。

この時、「users」が全10件あったとすると、

・「users」テーブルより全件を取得 → 1回
・user_id毎に「Items」テーブルから一致するものを取得 → 1~10で10回

合計で11回、クエリを実行することになります。
これが「N+1問題」となります。
(10件を取得するのに、11回クエリを実行する)

解決方法は?

正直なところ、対象が10件程度でしたらそれほど影響はありません。
しかし、これが100件、1000件と増えた場合はどうでしょうか?

クエリの数が多くなれば、その分処理速度が低下することになります。
そのため、基本的にN+1問題は避けなければなりません。

N+1問題の解決方法は、「with」メソッドを使用する方法です。

withメソッドには、「Eagerロード(積極的読み込み)」という特徴があります。
Eagerロードでは、データが事前に取得されます。

先ほどと同じ例を使って、流れを見てみましょう。


  $user = User::with('item')->get();

  foreach($users as $user)
  {
      $user->items;
  }

この際に実行されるクエリは、


  select * form Users;
  
  select * from Items where Items.user_id in (1, 2, ・・・);

このように、

・まず「users」テーブルより全件を取得
・次に、「Items」テーブルから全件を取得

と、2つへ削減されました。
そして、件数が増えてもクエリが増えることはありません。

まとめ

リレーションをする場合は、「with」メソッドを使用する方が良い。