たけるのプログラミング

作ったものとか、気ままにアップします。

【PHP】N+1問題をLaravel Debugbarを使って検証してみる【Laravel】

N+1問題とは
【Ruby on Rails】N+1問題ってなんだ? - Qiita

ループ処理の中で都度SQLを発行してしまい、大量のSQLが発行されてパフォーマンスが低下してしまう問題のこと。

1回のクエリ発行でN件のレコードを取得し、

それぞれN件のレコードが持っているリレーション先のテーブルのレコード

を取得しようとする(N回のクエリ発行)とこのN+1問題が起こる。

具体例

実際に以下のようなリレーションを持つテーブルがあったとする。

f:id:takeru232423:20220314133636p:plain

membersテーブルにて外部キーをclub_idとしてclubsテーブルとリレーションを作る。

テーブルに対応したモデルを作る。

Member.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Member extends Model
{
    use HasFactory;
    public function Club_content()
    {
        return $this->hasOne('App\Models\Club','id','club_id');
    }
}

Club.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Club extends Model
{
    use HasFactory;
}

ルーティング

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\testnplus1;

Route::get('/',[testnplus1::class,'index'])->name('test');


そして以下のようなコントローラーとブレードを書いてN+1問題を起こす。
testnplus1.php

<?php

namespace App\Http\Controllers;

use App\Models\Member;


class testnplus1 extends Controller
{
    //
    public function index()
    {
        $members = Member::all();
        return view('showname',['members' => $members]);
    }
}

blade

@foreach ($members as $member)

<div>
    <p>{{$member->name}}</p>
    <p>{{$member->Club_content->name}}</p>
</div>

@endforeach

そしてlaravel Debugbarを利用してどんなSQLクエリが実行されたか確認してみる。すると以下のような結果となった。f:id:takeru232423:20220314141526p:plain

まずコントローラ内のMember::all()はテーブル内のレコードを全て取得するメソッドなので、上記SQLの"select * from members"に対応している。
問題なのはそれ以下のSQLクエリである。ブレードの繰り返し処理内の"$member->Club_content->name"を実行するたびにクエリを実行することとなるのでデータベースに負担をかけてしまうこととなる。今回は5、6個のレコードだけでテストしているが、このレコード数が大きくなればなるほどデータベースへの負荷を高めてしまうこととなる。

このN+1問題を解決するためにイーガーロードを利用する。イーガーロードを利用することでN回行ったSQLクエリを1回のSQLクエリにまとめることができる。

イーガーロードを利用するにはwithメソッドを使用する。
Eloquent:リレーション 8.x Laravel

コントラーのコードを以下のように変更する。

<?php

namespace App\Http\Controllers;

use App\Models\Member;


class testnplus1 extends Controller
{
    //
    public function index()
    {
        //N+1問題が発生するコード
        //$members = Member::all();

        //イーガーローディングによりN+1問題を防ぐコード
        $members = Member::with('Club_content')->get();
        return view('showname',['members' => $members]);
    }
}

Laravel Debugbarを使ってクエリの発行数を見てみると7→2になっている。
f:id:takeru232423:20220314143412p:plain

これでデータベースへの負荷を下げることができる。

機能実現だけでなく、パフォーマンスのことも考えることが大切だな〜。