Activerecordのパフォーマンスチューニング
背景
仕事でactiverecordを使用していて、
複数のテーブルのデータを返す必要がある場合に、
レスポンスが一気に遅くなったために調べていた。
N+1問題を解消
まず、最初にあまり気にせずにN+1を行っていたので解消した。
解消方法としては、includes, prelaod、eager_loadがある。
今回はjoinして絞り込む必要がなく、
複数のクエリで単純にセレクトをしたかったのでpreloadをしていた。
これを行うことによって、クエリの発行数が抑えられた(もともと、大した量ではなかったけど)
ボトルネックを明確にする。
しかし、preloadを行っても性能はあまり改善が見られなかった。
ボトルネックを明確にする為に、
rack-profilerを使用し、
細かく各処理の時間を見ていたが、
どうもロジックと全体の時間が合っていない。
activerecordでセレクトし、生成されたオブジェクトのループ全体の時間と
ループ内の処理の時間を足した時間が一致しない。
ループ時のループ変数(って言葉ある?)を生成するタイミングで時間がかかっているようだった。
Object生成のコスト
N+1問題の解消の為に、予めクエリを発行し、キャッシュしていくことが解決方法であるが、
そこで予め生成するオブジェクトのコストはN+1でクエリを発行するよりも重い場合がある。
多くをpreloadしたオブジェクトを each メソッド等でループする際に、
紐づくテーブルの情報もオブジェクト生成が行われる。
結果として、1ループあたりの、ループ変数の生成に時間がかかるようになる。
つまり、N+1のクエリ発行とpreloadのオブジェクト生成の時間はトレードオフになる。
結論
レコード数や関連するテーブル、オブジェクト生成数によって
何が一番良いかが変わるため、正解がない。
今回の場合は、preloadはしておきたいものの、
preloadしたいレコード数が決まっていた。(全てを取る必要がない)
そのため、 preloadで全レコードのオブジェクト生成のコストよりN+1のコストのほうが低くなった。
tips
オブジェクトの生成が必要なく、
セレクトしてきたデータのみが必要な場合は、pluckメソッドを使用することで
オブジェクト生成せずに、データが取得できる。
User.all.pluck(:id, :name)
[ [1, 'hoge'], [2, 'moge'] ]
みたいなデータが取れる。