【问题标题】:Row_number over (Partition by xxx) in Linq?Linq中的行号超过(按xxx分区)?
【发布时间】:2012-04-02 16:59:52
【问题描述】:

我有一个DataTable,它有一个结构和数据:

id |   inst   |   name
------------------------
 1 |  guitar  |  john
 2 |  guitar  |  george
 3 |  guitar  |  paul
 4 |  drums   |  ringo
 5 |  drums   |  pete

我可以通过以下方式检索记录:

IEnumerable <Beatle>...

class Beatle
{
  int id;
  string inst;
  string name;
}

我想了解演奏不同乐器的人的内部顺序。在 MSSQL 中我会使用

SELECT 
    *
    ,Row_Number() OVER (PARTITION BY inst ORDER BY id) AS rn
FROM Beatles

这个查询返回

id |   inst   |   name  | rn
-----------------------------
 1 |  guitar  |  john   | 1
 2 |  guitar  |  george | 2
 3 |  guitar  |  paul   | 3
 4 |  drums   |  ringo  | 1
 5 |  drums   |  pete   | 2

问题:
我怎样才能在 Linq 中做到这一点?

【问题讨论】:

  • Linq2SQL 还是 EF?不确定这是否会有所不同:)
  • 如果我理解正确的话,考虑到带有索引参数的 Select 重载,这应该不难。

标签: c# linq .net-4.0


【解决方案1】:

试试这个班轮:

var o = beatles
    .OrderBy( x => x.id )
    .GroupBy( x => x.inst )
    .Select( group => new { Group = group, Count = group.Count() } )
    .SelectMany( groupWithCount =>
        groupWithCount.Group.Select( b => b)
        .Zip(
            Enumerable.Range( 1, groupWithCount.Count ),
            ( j, i ) => new { j.inst, j.name, RowNumber = i }
        )
    );

foreach (var i in o)
{
    Console.WriteLine( "{0} {1} {2}", i.inst, i.name, i.RowNumber );
}

输出:

Guitar John 1
Guitar George 2
Guitar Paul 3
drums Ringo 1
drums Pete 2

【讨论】:

  • @c.sokun :“t”是一个变量,它保存在.Select 语句中生成的匿名类型。所以保持分组的计数。
  • 我很好奇这会生成什么样的 SQL,如果 EF 支持的话。
  • 注意.GroupBy,因为 .NET 字符串比较区分大小写,而 SQL 则不区分。如果您有guitarGuitar,则与SQL 相比,您将得到不同的结果。请参阅此 SO 帖子以了解对字符串进行分组时的选项:stackoverflow.com/questions/16190180/…
  • 这是我见过的单排中最多的线条
  • 这仅适用于 Linq to object,但不适用于 Linq to Entities。无法转换 Zip。
【解决方案2】:

B"H

我知道这是旧的。但为什么不是简单的解决方案呢?

var o = beatles.GroupBy(x => x.inst)
               .SelectMany(g =>
                   g.Select((j, i) => new { j.inst, j.name, rn = i + 1 })
               );

【讨论】:

  • 如果需要明确的顺序,那么代码可以扩展为code var o = beatles .GroupBy(x => x.inst) .SelectMany(g => g.OrderBy(x = > x.name).Select((j, i) => new { j.inst, j.name, rn = i + 1 })); code
  • 这仅适用于 Linq to object,但不适用于 Linq to Entities。
【解决方案3】:

如果可能的话,另一个想法是使用视图。

【讨论】:

    【解决方案4】:

    正如@The_Smallest 指出的那样,LINQ 不支持行号。不过,您可以通过以下方式获得所需内容:

    var grouped = beatles.OrderBy( x => x.id )
      .ToList()   // required because SelectMany below doesn't evaluate to SQL
      .GroupBy( x => x.inst );
    var rns = grouped.ToDictionary( x => x.Key, x => 1 );
    var result = grouped
      .SelectMany( x => x.Select( 
        y => new { inst = y.inst, name = y.name, rn = rns[y.inst]++ } ) );
    

    【讨论】:

    • y =&gt; new { y.inst, y.name, rn = rns[y.inst]++ } 不是表达式,这就是它不会编译为 sql 查询的原因。可惜了,但是那个查询会在本地执行,没有sql的好处。
    • 好吧,你可以把它全部拖到内存中,它会在那里工作。请参阅上面的更新。
    • 是的,正如您所说,SQL 没有任何好处。如果你想利用 SQL,你必须像下面@The_Smallest 指出的那样创建一个存储过程。
    【解决方案5】:

    Linq to objects 的另一种解决方案是:

    var result = beatles
                .GroupBy(g => g.inst)
                // PARTITION BY ^^^^
                .Select(c => c.OrderBy(o => o.id).Select((v, i) => new { i, v }).ToList())
                //                   ORDER BY ^^
                .SelectMany(c => c)
                .Select(c => new { c.v.id, c.v.inst, c.v.name, rn = c.i + 1 })
                .ToList();
    

    [ C# Demo ]

    【讨论】:

      【解决方案6】:

      有些人可能会发现在您的代码中使用来获得正确的索引很有用

      .Select((item, i) => new { Item = item, Index = i })
      

      【讨论】:

        【解决方案7】:

        另一种解决方案相当于 RANK() OVER (PARTITION BY "partitionBy" ORDER BY "orderBy" DESC):

         DataTable Rank(DataTable dt, string partitionBy, string orderBy, int whichRank)
           {
        
                DataView dv = new DataView(dt);
                dv.Sort = partitionBy + ", " + orderBy + " DESC";
        
                DataTable rankDt = dv.ToTable();
                rankDt.Columns.Add("Rank");
                int rank = 1;
        
                for (int i = 0; i < rankDt.Rows.Count - 1; i++)
                {
                    rankDt.Rows[i]["Rank"] = rank;
                    DataRow thisRow = rankDt.Rows[i];
                    DataRow nextRow = rankDt.Rows[i + 1];
        
                    if (thisRow[partitionBy].ToString() != nextRow[partitionBy].ToString())
                        rank = 1;
                    else
                        rank++;
                }
        
                DataView selectRankdv = new DataView(rankDt);
                selectRankdv.RowFilter = "rank = " + whichRank;
                return selectRankdv.ToTable();
          }
        

        【讨论】:

          【解决方案8】:

          根据@Jon Comtois 的回答,如果您需要过滤掉特定的行号,您可以使用以下扩展方法;

              /// <summary>
              /// Groups and orders by the data partitioning and returns the list of data with provided rownumber
              /// It is the equivalent of SQL's ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)
              /// </summary>
              /// <typeparam name="TSource">Source type</typeparam>
              /// <typeparam name="TGroupingKey">Generic type for grouping property</typeparam>
              /// <typeparam name="TOrderKey">Generic type for ordering property</typeparam>
              /// <param name="source">Source list to be partitioned</param>
              /// <param name="groupingProperty">Grouping property</param>
              /// <param name="orderProperty">Ordering property</param>
              /// <param name="orderByAsc">Order direction</param>
              /// <param name="rowNumber">Rows to be filtered out finally</param>
              /// <returns>Partitioned list</returns>
              public static List<TSource> FilterByPartitioning<TSource, TGroupingKey, TOrderKey>(this List<TSource> source, Func<TSource, TGroupingKey> groupingProperty, Func<TSource, TOrderKey> orderProperty, bool orderByAsc = true, int rowNumber = 1)
              {
                  var orderedData = orderByAsc ? source.OrderBy(orderProperty) : source.OrderByDescending(orderProperty);
          
                  return orderedData.GroupBy(groupingProperty)
                      .Select(g => new { g, count = g.Count() })
                      .SelectMany(t => t.g.Select(b => b)
                          .Zip(Enumerable.Range(1, t.count), (source, i) => new { source, row = i }))
                      .Where(x => x.row == rowNumber)
                      .Select(x => x.source).ToList();
              }
          
              //Usage
              var result = myList.FilterByPartitioning(group => group.PropertyToGroup, order => order.PropertyToOrder, orderByAsc: false, rowNumber: 1);
          

          【讨论】:

            【解决方案9】:

            https://github.com/jurioli/Applied

            var result = data.GroupBy(a => new { a.inst }).AsPartition()
            .Over(p => p.RowNumber(), (a, value) => new { a.inst, a.name, RowNumber = value })
            .ToList();
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2019-10-01
              • 1970-01-01
              • 2016-02-13
              • 1970-01-01
              • 1970-01-01
              • 2012-04-21
              相关资源
              最近更新 更多