BULK INSERT在SQL Server 2022中仅当同时满足三个条件时才启用最小化日志:数据库恢复模式为BULK_LOGGED或SIMPLE、目标表无活跃非聚集索引(含主键)、显式指定WITH(TABLOCK)且无并发写入。

BULK INSERT 不是“写完就快”,它默认行为几乎总是慢的——必须显式配齐三个条件才能触发最小化日志,否则性能可能比多值 INSERT 还差。
必须同时满足的三个最小化日志前提
SQL Server 2022 的 BULK INSERT 只有在以下三者全部成立时,才会跳过完整日志记录、启用最小化日志(minimal logging):
- 数据库恢复模式为
BULK_LOGGED或SIMPLE(不能是FULL) - 目标表上无活跃的非聚集索引(主键/唯一约束自带的索引也算;可临时
DROP或DISABLE) -
WITH (TABLOCK)显式指定,且执行期间无其他并发写入者占用该表
漏掉任意一个,BULK INSERT 就退化成带锁的逐行日志路径,速度断崖下跌。别只改语句不改库——导入前必须跑:ALTER DATABASE yourdb SET RECOVERY BULK_LOGGED;导入完立刻切回 FULL 并做一次日志备份,否则后续日志无法截断。
TABLOCK 漏写或被忽略的典型表现
没加 TABLOCK 时,BULK INSERT 默认走行锁,尤其在目标表已有数据、且存在非空聚集索引时,极易触发锁升级为表锁,导致阻塞和超时。错误现象包括:
- 执行卡住,
sys.dm_exec_requests中看到wait_type = LCK_M_X或LCK_M_SCH_M - 同一张表并发执行多个
BULK INSERT直接失败,报错Cannot obtain a LOCK resource - 耗时随数据量非线性增长,10 万行比 5 万行慢 3 倍以上
正确写法必须带 TABLOCK,且放在 WITH 子句里:BULK INSERT orders FROM 'D:\data\orders.csv' WITH (FIELDTERMINATOR = ',', ROWTERMINATOR = '\n', TABLOCK)。别信“引擎会自动优化”的说法——它不会。
文件格式与字段解析的硬性限制
BULK INSERT 对源文件极其挑剔,类型不匹配或格式异常会导致整批失败,错误信息却只报 Cannot bulk load. The file ... could not be opened 或更模糊的 bulk load data conversion error,不指明哪一行哪一列。
- 字段值含换行符、未转义双引号、NULL 字符时,
FORMAT = 'CSV'(SQL Server 2017+)比手动设FIELDTERMINATOR更健壮,但必须加FIRSTROW = 2跳过表头 - 源字段类型必须严格匹配目标列:CSV 里的
"123"插INT列可以,但"123.45"插INT会整批失败 - 含
NULL的字段必须用KEEPNULLS,否则默认填默认值或触发NOT NULL约束错误
建议先导出一小段样本数据,用 SELECT TOP 10 * FROM OPENROWSET(BULK ...) 预校验格式是否可解析。
ROWS_PER_BATCH 设多少才稳
这个参数控制每次提交的行数,直接影响内存占用、tempdb 压力和日志写入节奏。设太小(如 1000)事务开销大;设太大(如 100000)容易引发 tempdb 争抢或内存不足。
- 实测在 SQL Server 2022 上,
ROWS_PER_BATCH = 10000是多数场景下的甜点值:兼顾吞吐与稳定性 - 超过 50000 后,
tempdb的allocation_bitmap争抢明显上升,sys.dm_db_task_space_usage中能看到大量等待 - 如果目标表有计算列或触发器(不推荐),必须大幅调低,否则单批处理时间过长,易超时
别依赖默认值(0,即全当一批)。加上它:ROWS_PER_BATCH = 10000,是最小代价的稳态保障。
真正卡住性能的往往不是语句本身,而是恢复模式没切、索引没禁、TABLOCK 没写、文件字段隐式转换失败——这些点全得手动确认,SQL Server 不会替你兜底。


















