研修中にN+1問題についてまとめたこと
はじめに
2024年6月にラグザイアに入社しました、Tamaと申します。
ここでは、新人研修で詰まったN+1問題について自分なりに分析したものを紹介します。
これが新人エンジニアの一助になれば幸いです。
自己紹介
- 社会人5年目、業務未経験でRubyエンジニアとしてラグザイアに入社
- 前職はサービス業
- 神奈川県在住、フルリモートで働いています
調べた経緯(なぜN+1問題を調べることになったのか)
ラグザイアでは、研修として自分でRuby on Ralisアプリの要件定義から実装までを行うカリキュラムがあります。
私はこの研修で、GitHubのように活動履歴を可視化するアプリを開発しました。活動履歴を見える化することで、達成感を得やすくし、モチベーションを維持できると考えたからです。
アプリのイメージはこちらです。
ER図はこんな感じです。
本題
日報(Report)を可視化するためのCalenderクラスを作成し、目標一覧でカレンダーを表示する機能を実装している時にN+1問題でつまづきました。
修正前のコード
class GoalsController < ApplicationController
def index
@goals = current_user.goals.includes(:reports).order(:created_at).page(params[:page]).per(PAGINATE_PER)
@calendars = @goals.map do |goal|
Calendar.new(goal, 14.days).generate_line
end
end
end
class Calendar
def initialize(goal)
@goal = goal
@target_reports = build_target_reports
end
def build_target_reports
@goal.reports.where(target_date: @range_start_date..@range_last_date)
.select(:target_date, :progress_value)
.index_by(&:target_date)
end
end
指摘の内容
bullet gem(N+1を検知して警告してくれるジェム)に、@goals配列を取得するときのincludes(:reports)
を外すように警告されたので、疑問に思いながらも該当の記述を削除しました。
しかしその後ログを見ていると想定以上にクエリが発生しているのです。よく確認すると、current_userの目標を取得するクエリだけでなく、目標一覧に表示される目標の件数だけクエリが追加で発生しているようです。
研修担当に相談したところ、以下のアドバイスをいただきました。
whereではなくeachで回すようにすれば、includes(:reports)が使えるようになって実質的なN+1がなくなるかも
調べてまとめた内容
調べたところ、絞り込みに使用していたwhere
はクエリメソッドというものに分類されるようで、これを使うたびにクエリを発行してしまうようです。
参考: https://railsguides.jp/active_record_querying.html
つまり、修正前のコードは
- indexアクションの中でcurrent_userが作成したgoalを取得してgoalsに格納
- indexアクションの中で繰り返しCalenderクラスのインスタンスを生成しており、その度にgoalに紐づいているReportを取得する(whereを使用しているせいで、新しくクエリを発行している)
- goalsに収まっているgoalの件数だけ追加でクエリが発生
このような流れになっているようです。
修正後のコード
class Calendar
def build_target_reports
reports_hash = {}
@goal.reports.each do |report|
if (@range_start_date..@range_last_date).include?(report.target_date)
reports_hash[report.target_date] = { progress_value: report.progress_value }
end
end
reports_hash
end
end
- indexアクション内で、current_userが作成したgoalとその子のデータであるReportをgoals配列に収める
- build_target_reportsメソッドの中でgoals配列の中に含まれたReportの情報をeachを使用してハッシュに収める(追加でReport配列のデータを取得する必要がない)
- current_userの作成したgoalを取得するクエリとそのgoalに紐づいているReportのデータを取得するクエリの2回で終了する
これでクエリの発生件数を抑えることができました。
まとめ
whereを使った絞り込みはクエリを発生させてしまうということを認識していなかったのが原因でした。
N+1を防ぐには、繰り返し処理の中でクエリが複数回発行されていないかを意識して確認すること、includesやwhereなどのメソッドが発生させる挙動やクエリについて理解することが大切なのだと感じました。
新人エンジニアは、Railsガイドやリファレンスマニュアル等信頼できるドキュメントを確認する癖をつけていきましょう。
株式会社ラグザイア(luxiar.com)の技術広報ブログです。 ラグザイアはRuby on RailsとC#に特化した町田の受託開発企業です。フルリモートでの開発を積極的に推進しており、全国からの参加を可能にしています。柔軟な働き方で最新のソフトウェアソリューションを提供します。
Discussion