N+1問題(include,preload,eagar_load)

今回は、N+1問題について勉強していきたいと思います。

N+1問題とは


N+1問題とは、はじめの1回のSQLでModelを取得し、そのModelに対するデータ数分(N回)のSQLが実行されてしまう状態のことを言う。

順番から考えると、N+1問題というよりは1+N問題と呼んだ方がイメージしやすいかと思います。

また、N+1問題を検知するgemで「bullet」というものがある。.preload、eager_load、includeを追加しなくても勝手にN+1問題を解決してくれる優れもの。

allを使った場合とincludeを使った場合を比較してN+1問題を理解してみましょう。

allを使用した場合

Image from Gyazo

Completed 200 OK in 204ms (Views: 100.3ms | ActiveRecord: 22.8ms | Allocations: 29218)

includeを使用した場合

Image from Gyazo

Completed 200 OK in 158ms (Views: 69.6ms | ActiveRecord: 68.7ms | Allocations: 23876)

includeとallを比較してみると、includeの方がviewにレンダリングする時間が短いですね。includeを使用すると事前にActiverecordからデータをひっぱってきて、その後にviewにレンダリングするので、viewsに表示させるときにかかる時間が短いようです。

ちなみにN+1問題を解決させる方法は他にもあるみたいなので、まとめておきます。

preloadとeager_load,includeの使い分け

  • includesはなるべく利用しない方が良い
    • 理由:意図しない挙動を防ぐため
    • 代わりに、preloadeager_loadを使う
  • preload
    • どんな場合に使うといいか : 多対多のアソシエーションの場合
    • できないこと : アソシエーション先のデータ参照(Whereによる絞り込みなど)
    • 注意 : データ量が大きいと、IN句が大きくなりがちで、メモリを圧迫する可能性がある
  • eager_loadはどんな場合に使うといいか
    • 1対1あるいはN対1のアソシエーションをJOINする場合(belongs_to, has_one アソシエーション)
    • JOINした先のテーブルの情報を参照したい場合(Whereによる絞り込みなど)
  • joinsはどんな場合に使うか
    • メモリの使用量を必要最低限に抑えたい場合
    • JOINした先のデータを参照せず、絞り込み結果だけが必要な場合
      • 逆に言うと、引用先のデータを参照しない場合、使用しないほうがいいです

対Nのアソシエーションの場合はpreload

データ量が増えるほど、eager_loadよりも、preloadの方がSQLを分割して取得するため、レスポンスタイムは早くなる。

対1のアソシエーションの場合はeager_load

データ量が増えても、1回のSQLでまとめて取得した方が効率的な場合が多い。

 

ケースに応じて使い分けることができるようになったら、熟練者って感じがしますね。