第28回: バッチ処理の分割と再開戦略 — 「途中で止まったバッチ」をゼロから再実行していませんか?

章: 第8章: パフォーマンスと本番運用実践

数時間かけたバッチが途中で失敗したら、どうしますか?

100万件のデータを処理するバッチが、90万件まで進んだところでメモリエラーで止まった——残り10万件のためにゼロから再実行するのは、時間もコストもかかります。

バッチを小さく分割し、途中から再開できる設計にしておけば、失敗時のダメージを最小限に抑えられます。Laravelの chunkById と Queue を組み合わせた設計が、この問題の現実的な解決策です。

問題の本質と解決策

問題: 一括処理は途中失敗時の再実行コストが高く、メモリ消費も大きくなります。また、処理が長時間にわたるとタイムアウトやロック競合のリスクも増えます。

解決策: chunkById() でデータを小分けにし、各チャンクをジョブとしてQueueに積みます。ジョブ単位で成否が管理されるため、失敗したチャンクだけを再試行でき、処理の再開が容易になります。

一括処理 vs 分割処理 比較

観点 一括処理 chunkById + Queue分割
失敗時の影響 全件やり直し 失敗チャンクだけ再試行
メモリ消費 全件分確保が必要 チャンクサイズ分のみ
タイムアウトリスク 高い 低い(小単位で完結)
進捗の可視化 難しい ジョブ完了数で把握可能

チェックポイント: chunk() ではなく chunkById() を使っていますか?chunk() はオフセットで取得するため、処理中にレコードが削除されると取りこぼしが発生します。chunkById() は主キーで範囲を絞るため安全です。

実装サンプル


<?php
User::query()->where('active', 1)->chunkById(500, function ($users) {
    foreach ($users as $user) {
        dispatch(new SyncUserReportJob($user->id));
    }
});

まとめ & 次のステップ

  • 大きなバッチは小さく分割し、各単位をQueueジョブとして扱うことで安全に処理できます
  • chunkById() は主キーで絞り込むため、処理中のレコード変動に強いです
  • ジョブ単位で失敗管理ができるため、途中再開のコストが大幅に下がります
  • チャンクサイズは500〜1000件を目安にメモリとスループットのバランスを調整します
  • まずは最も件数が多いバッチ処理1つを chunkById に切り替えるところから始めましょう

次回は S3ストレージ設計の基本 を学びます。ファイルの公開範囲と署名付きURLの使い方を解説します。

Related Articles