#07 Laravel基礎

EagerロードとN+1問題を解決する

❌ N+1問題 Post::all() SQL #1 posts 全件取得 SELECT * FROM posts foreach ($posts as $post) { $post->user->name ← ここで毎回SQL SQL #2〜N+1 SELECT * FROM users WHERE id=? ×N回 合計: N+1回のSQL (10件なら11回、100件なら101回) ✓ Eagerロード Post::with('user') ->get() SQL #1 posts 全件取得 SELECT * FROM posts SQL #2 users 一括取得(WHERE IN) SELECT * FROM users WHERE id IN (1,2,...) ループ内でSQLは発行されない $post->user はキャッシュ済み 合計: 2回のSQL (何件でも常に2回)
図1: N+1問題とEagerロードのSQL発行回数の比較

N+1問題とは

N+1問題? ループの中で毎回SQLを発行してしまうパフォーマンス問題。10件の投稿を表示するのに11回クエリが走る、などが典型例。 は、リレーションを持つデータを取得するときに起こるパフォーマンス問題です。

よくある例を見てみましょう。

// 📁 app/Http/Controllers/PostController.php

public function index()
{
    $posts = Post::all(); // SQL 1回:全投稿を取得
    return view('posts.index', compact('posts'));
}
{{-- 📁 resources/views/posts/index.blade.php --}}

@foreach ($posts as $post)
    <p>{{ $post->title }} by {{ $post->user->name }}</p>
                                  {{-- ↑ ここで毎回SQL発行! --}}
@endforeach

投稿が10件あれば:

  • Post::all() で 1回
  • $post->user でループごとに 10回

合計 11回のSQL が実行されます。100件なら101回です。


Eagerロードで解決する

with() を使うと、投稿と投稿者をまとめて2回のSQLで取得できます。

// 📁 app/Http/Controllers/PostController.php

public function index()
{
    // with('user') で user リレーションを先読み(Eager Load)
    $posts = Post::with('user')->get();
    return view('posts.index', compact('posts'));
}

実行されるSQL:

-- 1本目:全投稿を取得
SELECT * FROM posts;

-- 2本目:関連ユーザーをまとめて取得(WHERE IN で一括)
SELECT * FROM users WHERE id IN (1, 2, 3, ...);

何件あっても常に2回だけです。


複数のリレーションをまとめてEagerロード

// 📁 app/Http/Controllers/PostController.php

// user(作者)とtags(タグ)を同時にEagerロード
$posts = Post::with(['user', 'tags'])->get();

ネストしたリレーションもEagerロードできる

「投稿」→「コメント」→「コメントの作者」のようなネストも対応できます。

// posts → comments → user を全部まとめて取得
$posts = Post::with('comments.user')->get();

Eagerロードに条件を付ける

with() にクロージャーを渡すと、先読みするデータを絞り込めます。

// 📁 app/Http/Controllers/PostController.php

// 公開済みコメントだけをEagerロード
$posts = Post::with(['comments' => function ($query) {
    $query->where('status', 'approved')->latest();
}])->get();

lazyEagerLoad:後から Eager Load する

すでに取得済みのコレクションに後からEagerロードしたい場合は loadMissing() を使います。

$posts = Post::all();

// 後からuserリレーションを読み込む
$posts->loadMissing('user');

N+1を発見するには

開発中は preventLazyLoading() を使うと、Eagerロードしていないリレーションへのアクセスを検知できます。

// 📁 app/Providers/AppServiceProvider.php

use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    // 本番環境以外でN+1を検知する
    Model::preventLazyLoading(! app()->isProduction());
}

これを設定すると、$post->user をEagerロードせずにアクセスした場合に LazyLoadingViolationException がスローされます。


まとめ

  • N+1問題:ループ内でリレーションにアクセスするたびにSQLが1本発行される問題
  • with('リレーション名') でEagerロードすると2本のSQLで解決できる
  • with(['user', 'tags']) で複数リレーションをまとめてロードできる
  • ネストは with('comments.user') のように . で繋げる
  • Model::preventLazyLoading() で開発中にN+1を検知できる

次回はEloquentのスコープとアクセサーを学びます。