N+1問題の対応

N+1問題とは

主にインスタンスの配列に対して、ループ処理などによってその一つ一つに関連のあるデータを取得するときに、ループ処理の回数分クエリが走りパフォーマンスが低下すること。

@users = User.all

@users.each do |user|
  puts user.post.content
end

User.allで1回、@usersの件数分=N回のクエリが走る。


また一見コントローラでループ処理を行わない場合でも、配列をビューに渡してからループ処理させる場合N+1問題は起こる。

@comments = @board.comments
<!-- 配列をパーシャルに渡す -->
<%= render @comments %>
<!-- パーシャル -->
<h3><%= comment.user.name %></h3>

コメントの数だけパーシャルがイテレートされ、毎回comment.userを取得するクエリが走る。


まとめると

コントローラやビューにおいて、インスタンスの配列に対してループ処理によってそれぞれの要素に関連のあるデータを取得するときにN+1問題は起こる。

対策

eager_load
関連のあるテーブルを外部結合した上で一括読み込みしてキャッシュする=N+1問題に対応
用途:関連付いたテーブルの値で絞り込みたいし関連先のデータも使いたいときに使う
注意点:結合したテーブルが大きいとレスポンスが遅くなる。一対多、多対多の関連だとデータも増えるし重複を取り除くのに手間がかかる。一対一、多対一で使うのが望ましい。

@comments = @board.comments.eager_load(:user)

preload
関連のあるテーブルを結合させず、それらのデータをテーブルごとに複数のクエリで分けて一括読み込みし、キャッシュする=N+1問題に対応
用途:関連先のデータを使いたいけど、その値で絞り込む必要がないときに使う
注意点:取得する関連元のインスタンスの数に比例してクエリ(IN句)が長くなる。ページネーションとかで制限してあれば大丈夫

@comments = @board.comments.preload(:user)

includes
絞り込みが行われるかどうか(その後に絞り込むメソッドがチェーンされるかどうか)などによってよしなにeager_loadまたはpreloadの挙動になる=N+1問題に対応
用途:上の使い分けがわかってれば使う必要なし?
注意点:挙動をコントロールできないからあんまよくないらしい

@comments = @board.comments.includes(:user)


おまけ

joins
関連のあるデータを一括読み込みしない=N+1問題に対応できない
そのかわり:キャッシュしない分メモリの消費が少ない
用途:関連のあるテーブルの値で絞り込みたいけど、関連先のデータ自体は使わないときに使う