Máximo ou Padrão?

176

Qual é a melhor maneira de obter o valor máximo de uma consulta LINQ que pode não retornar linhas? Se eu apenas fizer

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

Eu recebo um erro quando a consulta não retorna linhas. eu poderia fazer

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

mas isso parece um pouco obtuso para um pedido tão simples. Estou perdendo uma maneira melhor de fazer isso?

UPDATE: Aqui está a história de fundo: Estou tentando recuperar o próximo contador de elegibilidade de uma tabela filho (sistema legado, não me inicie ...). A primeira linha de elegibilidade para cada paciente é sempre 1, a segunda é 2, etc. (obviamente, essa não é a chave primária da tabela filho). Portanto, estou selecionando o valor máximo do contador existente para um paciente e adicionando 1 a ele para criar uma nova linha. Quando não há valores filho existentes, preciso que a consulta retorne 0 (portanto, adicionar 1 me fornecerá um valor de contador 1). Observe que não quero contar com a contagem bruta de linhas filhas, caso o aplicativo herdado introduza lacunas nos valores dos contadores (possível). Meu mal por tentar tornar a pergunta muito genérica.

gfrizzle
fonte

Respostas:

206

Como DefaultIfEmptynão foi implementado no LINQ to SQL, fiz uma pesquisa sobre o erro retornado e encontrei um artigo fascinante que lida com conjuntos nulos em funções agregadas. Para resumir o que encontrei, você pode contornar essa limitação convertendo para um valor nulo em seu select. Meu VB está um pouco enferrujado, mas acho que seria algo assim:

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

Ou em C #:

var x = (from y in context.MyTable
         where y.MyField == value
         select (int?)y.MyCounter).Max();
Jacob Proffitt
fonte
1
Para corrigir o VB, o Select seria "Select CType (y.MyCounter, Inteiro?)". Eu tenho que fazer uma verificação original para converter Nothing em 0 para meus propósitos, mas eu gosto de obter os resultados sem uma exceção.
Gfrizzle
2
Uma das duas sobrecargas do DefaultIfEmpty é suportada no LINQ to SQL - aquele que não aceita parâmetros.
DamienG 05/12/08
Possivelmente, essas informações estão desatualizadas, pois acabei de testar com êxito as duas formas de DefaultIfEmpty no LINQ to SQL
Neil
3
@ Neil: por favor, responda. DefaultIfEmpty não funciona para mim: eu quero o Maxde a DateTime. Max(x => (DateTime?)x.TimeStamp)ainda a única maneira ..
duedl0r
1
Embora o DefaultIfEmpty agora esteja implementado no LINQ to SQL, essa resposta permanece melhor no IMO, pois o uso do DefaultIfEmpty resulta em uma instrução SQL 'SELECT MyCounter' que retorna uma linha para cada valor agregado , enquanto essa resposta resulta em MAX (MyCounter) que retorna um linha única e resumida. (Testado no EntityFrameworkCore 2.1.3.)
Carl Sharman
107

Eu apenas tive um problema semelhante, mas estava usando os métodos de extensão LINQ em uma lista, em vez da sintaxe de consulta. A conversão para um truque Nullable também funciona lá:

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

Parece um caso para DefaultIfEmpty(código não testado a seguir):

Dim x = (From y In context.MyTable _
         Where y.MyField = value _
         Select y.MyCounter).DefaultIfEmpty.Max
Jacob Proffitt
fonte
Não estou familiarizado com DefaultIfEmpty, mas recebo "Não foi possível formatar o nó 'OptionalValue' para execução como SQL" ao usar a sintaxe acima. Também tentei fornecer um valor padrão (zero), mas também não gostei.
Gfrizzle
Ah Parece que DefaultIfEmpty não é suportado no LINQ to SQL. Você pode contornar isso lançando uma lista primeiro em .ToList, mas isso é um impacto significativo no desempenho.
27230 Jacob Proffitt
3
Obrigado, é exatamente isso que eu estava procurando. Usando métodos de extensão:var colCount = RowsEnumerable.Select(row => row.Cols.Count).DefaultIfEmpty().Max()
Jani
35

Pense no que você está perguntando!

O máximo de {1, 2, 3, -1, -2, -3} é obviamente 3. O máximo de {2} é obviamente 2. Mas qual é o máximo do conjunto vazio {}? Obviamente, essa é uma pergunta sem sentido. O máximo do conjunto vazio simplesmente não está definido. Tentar obter uma resposta é um erro matemático. O máximo de qualquer conjunto deve ser um elemento desse conjunto. O conjunto vazio não possui elementos, portanto, afirmar que um número específico é o máximo desse conjunto sem estar nesse conjunto é uma contradição matemática.

Assim como é correto o comportamento do computador lançar uma exceção quando o programador pede para dividir por zero, também é correto o comportamento do computador lançar uma exceção quando o programador pede que ele tire o máximo do conjunto vazio. Divisão por zero, pegar o máximo do conjunto vazio, agitar o spacklerorke e montar o unicórnio voador em Neverland são todos sem sentido, impossíveis, indefinidos.

Agora, o que você realmente quer fazer?

yfeldblum
fonte
Bom ponto - atualizarei minha pergunta em breve com esses detalhes. Basta dizer que eu sei que quero 0 quando não há registros para selecionar, o que definitivamente afeta a eventual solução.
Gfrizzle
17
Frequentemente, tento voar com meu unicórnio para Neverland e me ofendo com a sua sugestão de que meus esforços são sem sentido e indefinidos.
precisa
2
Eu não acho que essa argumentação esteja certa. É claramente linq-to-sql, e no sql o máximo de zero linhas é definido como nulo, não?
precisa saber é o seguinte
4
O Linq geralmente deve produzir resultados idênticos, independentemente de a consulta ser executada na memória em objetos ou se a consulta é executada no banco de dados em linhas. As consultas do Linq são consultas do Linq e devem ser executadas fielmente, independentemente de qual adaptador esteja em uso.
Yfeldblum
1
Embora eu concorde na teoria de que os resultados do Linq devem ser idênticos, sejam executados na memória ou no sql, quando você se aprofundar um pouco mais, descobre por que isso nem sempre pode ser assim. Expressões Linq são convertidas em sql usando tradução de expressão complexa. Não é uma tradução simples e individual. Uma diferença é o caso de nulo. Em C # "null == null" é verdadeiro. No SQL, as correspondências "null == null" são incluídas para junções externas, mas não para junções internas. No entanto, as junções internas são quase sempre o que você deseja, então elas são o padrão. Isso causa possíveis diferenças de comportamento.
Curtis Yallop
25

Você sempre pode adicionar Double.MinValueà sequência. Isso garantiria a existência de pelo menos um elemento e Maxo retornaria apenas se realmente for o mínimo. Para determinar qual opção é mais eficiente ( Concat, FirstOrDefaultou Take(1)), você deve executar comparações adequadas.

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

Se a lista tiver algum elemento (ou seja, não estiver vazio), será necessário o máximo do campo MyCounter, caso contrário, retornará 0.

beastieboy
fonte
3
Isso não executa duas consultas?
28613 Andreapier
10

Desde o .Net 3.5, você pode usar DefaultIfEmpty () passando o valor padrão como argumento. Algo como uma das seguintes maneiras:

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

A primeira é permitida quando você consulta uma coluna NOT NULL e a segunda é a maneira como a usou para consultar uma coluna NULLABLE. Se você usar DefaultIfEmpty () sem argumentos, o valor padrão será o definido para o tipo de sua saída, como você pode ver na Tabela de Valores Padrão .

O SELECT resultante não será tão elegante, mas é aceitável.

Espero que ajude.

Fernando Brustolin
fonte
7

Acho que o problema é o que você deseja que aconteça quando a consulta não tiver resultados. Se esse for um caso excepcional, eu agruparia a consulta em um bloco try / catch e manipularia a exceção que a consulta padrão gera. Se não for aceitável que a consulta retorne nenhum resultado, você precisará descobrir o que deseja que seja o resultado nesse caso. Pode ser que a resposta de @ David (ou algo semelhante funcione). Ou seja, se o MAX sempre for positivo, poderá ser suficiente inserir um valor "ruim" conhecido na lista que será selecionada apenas se não houver resultados. Geralmente, eu esperaria que uma consulta que estivesse recuperando o máximo tivesse alguns dados para trabalhar e seguisse a rota try / catch, caso contrário, você sempre será forçado a verificar se o valor obtido está correto ou não. EU'

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
fonte
No meu caso, retornar nenhuma linha acontece com mais frequência do que nunca (sistema legado, o paciente pode ou não ter elegibilidade anterior, blá blá blá). Se esse fosse um caso mais excepcional, eu provavelmente seguiria esse caminho (e ainda assim, sem estar vendo muito melhor).
Gfrizzle
6

Outra possibilidade seria agrupar, semelhante à maneira como você pode abordá-la no SQL bruto:

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

A única coisa é (testando novamente no LINQPad) a mudança para o sabor VB LINQ fornece erros de sintaxe na cláusula de agrupamento. Tenho certeza de que o equivalente conceitual é fácil de encontrar, simplesmente não sei como refleti-lo no VB.

O SQL gerado seria algo 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

O SELECT aninhado parece nojento, como se a execução da consulta recuperasse todas as linhas e selecionasse a correspondente do conjunto recuperado ... a questão é se o SQL Server otimiza ou não a consulta em algo comparável à aplicação da cláusula where ao SELECT interno. Estou investigando isso agora ...

Não sou versado em interpretar planos de execução no SQL Server, mas parece que quando a cláusula WHERE está no SELECT externo, o número de linhas reais resultantes dessa etapa é todas as linhas da tabela, em comparação apenas às linhas correspondentes quando a cláusula WHERE estiver no SELECT interno. Dito isso, parece que apenas 1% do custo foi alterado para a etapa seguinte quando todas as linhas são consideradas, e de qualquer maneira apenas uma linha volta do SQL Server, portanto, talvez não seja essa a grande diferença no grande esquema das coisas .

Rex Miller
fonte
6

litt tarde, mas eu tinha a mesma preocupação ...

Reescrevendo seu código da postagem original, você deseja o máximo do conjunto S definido por

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

Tendo em conta o seu último comentário

Basta dizer que eu sei que quero 0 quando não há registros para selecionar, o que definitivamente afeta a eventual solução

Posso reformular seu problema como: Você deseja o máximo de {0 + S}. E parece que a solução proposta com concat é semanticamente a correta :-)

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

Por que não algo mais direto como:

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

Uma diferença interessante que parece digna de nota é que, enquanto FirstOrDefault e Take (1) geram o mesmo SQL (de qualquer forma, de acordo com o LINQPad), FirstOrDefault retorna um valor - o padrão - quando não há linhas correspondentes e Take (1) retorna sem resultados ... pelo menos no LINQPad.

Rex Miller
fonte
1

Apenas para que todos saibam que está usando o Linq to Entities, os métodos acima não funcionarão ...

Se você tentar fazer algo como

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

Irá lançar uma exceção:

System.NotSupportedException: o tipo de nó de expressão LINQ 'NewArrayInit' não é suportado no LINQ to Entities.

Eu sugeriria apenas fazer

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

E FirstOrDefaultretornará 0 se sua lista estiver vazia.

Nix
fonte
O pedido pode resultar em uma grave degradação do desempenho com grandes conjuntos de dados. É uma maneira muito ineficiente de encontrar um valor máximo.
31818 Peter Bruins
1
decimal Max = (decimal?)(context.MyTable.Select(e => e.MyCounter).Max()) ?? 0;
jong su.
fonte
1

Eu bati um MaxOrDefaultmétodo de extensão. Não há muito, mas sua presença no Intellisense é um lembrete útil de que Maxem uma sequência vazia causará uma exceção. Além disso, o método permite que o padrão seja especificado, se necessário.

    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
fonte
0

Para o Entity Framework e o Linq to SQL, podemos conseguir isso definindo um método de extensão que modifica um método Expressionpassado para o 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

A consulta gerada é idêntica, funciona como uma chamada normal ao IQueryable<T>.Max(...)método, mas se não houver registros, ele retornará um valor padrão do tipo T em vez de gerar uma exceção

Ashot Muradian
fonte
-1

Eu apenas tive um problema semelhante, meus testes de unidade passaram usando Max (), mas falharam quando executados em um banco de dados ativo.

Minha solução foi separar a consulta da lógica que está sendo executada, não juntá-los em uma consulta.
Eu precisava de uma solução para trabalhar em testes de unidade usando objetos Linq (em Linq-objects Max () trabalha com nulos) e Linq-sql ao executar em um ambiente ativo.

(Eu zombei do Select () nos meus testes)

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? Provavelmente.

Eu me importo, desde que meu aplicativo não caia na próxima vez? Não.

Seb
fonte