EF Core 慢速批量插入(~80k 行)

Posted

技术标签:

【中文标题】EF Core 慢速批量插入(~80k 行)【英文标题】:EF Core Slow Bulk Insert (~80k rows) 【发布时间】:2020-05-14 04:25:03 【问题描述】:

我有一个Save 对象,它关联了多个集合。对象的总大小如下:

对象之间的关系可以从这个映射中推断出来,并且似乎在数据库中正确表示。查询也很好。

modelBuilder.Entity<Save>().HasKey(c => c.SaveId).HasAnnotation("DatabaseGenerated",DatabaseGeneratedOption.Identity);
modelBuilder.Entity<Save>().HasMany(c => c.Families).WithOne(x => x.Save).HasForeignKey(x => x.SaveId);
modelBuilder.Entity<Save>().HasMany(c => c.Countries).WithOne(x => x.Save).HasForeignKey(x => x.SaveId);
modelBuilder.Entity<Save>().HasMany(c => c.Provinces).WithOne(x => x.Save).HasForeignKey(x => x.SaveId);
modelBuilder.Entity<Save>().HasMany(c => c.Pops).WithOne(x => x.Save).HasForeignKey(x => x.SaveId);
modelBuilder.Entity<Country>().HasOne(c => c.Save);
modelBuilder.Entity<Country>().HasMany(c => c.Technologies).WithOne(x => x.Country).HasForeignKey(x => new x.SaveId, x.CountryId);
modelBuilder.Entity<Country>().HasMany(c => c.Players).WithOne(x => x.Country).HasForeignKey(x => new x.SaveId, x.CountryId);
modelBuilder.Entity<Country>().HasMany(c => c.Families).WithOne(x => x.Country).HasForeignKey(x => new x.SaveId, x.OwnerId);
modelBuilder.Entity<Country>().HasMany(c => c.Provinces).WithOne(x => x.Owner);
modelBuilder.Entity<Country>().HasKey(c => new  c.SaveId, c.CountryId );
modelBuilder.Entity<Family>().HasKey(c => new  c.SaveId, c.FamilyId );
modelBuilder.Entity<Family>().HasOne(c => c.Save);
modelBuilder.Entity<CountryPlayer>().HasKey(c => new  c.SaveId, c.CountryId, c.PlayerName );
modelBuilder.Entity<CountryPlayer>().HasOne(c => c.Country);
modelBuilder.Entity<CountryPlayer>().Property(c => c.PlayerName).HasMaxLength(100);
modelBuilder.Entity<CountryTechnology>().HasKey(c => new  c.SaveId, c.CountryId, c.Type );
modelBuilder.Entity<CountryTechnology>().HasOne(c => c.Country);
modelBuilder.Entity<Province>().HasKey(c => new  c.SaveId, c.ProvinceId );
modelBuilder.Entity<Province>().HasMany(c => c.Pops).WithOne(x => x.Province);
modelBuilder.Entity<Province>().HasOne(c => c.Save);
modelBuilder.Entity<Population>().HasKey(c => new  c.SaveId, c.PopId );
modelBuilder.Entity<Population>().HasOne(c => c.Province);
modelBuilder.Entity<Population>().HasOne(c => c.Save);

我从一个文件中解析了整个save,所以我不能一个一个地添加所有的集合。解析后,我有一个 Save 及其所有关联的集合,最多可添加 80k 个对象,这些对象都不存在于数据库中。

然后,当我调用dbContext.Add(save)时,处理大约需要 44 秒,RAM 使用量从 100mb 上升到大约 700mb。

然后,当我调用 dbContext.SaveChanges()(我还尝试了 EF Extensions 中的常规 BulkSaveChanges() 方法,没有显着差异)时,它需要额外的 60 秒,RAM 使用量上升到 1.3Gb。

这里发生了什么?为什么这么长和这么多的内存使用?实际上传到数据库只需要大约最后 5 秒。

PS:我也尝试过禁用更改检测但没有任何效果。

PS2:实际用法和cmets中要求的完整代码:

public class HomeController : Controller

    private readonly ImperatorContext _db;

    public HomeController(ImperatorContext db)
    
        _db = db;
    

    [HttpPost]
    [RequestSizeLimit(200000000)]
    public async Task<IActionResult> UploadSave(List<IFormFile> files)
    
        [...]
        await using (var stream = new FileStream(filePath, FileMode.Open))
        
            var save = ParadoxParser.Parse(stream, new SaveParser());
            if (_db.Saves.Any(s => s.SaveKey == save.SaveKey))
            
                 response = "The save you uploaded already exists in the database.";
            
            else
            
                 _db.Saves.Add(save);
            
            _db.BulkSaveChanges();
        
        [...]
    


【问题讨论】:

请显示实际代码,并显示您如何使用BulkSaveChanges() 欢迎来到 ORM 的奇妙世界。数据访问代码太重要且性能至关重要,无法由机器设计。 实体框架不是批量操作的最佳选择。也许,在这种情况下,尝试创建一个存储过程并将数据作为 json 传递?我不知道 MariaDB。您仍然可以通过 EF 调用存储过程。 @holger 只有一次保存。 Any 搜索非常快,因为它由 ef 转换为 sql 中的存在选择。问题不存在 我明白你为什么要一次性保存完整的对象图,但正如你所说,它不符合你的性能预期,所以你必须找到另一种方法。如何将文件加载到“不同”对象图中,然后分别提取不同的“表”,然后将它们存储到数据库中,这意味着您一次可以进行 1000 次部分插入。 【参考方案1】:

编辑:1. 确保问题不是数据库。

执行你自己的命令,看看它的运行速度。

    通过为每个工作单元使用新上下文来保持活动上下文图较小,同时尝试关闭 AutoDetechChangesEnabled

3.将多个命令批处理在一起

这是Entity Framework and slow bulk INSERTs上的一篇好文章

【讨论】:

它不是 SQL(顺便说一下它的 MariaDB),因为缓慢的部分从尚未联系数据库的 Add 方法开始 对不起,我错过了那部分,只是自动假设它是sql。同样的原则仍然适用,因为它仍然可以帮助提高性能。你能告诉我们所有相关的代码吗?那会更有帮助。 我添加了完整的代码,希望它可以帮助您理解问题;) 谢谢,还有几个问题。 BulkSaveChanges 是做什么的?我想尝试找到一个与数据库往返次数最少的解决方案。你有没有尝试过这样的事情? entityframeworkcore.com/saving-data-bulk-insert?它为您做到这一点并且易于使用。 BulkSaveChanges 来自您链接我的同一个库。不过我也会测试批量插入。【参考方案2】:

从 nugets 下载 EFCore.BulkExtensions

删除“_db.BulkSaveChanges();”并替换“_db.Saves.Add(save);”使用此代码

_db.Saves.BulkInsert(save);

【讨论】:

以上是关于EF Core 慢速批量插入(~80k 行)的主要内容,如果未能解决你的问题,请参考以下文章

具有许多索引的表的慢速批量插入

Python批量插入问题?

PostgreSQL 上的 EF Core 批量删除

对EF Core进行扩展使支持批量操作/复杂查询

MyCat批量插入

ABP CORE+EF 批量删除修改