Max ou par défaut?

176

Quelle est la meilleure façon d'obtenir la valeur Max à partir d'une requête LINQ qui peut ne renvoyer aucune ligne? Si je fais juste

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

J'obtiens une erreur lorsque la requête ne renvoie aucune ligne. je pourrais faire

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

mais cela semble un peu obtus pour une demande aussi simple. Est-ce que je manque une meilleure façon de le faire?

MISE À JOUR: Voici l'histoire: j'essaie de récupérer le prochain compteur d'éligibilité à partir d'une table enfant (système hérité, ne me lancez pas ...). La première ligne d'éligibilité pour chaque patient est toujours 1, la seconde est 2, etc. (ce n'est évidemment pas la clé primaire de la table enfant). Donc, je sélectionne la valeur de compteur existante maximale pour un patient, puis j'y ajoute 1 pour créer une nouvelle ligne. Lorsqu'il n'y a aucune valeur enfant existante, j'ai besoin que la requête renvoie 0 (donc l'ajout de 1 me donnera une valeur de compteur de 1). Notez que je ne veux pas me fier au nombre brut de lignes enfants, au cas où l'application héritée introduirait des lacunes dans les valeurs de compteur (possible). Mon mal d'essayer de rendre la question trop générique.

gfrizzle
la source

Réponses:

206

Comme il DefaultIfEmptyn'est pas implémenté dans LINQ to SQL, j'ai effectué une recherche sur l'erreur qu'il a renvoyée et j'ai trouvé un article fascinant qui traite des ensembles nuls dans les fonctions d'agrégation. Pour résumer ce que j'ai trouvé, vous pouvez contourner cette limitation en castant en un nullable dans votre sélection. Mon VB est un peu rouillé, mais je pense que ça donnerait quelque chose comme ça:

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

Ou en C #:

var x = (from y in context.MyTable
         where y.MyField == value
         select (int?)y.MyCounter).Max();
Jacob Proffitt
la source
1
Pour corriger le VB, le Select serait "Select CType (y.MyCounter, Integer?)". Je dois faire une vérification originale pour convertir Nothing en 0 pour mes besoins, mais j'aime obtenir les résultats sans exception.
gfrizzle
2
L'une des deux surcharges de DefaultIfEmpty est prise en charge dans LINQ to SQL - celle qui ne prend pas de paramètres.
DamienG
Peut-être que ces informations sont obsolètes, car je viens de tester avec succès les deux formes de DefaultIfEmpty dans LINQ to SQL
Neil
3
@Neil: merci de répondre. DefaultIfEmpty ne fonctionne pas pour moi: je veux le Maxd'un DateTime. Max(x => (DateTime?)x.TimeStamp)toujours le seul moyen ..
duedl0r
1
Bien que DefaultIfEmpty soit maintenant implémenté dans LINQ to SQL, cette réponse reste meilleure IMO, car l'utilisation de DefaultIfEmpty entraîne une instruction SQL `` SELECT MyCounter '' qui renvoie une ligne pour chaque valeur additionnée , alors que cette réponse se traduit par MAX (MyCounter) qui renvoie un ligne unique et additionnée. (Testé dans EntityFrameworkCore 2.1.3.)
Carl Sharman
107

J'ai juste eu un problème similaire, mais j'utilisais des méthodes d'extension LINQ sur une liste plutôt qu'une syntaxe de requête. Le casting pour une astuce Nullable fonctionne également ici:

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

Sonne comme un cas pour DefaultIfEmpty(le code non testé suit):

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter).DefaultIfEmpty.Max
Jacob Proffitt
la source
Je ne suis pas familier avec DefaultIfEmpty, mais j'obtiens "Impossible de formater le nœud 'OptionalValue' pour une exécution en SQL" lors de l'utilisation de la syntaxe ci-dessus. J'ai également essayé de fournir une valeur par défaut (zéro), mais cela n'a pas plu non plus.
gfrizzle
Ah. Il semble que DefaultIfEmpty n'est pas pris en charge dans LINQ to SQL. Vous pouvez contourner cela en effectuant d'abord un casting dans une liste avec .ToList, mais c'est un impact significatif sur les performances.
Jacob Proffitt
3
Merci, c'est exactement ce que je cherchais. Utilisation des méthodes d'extension:var colCount = RowsEnumerable.Select(row => row.Cols.Count).DefaultIfEmpty().Max()
Jani
35

Pensez à ce que vous demandez!

Le maximum de {1, 2, 3, -1, -2, -3} est évidemment 3. Le maximum de {2} est évidemment 2. Mais quel est le maximum de l'ensemble vide {}? De toute évidence, c'est une question dénuée de sens. Le maximum de l'ensemble vide n'est tout simplement pas défini. Tenter d'obtenir une réponse est une erreur mathématique. Le maximum de tout ensemble doit lui-même être un élément de cet ensemble. L'ensemble vide n'a pas d'éléments, donc prétendre qu'un nombre particulier est le maximum de cet ensemble sans être dans cet ensemble est une contradiction mathématique.

Tout comme il est correct pour l'ordinateur de lancer une exception lorsque le programmeur lui demande de diviser par zéro, il est donc correct pour l'ordinateur de lancer une exception lorsque le programmeur lui demande de prendre le maximum de l'ensemble vide. Division par zéro, prendre le maximum de l'ensemble vide, agiter le spacklerorke et chevaucher la licorne volante jusqu'à Neverland sont tous dénués de sens, impossibles, indéfinis.

Maintenant, que voulez-vous réellement faire?

Yfeldblum
la source
Bon point - je vais mettre à jour ma question sous peu avec ces détails. Autant dire que je sais que je veux 0 quand il n'y a pas d'enregistrements à sélectionner, ce qui a certainement un impact sur la solution éventuelle.
gfrizzle
17
J'essaie fréquemment de faire voler ma licorne à Neverland, et je suis offensé de votre suggestion selon laquelle mes efforts sont dénués de sens et indéfinis.
Chris Shouts
2
Je ne pense pas que cette argumentation soit juste. C'est clair linq-to-sql, et dans sql Max sur zéro ligne est défini comme nul, non?
duedl0r
4
Linq devrait généralement produire des résultats identiques, que la requête soit exécutée en mémoire sur des objets ou que la requête soit exécutée dans la base de données sur des lignes. Les requêtes Linq sont des requêtes Linq et doivent être exécutées fidèlement quel que soit l'adaptateur utilisé.
yfeldblum
1
Bien que je sois d'accord en théorie sur le fait que les résultats Linq devraient être identiques, qu'ils soient exécutés en mémoire ou en SQL, lorsque vous creusez un peu plus profondément, vous découvrez pourquoi cela ne peut pas toujours être le cas. Les expressions Linq sont traduites en SQL en utilisant une traduction d'expression complexe. Ce n'est pas une simple traduction individuelle. Une différence est le cas de null. En C #, "null == null" est vrai. En SQL, les correspondances «null == null» sont incluses pour les jointures externes mais pas pour les jointures internes. Cependant, les jointures internes sont presque toujours ce que vous voulez, c'est donc la valeur par défaut. Cela entraîne des différences de comportement possibles.
Curtis Yallop
25

Vous pouvez toujours ajouter Double.MinValueà la séquence. Cela garantirait qu'il y a au moins un élément et Maxne le renverrait que s'il s'agit réellement du minimum. Pour déterminer quelle option est plus efficace ( Concat, FirstOrDefaultou Take(1)), vous devez effectuer l' étalonnage adéquat.

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

Si la liste contient des éléments (c'est-à-dire non vide), elle prendra le maximum du champ MyCounter, sinon retournera 0.

beastieboy
la source
3
Cela ne va-t-il pas exécuter 2 requêtes?
andreapier
10

Depuis .Net 3.5, vous pouvez utiliser DefaultIfEmpty () en passant la valeur par défaut comme argument. Quelque chose comme l'une des manières suivantes:

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

Le premier est autorisé lorsque vous interrogez une colonne NOT NULL et le second est la façon dont un l'a utilisé pour interroger une colonne NULLABLE. Si vous utilisez DefaultIfEmpty () sans arguments, la valeur par défaut sera celle définie pour le type de votre sortie, comme vous pouvez le voir dans le tableau des valeurs par défaut .

Le SELECT résultant ne sera pas si élégant mais il est acceptable.

J'espère que ça aide.

Fernando Brustolin
la source
7

Je pense que le problème est de savoir ce que vous voulez qu'il se passe lorsque la requête n'a pas de résultats. S'il s'agit d'un cas exceptionnel, j'envelopperais la requête dans un bloc try / catch et gérerais l'exception générée par la requête standard. S'il est acceptable que la requête ne renvoie aucun résultat, vous devez déterminer ce que vous voulez que le résultat soit dans ce cas. Il se peut que la réponse de @ David (ou quelque chose de similaire fonctionne). Autrement dit, si le MAX sera toujours positif, alors il peut être suffisant d'insérer une "mauvaise" valeur connue dans la liste qui ne sera sélectionnée que s'il n'y a pas de résultats. Généralement, je m'attendrais à ce qu'une requête qui récupère un maximum ait des données sur lesquelles travailler et j'irais sur la route try / catch, sinon vous êtes toujours obligé de vérifier si la valeur que vous avez obtenue est correcte ou non. JE'

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
la source
Dans mon cas, ne renvoyer aucune ligne se produit plus fréquemment qu'autrement (système hérité, le patient peut ou non avoir une éligibilité antérieure, bla bla bla). Si c'était un cas plus exceptionnel, j'irais probablement dans cette voie (et je pourrais encore, ne voyant pas beaucoup mieux).
gfrizzle
6

Une autre possibilité serait le regroupement, similaire à la façon dont vous pourriez l'aborder en SQL brut:

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

La seule chose est (de tester à nouveau dans LINQPad) le passage à la version VB LINQ donne des erreurs de syntaxe sur la clause de regroupement. Je suis sûr que l'équivalent conceptuel est assez facile à trouver, je ne sais tout simplement pas comment le refléter dans VB.

Le SQL généré serait quelque chose du genre:

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

Le SELECT imbriqué semble capricieux, comme l'exécution de la requête récupérerait toutes les lignes puis sélectionnerait celle qui correspond dans l'ensemble récupéré ... la question est de savoir si SQL Server optimise la requête en quelque chose de comparable à l'application de la clause where au SELECT interne. Je regarde ça maintenant ...

Je ne connais pas bien l'interprétation des plans d'exécution dans SQL Server, mais il semble que lorsque la clause WHERE se trouve sur le SELECT externe, le nombre de lignes réelles résultant de cette étape correspond à toutes les lignes de la table, par rapport uniquement aux lignes correspondantes lorsque la clause WHERE est sur le SELECT interne. Cela dit, il semble que seulement 1% du coût soit transféré à l'étape suivante lorsque toutes les lignes sont prises en compte, et de toute façon, une seule ligne revient jamais du serveur SQL, alors peut-être que ce n'est pas une si grande différence dans le grand schéma des choses .

Rex Miller
la source
6

peu tard, mais j'avais le même souci ...

En reformulant votre code à partir du message d'origine, vous voulez le maximum de l'ensemble S défini par

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

Tenir compte de votre dernier commentaire

Autant dire que je sais que je veux 0 quand il n'y a pas d'enregistrements à sélectionner, ce qui a certainement un impact sur la solution éventuelle

Je peux reformuler votre problème comme suit: Vous voulez le maximum de {0 + S}. Et il semble que la solution proposée avec concat soit sémantiquement la bonne :-)

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

Pourquoi pas quelque chose de plus direct comme:

Dim x = context.MyTable.Max(Function(DataItem) DataItem.MyField = Value)
légal
la source
1

Une différence intéressante qui semble à noter est que si FirstOrDefault et Take (1) génèrent le même SQL (selon LINQPad, de toute façon), FirstOrDefault renvoie une valeur - la valeur par défaut - lorsqu'il n'y a pas de lignes correspondantes et Take (1) renvoie aucun résultat ... au moins dans LINQPad.

Rex Miller
la source
1

Juste pour que tout le monde sache que l'utilisation de Linq to Entities, les méthodes ci-dessus ne fonctionneront pas ...

Si vous essayez de faire quelque chose comme

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

Cela lèvera une exception:

System.NotSupportedException: le type de nœud d'expression LINQ 'NewArrayInit' n'est pas pris en charge dans LINQ to Entities.

Je suggérerais juste de faire

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

Et le FirstOrDefaultrenverra 0 si votre liste est vide.

Rien
la source
La commande peut entraîner une sérieuse dégradation des performances avec de grands ensembles de données. C'est un moyen très inefficace de trouver une valeur maximale.
Peter Bruins
1
decimal Max = (decimal?)(context.MyTable.Select(e => e.MyCounter).Max()) ?? 0;
jong su.
la source
1

J'ai mis au point une MaxOrDefaultméthode d'extension. Il n'y a pas grand-chose à faire mais sa présence dans Intellisense est un rappel utile que Maxsur une séquence vide provoquera une exception. En outre, la méthode permet de spécifier la valeur par défaut si nécessaire.

    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
la source
0

Pour Entity Framework et Linq to SQL, nous pouvons y parvenir en définissant une méthode d'extension qui modifie une méthode Expressionpassée à 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);
    }
}

Usage:

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

La requête générée est identique, elle fonctionne comme un appel normal à une IQueryable<T>.Max(...)méthode, mais s'il n'y a pas d'enregistrements, elle renvoie une valeur par défaut de type T au lieu de lancer une exception

Ashot Muradian
la source
-1

J'ai juste eu un problème similaire, mes tests unitaires ont réussi avec Max () mais ont échoué lorsqu'ils sont exécutés sur une base de données en direct.

Ma solution était de séparer la requête de la logique en cours d'exécution, et non de les joindre en une seule requête.
J'avais besoin d'une solution pour travailler dans des tests unitaires en utilisant des objets Linq (dans Linq-objects Max () fonctionne avec des valeurs nulles) et Linq-sql lors de l'exécution dans un environnement en direct.

(Je me moque du Select () dans mes 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);

Moins efficace? Probablement.

Dois-je m'en soucier, tant que mon application ne tombera pas la prochaine fois? Nan.

Seb
la source