c# 예제 - Entity Framework에서 가장 빠른 삽입 방법




sql entity-framework (21)

SaveChanges ()를 수행 할 때 insert 문이 하나씩 데이터베이스에 전송되므로 Entity가 작동하기 때문에 여기에 작성된 모든 솔루션은 도움이되지 않습니다.

그리고 예를 들어 데이터베이스와 백으로의 여행이 50ms라면 삽입에 필요한 시간은 레코드 수 x 50ms입니다.

BulkInsert를 사용해야합니다. 링크는 다음과 같습니다. https://efbulkinsert.codeplex.com/

삽입 시간을 5-6 분에서 10-12 초로 줄였습니다.

Entity Framework에 삽입하는 가장 빠른 방법을 찾고 있습니다.

나는 당신이 활성 TransactionScope가 있고 삽입이 거대한 (4000+) 시나리오 때문에 이것을 요구하고있다. 잠재적으로 10 분 이상 지속될 수 있으며 (트랜잭션의 기본 시간 초과) 이로 인해 불완전한 트랜잭션이 발생합니다.


배경 작업자 또는 작업을 통해 삽입하려고 시도한 적이 있습니까?

필자의 경우, 7760 개의 레지스터를 삽입하고 182 개의 다른 테이블에 외래 키 관계 (NavigationProperties에 의해)로 배포했다.

그 일이 없으면 2 분 30 초가 걸렸습니다. Task ( Task.Factory.StartNew(...)) 내에서 15 초가 걸렸습니다.

SaveChanges()모든 엔티티를 컨텍스트에 추가 한 후에 만 수행합니다 . (데이터 무결성 보장)


또 다른 옵션은 Nuget에서 제공하는 SqlBulkTools를 사용하는 것입니다. 사용하기가 쉽고 강력한 기능이 있습니다.

예:

var bulk = new BulkOperations();
var books = GetBooks();

using (TransactionScope trans = new TransactionScope())
{
    using (SqlConnection conn = new SqlConnection(ConfigurationManager
    .ConnectionStrings["SqlBulkToolsTest"].ConnectionString))
    {
        bulk.Setup<Book>()
            .ForCollection(books)
            .WithTable("Books") 
            .AddAllColumns()
            .BulkInsert()
            .Commit(conn);
    }

    trans.Complete();
}

더 많은 예제와 고급 사용법 은 설명서 를 참조하십시오. 면책 조항 : 본인은이 도서관의 저자이며 모든 의견은 제 자신의 의견입니다.


이 조합은 속도를 충분히 높입니다.

context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;

EF를 사용하여 일괄 삽입하는 방법에 대한이 기사를 추천합니다.

Entity Framework 및 느린 대량 INSERT

그는이 영역을 탐구하고 성능을 비교합니다.

  1. 기본 EF (30,000 레코드 추가를 완료하는 데 57 분)
  2. ADO.NET 코드로 대체 (동일한 30,000의 경우 25 )
  3. 컨텍스트 블로 트 (Context Bloat) - 각 작업 단위 (Unit of Work)에 대해 새로운 컨텍스트를 사용하여 활성 컨텍스트 그래프를 작게 유지합니다 (동일한 30,000 개의 삽입 작업에는 33 초 소요)
  4. 대용량 목록 - AutoDetectChangesEnabled를 해제합니다 (시간을 약 20 초까지 줄입니다).
  5. 일괄 처리 (최대 16 초)
  6. DbTable.AddRange () - (성능이 12 범위 내에 있음)

내 지식에 따라 EntityFramework 에는 거대한 인서트의 성능을 높이기위한 no BulkInsertno BulkInsert .

이 시나리오에서는 문제를 해결하기 위해 ADO.net documentation 를 사용할 수 있습니다


SqlBulkCopy 사용 :

void BulkInsert(GpsReceiverTrack[] gpsReceiverTracks)
{
    if (gpsReceiverTracks == null)
    {
        throw new ArgumentNullException(nameof(gpsReceiverTracks));
    }

    DataTable dataTable = new DataTable("GpsReceiverTracks");
    dataTable.Columns.Add("ID", typeof(int));
    dataTable.Columns.Add("DownloadedTrackID", typeof(int));
    dataTable.Columns.Add("Time", typeof(TimeSpan));
    dataTable.Columns.Add("Latitude", typeof(double));
    dataTable.Columns.Add("Longitude", typeof(double));
    dataTable.Columns.Add("Altitude", typeof(double));

    for (int i = 0; i < gpsReceiverTracks.Length; i++)
    {
        dataTable.Rows.Add
        (
            new object[]
            {
                    gpsReceiverTracks[i].ID,
                    gpsReceiverTracks[i].DownloadedTrackID,
                    gpsReceiverTracks[i].Time,
                    gpsReceiverTracks[i].Latitude,
                    gpsReceiverTracks[i].Longitude,
                    gpsReceiverTracks[i].Altitude
            }
        );
    }

    string connectionString = (new TeamTrackerEntities()).Database.Connection.ConnectionString;
    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        using (var transaction = connection.BeginTransaction())
        {
            using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
            {
                sqlBulkCopy.DestinationTableName = dataTable.TableName;
                foreach (DataColumn column in dataTable.Columns)
                {
                    sqlBulkCopy.ColumnMappings.Add(column.ColumnName, column.ColumnName);
                }

                sqlBulkCopy.WriteToServer(dataTable);
            }
            transaction.Commit();
        }
    }

    return;
}

귀하의 의견에 대한 귀하의 의견에 :

"... 각 레코드에 대한 SavingChanges ..."

그것은 당신이 할 수있는 최악의 일입니다! 각 레코드에 대해 SaveChanges() 를 호출하면 대량 삽입이 매우 느려집니다. 성능을 향상시킬 수있는 몇 가지 간단한 테스트를 수행합니다.

  • 모든 레코드 뒤에 SaveChanges() 한 번 호출하십시오.
  • 예를 들어 100 개의 레코드 뒤에 SaveChanges() 호출하십시오.
  • 예를 들어 100 개의 레코드 뒤에 SaveChanges() 호출하고 컨텍스트를 삭제하고 새 컨텍스트를 만듭니다.
  • 변경 감지 비활성화

대량 삽입물의 경우 다음과 같은 패턴으로 작업하고 실험하고 있습니다.

using (TransactionScope scope = new TransactionScope())
{
    MyDbContext context = null;
    try
    {
        context = new MyDbContext();
        context.Configuration.AutoDetectChangesEnabled = false;

        int count = 0;            
        foreach (var entityToInsert in someCollectionOfEntitiesToInsert)
        {
            ++count;
            context = AddToContext(context, entityToInsert, count, 100, true);
        }

        context.SaveChanges();
    }
    finally
    {
        if (context != null)
            context.Dispose();
    }

    scope.Complete();
}

private MyDbContext AddToContext(MyDbContext context,
    Entity entity, int count, int commitCount, bool recreateContext)
{
    context.Set<Entity>().Add(entity);

    if (count % commitCount == 0)
    {
        context.SaveChanges();
        if (recreateContext)
        {
            context.Dispose();
            context = new MyDbContext();
            context.Configuration.AutoDetectChangesEnabled = false;
        }
    }

    return context;
}

560.000 엔티티 (9 스칼라 속성, 탐색 속성 없음)를 DB에 삽입하는 테스트 프로그램이 있습니다. 이 코드로 3 분 안에 작동합니다.

성능을 위해 "많은"레코드 ( "100"또는 "1000"주위의 "많은" SaveChanges() 다음에 SaveChanges() 를 호출하는 것이 중요합니다. 또한 SaveChanges 후에 컨텍스트를 처리하고 새 컨텍스트를 작성하는 성능을 향상시킵니다. 이렇게하면 모든 엔티티의 컨텍스트가 지워지고 SaveChanges 는이를 수행하지 않고 엔티티는 상태가 Unchanged 컨텍스트에 계속 첨부됩니다. 문맥에서 첨부 된 엔티티의 크기가 커지면서 삽입 단계가 단계적으로 느려집니다. 따라서 시간이 지나면 삭제하는 것이 좋습니다.

내 560.000 엔티티에 대한 몇 가지 측정 값은 다음과 같습니다.

  • commitCount = 1, recreateContext = false : 많은 시간 (이것이 현재 절차입니다.)
  • commitCount = 100, recreateContext = false : 20 분 초과
  • commitCount = 1000, recreateContext = false : 242 초
  • commitCount = 10000, recreateContext = false : 202 초
  • commitCount = 100000, recreateContext = false : 199 초
  • commitCount = 1000000, recreateContext = false : 메모리 부족 예외
  • commitCount = 1, recreateContext = true : 10 분 이상
  • commitCount = 10, recreateContext = true : 241 초
  • commitCount = 100, recreateContext = true : 164 초
  • commitCount = 1000, recreateContext = true : 191 초

위의 첫 번째 테스트의 동작은 성능이 매우 비선형 적이며 시간이 지나면 극도로 감소한다는 것입니다. ( "많은 시간"은 추정치이며, 나는이 테스트를 끝내지 못했고, 20 분 후에 50,000 개의 엔티티에서 멈췄습니다.)이 비선형 행동은 다른 모든 테스트에서 그리 중요하지 않습니다.


다음은 Entity Framework를 사용하고 실제 예제에서 SqlBulkCopy 클래스를 사용하는 것의 성능 비교입니다. 복잡한 개체를 SQL Server 데이터베이스에 대량 삽입하는 방법

다른 사람들이 이미 강조했듯이 ORM은 대량 작업에 사용하기위한 것이 아닙니다. 유연성, 관심사 및 기타 이점을 제공하지만 일괄 작업 (대량 읽기 제외)은 그 중 하나가 아닙니다.


위의 예에서 @Slauma의 일반적인 확장을 만들었습니다.

public static class DataExtensions
{
    public static DbContext AddToContext<T>(this DbContext context, object entity, int count, int commitCount, bool recreateContext, Func<DbContext> contextCreator)
    {
        context.Set(typeof(T)).Add((T)entity);

        if (count % commitCount == 0)
        {
            context.SaveChanges();
            if (recreateContext)
            {
                context.Dispose();
                context = contextCreator.Invoke();
                context.Configuration.AutoDetectChangesEnabled = false;
            }
        }
        return context;
    }
}

용법:

public void AddEntities(List<YourEntity> entities)
{
    using (var transactionScope = new TransactionScope())
    {
        DbContext context = new YourContext();
        int count = 0;
        foreach (var entity in entities)
        {
            ++count;
            context = context.AddToContext<TenancyNote>(entity, count, 100, true,
                () => new YourContext());
        }
        context.SaveChanges();
        transactionScope.Complete();
    }
}

비밀은 동일한 빈 스테이징 테이블에 삽입하는 것입니다. 삽입물이 빠르게 번지고 있습니다. 그런 다음 기본 큰 테이블에 단일 삽입을 실행하십시오. 그런 다음 스테이징 테이블을 절단하여 다음 배치를 준비하십시오.

즉.

insert into some_staging_table using Entity Framework.

-- Single insert into main table (this could be a tiny stored proc call)
insert into some_main_already_large_table (columns...)
   select (columns...) from some_staging_table
truncate table some_staging_table

다른 사람들은 SqlBulkCopy가 당신이 정말로 좋은 삽입 성능을 원한다면 그것을 할 수 있다고 말합니다.

구현하는 데 다소 번거롭지만 라이브러리를 통해 도움을받을 수 있습니다. 거기에 몇 가지가 있지만 나는이 시간에 내 자신의 라이브러리를 뻔뻔스럽게 연결합니다 : https://github.com/MikaelEliasson/EntityFramework.Utilities#batch-insert-entities

필요한 코드는 다음과 같습니다.

 using (var db = new YourDbContext())
 {
     EFBatchOperation.For(db, db.BlogPosts).InsertAll(list);
 }

그럼 얼마나 빠릅니까? 너무 많은 요인, 컴퓨터 성능, 네트워크, 객체 크기 등에 따라 달라지기 때문에 말하기가 매우 어렵습니다. 내가 만든 성능 테스트는 25k 엔티티가 로컬 호스트에 표준 방식 으로 약 10 초에 삽입 될 수 있다고 제안합니다. 다른 답변에서 언급했다. EFUtilities에는 약 300ms가 걸립니다. 더욱 흥미로운 점은이 방법을 사용하여 15 초 이내에 약 3 백만 개의 엔티티를 저장하고 초당 약 200,000 개의 엔티티를 평균화 한 것입니다.

한 가지 문제는 관련 데이터를 삽입해야하는 경우입니다. 위의 방법을 사용하여 SQL 서버에 효율적으로 수행 할 수 있지만 외부 키를 설정할 수 있도록 상위의 app-code에 ID를 생성 할 수있는 ID 생성 전략이 필요합니다. 이것은 GUID 또는 HiLo id 생성과 같은 것을 사용하여 수행 할 수 있습니다.


대량 패키지 라이브러리를 사용할 수 있습니다 . Bulk Insert 1.0.0 버전은 Entity framework> = 6.0.0 인 프로젝트에서 사용됩니다.

자세한 설명은 여기에서 찾을 수 있습니다. Bulkoperation 소스 코드


이를 위해 System.Data.SqlClient.SqlBulkCopy 를 사용해야합니다. 여기에 설명서가 있습니다. 물론 많은 온라인 자습서가 있습니다.

죄송합니다. 원하는 답변을 EF에 제공하는 간단한 답을 찾고 계시 겠지만 대량 작업은 ORM의 의미가 아닙니다.


가장 빠른 방법은 내가 개발 한 대량 삽입 확장 을 사용하는 것입니다.

SqlBulkCopy 및 사용자 지정 데이터 배열을 사용하여 최대 성능을 얻습니다. 결과적으로 일반 삽입 또는 AddRange를 사용하는 것보다 20 배 이상 빠릅니다.

사용법은 매우 간단하다.

context.BulkInsert(hugeAmountOfEntities);

하지만, (+4000) 개 이상의 삽입물의 경우 저장 프로 시저를 사용하는 것이 좋습니다. 경과 시간이 첨부되었습니다. 나는 20 인치의 11.788 행을 삽입했다.

그게 코드 야.

 public void InsertDataBase(MyEntity entity)
    {
        repository.Database.ExecuteSqlCommand("sp_mystored " +
                "@param1, @param2"
                 new SqlParameter("@param1", entity.property1),
                 new SqlParameter("@param2", entity.property2));
    }

나는 Adam Rackis와 동의한다. SqlBulkCopy 는 한 데이터 원본에서 다른 데이터 원본으로 대량 레코드를 전송하는 가장 빠른 방법입니다. 이것을 사용하여 20K 개의 레코드를 복사했으며 3 초도 채 걸리지 않았습니다. 아래 예를 살펴보십시오.

public static void InsertIntoMembers(DataTable dataTable)
{           
    using (var connection = new SqlConnection(@"data source=;persist security info=True;user id=;password=;initial catalog=;MultipleActiveResultSets=True;App=EntityFramework"))
    {
        SqlTransaction transaction = null;
        connection.Open();
        try
        {
            transaction = connection.BeginTransaction();
            using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
            {
                sqlBulkCopy.DestinationTableName = "Members";
                sqlBulkCopy.ColumnMappings.Add("Firstname", "Firstname");
                sqlBulkCopy.ColumnMappings.Add("Lastname", "Lastname");
                sqlBulkCopy.ColumnMappings.Add("DOB", "DOB");
                sqlBulkCopy.ColumnMappings.Add("Gender", "Gender");
                sqlBulkCopy.ColumnMappings.Add("Email", "Email");

                sqlBulkCopy.ColumnMappings.Add("Address1", "Address1");
                sqlBulkCopy.ColumnMappings.Add("Address2", "Address2");
                sqlBulkCopy.ColumnMappings.Add("Address3", "Address3");
                sqlBulkCopy.ColumnMappings.Add("Address4", "Address4");
                sqlBulkCopy.ColumnMappings.Add("Postcode", "Postcode");

                sqlBulkCopy.ColumnMappings.Add("MobileNumber", "MobileNumber");
                sqlBulkCopy.ColumnMappings.Add("TelephoneNumber", "TelephoneNumber");

                sqlBulkCopy.ColumnMappings.Add("Deleted", "Deleted");

                sqlBulkCopy.WriteToServer(dataTable);
            }
            transaction.Commit();
        }
        catch (Exception)
        {
            transaction.Rollback();
        }

    }
}

데이터를 삽입하기 위해 xml 형식의 입력 데이터를 사용하는 저장 프로 시저를 사용합니다.

C # 코드에서 데이터를 XML로 삽입하십시오.

예를 들어 C #에서는 구문이 다음과 같이됩니다.

object id_application = db.ExecuteScalar("procSaveApplication", xml)

[POSTGRESQL의 새로운 해결책] 이봐 요, 나는 오래된 게시물 인 것을 알고 있지만, 비슷한 문제가 생겼습니다.하지만 Postgresql을 사용하고있었습니다. 나는 효과적인 bulkinsert를 사용하기를 원했고, 꽤 어려웠다. 이 DB에 대해 적절한 무료 라이브러리를 찾지 못했습니다. 나는이 도우미를 찾았습니다 : https://bytefish.de/blog/postgresql_bulk_insert/ 또한 Nuget에 있습니다. 나는 작은 매퍼를 작성했다. 자동 맵핑 된 속성은 엔티티 프레임 워크와 같다 :

public static PostgreSQLCopyHelper<T> CreateHelper<T>(string schemaName, string tableName)
        {
            var helper = new PostgreSQLCopyHelper<T>("dbo", "\"" + tableName + "\"");
            var properties = typeof(T).GetProperties();
            foreach(var prop in properties)
            {
                var type = prop.PropertyType;
                if (Attribute.IsDefined(prop, typeof(KeyAttribute)) || Attribute.IsDefined(prop, typeof(ForeignKeyAttribute)))
                    continue;
                switch (type)
                {
                    case Type intType when intType == typeof(int) || intType == typeof(int?):
                        {
                            helper = helper.MapInteger("\"" + prop.Name + "\"",  x => (int?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type stringType when stringType == typeof(string):
                        {
                            helper = helper.MapText("\"" + prop.Name + "\"", x => (string)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type dateType when dateType == typeof(DateTime) || dateType == typeof(DateTime?):
                        {
                            helper = helper.MapTimeStamp("\"" + prop.Name + "\"", x => (DateTime?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type decimalType when decimalType == typeof(decimal) || decimalType == typeof(decimal?):
                        {
                            helper = helper.MapMoney("\"" + prop.Name + "\"", x => (decimal?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type doubleType when doubleType == typeof(double) || doubleType == typeof(double?):
                        {
                            helper = helper.MapDouble("\"" + prop.Name + "\"", x => (double?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type floatType when floatType == typeof(float) || floatType == typeof(float?):
                        {
                            helper = helper.MapReal("\"" + prop.Name + "\"", x => (float?)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                    case Type guidType when guidType == typeof(Guid):
                        {
                            helper = helper.MapUUID("\"" + prop.Name + "\"", x => (Guid)typeof(T).GetProperty(prop.Name).GetValue(x, null));
                            break;
                        }
                }
            }
            return helper;
        }

나는 그것을 다음과 같은 방식으로 사용한다.

var undertakingHelper = BulkMapper.CreateHelper<Model.Undertaking>("dbo", nameof(Model.Undertaking));
undertakingHelper.SaveAll(transaction.UnderlyingTransaction.Connection as Npgsql.NpgsqlConnection, undertakingsToAdd));

트랜잭션을 사용한 예를 보여 주었지만 컨텍스트에서 검색된 정상 연결로도 수행 할 수 있습니다. undertakingsToAdd는 데이터베이스로 bulkInsert하기를 원하는 일반 엔티티 레코드를 열거 할 수 있습니다.

몇 시간의 연구와 노력 끝에 얻은이 솔루션은 훨씬 빠르고 사용하기 쉽고 무료로 사용할 수 있습니다! 위에서 언급 한 이유뿐만 아니라 PostgreSQL 자체에 문제가 없었기 때문에이 솔루션을 사용하는 것이 좋습니다. 많은 다른 솔루션이 SqlServer와 같이 완벽하게 작동합니다.


Dispose() 컨텍스트는 Add() 엔티티가 컨텍스트에서 다른 미리로드 된 엔티티 (예 : 네비게이션 속성)에 의존하는 경우 문제를 만듭니다.

비슷한 컨셉으로 내 컨텍스트를 작게 유지하여 동일한 성능 달성

그러나 컨텍스트를 Dispose() 하는 대신에 이미 SaveChanges() 있는 엔티티를 분리하면됩니다.

public void AddAndSave<TEntity>(List<TEntity> entities) where TEntity : class {

const int CommitCount = 1000; //set your own best performance number here
int currentCount = 0;

while (currentCount < entities.Count())
{
    //make sure it don't commit more than the entities you have
    int commitCount = CommitCount;
    if ((entities.Count - currentCount) < commitCount)
        commitCount = entities.Count - currentCount;

    //e.g. Add entities [ i = 0 to 999, 1000 to 1999, ... , n to n+999... ] to conext
    for (int i = currentCount; i < (currentCount + commitCount); i++)        
        _context.Entry(entities[i]).State = System.Data.EntityState.Added;
        //same as calling _context.Set<TEntity>().Add(entities[i]);       

    //commit entities[n to n+999] to database
    _context.SaveChanges();

    //detach all entities in the context that committed to database
    //so it won't overload the context
    for (int i = currentCount; i < (currentCount + commitCount); i++)
        _context.Entry(entities[i]).State = System.Data.EntityState.Detached;

    currentCount += commitCount;
} }

try catch 및 TrasactionScope() 싸서 필요하면 여기에 표시하지 않고 코드를 깨끗하게 유지하십시오.


직접 실행 창에 다음과 같이 써야합니다. 3

(((exception as System.Data.Entity.Validation.DbEntityValidationException).EntityValidationErrors as System.Collections.Generic.List<System.Data.Entity.Validation.DbEntityValidationResult>)[0].ValidationErrors as System.Collections.Generic.List<System.Data.Entity.Validation.DbValidationError>)[0]

정확한 오류를 깊이 파헤 치기 위해!







c# sql entity-framework