更新日:
【Rails】 find_eachメソッドでメモリを節約して大量データを扱う方法
find_eachメソッドは、大量のレコードをループ処理する際にメモリの消費量を抑えることができるメソッドです。デフォルトでは、レコードを1000件ごとに取得して処理を実行します。
例えば、以下の動画のようにusersテーブルには1万件のレコードがあり、この大量のレコードを取得してループ処理を実行したいとします。
しかし、1万件のレコードを一度に取得してループ処理を実行すると、メモリが圧迫されてアプリケーションの動作が遅くなったり、フリーズしてしまう恐れがあります。
そこで、find_eachメソッドを使用します。以下のコードのようにレコードを一度に取得するのではなく、1000件ごとに取得してループ処理をするのでメモリの消費量を抑える事が出来ます。
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万件まで繰り返し実行されていますね。
find_eachメソッドは、大量のレコードに対してループ処理を実行したい場合にメモリ不足を防ぐことが出来るメソッドなので、是非使い方を理解していきましょう!
find_eachメソッドの使い方
find_eachメソッドの使い方について、基本構文から発行されるSQL、必要性、似ているメソッドとの違いまで幅広く解説します。この記事を読めば、あらゆる視点からfind_eachメソッドを学ぶことが出来ます。
基本構文
find_eachメソッドは、モデルのクラスに対して呼び出すことが出来るメソッドです。
1
2
3
モデル名.find_each do |変数|
# 処理
end
モデルに対応するテーブルのレコードをデフォルトで1000件に区切って取得し、ループ処理が実行されます。この処理の流れは、サンプルコードを使って1つ1つみていきましょう。
処理の流れを確認しよう
find_eachメソッドの処理の流れは、以下のサンプルコードを使って確かめていきます。
1
2
3
User.find_each do |user|
p user.id
end
上記のコードが実行されると、以下の画像のような順番で処理が実行されます。
上記の流れをまとめると、以下の通りです。
- usersテーブルから1000件(id: 1~1000)のレコードを取得する
- 取得したレコードをUserモデルのインスタンスとして1件ずつブロックに渡して、ユーザーidを出力させる
- 最後のレコード(id: 1000)の処理が終了したら、次の1000件(id: 1001~2000)のレコードを取得して処理を実行する、これを1万件まで繰り返す
これにより、find_eachメソッドを使うとデフォルトで1000件ごとにレコードを取得してループ処理が行われていることが分かりますね。
発行されるSQLクエリとは?
次は、find_eachメソッドによって発行されるSQLクエリをみていきましょう。
1
2
3
User.find_each do |user|
p user.id
end
上記のコードを実行した際に、最初に発行される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件に制限しています。
このようにデフォルトでは、取得するレコードの上限が1000件に制限されて、主キーを対象に昇順でソートされます。
whereメソッドと使う
以下のコードのように、whereメソッドで取得するレコードの条件を指定した後にfind_eachメソッドを使うことが出来ます。
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)
の指定がされていますね。
1
SELECT `users`.* FROM `users` WHERE (age >= 80) ORDER BY `users`.`id` ASC LIMIT 1000
このように大量のレコードを扱う場合は、whereメソッドで条件を指定すれば取得するレコード自体を減らすことが出来るのでメモリ消費量もさらに抑えることが出来ます。
find_eachメソッドの必要性
テーブルからレコードを取得してループ処理するには、以下のコードのようにallメソッドとeachメソッドを使用した方法も考えられるでしょう。しかし、以下のコードは大量のレコードを扱う場合には注意が必要です。
1
2
3
User.all.each do |user|
p user.id
end
上記のコードを実行すると、以下のような挙動になります。
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メソッドを使ってループ処理をします。
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件ごとに取得してループ処理を実行します。
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件ごとに取得してループ処理を実行します。
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件ごとに昇順で取得します。
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
と併用して開始から終了までの主キーの値を指定することも出来ます。
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件ごとに昇順で取得してループ処理を実行することが出来ます。
注意点
find_eachメソッドを使用する際の注意点について解説します。
用途
find_eachメソッドは、テーブルのレコードを取得してループ処理をすることが出来ますが、メモリが圧迫してしまう大量のレコードに対して、レコードを分割して処理することでメモリ不足を防ぐためのものです。
そのため、1000件以下のレコードに対して単にループ処理を行う場合は、eachメソッドなど他のメソッドを使用するようにしましょう。
1
2
3
User.find_each do |user|
p user.id
end
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件ごとにレコードを取得して、処理を実行することが出来ます。
1
2
3
モデル名.find_in_batches do |変数|
# 処理
end
しかし、find_eachメソッドが取得した1000件のレコードをモデルのインスタンスとして個別に1件ずつブロックへ渡すのに対して、find_in_batchesメソッドは取得した1000件のレコードをモデルの配列としてブロックに渡します。
1
2
3
User.find_each do |user|
p user.id #1人のユーザー情報が渡される
end
1
2
3
User.find_in_batches do |users|
p users #1000人のユーザー情報が配列として渡される
end
上記のfind_in_batchesメソッドのコードを実行すると、以下の動画のように1000件ごとに取得したレコードをモデルの配列として渡されているのが分かりますね。
in_batchesメソッドとの違い
in_batchesメソッドは、Rails5以降から使用することが出来るメソッドです。
1
2
3
モデル名.in_batches do |変数|
# 処理
end
先ほどのfind_in_batchesメソッドは、取得した1000件のレコードをモデルの配列としてブロックに渡していましたが、in_batchesメソッドはActiveRecord::Relation
をブロックに渡すことが出来ます。
1
2
3
User.in_batches do |users|
p users #1000人のユーザー情報がActiveRecord::Relationのオブジェクトとして渡される
end
上記のコードを実行すると、以下の動画のように1000件ごとにActiveRecord::Relationtとしてブロックに渡されていることが分かりますね。
この記事のまとめ
- find_eachメソッドは、レコードをデフォルトで1000件ごとに取得してループ処理できる
- 大量のレコードに対してメモリ不足を防ぐ用途で使われる
- レコードが1000件以下で単なるループ処理を行う場合は、eachメソッドなどを使おう