⚠️

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によるデータ重複の可能性
  • 複雑な関連では最適戦略の判断が必要
  • メモリ使用量が増加しうる

ユースケース

投稿一覧 + 作成者表示 注文一覧 + 商品情報 カテゴリ + 下位アイテム ダッシュボード統計ページ