Ruby on Railsでデータベースから大量のレコードを抽出してループ処理を行う場合、eachメソッドを使用するとメモリ不足になる可能性があります。そのような場合に、find_eachメソッドを使用することで、メモリ消費を抑えつつ安全にバッチ処理を行うことができます。
eachメソッドによるループ処理
# app/models/example.rb
class Example < ApplicationRecord
end
# メモリー不足になる可能性のあるスクリプト
# script/example_batch.rb
Example.all.each do |example|
do_some_batch(example)
end
上記のスクリプトでは、examplesテーブルの全レコードを一度にメモリへロードし、各レコードのインスタンスを生成してからeachループを開始します。この処理に対応するSQLは以下のようになります。
# SQL
SELECT "examples".* FROM "examples";
テーブルに数万件以上のレコードが保存されている場合、この方法ではメモリ不足が発生し、スクリプトがクラッシュしたり、スワップが発生したりする可能性があります。
find_eachメソッドによるバッチ処理
この問題を解決するために、eachメソッドの代わりにfind_eachメソッドを使用します。
# script/example_batch.rb
Example.all.find_each do |example|
do_some_batch(example)
end
find_eachメソッドは、DBテーブルのレコードを一定数ごとに分割し、バッチ処理を行います。デフォルトのバッチサイズは1,000レコードで、最初の1,000レコードのループ処理が完了すると、次の1,000レコードをメモリにロードして処理を繰り返します。
find_eachメソッドのSQLログを確認すると、eachメソッドとの違いが明確になります。
# 最初のバッチのSQL
SELECT "examples".* FROM "examples" ORDER BY "examples"."id" ASC LIMIT $1 [["LIMIT", 1000]]
# 2回目のバッチのSQL
SELECT "examples".* FROM "examples" WHERE "examples"."id" > $1 ORDER BY "examples"."id" ASC LIMIT $2 [["id", 1000], ["LIMIT", 1000]]
上記のように、find_eachメソッドは内部で自動的にバッチ処理を行うため、DBレコード数が非常に多い場合でもメモリを圧迫せずに処理できます。
また、SQLのORDER BY句からわかるように、find_eachメソッドは内部でプライマリIDの昇順にレコードを処理します。そのため、orderメソッドで並び順を指定しても無視され、警告が表示されるので注意が必要です。
# orderメソッドによる並び替えは無視され、警告が表示される
Example.all.order("created_at DESC").find_each do |example|
end
# 警告メッセージ
Scoped order is ignored, it's forced to be batch order.
まとめ
Railsで大量のDBレコードをループ処理する際は、find_eachメソッドによるバッチ処理を利用することで、メモリ不足を回避し安全に処理を行うことができます。
