2009-07-25 1 views
7

많은 Google 검색 및 코드 실험을 거친 후 SQL에서 복잡한 C# LINQ-to-objects 문제를 겪었습니다. ROW_NUMBER() ... PARTITION BY 함수와 하위 쿼리 또는 두 개의 쌍으로 쉽게 해결할 수 있습니다.그룹 내 LINQ- 개체 인덱스 + 다른 그룹화 용 (별칭은 PARTITION BY와 동일한 ROW_NUMBER)

  1. 첫째, 그룹 목록을 (document.title이, 문서 기준 :

    는 여기가 목록에서 중복 문서를 제거하고 기본 요구 사항을 code--에서 할 노력하고있어, 즉,이다. (단순화 된) 클래스 정의를 다음과 같이 가정합니다.

     
    class Document 
    { 
        string Title; 
        int SourceId; // sources are prioritized (ID=1 better than ID=2) 
    }
  2. 해당 그룹 내에서 각 문서에 색인을 지정합니다 (예 : 색인 0 ==이 원본에서이 제목을 가진 첫 번째 문서, 색인 1 =이 문서와 함께 두 번째 문서). 이 소스의 제목 등). 나는 SQL에서 ROW_NUMBER()에 해당하는 것을 좋아할 것이다!

  3. 이제 색인은 2 단계에서 계산 된 (Document.Title, Index)에 의해 그룹화됩니다. 각 그룹에 대해 가장 낮은 Document.SourceId가있는 문서 하나만 반환하십시오.

1 단계 (예를 들어 codepronet.blogspot.com/2009/01/group-by-in-linq.html) 쉽게,하지만 난 단계 # 2, # 3에 난처한 상황에 빠진는군요. 3 단계 모두를 해결하기 위해 붉은 색 삐뚤어지지 않는 C# LINQ 쿼리를 만들 수 없습니다.

this thread에 대한 Anders Heilsberg의 게시물은 올바른 구문을 얻을 수 있다면 위의 2 단계와 3 단계에 대한 대답이라고 생각합니다.

나는 인덱스 계산을 수행하기 위해 외부 로컬 변수를 사용하지 않는 것을 선호합니다 (slodge.blogspot.com/2009/01/adding-row-number-using-linq-to-objects.html에서 권장 됨) 외부 변수가 수정되면 해당 솔루션이 중단되기 때문입니다.

최적으로 그룹 별 제목 단계가 먼저 수행 될 수 있으므로 "내부"그룹화 (처음에는 소스를 기준으로 색인을 계산 한 다음 색인을 사용하여 중복을 필터링 함) 각 개체의 작은 수로 작업 할 수 있습니다 "제목 별"그룹입니다. 각 제목 별 그룹의 문서 수는 보통 100 개 미만이므로 N을 원하지 않습니다. N 솔루션!

이 문제는 중첩 된 foreach 루프를 사용하여 확실히 해결할 수 있지만 LINQ를 사용하면 간단해야하는 문제인 것처럼 보입니다.

아이디어가 있으십니까?

답변

5

jpbochi는 그룹핑을 값 쌍 (제목 + SourceId, 제목 + 색인)으로 바꾸기를 원하지 않는다고 생각합니다. 여기에 LINQ 쿼리의 (주로) 솔루션 : 제목 + SourceId (컴파일러가 그룹화 조회에 좋은 해시 코드를 기반으로하기 때문에 익명 형식을 사용)에 의해

var selectedFew = 
    from doc in docs 
    group doc by new { doc.Title, doc.SourceId } into g 
    from docIndex in g.Select((d, i) => new { Doc = d, Index = i }) 
    group docIndex by new { docIndex.Doc.Title, docIndex.Index } into g 
    select g.Aggregate((a,b) => (a.Doc.SourceId <= b.Doc.SourceId) ? a : b); 

먼저 그룹. 그런 다음 선택을 사용하여 그룹화 된 색인을 문서에 첨부합니다.이 색인은 두 번째 그룹에서 사용합니다. 마지막으로 각 그룹에 대해 가장 낮은 SourceId를 선택합니다. 이 입력을 감안할 때

는 :

{ Doc = { Title = ABC, SourceId = 0 }, Index = 0 } 
{ Doc = { Title = 123, SourceId = 5 }, Index = 0 } 
{ Doc = { Title = 123, SourceId = 5 }, Index = 1 } 
{ Doc = { Title = 123, SourceId = 7 }, Index = 2 } 

업데이트 : 난 그냥 처음 제목에 의해 그룹화에 대한 질문을보고

var docs = new[] { 
    new { Title = "ABC", SourceId = 0 }, 
    new { Title = "ABC", SourceId = 4 }, 
    new { Title = "ABC", SourceId = 2 }, 
    new { Title = "123", SourceId = 7 }, 
    new { Title = "123", SourceId = 7 }, 
    new { Title = "123", SourceId = 7 }, 
    new { Title = "123", SourceId = 5 }, 
    new { Title = "123", SourceId = 5 }, 
}; 

나는이 출력을 얻을. 당신은 당신의 제목 그룹에 하위 쿼리를 사용하여이 작업을 수행 할 수 있습니다

var selectedFew = 
    from doc in docs 
    group doc by doc.Title into titleGroup 
    from docWithIndex in 
     (
      from doc in titleGroup 
      group doc by doc.SourceId into idGroup 
      from docIndex in idGroup.Select((d, i) => new { Doc = d, Index = i }) 
      group docIndex by docIndex.Index into indexGroup 
      select indexGroup.Aggregate((a,b) => (a.Doc.SourceId <= b.Doc.SourceId) ? a : b) 
     ) 
    select docWithIndex; 
+0

안녕하세요 DahlbyK - 이거 멋지 네요! 귀하의 솔루션이 좋아 보인다. 이제는 처음으로 나 자신을 알아낼 수 없다는 것에 대해 나쁘게 생각하지 않습니다. Select-with-index 과부하를 발견했지만 LINQ 쿼리로 가져 오는 방법을 알 수 없습니다. 가능한 한도 내에서 도움과 교육을 주셔서 감사합니다. –

3

솔직히 말해서, 나는 당신의 질문에 상당히 혼란스러워합니다. 어쩌면 당신이 해결하려고하는 것이 무엇인지 설명해야한다면. 어쨌든, 내가 이해 한 것에 대답하려고 노력할 것입니다.

1) 먼저 문서 목록을 Title + SourceId으로 그룹화했다고 가정합니다. 테스트 목적을 위해, 나는 다음과 같은 목록 하드 코딩 : 모든 항목의 인덱스를 넣어 얻으려면

var docs = new [] { 
    new { Title = "ABC", SourceId = 0 }, 
    new { Title = "ABC", SourceId = 4 }, 
    new { Title = "ABC", SourceId = 2 }, 
    new { Title = "123", SourceId = 7 }, 
    new { Title = "123", SourceId = 5 }, 
}; 

2), 당신은 Func을 선택 기능을 전달하는 Select 확장 방법을 사용할 수 있습니다. 이처럼 : 내가 이해 한 것과

var docsWithIndex 
    = docs 
    .Select((d, i) => new { Doc = d, Index = i }); 

3), 다음 단계는 그룹에 Title에 의해 마지막 결과가 될 것입니다.그 방법은 다음과 같습니다

var docsGroupedByTitle 
    = docsWithIndex 
    .GroupBy(a => a.Doc.Title); 

(위 사용됨) GROUPBY 기능이 IEnumerable<IGrouping<string,DocumentWithIndex>>를 반환합니다. 그룹도 열거 가능하기 때문에 이제 열거 형을 열거 할 수 있습니다.

4) 위의 각 그룹에 대해 최소 SourceId 인 항목 만 가져옵니다. 이 작업을 수행하려면 2 단계의 재귀가 필요합니다. LINQ에서, 외부 레벨의 선택은 (각 그룹에 대해, 그 항목 중 하나를 얻을 수)이고, 내부 레벨은 응집 (최저 SourceId 가진 항목을 가져 오기)이다

var selectedFew 
    = docsGroupedByTitle 
    .Select(
     g => g.Aggregate(
      (a, b) => (a.Doc.SourceId <= b.Doc.SourceId) ? a : b 
     ) 
    ); 

단지 확인에 해당 작품, 나는 간단한 foreach으로 테스트 :

foreach (var a in selectedFew) Console.WriteLine(a); 
//The result will be: 
//{ Doc = { Title = ABC, SourceId = 0 }, Index = 0 } 
//{ Doc = { Title = 123, SourceId = 5 }, Index = 4 } 

나는 그것이 당신이 원하는 무엇 확실하지 않다. 그렇지 않다면 답을 말씀해주십시오. 답을 고칠 수 있습니다. 이게 도움이 되길 바란다.

Obs : 테스트에 사용한 모든 클래스는 anonymous입니다. 따라서 DocumentWithIndex 유형을 정의 할 필요가 없습니다. 사실, 난 심지어 Document 클래스를 선언하지 않았습니다.

+0

안녕 jpochi - 달비의 솔루션은 올바른이었다.미안해. 내가 명확히하기 위해 당신에게 빨리 돌아갈 수 없었다. 이것은 스택 오버플로에 대한 나의 첫번째 질문이었고, 일요일에 2 시간 이내에 2 개의 대답을 얻지 못할 것이라고 나는 결코 예상하지 못했다. 다음에 나는 다시 빨리 체크 할 것이다! :-) 어쨌든 도움 주셔서 감사합니다. –

+0

문제 없습니다. 나는 당신이 그의 대답을 받아 들였음을 표시해야한다고 생각한다. – jpbochi

1

방법을 기반으로 구문 :

var selectedFew = docs.GroupBy(doc => new {doc.Title, doc.SourceId}, doc => doc) 
         .SelectMany((grouping) => grouping.Select((doc, index) => new {doc, index})) 
           .GroupBy(anon => new {anon.doc.Title, anon.index}) 
           .Select(grouping => grouping.Aggregate((a, b) => a.doc.SourceId <= b.doc.SourceId ? a : b)); 

은 위의가에 해당하는 방법을 기반으로 구문입니다 말할 것입니까?

+0

네, 위의 DahlbyK의 LINQ-y 구문과 같은 (정확한) 결과를 내고 있습니다. (Dahlby의 업데이트 된 쿼리를 참조하십시오.)하지만 먼저 제목별로 그룹화하는 것이 더 효율적일 수 있으므로 모든 정렬/집계가 작은 세트에서 발생할 수 있습니다. 수십억 개의 문서가 있다면 모두로드하지 않아도되므로 큰 차이가 있습니다. 한 번에 RAM에 저장하십시오. 게다가 대부분의 타이틀에는 중복이 전혀 없을 것입니다 ... BCL이 한 멤버 세트에서 정렬 및 그룹화 작업을 최적화하기를 바랍니다. :-) –

1

확장 방법을 구현했습니다. 여러 주문 조건뿐만 아니라 필드별로 여러 파티션을 지원합니다.

public static IEnumerable<TResult> Partition<TSource, TKey, TResult>(
    this IEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector, 
    Func<IEnumerable<TSource>, IOrderedEnumerable<TSource>> sorter, 
    Func<TSource, int, TResult> selector) 
{ 
    AssertUtilities.ArgumentNotNull(source, "source"); 

    return source 
     .GroupBy(keySelector) 
     .Select(arg => sorter(arg).Select(selector)) 
     .SelectMany(arg => arg); 
} 

사용법 :

var documents = new[] 
{ 
    new { Title = "Title1", SourceId = 1 }, 
    new { Title = "Title1", SourceId = 2 }, 
    new { Title = "Title2", SourceId = 15 }, 
    new { Title = "Title2", SourceId = 14 }, 
    new { Title = "Title3", SourceId = 100 } 
}; 

var result = documents 
    .Partition(
     arg => arg.Title, // partition by 
     arg => arg.OrderBy(x => x.SourceId), // order by 
     (arg, rowNumber) => new { RowNumber = rowNumber, Document = arg }) // select 
    .Where(arg => arg.RowNumber == 0) 
    .Select(arg => arg.Document) 
    .ToList(); 

결과 :

{ Title = "Title1", SourceId = 1 }, 
{ Title = "Title2", SourceId = 14 }, 
{ Title = "Title3", SourceId = 100 }