zdogma's diary

徒然なるエンジニアな日々。

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

なんとなく別物だと思い込んでしまっていたので、この背景を知ってスッキリしました。

参考にしたページ