見出し画像

[laravel8]N+1問題

前回の記事でも出てきた

記事一覧ページ

resources/views/posts.blade.php

<x-layout>
  @foreach($posts as $post)
      <article>
          <h1>
              <a href="/posts/{{ $post->slug}}" >
                  {{ $post->title }}
              </a>
          </h1>

          <p>
              <a href="/categories/{{$post->category->slug}}">{{ $post->category->name }}</a>
          </p>

          <div>
              {{ $post->excerpt }}
          </div>
      </article>
  @endforeach
</x-layout>

ここで実はN+1問題というものが起きています。

N+1問題とは?

N+1問題は上のviewを例にすると、

{{$post->category->slug}}

のようなループ(foreach)の中でまだロードされていないリレーションシップ(category)にアクセスするときに起きます。

記事一覧($posts)を取得するために1回のクエリを実行。

その後にN個ある記事($post)にすべてでcategoryを取得するためのクエリを1回実行するので

合計1回+N回のクエリを実行する必要がでてきます。

これをN+1問題と呼びます。

実際に確認してみる

使用されているクエリを見てみます。

LaravelのDB::listenを使用しlogにクエリを吐き出してみます。

routes/web.php

Route::get('/', function () {
   \Illuminate\Support\Facades\DB::listen(function($query){
       logger($query->sql); // ここでログへクエリを吐き出す
   });

   // すべての記事を取得
   $posts = Post::all();

   // $posts(すべての記事のコレクション)をpostsとしてviewに送る
   return view('posts', [
       'posts' => $posts
   ]);
});

サイトのTOPページをリロードします。

スクリーンショット 2021-05-31 19.37.32

すると

storage/logs/laravel.log

[2021-05-31 10:34:51] local.DEBUG: select * from `posts`  
[2021-05-31 10:34:51] local.DEBUG: select * from `categories` where `categories`.`id` = ? limit 1  
[2021-05-31 10:34:51] local.DEBUG: select * from `categories` where `categories`.`id` = ? limit 1  
[2021-05-31 10:34:51] local.DEBUG: select * from `categories` where `categories`.`id` = ? limit 1  

3つの記事取得するために

select * from `posts`

が実行され、その3つの記事のそれぞれでカテゴリを取得するためにクエリが3回実行されていることが確認できます。

もし50記事ある場合だと、51つのクエリが実行され、サイトのパフォーマンスは悪くなります。

余談ですが、Laravelではプリペアードステートメントを使用しているのでログの中に出てきているクエリの中に『?』が出てきています。

select * from `categories` where `categories`.`id` = ? limit 1

$query->bindings を使うことでbindingsを確認できます。

routes/web.php

\Illuminate\Support\Facades\DB::listen(function($query){
       logger($query->sql, $query->bindings);
   });
[2021-05-31 10:43:45] local.DEBUG: select * from `posts`  
[2021-05-31 10:43:45] local.DEBUG: select * from `categories` where `categories`.`id` = ? limit 1 [1] 
[2021-05-31 10:43:45] local.DEBUG: select * from `categories` where `categories`.`id` = ? limit 1 [2] 
[2021-05-31 10:43:45] local.DEBUG: select * from `categories` where `categories`.`id` = ? limit 1 [3] 

Clockwork

クエリを調査するときに毎回上記のような方法を行うことは手間になるので、clockworkと呼ばれるツールを使います。

インストールも簡単でプロジェクトのディレクトリで

$ composer require itsgoingd/clockwork

を実行してインストールします。

このツールはブラウザ(chromeまたは firefox)で使うのでブラウザのエクステンションもインストールします。

chromeの場合

firefoxの場合

ブラウザのエクステンションをインストールし、プロジェクトをブラウザでリロードしデベロッパーツールを開きます。

Clockworkタブがあるのでそこをクリック

スクリーンショット 2021-05-31 21.15.31

Database tabをクリックすると先ほど見たクエリの方法がブラウザで確認できます。

スクリーンショット 2021-05-31 21.18.00

N+1問題解決方法

LaravelはデフォルトでEloquent Relationshipは取得しません。(レイジーロード)理由は簡単で使うかもわからないRelationshipを勝手に取得しないだけです。

現在の記事一覧の取得方法は次のようになっています。

$posts = Post::all();

上記の取得方法だとブログ記事のカテゴリーは取得してくれないので、次のように明示して取得することでN+1問題を解決できます。

$posts = Post::with('category')->get();

変更し、ブラウザをリロードして確認します。

スクリーンショット 2021-05-31 21.27.35

4クエリーだったのが2クエリーになりました。

記事数が少ないのでそこまで差が大きくないように見えますが、これが50記事だった場合は51クエリーを2クエリーにすることができます。

最後に

サイトを構築していく上でパフォーマンスを考えることはとても多いです。クエリーの数も直接的に影響してくるので、Clockworkのような便利なツールも使用しながらパフォーマンスを向上していきましょう。

現在のソースコード


この記事が気に入ったらサポートをしてみませんか?