とても久しぶりの投稿になります。
あれから4年くらいの月日が経ち、いろいろ変わりました。
今はPHPのLaravelを触っています。Laravelを始めてから2か月が立とうとしていて、少し容量を掴んできたので、記事の投稿をしてみます。
環境
- OS:Windows 11
- PHP:8.44
- Laravel:12.0
- Livewire:3.4
- 仮想環境:Laragon Full6.0
発生した問題
下記のコードは、render()でindexページへコレクションオブジェクトとしてデータを渡すとき、
public function render()
{
//省略
if ($this->pid === 'index') {
$viewData['List'] = $this->getList();
foreach ($viewData['List'] as $value) {
$value->rubi = ColorModel::list()
->joinRels()
->whereId($value->id)
->pluck('rubi');
}
}
//省略
}
これの問題は、ループ中にモデルのオブジェクトを呼び出しているので、クエリの発行を毎回行っていることです。
今まであんまり縁がなかったのですが、典型的なN+1問題という奴でした。
N+1問題
「1回のデータ取得で済むはずが、関連するデータの数(N)だけ余計にクエリを発行してしまう状態」
この処理で、ビューにリストを一覧で表示したいのですが、ループの中でクエリの発行をしているので当然重くなります。
表示速度はだいたい2.100msくらいでした。今回はDBの総量がそんなにないので遅すぎるってことはなかったですね。
解決方法
これの改善方法は、先にListの全IDを抽出し、whereInを使用して一括でデータを取得してから、PHP側で各要素に紐づけを行うことです。
修正後のコード
if ($this->pid === 'index') {
$viewData['List'] = $this->getList();
$Ids = $viewData['List']->pluck('id')->toArray();
if (!empty($Ids)) {
$Colors = ColorModel::list()
->joinColor()
// whereIn を想定
->whereIn('colors.id', $Ids)
->select('colors.rubi', 'colors_rels.id')
->get()
->groupBy('id');
foreach ($viewData['List'] as $value) {
// 該当するIDのデータがあればセット、なければ空のコレクション
$value->colr_rubi = isset($Colors[$value->id])
? $Colors[$value->id]->pluck('rubi')
: collect();
}
}
}
コードの中身は若干適当です。
伝えたいのは、ループの外で一括取得して、ループの中でセットするだけです。
これでおおよそ表示速度は1,200msになりました。
修正のポイント
groupBy の活用:
データベースから取得した全件データをidをキーにした連想配列に変換しています。これにより、ループ内でのデータ検索が高速になります。
学び・今後の対策
とにかく、ループの中でモデルの呼び出し及びクエリの発行がある場合には一度立ち止まって考えてみること
参考
Geminiさん