前回は、チャンキングでプログラムが使用するメモリの量を抑えることを説明しましたが、実行速度はどうなのでしょう?

私の仮想マシンで測定してみました。50万のレコードがあります。

1000レコードごとのチャンキングは、1分32秒

チャンキングなしで、メモリ制限を1ギガバイトとして、46秒

チャンキングの方が2倍以上時間かかっています。

チャンキングがどのようなSQLのクエリを実行するか、tinkerで見てみます。数が多いので、ここでは10,000レコードごとのチャンキングにしています。

>>> DB::enableQueryLog();
=> null
>>> DB::table('users')->count();
=> 500000
>>> App\User::chunk(10000, function($rows) { });
=> true
>>> DB::getQueryLog();
=> [
     [
       "query" => "select count(*) as aggregate from `users`",
       "bindings" => [],
       "time" => 152.78,
     ],
     [
       "query" => "select * from `users` order by `users`.`id` asc limit 10000 offset 0",
       "bindings" => [],
       "time" => 76.14,
     ],
     [
       "query" => "select * from `users` order by `users`.`id` asc limit 10000 offset 10000",
       "bindings" => [],
       "time" => 59.68,
     ],
... 途中はスキップ
     [
       "query" => "select * from `users` order by `users`.`id` asc limit 10000 offset 480000",
       "bindings" => [],
       "time" => 246.93,
     ],
     [
       "query" => "select * from `users` order by `users`.`id` asc limit 10000 offset 490000",
       "bindings" => [],
       "time" => 227.26,
     ],
     [
       "query" => "select * from `users` order by `users`.`id` asc limit 10000 offset 500000",
       "bindings" => [],
       "time" => 191.65,
     ],
   ]

チャンキングでは、limit 10000 offset 0のように、開始するレコードの場所と取得する数をクエリで指定しています。開始するレコードの場所がここでは10,000ごと増えていきます。そして、そのためにクエリの実行がだんだん遅くなっていきます。遅さは加速されているようで、最後の方では最初の5倍くらい時間がかかっています。

レコード数が5百万あるテーブルの実験では、1000のチャンキングでは百万程度から非常に遅くなっていき、何時間たっても終わらず、いつまでも終わらないのではないか、くらいのスピードになりました。

メモリの使用は控えてくれるけれど、レコード数が多い(とても多い)ときの処理時間が途方もなく長くなります。

どう解決したらよいかと考えて、思いついたのが以下の方法です。まずコードを見てください。

...
        $max = User::max('id');
        $unit = 1000;

        for ($i = 0; $i < intval(ceil($max/$unit)); $i++) {
            $from = $i * $unit + 1;
            $to = ($i + 1) * $unit;

            $rows = User::where('id', '>=', $from)
                ->where('id', '<=', $to)
                ->get();

            foreach ($rows as $row) {
                $values = [
                    $row->id,
                    $row->created_at,
                    $row->name,
                    $row->email
                ];
 
                fputcsv($fh, $values);
           }
       }
...

チャンキングと似ていますが、limit, offsetを使用するのではなく、idの値の1000ごとの範囲を指定してループします。idはDBテーブルのプライマリーキーゆえにスピード向上される、という仮定です。

測定してみました。処理時間は予想通り縮まり45秒となりました。チャンキングなしとほぼ同じです。しかも使用メモリは抑えられて。

レコード数だけでなく対象のDBテーブルのサイズなどとも関係があると思いますが、同じような状況に至ったときには試してみてください。

By khino