すでにメンバーの場合は

無料会員登録

GitHubアカウントで登録 Pikawakaが許可なくTwitterやFacebookに投稿することはありません。

登録がまだの方はこちらから

Pikawakaにログイン

GitHubアカウントでログイン Pikawakaが許可なくTwitterやFacebookに投稿することはありません。

Rails

【Rails】 find_eachメソッドでメモリを節約して大量データを扱う方法

ぴっかちゃん
ぴっかちゃん

find_eachメソッドは、大量のレコードをループ処理する際にメモリの消費量を抑えることができるメソッドです。デフォルトでは、レコードを1000件ごとに取得して処理を実行します。

例えば、以下の動画のようにusersテーブルには1万件のレコードがあり、この大量のレコードを取得してループ処理を実行したいとします。

レコードが1万件存在するusersテーブル

しかし、1万件のレコードを一度に取得してループ処理を実行すると、メモリが圧迫されてアプリケーションの動作が遅くなったり、フリーズしてしまう恐れがあります。

そこで、find_eachメソッドを使用します。以下のコードのようにレコードを一度に取得するのではなく、1000件ごとに取得してループ処理をするのでメモリの消費量を抑える事が出来ます。

Console | find_eachメソッドのサンプルコード
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[1] pry(main)> User.find_each do |user|
[1] pry(main)*  p user.id #処理(ユーザーのidを出力する)
[1] pry(main)*  end

#最初の1000件を取得して処理を実行する
SELECT  `users`.* FROM `users` WHERE `users`.`id` > 1000 ORDER BY `users`.`id` ASC LIMIT 1000
1  #1~1000まで出力される
2
3
.
.
1000 

#次の1000件を取得して処理を実行する
SELECT  `users`.* FROM `users` WHERE `users`.`id` > 2000 ORDER BY `users`.`id` ASC LIMIT 1000
1001  #1001~2000まで出力される
1002
1003
.
.
2000
#1万件まで1000件に区切って処理が実行される

usersテーブルから最初の1000件を取得すると、ブロックに1件ずつ渡してループ処理が実行されるのでidの1~1000が出力されます。処理が終わると、次の1000件のレコードを取得して同じように処理を繰り返します。

以下の動画からも分かるように、1万件まで繰り返し実行されていますね。

1万件までバッチ処理が行われる様子

find_eachメソッドは、大量のレコードに対してループ処理を実行したい場合にメモリ不足を防ぐことが出来るメソッドなので、是非使い方を理解していきましょう!

find_eachメソッドの使い方

find_eachメソッドの使い方について、基本構文から発行されるSQL、必要性、似ているメソッドとの違いまで幅広く解説します。この記事を読めば、あらゆる視点からfind_eachメソッドを学ぶことが出来ます。

基本構文

find_eachメソッドは、モデルのクラスに対して呼び出すことが出来るメソッドです。

基本構文-->
1
2
3
モデル名.find_each do |変数|
  # 処理
end

モデルに対応するテーブルのレコードをデフォルトで1000件に区切って取得し、ループ処理が実行されます。この処理の流れは、サンプルコードを使って1つ1つみていきましょう。

処理の流れを確認しよう

find_eachメソッドの処理の流れは、以下のサンプルコードを使って確かめていきます。

find_eachメソッドのサンプルコード-->
1
2
3
User.find_each do |user|
  p user.id 
end

上記のコードが実行されると、以下の画像のような順番で処理が実行されます。

find_eachメソッドの挙動

上記の流れをまとめると、以下の通りです。

  1. usersテーブルから1000件(id: 1~1000)のレコードを取得する
  2. 取得したレコードをUserモデルのインスタンスとして1件ずつブロックに渡して、ユーザーidを出力させる
  3. 最後のレコード(id: 1000)の処理が終了したら、次の1000件(id: 1001~2000)のレコードを取得して処理を実行する、これを1万件まで繰り返す

これにより、find_eachメソッドを使うとデフォルトで1000件ごとにレコードを取得してループ処理が行われていることが分かりますね。

ポイント
  1. デフォルトでレコードを1000件ごとに取得する
  2. 取得したレコードはモデルのインスタンスとして1件ずつブロックに渡される
  3. 1000件の処理が終わったら、次の1000件を取得して処理をする

発行されるSQLクエリとは?

次は、find_eachメソッドによって発行されるSQLクエリをみていきましょう。

find_eachメソッドのサンプルコード-->
1
2
3
User.find_each do |user|
  p user.id 
end

上記のコードを実行した際に、最初に発行されるSQLクエリは以下の通りです。

最初に発行されるSQL
1
2
3
4
5
6
7
8
9
10
SELECT
  `users`.* 
FROM 
  `users` 
WHERE 
  `users`.`id` > 1000 
ORDER BY 
  `users`.`id` ASC 
LIMIT
  1000

ここでの注目するポイントは、ORDER BY句LIMIT句です。

ORDER BY句は、指定したカラムを対象にソートします。そのため上記のORDER BY users.id ASCは、usersテーブルの主キーを対象に昇順でソートされます。

そして、LIMIT句は取得するレコードの上限を決めることが出来るので、上記のLIMIT 1000では取得するレコードを1000件に制限しています。

ORDER BY句とLIMIT句の解説

このようにデフォルトでは、取得するレコードの上限が1000件に制限されて、主キーを対象に昇順でソートされます。

whereメソッドと使う

以下のコードのように、whereメソッドで取得するレコードの条件を指定した後にfind_eachメソッドを使うことが出来ます。

Console | whereメソッドと使う場合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[1] pry(main)> User.where("age >= 80").find_each do |user|
[1] pry(main)*   p user.id #ageカラムの値が80以上のユーザーidを出力
[1] pry(main)* end

#ageカラムの値が80以上の最初の1000件のレコードを取得する
SELECT  `users`.* FROM `users` WHERE (age >= 80) ORDER BY `users`.`id` ASC LIMIT 1000
23
26
28
.
.
.
8076

#ageカラムの値が80以上の次の1000件のレコードを取得する
SELECT  `users`.* FROM `users` WHERE (age >= 80) AND `users`.`id` > 8076 ORDER BY `users`.`id` ASC LIMIT 1000
8081
8083
8087
.
.
.
9999

上記のコードは、whereメソッドで「ageカラムの値が80以上」のレコードを取得するように指定したので、その条件に合うレコードをfind_eachメソッドによって1000件ごとに取得して処理を実行しています。

以下は、最初に発行されるSQLクエリですが、WHERE句で(age >= 80)の指定がされていますね。

SQL | 最初に発行されるSQLクエリ
1
SELECT  `users`.* FROM `users` WHERE (age >= 80) ORDER BY `users`.`id` ASC LIMIT 1000

このように大量のレコードを扱う場合は、whereメソッドで条件を指定すれば取得するレコード自体を減らすことが出来るのでメモリ消費量もさらに抑えることが出来ます。

find_eachメソッドの必要性

テーブルからレコードを取得してループ処理するには、以下のコードのようにallメソッドeachメソッドを使用した方法も考えられるでしょう。しかし、以下のコードは大量のレコードを扱う場合には注意が必要です。

eachメソッドを使う場合-->
1
2
3
User.all.each do |user|
  p user.id
end

上記のコードを実行すると、以下のような挙動になります。

Console | 1万件のレコードに実行する場合の挙動
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[1] pry(main)> User.all.each do |user|
[1] pry(main)*  p user.id
[1] pry(main)* end

#usersテーブルから全てのレコードを一度に取得する
SELECT `users`.* FROM `users`
1
2
3
.
.
.
10000

#返り値
=> [#<User:0x00007fd6eb4318d0
  id: 1,
  name: "Arnita Crooks",
  age: 65,
  created_at: Mon, 05 Oct 2020 12:03:31 UTC +00:00,
  updated_at: Mon, 05 Oct 2020 12:03:31 UTC +00:00>,,,,,]
#1万件のインスタンスが格納された配列が返る

裏側で発行されるSQLクエリからも分かるように、usersテーブルから1万件のレコードを一度に取得しています。しかも、その1万件のレコードが全てUserモデルのインスタンスとなって配列に格納されるので、大量のメモリが消費されます。

まだレコードが1万件なので動いてはいますが、数百・数千万件のレコードになるとメモリが不足してアプリケーションの動作が遅くなったり、フリーズしてしまう恐れがあります。

このような事態を引き起こさないために、find_eachメソッドを使ってループ処理をします。

find_eachメソッドを使う場合
1
2
3
User.find_each do |user| #1000件ごとにレコードを取得する
  p user.id
end

1000件ごとにレコードを取得し、モデルのインスタンスとして1件ずつブロック渡して処理を実行することでメモリ消費量を抑えることが出来ます。

今回のfind_eachメソッドのように、実務向けのややハイレベルなRuby on Railsの知識について学びたい方は、こちらの参考書が良いでしょう。Rails 6.0も含めて広い範囲を体系的に学ぶことができます。

オプション

find_eachメソッドに設定することが出来るオプションを紹介します。

オプションを指定する場合
1
モデル名.find_each(オプション: )

:batch_size

:batch_sizeは、レコードの分割数を指定することが出来ます。

例えば、以下のコードのように:batch_sizeを4000に指定すると、レコードを4000件ごとに取得してループ処理を実行します。

Console | レコードの分割数を4000に指定した場合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[1] pry(main)>User.find_each(batch_size: 4000) do |user|
[1] pry(main)* p user.id
[1] pry(main)* end

#最初の4000件のレコードを取得
SELECT  `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 4000
1
2
3
.
.
.
4000

#次の4000件のレコードを取得
SELECT  `users`.* FROM `users` WHERE `users`.`id` > 4000 ORDER BY `users`.`id` ASC LIMIT 4000
4001
.
.
.
8000

デフォルトでは1000件ごとにレコードを取得しますが、:batch_sizeを指定することで発行するSQLクエリがLIMIT 4000になり、レコードを4000件ごとに取得することが出来ています。

レコードの分割数を指定したい場合は:batch_sizeのオプションを設定しましょう。

:start

:startは、レコードの取得を開始する主キーの値を指定することが出来ます。

例えば、以下のコードのように:startを5000に指定すると、主キーの値が5000以上のレコードを1000件ごとに取得してループ処理を実行します。

Console | レコードの取得を開始する主キーを5000に指定した場合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[1] pry(main)>User.find_each(start: 5000) do |user|
[1] pry(main)*  p user.id
[1] pry(main)*  end

#最初の1000件のレコードを取得
SELECT  `users`.* FROM `users` WHERE `users`.`id` >= 5000 ORDER BY `users`.`id` ASC LIMIT 1000
5000
5001
5002
.
.
.
5999

#次の1000件のレコードを取得
SELECT  `users`.* FROM `users` WHERE `users`.`id` >= 5000 AND `users`.`id` > 5999 ORDER BY `users`.`id` ASC LIMIT 1000
6000
6001
6002
.
.
.
6999

デフォルトでは、主キーを対象に昇順で取り出されます。上記のコードは、主キーの値が5000以上10000以下のレコードを昇順で1000件ごとに取得されていますね。

:finish

:finishは、レコードの取得を終了する主キーの値を指定することが出来ます。

例えば、以下のコードのように:finishを2000に指定すると、主キーの値が2000以下までのレコードを1000件ごとに昇順で取得します。

Console | レコードの取得を終了する主キーの値を2000に指定した場合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[1] pry(main)>User.find_each(finish: 2000) do |user|
[1] pry(main)*  p user.id
[1] pry(main)*  end

#最初の1000件を取得する
SELECT  `users`.* FROM `users` WHERE `users`.`id` <= 2000 ORDER BY `users`.`id` ASC LIMIT 1000
1
2
3
.
.
.
1000

#次の1000件を取得する
SELECT  `users`.* FROM `users` WHERE `users`.`id` <= 2000 AND `users`.`id` > 1000 ORDER BY `users`.`id` ASC LIMIT 1000
1001
1002
1003
.
.
.
2000

上記のコードは、主キーの値が2000を超えるレコードは取得されません。

また、以下のコードのように:startと併用して開始から終了までの主キーの値を指定することも出来ます。

Console | 開始から終了までの主キーの値を指定した場合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[2] pry(main)>User.find_each(start: 2001, finish: 4000) do |user|
[2] pry(main)*  p user.id
[2] pry(main)*  end

#最初の1000件を取得する
SELECT  `users`.* FROM `users` WHERE `users`.`id` >= 2001 AND `users`.`id` <= 4000  ORDER BY `users`.`id` ASC LIMIT 1000
2001
2002
2003
.
.
.
3000

#次の1000件を取得する
SELECT  `users`.* FROM `users` WHERE `users`.`id` >= 2001 AND `users`.`id` <= 4000 AND `users`.`id` > 3000 ORDER BY `users`.`id` ASC LIMIT 1000
3001
3002
3003
.
.
.
4000

上記のコードは、主キーの値が2001以上4000以下のレコードを1000件ごとに昇順で取得してループ処理を実行することが出来ます。

ポイント
  1. :batch_sizeは、レコードの分割数が指定できる
  2. :startは、レコードの取得を開始する主キーの値が指定できる
  3. :finishは、レコードの取得を終了する主キーの値が指定できる

注意点

find_eachメソッドを使用する際の注意点について解説します。

用途

find_eachメソッドは、テーブルのレコードを取得してループ処理をすることが出来ますが、メモリが圧迫してしまう大量のレコードに対して、レコードを分割して処理することでメモリ不足を防ぐためのものです。

そのため、1000件以下のレコードに対して単にループ処理を行う場合は、eachメソッドなど他のメソッドを使用するようにしましょう。

大量のレコードを扱う場合に、メモリ消費量を抑えたい場合-->
1
2
3
User.find_each do |user|
  p user.id 
end
1000件以下のレコードを単にループ処理したい場合-->
1
2
3
User.all.each do |user|
  p user.id 
end

ソート順

find_eachメソッドは、ソート順を保持しません。

そのため、以下のコードのようにorderメソッドでidを対象に降順でレコードを取得するように指定しても、実際には昇順で取得されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
[1] pry(main)>User.order(id: :desc).find_each do |user|
[1] pry(main)*  p user.id
[1] pry(main)*  end

#最初の1000件を取得する
SELECT  `users`.* FROM `users ORDER BY `users`.`id` ASC LIMIT 1000
1
2
3
.
.
.
1000

発行されるSQLクエリも、ORDER BY句で users.id ASCが指定されているのが分かりますね。このようにソート順は保持されないので注意してください。

find_in_batchesメソッドとの違い

find_in_batchesメソッドは、find_eachメソッドと同じように1000件ごとにレコードを取得して、処理を実行することが出来ます。

find_in_batchesメソッドの基本構文-->
1
2
3
モデル名.find_in_batches do |変数|
  # 処理
end

しかし、find_eachメソッドが取得した1000件のレコードをモデルのインスタンスとして個別に1件ずつブロックへ渡すのに対して、find_in_batchesメソッドは取得した1000件のレコードをモデルの配列としてブロックに渡します。

find_eachメソッドを使用した場合 -->
1
2
3
User.find_each do |user|
  p user.id #1人のユーザー情報が渡される
end
find_in_batchesメソッドを使用した場合-->
1
2
3
User.find_in_batches do |users|
  p users #1000人のユーザー情報が配列として渡される
end

上記のfind_in_batchesメソッドのコードを実行すると、以下の動画のように1000件ごとに取得したレコードをモデルの配列として渡されているのが分かりますね。

find_in_batchesの挙動

ポイント
  1. find_eachメソッドは、ブロックにモデルのインスタンスを1件ずつ個別に渡す
  2. find_in_batchesメソッドは、ブロックに1000件のレコードをモデルの配列として渡す

in_batchesメソッドとの違い

in_batchesメソッドは、Rails5以降から使用することが出来るメソッドです。

in_batchesメソッドの基本構文-->
1
2
3
モデル名.in_batches do |変数|
  # 処理
end

先ほどのfind_in_batchesメソッドは、取得した1000件のレコードをモデルの配列としてブロックに渡していましたが、in_batchesメソッドはActiveRecord::Relationをブロックに渡すことが出来ます。

in_batchesメソッドを使用した場合-->
1
2
3
User.in_batches do |users|
  p users #1000人のユーザー情報がActiveRecord::Relationのオブジェクトとして渡される
end

上記のコードを実行すると、以下の動画のように1000件ごとにActiveRecord::Relationtとしてブロックに渡されていることが分かりますね。

in_batchesメソッドの挙動

ポイント
  1. find_eachメソッドは、モデルのインスタンスを個別にブロックに渡すことができる
  2. find_in_batchesメソッドは、モデルのインスタンスを配列としてブロックに渡すことができる
  3. in_batchesメソッドは、ActiveRecord::Relationとしてブロックに渡すことができる

この記事のまとめ

  • find_eachメソッドは、レコードをデフォルトで1000件ごとに取得してループ処理できる
  • 大量のレコードに対してメモリ不足を防ぐ用途で使われる
  • レコードが1000件以下で単なるループ処理を行う場合は、eachメソッドなどを使おう