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を接続文字列に含めてください。