バックエンドエンジニアの実務では、N+1問題を見つけ、解決する場面がある、ということを聞きました。
そこで、練習のためにも、自分のアプリを使って、N+1問題について勉強したことをまとめてみます!
N+1問題は何が問題(悪影響)なのか?
N+1問題の何が問題かというと、
- 何度も(N回)テーブルにデータを取りに行くと、当然、処理が重くなる
- これが例えば、N=20回程度なら問題はないかもしれないが、N=50,000など大量のデータを処理する際には、負荷がかかる
- 結果、サービスの利便性が悪くなる。ビジネスに影響が出る
という認識です。
つまり、そこまで多くのデータを処理しないケースなら、あまり問題にはならないはずです。
私のアプリケーションもそれに該当するのかなと思います。
しかし、大量の処理を想定して、設計したらどうなるか、ということを実際にやって見ることは勉強になるので、試してみました。
N+1問題の特定
私は以下の方法で、N+1問題が発生しているところを特定しようと思いました。
- gem'bullet'の活用
- 複数テーブルにアクセスしているアクションを見る
gem'bullet'の活用
rubyには「bullet」というgemが存在し、N+1問題を特定することを補助する目的のgemです。
ただし、私が他に使っているgemとの相性が悪く、確認してみても、一つのテーブルについて処理しているクエリにも誤作動するようになっていました。
もう一つ良くない点として、このgemでは、includesの使用を推奨することがありますが、後述するように、使う意図が分かりにくく推奨されないメソッドです。
bulletの使用は行わないものとしました。
複数テーブルにアクセスしているアクションを見る
controllerを確認して、いくつかのテーブルにクエリを出しているアクションを見る方法です。
N+1問題は、複数のテーブルに対してクエリを出す文脈で起こる問題です。
Idea.findなど、クエリに関連するコードを探して、問題が生じているかを見ていきました。
実際のケース
自分と、他ユーザーの公開アイデアを一覧表示する、というthemeページに問題が存在していました。
該当箇所を簡略化して抜き出します。
def theme
@another_user_themes = []
@public_themes = Value.where(public: true)
@public_themes.each do |public_theme|
idea = Idea.find_by(id: public_theme.idea_id)
#他の人のテーマのみを配列に入れる
if idea.user_id != @current_user.id
@another_user_themes << idea
end
end
end
Valuesテーブルから取り出したデータを参照して、Ideasテーブルに複数回アクセスしています。
@public_themesが増えるたびに、Idea.findが繰り返され、クエリは増大するはずです。
これを解消します。
解消するメソッドの選定
N+1問題をrailsで解決するには、preloadかeager_loadを使います。
2つの使い分け
preload
- テーブルは結合されない。joinされない
- 2つのテーブルに対してそれぞれSQLを発行する
- 結合されていないので、whereでの絞り込みができない
eager_load
- テーブルを結合する
- whereでの絞り込みが可能
- 結合するので、レコードの多さなどによっては重い処理となる
今回の場合、
- 絞り込みを使いたい
- レコードをつなげることで、簡単に対応するideaが取得できる
と思ったので、eager_loadを採用します。
※includesについて
includes状況に応じて、2つを自動で判断するようです。
便利なメソッドですが、欠点もあります。
どちらを意図しているのか分からない、制御が難しい、という欠点です。
非推奨とされていることが多かったので、使いません。
コードの修正案
@public_themes = Value.eager_load(:idea).where(public: true)
@another_user_themes = @public_themes.map(&:idea).select { |idea| idea.user_id != @current_user.id }
テーブルを結合させ、.map(&:idea)ができるようにした後で、selectメソッドを使い条件を絞り込みました。
ネストが深くならないように、メソッドチェインを使って、条件を絞っていくことを意識しました。
ターミナルでビフォーアフターを確認したところ、以前は、themeアクションにおいて、 Value Load (0.1ms) が行われたあと、該当する条件の数だけIdea Load (0.1ms)が行われていました。
しかし、修正したところ、SQL (0.1ms) のみで処理が完了しており、N+1問題が解消できていることが分かりました。
まとめ
N+1問題の発見から、解決するためのメソッド選定までを行い、クエリを改良してみました。
個人的なアプリだと意識から逸れてしまうクエリの問題ですが、実務で意識することは確実に必要になってくるようなので、これから気をつけたいと思います。
記事内で、認識が間違っている部分、こうした方が良いよ、などありましたら、コメントいただけると助かります!☺