【Laravel】Laravel 組み込みの with,load メソッド的な N+1 問題を防ぐクエリを作る

 Laravel の様なデータベースを写したモデルをプログラム中で取り扱う時、しばしば次の様にしてしまいがちです。

// 全ての会員を取得し
Member::get()->map(static function(Member $member){
    // 各会員の持つ投稿全てについているコメントの数を集計する
    return $member->posts->sum(fn($post)=>$post->comments->count());
});

 これは PHP のコード上では短いコードですが、実際に実行すると長大な実行時間になりやすいです。実際には次の様に 1 度のクエリで引っ張ったレコードの数 N 分のクエリを発行する、N + 1 問題が起きます。これが起きると SQL の実行回数が非常に多くなりやすく、パフォーマンスの低下を招きます。

クエリ実行回数 実行されるクエリの簡易表現
1 select * from members
会員の数 select * from posts where member_id = {$memberId}
会員の数×投稿の数 select * from comments where post_id = {$postId}

 これの対策としては JOIN 句を使って必要なデータ全てを一度のクエリで取得する方法があります。また、Laravel の様なフレームワークにはしばしば N+1 が起きそうな書き方をしても N+1 を起こさない仕組みがあります。Laravel では次の様に with メソッドを使うことで少ないクエリ数で必要なレコードを集められます。
 Eloquent:リレーション 6.x Laravel#Eagerロード

// with('posts.comments') とリレーションを指定すると適した形でリレーション先をあらかじめロードしてくれます。
Member::with('posts.comments')->get()->map(static function(Member $member){
    return $member->posts->sum(fn($post)=>$post->comments->count());
});

 また、既にモデルインスタンスがある場合でも with を使わずに次の様に同様のリレーション先のロードができます。
Eloquent:リレーション 6.x Laravel#遅延Eagerロード

/** @var Member $member */
$member->load('posts.comments');

 毎回これらを使えればいいのですが、難しい時もあります。そういった時、同様のクエリを手動で作りたい時があります。これは次の様にできます。

    public static function loadPosts(Collection $members): Collection
    {
        // 紐づけ用に主キーをCollectionのキーにします。(元のキーが大事な場合はもっと工夫が必要)
        $members = $members->keyBy('member_id');
        // 主キーである member_id の値を集めます
        $memberIds = $members->map->member_id;

        $posts = Post::whereIn('member_id', $memberIds)// members と紐づいているキーで where in
            ->get();// 必要な Post を一度のクエリで全取得

        $posts->each(static function (Post $p) use ($members) {
            $member = $members[$p->member_id];// ↑で主キーをキーにしたCollectionにすることで直ぐに呼べます
            if(!isset($member->posts)){
                $member->posts = collect();// null なら空のCollectionを用意
            }
            $members->posts->push($p);// postsプロパティに Post インスタンスを追加
        });

        // posts を付け終わった members を返却
        return $members;
    }

 この様に WHERE IN で必要なレコードを全取得、数え上げ系処理で取得した各レコードをリレーションにしたがって後付け、とすることで with, load なしに同様の後付け処理が少ないクエリ数で実現できます。

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG