Max czy Default?

176

Jaki jest najlepszy sposób uzyskania wartości Max z zapytania LINQ, które może nie zwracać żadnych wierszy? Jeśli po prostu to zrobię

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

Pojawia się błąd, gdy zapytanie nie zwraca żadnych wierszy. mógłbym zrobić

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter _
         Order By MyCounter Descending).FirstOrDefault

ale wydaje się to trochę tępe jak na tak prostą prośbę. Czy brakuje mi lepszego sposobu, aby to zrobić?

AKTUALIZACJA: Oto historia: próbuję pobrać następny licznik uprawnień z tabeli podrzędnej (starszy system, nie zaczynaj mnie ...). Pierwszy wiersz uprawnień dla każdego pacjenta to zawsze 1, drugi to 2 itd. (Oczywiście nie jest to klucz podstawowy tabeli podrzędnej). Tak więc wybieram maksymalną istniejącą wartość licznika dla pacjenta, a następnie dodaję do niej 1, aby utworzyć nowy wiersz. Gdy nie ma żadnych istniejących wartości podrzędnych, potrzebuję zapytania, aby zwrócić 0 (więc dodanie 1 da mi wartość licznika 1). Zauważ, że nie chcę polegać na nieprzetworzonej liczbie wierszy podrzędnych, na wypadek gdyby starsza aplikacja wprowadziła luki w wartościach liczników (możliwe). Moja wina, że ​​próbowałem uczynić to pytanie zbyt ogólnym.

mruczeć
źródło

Odpowiedzi:

206

Ponieważ DefaultIfEmptynie jest zaimplementowany w LINQ to SQL, wyszukałem błąd, który zwrócił i znalazłem fascynujący artykuł, który dotyczy zbiorów zerowych w funkcjach agregujących. Podsumowując to, co znalazłem, możesz obejść to ograniczenie, rzucając na wartość null w ramach swojego wyboru. Mój VB jest trochę zardzewiały, ale myślę , że wyglądałoby to mniej więcej tak:

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

Lub w C #:

var x = (from y in context.MyTable
         where y.MyField == value
         select (int?)y.MyCounter).Max();
Jacob Proffitt
źródło
1
Aby poprawić VB, wybierz opcję „Select CType (y.MyCounter, Integer?)”. Muszę wykonać oryginalne sprawdzenie, aby przekonwertować Nothing na 0 do moich celów, ale lubię uzyskiwać wyniki bez wyjątku.
gfrizzle,
2
Jedno z dwóch przeciążeń DefaultIfEmpty jest obsługiwane w LINQ to SQL - ten, który nie przyjmuje parametrów.
DamienG,
Prawdopodobnie te informacje są nieaktualne, ponieważ właśnie pomyślnie przetestowałem obie formy DefaultIfEmpty w LINQ to SQL
Neil,
3
@Neil: proszę, odpowiedz. DefaultIfEmpty nie działa dla mnie: chcę, aby Maxplik DateTime. Max(x => (DateTime?)x.TimeStamp)wciąż jedyny sposób ..
duedl0r
1
Chociaż DefaultIfEmpty jest teraz zaimplementowana w LINQ to SQL, ta odpowiedź pozostaje lepsza IMO, ponieważ użycie DefaultIfEmpty skutkuje instrukcją SQL „SELECT MyCounter”, która zwraca wiersz dla każdej sumowanej wartości , podczas gdy ta odpowiedź daje MAX (MyCounter), który zwraca pojedynczy, zsumowany wiersz. (Testowane w EntityFrameworkCore 2.1.3.)
Carl Sharman,
107

Właśnie miałem podobny problem, ale korzystałem z metod rozszerzenia LINQ na liście, a nie na składni zapytania. Rzutowanie na sztuczkę Nullable również działa tutaj:

int max = list.Max(i => (int?)i.MyCounter) ?? 0;
Eddie Deyo
źródło
48

Brzmi jak przypadek DefaultIfEmpty(nieprzetestowany kod poniżej):

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter).DefaultIfEmpty.Max
Jacob Proffitt
źródło
Nie znam DefaultIfEmpty, ale przy użyciu powyższej składni pojawia się komunikat „Nie można sformatować węzła„ Wartość opcjonalna ”do wykonania jako SQL”. Próbowałem też podać wartość domyślną (zero), ale to też się nie podobało.
gfrizzle
Ach. Wygląda na to, że DefaultIfEmpty nie jest obsługiwana w LINQ to SQL. Możesz to obejść, przesyłając najpierw do listy za pomocą .ToList, ale jest to znaczący hit wydajnościowy.
Jacob Proffitt
3
Dzięki, właśnie tego szukałem. Korzystanie z metod rozszerzających:var colCount = RowsEnumerable.Select(row => row.Cols.Count).DefaultIfEmpty().Max()
Jani
35

Zastanów się, o co pytasz!

Maksimum {1, 2, 3, -1, -2, -3} to oczywiście 3. Maksimum z {2} to oczywiście 2. Ale jakie jest maksimum pustego zbioru {}? Oczywiście jest to bezsensowne pytanie. Po prostu nie zdefiniowano maksimum pustego zestawu. Próba uzyskania odpowiedzi jest błędem matematycznym. Maksimum dowolnego zestawu musi samo być elementem w tym zestawie. Pusty zbiór nie ma elementów, więc twierdzenie, że jakaś konkretna liczba jest maksimum tego zbioru bez znajdowania się w tym zbiorze, jest matematyczną sprzecznością.

Tak jak poprawnym zachowaniem jest rzucanie przez komputer wyjątku, gdy programista prosi go o podzielenie przez zero, tak też poprawnym zachowaniem jest rzucanie przez komputer wyjątku, gdy programista prosi go o maksymalne wykorzystanie pustego zestawu. Dzielenie przez zero, maksymalne wykorzystanie pustego zestawu, poruszanie spacklerorke i jazda na latającym jednorożcu do Nibylandii są bez znaczenia, niemożliwe, nieokreślone.

Co właściwie chcesz zrobić?

yfeldblum
źródło
Słuszna uwaga - wkrótce zaktualizuję moje pytanie tymi szczegółami. Dość powiedzieć, że wiem, że chcę 0, gdy nie ma rekordów do wyboru, co z pewnością ma wpływ na ostateczne rozwiązanie.
gfrizzle
17
Często próbuję latać moim jednorożcem do Neverlandu i obrażam się na twoją sugestię, że moje wysiłki są bez znaczenia i nieokreślone.
Chris krzyczy
2
Nie sądzę, żeby ta argumentacja była słuszna. Jest to jasne linq-to-sql, aw sql Max powyżej zera wierszy jest zdefiniowane jako null, nie?
duedl0r
4
Linq powinien generalnie dawać identyczne wyniki niezależnie od tego, czy zapytanie jest wykonywane w pamięci obiektów, czy też w bazie danych w wierszach. Zapytania Linq są zapytaniami Linq i powinny być wykonywane wiernie, niezależnie od używanego adaptera.
yfeldblum
1
Chociaż teoretycznie zgadzam się, że wyniki Linq powinny być identyczne, niezależnie od tego, czy są wykonywane w pamięci, czy w sql, kiedy faktycznie zagłębisz się trochę głębiej, odkryjesz, dlaczego nie zawsze tak jest. Wyrażenia Linq są tłumaczone na sql przy użyciu złożonego tłumaczenia wyrażeń. Nie jest to proste tłumaczenie typu „jeden na jeden”. Jedną różnicą jest przypadek null. W języku C # „null == null” jest prawdziwe. W języku SQL dopasowania „null == null” są uwzględniane dla złączeń zewnętrznych, ale nie dla złączeń wewnętrznych. Jednak łączenia wewnętrzne są prawie zawsze tym, czego chcesz, więc są one domyślne. To powoduje możliwe różnice w zachowaniu.
Curtis Yallop
25

Zawsze możesz dodać Double.MinValuedo sekwencji. Zapewniłoby to istnienie co najmniej jednego elementu i Maxzwróciłoby go tylko wtedy, gdy jest to faktycznie minimum. Aby określić, która opcja jest bardziej wydajna (Concat , FirstOrDefaultlub Take(1)), należy przeprowadzić odpowiednią analizę porównawczą.

double x = context.MyTable
    .Where(y => y.MyField == value)
    .Select(y => y.MyCounter)
    .Concat(new double[]{Double.MinValue})
    .Max();
David Schmitt
źródło
10
int max = list.Any() ? list.Max(i => i.MyCounter) : 0;

Jeśli lista zawiera jakieś elementy (tj. Nie jest pusta), zajmie maksimum pola MyCounter, w przeciwnym razie zwróci 0.

beastieboy
źródło
3
Czy to nie spowoduje uruchomienia 2 zapytań?
andreapier
10

Od .Net 3.5 możesz używać DefaultIfEmpty () przekazując domyślną wartość jako argument. Coś jak jeden z następujących sposobów:

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

Pierwsza z nich jest dozwolona, ​​gdy wykonujesz zapytanie o kolumnę NOT NULL, a druga to sposób, w jaki został użyty do zapytania o kolumnę NULLABLE. Jeśli użyjesz DefaultIfEmpty () bez argumentów, domyślną wartością będzie ta zdefiniowana dla typu twojego wyniku, jak widać w tabeli wartości domyślnych .

Powstały SELECT nie będzie tak elegancki, ale można go zaakceptować.

Mam nadzieję, że to pomoże.

Fernando Brustolin
źródło
7

Myślę, że chodzi o to, co chcesz, aby się stało, gdy zapytanie nie ma wyników. Jeśli jest to wyjątkowy przypadek, zawinąłbym zapytanie w blok try / catch i obsłużył wyjątek generowany przez standardowe zapytanie. Jeśli zapytanie nie zwraca żadnych wyników, musisz dowiedzieć się, jaki ma być wynik w takim przypadku. Możliwe, że odpowiedź @ David (lub coś podobnego zadziała). Oznacza to, że jeśli MAX zawsze będzie dodatni, może wystarczyć wstawienie znanej „złej” wartości do listy, która zostanie wybrana tylko wtedy, gdy nie będzie żadnych wyników. Generalnie spodziewałbym się, że zapytanie, które pobiera maksimum, ma pewne dane do pracy, i wybrałbym trasę try / catch, ponieważ w przeciwnym razie zawsze jesteś zmuszony sprawdzić, czy uzyskana wartość jest poprawna, czy nie. JA'

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
tvanfosson
źródło
W moim przypadku zwracanie żadnych wierszy zdarza się częściej niż nie (starszy system, pacjent może lub nie ma wcześniejszej kwalifikacji, bla bla bla). Gdyby to był bardziej wyjątkowy przypadek, prawdopodobnie wybrałbym tę trasę (i może nadal, nie widząc dużo lepiej).
gfrizzle
6

Inną możliwością byłoby grupowanie, podobne do tego, jak można podejść do tego w surowym SQL:

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

Jedyną rzeczą jest to (testowanie ponownie w LINQPad) przełączenie na smak VB LINQ powoduje błędy składniowe w klauzuli grupującej. Jestem pewien, że konceptualny odpowiednik jest dość łatwy do znalezienia, po prostu nie wiem, jak odzwierciedlić go w VB.

Wygenerowany kod SQL byłby podobny do:

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

Zagnieżdżony SELECT wygląda źle, jakby wykonanie zapytania pobrał wszystkie wiersze, a następnie wybrał pasujący z pobranego zestawu ... pytanie brzmi, czy SQL Server optymalizuje zapytanie do czegoś porównywalnego z zastosowaniem klauzuli where do wewnętrznego SELECT. Patrzę teraz na to ...

Nie jestem dobrze zorientowany w interpretowaniu planów wykonania w SQL Server, ale wygląda na to, że gdy klauzula WHERE znajduje się na zewnętrznym polu SELECT, liczba rzeczywistych wierszy powodujących ten krok to wszystkie wiersze w tabeli, a tylko pasujące wiersze gdy klauzula WHERE znajduje się w wewnętrznym SELECT. To powiedziawszy, wygląda na to, że tylko 1% kosztu jest przenoszony do następnego kroku, gdy uwzględniane są wszystkie wiersze, a tak czy inaczej tylko jeden wiersz wraca z serwera SQL, więc może nie jest to aż tak duża różnica w ogólnym schemacie rzeczy .

Rex Miller
źródło
6

trochę późno, ale miałem ten sam problem ...

Ponownie zapisując swój kod z oryginalnego postu, chcesz, aby maksimum zestawu S było zdefiniowane przez

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

Biorąc pod uwagę Twój ostatni komentarz

Dość powiedzieć, że wiem, że chcę 0, gdy nie ma rekordów do wyboru, co zdecydowanie ma wpływ na ostateczne rozwiązanie

Mogę sformułować Twój problem następująco: Chcesz maksymalnie {0 + S}. I wygląda na to, że proponowane rozwiązanie z concat jest semantycznie właściwe :-)

var max = new[]{0}
          .Concat((From y In context.MyTable _
                   Where y.MyField = value _
                   Select y.MyCounter))
          .Max();
Dom Ribaut
źródło
3

Dlaczego nie coś bardziej bezpośredniego, na przykład:

Dim x = context.MyTable.Max(Function(DataItem) DataItem.MyField = Value)
prawny
źródło
1

Ciekawą różnicą, która wydaje się warta odnotowania, jest to, że podczas gdy FirstOrDefault i Take (1) generują ten sam kod SQL (zgodnie z LINQPad, tak czy inaczej), FirstOrDefault zwraca wartość - domyślną - gdy nie ma pasujących wierszy, a Take (1) zwraca brak wyników ... przynajmniej w LINQPad.

Rex Miller
źródło
1

Aby wszyscy wiedzieli, że używa Linq do Entities, powyższe metody nie będą działać ...

Jeśli spróbujesz zrobić coś takiego

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

Zrzuci wyjątek:

System.NotSupportedException: typ węzła wyrażenia LINQ „NewArrayInit” nie jest obsługiwany w LINQ to Entities.

Sugerowałbym po prostu zrobić

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

A FirstOrDefaultzwróci 0, jeśli lista jest pusta.

Nic
źródło
Zamawianie może spowodować poważne pogorszenie wydajności w przypadku dużych zbiorów danych. Jest to bardzo nieefektywny sposób na znalezienie wartości maksymalnej.
Peter Bruins
1
decimal Max = (decimal?)(context.MyTable.Select(e => e.MyCounter).Max()) ?? 0;
jong su.
źródło
1

Wrzuciłem MaxOrDefaultmetodę rozszerzenia. Nie ma tego wiele, ale jego obecność w Intellisense jest użytecznym przypomnieniem, że Maxw pustej sekwencji spowoduje wyjątek. Ponadto metoda umożliwia określenie wartości domyślnej, jeśli jest to wymagane.

    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;
    }
Stephen Kennedy
źródło
0

Dla Entity Framework i Linq to SQL możemy to osiągnąć poprzez zdefiniowanie metody rozszerzenia, która modyfikuje Expressionprzekazaną do IQueryable<T>.Max(...)metody:

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

Stosowanie:

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

Wygenerowane zapytanie jest identyczne, działa jak normalne wywołanie IQueryable<T>.Max(...)metody, ale jeśli nie ma rekordów, zwraca domyślną wartość typu T zamiast rzucać wyjątek

Ashot Muradian
źródło
-1

Właśnie miałem podobny problem, moje testy jednostkowe przeszły pomyślnie przy użyciu Max (), ale zakończyły się niepowodzeniem, gdy zostały uruchomione w działającej bazie danych.

Moim rozwiązaniem było oddzielenie zapytania od wykonywanej logiki, a nie połączenie ich w jedno zapytanie.
Potrzebowałem rozwiązania do pracy w testach jednostkowych przy użyciu obiektów Linq (w Linq-objects Max () działa z wartościami zerowymi) i Linq-sql podczas wykonywania w środowisku na żywo.

(Kpię z Select () w moich testach)

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

Mniej wydajne? Prawdopodobnie.

Czy mnie to obchodzi, o ile moja aplikacja nie spadnie następnym razem? Nie.

Seb
źródło