Max o Defecto?


¿Cuál es la mejor manera de obtener el valor Máximo de una consulta LINQ que puede devolver sin 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. 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 petición tan simple. Me estoy perdiendo una mejor manera de hacerlo?

ACTUALIZACIÓN: Aquí está la historia de fondo: Estoy tratando de recuperar el próximo contador de elegibilidad de una tabla secundaria (sistema heredado, no me hagas empezar...). La primera fila de elegibilidad para cada paciente es siempre 1, el segundo es 2, etc. (obviamente esta no es la clave principal de la tabla secundaria). Por lo tanto, estoy seleccionando el valor máximo del contador existente para un paciente, y luego agregar 1 para crear una nueva fila. Cuando no hay valores secundarios existentes, necesito que la consulta devuelva 0 (por lo que agregar 1 me dará un valor de contador de 1). Tenga en cuenta que no quiero confiar en el recuento sin procesar de filas secundarias, en caso de que la aplicación heredada introduzca huecos en los valores del contador (posible). Mi mal por tratar de hacer la pregunta también generico.

Author: Winger Sendon, 2008-12-04

17 answers

Dado que DefaultIfEmpty no está implementado en LINQ a SQL, hice una búsqueda en el error que devolvió y encontré un fascinante artículo que trata con conjuntos nulos en funciones agregadas. Para resumir lo que encontré, puedes sortear esta limitación haciendo un casting a un nullable dentro de tu select. Mi VB está un poco oxidado, pero yo 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();
 199
Author: Jacob Proffitt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-05 12:12:32

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

int max = list.Max(i => (int?)i.MyCounter) ?? 0;
 90
Author: Eddie Deyo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-04-07 14:05:32

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
 46
Author: Jacob Proffitt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-05 12:13:22

¡Piensa en lo que estás pidiendo!

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 algún número en particular es el máximo de ese conjunto sin estar en ese conjunto es un contradicción matemática.

Así como es un comportamiento correcto para la computadora lanzar una excepción cuando el programador le pide que divida por cero, así es un comportamiento correcto para la computadora lanzar una excepción cuando el programador le pide que tome el máximo del conjunto vacío. División por cero, tomar el máximo del conjunto vacío, menear el spacklerorke, y montar el unicornio volador a Nunca Jamás son todos sin sentido, imposible, indefinido.

Ahora, ¿qué es lo que en realidad quieres hacer?

 35
Author: yfeldblum,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-04 22:09:44

Siempre se puede añadir Double.MinValue a la secuencia. Esto aseguraría que hay al menos un elemento y Max lo devolvería solo si es realmente el mínimo. Para determinar qué opción es más eficiente (Concat, FirstOrDefault o 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();
 23
Author: David Schmitt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-04 16:56:19
int max = list.Any() ? list.Max(i => i.MyCounter) : 0;

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

 9
Author: beastieboy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-02-07 09:41:33

Desde.Net 3.5 puedes 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();

La primera se permite cuando se consulta una columna NOT NULL y la segunda es la forma en que a la 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 Valores predeterminados .

La SELECCIÓN resultante no será así elegante pero aceptable.

Espero que ayude.

 9
Author: Fernando Brustolin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-01-07 17:11:16

Creo que el problema es qué quieres que suceda cuando la consulta no tiene resultados. Si este es un caso excepcional, entonces 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 resultados, entonces debe averiguar cuál desea que sea el resultado en ese caso. Puede ser que la respuesta de @David (o algo similar funcione). Es decir, si el MÁXIMO siempre será positivo, entonces puede ser suficiente para insertar un valor "malo" conocido en la lista que solo se seleccionará si no hay resultados. Generalmente, esperaría que una consulta que esté recuperando un máximo tenga algunos datos en los que 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. Preferiría que el caso no excepcional solo fuera capaz de 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
 7
Author: tvanfosson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-04 20:08:04

Otra posibilidad sería agrupar, similar a cómo se podría 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 (probando de nuevo en LINQPad) cambiar al tipo 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 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

La SELECCIÓN anidada parece asquerosa, como si la ejecución de la consulta recuperara todo filas a continuación, seleccione la que coincida con el conjunto recuperado... la pregunta es si SQL Server optimiza o no la consulta en algo comparable a aplicar la cláusula where a la SELECCIÓN interna. 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 la selección externa, el número de filas reales que resultan en ese paso son todas las filas de la tabla, en comparación con solo las filas coincidentes cuando la cláusula WHERE es en la SELECCIÓN interior. Dicho esto, parece que solo el costo del 1% se desplaza al siguiente paso cuando se consideran todas las filas, y de cualquier manera solo una fila regresa del servidor SQL, por lo que tal vez no sea una gran diferencia en el gran esquema de las cosas.

 6
Author: Rex Miller,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-04 18:53:18

Poco tarde, pero yo tenía la misma preocupación...

Reformulando tu código del post original, quieres el máximo del conjunto S definido por

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

Teniendo en cuenta su último comentario

Basta con decir que sé que quiero 0 cuando no hay registros para seleccionar de, que definitivamente tiene un impacto sobre la posible solución

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

var max = new[]{0}
          .Concat((From y In context.MyTable _
                   Where y.MyField = value _
                   Select y.MyCounter))
          .Max();
 6
Author: Dom Ribaut,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-07-29 13:20:26

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

Dim x = context.MyTable.Max(Function(DataItem) DataItem.MyField = Value)
 3
Author: legal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-05-03 14:06:22

Solo para que todos sepan que está usando Linq para Entidades, 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:

Sistema.NotSupportedException: El tipo de nodo de expresión LINQ 'NewArrayInit' no es compatible con las entidades LINQ to..

Yo sugeriría simplemente hacer

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

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

 2
Author: Nix,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-03-04 16:49:24

Una diferencia interesante que parece que vale la pena señalar es que mientras FirstOrDefault y Take(1) generan el mismo SQL (de acuerdo con LINQPad, de todos modos), FirstOrDefault devuelve un valor default el predeterminado when cuando no hay filas coincidentes y Take(1) no devuelve resultados... al menos en LINQPad.

 1
Author: Rex Miller,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-04 17:26:57
decimal Max = (decimal?)(context.MyTable.Select(e => e.MyCounter).Max()) ?? 0;
 1
Author: jong su.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-27 19:09:59

Acabo de tener un problema similar, mis pruebas unitarias 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 estaba realizando, no unirlas en una consulta.
Necesitaba una solución para trabajar en pruebas unitarias usando Linq-objects (en Linq-objects Max () trabaja con nulls) 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, mientras mi aplicación no se caiga la próxima vez? No.

 0
Author: Seb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-07-12 11:17:26

He embarazado un método de extensión MaxOrDefault. No hay mucho, 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<TSource, TResult>(this 
    IQueryable<TSource> source, Expression<Func<TSource, TResult?>> selector,
    TResult defaultValue = default (TResult)) where TResult : struct
    {
        return source.Max(selector) ?? defaultValue;
    }

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

    sequence.DefaultOrMax(s => (int?)s.Id);
 0
Author: Stephen Kennedy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-10-08 14:20:16

Para Entity Framework y Linq to SQL podemos lograr esto definiendo un método de extensión que modifica un Expression pasado a IQueryable<T>.Max(...) método:

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 como una llamada normal al método IQueryable<T>.Max(...), pero si no hay registros devuelve un valor predeterminado de tipo T en lugar de lanzar una excepción

 0
Author: Ashot Muradian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-06-08 17:05:23