最大还是默认?

时间:2008-12-04 16:45:04

标签: c# .net linq linq-to-sql

从可能不返回任何行的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)。请注意,我不想依赖子行的原始计数,以防遗留应用程序在计数器值中引入间隙(可能)。我试图让这个问题过于通用我不好。

17 个答案:

答案 0 :(得分:202)

由于DefaultIfEmpty未在LINQ to SQL中实现,因此我对其返回的错误进行了搜索,并找到了fascinating article来处理聚合函数中的空集。总结一下我发现的内容,你可以通过在你的选择中转换为可空来解决这个限制。我的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 :(得分:95)

我遇到了类似的问题,但我在列表而不是查询语法上使用LINQ扩展方法。对Nullable技巧的投射也适用于那里:

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

答案 2 :(得分:47)

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

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

答案 3 :(得分:35)

想想你在问什么!

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

正当程序员要求它除以零时计算机抛出异常是正确的行为,因此当程序员要求它取出空的最大值时,计算机抛出异常是正确的行为组。除以零,取出空集的最大值,摆动spacklerorke,乘坐飞行的独角兽到Neverland都是毫无意义的,不可能的,未定义的。

现在,实际想要做什么?

答案 4 :(得分:24)

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

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

答案 5 :(得分:10)

从.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(),则默认值将定义为输出类型,如Default Values Table 中所示。

生成的SELECT不会那么优雅,但是可以接受。

希望它有所帮助。

答案 6 :(得分:9)

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

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

答案 7 :(得分:7)

我认为问题是当查询没有结果时你想要发生什么。如果这是一个例外情况,那么我将查询包装在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

答案 8 :(得分: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返回,所以也许它在宏观方案中没有那么大的区别

答案 9 :(得分: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();

答案 10 :(得分:3)

为什么没有更直接的东西:

Dim x = context.MyTable.Max(Function(DataItem) DataItem.MyField = Value)

答案 11 :(得分:2)

只是为了让所有人都知道使用Linq to Entities,上面的方法将无效......

如果您尝试执行类似

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

它会引发异常:

  

System.NotSupportedException:LINQ to Entities中不支持LINQ表达式节点类型'NewArrayInit ..

我建议你做什么

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

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

答案 12 :(得分:1)

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

答案 13 :(得分:1)

decimal Max = (decimal?)(context.MyTable.Select(e => e.MyCounter).Max()) ?? 0;

答案 14 :(得分:0)

我遇到了类似的问题,我的单元测试使用Max()传递,但在对实时数据库运行时失败。

我的解决方案是将查询与正在执行的逻辑分开,而不是将它们连接到一个查询中 我需要一个解决方案,使用Linq-objects(在Linq-objects Max()中使用nulls)和Linq-sql在实时环境中执行时进行单元测试。

(我在测试中模拟了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);

效率低下?大概。

我是否在乎,只要我的应用程序下次没有摔倒?不。

答案 15 :(得分:0)

我已经敲了一个MaxOrDefault扩展方法。它并不多,但它在Intellisense中的存在是一个有用的提示,空序列上的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;
    }

用法,名为int的{​​{1}}类型的列或属性:

Id

答案 16 :(得分:0)

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

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的默认值而不是抛出一个exeption