Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Rails: Active Record関連付けのループではeachよりfind_eachを検討しよう(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

参考: 週刊Railsウォッチ20230314 Active Record関連付けのループはfind_each

なお、Rails 6.1.0からfind_each, find_in_batchesin_batchesメソッドでorder: :descオプションを指定可能になっています(#30590)。

参考: §2.2.1.1 find_eachのオプション -- Active Record クエリインターフェイス - Railsガイド
参考: PR Support order DESC for find_each, find_in_batches and in_batches by le0pard · Pull Request #30590 · rails/rails

Rails: Active Record関連付けのループではeachよりfind_eachを検討しよう(翻訳)

オブジェクトのグループを列挙する標準的なメソッドといえば、Rubyの配列でもRailsのActive Recordモデルでも、eachです。

しかし、大量のデータをループで処理すると(あるモデルの全レコードでデータを入れ直すなど)、多数のレコードを読み込むときにも処理するときにも重大なメモリ問題やスピード低下問題が発生する可能性があります。

そういうときは、ActiveRecord::Batchesが提供する機能を検討すべきです。ここではその中からfind_eachを使ってデモを行います。

以下のように書くのではなく

多数のActive Recordオブジェクトを.eachで回す。

post.comments.each do |comment|
  # 各`comment:`に対して何かするジョブをエンキューする
end

以下のように書く

.find_eachでデータベースからレコードを効率よく読み込む。

post.comments.find_each do |comment|
  # 各`comment:`に対して何かするジョブをエンキューする
end

そうする理由

eachを使うと、「1個の」SQL呼び出しをデータベースに送信してオブジェクトのセット全体をメモリに読み込み、それからループ処理を行います。これは、最初の例で post.comments.all.eachを呼び出すのと同じです。

これは2つの点で問題になります。
第1に、データベースクエリの実行に長時間を要する可能性があり、タイムアウトの可能性もあります。
第2に、ループ処理のために全レコードをメモリに読み込むので、データを返すときに(返せたとしても)メモリを大量に消費する可能性があります。

.find_eachなら、(多くの賢いデフォルト設定を持つ)効率の良い複数のSQLクエリを作成することでデータベースからレコードを取得します。これは多くの場合、一度に全レコードをメモリに読み込むよりもずっと効率が高まります。

そうしない理由があるとすれば

.find_eachはループ中のソートに主キーしか使わないので、レコードを特定のソート順で表示する必要がある場合についてはサポートされません。モデルでUUID主キーを使っている場合は、この理由によってfind_eachが期待通りに動かなくなります(理由: UUIDはシーケンシャルではありません)。つまり、ループ中に新しいデータが追加されると、レコードがスキップされる可能性があります(lainの指摘に感謝します)。

レコードをその場で変更する必要がある場合は、このようなループ処理は理想的ではありません。各レコードに対して#updateを実行すると「大量の」クエリを実行することになります。代わりに、#update_allなどのより適切なバルク更新メソッドを検討しましょう。

ビューのコードを生成する場合は、eachfind_eachも使わないでください。レコードが何件あるかもわからない状態で、巨大なコレクションをビューのコードでループ処理すると、ページが遅くなってユーザーエクスペリエンスを損なう原因になります。ページネーションを検討しましょう。

Railsガイドによると、find_eachが必要になるのは一度にメモリに乗せきれないような大量のレコードを処理する場合だけだそうです。レコード数が1000件に満たない場合のループ処理であれば通常のメソッドで問題ありませんし、実際推奨されています。

規模がさらに拡大すると、長時間のループを克服するためにもっと高度な手法が必要になります。おそらく、データベースへの大量の読み書きを削減する、あるいは数時間かかる実行を分単位に減らすなどの対応を取りたくなるでしょう。

関連記事

Rails: SidekiqはActive Jobを経由せずに直接使おう(翻訳)

Rails: ビューのパーシャルではローカル変数だけを使うこと(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。