【Rails】find_eachで大量データのループ処理を改善する

仕事でパフォーマンス改善のタスクをしていたんですが、大量データのループ処理をどう改善すればよいかわからなかったため、備忘録としてまとめます。

バージョン

  • 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メソッドは、次の流れを繰り返して処理を実行します。

  1. データベースから一定数のレコードだけを取得
  2. 各レコードに対する処理を実行
  3. 次のレコードをまた一定数だけ取得する

一度に読み込むレコード数を制限できるため、メモリの消費を抑えることができるんです。

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エンジニアになりたいと考えている方向けにおすすめの技術書を紹介しています。

こちらの記事もぜひ読んでみてください。

あわせて読みたい
【実務経験1年以上向け】Rubyで設計を学べる技術書3選 Rubyで設計を学べる技術書を知りたくないですか?この記事ではRubyエンジニア向けにサンプルコードがRubyで書かれていて設計を学べる技術書を3つ紹介しています。自分に必要なのはどの1冊なのか、ぜひ考えてみてください。
参考文献
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

未経験でSESから従業員300名以上の自社開発企業に転職しました。業務や個人開発で直面した問題や、転職・学習の経験を発信していきます。

コメント

コメントする

目次