⚠️
N+1クエリ問題
パフォーマンスの敵 — includesで解決する方法
N+1問題はRailsで最も一般的なパフォーマンス問題です。
問題状況:
# Controller
@posts = Post.all # クエリ1: SELECT * FROM posts
# View
<% @posts.each do |post| %>
<%= post.user.name %> # クエリN: SELECT * FROM users WHERE id = ?(毎回実行!)
<% end %>
ポストが100件なら101回クエリ実行!
解決: includes
@posts = Post.includes(:user) # クエリ2回で解決
# SELECT * FROM posts
# SELECT * FROM users WHERE id IN (1, 2, 3, ...)
3つの方法:
includes— Railsが自動で最適戦略を選択preload— 別クエリでロード(デフォルト)eager_load— LEFT OUTER JOIN使用
検知ツール: bullet gemがN+1を自動検知して警告を表示します。
構造ダイアグラム
N+1問題 (101クエリ!)
1. SELECT * FROM posts
2. SELECT * FROM users WHERE id = 1
3. SELECT * FROM users WHERE id = 2
4. SELECT * FROM users WHERE id = 3
... (N回繰り返し!)
101. SELECT * FROM users WHERE id = 100
Post.all → post.user (毎回クエリ)
includes適用 (2クエリ!)
1. SELECT * FROM posts
2. SELECT * FROM users WHERE id IN (1, 2, 3, ..., 100)
完了!たった2回のクエリで完了
Post.includes(:user) → 事前ロード
コード比較:
Before
@posts = Post.all
After
@posts = Post.includes(:user)
ポイント: <strong>includesの1行</strong>で101回のクエリを2回に削減 — bullet gemで自動検出可能
キーポイント
1
問題認識: ループ内で関連データにアクセスするとN+1発生
2
railsログで繰り返されるSELECTクエリを確認
3
includes(:association)をコントローラクエリに追加
4
ネスト関連: includes(posts: :comments)またはincludes(:posts, :profile)
5
bullet gemインストールでN+1自動検知
6
strict_loadingモードでN+1発生時例外を出す設定(Rails 6.1+)
メリット
- ✓ includes 1行でパフォーマンス数十倍向上
- ✓ bullet gemで自動検知可能
- ✓ strict_loadingでランタイム防止可能
- ✓ SQLログで簡単に確認可能
デメリット
- ✗ 全てにincludesを入れると不必要なデータロード
- ✗ eager_loadはJOINによるデータ重複の可能性
- ✗ 複雑な関連では最適戦略の判断が必要
- ✗ メモリ使用量が増加しうる
ユースケース
投稿一覧 + 作成者表示
注文一覧 + 商品情報
カテゴリ + 下位アイテム
ダッシュボード統計ページ