SqlBulkCopy を用いた大規模データ挿入のパフォーマンス評価

SQL Server 向けの大規模データ挿入処理において、SqlBulkCopy クラスは従来の INSERT 文やストアドプロシージャに比べて顕著な高速化を実現します。本稿では、30,000件規模のサンプルデータを対象に、SqlBulkCopy の実行効率を定量的に検証し、標準的な逐次挿入との比較を行います。

前提条件と制約

  • SqlBulkCopy は Microsoft SQL Server 専用であり、ローカルまたはリモートの任意の SQL Server インスタンスへ書き込み可能です。
  • データソースは DataTable、IDataReader、または IAsyncEnumerable<object[]>(.NET 6+)など多様な形式に対応します。
  • ターゲットテーブルには既存の IDENTITY 列がある場合、SqlBulkCopyOptions.KeepIdentity を明示的に指定する必要があります。

テスト用データモデルと準備

まず、対応するエンティティクラスとスキーマを定義します。

public record PersonRecord(
    string Name,
    string CredentialHash,
    string Location,
    string ContactNumber
);

SQL Server 側のテーブル定義(例):

CREATE TABLE dbo.Persons (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    Name NVARCHAR(50),
    CredentialHash NVARCHAR(64),
    Location NVARCHAR(100),
    ContactNumber VARCHAR(15)
);

DataTable 生成ロジック(30,000件)

以下のメソッドは、静的値ではなく、ランダム性を持たせたテストデータを生成します(重複回避と現実性向上のため)。

private static DataTable BuildPersonTable(int rowCount)
{
    var table = new DataTable();
    table.Columns.Add("Name", typeof(string));
    table.Columns.Add("CredentialHash", typeof(string));
    table.Columns.Add("Location", typeof(string));
    table.Columns.Add("ContactNumber", typeof(string));

    var random = new Random();
    var names = new[] { "Tanaka", "Sato", "Yamamoto", "Watanabe", "Ito" };
    var cities = new[] { "Tokyo", "Osaka", "Nagoya", "Sapporo", "Fukuoka" };

    for (int i = 0; i < rowCount; i++)
    {
        var row = table.NewRow();
        row["Name"] = $"{names[random.Next(names.Length)]}_{i:D5}";
        row["CredentialHash"] = $"sha256_{Guid.NewGuid():N}";
        row["Location"] = $"{cities[random.Next(cities.Length)]} Central District";
        row["ContactNumber"] = $"090-{random.Next(1000, 9999)}-{random.Next(1000, 9999)}";
        table.Rows.Add(row);
    }

    return table;
}

SqlBulkCopy 実行コード(トランザクション制御付き)

以下は、接続文字列を外部から注入し、バッチサイズとタイムアウトを調整可能な実装です。

public static async Task<long> BulkInsertAsync(string connectionString, int recordCount)
{
    var dataTable = BuildPersonTable(recordCount);

    using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync();

    using var bulk = new SqlBulkCopy(connection)
    {
        DestinationTableName = "dbo.Persons",
        BatchSize = 10000,
        BulkCopyTimeout = 300,
        EnableStreaming = true,
        NotifyAfter = 5000
    };

    bulk.ColumnMappings.Add("Name", "Name");
    bulk.ColumnMappings.Add("CredentialHash", "CredentialHash");
    bulk.ColumnMappings.Add("Location", "Location");
    bulk.ColumnMappings.Add("ContactNumber", "ContactNumber");

    var sw = Stopwatch.StartNew();
    await bulk.WriteToServerAsync(dataTable);
    sw.Stop();

    return sw.ElapsedMilliseconds;
}

対照実験:T-SQL バッチ INSERT の実装

同条件で比較するため、動的に生成した INSERT ステートメントを含むストアドプロシージャを作成します(ただし、実際の運用ではこの手法は非推奨です)。

CREATE OR ALTER PROCEDURE dbo.InsertPersonsBatch
    @Count INT = 30000
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @i INT = 1;
    BEGIN TRANSACTION;
    
    WHILE @i <= @Count
    BEGIN
        INSERT INTO dbo.Persons (Name, CredentialHash, Location, ContactNumber)
        VALUES (
            CONCAT('User_', RIGHT('00000' + CAST(@i AS VARCHAR), 5)),
            CONCAT('hash_', NEWID()),
            'Tokyo Central District',
            CONCAT('090-', RIGHT('0000' + CAST(ABS(CHECKSUM(NEWID())) % 10000 AS VARCHAR), 4), '-', RIGHT('0000' + CAST(ABS(CHECKSUM(NEWID())) % 10000 AS VARCHAR), 4))
        );
        SET @i += 1;
    END
    
    COMMIT TRANSACTION;
END

ベンチマーク測定手順

信頼性のある比較を行うため、各試行前にキャッシュとバッファをクリアします:

DBCC DROPCLEANBUFFERS;
DBCC FREEPROCCACHE;
DELETE FROM dbo.Persons;

さらに、SET STATISTICS TIME ON および SQL Server Profiler(TSQL_SqlBatchCompleted イベント)を併用して、CPU 時間・経過時間・I/O を計測します。

実測結果の傾向(30,000件)

手法 平均経過時間(ms) 平均ログ書き込み量(KB) CPU 使用時間(ms)
SqlBulkCopy(BatchSize=10000)~820~12,400~610
T-SQL WHILE ループ~17,300~48,900~16,200

※ 測定環境:SQL Server 2022 on Azure VM (B4ms), .NET 8, SSD ストレージ

補足:最適化ヒント

  • SqlBulkCopyOptions.TableLock を追加すると、大量挿入時にテーブルロックが取得され、パフォーマンスがさらに向上します(排他性を許容できる場合)。
  • 列マッピングを省略可能ですが、明示的に指定することでカラム順序依存性を排除し、保守性を高めます。
  • 暗号化された接続が必要な場合は、Encrypt=true を接続文字列に含めてください。

タグ: SqlBulkCopy SQLServer .NET performance-benchmark bulk-insert

6月29日 20:01 投稿