前提
下記コードで取得されるcompanyの件数が50件である前提で進めます
Company.joins(:shops).where(shops: { prefecture: '東京都' }).distinct
each
companies.each.with_index(1) do |company, i|
  puts "loop_count: #{i}, company_id: #{company.id}"
end
※何回目のループでどんなレコードが渡っているか、視覚的に分かりやすくするために#with_indexを使用します。
[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,
  省略>,
  ### 以降省略 ###
全てのレコードをメモリに読み込んでから実行している為、メモリ使用量には要注意!(特に大規模なデータの場合)
※実際にデータがメモリに読み込まれるのは#eachや#map、#to_a等のメソッドを呼び出した時点です。(クエリは遅延評価されます)
find_each
分割してレコードを取得して1件ずつ処理
デフォルトでは1000件ずつ処理 大きなデータをもつモデルなどを処理する時に使う
- find_in_batchesとの違いは1件ずつ処理すること
| オプション | 説明 | デフォルト値 | 
|---|---|---|
| :batch_size | 同時処理数 | 1000 | 
| :start | 処理開始位置 | |
| :finish | 終了するときのキー | |
| :error_on_ignore | 例外を発生させる | |
| :order | 主キーの順序を指定 | :asc | 
実際に走っているSQLを、コンソールで確認します。
companies.find_each(batch_size: 20).with_index(1) do |company, i|
  puts "loop_count: #{i}, company_id: #{company.id}"
end
[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
一度に全てのレコードをメモリに読み込まず、指定したbatch_size毎にSQLを実行してデータを読み込み、ブロック処理を行っていることが分かります
デフォルトでは、ID順(昇順) でORDERしている
eachとfind_eachのメモリ使用量の違い
メモリ使用量について検証している興味深い記事があったので、リンクを記載します。
find_each未使用
3回実行し、MAXで72.43%ほどを使用していることが分かりました。find_each使用
3回実行し、MAXで22.48%ほどを使用していることが分かりました。結論
find_eachを使用することで、メモリの消費が大幅に抑えられることが分かりました。
バッチなどの大量データを使用する場合にはfind_eachを使用し、メモリの消費を抑えることを意識した方が良いと思います。
find_eachは、メモリ削減において力を発揮してくれる(特に大規模なデータを扱う場合)
find_eachの注意点
使い方を間違えると、無限ループを起こして逆にメモリを食い潰すらしいです。
実装する際にこちらの注意点を把握しておくと良さそうなので、リンクを載せておきます。
SELECT句で結合先テーブルのIDを指定する際は、結合元テーブルのIDにも(双方)明示的に別名(AS)をつけておくのが良さそう
find_in_batches
分割してレコードを取得して処理
デフォルトで1000件ずつ処理処理する順序は指定できない
find_eachとの違いは配列で値を受けとること
順番に処理したい場合は、find_eachを使用
| オプション | 説明 | デフォルト値 | 
|---|---|---|
| :batch_size | 同時処理数 | 1000 | 
| :start | 処理開始位置 | |
| :finish | 終了するときのキー | |
| :error_on_ignore | 例外を発生させる | 
こちらについても、実際に走っているSQLをコンソールで確認してみます。
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
[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
こちらも、指定したbatch_size毎にSQLを実行し、Loadしていることが分かります
一度に大量のレコードをメモリに読み込むことを避け、メモリ使用量を抑えている
ActiveRecord::Relationを配列に直しても問題ない ∧ バルクアップデートやバルクインサート等、batch_size毎にまとめて処理を行うケースで有効そう
in_batches(Rails5以降)
Rails5以降では、#in_batches というメソッドも使うことが出来ます。
find_in_batchesとの違いはActiveRecord::Relationで値を返す
| オプション | 説明 | デフォルト値 | 
|---|---|---|
| :of | 同時処理数 | 1000 | 
| :load | ロードするか | false | 
| :start | 処理開始位置 | |
| :finish | 終了するときのキー | |
| :error_on_ignore | 例外を発生させる | |
| :order | 主キーの順序を指定 | :asc | 
配列ではなくActiveRecord::Relationを返すのは扱い易そうです。クエリをメソッドチェーンで追加することもできそうです
オプションでloadするかを決められる点も、find_eachやfind_in_batchesとは違いますね
ActiveRecord::Relationを配列に直すとデータ規模によってはメモリ使用量が増加しそうですが、こちらのメソッドはよりメモリ使用量の削減において力を発揮してくれそうです
そもそもin_batchesを使わずに、
ID配列.each_slice(1_000) { |ids| User.where(id: ids).each { |user| ... } }
の方が処理パフォーマンスは良さげといった結論のようですw
ただ、私自身で確かめた訳ではない ∧ ケースによっては#in_batchesにも他のメリットもある筈なので、あくまでも参考に留めます。
