1000 개의 Entity Framework 개체를 만들 때 SaveChanges ()를 언제 호출해야합니까? (가져 오는 동안처럼)


80

각 실행에 대해 1000 개의 레코드가있는 가져 오기를 실행하고 있습니다. 내 가정에 대한 확인을 찾고 있습니다.

다음 중 가장 의미가있는 것은 무엇입니까?

  1. SaveChanges()모든 AddToClassName()통화를 실행하십시오 .
  2. n 번의 호출 SaveChanges()마다 실행 합니다.AddToClassName()
  3. 실행 SaveChanges()모든AddToClassName()호출.

첫 번째 옵션은 아마 느립니다. 메모리의 EF 개체를 분석하고 SQL을 생성하는 등의 작업이 필요하기 때문입니다.

두 번째 옵션이 두 세계 모두에서 최고라고 가정합니다. 그 SaveChanges()호출에 대해 try catch를 래핑하고 둘 중 하나가 실패 할 경우 한 번에 n 개의 레코드 만 잃을 수 있기 때문입니다. 각 배치를 List <>에 저장할 수 있습니다. 경우 SaveChanges()호출이 성공 목록을 없애. 실패하면 항목을 기록하십시오.

모든 단일 EF 개체 SaveChanges()가 호출 될 때까지 메모리에 있어야하므로 마지막 옵션도 매우 느려질 수 있습니다 . 그리고 저장이 실패하면 아무것도 커밋되지 않을 것입니다.

답변:


62

나는 그것을 확인하기 위해 먼저 테스트 할 것입니다. 성능이 그렇게 나쁠 필요는 없습니다.

하나의 트랜잭션에 모든 행을 입력해야하는 경우 모든 AddToClassName 클래스 다음에 호출하십시오. 행을 독립적으로 입력 할 수있는 경우 모든 행 후에 변경 사항을 저장합니다. 데이터베이스 일관성이 중요합니다.

두 번째 옵션이 마음에 들지 않습니다. 시스템으로 가져 오기를 수행하면 (최종 사용자 관점에서) 혼란스럽고 1이 나쁘기 때문에 1000 행 중 10 행이 감소합니다. 10 개 가져 오기를 시도 할 수 있으며 실패 할 경우 하나씩 시도한 다음 기록합니다.

시간이 오래 걸리는지 테스트하십시오. '적절하게'쓰지 마십시오. 당신은 아직 그것을 모릅니다. 실제로 문제가되는 경우에만 다른 솔루션 (marc_s)을 생각하십시오.

편집하다

몇 가지 테스트를 수행했습니다 (밀리 초 단위).

10000 개의 행 :

1 행 이후 SaveChanges () : 18510,534
100 행 이후
SaveChanges () : 4350,3075 10000 행 이후 SaveChanges () : 5233,0635

50000 행 :

1 행 이후 SaveChanges () : 78496,929
500 행 이후
SaveChanges () : 22302,2835 50000 행 이후 SaveChanges () : 24022,8765

따라서 실제로는 결국 n 행 이후에 커밋하는 것이 더 빠릅니다.

내 추천은 다음과 같습니다.

  • n 행 이후 SaveChanges ().
  • 하나의 커밋이 실패하면 하나씩 시도하여 잘못된 행을 찾습니다.

테스트 클래스 :

표:

CREATE TABLE [dbo].[TestTable](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [SomeInt] [int] NOT NULL,
    [SomeVarchar] [varchar](100) NOT NULL,
    [SomeOtherVarchar] [varchar](50) NOT NULL,
    [SomeOtherInt] [int] NULL,
 CONSTRAINT [PkTestTable] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

수업:

public class TestController : Controller
{
    //
    // GET: /Test/
    private readonly Random _rng = new Random();
    private const string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private string RandomString(int size)
    {
        var randomSize = _rng.Next(size);

        char[] buffer = new char[randomSize];

        for (int i = 0; i < randomSize; i++)
        {
            buffer[i] = _chars[_rng.Next(_chars.Length)];
        }
        return new string(buffer);
    }


    public ActionResult EFPerformance()
    {
        string result = "";

        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(10000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 100 rows:" + EFPerformanceTest(10000, 100).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 10000 rows:" + EFPerformanceTest(10000, 10000).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(50000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 500 rows:" + EFPerformanceTest(50000, 500).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 50000 rows:" + EFPerformanceTest(50000, 50000).TotalMilliseconds + "<br/>";
        TruncateTable();

        return Content(result);
    }

    private void TruncateTable()
    {
        using (var context = new CamelTrapEntities())
        {
            var connection = ((EntityConnection)context.Connection).StoreConnection;
            connection.Open();
            var command = connection.CreateCommand();
            command.CommandText = @"TRUNCATE TABLE TestTable";
            command.ExecuteNonQuery();
        }
    }

    private TimeSpan EFPerformanceTest(int noOfRows, int commitAfterRows)
    {
        var startDate = DateTime.Now;

        using (var context = new CamelTrapEntities())
        {
            for (int i = 1; i <= noOfRows; ++i)
            {
                var testItem = new TestTable();
                testItem.SomeVarchar = RandomString(100);
                testItem.SomeOtherVarchar = RandomString(50);
                testItem.SomeInt = _rng.Next(10000);
                testItem.SomeOtherInt = _rng.Next(200000);
                context.AddToTestTable(testItem);

                if (i % commitAfterRows == 0) context.SaveChanges();
            }
        }

        var endDate = DateTime.Now;

        return endDate.Subtract(startDate);
    }
}

내가 "아마"라고 쓴 이유는 내가 교육적인 추측을했기 때문이다. "잘 모르겠다"는 것을 더 명확히하기 위해 질문을했습니다. 또한 잠재적 인 문제에 부딪히기 전에 생각하는 것이 합리적이라고 생각합니다. 이것이 제가이 질문을 한 이유입니다. 나는 누군가가 어떤 방법이 가장 효율적인지 알기를 바라고 있었고, 바로 그 방법을 사용할 수있었습니다.
John Bubriski

멋진 친구. 내가 찾던 바로 그것. 시간을내어 테스트 해 주셔서 감사합니다! 각 배치를 메모리에 저장하고 커밋을 시도한 다음 실패하면 각 배치를 개별적으로 진행할 수 있다고 생각합니다. 그런 다음 배치가 완료되면 해당 100 개 항목에 대한 참조를 해제하여 메모리에서 지울 수 있습니다. 다시 한 번 감사드립니다!
John Bubriski

3
모든 개체가 ObjectContext에 의해 유지되기 때문에 메모리가 해제되지 않지만 컨텍스트에 50000 또는 100000이 있으면 오늘날 많은 공간을 차지하지 않습니다.
LukLed

6
실제로 SaveChanges ()를 호출 할 때마다 성능이 저하된다는 사실을 발견했습니다. 이에 대한 해결책은 각 SaveChanges () 호출 후 실제로 컨텍스트를 삭제하고 추가 할 다음 데이터 일괄 처리를 위해 새 컨텍스트를 다시 인스턴스화하는 것입니다.
Shawn de Wet

1
@LukLed가 아닙니다 ... For 루프 내에서 SaveChanges를 호출하고 있습니다 ... 따라서 코드는 동일한 ctx 인스턴스의 for 루프 내에 저장할 항목을 더 추가하고 나중에 동일한 인스턴스에서 SaveChanges를 다시 호출 할 수 있습니다. .
Shawn de Wet

18

방금 내 코드에서 매우 유사한 문제를 최적화했으며 저에게 효과가있는 최적화를 지적하고 싶습니다.

한 번에 100 개 또는 1000 개의 레코드를 처리하든 SaveChanges를 처리하는 데 많은 시간이 CPU 제한이라는 것을 알았습니다. 따라서 생산자 / 소비자 패턴 (BlockingCollection으로 구현 됨)으로 컨텍스트를 처리함으로써 CPU 코어를 훨씬 더 잘 사용할 수 있었고 초당 총 4000 회의 변경 (SaveChanges의 반환 값으로보고 됨)에서 초당 14,000 개 이상의 변경. CPU 사용률이 약 13 % (코어가 8 개 있음)에서 약 60 %로 이동했습니다. 여러 소비자 스레드를 사용하더라도 (매우 빠른) 디스크 IO 시스템에 거의 부담을주지 않았고 SQL Server의 CPU 사용률은 15 %를 넘지 않았습니다.

저장을 여러 스레드로 오프로드하면 커밋 이전의 레코드 수와 커밋 작업을 수행하는 스레드 수를 모두 조정할 수 있습니다.

1 개의 생산자 스레드와 (CPU 코어 수) -1 개의 소비자 스레드를 생성하면 BlockingCollection의 항목 수가 0과 1 사이에서 변동하도록 배치 당 커밋 된 레코드 수를 조정할 수 있다는 것을 발견했습니다 (소비자 스레드가 안건). 그렇게하면 소비하는 스레드가 최적으로 작동 할 수있는 작업이 충분했습니다.

물론이 시나리오에서는 모든 배치에 대해 새로운 컨텍스트를 만들어야하는데, 이는 제 사용 사례에 대한 단일 스레드 시나리오에서도 더 빠릅니다.


안녕하세요, @ eric-j "생산자 / 소비자 패턴 (BlockingCollection으로 구현 됨)을 사용하여 컨텍스트를 처리하여"이 줄을 약간 정교하게 만들어 제 코드를 사용해 볼 수 있습니까?
Foyzul 카림

14

수천 개의 레코드를 가져와야하는 경우 Entity Framework가 아닌 SqlBulkCopy와 같은 것을 사용합니다.


15
나는 사람들이 내 질문에 대답하지 않는 것이 싫다. 그럼 뭐야?
John Bubriski

3
글쎄, 정말로 EF를 사용해야 한다면 500 개 또는 1000 개의 레코드를 일괄 처리 한 후에 커밋을 시도 할 것입니다. 그렇지 않으면 너무 많은 리소스를 사용하게되고 실패하면 잠재적으로 100000 번째 행이 실패 할 때 업데이트 한 모든 99999 행이 롤백됩니다.
marc_s

같은 문제로 SqlBulkCopy를 사용하여 끝냈습니다.이 경우 EF보다 성능이 더 좋습니다. 데이터베이스에 액세스하는 데 여러 가지 방법을 사용하고 싶지는 않지만.
Julien N

2
나는 또한 같은 문제가 있기 때문에이 솔루션을 찾고 있습니다 ... 대량 복사는 훌륭한 솔루션이지만 내 호스팅 서비스는 그것을 사용하지 못하도록 (그리고 다른 사람들도 그렇게 할 것이라고 추측합니다), 이것은 실행 가능하지 않습니다 어떤 사람들에게는 옵션입니다.
Dennis Ward

3
@marc_s : SqlBulkCopy를 사용할 때 비즈니스 개체에 내재 된 비즈니스 규칙을 적용해야하는 필요성을 어떻게 처리합니까? 내가하는 방법이 표시되지 않습니다 하지 중복 규칙을 구현하지 않고 EF를 사용합니다.
Eric J.

2

저장 프로 시저를 사용하십시오.

  1. SQL Server에서 사용자 정의 데이터 형식을 만듭니다.
  2. 코드에서이 유형의 배열을 만들고 채 웁니다 (매우 빠름).
  3. 한 번의 호출로 저장 프로 시저에 배열을 전달합니다 (매우 빠름).

이것이 가장 쉽고 빠른 방법이라고 생각합니다.


7
일반적으로 SO에서 "이것이 가장 빠름"이라는 주장은 테스트 코드와 결과로 입증되어야합니다.
Michael Blackburn

2

죄송합니다.이 스레드가 오래되었다는 것을 알고 있지만이 문제가 다른 사람들에게 도움이 될 수 있다고 생각합니다.

나는 같은 문제가 있었지만 커밋하기 전에 변경 사항을 확인할 가능성이 있습니다. 내 코드는 다음과 같이 잘 작동합니다. 으로 chUser.LastUpdatedI 검사는 새 항목 또는 변경 만있는 경우. 아직 데이터베이스에없는 항목을 다시로드 할 수 없기 때문입니다.

// Validate Changes
var invalidChanges = _userDatabase.GetValidationErrors();
foreach (var ch in invalidChanges)
{
    // Delete invalid User or Change
    var chUser  =  (db_User) ch.Entry.Entity;
    if (chUser.LastUpdated == null)
    {
        // Invalid, new User
        _userDatabase.db_User.Remove(chUser);
        Console.WriteLine("!Failed to create User: " + chUser.ContactUniqKey);
    }
    else
    {
        // Invalid Change of an Entry
        _userDatabase.Entry(chUser).Reload();
        Console.WriteLine("!Failed to update User: " + chUser.ContactUniqKey);
    }                    
}

_userDatabase.SaveChanges();

네, 거의 같은 문제 죠? 이를 통해 1000 개의 레코드를 모두 추가 할 수 있으며 실행하기 전에 saveChanges()오류를 유발할 수있는 레코드 를 삭제할 수 있습니다.
Jan Leuenberger

1
그러나 질문의 ​​강조점은 한 번의 SaveChanges호출 에서 얼마나 많은 삽입 / 업데이트를 효율적으로 커밋 할 수 있는지에 있습니다. 당신은 그 문제를 다루지 않습니다. 유효성 검사 오류보다 SaveChanges가 실패하는 잠재적 인 이유가 더 많습니다. 그런데 엔티티 Unchanged를 다시로드 / 삭제하는 대신으로 표시 할 수도 있습니다.
Gert Arnold

1
당신 말이 맞습니다. 질문을 직접적으로 다루지는 않지만, 다른 이유 SaveChanges가 있지만 실패하는 이유 가 있지만 대부분의 사람들이이 스레드를 우연히 발견하여 검증에 문제가 있다고 생각 합니다. 그리고 이것은 문제를 해결합니다. 이 게시물이 정말로이 스레드에서 당신을 방해한다면 나는 이것을 삭제할 수 있습니다.
Jan Leuenberger

이것에 대해 질문이 있습니다. 호출 할 때 GetValidationErrors()데이터베이스 호출을 "위조"하고 오류 또는 무엇을 검색합니까? 답장 해 주셔서 감사합니다 :)
Jeancarlo Fontalvo
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.