如何修复EF Core 7批量插入性能缓慢问题
批量插入Parent/Child实体的性能优化方案
先指出你代码里的两个明显错误:
- Child类定义写错了,应该是
public class Child,不是public class Parent DoSomething方法用了await但没标记为async,得改成public async Task DoSomething(List<Parent> newItems)
下面是针对10万条关联实体批量插入的核心优化建议:
1. 用全新DbContext处理每一批数据
ChangeTracker.Clear()没法彻底清除上下文内部的状态管理开销,不如直接为每一批数据创建新的上下文实例,用完自动销毁:
public async Task DoSomething(List<Parent> newItems) { const int batchSize = 500; // .NET 6+ 用Chunk分批,低版本可自行写循环拆分列表 var batches = newItems.Chunk(batchSize); foreach (var batch in batches) { using var dbContext = new YourDbContext(); // 替换成你的上下文类型 // 关闭自动变更检测和延迟加载,砍掉不必要的性能消耗 dbContext.ChangeTracker.AutoDetectChangesEnabled = false; dbContext.ChangeTracker.LazyLoadingEnabled = false; foreach (var parent in batch) { dbContext.Parents.Add(parent); } await dbContext.SaveChangesAsync(); // using块结束时上下文自动释放,无残留状态 } }
2. 强制关闭自动变更检测
批量操作时,EF默认的AutoDetectChangesEnabled会在每次Add时触发变更检测,随着实体数量增加,这个操作的开销会指数级上升,必须关闭。如果后续需要正常使用上下文,操作完再重新开启即可。
3. 使用第三方批量插入库
如果项目允许引入依赖,Z.EntityFramework.Extensions.EFCore或者EntityFramework.BulkInsert这类库能直接生成批量插入SQL,完全绕过EF的实体跟踪逻辑,性能提升非常显著:
// 示例:用Z.EntityFramework.Extensions批量插入整批数据 await _db.BulkInsertAsync(newItems); // 或者分批插入,避免单次数据量过大 foreach (var batch in batches) { await dbContext.BulkInsertAsync(batch); }
4. 直接用SqlBulkCopy做极致优化
如果追求最高性能,可以跳过EF,直接用SqlBulkCopy把数据写入数据库。需要把实体转换成DataTable或者SqlDataRecord,注意手动处理父子实体的关联ID:
var connectionString = "你的数据库连接字符串"; using var bulkCopy = new SqlBulkCopy(connectionString); bulkCopy.DestinationTableName = "Parents"; // 映射实体属性和数据库列 bulkCopy.ColumnMappings.Add(nameof(Parent.Id), "Id"); bulkCopy.ColumnMappings.Add(nameof(Parent.SomeProperty), "SomeProperty"); // 把Parent列表转成DataTable var parentTable = ConvertParentListToDataTable(newItems); await bulkCopy.WriteToServerAsync(parentTable); // 同理处理Child表,注意关联ParentId // 若Parent用自增ID,可通过OUTPUT子句批量获取插入后的ID再关联Child
5. 修正原代码的批次逻辑
如果坚持使用原上下文,先修正批次判断的顺序(原代码会先执行一次空的Save操作),再配合ChangeTracker优化:
public async Task DoSomething(List<Parent> newItems) { var count = 0; const int batchSize = 500; _db.ChangeTracker.AutoDetectChangesEnabled = false; foreach (var newItem in newItems) { _db.Parents.Add(newItem); count++; if (count % batchSize == 0) { await _db.SaveChangesAsync(); _db.ChangeTracker.Clear(); // Clear后自动检测可能会重置,需要重新关闭 _db.ChangeTracker.AutoDetectChangesEnabled = false; } } // 处理最后一批不足500条的数据 if (count % batchSize != 0) { await _db.SaveChangesAsync(); _db.ChangeTracker.Clear(); } }
另外,existingItems.Add(newItem)如果是保存已插入的实体,记得把实体从上下文分离,避免持续跟踪带来的开销:
_db.Entry(newItem).State = EntityState.Detached; existingItems.Add(newItem);
内容的提问来源于stack exchange,提问作者Bob.at.Indigo.Health
相关产品推荐
相关产品推荐

