N+1問題を解決してRDS障害を防ぐ:Laravel Eloquentの効率的な活用
はじめに
ある大規模なLaravelアプリケーションで突然RDSが落ちるという重大な障害が発生しました。調査の結果、多数のユーザーが同時に記事一覧APIを叩いたことで、N+1クエリ問題が原因でデータベースに過負荷が発生していたことが判明しました。この記事では、問題の特定から解決までのプロセスを解説します。
問題の特定
RDSのログから以下のようなSQLクエリが大量に実行されていることが確認されました:
SELECT * FROM categories WHERE article_id = ?
このクエリは記事一覧を取得する際に、各記事に関連するカテゴリー情報を個別に取得するために実行されていました。
N+1クエリ問題とは
N+1クエリ問題とは、データベースから主となるレコード(N件)を取得した後、それぞれのレコードに関連するデータを個別に取得するクエリが追加で実行される問題です。
例:記事一覧の場合
- メインクエリで記事20件を取得
- 各記事に対して、関連するカテゴリー情報を取得するクエリが20回実行
- 合計:21回のクエリが実行される
多数のユーザーが同時アクセスすると、数千のクエリが短時間に実行され、RDSに過負荷がかかります。
解決策:Eager Loading(事前読み込み)
Laravelでは、with()
メソッドを使用してEager Loadingを実装することで、N+1問題を解決できます。
修正前のコード
// ArticleController.php
public function index(Request $request)
{
$articles = Article::getBlogList(
$request->input('blog_id'),
$request->input('page', 1),
$request->input('per_page', 20)
);
return response()->json([
'status' => 'success',
'data' => ArticleResource::collection($articles),
]);
}
このとき、ArticleResource
内で各記事に対してCategory::getForArticle($this->id)
が呼び出され、N+1問題が発生していました。
修正後のコード
public function index(Request $request)
{
$articles = Article::getBlogList(
$request->input('blog_id'),
$request->input('page', 1),
$request->input('per_page', 20)
);
// Eager Loading追加
$articles->load('categories');
return response()->json([
'status' => 'success',
'data' => ArticleResource::collection($articles),
]);
}
解決後のSQL実行パターン
修正後は以下の2つのクエリのみが実行されます:
- メインクエリ:記事一覧を取得
- 関連データ一括取得:
SELECT * FROM categories WHERE article_id IN (1, 2, 3, ...)
20件の記事がある場合、クエリ実行回数は21回から2回に削減されます。多数のユーザーが同時アクセスしても、クエリ数は大幅に減少し、RDSへの負荷が劇的に軽減されます。
Eager Loadingの実装ポイント
-
リレーションの定義:Articleモデルに正しくリレーションが定義されていることを確認
public function categories() { return $this->hasMany(Category::class); }
-
コントローラーでの実装:取得したコレクションに対して
load()
メソッドを使用$articles->load('categories');
-
リソースクラスの最適化:事前に読み込んだデータを使用するよう修正(オプション)
'categories' => $this->whenLoaded('categories', function() { return $this->categories; }, function() { return Category::getForArticle($this->id); }),
結論
N+1クエリ問題は、Laravelアプリケーションでよく見られるパフォーマンス問題です。特に大規模なアプリケーションや高トラフィック環境では致命的な障害につながる可能性があります。Eager Loadingを適切に実装することで、データベースへのクエリ数を大幅に削減し、アプリケーションのパフォーマンスと安定性を向上させることができます。
今回のケースでは、コントローラーの一部を修正するだけで、クエリ数を95%以上削減することができました。これにより、多数のユーザーが同時アクセスしても安定してAPIが動作するようになりました。