最大还是默认?

205
什么是从可能返回零行的LINQ查询中获取最大值的最佳方法?如果我只是这样做
Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter).Max

当查询没有返回任何行时,我会遇到一个错误。我可以这样处理。
Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter _
         Order By MyCounter Descending).FirstOrDefault

但对于这样一个简单的请求来说,这感觉有点笨拙。我是不是错过了更好的方法?

为什么这个标签被标记为C#,明明大部分答案都是VB,并且原始问题也是? - micsthepick
17个回答

219

由于在LINQ to SQL中未实现DefaultIfEmpty,我搜索了它返回的错误,并发现了一篇有趣的文章,涉及聚合函数中的空集。总结一下,您可以通过在选择语句中转换为可空类型来避免此限制。我的VB语言有点生疏,但我认为它应该是这样的:

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select CType(y.MyCounter, Integer?)).Max

或者在 C# 中:

var x = (from y in context.MyTable
         where y.MyField == value
         select (int?)y.MyCounter).Max();

1
为了纠正VB,选择应该是“Select CType(y.MyCounter,Integer?)”。我必须进行原始检查以将Nothing转换为0以满足我的需求,但我喜欢在没有异常的情况下获得结果。 - gfrizzle
3
LINQ to SQL支持DefaultIfEmpty的两个重载方法之一 - 不带参数的那个。 - DamienG
可能这些信息已经过时了,因为我刚刚在LINQ to SQL中成功测试了DefaultIfEmpty的两种形式。 - Neil
6
@Neil:请回答。DefaultIfEmpty 对我没有用:我想要一个DateTime的最大值。使用 Max(x => (DateTime?)x.TimeStamp) 仍然是唯一的方法。 - duedl0r
1
虽然DefaultIfEmpty现在已经在LINQ to SQL中实现,但在我看来,使用DefaultIfEmpty会导致SQL语句“SELECT MyCounter”返回每个被求和值的一行,而这个答案会返回MAX(MyCounter)的单个求和行,因此这个答案更好。(在EntityFrameworkCore 2.1.3中测试过。) - Carl Sharman

128

我之前遇到过类似的问题,但是我使用的是列表上的LINQ扩展方法而不是查询语法。在那里将类型转换为可空类型也同样有效:

int max = list.Max(i => (int?)i.MyCounter) ?? 0;

55
听起来需要使用DefaultIfEmpty(以下是未经测试的代码):
Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter).DefaultIfEmpty.Max

我不熟悉DefaultIfEmpty,但是当我使用上述语法时,我收到了“无法将节点'OptionalValue'格式化为SQL以执行”的错误提示。我还尝试提供默认值(零),但它也不喜欢。 - gfrizzle
啊。看起来在LINQ到SQL中不支持DefaultIfEmpty。但你可以先通过.ToList强制转换为列表,然后绕过这个问题,但这会显著降低性能。 - Jacob Proffitt
3
谢谢,这正是我在寻找的。使用扩展方法:var colCount = RowsEnumerable.Select(row => row.Cols.Count).DefaultIfEmpty().Max() - Jani

37

考虑一下你在请求什么!

{1, 2, 3, -1, -2, -3}的最大值显然是3。{2}的最大值显然是2。但是空集合{ }的最大值是多少呢?很显然这个问题是没有意义的。空集合的最大值根本没有定义。试图得到答案是数学上的错误。任何一个集合的最大值必须是该集合中的元素。空集合没有元素,因此声称某个特定数字是该集合的最大值而不在该集合中是一个数学矛盾。

当程序员要求计算机除以零时,计算机抛出异常是正确的行为,就像当程序员要求计算机取空集合的最大值时,计算机抛出异常也是正确的行为。除以零,取空集合的最大值,wiggering the spacklerorke(无实际含义),和骑着飞天独角兽去永无岛,这些都是没有意义、不可能、未定义的。

现在,你究竟想做什么呢?


18
我经常尝试骑着我的独角兽飞往梦幻岛,你对我的努力毫无意义和定义的建议让我感到不满。 - Chris Shouts
2
我认为这个论点是错误的。很明显是linq-to-sql,而在sql中,零行的Max被定义为null,不是吗? - duedl0r
4
通常情况下,Linq 应该会产生相同的结果,无论查询是在内存中针对对象执行还是在数据库中针对行执行。Linq 查询就是 Linq 查询,应该忠实地执行,无论使用哪种适配器。 - yfeldblum
1
虽然我理论上同意 Linq 的结果无论在内存中还是在 SQL 中执行都应该是相同的,但当你深入挖掘时,你会发现为什么这并不总是如此。Linq 表达式使用复杂的表达式翻译被转换成 SQL。这不是简单的一对一翻译。一个区别是 null 的情况。在 C# 中,“null == null” 是 true。在 SQL 中,“null == null” 匹配包括外连接但不包括内连接。然而,内连接几乎总是你想要的,所以它们是默认的。这可能导致行为上的差异。 - Curtis Yallop
1
@Kyle - 这完全是错误的。用更简单的英语来表达这个问题:“这些东西中哪一个最大?”如果没有任何东西,那么就没有其中任何一个是最大的,而且无法回答所提出的问题。在这种情况下,我们不会想出一个中等大小的东西,然后指着它说“这个东西最大”。不,我们只会说“没有东西,所以无法回答这个问题”。 - yfeldblum
显示剩余3条评论

26

你可以始终将Double.MinValue添加到序列中。这将确保至少有一个元素,并且在实际上是最小值时Max仅返回它。要确定哪个选项更有效率(ConcatFirstOrDefault还是Take(1)),你应该进行充分的基准测试。

double x = context.MyTable
    .Where(y => y.MyField == value)
    .Select(y => y.MyCounter)
    .Concat(new double[]{Double.MinValue})
    .Max();

11

从.Net 3.5开始,您可以使用DefaultIfEmpty()将默认值作为参数传递。 可以像以下其中一种方式之一:

int max = (from e in context.Table where e.Year == year select e.RecordNumber).DefaultIfEmpty(0).Max();
DateTime maxDate = (from e in context.Table where e.Year == year select e.StartDate ?? DateTime.MinValue).DefaultIfEmpty(DateTime.MinValue).Max();

第一种情况是在查询NOT NULL列时可以使用,第二种情况是用来查询可为空的列。如果您在使用DefaultIfEmpty()时没有提供参数,则默认值将是输出类型定义的默认值,可以在默认值表中查看。

生成的SELECT语句可能不会那么优雅,但是它是可接受的。

希望能够帮到您。


11
int max = list.Any() ? list.Max(i => i.MyCounter) : 0;
如果列表有任何元素(即不为空),它将取MyCounter字段的最大值,否则将返回0。

7
我认为问题在于当查询没有结果时你希望发生什么。如果这是一个异常情况,那么我会将查询包装在 try/catch 块中,并处理标准查询生成的异常。如果查询返回无结果是可以接受的,那么你需要确定在这种情况下你希望得到的结果是什么。也许 @David 的答案(或类似的答案)可以解决问题。即,如果 MAX 始终为正数,则将已知的“坏”值插入列表中,只有在没有结果时才会选择该值。通常,我期望检索最大值的查询具有一些要处理的数据,否则你总是被迫检查所获得的值是否正确。我宁愿非异常情况能够直接使用获得的值。
Try
   Dim x = (From y In context.MyTable _
            Where y.MyField = value _
            Select y.MyCounter).Max
   ... continue working with x ...
Catch ex As SqlException
       ... do error processing ...
End Try

在我的情况下,返回零行的情况比不返回更为频繁(遗留系统,患者可能有或没有先前的资格,等等)。如果这是一个更为特殊的情况,我可能会选择这种方法(而且我仍然可能会这样做,因为没有看到更好的方法)。 - gfrizzle

6

有点晚了,但我也有同样的疑问...

重新构造你原始帖子中的代码,你想要集合S的最大值,它被定义为

(From y In context.MyTable _
 Where y.MyField = value _
 Select y.MyCounter)

考虑到您最近的评论

可以说我知道当没有记录可选择时,我想要0,这肯定会对最终的解决方案产生影响

我可以重新表述您的问题为:您想要{0 + S}的最大值。看起来,使用concat提出的解决方案在语义上是正确的 :-)

var max = new[]{0}
          .Concat((From y In context.MyTable _
                   Where y.MyField = value _
                   Select y.MyCounter))
          .Max();

6

另一个可能性是分组,类似于您在原始SQL中的处理方式:

from y in context.MyTable
group y.MyCounter by y.MyField into GrpByMyField
where GrpByMyField.Key == value
select GrpByMyField.Max()

唯一的问题是(在LINQPad中再次测试),切换到VB LINQ风格会在分组子句上产生语法错误。我相信概念上的等效物很容易找到,只是我不知道如何在VB中反映它。

生成的SQL大致如下:

SELECT [t1].[MaxValue]
FROM (
    SELECT MAX([t0].[MyCounter) AS [MaxValue], [t0].[MyField]
    FROM [MyTable] AS [t0]
    GROUP BY [t0].[MyField]
    ) AS [t1]
WHERE [t1].[MyField] = @p0

嵌套的SELECT看起来很麻烦,就像查询执行会检索所有行,然后从检索到的集合中选择匹配的行...问题是SQL Server是否会将查询优化为类似于将where子句应用于内部SELECT。我正在研究这个问题...
我不太擅长解释SQL Server中的执行计划,但看起来当WHERE子句在外部SELECT上时,该步骤产生的实际行数为表中的所有行,而当WHERE子句在内部SELECT上时,只有匹配的行才会产生。话虽如此,当考虑所有行时,成本只有1%转移到以下步骤,无论哪种方式,只有一行从SQL Server返回,因此在大局中可能没有太大的差别。

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