はじめに
RailsでのN+1対策についての記事になります
N+1問題とは?
N+1問題とは、データベースからデータを取り出す際に、無駄なSQLが発行されてパフォーマンスが落ちるという問題です。
どのような時に起こるのか?
例として、UserモデルとAddressモデルを用意して考えます。
以下のような関連を持つテーブルからデータを取り出すとします。
class User < ApplicationRecord
has_one :address
end
class Address < ApplicationRecord
belongs_to :user
end
# controller側
@users = User.all
# view側
@users.each do |user|
user.address.city
end
userに紐づくaddressのcityを取り出す記述になります。
こちらがN+1問題が発生している
コードになります。
N+1問題 = SQLが無駄に発行されている
具体的に説明すると、userの情報を全て取得するためにSQLを1回発行、その後にループ件数分のSQLの発行が行われます。
userに紐づくaddress情報が10件あったとすれば合計11回のSQLを発行していることになっています。
実際にページにアクセスした際のコンソールを見てもらえればSQLが多く発行されていることが分かります!
これが万単位のデータを扱うことになるとパフォーマンスに直結
してくるため改善する必要がでてきます!
ではどう改善すればいいの?
初めのuser情報を取得する時に、それに紐づくaddress情報も取得できれば解決します!
ループ処理の前にデータを事前に読み込んでおく
ということです!
Railsではその方法としてincludesメソッド
が用意されています。
実際にこのように使用します。
# controller側
@users = User.all
# view側
@users.includes(:address).each do |user|
user.address.city
end
こうすることでuserに紐づくaddress情報を事前に取得してくれます!
コンソールで確認してみるとSQLの発行数が減っている
ことが確認できます。
ではこれで解決!
と言いたいところですが、ここからが本題になります。
includes以外にもメソッドが用意されている
RailsにはN+1問題を解決するメソッドとしてincludesの他にpreload
とeager_load
があります。
@users.preload(:address).each do |user|
user.address.city
end
@users.eager_load(:address).each do |user|
user.address.city
end
使い方としてはincludesと遜色ありません
では結局どれをどう使えばいいの?
という話になってきますね
実はpreloadとeager_loadを集約したメソッドがincludesになります!
つまりincludesを使用すると、内部でpreloadとeager_loadの挙動を自動で振り分けてくれます。
includesを用いると、デフォルトではpreloadと同様の挙動
他テーブルを結合しているか、関連先のテーブルで絞り込みを行っている場合はeager_loadと同じ挙動となる
じゃあ便利そうなincludesを使っておけば間違いないね!
という話ではありません!
includesはなるべく利用しない方が良い
**結論:**includesはなるべく使用せずに、preloadとeager_loadを使い分けよう!という話です。
**理由:**includesはクエリが状況によって変わるためコントロールが難しい。
データが膨大になってくると意図しない挙動を起こしてしまう
可能性があるということです
preload、eager_loadの使い分け
上記の理由から、基本はpreload、eager_loadを使用する
方が良いと考えられます。
では肝心の使い分けについてですが
難しい話は割愛し、どのように使い分けるのが適切なのかだけ書いておきます。
preload
多対多のアソシエーションの場合に有効(has_manyなアソシエーション) データ量が大きいと考えられる状況であれば、クエリを分割して取得するpreloadの方がレスポンスが早いと考えられます。eager_load
1対1あるいは多対1のアソシエーションの場合に有効(has_one、belongs_toなアソシエーション) ?対1で関連付けられている場合は、外部結合しても取得するレコード数が変わらないため効率が良いと考えられます。ぜひこちらを参考に使い分けを行い、N+1問題を解決してみてください!
そもそもどこでN+1問題が起きているか分からない
そんな方にはN+1クエリを自動で検出してくれるBulletという便利なgemがあります。
Bulletを使うと、RailsのログやブラウザでN+1であることを確認できます。
gem "bullet"
使用方法については別の記事を参考にしてみてください!
参考文献