SqlBulkCopy比循环Insert快十倍以上,因其绕过SQL解析与单行事务,直接通过TDS协议批量写入数据页;需设BatchSize、DestinationTableName、EnableStreaming三参数,并注意列映射、IDENTITY及NULL处理。

SqlBulkCopy 为什么比循环 Insert 快十倍以上
因为 SqlBulkCopy 绕过了 SQL Server 的查询解析、计划编译、单行事务开销,直接把内存数据流按协议写入数据页。它本质是 TDS 协议层的“裸数据灌入”,不是“执行 SQL”。你用 INSERT INTO ... VALUES (...) 插千万行,实际是发千万次网络包、建千万次语句上下文;而 SqlBulkCopy 默认一次连接、一次会话、一个批处理上下文——这才是吞吐量差异的根源。
但这也意味着:它不走触发器、不校验默认约束(除非显式开启)、不触发 IDENTITY_INSERT 自动切换。如果你依赖这些机制,得提前补逻辑。
必须设置的三个关键参数
SqlBulkCopy 不设参数也能跑,但默认行为在千万级场景下大概率失败或极慢。以下三项必须显式配置:
-
BatchSize = 10000:每批提交行数。太小(如 100)导致网络往返过多;太大(如 100000)可能触发内存溢出或锁升级。1w 是多数 OLTP 环境的甜点值 -
DestinationTableName = "dbo.MyTable":必须带 schema,不能只写"MyTable",否则抛"Invalid object name 'MyTable'" -
EnableStreaming = true:对DataTable或IDataReader源启用流式读取。不设它,DataTable会全量加载进内存再传,插 500 万行就可能 OOM
列映射错位会导致静默丢数据
SqlBulkCopy 默认按列序匹配(source 第 1 列 → target 第 1 列),不是按列名。如果源 DataTable 列顺序和目标表不一致,或者你用了 Select id, name from... 但目标表是 (name, id),数据就全插反了——不会报错,只会“成功”写入错误字段。
解决方法只有两个:
- 确保
DataTable列顺序与目标表完全一致(查SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='MyTable' ORDER BY ORDINAL_POSITION) - 或显式调用
ColumnMappings.Add("SourceColName", "TargetColName"),注意这里填的是源列名和目标列名,不是索引
漏掉映射、拼错名字、大小写不一致(SQL Server 默认不区分,但某些 collation 下会失败),都会让某列变成 NULL 或类型转换异常。
IDENTITY 和 NULL 列的坑怎么绕
目标表有 IDENTITY 列时,SqlBulkCopy 默认拒绝插入值(除非你手动 SET IDENTITY_INSERT ON)。但千万级场景下,开 IDENTITY_INSERT 有风险:它要求会话级独占,且不能跨多个表同时开。
更稳妥的做法是:
- 用
SqlBulkCopy.ColumnMappings显式跳过IDENTITY列(即不映射它),让 SQL Server 自增生成 - 如果必须导入指定 ID(比如迁移老数据),才开
IDENTITY_INSERT,并在SqlBulkCopy构造后立即执行:new SqlCommand("SET IDENTITY_INSERT dbo.MyTable ON", connection).ExecuteNonQuery() - 目标列允许
NULL,但源数据对应列为DBNull.Value—— 这没问题;但如果源是 C# 的null(比如string类型字段),必须转成DBNull.Value,否则抛"Specified cast is not valid"
另外,SqlBulkCopy 对 datetime2 和 decimal 的精度容忍度低,源列类型最好和目标完全一致,避免隐式转换失败。


















