カテゴリー: BackEnd

Laravelのchunkメソッドとcursorメソッドのメモリ使用量

はじめに

テーブルの全レコードに一括で処理を行うバッチを作成する事ってありますよね。
そういう時に出来るだけメモリ節約しながらの実装が出来る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()

試しに、普通に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()メソッドで取得してみます。
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・・・)"

chunkById()

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()

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()の方がメモリ節約は出来るようでしたが、実装の際は、実際の処理で一度試してから使いたい所です。

おすすめ書籍

Yossy

シェア
執筆者:
Yossy
タグ: laravelphp

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

2週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

4週間 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前