最大还是默认?

从一个可能不返回行的LINQ查询中获得Max值的最佳方法是什么?如果我这么做

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

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

更新:下面是背景故事:我正在尝试从子表(遗留系统,不要让我开始…)中检索下一个资格计数器。每个患者的第一个合格行总是1,第二个是2,等等(显然这不是子表的主键)。因此,我为一个病人选择最大的现有计数器值,然后加上1来创建一个新行。当没有现有的子值时,我需要查询返回0(因此添加1将给我一个1的计数器值)。注意,我不想依赖于原始的子行计数,以防遗留应用程序在计数器值中引入空白(可能)。我的错是试图把这个问题弄得太笼统了。

114377 次浏览

你总是可以将Double.MinValue添加到序列中。这将确保至少有一个元素,并且Max只在它实际上是最小值时才会返回它。为了确定哪个选项更有效(ConcatFirstOrDefaultTake(1)),您应该执行足够的基准测试。

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

一个有趣的区别似乎值得注意的是,当FirstOrDefault和Take(1)生成相同的SQL(根据LINQPad,无论如何),当没有匹配的行和Take(1)返回没有结果时,FirstOrDefault返回一个值——默认值……至少在LINQPad中是这样。

听起来像是DefaultIfEmpty的情况(以下是未经测试的代码):

Dim x = (From y In context.MyTable _
Where y.MyField = value _
Select y.MyCounter).DefaultIfEmpty.Max

另一种可能是分组,类似于你在原始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返回,所以可能在大方案中没有那么大的差异。

我认为问题在于,当查询没有结果时,您希望发生什么。如果这是一个例外情况,那么我将把查询包装在一个try/catch块中,并处理标准查询生成的异常。如果可以让查询不返回结果,那么您需要确定在这种情况下希望得到什么样的结果。可能是@David的答案(或者类似的答案)。也就是说,如果MAX总是正的,那么在列表中插入一个已知的“坏”值就足够了,只有在没有结果的情况下才会选择它。一般来说,我希望检索最大值的查询有一些数据要处理,我将走try/catch路线,否则您总是被迫检查获得的值是否正确。我宁愿在非例外情况下只能使用得到的值。

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

由于DefaultIfEmpty没有在LINQ to SQL中实现,我对它返回的错误进行了搜索,并找到了一个处理聚合函数中的空集的有趣的文章。总结一下我的发现,您可以通过在选择范围内强制转换为可空值来绕过这个限制。我的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,2,3, -1, -2, -3}的最大值显然是3。{2}的最大值显然是2。但是空集合{}的最大值是多少呢?显然,这是一个毫无意义的问题。空集的最大值没有定义。试图得到答案是一个数学错误。任何集合的最大值本身必须是该集合中的一个元素。空集合没有元素,因此声称某个特定的数字是该集合的最大值,而不属于该集合,这是一个数学矛盾。

就像程序员要求计算机除0时计算机抛出异常是正确的行为一样,程序员要求计算机取空集的最大值时计算机抛出异常也是正确的行为。除以0,取空集的最大值,摆动散填板,骑着飞行的独角兽去梦幻岛,这些都是毫无意义的,不可能的,没有定义的。

现在,你实际上想要做什么?

有点晚了,但我也有同样的担心…

改写原文章中的代码,你想要的是由定义的集合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();

只是让每个人都知道,使用Linq实体上面的方法将不起作用…

如果你想做什么

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

它将抛出一个异常:

系统。LINQ to Entities不支持LINQ表达式节点类型“NewArrayInit”。

我的建议是

(From y In context.MyTable _
Where y.MyField = value _
Select y.MyCounter))
.OrderByDescending(x=>x).FirstOrDefault());

如果列表为空,FirstOrDefault将返回0。

我只是有一个类似的问题,但我是在一个列表上使用LINQ扩展方法,而不是查询语法。转换为Nullable的技巧在这里也适用:

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

为什么不直接点呢?

Dim x = context.MyTable.Max(Function(DataItem) DataItem.MyField = Value)
decimal Max = (decimal?)(context.MyTable.Select(e => e.MyCounter).Max()) ?? 0;
int max = list.Any() ? list.Max(i => i.MyCounter) : 0;

如果列表中有任何元素(例如。非空),它将取MyCounter字段的最大值,否则将返回0。

我只是有一个类似的问题,我的单元测试通过使用Max(),但失败时,对一个实时数据库运行。

我的解决方案是将查询从正在执行的逻辑中分离出来,而不是将它们连接到一个查询中 我需要一个解决方案,在单元测试中使用Linq-objects(在Linq-objects Max()工作时使用null)和Linq-sql在活动环境中执行。< / p >

(我在测试中模拟Select())

var requiredDataQuery = _dataRepo.Select(x => new { x.NullableDate1, .NullableDate2 });
var requiredData.ToList();
var maxDate1 = dates.Max(x => x.NullableDate1);
var maxDate2 = dates.Max(x => x.NullableDate2);

低效率的?可能。

我在乎吗,只要我的应用下次不会崩溃?不。

从。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列时允许使用第一种方法,而在查询NULLABLE列时使用第二种方法。如果你使用不带参数的DefaultIfEmpty(),默认值将是定义为输出类型的值,正如你在默认值中看到的那样。

结果的SELECT将不是那么优雅,但它是可以接受的。

希望能有所帮助。

我已经创建了一个MaxOrDefault扩展方法。它没有太多内容,但它在智能感知中的存在是一个有用的提醒,即空序列上的Max将导致异常。此外,如果需要,该方法允许指定默认值。

    public static TResult MaxOrDefault<TSource, TResult>(this
IQueryable<TSource> source, Expression<Func<TSource, TResult?>> selector,
TResult defaultValue = default (TResult)) where TResult : struct
{
return source.Max(selector) ?? defaultValue;
}

对于实体框架和Linq to SQL,我们可以通过定义一个扩展方法来实现这一点,该扩展方法修改传递给IQueryable<T>.Max(...)方法的Expression:

static class Extensions
{
public static TResult MaxOrDefault<T, TResult>(this IQueryable<T> source,
Expression<Func<T, TResult>> selector)
where TResult : struct
{
UnaryExpression castedBody = Expression.Convert(selector.Body, typeof(TResult?));
Expression<Func<T, TResult?>> lambda = Expression.Lambda<Func<T,TResult?>>(castedBody, selector.Parameters);
return source.Max(lambda) ?? default(TResult);
}
}

用法:

int maxId = dbContextInstance.Employees.MaxOrDefault(employee => employee.Id);
// maxId is equal to 0 if there is no records in Employees table

生成的查询是相同的,它的工作方式就像对IQueryable<T>.Max(...)方法的正常调用一样,但如果没有记录,它将返回T类型的默认值,而不是抛出异常