テーブルの全レコードに一括で処理を行うバッチを作成する事ってありますよね。
そういう時に出来るだけメモリ節約しながらの実装が出来るchunk()
メソッドとcursor()
メソッドの紹介です
ローカル環境でテスト用にSeederとFactoryを利用して、100,000件のレコードを作成します。
Memberモデルとリレーション用にCommentモデルのレコードも同数作成します。
Seederファイル
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ファイル
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()
メソッドで取得してみます。
// Memberモデルの取得。getメソッド。 $members = MemberModel::with('comments')->get(); dump(memory_get_usage() / (1024 * 1024)); // 316MB dump(memory_get_peak_usage() / (1024 * 1024)); // 332MB
それでは、chunk()
メソッドで取得してみます。
chunkメソッドを利用すると、クエリを複数回発行しながら、データを小分けにして取得可能です。
第一引数に取得するレコード数、第二引数に取得毎に行う処理をクロージャで指定出来ます。
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を指定していて、文字通り小分けして取得しています。
"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・・・)"
chunkした結果を使って、DB更新する場合はchunkById()
メソッドを使用します。
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の度に検索結果が変わっても抜け漏れがなくなります
"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()
メソッドでも取得してみます。
これは、内部でLazyCollection(Generatorのラッパーのようなもの?)を返し、取得したデータを1行ずつ取得出来るようです。
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()
メソッドでリレーション指定していますが、取得されていないようです。
"select * from `t_member`"
今回の結果ではchunk()の方がメモリ節約は出来るようでしたが、実装の際は、実際の処理で一度試してから使いたい所です。