如何使用LINQ获取每个键的一行数据

9
我有一个包含以下内容的列表:
EMP_ID | UPDATED_DATE | MARK
------ | ------------ | ----
111    | 01/01/2015   | 99  
111    | 01/01/2013   | 85  
111    | 01/01/2017   | 80  
222    | 01/01/2011   | 70  
222    | 01/01/2015   | 55  
222    | 01/01/2002   | 60  
我需要选择每个ID的最新UPDATED_DATE对应的一行。 在我们的等式中,结果如下:
EMP_ID | UPDATED_DATE | MARK
------ | ------------ | ----
111    | 01/01/2017   | 80  
222    | 01/01/2015   | 55  
这是按顺序排列的代码:
empMarksList.OrderBy(x=>x.EMP_ID).ThenBy(y=>y.UPDATED_DATE)

1
empMarksList.GroupBy(x => x.Id).Select(x=>x.OrderByDescending(y=>y.UPDATED_DATE).First()) - peinearydevelopment
1
OrderBy(x => x.Id)中的Id从哪里来? - Mong Zhu
不幸啊,你的答案在哪里??那是一个很棒的答案!! - user1012506
1
可能是重复的问题:为每个ID选择具有最大日期的值 - Drag and Drop
你在这里使用EntityFramework吗?我可以看到它在标签中。 - arekzyla
4个回答

11

使用GroupBy

var items = empMarksList
                   .GroupBy(e => e.EMP_ID)
                   .Select(grp => grp.OrderByDescending(v => v.UPDATED_DATE).First());

或者,如果你需要一个字典:

var dict = empMarksList
              .GroupBy(e => e.EMP_ID)
              .ToDictionary(grp => grp.Key,
                            grp => grp.OrderByDescending(v => v.UPDATED_DATE).First());

7
在这段代码中,.First() 是完全可以的,因为 .GroupBy 始终保证至少有一个结果。 - Enigmativity
@Enigmativity 我遇到了一个异常:'The method 'First' can only be used as a final query operation. Consider using the method 'FirstOrDefault' in this instance instead'. 对于Linq-To-Entities,这种方法不起作用。此外,GROUP BY不会从此查询中生成,而只有类似于WHERE inner.Id = outer.IdOUTER APPLY - arekzyla
@arekzyla 这个查询不应该有影响,这里是一个工作示例实现 - Marie
@AmirPopovich 我不是100%确定,但检查Max应该比OrderByDescending更快,对吧?例如:grp.First(v => v.UPDATED_DATE == grp.Max(g => g.UPDATED_DATE)) - Marie
@Marie但是您的示例使用的是Linq-to-Objects,而OP使用的是EntityFramework(如您所见的标签),因此在Linq-To-Entities提供程序中,您只能将First用作最终查询操作。 - arekzyla

3
我更喜欢这个变体,但它和Amir的答案是一样的。
var query =
    empMarksList
        .GroupBy(x => x.EMP_ID)
        .SelectMany(x => x.OrderByDescending(y => y.UPDATED_DATE).Take(1));

首选,仅获取第一个结果而不执行完整结果然后返回第一个项目。 - Incredible
1
@ItiTyagi - .First().Take(1)都只返回第一个项目,它们都不执行完整的结果。.Take(1)更好,因为如果它之前的查询返回一个空列表,它不会抛出异常。 - Enigmativity
“因为如果它之前的查询返回了”这句话是什么意思? - Incredible
2
如果您担心空列表,可以使用FirstOrDefault,它具有相同的结果,但在语义上更正确。在这种情况下,您的枚举由GroupBy返回,我99%确定GroupBy不会返回空列表。那是没有任何意义的。 - Marie
@ItiTyagi - 我只是假设性地评论了“如果查询在返回空列表之前”,在这种情况下它不可能,但如果插入.Where,那么就有可能了,最好养成一个好习惯。.FirstOrDefault将提供不同的语义。 - Enigmativity
@Marie - 如果在.Take(1)之前的部分未生成任何值,则.FirstOrDefault将提供不同的语义。只需插入一个.Where子句即可中断代码。 - Enigmativity

3
另一个选择是:
var items = context.EmpMarks
    .GroupBy(e => e.EMP_ID, (k, g) => g
        .FirstOrDefault(e => g.Max(v => v.UPDATED_DATE) == e.UPDATED_DATE));

这实际上应该在 SQL 中生成 GROUP BY


1
你可以使用类似这样的代码:
var result = empMarksList.GroupBy(x => x.Id)
    .Select(g => 
        g.Aggregate((a, x) => a == null || a.UPDATRED_DATE < x.UPDATRED_DATE ? x : a));

使用OrderBy比这种方式更加繁琐,但是通过这种方式,您不会对所有的子集合进行排序,这在这里有点过度杀伤力,并且使用更多资源。

编辑: 在@arekzyla的回答之后,我意识到我的选项也可以写成这样:

var items = empMarksList.GroupBy(
   x => x.Id,
   (k, g) => g.Aggregate((a, x) => a == null || a.UPDATRED_DATE < x.UPDATRED_DATE ? x : a));

这样写可能不太易读,但在大多数情况下,只需要一次子集合遍历,而不是两次,差异微不足道。

我不确定在什么情况下生成的SQL更优,因此值得检查。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接