はじめに:「Batch処理」は“まとめてやるけど、やり過ぎない”ための設計
業務で C# を書いていると、こういう悩みがよく出てきます。
一度に 10 万件 INSERT すると重いし、失敗したときの巻き戻しも大変。
外部 API に 1 万件まとめて投げるとタイムアウトする。
でも 1 件ずつ処理すると、今度は遅すぎて現実的じゃない。
この「まとめて処理したいけど、一度にやり過ぎると危ない」というジレンマを解決する考え方が「Batch処理」です。
そして、LINQ の Chunk や自作の Batch 拡張メソッドを使うと、「N件ずつまとめて処理する」コードを、とても素直に書けるようになります。
ここでは、プログラミング初心者向けに
Batch処理の考え方Chunk を使ったシンプルなバッチ処理
自作 Batch 拡張メソッドの書き方
DB・外部API・業務バッチでの具体的な使い方
を、例題付きでかみ砕いて説明していきます。
Batch処理の基本イメージをつかむ
「全部一気」でも「1件ずつ」でもなく、その中間を取る
まずは、極端な 3 パターンを頭に置いてください。
全部一気に処理する
1 件ずつ処理する
適当なサイズに分けて、かたまりごとに処理する(=Batch処理)
例えば 10 万件のレコードを DB に保存するとき、
10 万件を 1 トランザクションで一気に保存 → 失敗したときの影響が大きいし、重い。
1 件ずつ保存 → 安全だけど遅すぎる。
1000 件ずつ保存 → 速度と安全性のバランスが良い。
この「1000 件ずつ保存」のようなやり方が、まさに Batch処理です。
ここでの重要ポイントは、「Batch処理は“速度と安全性のバランスを取るための折衷案”」だと理解することです。
そして、その「1000 件ずつ」のような単位を、LINQ で気持ちよく書くための道具が Chunk や Batch です。
Chunk を使ったシンプルな Batch処理
Enumerable.Chunk をそのままバッチ単位として使う
.NET 6 以降なら、LINQ に標準で Chunk が入っています。
これは「シーケンスを指定サイズごとの配列に分割する」メソッドです。
using System;
using System.Collections.Generic;
using System.Linq;
public class Item
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
IEnumerable<Item> GetManyItems()
{
return Enumerable.Range(1, 10_000)
.Select(i => new Item { Id = i, Name = $"Item {i}" });
}
void SaveBatch(IEnumerable<Item> items)
{
Console.WriteLine($"SaveBatch: {items.Count()} 件");
// 実際はここで DB に一括保存する
}
var allItems = GetManyItems();
foreach (var batch in allItems.Chunk(1000))
{
SaveBatch(batch);
}
C#このコードは、「10,000 件を 1000 件ずつ、合計 10 回に分けて保存する」処理になっています。
ここでの重要ポイントは、「Chunk(1000) という 1 行で、“1000 件単位のバッチ”という業務的な単位を、そのままコードに表現できている」ことです。for でインデックスをいじるより、意図が圧倒的に読みやすくなります。
自作 Batch 拡張メソッドを作ってみる
MoreLINQ の Batch と同じ発想で書く
.NET 6 未満 や「Chunk ではなく IEnumerable<IEnumerable<T>> が欲しい」場合は、自分で Batch 拡張メソッドを書くこともできます。
代表的な実装パターンは、MoreLINQ の Batch とほぼ同じ考え方です。
using System;
using System.Collections.Generic;
public static class BatchExtensions
{
public static IEnumerable<IEnumerable<T>> Batch<T>(
this IEnumerable<T> source,
int size)
{
if (source is null) throw new ArgumentNullException(nameof(source));
if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
T[]? bucket = null;
var count = 0;
foreach (var item in source)
{
if (bucket is null)
{
bucket = new T[size];
}
bucket[count++] = item;
if (count == size)
{
yield return bucket;
bucket = null;
count = 0;
}
}
if (bucket is not null && count > 0)
{
var last = new T[count];
Array.Copy(bucket, last, count);
yield return last;
}
}
}
C#使い方は Chunk とほぼ同じです。
var numbers = Enumerable.Range(1, 10);
foreach (var batch in numbers.Batch(3))
{
Console.WriteLine($"[{string.Join(", ", batch)}]");
}
C#ここでの重要ポイントは、「Batch は“バッファ配列に詰めて、いっぱいになったら yield する”というシンプルな仕組み」であることです。
最後のバッチだけサイズが小さくなるので、そこだけ別処理しているのもポイントです。
実務での具体例 1:DB への一括保存をバッチ化する
例:10 万件を 2000 件ずつトランザクションで保存
DB への INSERT/UPDATE を Batch処理にするときの典型パターンです。
void SaveAll(IEnumerable<Item> items)
{
const int BatchSize = 2000;
foreach (var batch in items.Chunk(BatchSize))
{
SaveBatchWithTransaction(batch);
}
}
void SaveBatchWithTransaction(IEnumerable<Item> batch)
{
using var tran = _db.BeginTransaction();
try
{
foreach (var item in batch)
{
// ここで INSERT / UPDATE
}
tran.Commit();
}
catch
{
tran.Rollback();
throw;
}
}
C#ここでの重要ポイントは、「トランザクションの単位を“バッチサイズ”にしている」ことです。
1 件ずつトランザクションを張るより速く、10 万件を 1 回でやるより安全、というバランスを取れます。
実務での具体例 2:外部 API の「最大件数制限」に合わせた Batch処理
例:ID を 100 件ずつまとめて API に送る
外部 API に「1 回のリクエストで送れる ID は最大 100 件まで」という制限があるケースです。
IEnumerable<string> GetTargetIds()
{
return Enumerable.Range(1, 250)
.Select(i => $"ID{i:D3}");
}
void CallExternalApi(IEnumerable<string> ids)
{
var idArray = ids.ToArray();
Console.WriteLine($"API 呼び出し: {idArray.Length} 件");
// 実際はここで HTTP リクエストを送る
}
var ids = GetTargetIds();
foreach (var idBatch in ids.Chunk(100))
{
CallExternalApi(idBatch);
}
C#このコードは、「250 件の ID を 100 件・100 件・50 件の 3 回に分けて API に送る」処理になります。
ここでの重要ポイントは、「Batch処理は“外部システムの制限に合わせる”ための道具としても使える」ということです。
API 側の「最大件数」を、そのままバッチサイズとしてコードに刻み込めます。
実務での具体例 3:重い処理を Batch単位で並列化する
チャンク × Parallel.ForEach という組み合わせ
CPU を使う重い処理をするとき、「1 件ずつ Parallel.ForEach する」のではなく、「バッチ単位で並列化する」ほうが効率的なことがあります。
using System.Threading.Tasks;
void ProcessAll(IEnumerable<Item> items)
{
const int BatchSize = 500;
var batches = items.Chunk(BatchSize).ToList();
Parallel.ForEach(batches, batch =>
{
foreach (var item in batch)
{
ProcessItem(item);
}
});
}
void ProcessItem(Item item)
{
// CPU を使う重い処理
}
C#ここでの重要ポイントは、「並列化の単位を“バッチ”にすることで、スレッドのオーバーヘッドを抑えつつ、全体を速くできる」ことです。
1 件ずつ並列にするより、まとまりで動かしたほうが現実的なことが多いです。
Batch処理を設計するときに必ず考えるべきこと
バッチサイズをどう決めるか
バッチサイズは「大きければ速い、小さければ安全」というトレードオフになります。
大きすぎると
メモリ使用量が増える
1 バッチ失敗時の影響範囲が大きい
タイムアウトしやすい
小さすぎると
トランザクションや接続のオーバーヘッドが増える
API 呼び出し回数が増えて遅くなる
なので、実務では
まずは仮の値(例:100、500、1000)で動かしてみる
ログやメトリクスを見ながら、徐々に調整する
というスタイルが現実的です。
ここでの重要ポイントは、「バッチサイズは“チューニングパラメータ”であり、コードにベタ書きするなら定数名で意味を持たせる」ことです。
const int ExternalApiBatchSize = 100;
const int DbSaveBatchSize = 2000;
C#こうしておくと、「なぜ 100 なのか」「なぜ 2000 なのか」が読み手に伝わりやすくなります。
失敗時にどうリトライするか
Batch処理は、「1 バッチ単位で失敗する」ことを前提に設計するのが基本です。
1 バッチ失敗時に
そのバッチだけリトライするのか
ログだけ出してスキップするのか
全体を止めるのか
などを、業務要件に合わせて決めておきます。
例えば、「API が一時的に落ちているだけならリトライしたい」なら、バッチ単位でリトライロジックを書くことになります。
foreach (var batch in items.Chunk(100))
{
RetryPolicy.Execute(() => CallExternalApi(batch));
}
C#ここでの重要ポイントは、「Batch処理は“失敗の単位”も決めてしまう」ということです。
だからこそ、バッチサイズと同じくらい、「失敗時の扱い」を最初に決めておく必要があります。
まとめ:「Batch処理ユーティリティ」は“現実的な単位で世界を刻む道具”
Batch処理の本質は、
「全部一気にやるのは危ないし、1 件ずつは遅すぎるから、
現実的なサイズの“かたまり”に分けて処理する」
ことです。
そのための実装として、
.NET 6 以降なら Chunk をそのまま使う
古い環境や好みに応じて、自作 Batch 拡張メソッドを用意する
DB 保存、外部 API 呼び出し、重い処理の並列化などに組み込む
というパターンを押さえておけば、業務コードの「重さ」と「安全性」をかなりコントロールできるようになります。
最後にもう一度、頭に置いておいてほしいポイントは、
Batch処理は「速度」と「安全性」のバランスを取るための設計
バッチサイズはチューニングパラメータChunk / Batch を使うと、“N件ずつ処理している”ことがコードから一目で分かる
ここまで腹落ちしていれば、
「なんとなく全部 ToList して一気に処理する」段階から卒業して、
“現実的なサイズで世界を刻む Batch処理ユーティリティ”を、自分の手で設計できるようになります。

