C# Tips | コレクション・LINQ:Batch処理

C# C#
スポンサーリンク

はじめに:「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 で気持ちよく書くための道具が ChunkBatch です。


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処理ユーティリティ”を、自分の手で設計できるようになります。

タイトルとURLをコピーしました