programing

엔티티 프레임워크에 가장 빠르게 삽입하는 방법

stoneblock 2023. 4. 14. 21:06

엔티티 프레임워크에 가장 빠르게 삽입하는 방법

엔티티 프레임워크에 가장 빠르게 삽입할 수 있는 방법을 찾고 있습니다.

이 을 하는 입니다.TransactionScope삽입이 큰 편입니다(substance+).이는 잠재적으로 10분 이상 지속될 수 있으며(트랜잭션의 기본 타임아웃), 이로 인해 트랜잭션이 불완전해집니다.

질문에 대한 코멘트의 코멘트에 대해서:

"... Saving Changes ( 레코드에 대해)..."

''콜링'SaveChanges()각 레코드에 대해 벌크 삽입 속도가 매우 느려집니다.퍼포먼스를 향상시킬 수 있는 간단한 테스트를 몇 가지 실시합니다.

  • ★★SaveChanges()★★★★★★★★★★★★★★★★★」
  • ★★SaveChanges()100달러
  • ★★SaveChanges()예를 들어 100개의 레코드 뒤에 콘텍스트를 폐기하고 새 콘텍스트를 만듭니다.
  • 변경 감지 사용 안 함

벌크 인서트의 경우 다음과 같은 패턴으로 작업 및 실험하고 있습니다.

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

DB에 560.000개의 엔티티(9개의 스칼라 속성, 내비게이션 속성 없음)를 삽입하는 테스트 프로그램이 있습니다.이 코드를 사용하면 3분 이내에 동작합니다.

을 하기 는 꼭 .SaveChanges()"다수" 레코드('다수' 약 100 또는 1000) 뒤에 표시됩니다.또한 SaveChanges 이후에 컨텍스트를 폐기하고 새 컨텍스트를 생성하는 성능도 향상됩니다. 모든.SaveChanges 않습니다.. 엔티티는 여전히 스테이트의 컨텍스트에 접속되어 있습니다.Unchanged 된 엔티티의 것이 으로 늦춘다. 콘텍스트에서 접속된 엔티티의 크기가 커지는 것이 삽입 속도를 단계적으로 늦춘다.그래서 조금 시간이 지난 후에 클리어 하는 것이 도움이 됩니다.

다음은 560000 엔티티에 대한 몇 가지 측정값입니다.

  • commitCount = 1, refreateContext = false: 여러 시간(현재 절차)
  • commitCount = 100, refreateContext = false: 20분 초과
  • commitCount = 1000, refreateContext = false: 242초
  • commitCount = 10000, refreateContext = false: 202초
  • commitCount = 100000, refreateContext = false: 199초
  • commitCount = 1000000, refreateContext = false: 메모리 부족 예외
  • commitCount = 1, refreateContext = true: 10분 이상 소요됨
  • commitCount = 10, refreateContext = true: 241초
  • commitCount = 100, refreateContext = true: 164초
  • commitCount = 1000, refreateContext = true: 191초

위의 첫 번째 테스트에서의 동작은 퍼포먼스가 매우 비선형이며 시간이 지남에 따라 현저하게 저하되는 것입니다.('많은 시간'은 추정치입니다.이 테스트를 끝내지 못했습니다.20분 후에 50.000개의 엔티티에서 정지했습니다.)이 비선형 동작은 다른 모든 테스트에서 유의하지 않습니다.

이 조합으로 충분히 속도를 높일 수 있습니다.

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

여기에 언급되지 않았기 때문에 저는 EFCore를 다시 언급하고 싶습니다.벌크 익스텐션

context.BulkInsert(entitiesList);                 context.BulkInsertAsync(entitiesList);
context.BulkUpdate(entitiesList);                 context.BulkUpdateAsync(entitiesList);
context.BulkDelete(entitiesList);                 context.BulkDeleteAsync(entitiesList);
context.BulkInsertOrUpdate(entitiesList);         context.BulkInsertOrUpdateAsync(entitiesList);         // Upsert
context.BulkInsertOrUpdateOrDelete(entitiesList); context.BulkInsertOrUpdateOrDeleteAsync(entitiesList); // Sync
context.BulkRead(entitiesList);                   context.BulkReadAsync(entitiesList);

경우 .System.Data.SqlClient.SqlBulkCopy이걸 위해서.여기 매뉴얼이 있습니다.물론 온라인에는 많은 튜토리얼이 있습니다.

죄송합니다. EF가 원하는 대로 수행하도록 하기 위해 간단한 답을 찾고 있었다는 것은 알지만, 대량 운영은 ORM의 용도가 아닙니다.

가장 빠른 방법은 제가 개발한 벌크 인서트 익스텐션을 사용하는 것입니다.

주의: 이 제품은 시판 제품으로 무료가 아닙니다.

SqlBulkCopy와 커스텀 데이터레이더를 사용하여 최고의 성능을 제공합니다. 일반 또는 Range를 .엔티티 프레임워크

사용법은 극히 간단하다

context.BulkInsert(hugeAmountOfEntities);

아담 라키스 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();
        }

    }
}

EF를 사용한 벌크 인서트 방법에 대한 이 기사를 추천합니다.

엔티티 프레임워크 및 느린 대량 삽입

그는 다음 분야를 살펴보고 성능을 비교합니다.

  1. 디폴트 EF(30,000 레코드 추가 완료까지 57분)
  2. ADO로 대체.NET 코드(같은 30,000의 경우 25초)
  3. Context Bloat - 각 작업단위에 대해 새로운 콘텍스트를 사용하여 액티브한 콘텍스트그래프를 작게 유지합니다(같은 30,000개의 삽입에는 33초가 소요됩니다).
  4. Large Lists - AutoDetectChangesEnabled를 해제합니다(시간을 약 20초로 단축).
  5. 배치(최소 16초)
  6. [ DbTable 。Add Range() - (퍼포먼스는 12 범위 내)

[2019 업데이트] EF Core 3.1

위에서 설명한 바와 같이 EF Core에서 AutoDetectChangesEnabled를 비활성화하면 삽입 시간이 100으로 분할됩니다(수 분에서 수 초, 크로스 테이블 관계가 있는 10,000 레코드).

업데이트된 코드는 다음과 같습니다.

context.ChangeTracker.AutoDetectChangesEnabled = false;
foreach (IRecord record in records) {
    //Add records to your database        
}
context.ChangeTracker.DetectChanges();
context.SaveChanges();
context.ChangeTracker.AutoDetectChangesEnabled = true; //do not forget to re-enable

Slauma의 답변(아이디어맨 덕분에 훌륭)을 조사하여 최적의 속도에 도달할 때까지 배치 크기를 줄였습니다.Slauma의 결과를 보면:

  • commitCount = 1, refreateContext = true: 10분 이상 소요됨
  • commitCount = 10, refreateContext = true: 241초
  • commitCount = 100, refreateContext = true: 164초
  • commitCount = 1000, refreateContext = 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의 값입니다.10개랑 100개 둘 다 아니에요.문제는 왜 30이 최적인지도 모르겠고 그에 대한 논리적인 설명도 찾을 수 없다는 것입니다.

SqlBulkCopy가 뛰어난 삽입 성능을 원한다면 다른 사용자가 말했듯이 SqlBulkCopy를 사용하는 것이 좋습니다.

구현이 다소 번거롭지만 이를 지원하는 라이브러리가 있습니다.몇 개 밖에 있지만 이번에는 뻔뻔하게 내 라이브러리를 연결할 것이다.https://github.com/MikaelEliasson/EntityFramework.Utilities#batch-insert-entities

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

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

그럼 얼마나 더 빨라?컴퓨터 성능, 네트워크, 객체 크기 등에 따라 달라지기 때문에 말하기 매우 어렵습니다.제가 수행한 성능 테스트 결과, 다른 답변에서 언급한 것처럼 EF 구성을 최적화할 경우 localhost에 표준 방식으로 25k개의 엔티티를 약 10s로 삽입할 수 있는 것으로 나타났습니다.300밀리초 정도 걸리는 EFUtilities 포함.더욱 흥미로운 것은 이 방법을 사용하여 약 3백만 엔티티를 15초 이내에 절약하고 초당 평균 약 20만 엔티티를 절약했다는 점입니다.

한 가지 문제는 당연히 관련된 데이터를 삽입해야 한다는 것입니다.이는 위의 방법으로 SQL 서버에서 효율적으로 수행할 수 있지만, 외부 키를 설정할 수 있도록 부모 앱 코드에서 ID를 생성할 수 있는 ID 생성 전략이 필요합니다.이것은 GUID 또는 HiLo ID 생성 등을 사용하여 수행할 수 있습니다.

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()를 위해서 하지 말아 주세요.

매우 오래된 질문인 것은 알지만, 여기 있는 한 남자가 EF에서 벌크 인서트를 사용하는 확장 방법을 개발했다고 말했습니다. 그리고 제가 확인해보니 오늘 라이브러리의 가격은 (개발자 한 명당) $599입니다.라이브러리 전체로는 타당할 수 있지만, 대량 삽입만 해도 너무 큽니다.

여기 제가 만든 아주 간단한 확장 방법이 있습니다.먼저 데이터베이스와 쌍으로 사용합니다(코드로 테스트하는 것은 아니지만, 마찬가지로 동작한다고 생각합니다).YourEntities콘텍스트의 이름을 지정합니다.

public partial class YourEntities : DbContext
{
    public async Task BulkInsertAllAsync<T>(IEnumerable<T> entities)
    {
        using (var conn = new SqlConnection(Database.Connection.ConnectionString))
        {
            await conn.OpenAsync();

            Type t = typeof(T);

            var bulkCopy = new SqlBulkCopy(conn)
            {
                DestinationTableName = GetTableName(t)
            };

            var table = new DataTable();

            var properties = t.GetProperties().Where(p => p.PropertyType.IsValueType || p.PropertyType == typeof(string));

            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 => property.GetValue(entity, null) ?? DBNull.Value).ToArray());
            }

            bulkCopy.BulkCopyTimeout = 0;
            await bulkCopy.WriteToServerAsync(table);
        }
    }

    public void BulkInsertAll<T>(IEnumerable<T> entities)
    {
        using (var conn = new SqlConnection(Database.Connection.ConnectionString))
        {
            conn.Open();

            Type t = typeof(T);

            var bulkCopy = new SqlBulkCopy(conn)
            {
                DestinationTableName = GetTableName(t)
            };

            var table = new DataTable();

            var properties = t.GetProperties().Where(p => p.PropertyType.IsValueType || p.PropertyType == typeof(string));

            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 => property.GetValue(entity, null) ?? DBNull.Value).ToArray());
            }

            bulkCopy.BulkCopyTimeout = 0;
            bulkCopy.WriteToServer(table);
        }
    }

    public string GetTableName(Type type)
    {
        var metadata = ((IObjectContextAdapter)this).ObjectContext.MetadataWorkspace;
        var objectItemCollection = ((ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace));

        var entityType = metadata
                .GetItems<EntityType>(DataSpace.OSpace)
                .Single(e => objectItemCollection.GetClrType(e) == type);

        var entitySet = metadata
            .GetItems<EntityContainer>(DataSpace.CSpace)
            .Single()
            .EntitySets
            .Single(s => s.ElementType.Name == entityType.Name);

        var mapping = metadata.GetItems<EntityContainerMapping>(DataSpace.CSSpace)
                .Single()
                .EntitySetMappings
                .Single(s => s.EntitySet == entitySet);

        var table = mapping
            .EntityTypeMappings.Single()
            .Fragments.Single()
            .StoreEntitySet;

        return (string)table.MetadataProperties["Table"].Value ?? table.Name;
    }
}

다음에서 상속되는 컬렉션에 대해 사용할 수 있습니다.IEnumerable다음과 같이 합니다.

await context.BulkInsertAllAsync(items);

ㅇㅇ.SqlBulkUpdate이런 종류의 작업을 위한 가장 빠른 도구입니다.에서 '최소한의 노력'이라는 일반적인 방법을 찾고 싶었습니다.NET Core는 결국 FastMember라는 Marc Gravell의 훌륭한 라이브러리를 사용하여 엔티티 프레임워크 DB 컨텍스트를 위한 작은 확장 방법을 작성하게 되었습니다.번개처럼 빠른 동작:

using System.Collections.Generic;
using System.Linq;
using FastMember;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;

namespace Services.Extensions
{
    public static class DbContextExtensions
    {
        public static void BulkCopyToServer<T>(this DbContext db, IEnumerable<T> collection)
        {
            var messageEntityType = db.Model.FindEntityType(typeof(T));

            var tableName = messageEntityType.GetSchema() + "." + messageEntityType.GetTableName();
            var tableColumnMappings = messageEntityType.GetProperties()
                .ToDictionary(p => p.PropertyInfo.Name, p => p.GetColumnName());

            using (var connection = new SqlConnection(db.Database.GetDbConnection().ConnectionString))
            using (var bulkCopy = new SqlBulkCopy(connection))
            {
                foreach (var (field, column) in tableColumnMappings)
                {
                    bulkCopy.ColumnMappings.Add(field, column);
                }

                using (var reader = ObjectReader.Create(collection, tableColumnMappings.Keys.ToArray()))
                {
                    bulkCopy.DestinationTableName = tableName;
                    connection.Open();
                    bulkCopy.WriteToServer(reader);
                    connection.Close();
                }
            }
        }
    }
}

엔티티 프레임워크에 가장 빠르게 삽입할 수 있는 방법을 찾고 있습니다.

대량 삽입을 지원하는 일부 서드파티 라이브러리가 있습니다.

  • Z.EntityFramework내선번호(권장)
  • EFUtilities(EFUtilities)
  • 엔티티 프레임워크일괄 삽입

참조: 엔티티 프레임워크 일괄 삽입 라이브러리

대량 삽입 라이브러리를 선택할 때는 주의하십시오.모든 종류의 연결 및 상속을 지원하는 것은 엔티티 프레임워크 확장뿐이며 현재 지원되는 것은 이 확장뿐입니다.


면책사항: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;
});

목록을 저장하는 가장 빠른 방법 중 하나는 다음 코드를 적용해야 합니다.

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

AutoDetectChangesEnabled = false

변경 내용 추가, 추가 및 저장:변경을 검출하지 않습니다.

ValidateOnSaveEnabled = false;

변경 추적기를 감지하지 못함

nuget을 추가해야 합니다.

Install-Package Z.EntityFramework.Extensions

이제 다음 코드를 사용할 수 있습니다.

var context = new MyContext();

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

context.BulkInsert(list);
context.BulkSaveChanges();

삽입할 데이터의 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();
    }
}

SqlBulkCopy는 매우 빠릅니다.

구현은 다음과 같습니다.

// at some point in my calling code, I will call:
var myDataTable = CreateMyDataTable();
myDataTable.Rows.Add(Guid.NewGuid,tableHeaderId,theName,theValue); // e.g. - need this call for each row to insert

var efConnectionString = ConfigurationManager.ConnectionStrings["MyWebConfigEfConnection"].ConnectionString;
var efConnectionStringBuilder = new EntityConnectionStringBuilder(efConnectionString);
var connectionString = efConnectionStringBuilder.ProviderConnectionString;
BulkInsert(connectionString, myDataTable);

private DataTable CreateMyDataTable()
{
    var myDataTable = new DataTable { TableName = "MyTable"};
// this table has an identity column - don't need to specify that
    myDataTable.Columns.Add("MyTableRecordGuid", typeof(Guid));
    myDataTable.Columns.Add("MyTableHeaderId", typeof(int));
    myDataTable.Columns.Add("ColumnName", typeof(string));
    myDataTable.Columns.Add("ColumnValue", typeof(string));
    return myDataTable;
}

private void BulkInsert(string connectionString, DataTable dataTable)
{
    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        SqlTransaction transaction = null;
        try
        {
            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();
        }
        catch (Exception)
        {
            transaction?.Rollback();
            throw;
        }
    }
}

다음은 실제 예에서 Entity Framework를 사용하는 것과 SqlBulkCopy 클래스를 사용하는 것의 성능 비교입니다.SQL Server 데이터베이스에 복합 객체를 일괄 삽입하는 방법

다른 사람들이 이미 강조했듯이, 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;
}

가 바로는 있다no BulkInsertEntityFramework큰 인서트의 성능을 높이기 위해서입니다.

이 시나리오에서는 SqlBulkCopy를 사용하여ADO.net

SaveChanges()를 실행하면 삽입문이 하나씩 데이터베이스로 전송되므로 여기에 기재된 모든 솔루션은 도움이 되지 않습니다.

데이터베이스로의 이동과 복귀가 예를 들어 50밀리초인 경우 삽입에 필요한 시간은 레코드 수 x 50밀리초입니다.

BulkInsert를 사용해야 합니다.이 링크는 https://efbulkinsert.codeplex.com/ 입니다.

그것을 사용함으로써 삽입 시간을 5~6분에서 10~12초로 단축했습니다.

또 다른 옵션은 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();
}

자세한 예와 고급 사용 방법은 설명서를 참조하십시오.면책사항:나는 이 도서관의 저자이며 어떤 의견도 내 자신의 의견이다.

[POSTGRESQL의 새로운 솔루션]오래된 투고인 것은 알지만, 최근 같은 문제가 발생하고 있습니다만, Postgresql을 사용하고 있었습니다.효과적인 벌킨서트를 사용하고 싶었지만, 꽤 어려운 일이었습니다.이 DB에서 적절한 빈 라이브러리를 찾을 수 없습니다.Nuget에 있는 도우미 https://bytefish.de/blog/postgresql_bulk_insert/만 찾았습니다.엔티티 프레임워크에 따라 속성을 자동으로 매핑하는 작은 매퍼를 작성했습니다.

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

트랜잭션의 예를 보여드렸습니다만, 컨텍스트로부터 취득한 통상의 접속에서도 실행할 수 있습니다.understakingsToAdd는 벌크하고 싶은 일반 엔티티 레코드의 열거형입니다.DB에 삽입합니다.

몇 시간 동안 조사와 시도를 거쳐 얻은 이 솔루션은 훨씬 빠르고 사용하기 쉽고 무료입니다.위의 이유뿐만 아니라 Postgresql 자체에서 문제가 없었던 유일한 솔루션이기 때문에 이 솔루션을 사용하는 것이 좋습니다.예를 들어 SQL Server에서는 다른 많은 솔루션이 완벽하게 동작합니다.

몇 가지 메모를 해두면, 이것은 나의 개선사항과 다른 답변과 코멘트를 포함한 나의 실장입니다.

개선점:

  • 내 엔티티에서 SQL 연결 문자열을 가져오는 중

  • 일부 부분에서만 SQLBulk를 사용하고 나머지는 엔티티 프레임워크만 사용합니다.

  • 각 열을 매핑하지 않고 SQL 데이터베이스를 사용하는 동일한 Datetable 열 이름 사용

  • SQL Datatable을 사용하는 동일한 Datatable 이름 사용

    public  void InsertBulkDatatable(DataTable dataTable)
         {
             EntityConnectionStringBuilder entityBuilder =  new EntityConnectionStringBuilder(ConfigurationManager.ConnectionStrings["MyDbContextConnectionName"].ConnectionString);
             string cs = entityBuilder.ProviderConnectionString;
             using (var connection = new SqlConnection(cs))
             {
                 SqlTransaction transaction = null;
                 connection.Open();
                 try
                 {
                     transaction = connection.BeginTransaction();
                     using (var sqlBulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.TableLock, transaction))
                     {
                         sqlBulkCopy.DestinationTableName = dataTable.TableName; //Uses the SQL datatable to name the datatable in c#
                         //Maping Columns
                         foreach (DataColumn column in dataTable.Columns) {
                             sqlBulkCopy.ColumnMappings.Add(column.ColumnName, column.ColumnName);
    
                         }
    
    
                         sqlBulkCopy.WriteToServer(dataTable);
                     }
                     transaction.Commit();
                 }
                 catch (Exception)
                 {
                     transaction.Rollback();
                 }
    
             }
         }
    

비밀은 동일한 빈 스테이징 테이블에 삽입하는 것입니다.인서트는 번개가 빨리 뜹니다.그런 다음 여기서 메인 대형 테이블에 삽입을 1개 실행합니다.그런 다음 다음 배치에 사용할 수 있도록 준비 테이블을 잘라냅니다.

즉,

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

백그라운드 워커나 태스크를 통해 삽입을 시도한 적이 있습니까?

제 경우, 7760 레지스터를 삽입하고 있습니다.이 레지스터는 (Navigation Properties에 의해) 182개의 다른 테이블에 분산되어 있습니다.

2시 30분이다. )Task.Factory.StartNew(...)15분

는 그냥 '어울릴 것 같다'만 있어요.SaveChanges() 정합성을 (데이터 정합성을 보증하기 위해) (데이터 정합성을 보증하기 위해)

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

자세한 설명은 여기에서 확인할 수 있습니다. 대량 작업 소스

TL;DR은 오래된 투고라는 것을 알고 있습니다만, 제안의 1개로부터 솔루션을 전개해, 몇개의 문제를 해결했습니다.또, 제시된 다른 솔루션도 읽어 보았습니다만, 이것들과 비교하면, 원래의 질문의 요구에 훨씬 더 적합한 솔루션을 제안하고 있는 것 같습니다.

이 솔루션에서는 Slauma의 접근방식을 확장합니다.이 접근방식은 첫 번째 질문에서 제안된 케이스에 매우 적합합니다.즉, 엔티티 프레임워크와 트랜잭션 범위를 사용하여 고가의 DB 쓰기 작업을 수행합니다.

Slauma의 솔루션에는 다음과 같은 문제가 있었습니다. 이 솔루션은 초안이었고 대량 삽입을 구현하는 전략으로 EF의 속도를 파악하는 데만 사용되었습니다.

  1. 트랜잭션 타임아웃(디폴트로는 코드를 통해 최대 10분까지 연장 가능)
  2. 트랜잭션 종료 시 사용된 커밋 크기와 동일한 폭의 첫 번째 데이터 블록을 복제합니다(이 문제는 매우 이상하고 회피책으로 회피됩니다).

또한 여러 종속 엔티티의 컨텍스트 삽입을 포함하는 예를 보고함으로써 Slauma가 제시한 사례 연구를 확장했습니다.

제가 검증할 수 있는 퍼포먼스는 각각 약 1KB의 20K 와이드 레코드의 블록을 db에 삽입하는 10K rec/min이었습니다.속도는 일정했고 성능 저하도 없었고 테스트가 성공적으로 실행되는 데 약 20분이 소요되었습니다.

솔루션 상세

예제 저장소 클래스에 삽입된 대량 삽입 작업을 수행하는 메서드:

abstract class SomeRepository { 

    protected MyDbContext myDbContextRef;

    public void ImportData<TChild, TFather>(List<TChild> entities, TFather entityFather)
            where TChild : class, IEntityChild
            where TFather : class, IEntityFather
    {

        using (var scope = MyDbContext.CreateTransactionScope())
        {

            MyDbContext context = null;
            try
            {
                context = new MyDbContext(myDbContextRef.ConnectionString);

                context.Configuration.AutoDetectChangesEnabled = false;

                entityFather.BulkInsertResult = false;
                var fileEntity = context.Set<TFather>().Add(entityFather);
                context.SaveChanges();

                int count = 0;

                //avoids an issue with recreating context: EF duplicates the first commit block of data at the end of transaction!!
                context = MyDbContext.AddToContext<TChild>(context, null, 0, 1, true);

                foreach (var entityToInsert in entities)
                {
                    ++count;
                    entityToInsert.EntityFatherRefId = fileEntity.Id;
                    context = MyDbContext.AddToContext<TChild>(context, entityToInsert, count, 100, true);
                }

                entityFather.BulkInsertResult = true;
                context.Set<TFather>().Add(fileEntity);
                context.Entry<TFather>(fileEntity).State = EntityState.Modified;

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

            scope.Complete();
        }

    }

}

예시로만 사용되는 인터페이스:

public interface IEntityChild {

    //some properties ...

    int EntityFatherRefId { get; set; }

}

public interface IEntityFather {

    int Id { get; set; }
    bool BulkInsertResult { get; set; }
}

솔루션의 다양한 요소를 정적 방법으로 구현한 db 컨텍스트:

public class MyDbContext : DbContext
{

    public string ConnectionString { get; set; }


    public MyDbContext(string nameOrConnectionString)
    : base(nameOrConnectionString)
    {
        Database.SetInitializer<MyDbContext>(null);
        ConnectionString = Database.Connection.ConnectionString;
    }


    /// <summary>
    /// Creates a TransactionScope raising timeout transaction to 30 minutes
    /// </summary>
    /// <param name="_isolationLevel"></param>
    /// <param name="timeout"></param>
    /// <remarks>
    /// It is possible to set isolation-level and timeout to different values. Pay close attention managing these 2 transactions working parameters.
    /// <para>Default TransactionScope values for isolation-level and timeout are the following:</para>
    /// <para>Default isolation-level is "Serializable"</para>
    /// <para>Default timeout ranges between 1 minute (default value if not specified a timeout) to max 10 minute (if not changed by code or updating max-timeout machine.config value)</para>
    /// </remarks>
    public static TransactionScope CreateTransactionScope(IsolationLevel _isolationLevel = IsolationLevel.Serializable, TimeSpan? timeout = null)
    {
        SetTransactionManagerField("_cachedMaxTimeout", true);
        SetTransactionManagerField("_maximumTimeout", timeout ?? TimeSpan.FromMinutes(30));

        var transactionOptions = new TransactionOptions();
        transactionOptions.IsolationLevel = _isolationLevel;
        transactionOptions.Timeout = TransactionManager.MaximumTimeout;
        return new TransactionScope(TransactionScopeOption.Required, transactionOptions);
    }

    private static void SetTransactionManagerField(string fieldName, object value)
    {
        typeof(TransactionManager).GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static).SetValue(null, value);
    }


    /// <summary>
    /// Adds a generic entity to a given context allowing commit on large block of data and improving performance to support db bulk-insert operations based on Entity Framework
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="context"></param>
    /// <param name="entity"></param>
    /// <param name="count"></param>
    /// <param name="commitCount">defines the block of data size</param>
    /// <param name="recreateContext"></param>
    /// <returns></returns>
    public static MyDbContext AddToContext<T>(MyDbContext context, T entity, int count, int commitCount, bool recreateContext) where T : class
    {
        if (entity != null)
            context.Set<T>().Add(entity);

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

        return context;
    }
}

배열.LazyLoadingEnabled = false; 설정.ProxyCreationEnabled = false;

AutoDetectChangesEnabled = false 없이 속도를 내기에는 너무 효과적이며 dbo와 다른 테이블 헤더를 사용하는 것이 좋습니다.나는 일반적으로 nop, filter, tbl 등을 사용한다.

언급URL : https://stackoverflow.com/questions/5940225/fastest-way-of-inserting-in-entity-framework