并发INSERT在SQL Server 2022中更慢,因锁升级、日志争用和闩锁竞争导致吞吐不升反降;需用TABLOCK、BULK INSERT或TVP小批提交优化。

为什么并发INSERT在SQL Server 2022里反而更慢?
并发INSERT不等于吞吐翻倍——默认行为下,多个会话同时向同一张表插入数据,大概率触发锁升级、日志争用和闩锁(latch)竞争,尤其当目标表有聚集索引且数据非顺序写入时。常见现象包括:wait_type = LCK_M_X、PAGEIOLATCH_SH飙升、sys.dm_os_waiting_tasks中大量WRITELOG等待。
根本原因不是CPU或磁盘不够,而是事务日志写入路径串行化 + 索引页分裂 + 缓冲区管理器争用。SQL Server 2022没改这个底层模型,所以“开10个线程INSERT”常比单线程还慢。
- 别盲目增加并发数:实测表明,对普通OLTP表,2–4路并发常是拐点;超过后吞吐不升反降
- 确保所有并发INSERT都显式使用
TABLOCK(仅适用于BULK INSERT或INSERT INTO ... SELECT),否则各行锁叠加成表锁 - 避免在高并发INSERT期间执行
ALTER INDEX ... REORGANIZE或统计信息自动更新,它们会抢KEY或PAGElatch
BULK INSERT并发必须配齐三个最小化日志条件
BULK INSERT是唯一能真正提升并发写入吞吐的原生命令,但它不是“加个WITH就快”,漏掉任一条件就会退化为逐行完整日志,速度可能比INSERT ... VALUES还差。
- 数据库恢复模式必须是
BULK_LOGGED或SIMPLE:ALTER DATABASE yourdb SET RECOVERY BULK_LOGGED,导入完立刻切回FULL并做日志备份,否则后续日志无法截断 - 目标表不能有活跃的非聚集索引(含主键/唯一约束自建的索引):要么提前
DROP,要么DISABLE,导入完成后再REBUILD - 必须显式指定
WITH (TABLOCK),且执行期间无其他会话对该表做写操作;TABLOCK不在WITH子句里=无效
错误示例:BULK INSERT t FROM 'x.csv' WITH (FIELDTERMINATOR=',') —— 缺TABLOCK,默认走行锁,10万行可能卡住;正确写法:BULK INSERT t FROM 'x.csv' WITH (FIELDTERMINATOR=',', TABLOCK)。
用TVP+小批次提交替代盲目并发
当数据来自应用层(如.NET API)、无法落地为文件时,Table-Valued Parameter(TVP)比多线程拼SQL更可控。它把一批行打包传入存储过程,减少网络往返,但依然走完整日志路径——所以关键在“小批次+及时提交”。
- 单次TVP调用建议≤5000行:超1万行时,.NET序列化开销陡增,且SQL Server解析参数表的内存压力明显上升
- 必须显式分批提交:在循环中每插5000行就
COMMIT一次,绝不能把全部百万行包在一个事务里 - 存储过程中禁用
SET NOCOUNT OFF(保持默认ON),避免客户端被大量xx rows affected消息阻塞 - 目标表对应字段要有索引:如果TVP数据要JOIN维度表,驱动侧字段(如
region_id)没索引,优化器仍会扫全表,吞吐瓶颈立刻转移过去
临时表驱动+边界ID分片比@@ROWCOUNT可靠
用WHILE @@ROWCOUNT > 0做分批INSERT是经典陷阱:最后一批不足设定大小时,@@ROWCOUNT变成实际插入行数(比如123),下一轮SELECT没命中任何行,@@ROWCOUNT变0,循环直接退出,剩余数据丢失。
正确做法是用边界值(如id)切片,配合临时表预存范围:
- 先建带聚簇索引的临时表:
CREATE TABLE #batch_ids (id INT PRIMARY KEY),再INSERT INTO #batch_ids SELECT id FROM source WHERE ... ORDER BY id - 用
MIN(id)和MAX(id)驱动分片:WHERE id BETWEEN @start AND @end,每次取固定数量(如10000)的id - 确保源表
id有索引,否则SELECT MIN/MAX本身就成了性能瓶颈
这种模式下,控制逻辑在应用层或SQL脚本里更清晰,不会因某批数据量波动而意外中断——这是生产环境批量导入最不容易出错的底座设计。

















