Max oder Standard?

176

Was ist der beste Weg, um den Max-Wert aus einer LINQ-Abfrage zu erhalten, die möglicherweise keine Zeilen zurückgibt? Wenn ich es nur tue

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

Ich erhalte eine Fehlermeldung, wenn die Abfrage keine Zeilen zurückgibt. ich könnte

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

aber das fühlt sich für eine so einfache Anfrage etwas stumpf an. Vermisse ich einen besseren Weg, es zu tun?

UPDATE: Hier ist die Hintergrundgeschichte: Ich versuche, den nächsten Berechtigungszähler von einer untergeordneten Tabelle abzurufen (Legacy-System, lass mich nicht anfangen ...). Die erste Berechtigungszeile für jeden Patienten ist immer 1, die zweite 2 usw. (dies ist offensichtlich nicht der Primärschlüssel der untergeordneten Tabelle). Also wähle ich den maximal vorhandenen Zählerwert für einen Patienten aus und füge dann 1 hinzu, um eine neue Zeile zu erstellen. Wenn keine untergeordneten Werte vorhanden sind, muss die Abfrage 0 zurückgeben (wenn Sie also 1 hinzufügen, erhalten Sie einen Zählerwert von 1). Beachten Sie, dass ich mich nicht auf die rohe Anzahl der untergeordneten Zeilen verlassen möchte, falls die Legacy-App Lücken in den Zählerwerten einführt (möglich). Mein schlechtes für den Versuch, die Frage zu allgemein zu machen.

gfrizzle
quelle

Antworten:

206

Da DefaultIfEmptyes in LINQ to SQL nicht implementiert ist, habe ich nach dem zurückgegebenen Fehler gesucht und einen faszinierenden Artikel gefunden , der sich mit Nullmengen in Aggregatfunktionen befasst. Um zusammenzufassen, was ich gefunden habe, können Sie diese Einschränkung umgehen, indem Sie in eine Null in Ihrer Auswahl umwandeln. Mein VB ist ein bisschen rostig, aber ich denke, es würde ungefähr so ​​aussehen:

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

Oder in C #:

var x = (from y in context.MyTable
         where y.MyField == value
         select (int?)y.MyCounter).Max();
Jacob Proffitt
quelle
1
Um die VB zu korrigieren, wäre die Auswahl "CType auswählen (y.MyCounter, Integer?)". Ich muss eine Originalprüfung durchführen, um Nothing für meine Zwecke in 0 umzuwandeln, aber ich möchte die Ergebnisse ausnahmslos erhalten.
Gfrizzle
2
Eine der beiden Überladungen von DefaultIfEmpty wird in LINQ to SQL unterstützt - diejenige, die keine Parameter akzeptiert.
DamienG
Möglicherweise sind diese Informationen nicht mehr aktuell, da ich gerade beide Formen von DefaultIfEmpty in LINQ to SQL erfolgreich getestet habe
Neil
3
@Neil: bitte gib eine Antwort. DefaultIfEmpty funktioniert bei mir nicht: Ich möchte das Maxvon a DateTime. Max(x => (DateTime?)x.TimeStamp)immer noch der einzige Weg ..
duedl0r
1
Obwohl DefaultIfEmpty jetzt in LINQ to SQL implementiert ist, bleibt diese Antwort IMO besser, da die Verwendung von DefaultIfEmpty zu einer SQL-Anweisung 'SELECT MyCounter' führt, die für jeden summierten Wert eine Zeile zurückgibt , während diese Antwort zu MAX (MyCounter) führt, das a zurückgibt einzelne, summierte Zeile. (Getestet in EntityFrameworkCore 2.1.3.)
Carl Sharman
107

Ich hatte gerade ein ähnliches Problem, aber ich verwendete LINQ-Erweiterungsmethoden für eine Liste anstelle der Abfragesyntax. Das Casting zu einem Nullable-Trick funktioniert auch dort:

int max = list.Max(i => (int?)i.MyCounter) ?? 0;
Eddie Deyo
quelle
48

Klingt nach einem Fall für DefaultIfEmpty(ungetesteter Code folgt):

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter).DefaultIfEmpty.Max
Jacob Proffitt
quelle
Ich bin mit DefaultIfEmpty nicht vertraut, erhalte jedoch bei Verwendung der obigen Syntax die Meldung "Der Knoten 'OptionalValue' konnte nicht als SQL ausgeführt werden". Ich habe auch versucht, einen Standardwert (Null) anzugeben, aber das hat mir auch nicht gefallen.
Gfrizzle
Ah. DefaultIfEmpty wird in LINQ to SQL anscheinend nicht unterstützt. Sie könnten das umgehen, indem Sie zuerst mit .ToList eine Liste erstellen, aber das ist ein bedeutender Leistungseinbruch.
Jacob Proffitt
3
Danke, genau das habe ich gesucht. Verwenden von Erweiterungsmethoden:var colCount = RowsEnumerable.Select(row => row.Cols.Count).DefaultIfEmpty().Max()
Jani
35

Überlegen Sie, was Sie fragen!

Das Maximum von {1, 2, 3, -1, -2, -3} ist offensichtlich 3. Das Maximum von {2} ist offensichtlich 2. Aber was ist das Maximum der leeren Menge {}? Das ist natürlich eine bedeutungslose Frage. Das Maximum des leeren Satzes ist einfach nicht definiert. Der Versuch, eine Antwort zu erhalten, ist ein mathematischer Fehler. Das Maximum einer Menge muss selbst ein Element in dieser Menge sein. Die leere Menge enthält keine Elemente. Die Behauptung, dass eine bestimmte Zahl das Maximum dieser Menge ist, ohne in dieser Menge zu sein, ist ein mathematischer Widerspruch.

So wie es ein korrektes Verhalten für den Computer ist, eine Ausnahme auszulösen, wenn der Programmierer ihn auffordert, durch Null zu teilen, so ist es ein korrektes Verhalten für den Computer, eine Ausnahme auszulösen, wenn der Programmierer ihn auffordert, das Maximum des leeren Satzes zu nehmen. Die Division durch Null, das Maximum des leeren Satzes, das Wackeln der Spacklerorke und das Reiten des fliegenden Einhorns nach Neverland sind alle bedeutungslos, unmöglich und undefiniert.

Was möchten Sie nun eigentlich tun?

Yfeldblum
quelle
Guter Punkt - ich werde meine Frage in Kürze mit diesen Details aktualisieren. Es genügt zu sagen, dass ich weiß, dass ich 0 möchte, wenn keine Datensätze zur Auswahl stehen, was sich definitiv auf die mögliche Lösung auswirkt.
Gfrizzle
17
Ich versuche häufig, mein Einhorn nach Neverland zu fliegen, und ich beleidige Ihren Vorschlag, dass meine Bemühungen bedeutungslos und undefiniert sind.
Chris Shouts
2
Ich denke nicht, dass diese Argumentation richtig ist. Es ist klar, von Linq zu SQL, und in SQL ist Max über Null Zeilen als Null definiert, nein?
duedl0r
4
Linq sollte im Allgemeinen identische Ergebnisse liefern, unabhängig davon, ob die Abfrage im Speicher für Objekte ausgeführt wird oder ob die Abfrage in der Datenbank für Zeilen ausgeführt wird. Linq-Abfragen sind Linq-Abfragen und sollten unabhängig davon ausgeführt werden, welcher Adapter verwendet wird.
Yfeldblum
1
Während ich theoretisch zustimme, dass Linq-Ergebnisse identisch sein sollten, egal ob sie im Speicher oder in SQL ausgeführt werden, entdecken Sie, wenn Sie tatsächlich etwas tiefer graben, warum dies nicht immer so sein kann. Linq-Ausdrücke werden mithilfe einer komplexen Ausdrucksübersetzung in SQL übersetzt. Es ist keine einfache Eins-zu-Eins-Übersetzung. Ein Unterschied ist der Fall von Null. In C # ist "null == null" wahr. In SQL sind "null == null" -Matches für äußere Joins enthalten, nicht jedoch für innere Joins. Innere Verknüpfungen sind jedoch fast immer das, was Sie möchten, sodass sie die Standardeinstellungen sind. Dies führt zu möglichen Verhaltensunterschieden.
Curtis Yallop
25

Sie können Double.MinValuedie Sequenz jederzeit ergänzen . Dies würde sicherstellen, dass mindestens ein Element vorhanden ist, und Maxes nur zurückgeben, wenn es tatsächlich das Minimum ist. Um festzustellen , welche Option effizienter ist ( Concat, FirstOrDefaultoder Take(1)), sollten Sie eine angemessene Benchmarking durchzuführen.

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

Wenn die Liste Elemente enthält (dh nicht leer ist), wird das Maximum des MyCounter-Felds verwendet, andernfalls wird 0 zurückgegeben.

Beastieboy
quelle
3
Wird dies nicht 2 Abfragen ausführen?
andreapier
10

Seit .Net 3.5 können Sie DefaultIfEmpty () verwenden und den Standardwert als Argument übergeben. So etwas wie eine der folgenden Möglichkeiten:

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

Die erste ist zulässig, wenn Sie eine NOT NULL-Spalte abfragen, und die zweite ist die Art und Weise, wie eine NULLABLE-Spalte abgefragt wird. Wenn Sie DefaultIfEmpty () ohne Argumente verwenden, ist der Standardwert der für den Typ Ihrer Ausgabe definierte, wie Sie in der Standardwertetabelle sehen können .

Das resultierende SELECT wird nicht so elegant sein, aber es ist akzeptabel.

Ich hoffe es hilft.

Fernando Brustolin
quelle
7

Ich denke, das Problem ist, was Sie tun möchten, wenn die Abfrage keine Ergebnisse hat. Wenn dies ein Ausnahmefall ist, würde ich die Abfrage in einen try / catch-Block einschließen und die Ausnahme behandeln, die die Standardabfrage generiert. Wenn es in Ordnung ist, dass die Abfrage keine Ergebnisse zurückgibt, müssen Sie herausfinden, wie das Ergebnis in diesem Fall aussehen soll. Es kann sein, dass @ Davids Antwort (oder etwas Ähnliches wird funktionieren). Das heißt, wenn der MAX immer positiv ist, kann es ausreichen, einen bekannten "schlechten" Wert in die Liste einzufügen, der nur ausgewählt wird, wenn keine Ergebnisse vorliegen. Im Allgemeinen würde ich erwarten, dass eine Abfrage, die ein Maximum abruft, einige Daten enthält, an denen gearbeitet werden kann, und ich würde den Try / Catch-Weg gehen, da Sie sonst immer gezwungen sind, zu überprüfen, ob der von Ihnen erhaltene Wert korrekt ist oder nicht. ICH'

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
quelle
In meinem Fall kommt es häufig vor, dass keine Zeilen zurückgegeben werden (Legacy-System, der Patient kann eine vorherige Berechtigung haben oder nicht, bla bla bla). Wenn dies ein außergewöhnlicherer Fall wäre, würde ich wahrscheinlich diesen Weg gehen (und ich kann immer noch nicht viel besser sehen).
gfrizzle
6

Eine andere Möglichkeit wäre die Gruppierung, ähnlich wie Sie es in Raw SQL tun könnten:

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

Das einzige, was (erneutes Testen in LINQPad) zum Umschalten auf die VB LINQ-Variante führt, führt zu Syntaxfehlern in der Gruppierungsklausel. Ich bin mir sicher, dass das konzeptionelle Äquivalent leicht zu finden ist. Ich weiß nur nicht, wie ich es in VB wiedergeben soll.

Das generierte SQL wäre ungefähr so:

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

Das verschachtelte SELECT sieht schwierig aus, als würde die Abfrageausführung alle Zeilen abrufen und dann die passende aus dem abgerufenen Satz auswählen. Die Frage ist, ob SQL Server die Abfrage in etwas optimiert, das mit der Anwendung der where-Klausel auf das innere SELECT vergleichbar ist. Ich untersuche das jetzt ...

Ich bin nicht gut mit der Interpretation von Ausführungsplänen in SQL Server vertraut, aber es sieht so aus, als ob die WHERE-Klausel im äußeren SELECT die Anzahl der tatsächlichen Zeilen, die zu diesem Schritt führen, alle Zeilen in der Tabelle und nur die übereinstimmenden Zeilen sind wenn sich die WHERE-Klausel auf der inneren SELECT befindet. Es sieht jedoch so aus, als würden nur 1% der Kosten in den folgenden Schritt verschoben, wenn alle Zeilen berücksichtigt werden, und so oder so kommt immer nur eine Zeile vom SQL Server zurück. Vielleicht ist es also kein so großer Unterschied im großen Schema der Dinge .

Rex Miller
quelle
6

Ein bisschen spät, aber ich hatte die gleiche Sorge ...

Wenn Sie Ihren Code aus dem ursprünglichen Beitrag umformulieren, möchten Sie, dass das Maximum der Menge S durch definiert wird

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

Berücksichtigen Sie Ihren letzten Kommentar

Es genügt zu sagen, dass ich weiß, dass ich 0 möchte, wenn keine Datensätze zur Auswahl stehen, was sich definitiv auf die mögliche Lösung auswirkt

Ich kann Ihr Problem wie folgt umformulieren: Sie möchten das Maximum von {0 + S}. Und es sieht so aus, als ob die vorgeschlagene Lösung mit concat semantisch die richtige ist :-)

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

Warum nicht etwas direkteres wie:

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

Ein interessanter Unterschied, der erwähnenswert zu sein scheint, ist, dass FirstOrDefault und Take (1) dieselbe SQL generieren (jedenfalls laut LINQPad), FirstOrDefault jedoch einen Wert zurückgibt - den Standardwert - wenn keine übereinstimmenden Zeilen vorhanden sind und Take (1) zurückgibt Keine Ergebnisse ... zumindest in LINQPad.

Rex Miller
quelle
1

Nur um alle da draußen wissen zu lassen, dass Linq to Entities verwendet wird, funktionieren die oben genannten Methoden nicht ...

Wenn Sie versuchen, so etwas zu tun

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

Es wird eine Ausnahme auslösen:

System.NotSupportedException: Der LINQ-Ausdrucksknotentyp 'NewArrayInit' wird in LINQ to Entities nicht unterstützt.

Ich würde vorschlagen, nur zu tun

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

Und das FirstOrDefaultgibt 0 zurück, wenn Ihre Liste leer ist.

Nix
quelle
Die Bestellung kann bei großen Datenmengen zu einer ernsthaften Leistungsminderung führen. Es ist ein sehr ineffizienter Weg, einen Maximalwert zu finden.
Peter Bruins
1
decimal Max = (decimal?)(context.MyTable.Select(e => e.MyCounter).Max()) ?? 0;
jong su.
quelle
1

Ich habe eine MaxOrDefaultErweiterungsmethode entwickelt. Es gibt nicht viel zu tun, aber seine Anwesenheit in Intellisense ist eine nützliche Erinnerung daran, dass Maxbei einer leeren Sequenz eine Ausnahme verursacht wird. Darüber hinaus ermöglicht die Methode die Angabe der Standardeinstellung, falls erforderlich.

    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
quelle
0

Für Entity Framework und LINQ to SQL können wir dies durch die Definition eine Erweiterungsmethode erreichen , die eine modifiziert Expressionbestanden IQueryable<T>.Max(...)Methode:

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

Verwendung:

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

Die generierte Abfrage ist identisch. Sie funktioniert wie ein normaler Aufruf der IQueryable<T>.Max(...)Methode. Wenn jedoch keine Datensätze vorhanden sind, wird ein Standardwert vom Typ T zurückgegeben, anstatt eine Ausnahme auszulösen

Ashot Muradian
quelle
-1

Ich hatte gerade ein ähnliches Problem. Meine Komponententests wurden mit Max () bestanden, schlugen jedoch fehl, wenn sie mit einer Live-Datenbank ausgeführt wurden.

Meine Lösung bestand darin, die Abfrage von der ausgeführten Logik zu trennen und sie nicht zu einer Abfrage zusammenzufügen.
Ich brauchte eine Lösung, um in Unit-Tests mit Linq-Objekten (in Linq-Objekten arbeitet Max () mit Nullen) und Linq-SQL zu arbeiten, wenn ich in einer Live-Umgebung ausgeführt werde.

(Ich verspotte die Select () in meinen 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);

Weniger effizient? Wahrscheinlich.

Interessiert es mich, solange meine App beim nächsten Mal nicht umfällt? Nee.

Seb
quelle