はじめに
テーブルの全レコードに一括で処理を行うバッチを作成する事ってありますよね。
そういう時に出来るだけメモリ節約しながらの実装が出来る
chunk()
メソッドと
cursor()
メソッドの紹介です
テスト用のデータ準備
ローカル環境でテスト用にSeederとFactoryを利用して、100,000件のレコードを作成します。
Memberモデルとリレーション用にCommentモデルのレコードも同数作成します。
Seederファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class MemberTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { Member::factory(100000)->create()->each(function ($member) { Comment::factory(1)->create(['member' => $member->id]); }); } } |
Factoryファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class MemberFactory extends Factory { /** * The name of the factory's corresponding model. * * @var string */ protected $model = Member::class; /** * Define the model's default state. * * @return array */ public function definition() { return [ 'name' => $this->faker->name(), 'age' => $this->faker->randomNumber(2), 'hobby' => $this->faker->word(), 'created_at' => now(), 'updated_at' => now(), ]; } } |
get()
試しに、普通に
get()
メソッドで取得してみます。
1 2 3 4 | // Memberモデルの取得。getメソッド。 $members = MemberModel::with('comments')->get(); dump(memory_get_usage() / (1024 * 1024)); // 316MB dump(memory_get_peak_usage() / (1024 * 1024)); // 332MB |
chunk()
それでは、
chunk()
メソッドで取得してみます。
chunkメソッドを利用すると、クエリを複数回発行しながら、データを小分けにして取得可能です。
第一引数に取得するレコード数、第二引数に取得毎に行う処理をクロージャで指定出来ます。
1 2 3 4 5 6 7 8 | MemberModel::with('comments')->chunk(1000, function ($members) { // DBからデータ取得後の処理 foreach ($members as $member) { // 処理 } }); dump(memory_get_usage() / (1024 * 1024)); // 6MB dump(memory_get_peak_usage() / (1024 * 1024)); // 9MB |
メモリ使用量が大分減りましたね。
内部的には以下のように、第一引数で指定した数分offsetを指定していて、文字通り小分けして取得しています。
1 2 3 4 | "select * from `t_member` order by `t_member`.`id` asc limit 1000 offset 0" "select * from `t_comment` where `t_comment`.`member` in (1, 2, 3・・・)" "select * from `t_member` order by `t_member`.`id` asc limit 1000 offset 1000" "select * from `t_comment` where `t_comment`.`member` in (1001, 1002, 1003・・・)" |
chunkById()
chunkした結果を使って、DB更新する場合は
chunkById()
メソッドを使用します。
1 2 3 4 5 6 7 | MemberModel::with('comments')->where('hobby', '!=', 'サッカー')->chunkById(1000, function ($members) { // DBからデータ取得後の処理の中でDB更新する場合 foreach ($members as $member) { $member->hobby = 'サッカー'; $member->save(); } }); |
chunk()との違いは、selectの際に
where id > ?
が付与されていて、プライマリキーを基準にoffset的にデータ取得するのでselectの度に検索結果が変わっても抜け漏れがなくなります
1 2 3 4 5 | "select * from `t_member` where `hobby` != ? order by `id` asc limit 2" "select * from `t_comment` where `t_comment`.`member` in (99997, 99998)" "update `t_member` set `hobby` = ?, `t_member`.`upd_time` = ? where `id` = ?" "update `t_member` set `hobby` = ?, `t_member`.`upd_time` = ? where `id` = ?" "select * from `t_member` where `hobby` != ? and `id` > ? order by `id` asc limit 2" |
cursor()
cursor()
メソッドでも取得してみます。
これは、内部でLazyCollection(Generatorのラッパーのようなもの?)を返し、取得したデータを1行ずつ取得出来るようです。
1 2 3 4 5 | foreach (MemberModel::with('comments')->cursor() as $member) { // 処理 } dump(memory_get_usage() / (1024 * 1024)); // 3MB dump(memory_get_peak_usage() / (1024 * 1024)); // 36MB |
メモリの最大使用量はchunk()よりも大きい結果となりました。
クエリを見ると、シンプルに全件取得する1つだけが発行されています。
また、
with()
メソッドでリレーション指定していますが、取得されていないようです。
1 | "select * from `t_member`" |
さいごに
今回の結果ではchunk()の方がメモリ節約は出来るようでしたが、実装の際は、実際の処理で一度試してから使いたい所です。