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 なしに同様の後付け処理が少ないクエリ数で実現できます。