ITエンジニアによるITエンジニアのためのブログ

Railsのfind_eachで安全なバッチ処理を実装する

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メソッドによるバッチ処理を利用することで、メモリ不足を回避し安全に処理を行うことができます。