Programing

최대 또는 기본?

crosscheck 2020. 5. 26. 19:45
반응형

최대 또는 기본?


행을 반환하지 않을 수있는 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이됩니다. 레거시 앱이 카운터 값에 차이를 일으킬 수있는 경우 자식 행의 원시 수에 의존하고 싶지 않습니다. 질문을 너무 일반적으로 만들려고 노력했기 때문에 나쁘다.


이후 DefaultIfEmptySQL에 LINQ에서 구현되지 않습니다, 나는 그것이 반환 된 오류에서 검색을했고, 발견 된 매혹적인 기사 집계 함수에 널 (null) 세트를 다루고 그. 내가 찾은 것을 요약하면 선택 내에서 nullable로 캐스팅 하여이 제한을 해결할 수 있습니다. 내 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();

방금 비슷한 문제가 있었지만 쿼리 구문이 아닌 목록에서 LINQ 확장 메서드를 사용하고있었습니다. Nullable 트릭으로 캐스팅하면 다음과 같이 작동합니다.

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

DefaultIfEmpty다음과 같은 경우처럼 들립니다 (예상치 않은 코드는 다음과 같습니다).

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

당신이 요구하는 것을 생각하십시오!

{1, 2, 3, -1, -2, -3}의 최대 값은 분명히 3입니다. {2}의 최대 값은 분명히 2입니다. 그러나 빈 세트의 최대 값 {}은 무엇입니까? 분명히 그것은 무의미한 질문입니다. 빈 세트의 최대 값은 단순히 정의되지 않았습니다. 답을 얻으려고하는 것은 수학적 오류입니다. 세트의 최대 값 자체는 해당 세트의 요소 여야합니다. 빈 집합에는 요소가 없으므로 특정 집합이 해당 집합에 속하지 않고 해당 집합의 최대 값이라고 주장하면 수학적 모순입니다.

프로그래머가 0으로 나누기를 요청할 때 컴퓨터가 예외를 throw하는 것이 올바른 동작이므로, 프로그래머가 빈 세트의 최대 값을 가져 오도록 요청할 때 컴퓨터가 예외를 throw하는 것이 올바른 동작입니다. 빈 세트의 최대 값을 취하고, spacklerorke를 자극하고, 비행 유니콘을 Neverland로 타는 것은 의미가 없으며 불가능하며 정의되지 않습니다.

이제 실제로 하고 싶은 것은 무엇입니까?


항상 Double.MinValue시퀀스에 추가 할 수 있습니다 . 이렇게하면 최소한 하나의 요소 Max가 있으며 실제로 최소 인 경우에만 반환합니다. 더 효율적입니다 (옵션을 결정하기 위해 Concat, FirstOrDefault또는 Take(1)), 당신은 적절한 벤치마킹을 수행해야합니다.

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

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

목록에 요소가없는 경우 (즉, 비어 있지 않은 경우) MyCounter 필드의 최대 값을 사용하고 그렇지 않으면 0을 반환합니다.


.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는 그렇게 우아하지는 않지만 수용 가능합니다.

도움이 되길 바랍니다.


I think the issue is what do you want to happen when the query has no results. If this is an exceptional case then I would wrap the query in a try/catch block and handle the exception that the standard query generates. If it's ok to have the query return no results, then you need to figure out what you want the result to be in that case. It may be that @David's answer (or something similar will work). That is, if the MAX will always be positive, then it may be enough to insert a known "bad" value into the list that will only be selected if there are no results. Generally, I would expect a query that is retrieving a maximum to have some data to work on and I would go the try/catch route as otherwise you are always forced to check if the value you obtained is correct or not. I'd rather that the non-exceptional case was just able to use the obtained value.

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

Another possibility would be grouping, similar to how you might approach it in raw SQL:

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

The only thing is (testing again in LINQPad) switching to the VB LINQ flavor gives syntax errors on the grouping clause. I'm sure the conceptual equivalent is easy enough to find, I just don't know how to reflect it in VB.

The generated SQL would be something along the lines of:

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

The nested SELECT looks icky, like the query execution would retrieve all rows then select the matching one from the retrieved set... the question is whether or not SQL Server optimizes the query into something comparable to applying the where clause to the inner SELECT. I'm looking into that now...

I'm not well-versed in interpreting execution plans in SQL Server, but it looks like when the WHERE clause is on the outer SELECT, the number of actual rows resulting in that step is all rows in the table, versus only the matching rows when the WHERE clause is on the inner SELECT. That said, it looks like only 1% cost is shifted to the following step when all rows are considered, and either way only one row ever comes back from the SQL Server so maybe it's not that big of a difference in the grand scheme of things.


litt late, but I had the same concern...

Rephrasing your code from the original post, you want the max of the set S defined by

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

Taking in account your last comment

Suffice to say that I know I want 0 when there are no records to select from, which definitely has an impact on the eventual solution

I can rephrase your problem as: You want the max of {0 + S}. And it looks like the proposed solution with concat is semantically the right one :-)

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

Why Not something more direct like:

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

Just to let everyone out there know that is using Linq to Entities the methods above will not work...

If you try to do something like

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

It will throw an exception:

System.NotSupportedException: The LINQ expression node type 'NewArrayInit' is not supported in LINQ to Entities..

I would suggest just doing

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

And the FirstOrDefault will return 0 if your list is empty.


One interesting difference that seems worth noting is that while FirstOrDefault and Take(1) generate the same SQL (according to LINQPad, anyway), FirstOrDefault returns a value--the default--when there are no matching rows and Take(1) returns no results... at least in LINQPad.


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

I just had a similar problem, my unit tests passed using Max() but failed when run against a live database.

My solution was to separate the query from the logic being performed, not join them in one query.
I needed a solution to work in unit tests using Linq-objects (in Linq-objects Max() works with nulls) and Linq-sql when executing in a live environment.

(I mock the Select() in my tests)

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);

Less efficient? Probably.

Do I care, as long as my app doesn't fall over next time? Nope.


I've knocked up a MaxOrDefault extension method. There's not much to it but its presence in Intellisense is a useful reminder that Max on an empty sequence will cause an exception. Additionally, the method allows the default to be specified if required.

    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;
    }

Usage, on a column or property of type int named Id:

    sequence.DefaultOrMax(s => (int?)s.Id);

For Entity Framework and Linq to SQL we can achieve this by defining an extension method which modifies an Expression passed to IQueryable<T>.Max(...) method:

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);
    }
}

Usage:

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

The generated query is identical, it works just like a normal call to IQueryable<T>.Max(...) method, but if there is no records it returns a default value of type T instead of throwing an exeption

참고URL : https://stackoverflow.com/questions/341264/max-or-default

반응형