SqlBulkCopy
Inserting 100,000 rows one by one takes minutes. SqlBulkCopy does it in seconds using SQL Server’s bulk load protocol.
On this page · 5 min · Advanced
On this page
- In simple words
- Learning objectives
- Explanation
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 5 min
- Difficulty
- Advanced
- Updated
- Oct 29, 2026
Related tutorials
In simple words#
Instead of carrying 10,000 bricks one by one, you bring a truck. SqlBulkCopy loads thousands of rows in one fast operation.
- SqlBulkCopy
- Fast bulk insert.
- ColumnMappings
- Match source and destination columns by name.
- BatchSize
- Rows sent per batch.
Learning objectives#
- Bulk insert from a DataTable
- Map columns explicitly
- Tune BatchSize and options
- Use a staging table for merges
Explanation#
SqlBulkCopy streams rows from a DataTable, DataRow[] or any IDataReader into a table.
Always add ColumnMappings: without them, columns are matched by position, which breaks when the table has an identity column or a different order.
BatchSize controls how many rows go per batch; SqlBulkCopyOptions.TableLock speeds up large loads; CheckConstraints and FireTriggers are off by default.
For “insert or update”, bulk load into a staging table, then run one MERGE or UPDATE/INSERT statement.
Code example: Import a CSV price list#
var table = new DataTable();
table.Columns.Add("Name", typeof(string));
table.Columns.Add("Price", typeof(decimal));
table.Columns.Add("Stock", typeof(int));
foreach (var line in File.ReadLines("products.csv").Skip(1)) // Name,Price,Stock
{
var parts = line.Split(',');
table.Rows.Add(parts[0], decimal.Parse(parts[1], CultureInfo.InvariantCulture), int.Parse(parts[2]));
}
await using var conn = new SqlConnection(cs);
await conn.OpenAsync();
using var bulk = new SqlBulkCopy(conn, SqlBulkCopyOptions.TableLock | SqlBulkCopyOptions.CheckConstraints, null)
{
DestinationTableName = "dbo.Products",
BatchSize = 5000,
NotifyAfter = 20000,
};
bulk.ColumnMappings.Add("Name", "Name");
bulk.ColumnMappings.Add("Price", "Price");
bulk.ColumnMappings.Add("Stock", "Stock");
bulk.SqlRowsCopied += (_, e) => Console.WriteLine(quot;{e.RowsCopied:N0} rows…");
var sw = Stopwatch.StartNew();
await bulk.WriteToServerAsync(table);
Console.WriteLine(quot;Imported {table.Rows.Count:N0} rows in {sw.Elapsed.TotalSeconds:F1}s");
20,000 rows… 40,000 rows… ... Imported 100,000 rows in 1.8s (timings vary)
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- The CSV is parsed into a DataTable with the right types.
- TableLock and CheckConstraints are enabled.
- Explicit mappings skip the identity Id column.
- SqlRowsCopied reports progress every 20,000 rows.
Real-world use case#
A nightly job imports a supplier’s 200,000-row price file into a staging table, then MERGEs it into Products in one statement.
Common mistakes#
Position-based mapping silently puts values in the wrong columns or fails with type errors.
They are skipped by default. Enable CheckConstraints/FireTriggers if you rely on them.
Best practices#
- Always map columns
- Load into a staging table for upserts
- Use an IDataReader source for huge files to avoid memory spikes
- Wrap in a transaction if the load must be all-or-nothing
Remember this#
Many rows → SqlBulkCopy, not a loop of INSERTs.
Map columns explicitly.
Upsert = bulk to staging + MERGE.
Practice exercise#
Your task: Bulk copy into dbo.ProductsStaging instead and then insert only new names into Products.
Show solution
INSERT INTO dbo.Products (Name, Price, Stock)
SELECT s.Name, s.Price, s.Stock
FROM dbo.ProductsStaging s
WHERE NOT EXISTS (SELECT 1 FROM dbo.Products p WHERE p.Name = s.Name);
TRUNCATE TABLE dbo.ProductsStaging;
Mini quiz#
Why add ColumnMappings?
Want more? Practise all ADO.NET MCQs · ADO.NET interview questions
My notes#
Saved in this browser.