LoginSignup
5
6

More than 1 year has passed since last update.

【Laravel】LaravelのN+1問題とは、その解決方法

Last updated at Posted at 2021-08-02

はじめに

Laravel: 7.30.4
PHP: 7.2.3

N+1問題についてなぜこの問題が発生するのか、初学者が簡単に理解できるように書いていきたいと思います。

準備

まず、適当にリレーションを張ったモデルを用意します。今回はAuthor(著者)モデルとArticle(記事)モデルを用意して、Authorが複数のArticleを持つようにします。

Article.php
<?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/

ルーティングも書いておきましょう↓

web.php
<?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(記事)を持っている時にこんな処理を書くことがあると思います↓

ArticleController.php
<?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()を使用して書き換えてみましょう。

ArticleController.php
<?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問題が解決できました。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6