티스토리 뷰



 ※ LINQ 연관글 모음

 

테이블의 특정 컬럼 값을 기준으로 데이터를 묶어서(Grouping) 동일 컬럼 값을 가지고 있는 행의 개수(Count)를 구하거나 합계나 평균을 구하는 작업은 굳이 데이터를 모두 읽지 않더라도 SQL을 통해서 DBMS에 해당 작업을 요청하는 것으로 간편하게 수행할 수 있는 작업입니다. SQL을 사용하는 데이터베이스 뿐만아니라 XML이나 내부 오브젝트도 데이터 소스로 사용할 수 있는 LINQ에서도 group 연산자를 제공하고 있으며 GroupBy() 메소드를 통해서도 데이터 그룹 작업을 수행할 수 있습니다.

Dictionary<string, string> user_names = new Dictionary<string, string>()
    { {"hong1", "홍길동"}, {"hongsun", "홍길순"}, {"parkms", "박문수"}, {"kimsg", "김삿갓" },
      {"hwang1", "황금복"}, {"hwangso", "황소"}};
Dictionary<string, int> user_ages = new Dictionary<string, int>() 
    { { "hong1", 23 }, { "hongsun", 32 }, { "parkms", 54 }, { "kimsg", 63 },
      {"hwang1", 45 }, {"hwangso", 25}};

var qry2 = from name in user_names
           group name by name.Key[0] into namegrp
           select new { Char = namegrp.Key, Grps = namegrp };
int j = 0;
foreach (var item in qry2)
{
    Console.WriteLine(" {0} : {1} ", ++j, item.Char);
    foreach (var row in item.Grps)
    {
        Console.WriteLine(" {0} - {1}", row.Key, row.Value);
    }
}


group 다음에는 묶음의 대상을 기술합니다. by 다음에는 그룹의 기준이 되는 항목이나 항목 값의 연산식이 옵니다. 예제가 특정 항목의 첫 문자를 대상으로 한것처럼 다양한 C# 구문을 사용할 수 있습니다.  into 다음에는 묶여진 그룹의 단위를 지칭하며 이후 select 구문의 입력으로 사용합니다. select 에서는 그룹의 기준이 되는 값을 "그룹 단위".Key 속성으로 참조할 수 있고 "그룹 단위"는 묶여진 그룹의 데이터 집합을 의미합니다. 결과적으로 "foreach (var row in item.Grps)" 처럼 각 그룹 단위를 foreach로 참조해야 합니다. DBMS별로 차이가 있지만 SQL 질의에서 GROUP BY를 사용하면 데이터 뷰 또한 GROUP BY 범위로 축소되는데에 반해 LINQ에서는 뷰를 축소시키지 않고 결과에 대한 재정렬만 수행한다고 이해할 수 있습니다.


var qry2 = from name in user_names
           group name by name.Key[0] into keygrp
           select new { Char = keygrp.Key, 
               Grp1 = from grpname in keygrp
                      group grpname by grpname.Value[0] into namegrp
                      select new { namegrp.Key, namegrp } };
int j = 0;
foreach (var item in qry2)
{
    Console.WriteLine("{0} : {1} ", ++j, item.Char);
    foreach (var row in item.Grp1)
    {
        Console.WriteLine("  {0}", row.Key);
        foreach (var row2 in row.namegrp)
        {
            Console.WriteLine("    {0} - {1}", row2.Key, row2.Value);
        }
    }
}


위의 예제는 그룹화한 결과를 다른 질의의 입력으로 해서(Grp1 = from grpname in keygrp) 그룹내에서 재그룹화하는 코드입니다. 상위 단계에서는 .Key의 첫문자를 기준으로 그룹화하고 그 내부에서는 .Value의 첫 문자를 기준으로 그룹화 했습니다. 키-오브젝트로 구성된 오브젝트를 다시 키-오브젝트 형태로 내장시킨 것입니다.

 

var qry = user_names.GroupBy(name => name.Key[0])
            .OrderByDescending(namegrp => namegrp.Key)
            .Select(namegrp => new { namegrp.Key, namegrp });

int i = 0;
foreach (var item in qry)
{
    Console.WriteLine(" {0} : {1}", ++i, item.Key);
    foreach (var row in item.namegrp)
    {
        Console.WriteLine(" {0} - {1}", row.Key, row.Value);
    }
}


위의 예제는 질의 문법 대신에 메소드 문법을 사용한 것으로 GroupBy 메소드로 그룹 기준을 지정합니다. 위의 예제에서는 GroupBy 다음에 OrderBy 구문을 기술했는데 그룹화 결과인 그룹 단위에 대한 정렬 기준을 지정합니다. OrderBy가 GroupBy 이전에 오는 경우는 정렬 결과를 그룹화하므로 필요에 맞게 메소드 순서를 적절하게 적용해야 합니다.

var qry = user_names.GroupBy(name => new {name.Key, name.Value})
            .OrderByDescending(namegrp => namegrp.Key.Key)
            .Select(namegrp => new { namegrp.Key, namegrp });

int i = 0;
foreach (var item in qry)
{
    Console.WriteLine(" {0} : {1} - {2}", ++i, item.Key.Key, item.Key.Value);
    foreach (var row in item.namegrp)
    {
        Console.WriteLine("   {0} - {1}", row.Key, row.Value);
    }
}

위의 코드는 그룹의 기준으로 단일 항목이나 연산 결과가 아닌 여러 항목을 복합적으로 기준으로 삼은 예제입니다.  그룹의 기준이 단일 스트링이 아니라 집합인(item.Key.Key, item.Key.Value) 것을 확인할 수 있습니다. group으로 묶은 데이터들을 그냥 사용할 수도 있지만 개수, 합계등의 집합 연산을 질의문 차원에서 수행할 수도 있습니다. 다음은 LINQ에서 지원하는 대표적인 집합 연산자(Aggregate Operators)입니다.

  • Count
    그룹 단위.Count()로 그룹의 항목수를 얻을 수 있고, 그룹화 없이 항목수를 추출할 수도 있습니다.
    Count(itm => itm.Length < 8) 처럼 람다 구문으로 특정 조건에 해당하는 
    항목수를 추출할 수도 있습니다.

  • Sum
    숫자 배열인 경우는 배열.Sum()으로 바로 합계를 구할 수 있습니다. 숫자 배열이 아니더라도 Sum(itm.Value)처럼 Sum() 메소드의 인수로 숫자 항목을 제공하거나 숫자값으로 변환해서 제공하면 원하는 합계를 구할 수 있습니다.
    "
    select new { key = ugrp.Key, sum = ugrp.Sum(ag => ag.Value) };" 처럼 그룹별 합계를 구할 수 있는데 람다 구문으로 지정하는 합산 대상은 다양한 수식을 지정할 수도 있습니다.  주의할 점은 "sum = "처럼 함계에 대한 이름을 반드시 지정해 주어야 하는 것입니다.

  • Min
    최소값을 찾는 기능으로 Sum()에 준해서 사용합니다. 아래의 예제는 그룹화한 상태에서 각 그룹별로 가장 작은 값을 가진 데이터를 찾는 코드입니다. let 구문을 통해서 그룹별 최소값을 먼저 추출하고 "ugrp.Where"를 처럼 최소값을 가진 데이터를 구하면 됩니다.

    var qry3 = from ua in user_ages
               group ua by ua.Key into ugrp
               let minage = ugrp.Min(ug => ug.Value)
               select new { key = ugrp.Key, keyrow = ugrp.Where(ug => ug.Value == minage) };
    foreach (var row in qry3)
    {
        Console.WriteLine("  {0}", row.key);
        foreach (var row2 in row.keyrow)
        {
            Console.WriteLine("    {0} - {1}", row2.Key, row2.Value);
        }
    }
    
  • Max
    최대값을 찾는 기능으로 Min()에 준해서 사용합니다.

  • Average
    평균을 구하는 기능으로 Sum()에 준해서 사용합니다.

  • Aggregate
    위에서 소개한 여러 집합 연산자와 같은 기능을 개발자 나름으로 제작할 수 있는 기능으로 람다 구문 앞의 초기값(예제에서는 string.Empty)을 지정할 수도 하지 않을 수도 있습니다.  curr은 현재 작업 값을 의미하고 next는 그룹화 또는 전체 데이터의 해당 항목을 지정합니다. 아래의 예제는 그룹으로 묶은 이후 해당 그룹에 해당하는 값을 하나의 스트링으로 연결하는 기능입니다.

    var qry3 = from un in user_names
               group un by un.Key[0] into ugrp
               select new { key = ugrp.Key, allinone = 
                   ugrp.Aggregate(string.Empty, (curr, next) => curr + " " + next.Value)
               };
    foreach (var row in qry3)
    {
        Console.WriteLine("  {0},{1}", row.key, row.allinone);
    }
    


public class myEQCompare : IEqualityComparer
{
    public bool Equals(string x, string y)
    {
        if (x.Substring(0,1).ToUpper() == y.Substring(0,1).ToUpper()) return true;
        else return false;
    }

    public int GetHashCode(string obj)
    {
        return obj.Substring(0,1).ToUpper().GetHashCode();
    } 
}

.......

var qry = user_names.GroupBy(name => name.Key, new myEQCompare())
            .OrderByDescending(namegrp => namegrp.Key)
                .Select(namegrp => new { namegrp.Key, namegrp });

위의 코드는 그룹을 묶을 기준을 선정하는 과정에서 사용할 함수를 개발자가 제공한 예제입니다. 그룹으로 묶는 기준은 "값"의 동일성에 있으므로 IEqualityComparer를 구현하는 클래스를 작성하고 같은 경우 true를 리턴하는 Equals() 메소드와 해시코드를 리턴하는 GetHashCode() 메소드를 작성하면 됩니다. 위의 코드는 값의 첫문자가 대소문자 인지를 구별하지 않고 동일한 지를 판단하도록 한 것입니다.


var qry2 = from name in user_names
           group name by name.Key[0] into keygrp
           where keygrp.All(ki => ki.Key.Length > 5)
           select new { keygrp.Key, keygrp };

위의 코드는 그룹화 이후에 여러 그룹 중에 특정 조건에 전부 부합하는지(All) 또는 하나라도 조건에 맞는 것이 존재하는지(Any)를 걸러 내는(필터링) 예제 입니다. Any() 또는 All() 메소드에 람다 구문으로 조건을 기술하면 됩니다. 위의 예제는 키의 첫 문자를 기준으로 그룹화한 이후 각 그룹에 속한 데이터의 모든 키 값의 길이가 5를 넘는지 확인해서 모든 키 값의 길이가 5를 넘는 그룹만을 표시합니다. Any()와 All()은 물론 그룹화와 무관하게 전체 데이터에 대한 검증(true/false 리턴) 용도로도 사용할 수 있습니다.


댓글
댓글쓰기 폼