Max o Default?

¿Cuál es la mejor manera de obtener el valor máximo de una consulta LINQ que puede devolver ninguna fila? Si solo hago

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

Aparece un error cuando la consulta no devuelve filas. Yo podría hacer

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

pero eso se siente un poco obtuso por una solicitud tan simple. ¿Me estoy perdiendo una mejor manera de hacerlo?

ACTUALIZACIÓN: Aquí está la historia de fondo: estoy tratando de recuperar el siguiente contador de elegibilidad de una tabla secundaria (sistema heredado, no me inicie …). La primera fila de elegibilidad para cada paciente es siempre 1, la segunda es 2, etc. (obviamente esta no es la clave principal de la tabla secundaria). Por lo tanto, estoy seleccionando el máximo valor de contador existente para un paciente y luego agregué 1 para crear una nueva fila. Cuando no hay valores secundarios existentes, necesito que la consulta devuelva 0 (de modo que al agregar 1 obtendré un valor de contador de 1). Tenga en cuenta que no deseo confiar en el recuento bruto de filas secundarias, en caso de que la aplicación heredada introduzca vacíos en los valores del contador (posible). Es malo para tratar de hacer la pregunta demasiado genérica.

Como DefaultIfEmpty no está implementado en LINQ to SQL, hice una búsqueda sobre el error que devolvió y encontré un artículo fascinante que trata sobre conjuntos nulos en funciones agregadas. Para resumir lo que encontré, puedes soslayar esta limitación al enviar a una opción que se puede anular dentro de tu selección. Mi VB está un poco oxidado, pero creo que sería algo como esto:

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

O en C #:

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

Acabo de tener un problema similar, pero estaba usando los métodos de extensión LINQ en una lista en lugar de la syntax de la consulta. El casting a un truco Nullable también funciona allí:

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

Suena como un caso para DefaultIfEmpty (sigue el código no probado):

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

¡Piensa en lo que estás preguntando!

El máximo de {1, 2, 3, -1, -2, -3} es obviamente 3. El máximo de {2} es obviamente 2. Pero, ¿cuál es el máximo del conjunto vacío {}? Obviamente esa es una pregunta sin sentido. El máximo del conjunto vacío simplemente no está definido. Intentar obtener una respuesta es un error matemático. El máximo de cualquier conjunto debe ser un elemento en ese conjunto. El conjunto vacío no tiene elementos, por lo que afirmar que un número particular es el máximo de ese conjunto sin estar en ese conjunto es una contradicción matemática.

Así como el comportamiento correcto de la computadora arroja una excepción cuando el progtwigdor le pide que se divida por cero, entonces es un comportamiento correcto que la computadora arroje una excepción cuando el progtwigdor le pide que tome el máximo del conjunto vacío. La división por cero, tomar el máximo del conjunto vacío, mover el spacklerorke, y montar el unicornio volador a Neverland son todos sin sentido, imposible, indefinido.

Ahora, ¿qué es lo que realmente quieres hacer?

Siempre Double.MinValue agregar Double.MinValue a la secuencia. Esto aseguraría que haya al menos un elemento y Max lo devolverá solo si es realmente el mínimo. Para determinar qué opción es más eficiente ( Concat , FirstOrDefault o Take(1) ), debe realizar un benchmarking adecuado.

 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; 

Si la lista tiene algún elemento (es decir, no está vacío), tomará el máximo del campo MyCounter, de lo contrario devolverá 0.

Desde .Net 3.5, puede usar DefaultIfEmpty () pasando el valor predeterminado como argumento. Algo así como una de las siguientes maneras:

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

El primero está permitido cuando consulta una columna NOT NULL y el segundo es la forma en que lo usó para consultar una columna NULLABLE. Si usa DefaultIfEmpty () sin argumentos, el valor predeterminado será el definido para el tipo de su salida, como puede ver en la Tabla de valores predeterminados .

El SELECT resultante no será tan elegante, pero es aceptable.

Espero eso ayude.

Creo que el problema es qué quieres que suceda cuando la consulta no tenga resultados. Si este es un caso excepcional, envolvería la consulta en un bloque try / catch y manejaría la excepción que genera la consulta estándar. Si está bien que la consulta no devuelva ningún resultado, entonces necesita averiguar cuál es el resultado en ese caso. Puede ser que la respuesta de @David (o algo similar funcione). Es decir, si el MAX siempre será positivo, entonces puede ser suficiente insertar un valor “malo” conocido en la lista que solo se seleccionará si no hay resultados. En general, esperaría una consulta que está recuperando un máximo para tener algunos datos para trabajar y yo iría a la ruta de prueba / captura ya que de lo contrario siempre estará obligado a verificar si el valor que obtuvo es correcto o no. Prefiero que el caso no excepcional solo pueda usar el valor obtenido.

 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 

Otra posibilidad sería la agrupación, similar a cómo se puede abordar en SQL crudo:

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

Lo único es (probar nuevamente en LINQPad) cambiar al sabor de VB LINQ proporciona errores de syntax en la cláusula de agrupación. Estoy seguro de que el equivalente conceptual es bastante fácil de encontrar, simplemente no sé cómo reflejarlo en VB.

El SQL generado sería algo así como:

 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 

El SELECT nested parece asqueroso, al igual que la ejecución de la consulta recuperaría todas las filas y luego seleccionaría la correspondiente del conjunto recuperado … la pregunta es si SQL Server optimiza la consulta en algo comparable a aplicar la cláusula where al SELECT interno. Estoy investigando eso ahora …

No soy muy versado en la interpretación de planes de ejecución en SQL Server, pero parece que cuando la cláusula WHERE está en el SELECT externo, el número de filas reales que resultan en ese paso son todas las filas en la tabla, frente a solo las filas correspondientes cuando la cláusula WHERE está en el SELECCIONAR interno. Dicho esto, parece que solo el 1% del costo se desplaza al siguiente paso cuando se consideran todas las filas, y de cualquier forma solo una fila vuelve del servidor SQL, así que tal vez no sea tan importante en el gran esquema de cosas .

Poco después, pero tenía la misma preocupación …

Al reformular el código de la publicación original, desea que el máximo del conjunto S definido por

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

Tomando en cuenta su último comentario

Baste decir que sé que quiero 0 cuando no hay registros para seleccionar, lo que definitivamente tiene un impacto en la solución final

Puedo reformular tu problema como: quieres el máximo de {0 + S}. Y parece que la solución propuesta con concat es semánticamente la correcta 🙂

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

¿Por qué no algo más directo como:

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

Solo para que todos sepan que está usando Linq para Entidades, los métodos anteriores no funcionarán …

Si tratas de hacer algo como

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

Lanzará una excepción:

System.NotSupportedException: El nodo de expresión LINQ tipo ‘NewArrayInit’ no es compatible con LINQ to Entities ..

Sugeriría simplemente hacer

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

Y FirstOrDefault devolverá 0 si su lista está vacía.

Una diferencia interesante que parece digna de mención es que mientras FirstOrDefault y Take (1) generan el mismo SQL (de acuerdo con LINQPad, de todos modos), FirstOrDefault devuelve un valor, el predeterminado, cuando no hay filas coincidentes y Take (1) returns sin resultados … al menos en LINQPad.

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

Acabo de tener un problema similar, mis pruebas de unidad pasaron usando Max () pero fallaron cuando se ejecutaron en una base de datos en vivo.

Mi solución fue separar la consulta de la lógica que se realiza, no unirlos en una consulta.
Necesitaba una solución para trabajar en pruebas unitarias utilizando Linq-objects (en Linq-objects Max () funciona con nulos) y Linq-sql cuando se ejecuta en un entorno en vivo.

(Me burlo de Select () en mis pruebas)

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

¿Menos eficiente? Probablemente.

¿Me importa, siempre y cuando mi aplicación no se caiga la próxima vez? Nop.

He golpeado un método de extensión MaxOrDefault . No hay mucho para eso, pero su presencia en Intellisense es un recordatorio útil de que Max en una secuencia vacía causará una excepción. Además, el método permite especificar el valor predeterminado si es necesario.

  public static TResult MaxOrDefault(this IQueryable source, Expression> selector, TResult defaultValue = default (TResult)) where TResult : struct { return source.Max(selector) ?? defaultValue; } 

Uso, en una columna o propiedad de tipo int llamado Id :

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

Para Entity Framework y Linq to SQL podemos lograrlo definiendo un método de extensión que modifique una Expression pasada al método IQueryable.Max(...) :

 static class Extensions { public static TResult MaxOrDefault(this IQueryable source, Expression> selector) where TResult : struct { UnaryExpression castedBody = Expression.Convert(selector.Body, typeof(TResult?)); Expression> lambda = Expression.Lambda>(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 consulta generada es idéntica, funciona igual que una llamada normal al método IQueryable.Max(...) , pero si no hay registros, devuelve un valor predeterminado de tipo T en lugar de arrojar una excepción