【问题标题】:Improving bulk insert performance in Entity framework [duplicate]提高实体框架中的批量插入性能[重复]
【发布时间】:2011-08-31 17:19:18
【问题描述】:

我想通过实体框架在一个表中插入20000条记录,大约需要2分钟。除了使用SP来提高它的性能,还有什么办法。这是我的代码:

 foreach (Employees item in sequence)
 {
   t = new Employees ();
   t.Text = item.Text;
   dataContext.Employees.AddObject(t);                  
 }
 dataContext.SaveChanges();

【问题讨论】:

  • 查看此解决方案 [关于将 SqlBulkCopy 用于通用列表 ](elegantcode.com/2012/01/26/…)。与代码优先 POCO 一起使用,速度会快得多。
  • 我在一些数据上使用了它,并将我的插入时间从半小时以上缩短到约 15 秒(大约 50K 行)。
  • SqlBulkCopy 是(并且一直是)将记录插入 SQL Server 的最快方法,我认为我在下面的答案中提供的实现比@dubbreak 的实现更好。我所描述的问题也适用于该代码。

标签: entity-framework entity


【解决方案1】:

有一些改进的机会(如果您使用的是DbContext):

设置:

yourContext.Configuration.AutoDetectChangesEnabled = false;
yourContext.Configuration.ValidateOnSaveEnabled = false;

SaveChanges() 在 100 个插入的包装中... 或者您可以尝试使用 1000 件的包装并查看性能变化。

由于在所有这些插入过程中,上下文是相同的并且越来越大,您可以每插入 1000 次就重建上下文对象。 var yourContext = new YourContext(); 我认为这是最大的收获。 p>

在我的导入数据过程中进行此改进,从 7 分钟缩短到 6 秒。

实际数字...在您的情况下不能是 100 或 1000...尝试并调整它。

【讨论】:

  • 我这样做了,我的 19,000 行数据插入时间从 20 分钟缩短到不到 10 秒
  • 40000 行用了大约 4 秒。我没有更新上下文,只是使用了配置更改并每 1000 次保存一次。太棒了。
  • 我可以确认。这将批量导入提高了 100000%!
  • 我什至不需要将它保存在 100 或 1000 个对象的包中,我可以看到巨大的性能提升。在生产代码中使用一些幻数作为包大小可能是危险的,因为它可以在您的环境中工作,但不能在客户环境中工作。总之效果很好
  • 我想我是唯一的一个,但对我没有任何影响。
【解决方案2】:

以这种方式执行此操作时,无法强制 EF 提高性能。问题是 EF 在到数据库的单独往返中执行每个插入。是不是很厉害?甚至 DataSet 也支持批处理。检查this article 以获得一些解决方法。另一种解决方法是使用自定义存储过程接受表值参数,但您需要原始 ADO.NET。

【讨论】:

  • 你也可以查看我的回答,还有提升性能的空间。
  • 我不确定为什么这是公认的答案,因为它明显是错误的。使用 EF 进行大型插入时,有一些方法可以提高性能。 Romias 提到了其中之一。另一种是将所有插入包装在单个事务范围内。如果这些选项对您来说仍然不够好(如果是这种情况,您可能还有其他问题),您可以从 context 获取 Connection 对象并将其与 SQLBulkCopy 对象一起使用以加载数据。
【解决方案3】:

使用下面的代码,您可以使用一种方法扩展部分上下文类,该方法将获取实体对象的集合并将它们批量复制到数据库中。只需将类的名称从 MyEntities 替换为您的实体类的名称,并将其添加到您的项目中,在正确的命名空间中。之后,您需要做的就是调用 BulkInsertAll 方法来移交您要插入的实体对象。不要重用上下文类,而是在每次使用时创建一个新实例。这是必需的,至少在某些版本的 EF 中是必需的,因为与此处使用的 SQLConnection 关联的身份验证数据在使用该类一次后会丢失。我不知道为什么。

此版本适用于 EF 5

public partial class MyEntities
{
    public void BulkInsertAll<T>(T[] entities) where T : class
    {
        var conn = (SqlConnection)Database.Connection;

        conn.Open();

        Type t = typeof(T);
        Set(t).ToString();
        var objectContext = ((IObjectContextAdapter)this).ObjectContext;
        var workspace = objectContext.MetadataWorkspace;
        var mappings = GetMappings(workspace, objectContext.DefaultContainerName, typeof(T).Name);

        var tableName = GetTableName<T>();
        var bulkCopy = new SqlBulkCopy(conn) { DestinationTableName = tableName };

        // Foreign key relations show up as virtual declared 
        // properties and we want to ignore these.
        var properties = t.GetProperties().Where(p => !p.GetGetMethod().IsVirtual).ToArray();
        var table = new DataTable();
        foreach (var property in properties)
        {
            Type propertyType = property.PropertyType;

            // Nullable properties need special treatment.
            if (propertyType.IsGenericType &&
                propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                propertyType = Nullable.GetUnderlyingType(propertyType);
            }

            // Since we cannot trust the CLR type properties to be in the same order as
            // the table columns we use the SqlBulkCopy column mappings.
            table.Columns.Add(new DataColumn(property.Name, propertyType));
            var clrPropertyName = property.Name;
            var tableColumnName = mappings[property.Name];
            bulkCopy.ColumnMappings.Add(new SqlBulkCopyColumnMapping(clrPropertyName, tableColumnName));
        }

        // Add all our entities to our data table
        foreach (var entity in entities)
        {
            var e = entity;
            table.Rows.Add(properties.Select(property => GetPropertyValue(property.GetValue(e, null))).ToArray());
        }

        // send it to the server for bulk execution
        bulkCopy.BulkCopyTimeout = 5 * 60;
        bulkCopy.WriteToServer(table);

        conn.Close();
    }

    private string GetTableName<T>() where T : class
    {
        var dbSet = Set<T>();
        var sql = dbSet.ToString();
        var regex = new Regex(@"FROM (?<table>.*) AS");
        var match = regex.Match(sql);
        return match.Groups["table"].Value;
    }

    private object GetPropertyValue(object o)
    {
        if (o == null)
            return DBNull.Value;
        return o;
    }

    private Dictionary<string, string> GetMappings(MetadataWorkspace workspace, string containerName, string entityName)
    {
        var mappings = new Dictionary<string, string>();
        var storageMapping = workspace.GetItem<GlobalItem>(containerName, DataSpace.CSSpace);
        dynamic entitySetMaps = storageMapping.GetType().InvokeMember(
            "EntitySetMaps",
            BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Instance,
            null, storageMapping, null);

        foreach (var entitySetMap in entitySetMaps)
        {
            var typeMappings = GetArrayList("TypeMappings", entitySetMap);
            dynamic typeMapping = typeMappings[0];
            dynamic types = GetArrayList("Types", typeMapping);

            if (types[0].Name == entityName)
            {
                var fragments = GetArrayList("MappingFragments", typeMapping);
                var fragment = fragments[0];
                var properties = GetArrayList("AllProperties", fragment);
                foreach (var property in properties)
                {
                    var edmProperty = GetProperty("EdmProperty", property);
                    var columnProperty = GetProperty("ColumnProperty", property);
                    mappings.Add(edmProperty.Name, columnProperty.Name);
                }
            }
        }

        return mappings;
    }

    private ArrayList GetArrayList(string property, object instance)
    {
        var type = instance.GetType();
        var objects = (IEnumerable)type.InvokeMember(property, BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Instance, null, instance, null);
        var list = new ArrayList();
        foreach (var o in objects)
        {
            list.Add(o);
        }
        return list;
    }

    private dynamic GetProperty(string property, object instance)
    {
        var type = instance.GetType();
        return type.InvokeMember(property, BindingFlags.GetProperty | BindingFlags.NonPublic | BindingFlags.Instance, null, instance, null);
    }
}

此版本适用于 EF 6

public partial class CMLocalEntities
{
    public void BulkInsertAll<T>(T[] entities) where T : class
    {
        var conn = (SqlConnection)Database.Connection;

        conn.Open();

        Type t = typeof(T);
        Set(t).ToString();
        var objectContext = ((IObjectContextAdapter)this).ObjectContext;
        var workspace = objectContext.MetadataWorkspace;
        var mappings = GetMappings(workspace, objectContext.DefaultContainerName, typeof(T).Name);

        var tableName = GetTableName<T>();
        var bulkCopy = new SqlBulkCopy(conn) { DestinationTableName = tableName };

        // Foreign key relations show up as virtual declared 
        // properties and we want to ignore these.
        var properties = t.GetProperties().Where(p => !p.GetGetMethod().IsVirtual).ToArray();
        var table = new DataTable();
        foreach (var property in properties)
        {
            Type propertyType = property.PropertyType;

            // Nullable properties need special treatment.
            if (propertyType.IsGenericType &&
                propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                propertyType = Nullable.GetUnderlyingType(propertyType);
            }

            // Since we cannot trust the CLR type properties to be in the same order as
            // the table columns we use the SqlBulkCopy column mappings.
            table.Columns.Add(new DataColumn(property.Name, propertyType));
            var clrPropertyName = property.Name;
            var tableColumnName = mappings[property.Name];
            bulkCopy.ColumnMappings.Add(new SqlBulkCopyColumnMapping(clrPropertyName, tableColumnName));
        }

        // Add all our entities to our data table
        foreach (var entity in entities)
        {
            var e = entity;
            table.Rows.Add(properties.Select(property => GetPropertyValue(property.GetValue(e, null))).ToArray());
        }

        // send it to the server for bulk execution
        bulkCopy.BulkCopyTimeout = 5*60;
        bulkCopy.WriteToServer(table);

        conn.Close();
    }

    private string GetTableName<T>() where T : class
    {
        var dbSet = Set<T>();
        var sql = dbSet.ToString();
        var regex = new Regex(@"FROM (?<table>.*) AS");
        var match = regex.Match(sql);
        return match.Groups["table"].Value;
    }

    private object GetPropertyValue(object o)
    {
        if (o == null)
            return DBNull.Value;
        return o;
    }

    private Dictionary<string, string> GetMappings(MetadataWorkspace workspace, string containerName, string entityName)
    {
        var mappings = new Dictionary<string, string>();
        var storageMapping = workspace.GetItem<GlobalItem>(containerName, DataSpace.CSSpace);
        dynamic entitySetMaps = storageMapping.GetType().InvokeMember(
            "EntitySetMaps",
            BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance,
            null, storageMapping, null);

        foreach (var entitySetMap in entitySetMaps)
        {
            var typeMappings = GetArrayList("EntityTypeMappings", entitySetMap);
            dynamic typeMapping = typeMappings[0];
            dynamic types = GetArrayList("Types", typeMapping);

            if (types[0].Name == entityName)
            {
                var fragments = GetArrayList("MappingFragments", typeMapping);
                var fragment = fragments[0];
                var properties = GetArrayList("AllProperties", fragment);
                foreach (var property in properties)
                {
                    var edmProperty = GetProperty("EdmProperty", property);
                    var columnProperty = GetProperty("ColumnProperty", property);
                    mappings.Add(edmProperty.Name, columnProperty.Name);
                }
            }
        }

        return mappings;
    }

    private ArrayList GetArrayList(string property, object instance)
    {
        var type = instance.GetType();
        var objects = (IEnumerable)type.InvokeMember(
            property, 
            BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance, null, instance, null);
        var list = new ArrayList();
        foreach (var o in objects)
        {
            list.Add(o);
        }
        return list;
    }

    private dynamic GetProperty(string property, object instance)
    {
        var type = instance.GetType();
        return type.InvokeMember(property, BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance, null, instance, null);
    }

}

最后,为您的 Linq-To-Sql 爱好者准备了一些小东西。

partial class MyDataContext
{
    partial void OnCreated()
    {
        CommandTimeout = 5 * 60;
    }

    public void BulkInsertAll<T>(IEnumerable<T> entities)
    {
        entities = entities.ToArray();

        string cs = Connection.ConnectionString;
        var conn = new SqlConnection(cs);
        conn.Open();

        Type t = typeof(T);

        var tableAttribute = (TableAttribute)t.GetCustomAttributes(
            typeof(TableAttribute), false).Single();
        var bulkCopy = new SqlBulkCopy(conn) { 
            DestinationTableName = tableAttribute.Name };

        var properties = t.GetProperties().Where(EventTypeFilter).ToArray();
        var table = new DataTable();

        foreach (var property in properties)
        {
            Type propertyType = property.PropertyType;
            if (propertyType.IsGenericType &&
                propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                propertyType = Nullable.GetUnderlyingType(propertyType);
            }

            table.Columns.Add(new DataColumn(property.Name, propertyType));
        }

        foreach (var entity in entities)
        {
            table.Rows.Add(properties.Select(
              property => GetPropertyValue(
              property.GetValue(entity, null))).ToArray());
        }

        bulkCopy.WriteToServer(table);
        conn.Close();
    }

    private bool EventTypeFilter(System.Reflection.PropertyInfo p)
    {
        var attribute = Attribute.GetCustomAttribute(p, 
            typeof (AssociationAttribute)) as AssociationAttribute;

        if (attribute == null) return true;
        if (attribute.IsForeignKey == false) return true; 

        return false;
    }

    private object GetPropertyValue(object o)
    {
        if (o == null)
            return DBNull.Value;
        return o;
    }
}

【讨论】:

  • 任何人都知道为什么当我尝试这个时我得到一个错误引用 EntitySetMaps:"Method 'System.Data.Entity.Core.Mapping.EntityContainerMapping.EntitySetMaps' not found."
  • 您使用的是哪个版本的 EF?
  • 啊哎呀,你的代码的 ef 6 版本和根据 nuget 的 6.1.1。我首先使用代码。 'slow' 方法可以正常工作。
  • 他们可能在 6.1.1 中更改了一些元数据属性名称。我会检查一下。
  • @MånsTånneryd 谢谢!我使用 EF 6.1.3,确实属性名称已更改。所以我将 GetMappings() 更改为:EntitySetMaps to EntitySetMappings ;Types to EntityTypes; ;MappingFragments 到 Fragments ;AllProperties 到 PropertyMappings ;EdmProperty 到 Property ;ColumnProperty 到 Column
【解决方案4】:

也许这里的answer 会对您有所帮助。似乎您想定期处理上下文。这是因为上下文随着附加实体的增长而变得越来越大。

【讨论】:

    【解决方案5】:

    您的代码存在两个主要的性能问题:

    • 使用添加方法
    • 使用 SaveChanges

    使用添加方法

    Add 方法只会在您添加的每个实体上变得越来越慢。

    见:http://entityframework.net/improve-ef-add-performance

    例如,通过以下方式添加 10,000 个实体:

    • 添加(大约需要 105,000 毫秒)
    • AddRange(大约需要 120 毫秒)

    注意:实体尚未保存在数据库中!

    问题在于 Add 方法会尝试在添加的每个实体上检测更改,而 AddRange 在所有实体都添加到上下文后执行一次。

    常见的解决方案有:

    • 使用 AddRange 而不是 Add
    • 将 AutoDetectChanges 设置为 false
    • 多批次拆分 SaveChanges

    使用 SaveChanges

    尚未为批量操作创建实体框架。对于您保存的每个实体,都会执行一次数据库往返。

    因此,如果您要插入 20,000 条记录,您将执行 20,000 次数据库往返,这是 INSANE

    有一些支持批量插入的第三方库可用:

    • Z.EntityFramework.Extensions(推荐
    • EFutilities
    • EntityFramework.BulkInsert

    见:Entity Framework Bulk Insert library

    在选择批量插入库时要小心。只有 Entity Framework Extensions 支持所有类型的关联和继承,并且它是唯一仍然支持的。


    免责声明:我是Entity Framework Extensions的所有者

    此库允许您执行场景所需的所有批量操作:

    • 批量保存更改
    • 批量插入
    • 批量删除
    • 批量更新
    • 批量合并

    例子

    // Easy to use
    context.BulkSaveChanges();
    
    // Easy to customize
    context.BulkSaveChanges(bulk => bulk.BatchSize = 100);
    
    // Perform Bulk Operations
    context.BulkDelete(customers);
    context.BulkInsert(customers);
    context.BulkUpdate(customers);
    
    // Customize Primary Key
    context.BulkMerge(customers, operation => {
       operation.ColumnPrimaryKeyExpression = 
            customer => customer.Code;
    });
    

    编辑:在评论中回答问题

    对于您创建的库,是否有建议的每个批量插入的最大大小

    不要太高,不要太低。没有适合所有场景的特定值,因为它取决于多个因素,例如行大小、索引、触发器等。

    一般建议在4000左右。

    还有一种方法可以将所有这些都绑定在一个事务中,而不必担心它会超时

    您可以使用实体框架事务。如果一个事务被启动,我们的图书馆就会使用该事务。但是要小心,一个花费太多时间的事务也会带来一些问题,比如一些行/索引/表锁定。

    【讨论】:

    • 对您创建的库的每个批量插入是否有建议的最大大小?还有一种方法可以将所有这些都绑定在一个事务中而不用担心它会超时吗?谢谢!
    【解决方案6】:

    目前没有更好的方法,但是通过将 SaveChanges 移动到 for 循环中可能有 10 个项目可能会有边际改进。

    int i = 0;
    
    foreach (Employees item in sequence)
    {
       t = new Employees ();
       t.Text = item.Text;
       dataContext.Employees.AddObject(t);   
    
       // this will add max 10 items together
       if((i % 10) == 0){
           dataContext.SaveChanges();
           // show some progress to user based on
           // value of i
       }
       i++;
    }
    dataContext.SaveChanges();
    

    您可以将 10 调整为更接近更好的性能。它不会大大提高速度,但可以让您向用户显示一些进度并使其更加用户友好。

    【讨论】:

      【解决方案7】:

      在具有 1 个实例的基本网站的 Azure 环境中。我尝试使用 for 循环在 25000 条记录中一次插入 1000 条记录的批次,这需要 11.5 分钟,但在并行执行中只用了不到一分钟。所以我推荐使用 TPL(Task Parallel Library)。

               var count = (you collection / 1000) + 1;
               Parallel.For(0, count, x =>
              {
                  ApplicationDbContext db1 = new ApplicationDbContext();
                  db1.Configuration.AutoDetectChangesEnabled = false;
      
                  var records = members.Skip(x * 1000).Take(1000).ToList();
                  db1.Members.AddRange(records).AsParallel();
      
                  db1.SaveChanges();
                  db1.Dispose();
              });
      

      【讨论】:

      • 让我澄清一下这段代码:第 1 行:var count = (your collections.Count / 1000) + 1; 第 7 行:members 是您的收藏。当我为我的案例运行此代码时,我得到了这个错误 事务(进程 ID 80)在锁定资源上与另一个进程死锁,并已被选为死锁牺牲品。重新运行事务。
      • 对于可能发生的异常情况我宁愿把dbContext的创建和处理放到using块中
      【解决方案8】:

      更好的方法是完全跳过实体框架进行此操作并依赖 SqlBulkCopy 类。其他操作可以像以前一样继续使用 EF。

      这增加了解决方案的维护成本,但与使用 EF 相比,无论如何有助于将大型对象集合插入数据库所需的时间减少一到两个数量级。

      这里有一篇文章比较了 SqlBulkCopy 类和 EF 对于具有父子关系的对象(也描述了实现批量插入所需的设计更改):How to Bulk Insert Complex Objects into SQL Server Database

      【讨论】:

      • 外键问题或唯一键冲突会怎样?整个操作回滚了吗?
      • 考虑批量插入业务事务,而不是系统事务。您的问题应交由企业主决定。我在实践中看到了不同的选择,对我们程序员来说都一样好:(1)全部回滚并让用户更正数据; (2) 提交到该点并通知用户其余的未处理,(3) 跳过并继续,然后通知用户失败的记录。解决方案 2 和 3 需要一些例外情况,并且通常难以实施。
      【解决方案9】:

      尝试使用批量插入....

      http://code.msdn.microsoft.com/LinqEntityDataReader

      如果您有实体集合,例如 storeEntities,您可以使用 SqlBulkCopy 存储它们,如下所示

              var bulkCopy = new SqlBulkCopy(connection);
              bulkCopy.DestinationTableName = TableName;
              var dataReader = storeEntities.AsDataReader();
              bulkCopy.WriteToServer(dataReader);
      

      此代码有一个问题。确保实体的实体框架定义与表定义完全相关,确保实体的属性在实体模型中的顺序与 SQL Server 表中的列的顺序相同。不这样做将导致异常。

      【讨论】:

        【解决方案10】:

        虽然回复晚了,但我发布答案是因为我遭受了同样的痛苦。 我为此创建了一个新的 GitHub 项目,截至目前,它支持使用 SqlBulkCopy 透明地为 Sql 服务器批量插入/更新/删除。

        https://github.com/MHanafy/EntityExtensions

        还有其他好东西,希望它会被扩展以做更多的事情。

        使用起来很简单

        var insertsAndupdates = new List<object>();
        var deletes = new List<object>();
        context.BulkUpdate(insertsAndupdates, deletes);
        

        希望对你有帮助!

        【讨论】:

          【解决方案11】:
           Use : db.Set<tale>.AddRange(list); 
          Ref :
          TESTEntities db = new TESTEntities();
          List<Person> persons = new List<Person> { 
          new  Person{Name="p1",Place="palce"},
          new  Person{Name="p2",Place="palce"},
          new  Person{Name="p3",Place="palce"},
          new  Person{Name="p4",Place="palce"},
          new  Person{Name="p5",Place="palce"}
          };
          db.Set<Person>().AddRange(persons);
          db.SaveChanges();
          

          【讨论】:

          • 请添加描述
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-07-23
          • 1970-01-01
          相关资源
          最近更新 更多