[c#] Entity Framework에서 가장 빠른 삽입 방법


11 Answers

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

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

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

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




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

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

그러나 컨텍스트를 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 ()로 둘러 싸서 필요하면 여기에 표시하지 않고 코드를 깨끗하게 유지하십시오.




나는 Slauma의 대답 (멋진 사람, 아이디어 맨의 덕택으로)을 조사했고, 최적의 속도에 도달 할 때까지 배치 크기를 줄였습니다. Slauma의 결과를 살펴보면 :

  • commitCount = 1, recreateContext = true : 10 분 이상
  • commitCount = 10, recreateContext = true : 241 초
  • commitCount = 100, recreateContext = true : 164 초
  • commitCount = 1000, recreateContext = true : 191 초

1에서 10, 10에서 100으로 이동할 때 속도가 증가하지만 100에서 1000까지의 삽입 속도가 다시 떨어지는 것을 볼 수 있습니다.

따라서 배치 크기를 10에서 100 사이의 값으로 줄이면 무슨 일이 일어나는지 집중적으로 살펴 보았습니다. 여기에 내 결과가 있습니다 (다른 행 내용을 사용하므로 시간이 다른 값을 가짐).

Quantity    | Batch size    | Interval
1000    1   3
10000   1   34
100000  1   368

1000    5   1
10000   5   12
100000  5   133

1000    10  1
10000   10  11
100000  10  101

1000    20  1
10000   20  9
100000  20  92

1000    27  0
10000   27  9
100000  27  92

1000    30  0
10000   30  9
100000  30  92

1000    35  1
10000   35  9
100000  35  94

1000    50  1
10000   50  10
100000  50  106

1000    100 1
10000   100 14
100000  100 141

내 결과에 따르면 실제 최적 값은 배치 크기의 경우 약 30입니다. 문제는 30 가지가 왜 최적인지 전혀 알지 못합니다. 또 어떤 논리적 인 설명도 찾을 수 없었습니다.




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

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

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

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



삽입 할 데이터의 XML을 가져올 저장 프로 시저 를 사용해보십시오.




위의 예에서 @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();
    }
}



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

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




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

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

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

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




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

즉.

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



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

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




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



[NEW SOLUTION FOR POSTGRESQL] Hey, I know it's quite an old post, but I have recently run into similar problem, but we were using Postgresql. I wanted to use effective bulkinsert, what turned out to be pretty difficult. I haven't found any proper free library to do so on this DB. I have only found this helper: https://bytefish.de/blog/postgresql_bulk_insert/ which is also on Nuget. I have written a small mapper, which auto mapped properties the way Entity Framework:

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

I use it the following way (I had entity named Undertaking):

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

I showed an example with transaction, but it can also be done with normal connection retrieved from context. undertakingsToAdd is enumerable of normal entity records, which I want to bulkInsert into DB.

This solution, to which I've got after few hours of research and trying, is as you could expect much faster and finally easy to use and free! I really advice you to use this solution, not only for the reasons mentioned above, but also because it's the only one with which I had no problems with Postgresql itself, many other solutions work flawlessly for example with SqlServer.






Related