必须主动规避页拆分,核心是确保源数据按聚集索引键预排序,并禁用非聚集索引、启用TABLOCK与BULK_LOGGED模式;禁用聚集索引不可行,NEWID()等无序主键须避免。

批量插入时出现页拆分,本质是数据写入顺序与聚集索引键顺序不一致,导致 SQL Server 被迫分裂已有数据页来腾出空间。在 SQL Server 2019 中,这不是“能不能避免”的问题,而是“必须主动规避”,否则即使用了 BULK INSERT 或 SqlBulkCopy,性能优势也会被碎片和锁争用吃掉。
确保源数据按聚集索引键预排序
页拆分最常发生在向有聚集索引的表中无序插入数据时。SQL Server 会试图把新行插入到逻辑位置对应的位置,若该页已满,就触发拆分。解决办法不是关索引,而是让数据来得“顺理成章”:
- 如果使用
BULK INSERT,在WITH子句中显式指定ORDER (clustered_key_column),前提是源文件本身已按该列排序;否则该提示会被忽略 - 如果使用
SqlBulkCopy,在调用前对内存中的DataTable或DataReader按聚集索引列排序(例如ORDER BY Id),并设置SqlBulkCopy.DestinationTableName对应的表确实以该列为聚集键 - 未排序数据强制走
ORDER提示会导致导入失败或警告:Warning: The ORDER hint is ignored because the data file is not sorted.
临时禁用非聚集索引,但别碰聚集索引
非聚集索引更新是页拆分的间接推手——每插一行,都要维护所有非聚集索引的 B+ 树结构。而聚集索引本身是数据的物理组织方式,禁用它等于删除表。所以操作原则很明确:
- 导入前用
ALTER INDEX ... DISABLE禁用所有非聚集索引(包括唯一、包含列、筛选索引) - 导入完成后用
ALTER INDEX ... REBUILD重建,此时 SQL Server 会基于已排序的数据一次性构建索引页,大幅减少碎片 - 不要尝试
DROP非聚集索引再CREATE,重建比删建快,且保留统计信息元数据 - 注意:若表有
UNIQUE或PRIMARY KEY约束,对应索引无法DISABLE,需先NOCHECK CONSTRAINT(仅适用于可信约束)
配合 TABLOCK + BULK_LOGGED 恢复模式
单靠排序和禁用索引还不够,页拆分还受锁粒度和日志压力影响。这两个配置能从底层减少页级竞争:
- 在
BULK INSERT的WITH子句中加TABLOCK,让整个导入过程只持有一个表级锁,避免多线程/多批次插入时因页锁升级引发的阻塞和拆分连锁反应 - 导入前将数据库恢复模式临时设为
BULK_LOGGED:ALTER DATABASE YourDB SET RECOVERY BULK_LOGGED,这样最小化日志记录量,加快写入速度,间接降低页写入延迟导致的拆分概率 - 导入后立即切回
FULL恢复模式,并做一次日志备份,否则后续日志截断会失败 -
TABLOCK对SqlBulkCopy无效,它默认行为等价于加了该提示,但需确保连接字符串中没启用MultipleActiveResultSets=true(会干扰锁行为)
警惕 identity 列和 newid() 导致的天然无序
很多人以为只要用了自增 IDENTITY 就自动有序,其实不然。批量导入时,IDENTITY 值由 SQL Server 批量分配(如一次分 1000 个),但这些值写入页的物理顺序仍取决于缓冲区调度和锁竞争。更危险的是用 NEWID() 或 NEWSEQUENTIALID():
-
NEWID()生成完全随机 GUID,插入必然打乱物理顺序,几乎必然引发高频页拆分,绝对禁止用于聚集索引列 -
NEWSEQUENTIALID()虽然递增,但它只保证“重启后递增”,不保证跨会话连续,且在 Always On 可用性组中可能重复,仍不如整型IDENTITY可控 - 若必须用 GUID 作主键,务必把聚集索引建在另一列(如
Id INT IDENTITY),而用UNIQUE NONCLUSTERED约束保护Guid列
真正难处理的不是技术选项,而是业务数据流本身是否允许预排序——比如实时日志归档场景,时间戳虽有序,但入库前被 Kafka 分区打散,这时就得在 ETL 层做 merge sort,而不是指望 SQL Server 自己“理顺”。










