##はじめに
Laravel: 7.30.4
PHP: 7.2.3
N+1問題についてなぜこの問題が発生するのか、初学者が簡単に理解できるように書いていきたいと思います。
##準備
まず、適当にリレーションを張ったモデルを用意します。今回はAuthor
(著者)モデルとArticle
(記事)モデルを用意して、Author
が複数のArticle
を持つようにします。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
public function Author()
{
return $this->belongsTo('App\Author');
}
}
※Authorモデルには特になにも書かなくて大丈夫です。
次にテーブルを用意して適当にデータを挿入していきましょう
テーブル作成はこちら↓のURLが参考になります。
https://qiita.com/yukibe/items/f05bf5e829a9a05616f7
ダミーデータを登録する際はこちら↓のURLが参考になります
https://www.larajapan.com/2021/05/03/bulk-insert%E3%81%A7%E5%A4%A7%E9%87%8F%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92db%E3%81%AB%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B/
ルーティングも書いておきましょう↓
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::get('/article', 'ArticleController@handle');
##N+1問題を発生させる
ここからが本題です、まず実際にN+1問題を発生させてみましょう!
そもそもN+1問題とは何なのでしょうか?
N+1問題とは、例えば、Author
(著者)が複数のArticle
(記事)を持っている時にこんな処理を書くことがあると思います↓
<?php
namespace App\Http\Controllers;
use App\Article;
class ArticleController extends Controller
{
public function handle()
{
$articles = Article::all(); // ①記事を全件取得する
// ②取得した記事をループで回して一件ずつ著者の名前を出力する
foreach($articles as $article) {
echo $article->author->name;
}
}
}
※普段はコントローラからecho
で出力はしないと思いますが今回は参考として書きます。
上記のコードでは下記のことが起こります。
・①記事を全件取得する
・②記事の数だけ著者の名前を取得する
ということが起こります。
記事を全件取得する : 記事の数だけ著者の名前を取得する = 1 : N
という感じです
つまり、著者の名前を取得したい時に、記事の数(N回)だけクエリを発行されるということです。
これが、記事の数が少ないときはパフォーマンスにさほど影響は与えないのですが、もし、記事が10万件あったら処理のスピードがかなり遅くなってしまいます。これがN+1問題です。
実際に上記のコードが実行された時に発行されたクエリを見てみましょう↓
※ダミーデータを100件登録してあります。
array:101 [▼
0 => array:3 [▼
"query" => "select * from "articles""
"bindings" => []
"time" => 0.36
]
1 => array:3 [▼
"query" => "select * from "authors" where "authors"."id" = ? limit 1"
"bindings" => array:1 [▶]
"time" => 0.09
]
2 => array:3 [▼
"query" => "select * from "authors" where "authors"."id" = ? limit 1"
"bindings" => array:1 [▶]
"time" => 0.06
]
3 => array:3 [▼
"query" => "select * from "authors" where "authors"."id" = ? limit 1"
"bindings" => array:1 [▶]
"time" => 0.06
]
4 => array:3 [▼
"query" => "select * from "authors" where "authors"."id" = ? limit 1"
"bindings" => array:1 [▶]
"time" => 0.06
]
5 => array:3 [▼
"query" => "select * from "authors" where "authors"."id" = ? limit 1"
"bindings" => array:1 [▶]
"time" => 0.06
]
※以下省略
6 => array:3 [▶]
7 => array:3 [▶]
8 => array:3 [▶]
9 => array:3 [▶]
10 => array:3 [▶]
11 => array:3 [▶]
12 => array:3 [▶]
13 => array:3 [▶]
14 => array:3 [▶]
15 => array:3 [▶]
16 => array:3 [▶]
17 => array:3 [▶]
18 => array:3 [▶]
19 => array:3 [▶]
20 => array:3 [▶]
21 => array:3 [▶]
22 => array:3 [▶]
23 => array:3 [▶]
24 => array:3 [▶]
25 => array:3 [▶]
26 => array:3 [▶]
27 => array:3 [▶]
28 => array:3 [▶]
29 => array:3 [▶]
30 => array:3 [▶]
31 => array:3 [▶]
32 => array:3 [▶]
33 => array:3 [▶]
34 => array:3 [▶]
35 => array:3 [▶]
36 => array:3 [▶]
37 => array:3 [▶]
38 => array:3 [▶]
39 => array:3 [▶]
40 => array:3 [▶]
41 => array:3 [▶]
42 => array:3 [▶]
43 => array:3 [▶]
44 => array:3 [▶]
45 => array:3 [▶]
46 => array:3 [▶]
47 => array:3 [▶]
48 => array:3 [▶]
49 => array:3 [▶]
50 => array:3 [▶]
51 => array:3 [▶]
52 => array:3 [▶]
53 => array:3 [▶]
54 => array:3 [▶]
55 => array:3 [▶]
56 => array:3 [▶]
57 => array:3 [▶]
58 => array:3 [▶]
59 => array:3 [▶]
60 => array:3 [▶]
61 => array:3 [▶]
62 => array:3 [▶]
63 => array:3 [▶]
64 => array:3 [▶]
65 => array:3 [▶]
66 => array:3 [▶]
67 => array:3 [▶]
68 => array:3 [▶]
69 => array:3 [▶]
70 => array:3 [▶]
71 => array:3 [▶]
72 => array:3 [▶]
73 => array:3 [▶]
74 => array:3 [▶]
75 => array:3 [▶]
76 => array:3 [▶]
77 => array:3 [▶]
78 => array:3 [▶]
79 => array:3 [▶]
80 => array:3 [▶]
81 => array:3 [▶]
82 => array:3 [▶]
83 => array:3 [▶]
84 => array:3 [▶]
85 => array:3 [▶]
86 => array:3 [▶]
87 => array:3 [▶]
88 => array:3 [▶]
89 => array:3 [▶]
90 => array:3 [▶]
91 => array:3 [▶]
92 => array:3 [▶]
93 => array:3 [▶]
94 => array:3 [▶]
95 => array:3 [▶]
96 => array:3 [▶]
97 => array:3 [▶]
98 => array:3 [▶]
99 => array:3 [▶]
100 => array:3 [▶]
]
1番最初に実行されているのが、記事の全件取得のクエリで、その後にループで著者の名前を1つづつ取得しているのがわかると思います。掛かっている時間がだいたい60ミリ秒です。
これはLaravelの動的プロパティは遅延ロードされるという性質が原因で引き起こされるのですが、今回その説明は割愛します。
N+1問題を解決していく
では、さきほどのN+1問題を解決していきましょう。
Laravelではとても簡単にN+1問題を解決できるメソッドが用意されています。with()
というメソッドです。
$articles = Article::with('author')->get();
このように書くことで、Article
(記事)の全件取得をする時に、ついでにリレーションが張られているAuthor
(著者)の情報も一緒に取得してくれるような感じです。
これをEagerロードといいます。
先程のコードをwith()
を使用して書き換えてみましょう。
<?php
namespace App\Http\Controllers;
use App\Article;
class ArticleController extends Controller
{
public function handle()
{
$articles = Article::with('author')->get();
foreach($articles as $article) {
$article->author->name;
}
}
}
$articles = Article::all();
の部分が
$articles = Article::with('author')->get();
に変わっただけです。
ではこちらを実行してみましょう。発行されたクエリはこんな感じです↓
array:2 [▼
0 => array:3 [▼
"query" => "select * from "articles""
"bindings" => []
"time" => 0.39
]
1 => array:3 [▼
"query" => "select * from "authors" where "authors"."id" in (1)"
"bindings" => []
"time" => 0.07
]
]
なんと発行されたクエリは2件だけですね。かなりすっきりしたのがわかると思います。
掛かっている時間も0.46ミリ秒とかなり短縮されています。
これでN+1問題が解決できました。