仕事でパフォーマンス改善のタスクをしており、N+1問題を解消しようとしていました。その際にeager_load
とpreload
のどちらを使うべきか迷ったため、備忘録として判断基準をまとめます。
バージョン
- Ruby 3.0.3
- Rails 6.1.7.7
記事の信頼性
- ぼくは独学で未経験から従業員300名以上の自社開発企業へ転職しました。
- 実務ではVue.jsとRailsを毎日書いています。
- 初心者や駆け出しエンジニアがつまづくポイントも身をもってよく理解しています。
問題
パフォーマンス改善のタスクで、N+1問題が発生している箇所を見つけました。
修正のためには以下の箇所で関連モデル(Category、User、Company、Summary、MetaInfo、Review)の事前読み込みが必要です。
# where句の後に事前読み込みの処理を入れたい
Task.where(id: task_ids, status: :suspended).each do |task|
# 中身の処理は省略
end
この6モデルはすべてTaskモデルに対してbelongs_to
かhas_one
の関係にあります。
この場面でeager_load
とpreload
のどちらを使うべきか、判断がつきませんでした。
# 以下のどちらが最適か判断がつかない
Task.where(id: task_ids, status: :suspended)
.eager_load(:user, :company, :summary, category: [:metainfo, :review]).each do |task|
# 中身の処理は省略
end
Task.where(id: task_ids, status: :suspended)
.preload(:user, :company, :summary, category: [:metainfo, :review]).each do |task|
# 中身の処理は省略
end
現在の状況をまとめておきます。
- テーブル数: 6つ
- 各テーブルのカラム数: 20以上
- 各テーブルのレコード数: 数万以上
解決方法
結論から言うと、今回はpreload
を採用することになりました。
以下に、eager_load
とpreload
それぞれの特徴と、今回preload
を選択した理由をまとめます。
eager_load を使う場合
eager_load
は、SQLのJOIN句を使って関連テーブルのデータを1つのクエリで取得します。
今回のように結合するテーブルが6つあり、各テーブルのカラム数が20以上ある場合、JOIN句が複雑になってしまいます。
そして複雑なJOIN句は、データベースのオプティマイザにとってクエリの最適化が難しくなります。
さらにレコード数も多いため、1つのクエリで大量のデータを取得すると実行時間が長くなる可能性があると考えました。
preload を使う場合
preload
は、メインのクエリとは別に、関連テーブルのデータを個別のクエリで取得します。
今回のケースだと、メインのクエリとは別に、6回のクエリを発行するイメージです。
シンプルなクエリを複数発行することになるため、テーブル結合のコストを回避でき、クエリの複雑さに起因する問題は発生しにくいです。
もちろん、クエリの数が増える分だけパフォーマンスは悪化しますが、今回のようなデータ量の場合はpreload
の方が適していると判断しました。
preload を選択した理由
1. テーブル結合のコストを回避できる
2. クエリの複雑さに起因する問題が発生しにくい
3. データ量が多い場合、シンプルなクエリを複数発行する方が効率的
ローカル環境に大量にデータを用意して両者のパフォーマンスを10回ずつ計測してみたんですが、平均値・中央値・ブレ幅ともにpreload
の方が優れていました。
10回の中にはeager_load
のパフォーマンスが優れていたときもあったので、明確な差とまでは言い切れないんですが、安定感も含めてpreload
を採用しました。
おわりに
今回のようにデータ量が多い場合は、preload
を使ってクエリを分割することで、パフォーマンスの改善が期待できると思いました。
preload
を使うとバカでかいクエリを発行してしまうリスクを回避できるため、ある程度重い処理のときはpreload
を使う方がベターなのかな?と感じました。
参考になれば幸いです。
コメント