developer tip

엔터티 프레임 워크에서 대량 삽입 성능 향상

optionbox 2020. 8. 3. 08:31
반응형

엔터티 프레임 워크에서 대량 삽입 성능 향상


이 질문에는 이미 답변이 있습니다.

엔티티 프레임 워크별로 테이블에 20000 레코드를 삽입하고 약 2 분이 걸립니다. SP를 사용하여 성능을 향상시키는 것 외에 다른 방법이 있습니까? 이것은 내 코드입니다.

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

여러 가지 개선 기회가 있습니다 (를 사용하는 경우 DbContext).

세트:

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

SaveChanges()100 개의 인서트 패키지로 작업 하십시오. 또는 1000 개 항목의 패키지로 시도하여 성능의 변화를 볼 수 있습니다.

이 모든 삽입 중에 컨텍스트가 동일하고 커지고 있으므로 1000 개의 삽입마다 컨텍스트 오브젝트를 다시 빌드 할 수 있습니다 . var yourContext = new YourContext();이것이 큰 이익이라고 생각합니다.

데이터 가져 오기 프로세스에서이 개선 작업을 수행하는 데 7 분에서 6 초가 걸렸습니다.

실제 숫자는 귀하의 경우 100 또는 1000이 될 수 없습니다 ... 시도하고 조정하십시오.


이런 식으로 EF가 성능을 향상 시키도록 강제 할 수는 없습니다. 문제는 EF가 데이터베이스에 대한 개별 왕복으로 각 삽입을 실행한다는 것입니다. 굉장하지 않습니까? DataSet조차도 일괄 처리를 지원했습니다. 해결 방법은 이 기사확인하십시오 . 다른 해결 방법은 테이블 값 매개 변수를 허용하는 사용자 지정 저장 프로 시저를 사용할 수 있지만이를 위해서는 원시 ADO.NET이 필요합니다.


아래 코드를 사용하면 엔터티 개체의 컬렉션을 가져 와서 데이터베이스에 대량 복사하는 메서드로 부분 컨텍스트 클래스를 확장 할 수 있습니다. MyEntities의 클래스 이름을 엔티티 클래스의 이름으로 바꾸고 올바른 네임 스페이스에서 프로젝트에 추가하기 만하면됩니다. 그런 다음 삽입하려는 엔티티 객체를 넘겨주는 BulkInsertAll 메소드를 호출하면됩니다. 컨텍스트 클래스를 재사용하지 말고 사용할 때마다 새 인스턴스를 작성하십시오. 여기에 사용 된 SQLConnection과 연관된 인증 데이터는 클래스를 한 번 사용한 후 유실되므로, 일부 버전의 EF에서는 이것이 필요합니다. 이유를 모르겠습니다.

이 버전은 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;
    }
}

어쩌면이 답변 이 도움이 될 것입니다. 주기적으로 컨텍스트를 삭제하려는 것 같습니다. 연결된 엔티티가 커질수록 컨텍스트가 점점 커지기 때문입니다.


현재 더 좋은 방법은 없지만 10 개 항목에 대해 for 루프 내에서 SaveChanges를 이동하면 한계가 개선 될 수 있습니다.

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을 조정할 수 있습니다. 속도를 크게 향상 시키지는 않지만 사용자에게 진행 상황을 표시하고보다 사용자 친화적으로 만들 수 있습니다.


1 인스턴스가있는 기본 웹 사이트가있는 Azure 환경에서 for 루프를 사용하여 25000 레코드 중 한 번에 1000 레코드의 배치를 삽입하려고 시도했지만 11.5 분이 걸렸지 만 병렬 실행에서는 1 분 미만이 걸렸습니다. (작업 병렬 라이브러리).

         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();
        });

더 좋은 방법은이 작업에서 Entity Framework를 완전히 건너 뛰고 SqlBulkCopy 클래스에 의존하는 것입니다. 다른 작업은 이전과 마찬가지로 EF를 계속 사용할 수 있습니다.

따라서 솔루션의 유지 관리 비용이 증가하지만 어쨌든 EF를 사용할 때보 다 대량의 개체 모음을 데이터베이스에 1-2 배 정도 삽입하는 데 필요한 시간을 줄일 수 있습니다.

다음은 부모-자식 관계가있는 개체의 SqlBulkCopy 클래스와 EF를 비교하는 문서입니다 (대량 삽입을 구현하는 데 필요한 디자인 변경 사항도 설명 함). 복잡한 개체를 SQL Server 데이터베이스에 대량 삽입하는 방법


코드에는 두 가지 주요 성능 문제가 있습니다.

  • 추가 방법 사용
  • 저장 변경 사용

추가 방법 사용

Add 메소드는 추가하는 각 엔티티에서 느리고 느려집니다.

참조 : http://entityframework.net/improve-ef-add-performance

예를 들어 다음을 통해 10,000 개의 엔터티를 추가합니다.

  • 추가 (~ 105,000ms 소요)
  • AddRange (~ 120ms 사용)

참고 : 엔티티는 아직 데이터베이스에 저장되지 않았습니다!

문제는 Add 메소드가 추가 된 모든 엔티티에서 DetectChanges를 감지하려고하지만 모든 엔티티가 컨텍스트에 추가 된 후에 AddRange가 한 번 수행한다는 것입니다.

일반적인 해결책은 다음과 같습니다.

  • 추가보다 AddRange 사용
  • SET AutoDetect false로 변경
  • 여러 배치로 SPLIT SaveChanges

저장 변경 사용

대량 작업을위한 Entity Framework가 만들어지지 않았습니다. 저장하는 모든 엔터티에 대해 데이터베이스 왕복이 수행됩니다.

따라서 20,000 개의 레코드를 삽입하려는 경우 INSANE 인 20,000 개의 데이터베이스 왕복을 수행합니다 .

대량 삽입을 지원하는 타사 라이브러리가 있습니다.

  • Z.EntityFramework.Extensions ( 권장 )
  • 유틸리티
  • EntityFramework.BulkInsert

참조 : Entity Framework 대량 삽입 라이브러리

대량 삽입 라이브러리를 선택할 때주의하십시오. 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 정도 인 것이 좋습니다.

또한 한 번의 트랜잭션으로 모든 것을 묶고 시간 초과에 대해 걱정하지 않는 방법이 있습니까?

Entity Framework 트랜잭션을 사용할 수 있습니다. 우리 도서관은 거래가 시작되면 거래를 사용합니다. 그러나 너무 많은 시간이 걸리는 트랜잭션에는 행 / 인덱스 / 테이블 잠금과 같은 문제가 있습니다.


대량 삽입 ...을 사용해보십시오.

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 테이블의 열과 동일한 순서인지 확인하십시오. 그렇지 않으면 예외가 발생합니다.


답변이 늦었지만 같은 고통을 겪었 기 때문에 답변을 게시하고 있습니다. 이를 위해 새 GitHub 프로젝트를 만들었습니다. 현재 SqlBulkCopy를 사용하여 Sql 서버에 대한 대량 삽입 / 업데이트 / 삭제를 투명하게 지원합니다.

https://github.com/MHanafy/EntityExtensions

다른 장점들도 있지만, 더 많은 노력을 기울일 수 있기를 바랍니다.

그것을 사용하는 것은 간단합니다

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

그것이 도움이되기를 바랍니다!


 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();

참고 URL : https://stackoverflow.com/questions/6107206/improving-bulk-insert-performance-in-entity-framework

반응형