【Rails】each, find_each, find_in_batches の違い

# 前提
下記コードで取得される***company***の件数が`50件`である前提で進めます
```rb
Company.joins(:shops).where(shops: { prefecture: '東京都' }).distinct
```


# each

```rb:メインの確認コード
companies.each.with_index(1) do |company, i|
  puts "loop_count: #{i}, company_id: #{company.id}"
end
```

※何回目のループでどんなレコードが渡っているか、視覚的に分かりやすくするために[#with_index](https://docs.ruby-lang.org/ja/latest/method/Enumerator/i/with_index.html)を使用します。

```rb:rails c
[1] pry(main)> companies = Company.joins(:shops).where(shops: { prefecture: '東京都' }).distinct
  Company Load (5.3ms)  SELECT DISTINCT "companies".* FROM "companies" INNER JOIN "shops" ON "shops"."company_id" = "companies"."id" WHERE "shops"."prefecture" = $1  [["prefecture", "東京都"]]
=> [#<Company:0x00007f98ebc5b468
  id: 18,
  ### 省略 ###
[2] pry(main)> companies.size
=> 50
[3] pry(main)> companies.each.with_index(1) do |company, i|
[3] pry(main)*   puts "loop_count: #{i}, company_id: #{company.id}"
[3] pry(main)* end
loop_count: 1, company_id: 18
loop_count: 2, company_id: 40
loop_count: 3, company_id: 10
loop_count: 4, company_id: 127
loop_count: 5, company_id: 21
loop_count: 6, company_id: 125
loop_count: 7, company_id: 48
loop_count: 8, company_id: 22
loop_count: 9, company_id: 2
loop_count: 10, company_id: 64
loop_count: 11, company_id: 36
loop_count: 12, company_id: 25
loop_count: 13, company_id: 47
loop_count: 14, company_id: 9
loop_count: 15, company_id: 102
loop_count: 16, company_id: 77
loop_count: 17, company_id: 107
loop_count: 18, company_id: 71
loop_count: 19, company_id: 43
loop_count: 20, company_id: 3
loop_count: 21, company_id: 79
loop_count: 22, company_id: 81
loop_count: 23, company_id: 14
loop_count: 24, company_id: 114
loop_count: 25, company_id: 34
loop_count: 26, company_id: 19
loop_count: 27, company_id: 1
loop_count: 28, company_id: 17
loop_count: 29, company_id: 80
loop_count: 30, company_id: 33
loop_count: 31, company_id: 11
loop_count: 32, company_id: 55
loop_count: 33, company_id: 13
loop_count: 34, company_id: 53
loop_count: 35, company_id: 118
loop_count: 36, company_id: 76
loop_count: 37, company_id: 8
loop_count: 38, company_id: 128
loop_count: 39, company_id: 7
loop_count: 40, company_id: 84
loop_count: 41, company_id: 20
loop_count: 42, company_id: 16
loop_count: 43, company_id: 29
loop_count: 44, company_id: 101
loop_count: 45, company_id: 65
loop_count: 46, company_id: 50
loop_count: 47, company_id: 95
loop_count: 48, company_id: 91
loop_count: 49, company_id: 86
loop_count: 50, company_id: 54
=> [#<Company:0x00007f98ebc5b468
  id: 18,
  省略>,
 #<Company:0x00007f989b8bf6d8
  id: 40,
  省略>,
  ### 以降省略 ###
```

:::note warn
全てのレコードをメモリに読み込んでから実行している為、メモリ使用量には要注意！（特に大規模なデータの場合）
:::

※実際にデータがメモリに読み込まれるのは`#each`や`#map`、`#to_a`等のメソッドを呼び出した時点です。（クエリは遅延評価されます）

# find_each
https://railsdoc.com/page/find_each

>分割してレコードを取得して1件ずつ処理
デフォルトでは1000件ずつ処理 大きなデータをもつモデルなどを処理する時に使う
>
>- find_in_batchesとの違いは1件ずつ処理すること

|オプション  |説明  |デフォルト値  |
|---|---|---|
|:batch_size  |同時処理数  |1000  |
|:start  |処理開始位置  | |
|:finish  |終了するときのキー  | |
|:error_on_ignore  |例外を発生させる  | |
|:order  |主キーの順序を指定  |:asc |


実際に走っているSQLを、コンソールで確認します。
```rb:メインの確認コード
companies.find_each(batch_size: 20).with_index(1) do |company, i|
  puts "loop_count: #{i}, company_id: #{company.id}"
end
```


```rb:rails c
[1] pry(main)> companies = Company.joins(:shops).where(shops: { prefecture: '東京都' }).distinct
  Company Load (5.3ms)  SELECT DISTINCT "companies".* FROM "companies" INNER JOIN "shops" ON "shops"."company_id" = "companies"."id" WHERE "shops"."prefecture" = $1  [["prefecture", "東京都"]]
=> [#<Company:0x00007f989b847160
  id: 18,
  ### 省略 ###
[2] pry(main)> companies.size
=> 50
[15] pry(main)> companies.find_each(batch_size: 20).with_index(1) do |company, i|
[15] pry(main)*   puts "loop_count: #{i}, company_id: #{company.id}"
[15] pry(main)* end
  Company Load (1.9ms)  SELECT  DISTINCT "companies".* FROM "companies" INNER JOIN "shops" ON "shops"."company_id" = "companies"."id" WHERE "shops"."prefecture" = $1  ORDER BY "companies"."id" ASC LIMIT 20  [["prefecture", "東京都"]]
loop_count: 1, company_id: 1
loop_count: 2, company_id: 2
loop_count: 3, company_id: 3
loop_count: 4, company_id: 7
loop_count: 5, company_id: 8
loop_count: 6, company_id: 9
loop_count: 7, company_id: 10
loop_count: 8, company_id: 11
loop_count: 9, company_id: 13
loop_count: 10, company_id: 14
loop_count: 11, company_id: 16
loop_count: 12, company_id: 17
loop_count: 13, company_id: 18
loop_count: 14, company_id: 19
loop_count: 15, company_id: 20
loop_count: 16, company_id: 21
loop_count: 17, company_id: 22
loop_count: 18, company_id: 25
loop_count: 19, company_id: 29
loop_count: 20, company_id: 33
  Company Load (1.2ms)  SELECT  DISTINCT "companies".* FROM "companies" INNER JOIN "shops" ON "shops"."company_id" = "companies"."id" WHERE "shops"."prefecture" = $1 AND ("companies"."id" > 33)  ORDER BY "companies"."id" ASC LIMIT 20  [["prefecture", "東京都"]]
loop_count: 21, company_id: 34
loop_count: 22, company_id: 36
loop_count: 23, company_id: 40
loop_count: 24, company_id: 43
loop_count: 25, company_id: 47
loop_count: 26, company_id: 48
loop_count: 27, company_id: 50
loop_count: 28, company_id: 53
loop_count: 29, company_id: 54
loop_count: 30, company_id: 55
loop_count: 31, company_id: 64
loop_count: 32, company_id: 65
loop_count: 33, company_id: 71
loop_count: 34, company_id: 76
loop_count: 35, company_id: 77
loop_count: 36, company_id: 79
loop_count: 37, company_id: 80
loop_count: 38, company_id: 81
loop_count: 39, company_id: 84
loop_count: 40, company_id: 86
  Company Load (0.9ms)  SELECT  DISTINCT "companies".* FROM "companies" INNER JOIN "shops" ON "shops"."company_id" = "companies"."id" WHERE "shops"."prefecture" = $1 AND ("companies"."id" > 86)  ORDER BY "companies"."id" ASC LIMIT 20  [["prefecture", "東京都"]]
loop_count: 41, company_id: 91
loop_count: 42, company_id: 95
loop_count: 43, company_id: 101
loop_count: 44, company_id: 102
loop_count: 45, company_id: 107
loop_count: 46, company_id: 114
loop_count: 47, company_id: 118
loop_count: 48, company_id: 125
loop_count: 49, company_id: 127
loop_count: 50, company_id: 128
=> nil
```

:::note
一度に全てのレコードをメモリに読み込まず、指定した`batch_size`毎にSQLを実行してデータを読み込み、ブロック処理を行っていることが分かります
:::

:::note
デフォルトでは、`ID順（昇順）` で***ORDER***している
:::

[GitHub(find_each)](https://github.com/rails/rails/blob/984c3ef2775781d47efa9f541ce570daa2434a80/activerecord/lib/active_record/relation/batches.rb#L68-L79)

## eachとfind_eachのメモリ使用量の違い

メモリ使用量について検証している興味深い記事があったので、リンクを記載します。

https://chietech.com/2019/07/29/rails-performance-find-each

>find_each未使用
>3回実行し、MAXで72.43%ほどを使用していることが分かりました。
>
>find_each使用
>3回実行し、MAXで22.48%ほどを使用していることが分かりました。
>
>結論
>find_eachを使用することで、メモリの消費が大幅に抑えられることが分かりました。
>バッチなどの大量データを使用する場合にはfind_eachを使用し、メモリの消費を抑えることを意識した方が良いと思います。

:::note
find_eachは、メモリ削減において力を発揮してくれる（特に大規模なデータを扱う場合）
:::


## find_eachの注意点
:::note warn
使い方を間違えると、無限ループを起こして逆にメモリを食い潰すらしいです。
実装する際にこちらの注意点を把握しておくと良さそうなので、リンクを載せておきます。

[[Rails]find_eachが無限ループして本番環境のメモリを食いつぶした話](https://qiita.com/ham0215/items/f4b9d024585b5b9e9436)
:::

:::note
SELECT句で結合先テーブルのIDを指定する際は、結合元テーブルのIDにも（双方）明示的に別名（AS）をつけておくのが良さそう
:::

# find_in_batches
https://railsdoc.com/archive/6_0_2_1/page/find_in_batches

>分割してレコードを取得して処理
デフォルトで1000件ずつ処理
>
>処理する順序は指定できない
find_eachとの違いは配列で値を受けとること
順番に処理したい場合は、find_eachを使用

|オプション  |説明  |デフォルト値  |
|---|---|---|
|:batch_size  |同時処理数  |1000  |
|:start  |処理開始位置  | |
|:finish  |終了するときのキー  | |
|:error_on_ignore  |例外を発生させる  | |


こちらについても、実際に走っているSQLをコンソールで確認してみます。
```rb:メインの確認コード
companies.find_in_batches(batch_size: 20).with_index(1) do |company_group, i|
  puts "loop_count: #{i}, company_group.size: #{company_group.size}, class: #{company_group.class}"
  puts "company_ids: #{company_group.map(&:id)}"
  puts ''
end
```

```rb:rails c
[1] pry(main)> companies = Company.joins(:shops).where(shops: { prefecture: '東京都' }).distinct
  Company Load (5.3ms)  SELECT DISTINCT "companies".* FROM "companies" INNER JOIN "shops" ON "shops"."company_id" = "companies"."id" WHERE "shops"."prefecture" = $1  [["prefecture", "東京都"]]
=> [#<Company:0x00007f989b81e440
  id: 18,
  ### 省略 ###
[2] pry(main)> companies.size
=> 50
[3] pry(main)> companies.find_in_batches(batch_size: 20).with_index(1) do |company_group, i|
[3] pry(main)*   puts "loop_count: #{i}, company_group.size: #{company_group.size}, class: #{company_group.class}"
[3] pry(main)*   puts "company_ids: #{company_group.map(&:id)}"
[3] pry(main)*   puts ''
[3] pry(main)* end
  Company Load (3.2ms)  SELECT  DISTINCT "companies".* FROM "companies" INNER JOIN "shops" ON "shops"."company_id" = "companies"."id" WHERE "shops"."prefecture" = $1  ORDER BY "companies"."id" ASC LIMIT 20  [["prefecture", "東京都"]]
loop_count: 1, company_group.size: 20, class: Array
company_ids: [1, 2, 3, 7, 8, 9, 10, 11, 13, 14, 16, 17, 18, 19, 20, 21, 22, 25, 29, 33]

  Company Load (1.2ms)  SELECT  DISTINCT "companies".* FROM "companies" INNER JOIN "shops" ON "shops"."company_id" = "companies"."id" WHERE "shops"."prefecture" = $1 AND ("companies"."id" > 33)  ORDER BY "companies"."id" ASC LIMIT 20  [["prefecture", "東京都"]]
loop_count: 2, company_group.size: 20, class: Array
company_ids: [34, 36, 40, 43, 47, 48, 50, 53, 54, 55, 64, 65, 71, 76, 77, 79, 80, 81, 84, 86]

  Company Load (1.3ms)  SELECT  DISTINCT "companies".* FROM "companies" INNER JOIN "shops" ON "shops"."company_id" = "companies"."id" WHERE "shops"."prefecture" = $1 AND ("companies"."id" > 86)  ORDER BY "companies"."id" ASC LIMIT 20  [["prefecture", "東京都"]]
loop_count: 3, company_group.size: 10, class: Array
company_ids: [91, 95, 101, 102, 107, 114, 118, 125, 127, 128]

=> nil
```

:::note
こちらも、指定したbatch_size毎にSQLを実行し、Loadしていることが分かります
:::

:::note
一度に大量のレコードをメモリに読み込むことを避け、メモリ使用量を抑えている
:::

:::note
`ActiveRecord::Relation`を配列に直しても問題ない ∧ [バルクアップデートやバルクインサート](https://qiita.com/royal_straight_flush/items/89100ce58821caed6439)等、`batch_size`毎にまとめて処理を行うケースで有効そう
:::

[GitHub(find_in_batches)](https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/activerecord/lib/active_record/relation/batches.rb#L126-L138)


# in_batches（Rails5以降）
Rails5以降では、[#in_batches](https://railsdoc.com/page/in_batches) というメソッドも使うことが出来ます。
>find_in_batchesとの違いはActiveRecord::Relationで値を返す

|オプション  |説明  |デフォルト値  |
|---|---|---|
|:of  |同時処理数  |1000  |
|:load  |ロードするか  |false  |
|:start  |処理開始位置  | |
|:finish  |終了するときのキー  | |
|:error_on_ignore  |例外を発生させる  | |
|:order  |主キーの順序を指定  |:asc |

[GitHub(in_batches)](https://github.com/rails/rails/blob/984c3ef2775781d47efa9f541ce570daa2434a80/activerecord/lib/active_record/relation/batches.rb#L204-L265)


:::note
配列ではなく`ActiveRecord::Relation`を返すのは扱い易そうです。クエリをメソッドチェーンで追加することもできそうです
:::

:::note
オプションで`load`するかを決められる点も、`find_each`や`find_in_batches`とは違いますね
:::

:::note
`ActiveRecord::Relation`を配列に直すとデータ規模によってはメモリ使用量が増加しそうですが、こちらのメソッドはよりメモリ使用量の削減において力を発揮してくれそうです
:::

:::note warn
但し、パフォーマンス面においては処理速度を上げる工夫が必要なようです。

良い記事がありましたので、リンクを載せておきます。

[Railsでin_batches使うととても遅い](https://note.zochang.com/n/nf77b9e37ef46)
:::

そもそも`in_batches`を使わずに、
`ID配列.each_slice(1_000) { |ids| User.where(id: ids).each { |user| ... } }`
の方が処理パフォーマンスは良さげといった結論のようですw


ただ、私自身で確かめた訳ではない ∧ ケースによっては`#in_batches`にも他のメリットもある筈なので、あくまでも参考に留めます。


# 参考リンク
- [【Rails】 find_eachメソッドでメモリを節約して大量データを扱う方法](https://pikawaka.com/rails/find_each)
- [find_each（Railsドキュメント）](https://railsdoc.com/page/find_each)
- [find_in_batches（Railsドキュメント）](https://railsdoc.com/archive/6_0_2_1/page/find_in_batches)
- [in_batches（Railsドキュメント）](https://railsdoc.com/page/in_batches)
- [find_each（GitHub）](https://github.com/rails/rails/blob/984c3ef2775781d47efa9f541ce570daa2434a80/activerecord/lib/active_record/relation/batches.rb#L68-L79)
- [find_in_batches（GitHub）](https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/activerecord/lib/active_record/relation/batches.rb#L126-L138)
- [in_batches（GitHub）](https://github.com/rails/rails/blob/984c3ef2775781d47efa9f541ce570daa2434a80/activerecord/lib/active_record/relation/batches.rb#L204-L265)
- [【Rails】find_eachによるメモリの使用量の変化](https://chietech.com/2019/07/29/rails-performance-find-each)
- [[Rails]find_eachが無限ループして本番環境のメモリを食いつぶした話](https://qiita.com/ham0215/items/f4b9d024585b5b9e9436)
- [Railsでin_batches使うととても遅い](https://note.zochang.com/n/nf77b9e37ef46)