ActiveRecord N+1問題の解決:includes・preload・eager_loadまとめ

スポンサーリンク

ActiveRecord N+1問題の解決:includes・preload・eager_loadまとめ

N+1問題とは

関連を持つレコードをループで取得するとき、1回の一覧取得 + N件分の関連取得 = N+1回のSQLが発生する問題です。

posts = Post.all         # SELECT * FROM posts (1回)

posts.each do |post|
  puts post.user.name   # SELECT * FROM users WHERE id = ? (Nレコード分)
end

投稿が100件あれば101回のSQLが走ります。


includes:通常はこれを使う

includes はRailsが状況に応じてSELECT INかLEFT OUTER JOINを自動選択します。

posts = Post.includes(:user)

posts.each do |post|
  puts post.user.name   # 追加クエリなし
end
-- 発行されるSQL(SELECT IN方式)
SELECT * FROM posts;
SELECT * FROM users WHERE id IN (1, 2, 3, ...);

関連テーブルで絞り込む場合はJOINに切り替わる

Post.includes(:comments).where(comments: { approved: true })
-- JOINに自動切り替え
SELECT posts.*, comments.*
FROM posts
LEFT OUTER JOIN comments ON comments.post_id = posts.id
WHERE comments.approved = true;

ネストした関連もまとめてロード

Post.includes(comments: :user)  # コメントとそのユーザーを一括ロード
Post.includes(:user, :tags)     # 複数の関連を同時に指定

preload:常にSELECT IN(2クエリ)

preload は常にSELECT IN方式で関連をロードします。includes と異なり、関連テーブルへの条件指定があってもJOINに切り替わりません。

posts = Post.preload(:user)
SELECT * FROM posts;
SELECT * FROM users WHERE id IN (1, 2, 3, ...);

関連テーブルに where を使いたい場合は preload では動作しないため、eager_load を使います。


eager_load:常にLEFT OUTER JOIN(1クエリ)

eager_load は常にLEFT OUTER JOINで1クエリにまとめます。関連テーブルの条件で絞り込む場合に使います。

Post.eager_load(:comments).where(comments: { approved: true })
SELECT posts.*, comments.*
FROM posts
LEFT OUTER JOIN comments ON comments.post_id = posts.id
WHERE comments.approved = true;

データ件数が多いとSELECT INより遅くなる場合があります。


3つの比較

メソッド クエリ方式 関連テーブルへの条件 使いどころ
includes SELECT IN / JOIN(自動選択) 使えるがJOINに切り替わる 通常はこれ
preload 常にSELECT IN 使えない 必ずSELECT INにしたい場合
eager_load 常にLEFT OUTER JOIN 使える 関連テーブルで絞り込む場合

joins との違い

joins はSQLのJOINを発行しますが、関連データをロードしません。関連テーブルで絞り込みたいだけで、関連データは使わない場合に使います。

# JOINして絞り込むが、post.commentsはロードされない(N+1は解決しない)
Post.joins(:comments).where(comments: { approved: true })

bullet gem でN+1を検出する

開発環境でN+1を自動検出するには bullet gemを使います。

# Gemfile
gem 'bullet', group: :development
# config/environments/development.rb
config.after_initialize do
  Bullet.enable = true
  Bullet.rails_logger = true
  Bullet.alert = true
end

N+1が発生しているとログに警告が出るため、開発中に気づけます。


まとめ

状況 使うメソッド
通常の関連ロード includes
関連テーブルで絞り込む eager_load
必ずSELECT INにしたい preload
絞り込みのみ(関連データは不要) joins