如何加速DbSet.Add()方法?

32

我需要从CSV文件中导入大约3万行数据到我的SQL数据库,但这需要20分钟的时间。

通过分析器进行故障排除后发现,DbSet.Add花费了最多的时间,但为什么呢?

我有以下Entity Framework Code-First类:

public class Article
{
    // About 20 properties, each property doesn't store excessive amounts of data
}

public class Database : DbContext
{
    public DbSet<Article> Articles { get; set; }
}

我的for循环中,对于每个条目我都会执行以下操作:

db.Articles.Add(article);

在 for 循环之外,我执行以下操作:

db.SaveChanges();

它连接到我的本地SQLExpress服务器,但我猜在调用SaveChanges之前没有任何东西被写入,所以我想服务器不会是问题....


1
你好。你是摆脱了Entity Framework还是和EF一起使用sqlbulkcopy?我在使用.Add()时遇到了完全相同的问题。 - Kervin Ramen
7
如果您设置如下代码: db.Configuration.ValidateOnSaveEnabled = false; db.Configuration.AutoDetectChangesEnabled = false;将会获得巨大的性能提升。但您必须确认这些值是正确的。 - Kervin Ramen
在注释中使用反引号(`)来表示代码。看起来很有趣,我稍后会研究这些属性... - Tamara Wijsman
5个回答

47

这是一个非常棒的知识点!解决了我插入4k条记录时使用EF遇到的巨大问题,而无需重新编写代码来使用批量复制。我认为批量复制是人们在没有进一步分析问题的情况下采用的简单答案。在我的情况下,SQL插入只需要<1秒,而EF添加则需要30-40秒,因此这个解决方法非常完美。感谢提供这些信息! - Alex

24

我要在Kervin Ramen的评论中补充说,如果你只是进行插入操作(没有更新或删除),那么通常在对上下文进行任何插入操作之前,可以安全地设置以下属性:

DbContext.Configuration.AutoDetectChangesEnabled = false;
DbContext.Configuration.ValidateOnSaveEnabled = false;

我在工作中遇到了一次性大量导入数据的问题。如果不设置上述属性,将约7500个复杂对象添加到上下文中需要超过30分钟。设置上述属性(即禁用EF检查和更改跟踪)将导入时间缩短到几秒钟。

但是,请注意仅在进行插入操作时使用此方法。如果您需要将插入与更新/删除混合使用,可以将代码分为两个路径,并对插入部分禁用EF检查,然后在更新/删除路径上重新启用检查。我已经成功地使用这种方法解决了DbSet.Add()操作速度慢的问题。


这真是太棒了,我可能会尝试一下并将其与批量插入进行比较。谢谢你的分享!还要感谢提醒,看来我忘记了那个评论,但我明天晚上一定会研究一下... - Tamara Wijsman
尝试后,这种方法似乎比批量插入慢,所以我不能使用这种方法。详细来说,我正在进行350,000个.Add()操作(实体没有引用其他实体,只有具有合理值的字段),然后是一个.SaveChanges()操作,在调用添加或保存更改之前将其设置为false,并在保存更改后将其设置为true;花费的时间比批量插入要长得多,所以我甚至不打算让它运行。 - Tamara Wijsman
我简直不敢相信。这让我的一天都变得美好了,而且我的老板也会很高兴。运行得非常顺畅 :) - Alireza

10

每个工作单元中的每个项目都有额外开销,因为它必须检查(和更新)身份管理器,添加到各种集合等。

我建议尝试的第一件事是分批处理,比如按照500个一组(根据需要更改数量),每次都使用新的对象上下文 - 因为否则可以合理地期望性能出现折叠。将其分成批次还可以防止庞大的事务使所有操作停止。

此外; SqlBulkCopy。它专为大型导入设计,具有最小的开销。不过它并不是EF。


如果在您的设计中适用,我肯定会选择SqlBulkCopy。 - Enrico Campidoglio
我现在正在尝试完成一些任务,但我想知道它是否只会根据列名而不是属性接受匹配... - Tamara Wijsman
2
分组建议让它变快了一点,但速度还不够快。经过一些迭代和严重错误的排查后,我成功地使用SqlBulkCopy了,虽然代码很糟糕,但它起作用了。可能以后会重构它或检查是否支持批量插入...感谢Marc和聊天中给出相似建议的人们!看,那个花费了20分钟的东西现在只需要2秒,简直像魔法... - Tamara Wijsman
@tomwij 这是一个相当可观的成果。 - Marc Gravell
这个将 SqlBulkCopy 在 EF 上下文中更易用的工具。https://ruijarimba.wordpress.com/2012/03/25/bulk-insert-dot-net-applications-part1/ 另请参阅 https://ruijarimba.wordpress.com/2012/03/18/entity-framework-get-mapped-table-name-from-an-entity/ - xan

5
这里有一个极易使用且速度非常快的扩展程序: https://efbulkinsert.codeplex.com/ 它叫做“Entity Framework Bulk Insert”。
扩展程序本身在命名空间EntityFramework.BulkInsert.Extensions中。因此,要揭示扩展方法,请添加using。
using EntityFramework.BulkInsert.Extensions;

接着,您可以这样做

context.BulkInsert(entities);

顺便提一下,如果由于某些原因您不想使用此扩展,您可以尝试创建一系列文章列表并使用AddRange(EF 6中的新功能之一)将它们一起添加到dbcontext,而不是为每篇文章运行db.Articles.Add(article)。


不幸的是,我遇到了“字典中未找到给定的键”的错误,并且似乎在这里没有很好的答案。 - PeterX
它跳过每一行的验证调用,在最后进行一次验证。 - ScottB
这不是关于验证的问题。关键区别在于Add每次内部执行昂贵的DetectChanges调用,而AddRange仅在添加所有项目后执行一次。 - Gert Arnold

1

我没有真正尝试过这个,但我的逻辑是保留ODBC驱动程序将文件加载到datatable中,然后使用SQL存储过程将表传递给过程。

对于第一部分,请尝试: http://www.c-sharpcorner.com/UploadFile/mahesh/AccessTextDb12052005071306AM/AccessTextDb.aspx

对于第二部分,请尝试使用以下SQL过程: http://www.builderau.com.au/program/sqlserver/soa/Passing-table-valued-parameters-in-SQL-Server-2008/0,339028455,339282577,00.htm

并在C#中创建SqlCommnand对象,并将SqlParameter添加到其Parameters集合中,该参数为SqlDbType.Structured

希望能对您有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接