find_each と find_in_batches の違い
ことはじめ
大量のデータにアクセスして処理を行うとき、全てメモリに展開するとよろしくないので、 少しずつメモリに展開しながら処理をしよう、というのはよくあるケースだと思いますが、 Rails を使っている場合は下記の2メソッドが候補に上がるかと思います。
- find_each
- find_in_batches
それぞれの使い分けがいまいちわかっていなかったので、調べてみました。
それぞれのメソッドについて
find_each
デフォルトで1,000件 ( batch_size で指定する)ずつデータを取得し、 取得したデータを 1件ずつ 処理する。
User.find_each(batch_size: 2) { |user| p user } => User Load (0.3ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 2 #<User id: 1, ..> #<User id: 2, ..> User Load (0.2ms) SELECT `users`.* FROM `users` WHERE (`users`.`id` > 2) ORDER BY `users`.`id` ASC LIMIT 2 #<User id: 3, ..> ..
find_in_batches
デフォルトで1,000件 ( batch_size で指定する)ずつデータを取得し、 取得したデータを 配列 で処理する。
User.find_in_batches(batch_size: 2) { |users| p users } => User Load (0.3ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 2 [#<User id: 1, ..>, #<User id: 2,..>] User Load (0.2ms) SELECT `users`.* FROM `users` WHERE (`users`.`id` > 2) ORDER BY `users`.`id` ASC LIMIT 2 [#<User id: 3, ..>, #<User id: 4,..>] ..
結局何が違うのか?
「find_each が1件ずつ処理するなら、batch_size を指定する意味ってなんなの?」
など、いろいろ気になるところがあったので、実際のコードを読んでみたところ、謎が解けました。
def find_each(options = {}) if block_given? find_in_batches(options) do |records| records.each { |record| yield record } end else enum_for :find_each, options do options[:start] ? where(table[primary_key].gteq(options[:start])).size : size end end end
ちょっとわかりづらいのですが、下記にあるように、 find_each は find_in_batches で取得した配列に each を噛ませている という実装だったのですね。
find_in_batches(options) do |records| records.each { |record| yield record } end
なんとなく別物だと思い込んでしまっていたので、この背景を知ってスッキリしました。