仕事でパフォーマンス改善のタスクをしていたんですが、大量データのループ処理をどう改善すればよいかわからなかったため、備忘録としてまとめます。
バージョン
- Ruby 3.0.3
- Rails 6.1.7
記事の信頼性
- ぼくは独学で未経験から従業員300名以上の自社開発企業へ転職しました。
- 実務ではVue.jsとRailsを毎日書いています。
- 初心者や駆け出しエンジニアがつまづくポイントも身をもってよく理解しています。
問題
パフォーマンス改善のタスクを進める中で、次のようなコードを見つけました。
def execute
Task.where(id: task_ids, status: :active).each do |task|
// 処理が続く
end
end
このtask_ids
は見てのとおりTaskモデルのidの配列なんですが、数がとても多いです。(数万以上)
このままだと数万以上のtask
をすべて取得してからループ処理を実行してしまうため、メモリの消費量が多くなることが予想されました。
しかし、そうは言ってもどう修正するのがよいかわかりませんでした、、。
解決方法
結論として、each
メソッドではなくfind_each
メソッドを使うことでメモリを圧迫せずに済むことがわかりました。
def execute
# each ではなく find_each を使う
Task.where(id: task_ids, status: :active).find_each do |task|
// 処理が続く
end
end
each
メソッドはデータベースから取得したすべてのレコードをメモリに読み込んでから、各レコードに対して処理を実行します。
一方のfind_each
メソッドは、次の流れを繰り返して処理を実行します。
- データベースから一定数のレコードだけを取得
- 各レコードに対する処理を実行
- 次のレコードをまた一定数だけ取得する
一度に読み込むレコード数を制限できるため、メモリの消費を抑えることができるんです。
find_each
メソッドが取得するレコード数はデフォルトで1000件ずつです。
もし設定を変えたい場合は、次のようにbatch_size
オプションを指定すればOKです。
def execute
# 500件ずつレコードを取得する
Task.where(id: task_ids, status: :active).find_each(batch_size: 500) do |task|
// 処理が続く
end
end
これでパフォーマンスを改善することができました。
まとめ
数千件程度のレコードであればeach
メソッドで十分ですが、大量のレコードに対してループ処理をする場合、find_each
を使うことでメモリ消費を抑えることができます。
大規模なRuby on Rails開発に携わる場合、こういったパフォーマンスを考慮した実装をすることはとても重要なので、しっかりマスターしておきましょう。
また、以下の記事ではワンランク上のRailsエンジニアになりたいと考えている方向けにおすすめの技術書を紹介しています。
こちらの記事もぜひ読んでみてください。
コメント