C# 为什么Entity Framework在一个SaveChanges()中添加多个项的速度如此之慢?

C# 为什么Entity Framework在一个SaveChanges()中添加多个项的速度如此之慢?,c#,entity-framework,entity-framework-6,C#,Entity Framework,Entity Framework 6,这是对的后续研究,我试图找出代码运行缓慢的主要原因。我想我已经把它缩小到下面一个最小的例子。我的基本数据库结构如下: public class Foo { public int Id { get; set; } public string Bar { get; set; } } public class FooContext : DbContext { public DbSet<Foo> Foos { get; set; } } 我最初的想法是实体框架可能

这是对的后续研究,我试图找出代码运行缓慢的主要原因。我想我已经把它缩小到下面一个最小的例子。我的基本数据库结构如下:

public class Foo
{
    public int Id { get; set; }
    public string Bar { get; set; }
}

public class FooContext : DbContext
{
    public DbSet<Foo> Foos { get; set; }
}

我最初的想法是实体框架可能会对每个条目使用单独的事务,但记录SQL表明情况并非如此。那么,为什么执行时间会有如此大的差异呢?

在研究您的问题时,我看到了这篇启发性的文章:

这里有一句话:

插入的每个对象都需要两条SQL语句——一条用于插入记录,另一条用于获取新记录的标识

插入多个记录时,这会成为一个问题。每个记录一次插入一条,这一事实加剧了问题的严重性(但这超出了问题的范围,因为您已经在逐个插入测试)。因此,如果要插入200条记录,则需要逐个执行400条sql语句

所以据我所知,EF并不是为批量插入而构建的。即使只需插入200条记录。对我来说,这似乎是一个很大的失望

我开始想,“那么EF到底有什么用呢?它甚至不能插入几条记录。”。我将在两个方面介绍EF道具:

  • 选择查询:很容易编写查询并将数据快速输入应用程序
  • 简化复杂记录的插入。如果您曾经有过一个包含大量外键的表,并且尝试在一个事务中插入所有链接记录,那么您知道我在说什么。谢天谢地,EF按顺序插入每条记录,并为您在一次交易中链接所有相关记录。但如上所述,这是有代价的
  • 简单地说,如果您有一个需要插入一组记录的操作,那么最好使用SqlBulkCopy。它可以在几秒钟内插入数千条记录


    我知道这可能不是你想听的答案,因为相信我,这也让我感到不安,因为我经常使用EF,但我看不到任何解决方法这只是猜测,但在完成第一个查询后,你是否尝试过通过Entity Framework运行第二个等效查询,然后计时,看它是否更接近原始SQL时间


    其他人指出实体框架执行第一个查询的速度很慢,因为它有构建模型的开销。我不知道这是否正是你所看到的问题,但这似乎是可能的。不管是哪种方式,了解第二次运行是否比第一次运行快得多可能会很有用,这样我们就可以证明或排除这种可能性。

    既然你不能接受它,也不能没有它,你是否考虑过调用savechangessync()

    我到处搜索,想找到一种禁用主键同步的方法,但找不到EF 6或更低版本的主键同步方法


    EF core将DBContext.SaveChanges()中的true传递给我认为最终触发此同步的对象。另一个重载允许调用方传递false作为控制参数。

    为什么要猜测?查看SQL Server Profiler.context.parents.Add()实际发出的语句,它甚至不访问数据库,因此此行不涉及单个事务。
    SaveChanges
    使用单个事务,但执行多个SQL命令-每个记录一个
    INSERT
    UPDATE
    DELETE
    @dnickless我将其切换为synchronous以测量执行时间,我想我应该更新样本以匹配。标准EF方式的差异是秒数,而我的替代方式的差异是百分之一百秒。否,此示例中没有打开可能会减慢速度的日志记录。是否可以尝试在
    ExecuteSqlCommand()
    中使用循环,而不使用
    string.Join()
    ,而是使用单独的语句?你要处理的桌子上有什么特别的东西吗?也许是聚集索引?看到执行时间上的巨大差异,我真的很惊讶。这必须是非常明显的…虽然示例没有显示,但我可以排除这种可能性,因为存在此问题的生产代码正在持续运行。我认为@Paul的答案可能是真正的原因。@Andrew Williamson他的答案很有可能是正确的,但我认为还是值得检查一下(而且只需要很短的时间来证明/反驳)并确定。如果这确实有很大的不同,它可能表明您在生产代码中看到的问题是完全不同的。我有一个单独的生产应用程序,其中这段代码不是要运行的唯一查询,也不是要运行的第一个查询,但它始终作为应用程序中的瓶颈显示在应用程序洞察中。@Andrew Williamson好的,但我仍然认为,在最多需要几分钟时间的情况下,不至少为MVCE证明/反驳这一点是有点奇怪的;至少,您在评论中添加的内容可能是您的问题中包含的有用信息。我想补充一点,EF Core的插入(和身份检索)性能比EF6(使用单db往返)要好得多。但目前的查询转换比EF6差得多。因此,除了明显的过渡成本外,目前还没有充分的快乐:)EF似乎缺少一些关键特性,即批量插入、批量更新和批量删除。这些都是非常常见的操作,如果没有导航属性的话,它们很容易实现。此示例正在同步运行
    class Program
    {
        static void Main(string[] args)
        {
            var foos = Enumerable.Range(0, 200).Select(index => new Foo { Bar = index.ToString() });
    
            // Make sure the timing doesn't include the first connection
            using (var context = new FooContext())
            {
                context.Database.Connection.Open();
            }
    
            var s1 = Stopwatch.StartNew();
            using (var context = new FooContext())
            {
                context.Foos.AddRange(foos);
                context.SaveChanges();
            }
            s1.Stop();
    
            var s2 = Stopwatch.StartNew();
            using (var context = new FooContext())
            {
                // Ignore the lack of sanitization, this is for demonstration purposes
                var query = string.Join(";\n", foos.Select(f => "INSERT INTO Foos ([Bar]) VALUES (" + f.Bar + ")"));
                context.Database.ExecuteSqlCommand(query);
            }
            s2.Stop();
    
            Console.WriteLine("Normal way: {0}", s1.Elapsed);
            Console.WriteLine("Hard way  : {0}", s2.Elapsed);
            Console.ReadKey();
        }
    }