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問題に対応できない
そのかわり:キャッシュしない分メモリの消費が少ない
用途:関連のあるテーブルの値で絞り込みたいけど、関連先のデータ自体は使わないときに使う