N+1問題とincludesの使用

Railsアプリケーションにおける「N+1問題」とその解決手段であるincludesの使用について、以下のように詳しく説明します。


N+1問題とは何か

概要

「N+1問題」とは、1回のクエリで取得できるはずの関連データを、繰り返し個別に問い合わせてしまう非効率な処理のことです。この問題は、ActiveRecordの関連モデルにアクセスした際に自動で発生することがあります。

具体例

例えば、次のようにPostモデルとそれに紐づくCommentモデルが「1対多」の関係にあるとします。

ruby
# モデルの関係 class Post < ApplicationRecord has_many :comments end class Comment < ApplicationRecord belongs_to :post end

そして、以下のように各投稿とそのコメント数を表示するビューを作成したとします。

erb
<% Post.all.each do |post| %> <h2><%= post.title %></h2> <p>コメント数: <%= post.comments.count %></p> <% end %>

この場合、実行されるSQLは以下のようになります:

  1. SELECT * FROM posts(投稿全件を取得) ← 1クエリ

  2. 投稿ごとに SELECT COUNT(*) FROM comments WHERE post_id = ? ← 投稿数Nに対してN回クエリ発行

合計で 1 + N 回のクエリが発行されるため、「N+1問題」と呼ばれます。


includesを使った解決方法

includesの基本

includesメソッドを使うと、**事前に関連データをまとめて取得する(Eager Loading)**ことができ、N+1問題を回避できます。

ruby
Post.includes(:comments).each do |post| puts post.comments.count end

この場合、Railsは以下のような 2つのクエリを発行します:

  1. SELECT * FROM posts

  2. SELECT * FROM comments WHERE post_id IN (...)

このように、全てのコメントをまとめて一括取得することで、N回のクエリ発行を避け、パフォーマンスが大幅に向上します。


注意点と補足

preloadとeager_loadの違い

includesは内部的にpreloadeager_loadを使い分けます。

  • preload(デフォルト):複数のクエリを発行して関連データを読み込む(上記例)

  • eager_loadLEFT OUTER JOINによって1つのクエリで結合取得

ruby
Post.includes(:comments).where("comments.body LIKE ?", "%keyword%") # → 自動的にeager_loadが選ばれJOINクエリになる

countの注意点

post.comments.count毎回SQLを発行するため、事前に読み込んだデータを使うにはpost.comments.sizepost.comments.loaded?で確認しつつ使うと良いです。


まとめ

項目 内容
N+1問題 関連データの取得でN回クエリが発行される非効率な状態
発生原因 ActiveRecordの遅延ロード(Lazy Loading)
解決策 includesを使って事前に関連データをEager Loading
効果 クエリ数の削減、パフォーマンス向上
その他 preloadeager_loadの使い分けに注意

必要であれば、Bulletというgemを使ってN+1問題の検出を自動化することもできます。

ChatGPT4o 生成日:2025/06/21