[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ページをリロードします。
すると
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タブがあるのでそこをクリック
Database tabをクリックすると先ほど見たクエリの方法がブラウザで確認できます。
N+1問題解決方法
LaravelはデフォルトでEloquent Relationshipは取得しません。(レイジーロード)理由は簡単で使うかもわからないRelationshipを勝手に取得しないだけです。
現在の記事一覧の取得方法は次のようになっています。
$posts = Post::all();
上記の取得方法だとブログ記事のカテゴリーは取得してくれないので、次のように明示して取得することでN+1問題を解決できます。
$posts = Post::with('category')->get();
変更し、ブラウザをリロードして確認します。
4クエリーだったのが2クエリーになりました。
記事数が少ないのでそこまで差が大きくないように見えますが、これが50記事だった場合は51クエリーを2クエリーにすることができます。
最後に
サイトを構築していく上でパフォーマンスを考えることはとても多いです。クエリーの数も直接的に影響してくるので、Clockworkのような便利なツールも使用しながらパフォーマンスを向上していきましょう。
現在のソースコード
この記事が気に入ったらサポートをしてみませんか?