¿Máximo o predeterminado?

176

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

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

Recibo 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 para 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 hagas empezar ...). 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). Entonces, selecciono el valor máximo de contador existente para un paciente, y luego agrego 1 para crear una nueva fila. Cuando no hay valores secundarios existentes, necesito que la consulta devuelva 0 (por lo tanto, agregar 1 me dará un valor de contador de 1). Tenga en cuenta que no quiero confiar en el recuento bruto de las filas secundarias, en caso de que la aplicación heredada presente huecos en los valores del contador (posible). Mi mal por tratar de hacer la pregunta demasiado genérica.

gfrizzle
fuente

Respuestas:

206

Como DefaultIfEmptyno está implementado en LINQ to SQL, hice una búsqueda en el error que devolvió y encontré un artículo fascinante que trata con conjuntos nulos en funciones agregadas. Para resumir lo que encontré, puede sortear esta limitación enviando a un nulo dentro de su selección. Mi VB está un poco oxidado, pero creo que sería algo así:

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();
Jacob Proffitt
fuente
1
Para corregir el VB, Select sería "Select CType (y.MyCounter, Integer?)". Tengo que hacer una verificación original para convertir Nothing a 0 para mis propósitos, pero me gusta obtener los resultados sin una excepción.
gfrizzle
2
Una de las dos sobrecargas de DefaultIfEmpty es compatible con LINQ to SQL, la que no toma parámetros.
DamienG
Posiblemente esta información está desactualizada, ya que probé con éxito ambas formas de DefaultIfEmpty en LINQ to SQL
Neil
3
@Neil: por favor responda. DefaultIfEmpty no funciona para mí: quiero el Maxde a DateTime. Max(x => (DateTime?)x.TimeStamp)sigue siendo la única manera ..
duedl0r
1
Aunque DefaultIfEmpty ahora se implementa en LINQ to SQL, esta respuesta sigue siendo IMO mejor, ya que el uso de DefaultIfEmpty da como resultado una instrucción SQL 'SELECT MyCounter' que devuelve una fila para cada valor que se suma , mientras que esta respuesta da como resultado MAX (MyCounter) que devuelve un sola fila sumada. (Probado en EntityFrameworkCore 2.1.3.)
Carl Sharman el
107

Simplemente tuve un problema similar, pero estaba usando métodos de extensión LINQ en una lista en lugar de la sintaxis de consulta. El lanzamiento a un truco de Nullable también funciona allí:

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

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

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter).DefaultIfEmpty.Max
Jacob Proffitt
fuente
No estoy familiarizado con DefaultIfEmpty, pero aparece "No se pudo formatear el nodo 'OpcionalValor' para su ejecución como SQL" al usar la sintaxis anterior. También intenté proporcionar un valor predeterminado (cero), pero tampoco me gustó.
gfrizzle
Ah Parece que DefaultIfEmpty no es compatible con LINQ to SQL. Puede evitar eso lanzando primero a una lista con .ToList, pero eso es un éxito significativo en el rendimiento.
Jacob Proffitt
3
Gracias, esto es exactamente lo que estaba buscando. Usando métodos de extensión:var colCount = RowsEnumerable.Select(row => row.Cols.Count).DefaultIfEmpty().Max()
Jani
35

¡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 es correcto que la computadora arroje una excepción cuando el programador le pide que se divida entre cero, así es correcto que la computadora arroje una excepción cuando el programador 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 hacia Neverland no tiene sentido, es imposible, no está definido.

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

Yfeldblum
fuente
Buen punto: actualizaré mi pregunta en breve con esos detalles. Baste decir que sé que quiero 0 cuando no hay registros para seleccionar, lo que definitivamente tiene un impacto en la solución final.
gfrizzle
17
Con frecuencia intento volar mi unicornio a Nunca Jamás, y me ofende su sugerencia de que mis esfuerzos no tienen sentido e indefinidos.
Chris grita el
2
No creo que esta argumentación sea correcta. Es claro linq-to-sql, y en sql Max sobre cero filas se define como nulo, ¿no?
duedl0r
44
Linq generalmente debería producir resultados idénticos si la consulta se ejecuta en memoria en objetos o si la consulta se ejecuta en la base de datos en filas. Las consultas de Linq son consultas de Linq y deben ejecutarse fielmente, independientemente del adaptador que esté en uso.
yfeldblum
1
Si bien estoy de acuerdo en teoría que los resultados de Linq deberían ser idénticos, ya sea que se ejecuten en memoria o en sql, cuando realmente profundizas un poco más, descubres por qué esto no siempre puede ser así. Las expresiones de linq se traducen a sql mediante la traducción de expresiones complejas. No es una traducción simple de uno a uno. Una diferencia es el caso de nulo. En C # "null == null" es verdadero. En SQL, las coincidencias "null == null" se incluyen para combinaciones externas pero no para combinaciones internas. Sin embargo, las uniones internas son casi siempre lo que desea, por lo que son las predeterminadas. Esto provoca posibles diferencias en el comportamiento.
Curtis Yallop
25

Siempre puedes agregar Double.MinValuea la secuencia. Esto garantizaría que haya al menos un elemento y Maxlo devolvería solo si en realidad es el mínimo. Para determinar qué opción es más eficiente ( Concat, FirstOrDefaulto Take(1)), debe realizar una evaluación comparativa adecuada.

double x = context.MyTable
    .Where(y => y.MyField == value)
    .Select(y => y.MyCounter)
    .Concat(new double[]{Double.MinValue})
    .Max();
David Schmitt
fuente
10
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.

beastieboy
fuente
3
¿Esto no ejecutará 2 consultas?
andreapier
10

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

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

La primera está permitida cuando consulta una columna NO NULL y la segunda es la forma en que se utiliza para consultar una columna NULLABLE. Si usa DefaultIfEmpty () sin argumentos, el valor predeterminado será el definido para el tipo de salida, como puede ver en la Tabla de valores predeterminados .

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

Espero eso ayude.

Fernando Brustolin
fuente
7

Creo que el problema es qué quieres que suceda cuando la consulta no tiene 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, debe averiguar cuál quiere que sea 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 que una consulta que está recuperando un máximo tenga algunos datos para trabajar e iría a la ruta try / catch ya que de lo contrario siempre se verá obligado a verificar si el valor que obtuvo es correcto o no. YO'

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
fuente
En mi caso, la devolución de ninguna fila ocurre con mayor frecuencia (sistema heredado, el paciente puede o no tener elegibilidad previa, bla, bla, bla). Si este fuera un caso más excepcional, probablemente seguiría esta ruta (y aún así, no veo mucho mejor).
gfrizzle
6

Otra posibilidad sería la agrupación, similar a la forma en que podría abordarlo en SQL sin formato:

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 VB LINQ da errores de sintaxis 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 similar a:

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 anidado parece repulsivo, como si la ejecución de la consulta recuperara todas las filas y luego seleccionara 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 estoy bien 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 dan como resultado ese paso es todas las filas de la tabla, en comparación con solo las filas coincidentes cuando la cláusula WHERE está en el SELECT interno. Dicho esto, parece que solo el 1% del costo se desplaza al siguiente paso cuando se consideran todas las filas, y de cualquier manera solo una fila regresa del SQL Server, por lo que tal vez no sea una gran diferencia en el gran esquema de las cosas .

Rex Miller
fuente
6

un poco tarde, pero tenía la misma preocupación ...

Reformulando su código de la publicación original, desea el máximo del conjunto S definido por

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

Teniendo en cuenta tu ú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 su problema como: Desea 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();
Dom Ribaut
fuente
3

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

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

Una diferencia interesante que parece destacarse es que, si bien FirstOrDefault y Take (1) generan el mismo SQL (de acuerdo con LINQPad, de todos modos), FirstOrDefault devuelve un valor, el valor predeterminado, cuando no hay filas coincidentes y Take (1) devuelve sin resultados ... al menos en LINQPad.

Rex Miller
fuente
1

Solo para que todos sepan que está utilizando Linq to Entities, los métodos anteriores no funcionarán ...

Si intentas 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 tipo de nodo de expresión LINQ 'NewArrayInit' no se admite en LINQ to Entities.

Sugeriría simplemente hacer

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

Y FirstOrDefaultdevolverá 0 si su lista está vacía.

Nada
fuente
Los pedidos pueden provocar una grave degradación del rendimiento con grandes conjuntos de datos. Es una forma muy ineficiente de encontrar un valor máximo.
Peter Bruins el
1
decimal Max = (decimal?)(context.MyTable.Select(e => e.MyCounter).Max()) ?? 0;
jong su.
fuente
1

He encontrado un MaxOrDefaultmétodo de extensión. No hay mucho, pero su presencia en Intellisense es un recordatorio útil de que Maxen 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<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
fuente
0

Para Entity Framework y Linq to SQL podemos lograr esto definiendo un método de extensión que modifique un método Expressionpasado 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 consulta generada es idéntica, funciona igual que una llamada normal al IQueryable<T>.Max(...)método, pero si no hay registros, devuelve un valor predeterminado de tipo T en lugar de arrojar una excepción

Ashot Muradian
fuente
-1

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

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

(Me burlo de Seleccionar () 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? No

Seb
fuente