Max o Default?


176

Qual è il modo migliore per ottenere il valore massimo da una query LINQ che potrebbe non restituire righe? Se solo lo facessi

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

Viene visualizzato un errore quando la query non restituisce righe. Potrei fare

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

ma sembra un po 'ottuso per una richiesta così semplice. Mi sto perdendo un modo migliore per farlo?

AGGIORNAMENTO: Ecco la storia precedente: sto cercando di recuperare il contatore di idoneità successivo da un tavolo figlio (sistema legacy, non farmi iniziare ...). La prima riga di idoneità per ogni paziente è sempre 1, la seconda è 2, ecc. (Ovviamente questa non è la chiave primaria della tabella figlio). Quindi, sto selezionando il valore del contatore massimo esistente per un paziente e quindi aggiungendo 1 ad esso per creare una nuova riga. Quando non ci sono valori figlio esistenti, ho bisogno che la query restituisca 0 (quindi l'aggiunta di 1 mi darà un valore contatore di 1). Si noti che non voglio fare affidamento sul conteggio non elaborato delle righe figlio, nel caso in cui l'app legacy introduca lacune nei valori del contatore (possibile). Il mio male per aver cercato di rendere la domanda troppo generica.

Risposte:


206

Dal momento che DefaultIfEmptynon è implementato in LINQ to SQL, ho fatto una ricerca sull'errore che ha restituito e ho trovato un articolo affascinante che si occupa di set null in funzioni aggregate. Per riassumere ciò che ho trovato, puoi aggirare questa limitazione lanciando su un valore nulla all'interno della tua selezione. Il mio VB è un po 'arrugginito, ma penso che andrebbe qualcosa del genere:

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

O in C #:

var x = (from y in context.MyTable
         where y.MyField == value
         select (int?)y.MyCounter).Max();

1
Per correggere il VB, selezionare sarebbe "Seleziona CType (y.MyCounter, Integer?)". Devo fare un controllo originale per convertire Niente in 0 per i miei scopi, ma mi piace ottenere i risultati senza eccezioni.
gfrizzle,

2
Uno dei due overload di DefaultIfEmpty è supportato in LINQ to SQL, quello che non accetta parametri.
DamienG,

Forse queste informazioni non sono aggiornate, dato che ho appena testato con successo entrambe le forme di DefaultIfEmpty in LINQ to SQL
Neil,

3
@Neil: per favore, rispondi. DefaultIfEmpty non funziona per me: voglio il simbolo Maxdi a DateTime. Max(x => (DateTime?)x.TimeStamp)ancora l'unico modo ..
duedl0r

1
Sebbene DefaultIfEmpty sia ora implementato in LINQ to SQL, questa risposta rimane IMO migliore, poiché l'utilizzo di DefaultIfEmpty genera un'istruzione SQL "SELECT MyCounter" che restituisce una riga per ogni valore che viene sommato , mentre questa risposta risulta in MAX (MyCounter) che restituisce un riga singola, sommata. (Testato in EntityFrameworkCore 2.1.3.)
Carl Sharman,

107

Ho appena avuto un problema simile, ma stavo usando i metodi di estensione LINQ in un elenco piuttosto che la sintassi della query. Il casting per un trucco Nullable funziona anche lì:

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

48

Sembra un caso per DefaultIfEmpty(segue il codice non testato):

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

Non ho familiarità con DefaultIfEmpty, ma ottengo "Impossibile formattare il nodo 'OptionalValue' per l'esecuzione come SQL" quando utilizzo la sintassi sopra. Ho anche provato a fornire un valore predefinito (zero), ma neanche quello mi è piaciuto.
gfrizzle,

Ah. Sembra che DefaultIfEmpty non sia supportato in LINQ to SQL. Potresti aggirare ciò lanciando prima un elenco con .ToList, ma questo è un notevole successo di prestazioni.
Jacob Proffitt,

3
Grazie, questo è esattamente quello che stavo cercando. Utilizzo dei metodi di estensione:var colCount = RowsEnumerable.Select(row => row.Cols.Count).DefaultIfEmpty().Max()
Jani,

35

Pensa a cosa stai chiedendo!

Il massimo di {1, 2, 3, -1, -2, -3} è ovviamente 3. Il massimo di {2} è ovviamente 2. Ma qual è il massimo dell'insieme vuoto {}? Ovviamente questa è una domanda insignificante. Il massimo dell'insieme vuoto semplicemente non è definito. Tentare di ottenere una risposta è un errore matematico. Il massimo di qualsiasi set deve essere esso stesso un elemento in quel set. L'insieme vuoto non ha elementi, quindi affermare che un determinato numero è il massimo di quell'insieme senza essere in quell'insieme è una contraddizione matematica.

Così come il comportamento corretto del computer genera un'eccezione quando il programmatore gli chiede di dividere per zero, così è il comportamento corretto del computer generare un'eccezione quando il programmatore gli chiede di prendere il massimo dell'insieme vuoto. La divisione per zero, prendendo il massimo del set vuoto, muovendo lo spacklerorke e guidando l'unicorno volante verso l'Isola che non c'è sono tutti insignificanti, impossibili, indefiniti.

Ora, cosa vuoi veramente fare?


Un buon punto: aggiornerò la mia domanda a breve con quei dettagli. Basti dire che so che voglio 0 quando non ci sono record tra cui scegliere, il che ha sicuramente un impatto sull'eventuale soluzione.
gfrizzle,

17
Tento spesso di far volare il mio unicorno nell'Isola che non c'è e offendo il tuo suggerimento che i miei sforzi siano insignificanti e indefiniti.
Chris Shouts,

2
Non penso che questa argomentazione sia giusta. È chiaramente linq-to-sql, e in sql Max su zero righe è definito come null, no?
duedl0r

4
Linq dovrebbe generalmente produrre risultati identici sia che la query sia eseguita in memoria sugli oggetti sia che la query sia eseguita nel database su righe. Le query Linq sono query Linq e devono essere eseguite fedelmente indipendentemente dall'adattatore in uso.
yfeldblum,

1
Mentre sono in teoria d'accordo sul fatto che i risultati di Linq dovrebbero essere identici se eseguiti in memoria o in sql, quando in realtà scavi un po 'più a fondo, scopri perché questo non può essere sempre così. Le espressioni Linq sono tradotte in sql usando la traduzione di espressioni complesse. Non è una semplice traduzione individuale. Una differenza è il caso di null. In C # "null == null" è true. In SQL, le corrispondenze "null == null" sono incluse per i join esterni ma non per i join interni. Tuttavia, i join interni sono quasi sempre ciò che desideri, quindi sono quelli predefiniti. Ciò provoca possibili differenze nel comportamento.
Curtis Yallop,

25

Puoi sempre aggiungere Double.MinValuealla sequenza. Ciò garantirebbe che vi sia almeno un elemento e Maxlo restituirebbe solo se è effettivamente il minimo. Per determinare quale opzione è più efficiente ( Concat, FirstOrDefaulto Take(1)), è necessario eseguire un adeguato benchmarking.

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

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

Se l'elenco contiene degli elementi (cioè non vuoti), prenderà il massimo del campo MyCounter, altrimenti restituirà 0.


3
Non verranno eseguite 2 query?
andreapier,

10

Da .Net 3.5 è possibile utilizzare DefaultIfEmpty () passando il valore predefinito come argomento. Qualcosa come uno dei seguenti modi:

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

Il primo è consentito quando si esegue una query su una colonna NOT NULL e il secondo è il modo in cui è stato utilizzato per eseguire una query su una colonna NULLABLE. Se si utilizza DefaultIfEmpty () senza argomenti, il valore predefinito sarà quello definito per il tipo di output, come si può vedere nella Tabella dei valori predefiniti .

La SELEZIONE risultante non sarà così elegante ma è accettabile.

Spero che sia d'aiuto.


7

Penso che il problema sia cosa vuoi che accada quando la query non ha risultati. Se questo è un caso eccezionale, avvolgo la query in un blocco try / catch e gestisco l'eccezione generata dalla query standard. Se è accettabile che la query non restituisca risultati, è necessario capire quale risultato si desidera ottenere in quel caso. Potrebbe essere la risposta di @ David (o qualcosa di simile funzionerà). Cioè, se il MAX sarà sempre positivo, potrebbe essere sufficiente inserire un valore "cattivo" noto nell'elenco che verrà selezionato solo se non ci sono risultati. In generale, mi aspetterei che una query che sta recuperando un massimo abbia alcuni dati su cui lavorare e seguirò il percorso try / catch poiché altrimenti sei sempre costretto a verificare se il valore ottenuto è corretto o meno. IO'

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

Nel mio caso, la restituzione di righe non avviene più frequentemente (sistema legacy, il paziente può o meno avere precedente idoneità, blah blah blah). Se questo fosse un caso più eccezionale, probabilmente seguirei questa strada (e potrei ancora non vedere molto meglio).
gfrizzle,

6

Un'altra possibilità potrebbe essere il raggruppamento, simile a come si potrebbe avvicinarlo in SQL raw:

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

L'unica cosa è (testare di nuovo in LINQPad) il passaggio al sapore VB LINQ genera errori di sintassi sulla clausola di raggruppamento. Sono sicuro che l'equivalente concettuale sia abbastanza facile da trovare, semplicemente non so come rifletterlo in VB.

L'SQL generato sarebbe qualcosa sulla falsariga di:

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

Il SELECT nidificato sembra malvagio, come se l'esecuzione della query recuperasse tutte le righe quindi selezionasse quella corrispondente dal set recuperato ... la domanda è se SQL Server ottimizza la query in qualcosa di paragonabile all'applicazione della clausola where al SELECT interno. Lo sto esaminando ora ...

Non ho una buona conoscenza dell'interpretazione dei piani di esecuzione in SQL Server, ma sembra che quando la clausola WHERE si trova sul SELECT esterno, il numero di righe effettive risultanti in quel passaggio siano tutte le righe nella tabella, rispetto solo alle righe corrispondenti quando la clausola WHERE è sul SELECT interno. Detto questo, sembra che solo l'1% dei costi sia spostato al passaggio successivo quando vengono considerate tutte le righe e in entrambi i casi solo una riga torna da SQL Server, quindi forse non è una grande differenza nel grande schema delle cose .


6

Litt tardi, ma avevo la stessa preoccupazione ...

Sostituendo il tuo codice dal post originale, vuoi che il massimo dell'insieme S sia definito da

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

Tenendo conto del tuo ultimo commento

Basti dire che so che voglio 0 quando non ci sono record tra cui scegliere, il che ha sicuramente un impatto sull'eventuale soluzione

Posso riformulare il tuo problema come: vuoi il massimo di {0 + S}. E sembra che la soluzione proposta con concat sia semanticamente quella giusta :-)

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

3

Perché non qualcosa di più diretto come:

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

1

Una differenza interessante che sembra degno di nota è che mentre FirstOrDefault e Take (1) generano lo stesso SQL (secondo LINQPad, comunque), FirstOrDefault restituisce un valore - il valore predefinito - quando non ci sono righe corrispondenti e restituisce Take (1) nessun risultato ... almeno in LINQPad.


1

Solo per far sapere a tutti là fuori che sta usando Linq alle Entità i metodi sopra non funzioneranno ...

Se provi a fare qualcosa del genere

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

Genererà un'eccezione:

System.NotSupportedException: il tipo di nodo di espressione LINQ 'NewArrayInit' non è supportato in LINQ to Entities.

Suggerirei solo di fare

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

E FirstOrDefaultrestituirà 0 se l'elenco è vuoto.


L'ordinazione può comportare un grave peggioramento delle prestazioni con grandi set di dati. È un modo molto inefficiente per trovare un valore massimo.
Peter Bruins,

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

1

Ho eliminato un MaxOrDefaultmetodo di estensione. Non c'è molto da fare, ma la sua presenza in Intellisense è un utile promemoria che Maxsu una sequenza vuota causerà un'eccezione. Inoltre, il metodo consente di specificare il valore predefinito, se necessario.

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

0

Per Entity Framework e Linq to SQL possiamo raggiungere questo obiettivo definendo un metodo di estensione che modifica un metodo Expressionpassato a 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);
    }
}

Uso:

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

La query generata è identica, funziona esattamente come una normale chiamata al IQueryable<T>.Max(...)metodo, ma se non ci sono record restituisce un valore predefinito di tipo T invece di generare un'eccezione


-1

Ho appena avuto un problema simile, i test delle mie unità hanno superato Max () ma non sono riusciti quando sono stati eseguiti su un database live.

La mia soluzione era quella di separare la query dalla logica eseguita, non unirle in una query.
Avevo bisogno di una soluzione per lavorare nei test unitari usando Linq-object (in Linq-object Max () funziona con valori null) e Linq-sql durante l'esecuzione in un ambiente live.

(Derido il Select () nei miei test)

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

Meno efficiente? Probabilmente.

Mi interessa, a condizione che la mia app non cada la prossima volta? No.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.