sqlbulkcopy是c#向sql server批量插入的最快方式,但需严格匹配数据类型、禁用计算列写入、避免复用事务连接,并注意datetime/nullable字段对齐及columnmappings显式绑定。

SqlBulkCopy 是 C# 向 SQL Server 批量插入的最快路径,但直接用它出错率极高——核心问题不在语法,而在数据准备、类型对齐和连接状态控制。
SqlBulkCopy 为什么比循环 Insert 快几十倍
它绕过 SQL Server 的查询解析、计划生成、日志逐条写入等开销,直接将内存中的数据流以专有协议推送到数据页。但这也意味着:它不走 T-SQL 引擎,DEFAULT 约束、IDENTITY 插入、触发器、外键检查(默认关闭)全都不生效——不是 bug,是设计使然。
- 必须确保目标表结构与源数据完全兼容,尤其是
DateTime和nullable字段(比如 C# 的DateTime?对应 SQL 的DATETIME2而非DATETIME) - 连接不能复用已开启事务的
SqlConnection(除非显式传入SqlTransaction) -
SqlBulkCopy.WriteToServer()是原子操作,失败即全滚,无法部分提交
DataTable 作为源数据时的三个致命坑
很多人用 DataTable 最省事,但它最容易踩类型陷阱:
- 列名必须与目标表**完全一致(大小写敏感)**,且不能有空格或特殊字符;否则要用
ColumnMappings显式绑定 -
DataTable中某列为null,但数据库对应列为NOT NULL→ 报错"The given value of type String from the data source cannot be converted to type nvarchar of the specified target column." - 时间字段含毫秒但目标列为
DATETIME(精度只到 3.33ms)→ 建议统一用DATETIME2(3),并在 C# 中用DateTime.SpecifyKind(dt, DateTimeKind.Utc)避免时区歧义
示例映射写法:
var bulk = new SqlBulkCopy(connection);
bulk.DestinationTableName = "Orders";
bulk.ColumnMappings.Add("OrderId", "OrderId");
bulk.ColumnMappings.Add("CreatedTime", "CreatedTime"); // 显式指定,避免列序错位
用 IDataReader 替代 DataTable 节省内存的关键时机
当数据量超 10 万行或单行超 1KB,DataTable 会吃光内存。此时应实现轻量 IDataReader,按需提供行数据:
- 继承
DbDataReader太重,推荐用ObjectReader.Create(IEnumerable<t>)</t>(来自FastMemberNuGet 包) - 确保
GetValue(int i)返回值类型与目标列严格匹配:SQL 的INT→ C#int或int?,不是long -
SqlBulkCopy.BatchSize设为 10000 左右较稳;设太大可能触发OutOfMemoryException,太小则网络往返增多
简例:
var reader = ObjectReader.Create(orderList); // orderList 是 List<order> bulk.WriteToServer(reader);</order>
常见报错及速查应对
遇到错误别急着改代码,先看是不是以下典型场景:
-
"Invalid object name 'xxx'"→ 检查DestinationTableName是否带 schema,如"dbo.Orders",而非仅"Orders" -
"The column X cannot be modified because it is a computed column."→ 目标列是计算列,SqlBulkCopy默认不允许写入,需设bulk.ColumnMappings并跳过该列,或建临时表中转 -
"Timeout expired"→ 不是网络问题,大概率是锁表或日志满;加bulk.BulkCopyTimeout = 600(单位秒),并确认目标表没被长事务阻塞 - 插入后查不到数据 → 检查是否忘了
connection.Open(),或用了using提前释放了连接
最易被忽略的是:SqlBulkCopy 不支持 OUTPUT 子句,也无法返回自增 ID。如果业务强依赖插入后的 ID,得换回 INSERT ... SELECT + 表值参数,或分批后用 SCOPE_IDENTITY() 补查。











