普通insert多值语法撑不住百万行,因每行触发约束检查、索引维护等完整日志路径,超10万行易锁升级和日志满;安全上限约5000–10000行/次。

直接用单条 INSERT INTO ... VALUES (...), (...) 插几万行,性能会断崖式下跌;真正能扛住百万级插入的,只有 BULK INSERT、SqlBulkCopy 或带显式提交的分批事务——其他写法基本是自我安慰。
为什么普通INSERT多值语法撑不住百万行
它看似批量,实则仍是完整日志路径:每行都触发约束检查、索引维护、触发器(如有)、锁升级判断。当数据量超过 10 万行,SQL Server 很可能把行锁升级为表锁,同时事务日志持续增长,最终卡在日志截断或 log_full 错误上。
- 最大安全边界约 5000–10000 行/次,再往上就容易触发 tempdb 压力或内存不足
- 字段含
NULL、默认值、计算列时,VALUES 列表必须显式写出,否则报错且不提示具体哪一列 - 不支持
TABLOCK提示,无法跳过锁争用优化,纯靠引擎自动判断,不可控
BULK INSERT 必须配 TABLOCK + 恢复模式切换
BULK INSERT 是唯一原生绕过常规日志路径的方式,但它的最小化日志(minimal logging)不是默认开启的——必须同时满足三个条件:目标表无活跃非聚集索引、使用 TABLOCK、数据库恢复模式为 BULK_LOGGED 或 SIMPLE。
- 别只改语句不改库:执行前先运行
ALTER DATABASE yourdb SET RECOVERY BULK_LOGGED -
TABLOCK必须写在WITH子句里,漏掉就退化成行锁,速度可能比普通 INSERT 还慢 - 导入完立刻切回
FULL恢复模式,并做一次日志备份,否则后续日志无法截断 -
ROWS_PER_BATCH = 10000比默认值更稳;超过 50000 容易引发 tempdb 争抢
SqlBulkCopy 是 .NET 场景下的实际首选
当你数据来自 API、内存集合或 DataTable,SqlBulkCopy 是唯一能逼近 BULK INSERT 性能的选择,但它不是“开箱即用”——默认行为反而容易翻车。
- 必须设
BatchSize = 5000,否则默认一次性提交全部行,事务日志和内存双爆 - 列映射要显式调用
ColumnMappings.Add("src","dst"),源列顺序≠目标表顺序时,不映射就报错 - 不要在外层手动
BeginTransaction——SqlBulkCopy内部已用最小日志模式,加事务只会扩大锁范围 - 大数据流启用
EnableStreaming = true,但要求源实现IDataReader,不能传List<t></t>
用 WHILE 分批时,别用 @@ROWCOUNT 控制循环
常见陷阱是写 WHILE @@ROWCOUNT > 0 BEGIN INSERT TOP (@BatchSize) ... SELECT ... END,这会导致最后一批不足 @BatchSize 时提前退出,剩余数据永远插不进去。
- 正确做法是用边界 ID:先查出最小 ID,每次插
WHERE id > @lastId AND id ,再更新 <code>@lastId - 每次 INSERT 后必须跟
COMMIT,否则整个 WHILE 包在一个隐式事务里,日志照涨不误 - 如果表有自增主键,优先用
WHERE id BETWEEN @start AND @end,比TOP+ 排序更稳定
最容易被忽略的一点:所有方案都依赖“目标表初始状态”。如果表已有大量数据且建了多个非聚集索引,BULK INSERT 和 SqlBulkCopy 都会退化成完整日志模式——此时必须先 DROP INDEX,插完再重建,而不是寄希望于“批次够小就能扛住”。










