ef core 原生不支持真正批量插入,addrange+savechanges 仍是逐条执行insert;z.entityframework.extensions.efcore 提供跨库真批量插入,但需注意主键返回、触发器、事务、约束等限制。

EF Core 原生不支持真正意义上的批量插入,AddRange + SaveChanges 只是“批量标记”,不是“批量执行”——1000 条数据会发 1000 条 INSERT,性能瓶颈明显。
为什么 AddRange 不等于 BulkInsert
很多人误以为调用 AddRange 就完成了批量优化,其实它只跳过了循环 Add 的开销,底层仍由 EF Core 逐条生成 SQL 并提交。实测 1 万条数据在 SQL Server 上耗时约 22 秒(未启用批处理),而原生批量可压到 1.1 秒左右。
-
AddRange后必须调用SaveChanges才写入,且每条 INSERT 独立执行,无法复用连接或参数化批处理 - 它参与 EF Core 的变更跟踪、验证、触发器、外键检查等全链路,开销固定偏高
- 不支持跳过索引、禁用约束、自定义
SqlBulkCopyOptions等数据库级控制
用 Z.EntityFramework.Extensions 实现真批量插入
目前最稳定、跨数据库(SQL Server / PostgreSQL / MySQL)、与 EF Core 9 兼容的方案是 Z.EntityFramework.Extensions.EFCore(注意不是已归档的 EFCore.BulkExtensions)。
- 安装包:
Z.EntityFramework.Extensions.EFCore(NuGet) - 引入命名空间:
using Z.EntityFramework.Extensions; - 直接调用:
await context.BulkInsertAsync(entities),无需AddRange或SaveChanges - 自动按数据库类型选择底层机制:SQL Server 用
SqlBulkCopy,PostgreSQL 用COPY,MySQL 用LOAD DATA INFILE或批量INSERT
示例:
var users = Enumerable.Range(1, 5000)
.Select(i => new User { Name = $"User{i}", Email = $"u{i}@test.com" })
.ToList();
await context.BulkInsertAsync(users); // 一次调用,底层单次传输
主键、触发器、事务这些坑必须提前确认
批量插入绕过了 EF Core 的常规生命周期,很多默认行为会失效或需显式开启:
- 自增 ID 默认不返回:若需插入后获取
Id,得设options.IncludeIdentity = true(仅 SQL Server 支持,且性能下降) - ON INSERT 触发器默认不执行:SQL Server 下需手动加
options.SqlBulkCopyOptions = SqlBulkCopyOptions.FireTriggers - 不自动加入当前
DbContext的事务:如果已在BeginTransaction中,必须显式传入 transaction 参数,否则独立提交 - 外键和 CHECK 约束默认校验:若要跳过(比如先插主表再插子表),需设
options.SqlBulkCopyOptions = SqlBulkCopyOptions.TableLock,并确保数据库级约束已禁用(慎用)
什么时候不该用 BulkInsert
它快,但不是万能解药。以下场景建议退回 AddRange 或 ExecuteInsert:
- 数据量 AddRange +
UseBatchSize(100)足够,还保有实体跟踪和业务逻辑钩子 - 需要插入后立刻用新 ID 做关联操作:BulkInsert 不返回 ID(除非 SQL Server +
IncludeIdentity),而ExecuteInsert(EF Core 7+)支持EntityEntry返回 - 表上有复杂触发器或审计逻辑:BulkInsert 会跳过大部分,除非你主动开启
FireTriggers并接受性能损失 - 目标数据库是 SQLite 或较老版本的 SQL Server(SqlBulkCopy
真正难的不是调哪个方法,而是判断“要不要绕过 EF Core”——一旦用了 BulkInsert,你就得自己管住一致性、ID、触发器、事务边界这些事,而不是交给框架兜底。











