SqlBulkCopy 및 DataTable의 성능 문제

c# datatable performance sqlbulkcopy streamreader

문제

파일에서 데이터베이스로 많은 양의 데이터를 효율적으로 가져와야합니다. 그 데이터를 포함하는 rrf 파일이 거의 없지만 파일 크기가 400MB 이상일 수 있으며 결국 파일로부터 데이터베이스에 2 백만이 넘는 레코드가 될 수 있습니다.

내가 뭘 한거지:

  1. DataTable에서 필요한 레코드를 읽고 있습니다.

    using (StreamReader streamReader = new StreamReader(filePath))
    {
        IEnumerable<string> values = new List<string>();
    
        while (!streamReader.EndOfStream)
        {
            string line = streamReader.ReadLine().Split('|');
    
             int index = 0;
             var dataRow = dataTable.NewRow();
    
             foreach (var value in values)
             {
                dataRow[index] = value;
                index++;
             }
    
             dataTable.Rows.Add(dataRow);
        }
    }
    
  2. 트랜잭션 내에서 (이것은 중요한 포인트입니다.) DataTable 데이터를 SqlBulkCopy 를 통해 데이터베이스에 삽입합니다.

    var bcp = new SqlBulkCopy(_sqlConnection, SqlBulkCopyOptions.Default, transaction);
    bcp.DestinationTableName = tableName;          
    bcp.WriteToServer(dataTable);
    

문제는 각 DataTable 2 백만 개가 넘는 레코드가 포함될 수 있기 때문에 DataTable 저장소에 많은 RAM (약 2GB)이 필요하다는 것입니다.

같은 것들

dataTable.Dispose();
dataTable = null;

또는

GC.Collect();
GC.SuppressFinalize();

실제로 도움이되지 마십시오.

SqlBulkCopyBatchsize 속성은 아무 관계가 없습니다. 모든 메모리는 삽입해야하는 행을 저장하는 DataTable 의해 사용됩니다.

데이터를 읽고 SqlBulkCopy 를 사용하는 효율적인 방법이 있습니까?

인기 답변

필자의 경험에 따르면 대량 삽입을위한 최적의 DataTable 크기는 60,000 개에서 100,000 개 사이입니다. 또한 DataTable을 재사용하면 새로운 것을 복제하는 것보다 느린 것으로 나타났습니다. DataTable.Rows.Clear ()는 제약 조건을 지우지 않으며 첫 번째 대량 삽입 후에 새 행을 추가하는 속도가 훨씬 느립니다. DataTable.Clear ()가 훨씬 좋았지 만 새로운 DataTable부터 시작하여 각 일괄 처리가 가장 빠릅니다.

따라서 코드는 다음과 같습니다.

int batchSize = 65000;
bool lastLine = streamReader.EndOfStream;

if (dataTable.Rows.Count == batchSize || lastLine) {
    // do bulk insert
    DataTable temp = dataTable.Clone();
    dataTable.Dispose();
    dataTable = temp;
}

이 외에도 대량 삽입을 자체 스레드로 분리 할 수 ​​있습니다. 따라서 파일 읽기 스레드는 대량 삽입 스레드가 사용할 DataTable 개체를 생성합니다. 파일 읽기 스레드가 과잉 생성하지 않도록 세마포를 추가해야합니다. 그렇지 않으면 너무 많은 메모리를 사용하게됩니다.

다음은 생산 / 소비 코드의 예입니다. 부담없이 개선하십시오.

잠자기 시간을 가지고 놀면 코드가 생산자 측 또는 소비자 측에서 어떻게 대기하는지 확인할 수 있습니다.

public static void produce() {

    DataObject o2 = new DataObject();
    Thread t = new Thread(consume);
    t.Start(o2);

    for (int i = 0; i < 10; i++) {
        if (o2.queue.Count > 2) {
            lock(o2.sb)
                o2.sb.AppendLine("3 items read, waiting for consume to finish");

            o2.wait.Set();
            o2.parentWait.WaitOne();
            o2.parentWait.Reset();
        }

        Thread.Sleep(500); // simulate reading data

        lock(o2.sb)
            o2.sb.AppendLine("Read file: " + i);

        lock(o2.queue) {
            o2.queue.Add(i);
        }
        o2.wait.Set();
    }

    o2.finished = true;
    o2.wait.Set();
}

public class DataObject {
    public bool finished = false;
    public List<int> queue = new List<int>();
    public ManualResetEvent wait = new ManualResetEvent(false);
    public ManualResetEvent parentWait = new ManualResetEvent(false);
    public StringBuilder sb = new StringBuilder();
}

public static void consume(Object o) {
    DataObject o2 = (DataObject) o;

    while (true) {
        if (o2.finished && o2.queue.Count == 0)
            break;

        if (o2.queue.Count == 0) {
            lock(o2.sb)
                o2.sb.AppendLine("Nothing in queue, waiting for produce.");
            o2.wait.WaitOne();
            o2.wait.Reset();
        }

        Object val = null;
        lock(o2.queue) {
            val = o2.queue[0];
            o2.queue.RemoveAt(0);
        }

        o2.parentWait.Set(); // notify parent to produce more

        lock(o2.sb)
            o2.sb.AppendLine("Loading data to SQL: " + val);

        Thread.Sleep(500);
    }
}


아래 라이선스: CC-BY-SA with attribution
와 제휴하지 않음 Stack Overflow
이 KB는 합법적입니까? 예, 이유를 알아보십시오.
아래 라이선스: CC-BY-SA with attribution
와 제휴하지 않음 Stack Overflow
이 KB는 합법적입니까? 예, 이유를 알아보십시오.