分割してデータを読み込むことでメモリの消費を抑える
each
はデータをすべてメモリにのせてからループ処理を実行するため、大量のデータを扱う場合はメモリを圧迫させる可能性があります。
たとえば以下の例では、booksテーブルの全レコードがActive Recordオブジェクトに変換され、全オブジェクトをまとめた配列がメモリへ配置されることになります。
### データが多いとメモリを大量に消費するダメな実装
Book.all.each do |book|
# 処理
end
Railsで大量のデータをループ処理する場合、データを割してメモリにのせるin_batches・find_in_batches・find_eachを利用することでメモリ消費が抑えられます。
in_batches
・find_in_batches
・find_each
はメソッドによってブロックへ渡すオブジェクトが異なります。
各メソッドとブロックへ渡すオブジェクトの対応は以下の通りです。
メソッド | ブロックへ渡すオブジェクト |
---|---|
in_batches | ActiveRecord::Relation |
find_in_batches | Array |
find_each | Active Record |
in_batches
・find_in_batches
・find_each
の違いの詳細解説は【Rails】in_batches、find_in_batches、find_eachの違いと使いどころで紹介しています。
大量のデータをループ処理する場合はeach
の代わりにin_batches
・find_in_batches
・find_each
を利用するとよいでしょう。
特にfind_each
はeach
と同様に1件ずつActive Recordをブロックに渡すので、eachのループ処理を省メモリで行いたいときに向いているメソッドです。
ただし、in_batches
・find_in_batches
・find_each
ではデータのソートは無視される注意が必要です。
以下の例をみてわかるように、find_each
の処理順にBook.order("price DESC")
は適用されていません。
ソースコード
### orderは適用されない
Book.order("price DESC").find_each(batch_size: 10) do |book|
print "#{book.price}, "
end
実行ログ
Scoped order is ignored, it's forced to be batch order.
Book Load (0.7ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 10
780, 780, 640, 1080, 1080, 1020, 1040, 800, 1020, 640,
Book Load (0.7ms) SELECT `books`.* FROM `books` WHERE `books`.`id` > 10 ORDER BY `books`.`id` ASC LIMIT 10
660, 940, 800, 600, 830, 940, 740, 900, 620, 870,
Book Load (0.7ms) SELECT `books`.* FROM `books` WHERE `books`.`id` > 20 ORDER BY `books`.`id` ASC LIMIT 10
940, 600, 1000, 730, 850, 1050, 680, 720, 760, 910,
一括更新を活用して処理時間を短縮する
大量のデータを更新する場合、1件ずつ処理をすると頻繁にデータベースへアクセスすることになるため時間がかかります。
たとえば以下のようにすると『booksテーブルにある全レコードのpriceを100増やす』という処理が1件ずつ処理されます。
### 1件ずつsaveが実行されるので時間がかかる
Book.find_each do |book|
book.price += 100
book.save
end
update_all
を利用してデータを一括更新することでデータベースへのアクセスが減り、処理時間を短縮できます。
in_batches
とupdate_all
を利用してデータの一括更新を実装した例が以下になります。
### 一括更新によって処理時間が短縮される
Book.in_batches do |relation|
relation.update_all("price = price + 100")
end
ただし、update_all
はバリデーションとコールバックが実行されずレコードのupdate_at
も更新されません。1 2
そのため、『each + save』と『in_batches + update_all』の挙動は不等価なので注意が必要です。
バリデーションをスキップしても問題ないシチュエーションであれば、処理時間の観点から一括更新を採用するとよいでしょう。
まとめ
- メモリの消費を抑えるためにデータは分割して読み込む
- eachではなくin_batches・find_in_batches・find_eachを利用してデータを読み込む
- 処理時間を短縮させるためにレコードは一括更新する
- データ更新はsaveではなくupdate_allを活用する
- update_allはバリデーションやコールバックが実行されないので使用する際は注意する
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!