【Laravel】chunk と chunkById を比較してわかったこと

エンジニアの小林です。

クラフではLaravelを使ったWEBサービスの開発も行っています。

今回はLaravelのchunkメソッドとchunkByIdメソッドの違いについて、解説してみたいと思います。

chunkとは

chunkとは「小さな塊(チャンク)」という意味で、Laravelではクエリビルダに用意されているchunkメソッドで、DBからデータを小さな「塊(チャンク)」ごとに分割して結果を取得し、処理を行うことができます。大量データを扱う場合はメモリ不足エラーを起こさず安全に処理することができます。

Laravelには似たような名前でchunkByIDというメソッドも用意されていますが、どちらを使えばいいのでしょうか。

結論:chunkByIDが使えるときはchunkByIDを使う

最初に結論を書いてしまうと、私はchunkByIDが使える局面ではchunkByIDを使うことをおすすめします。

なぜなら、chunkは大量データを処理する場合に処理速度の問題があるからです。

公式リファレンスには、「結果をチャンクしつつデータベースレコードを更新すると、チャンク結果が意図しない変化を起こす可能性があります。そのため、チャンク結果を更新する場合は、常に代わりのchunkByIdメソッドを使用するのが最善です。」と記載されています。たしかにそういう側面もあると思いますが、私の場合は、パフォーマンス面のほうが気になりました。

chunkの仕組み

chunkメソッドの仕組みを理解するために、展開されるSQLを確認していきたいと思います。

確認コード1

    Sample::chunk(100, function ($sample) {
        // SQL出力
    });

展開されるSQL1

select * from “samples” order by “samples”.”id” asc limit 100 offset 0;
select * from “samples” order by “samples”.”id” asc limit 100 offset 100;
select * from “samples” order by “samples”.”id” asc limit 100 offset 200;

一回呼び出されるごとに、offsetが100づつ増えているのがわかります。

chunkは、limit offset で分割処理を実現しているようです。

また、order by id が自動で付加されています。

次に、queryにorderBy句を設定してみます。

確認コード2

    Sample::orderBy(‘updated_at’)->chunk(100, function ($sample) {

        // SQL出力
    });

展開されるSQL2

select * from “samples” order by “updated_at” asc limit 100 offset 0;
select * from “samples” order by “updated_at” asc limit 100 offset 100;
select * from “samples” order by “updated_at” asc limit 100 offset 200;

orderBy句を設定すると、期待通り orderBy updated_at に変わりました。

chunkByIDの仕組み

一方のchunkByIdメソッドで展開されるSQLをみていきたいと思います。

確認コード3

    Sample::chunkById(100, function ($sample) { 
       // SQL出力
    });

展開されるSQL3

select * from “samples” order by “id” asc limit 100;
select * from “samples” where “id” > ? order by “id” asc limit 100;    [bindings => [100]]
select * from “samples” where “id” > ? order by “id” asc limit 100;    [bindings => [200]]

注目するのは、where “id” > ? の部分です。100, 200 と順に値が渡されることで、offset と同じ動作を実現しているようです。また、サンプルでは連番になっていますが、自動で1つ前のSQLで取得したIDの最大値をバインドしてくれるため、IDが連番になっていなくても大丈夫です。

また、order by id が自動で付加されています。

次に、queryにorderBy句を指定してみます。

確認コード4

    Sample::orderBy(‘updated_at’)->chunkById(100, function ($sample) { 
       // SQL出力
    });

展開されるSQL4

select * from “samples” order by “id” asc limit 100;
select * from “samples” where “id” > ? order by””updated_at” asc, “id” asc limit 100;
    [bindings => [100]]
select * from “samples” where “id” > ? order by””updated_at” asc, “id” asc limit 100;
    [bindings => [200]]

こちらもorderByにupdated_atに変わりました。

しかし、where “id” > ? という検索条件はそのまま設定されているため、chunkメソッドの結果とは異なります。期待する動作にはなりませんので、注意が必要です。

chunkByIDの第3引数を指定する

chunkByIdをID以外でおこないたい場合は、第3引数を設定します。

確認コード5

    Sample::chunkById(100, function ($sample) {
        // SQL出力
    }, ‘updated_at’);

展開されるSQL5

select * from “samples” order by “updated_at” asc limit 100;
select * from “samples” where “updated_at” > ? order by””updated_at” asc limit 100; 
   [bindings => [Illuminate\Support\Carbon @1594977896]]
select * from “samples” where “updated_at” > ? order by””updated_at” asc limit 100;
    [bindings => [Illuminate\Support\Carbon @1594977996]]

where句、orderBy句ともに、updated_atに置き換わり、期待している動作になりました。注意点としては、指定するカラムがユニークでない場合、データの抜け漏れが発生する可能性があります。

chunkのパフォーマンス問題について

chunkにはパフォーマンス問題があると前述しましたが、確認のためchunkByIdと処理速度を比較していきたいと思います。

以下は、70万件のデータを処理したときのスプリットタイムの比較です。

chunkByIdはほぼ一定の速度を保っていますが、chunkのほうは処理を重ねるごとに遅くなっていってます。

chunkが遅くなる原因はSQLにoffsetを使用しているためです。SQLにoffsetが指定された場合、DBエンジンの内部では、検索条件に一致するデータを取得したあと、取得したデータの先頭から順に読み出し、offsetの部分のレコードをあとから取り出すという動作を行っています。つまり、offsetの値が大きくなると比例してDBからのレスポンスが遅くなります。

参考までにoffset値別の1回のSQLの実行時間の比較をしてみます。

DBはPostgreSQL13を使っていますが、Mysqlなどでも同様の傾向になります。

offset値が0の場合は 3msで完了していますが、offset値が50万になると980ms掛かっています。offset値が大きくなるほど実行時間は伸びるということがわかります。

一方の chunkByIdは offsetを使わず、検索条件で対象データを絞り込んだあと、先頭100件を取得していますから常に高速です。

chunkByIDに設定する件数の最適値について

chunkByIdに設定する件数の最適値を探るため、設定する件数を変えて処理速度とメモリの使用量を比較してみました。

以下は、70万件のデータを処理したときの比較です。

処理時間は10件が一番長く、件数を増やすごとに短くなっていきますが、100件を超えるとその比率は緩やかになりました。またメモリの使用量は10,000件で急激に増加しています。

100件〜1000件を設定するとバランスが良さそうです。

ただし実際に設定する件数は、メモリ不足でエラーにならないように100件あたりで収めています。レコードの大きさによっては1000件でもエラーになることがありますし、ギリギリを狙ってメモリ不足でエラーになっては元も子もありませんから。

まとめ

  • chunkByIDが使える場合はchunkByIDを使う。
  • chunkは大量データの処理には不向き。
  • メモリ不足でエラーにならないように注意する。

最後に、大量データが予想されている場合は、早めに大量データの投入テストを行うことをおすすめします。

最後までお読みいただきありがとうございました。

今回はLaravelで大量データを扱う場合に便利な、chnkとchunkByIdの違いについて解説してみました。参考になれば幸いです。