はじめに
SQLを触っていると必ず目に入るN+1問題。
なんとなくは分かっているが整理のためにもN+1問題について自分なりのメモを残しておく。
N+1問題
N+1問題は端的に言うと、SQLがたくさん発行されてしまい動作が遅くなる問題のことですね。
もう少し詳しくいうと
・N件のデータ行を持つテーブルを読みだすのに1回
・別のテーブルから、先述のテーブルの各行に紐づくデータを(1件ずつ)読み出すのに計N回
なのでNが増えれば増えるほど処理が重くなってしまうんですね。
例を挙げますと、下記のようなBookモデルがあって、各Bookと1対1のAuthorモデルと繋げるリレーションメソッド authorが定義されていたとします。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Book extends Model
{
/**
* この本を書いた著者を取得
*/
public function author()
{
return $this->belongsTo('App\Author');
}
}
次のように、全BookのAuthorの名前を取得する場合、次のように書くと N+1 問題にぶつかります。
$books = App\Book::all();
foreach ($books as $book) {
echo $book->author->name;
}
このコードのSQLの中身は
SELECT * FROM books;
SELECT * FROM authors WHERE book_id = 1;
SELECT * FROM authors WHERE book_id = 2;
SELECT * FROM authors WHERE book_id = 3;
.
.
Book が全部で100 レコードあるとしたら、100+1で合計101ものクエリが発行されてしまう。
このうち大部分の SQL は book_id が違うだけなので無駄が多い。
処理も重くなってしまいますね。
これをLaravelではEagerLoad で削減できます。
全Bookを取得し、authorをEagerLoadしておく
$books = App\Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
$books = App\Book::with('author')->get()
のwith
の部分がEagerLoadです。
あらかじめauthorを行ったものを入れているわけですね!
処理はこのようにスッキリとなりました。
SELECT * FROM books;
SELECT * FROM authors WHERE id IN (1, 2, 3, 4, 5, ...)
いろいろなEagerLoad
さっき使ったwith。複数のリレーションに使用できる。
$books = App\Book::with(['author', 'publisher'])->get();
↓リレーションで取得した先のモデルからさらに別のリレーションに繋げる場合に使う
$books = App\Book::with('author.contacts')->get();
↓親のモデルを取得した後に、ある条件によって eagerload するかを決めたいときなど。
with の代わりに load を使う。
$books = App\Book::all();
if ($someCondition) {
$books->load('author', 'publisher');
}
↓使用するリレーションにさらに制約をかける場合。
例では、postのtitleにfirst という言葉を含むという制約をかけた。
where だけでなく orderBy など他のクエリビルダも使える。
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* ユーザーの全ポストの取得
*/
public function posts()
{
return $this->hasMany('App\Post');
}
/**
* タイトルにfirstの文字が入るPostを取得する
*/
public function postsInTitleFirst()
{
return $this->posts()->where('title', 'like', '%first%');
}
}
$user = App\User::with('postsInTitleFirst')->find(1);
$posts = $user->postsInTitleFirst;
foreach ($posts as $post) {
echo $post->title;
}
まとめ
投稿一覧とかなんらかの一覧を取得する時にはこのN+1問題が必ず発生すると思います。
LaravelではEagerLoadによってこれを防ぐことができるので、
特にリレーションメソッドを使う際には注意をする。
ま、要するにリレーションメソッドを使用したものを再度入れ込んでいきましょうってことですね!