C# 에서 for문에서 사용면 느린 또는 속도향상 시킬 방향 질문..

안녕하세요! C# 독학중인 초급개발자입니다.

지금 하고있는 프로젝트 중 기능 하나가 문제인데
추려낸 데이터(약 3000건)을 for문으로 순회해서 전체 데이터(약 4~50000건)에서 중복내역을 확인하고 있습니다. 하지만 지금 방식으로 비교하니 너무 느려 사용하기 불편하더라구요…

그래서 제가 짠 로직을 간략하게 설명드릴테니 여기서 사용하면 느린 변수나 바뀌면 빠르게 수행시간을 줄일 방법이 있다면 추천해주시면 감사하겠습니다!

<지금 방식>
*DataTable totalDt : 전체데이터, 약 4~5만건
*DataTable comparisonDt : 추려낸 데이터, 약 3천건

`

for(int i = 0; i < comparisonDt.Rows.Count; i++)
{
     //데이터 중복추출
     DataRow[] duplicateDr1 = totalDt.Select($"col1 = '{comparisonDt .Rows[i]["col1"].ToString()}'");
     duplicateDt1 = duplicateDr1.CopyToDataTable();

     DataRow[] duplicateDr2 = totalDt.Select($"col2 = '{comparisonDt .Rows[i]["col2"].ToString()}'");
     duplicateDt2 = duplicateDr2.CopyToDataTable();

     DataRow[] duplicateDr3 = totalDt.Select($"col3 = '{comparisonDt .Rows[i]["col3"].ToString()}'");
     duplicateDt3 = duplicateDr3.CopyToDataTable();

     DataRow[] duplicateDr4 = totalDt.Select($"col4 = '{comparisonDt .Rows[i]["col4"].ToString()}'");
     duplicateDt4 = duplicateDr4.CopyToDataTable();

     //추출데이터 -> List
     List<DataRow> duplicateList = new List<DataRow>();
     for(int j = 0; j < duplicateDt1.Rows.Count; j++)
          duplicateList.Add(duplicateDt1.Rows[j];

     for(int j = 0; j < duplicateDt2.Rows.Count; j++)
          duplicateList.Add(duplicateDt2.Rows[j];

     for(int j = 0; j < duplicateDt3.Rows.Count; j++)
          duplicateList.Add(duplicateDt3.Rows[j];

     for(int j = 0; j < duplicateDt4.Rows.Count; j++)
          duplicateList.Add(duplicateDt4.Rows[j];

     //List를 순회하며 데이터가공
     for(int j = 0; j < duplicateList.Count; j++)
     {
          //....
     }
}

`

이상입니다!

2개의 좋아요
  1. CopyToDataTable() 메서드는 직접 만드신 메서드 같은데 해당 메서드에서 오래 걸리는 부분이 없는지도 체크 필요 합니다.

  2. new List()
    List에 초기 capacity 설정 없이 Add 하게 될 경우 성능에 많은 영향을 끼칠 수 있습니다.
    (이유는 찾아보시면 많이 나옵니다.)

  3. 왠만 DB로 부터 데이터를 조회 할때 중복 데이터가 제거된 데이터를 조회하여 받아 오는 것이 나을 것 같습니다.

//List를 순회하며 데이터가공
for(int j = 0; j < duplicateList.Count; j++)

데이터 가공 목적으로 또 한번 데이터를 순회 하는데
순서가 보장되지 않아도 된다면
Parallel.ForEach 같은 병렬로 처리 하시면 성능에 도움이 될 것 입니다.

6개의 좋아요

중복 내역을 확인한다

라는 말이 조금 헷갈리는 것 같아요.
추려 낸 데이터와 같은 값의 row를 찾고싶으신건가요?

만약 그렇다면
오히려, 3천건도 원래 DB Query 에서 가져오신 거라면
처음 DB Query에서 조건에 걸고 가져오시는 게 나을 수도 있어요.
그게 여력이 안 되신다면, 오히려 가상 테이블에 INSERT를 한 다음 join 하시거나요.
단 당연히 Key를 사용하신다는 전제 하에요.

public static System.Data.DataTable CopyToDataTable<T> (this System.Collections.Generic.IEnumerable<T> source) where T : System.Data.DataRow;

이라는 확장 메서드가 있어요.
하지만, @aroooong 님께서 말씀하신 대로 퍼포먼스 체크가 필요한 의견엔 동의합니다.

2개의 좋아요

System.Data.DataTable CopyToDataTable() 확장를 사용하신거면

요 메서드 또한 내부적으로 새로운 DataTable을 만들어서 Row를 순회하여 추가 하기 때문에 느릴 것 같습니다.

3개의 좋아요

퇴근 전에 한번 테스트 코드를 작성해 보았는데요 !

질문자님의 의도와 같은 코드인지는 모르겠습니다.

저는 PLINQ를 사용해서 병렬로 중복 데이터를 빠르게 추출해 보았습니다.

  1. 중복이 포함 되어 있는 전체 데이터 10만건을 가지고
  2. 중복된 데이터만 병렬 처리로 빠르게 추출 해서
  3. 출력 합니다.
static DataTable CreateDT()
{
    string[] sampleDuplicationStr = { "apple", "banana", "melon", "kiwi", "grape" };

    DataTable dt = new DataTable();
    dt.Columns.Add("id", typeof(int));
    dt.Columns.Add("fruit", typeof(string));
    dt.Columns.Add("date", typeof(string));

    foreach(var num in Enumerable.Range(0, 100000))
    {
        if (new Random().NextDouble() >= 0.5)
        {
            Random rnd = new Random();
            var idx = rnd.Next(4);
            dt.Rows.Add(num, $"{sampleDuplicationStr[idx]}{num}", DateTime.Now.ToString("HH:mm:ss.fff"));
        }
        // 중복
        else
        {
            Random rnd = new Random();
            var idx = rnd.Next(4);
            dt.Rows.Add(num, $"{sampleDuplicationStr[idx]}", DateTime.Now.ToString("HH:mm:ss.fff"));
        }
    }

    return dt;
}

// 전체 데이터 Sample
DataTable allData = CreateDT();

// 데이터 중복추출, PLINQ 사용
var duplicateDt = allData.AsEnumerable().AsParallel()
.GroupBy(row => new { Fruit = row["fruit"] })
.Where(group => group.Count() > 1)
.SelectMany(group => group);

Console.WriteLine(duplicateDt.Count());

// 추출된 중복 데이터를 순회하며 데이터가공
foreach (var duplicate in duplicateDt)
{
    Console.WriteLine($"Id : {duplicate["id"]}, Fruit: {duplicate["fruit"]}, Date: {duplicate["date"]}");
}

[결과] 속도 약 0.1s / 중복 추출 데이터 수: 49903

// 추출된 중복 데이터를 순회하며 데이터가공
부분은 아까 말씀 드렸다시피 가공 처리에 있어서 순서 보장이 필요 없다면
Parallel.ForEach 병렬 처리로 하면 더 빠른 성능이 됩니다.

(적고 보니깐 이미 PLINQ로 중복 추출 할때 순서를 보장 받지 못하네요 힣…)

9개의 좋아요

현재 방식에서 순회 횟수는

totalDt row * comparisonDt row * column 갯수

입니다.

속도를 올리려면, totalDt의 각 컬럼의 값들을 검색이 용이한 자료 구조로 변형하는 작업이 선행되는 게 좋을 것 같습니다. 예를 들면,

  • B-tree : 검색 속도 o(log(n))
  • HashTable (Dictionary<string, List>) : 검색 속도 o(1)

두 방식 중, 데이터 양에 따라 o(1) 이 더 느릴 수도 있습니다.
이렇게 변경하면, 전체적인 순회 횟수는 아래처럼 급감합니다.

{ totalDt row + Min( o(1), o(Log(totalDt row) ) * comparisonDt row } * column

참고로, 제일 앞에 있는 1회는 자료 구조에 변형에 필요한 순회로 다소 길 수 있지만 대세에 영향을 미치지 않습니다.

바이너리 트리를 작성해봤습니다.

public class BTree
{
    private BTree? _precedant;
    private BTree? _postcedant;
    private readonly string _value;
    private readonly List<int> _rows = new();

    public static BTree GetEntry(string value, int row) => new (value, row);
    private BTree(string value, int row)
    {
        _value = value; _rows.Add(row);
    }

    public void Insert(string value, int row)
    {        
        if (value.Equals(_value))
        {
            _rows.Add(row); return;
        }

        if (string.Compare(value, _value, StringComparison.InvariantCulture) < 0)
        {
            if(_precedant == null)
            {
                _precedant = new(value, row); return;
            }

            _precedant.Insert(value, row); return;
        }

        if(_postcedant == null)
        {
            _postcedant = new(value, row); return;
        }

        _postcedant.Insert(value, row);
    }

    public bool TryFind(string value, ref List<int> rows)
    {
        if (value.Equals(_value))
        {
            rows.AddRange(_rows); return true;
        }

        if (string.Compare(value, _value, StringComparison.InvariantCulture) < 0)
        {
            return _precedant != null && _precedant.TryFind(value, ref rows);
        }
        
        return _postcedant != null && _postcedant.TryFind(value, ref rows);
    }


    public static void Test()
    {
        // Set compareValues
        var valuesToCompare = new string[3000];

        var rnd = new Random();
        
        for (int i = 0; i < valuesToCompare.Length; i++)
        {
            var val = Guid.NewGuid().ToString()[0..rnd.Next(32)];
            valuesToCompare[i] = val;
        }

        // Set Data
        var data = new string[50000];
        for (int i = 0; i < data.Length; i++)
        {
            data[i] = Guid.NewGuid().ToString()[0..rnd.Next(32)];
        }

        // Convert data into Btree
        var btree = GetEntry(data[0], 0);

        for (int i = 1; i < data.Length; i++)
        {
            btree.Insert(data[i], i);
        }

        var result = new Dictionary<string, List<int>>();

        // Test
        var watch = new Stopwatch();

        watch.Start();
        for (int i = 0; i < valuesToCompare.Length; i++)
        {
            var rows = new List<int>();
            var value = valuesToCompare[i];
            if(btree.TryFind(value, ref rows))
            {
                result[value] = rows;
            }
        }
        Console.WriteLine($"Time: {watch.ElapsedMilliseconds} ms");
        watch.Stop();


        // Print results
        foreach (var item in result)
        {
            Console.WriteLine($"{item.Key} : {string.Join(',', item.Value)}");
        }

    }
}

Test 는 하나의 컬럼을 가정한 것으로, 제 컴에서는 20 ms 정도 나오네요.

참고로, 자료가 정렬 상태라면, BTree는 뻘짓입니다.
HashTable 계열인 Dictionary<string, List<int>> 에 저장해야 합니다.

5개의 좋아요

안녕하세요! 선생님께서 답변해주신 내용으로 테스트 진행중입니다. 정말 도움이 많이 된 것 같아 감사드립니다!!

염치없이 하나만 더 질문 해도 될까요?

// 데이터 중복추출, PLINQ 사용
var duplicateDt = allData.AsEnumerable().AsParallel()
.GroupBy(row => new { Fruit = row[“fruit”] })
.Where(group => group.Count() > 1)
.SelectMany(group => group);

여기 중복 데이터 추출할 때 allData 안에서의 중복을 찾아내는거 같은데

다른 사용자가 입력한 불특정한 3000건 정도의 데이터를 allData에서 한줄씩 중복을 찾아야됩니다.
(DataTable inputDt = new DataTable(3000))

혹시 inputDt와 allData와 PLINQ를 해서 inputDt의 id와 allData의 id를 한줄씩 뽑아 낼수있을까요 left outer 조인같은 방식으로요…

감사합니다…

2개의 좋아요

allData 의 데이터는 고유 한가요 ?

allData 데이터가 고유 하다는 조건에서

inputDT의 데이터가 allData에 속해 있는지 다음과 같이 조인으로 중복 데이터를 추출해 볼 수 있습니다.


아래 샘플 코드에서는 allData가 dtA / inputDT가 dtB로 간주 합니다.

static DataTable CreateDT_A()
{
    string[] sampleData = { "kiwi", "grape" };

    DataTable dt = new DataTable();
    dt.Columns.Add("id", typeof(int));
    dt.Columns.Add("fruit", typeof(string));
    dt.Columns.Add("date", typeof(string));

    foreach (var num in Enumerable.Range(0, 100000))
    {
        Random rnd = new Random();
        var idx = rnd.Next(2);
        dt.Rows.Add(num, $"{sampleData[idx]}-{num}", DateTime.Now.ToString("HH:mm:ss.fff"));
    }

    return dt;
}

static DataTable CreateDT_B(DataTable referenceDT)
{
    DataTable dt = new DataTable();
    dt.Columns.Add("id", typeof(int));
    dt.Columns.Add("fruit", typeof(string));
    dt.Columns.Add("date", typeof(string));

    foreach (var num in Enumerable.Range(0, 100000))
    {
        if (new Random().NextDouble() >= 0.5)
        {
            dt.Rows.Add(num, null, DateTime.Now.ToString("HH:mm:ss.fff"));
        }
        // 중복
        else
        {
            dt.Rows.Add(num, $"{referenceDT.Rows[num]["fruit"]}", DateTime.Now.ToString("HH:mm:ss.fff"));
        }
    }

    return dt;
}

// 고유한 과일 순번 데이터
var dtA = CreateDT_A();

// 무작위 랜덤 확율로 중복
var dtB = CreateDT_B(dtA);

var watch = new Stopwatch();
watch.Start();

// 두개의 DT 조인, 중복 데이터 추출
var resultDt = from rowA in dtA.AsEnumerable().AsParallel()
               join rowB in dtB.AsEnumerable().AsParallel()
               on rowA.Field<string>("fruit") equals rowB.Field<string>("fruit") into resultRow
               from row in resultRow
               select row;

foreach (var row in resultDt)
{
    Console.WriteLine($"중복 과일 : {row["fruit"]}");
}
Console.WriteLine($"중복 추출 데이터 수: {resultDt.Count()}");

watch.Stop();

Console.WriteLine($"소요시간: {watch.ElapsedMilliseconds} ms");

[결과]

2개의 좋아요