A ordem das funções do LINQ é importante?

114

Basicamente, como afirma a pergunta ... a ordem das funções do LINQ é importante em termos de desempenho ? Obviamente, os resultados ainda teriam que ser idênticos ...

Exemplo:

myCollection.OrderBy(item => item.CreatedDate).Where(item => item.Code > 3);
myCollection.Where(item => item.Code > 3).OrderBy(item => item.CreatedDate);

Ambos retornam os mesmos resultados, mas estão em uma ordem LINQ diferente. Sei que reordenar alguns itens resultará em resultados diferentes, e não estou preocupado com isso. Minha principal preocupação é saber se, ao obter os mesmos resultados, o pedido pode impactar o desempenho. E, não apenas nas 2 chamadas LINQ que fiz (OrderBy, Where), mas em quaisquer chamadas LINQ.

Michael
fonte
9
Ótima pergunta.
Robert S.
É ainda mais óbvio que a otimização do provedor importa com um caso mais pedante como var query = myCollection.OrderBy(item => item.Code).Where(item => item.Code == 3);.
Mark Hurd,
1
Você merece um voto positivo :), perguntas interessantes. Vou considerar isso quando escrever meu Linq para entidades na EF.
GibboK
1
@GibboK: Tenha cuidado ao tentar "otimizar" suas consultas LINQ (veja a resposta abaixo). Às vezes, você acaba não otimizando nada. É melhor usar uma ferramenta de criação de perfil ao tentar a otimização.
myermian

Respostas:

147

Dependerá do provedor LINQ em uso. Para o LINQ to Objects, isso certamente faria uma grande diferença. Suponha que realmente temos:

var query = myCollection.OrderBy(item => item.CreatedDate)
                        .Where(item => item.Code > 3);

var result = query.Last();

Que requer a toda coleção a ser classificada e , em seguida, filtrada. Se tivéssemos um milhão de itens, dos quais apenas um tivesse um código maior que 3, estaríamos perdendo muito tempo solicitando resultados que seriam jogados fora.

Compare isso com a operação reversa, filtrando primeiro:

var query = myCollection.Where(item => item.Code > 3)
                        .OrderBy(item => item.CreatedDate);

var result = query.Last();

Desta vez, estamos apenas ordenando os resultados filtrados, que no caso de exemplo de "apenas um único item correspondendo ao filtro" será muito mais eficiente - tanto no tempo quanto no espaço.

Também pode fazer diferença se a consulta é executada corretamente ou não. Considerar:

var query = myCollection.Where(item => item.Code != 0)
                        .OrderBy(item => 10 / item.Code);

var result = query.Last();

Tudo bem - sabemos que nunca dividiremos por 0. Mas se realizarmos a ordenação antes da filtragem, a consulta lançará uma exceção.

Jon Skeet
fonte
2
@Jon Skeet, Há documentação sobre o Big-O para cada um dos provedores e funções do LINQ? Ou é apenas um caso de "cada expressão é única para a situação".
michael
1
@michael: Não está muito claramente documentado, mas se você ler minha série de blog "Edulinq", acho que falo sobre isso com detalhes razoáveis.
Jon Skeet
3
@michael: você pode encontrá-lo aqui msmvps.com/blogs/jon_skeet/archive/tags/Edulinq/default.aspx
VoodooChild
3
@gdoron: Para ser honesto, não está muito claro o que você quer dizer. Parece que você pode querer escrever uma nova pergunta. Lembre-se de que Queryable não está tentando interpretar sua consulta - seu trabalho é apenas preservar sua consulta para que outra pessoa possa interpretá-la. Observe também que o LINQ to Objects nem mesmo usa árvores de expressão.
Jon Skeet
1
@gdoron: A questão é que esse é o trabalho do provedor, não o trabalho do Queryable. E também não deve importar ao usar o Entity Framework. No entanto , isso importa para o LINQ to Objects. Mas sim, certamente faça outra pergunta.
Jon Skeet
17

Sim.

Mas exatamente qual é a diferença de desempenho depende de como a árvore de expressão subjacente é avaliada pelo provedor LINQ.

Por exemplo, sua consulta pode executar mais rápido na segunda vez (com a cláusula WHERE primeiro) para LINQ-to-XML, mas mais rápido na primeira vez para LINQ-to-SQL.

Para descobrir precisamente qual é a diferença de desempenho, você provavelmente desejará criar um perfil de seu aplicativo. Como sempre, porém, a otimização prematura geralmente não vale o esforço - você pode descobrir que outros problemas além do desempenho do LINQ são mais importantes.

Jeremy McGee
fonte
5

No seu exemplo específico, pode fazer diferença no desempenho.

Primeira consulta: sua OrderBychamada precisa iterar por toda a sequência de origem, incluindo os itens em que Codeé 3 ou menos. A Wherecláusula também precisa iterar toda a sequência ordenada.

Segunda consulta: A Wherechamada limita a sequência apenas aos itens em que Codeé maior que 3. A OrderBychamada então só precisa percorrer a sequência reduzida retornada pela Wherechamada.

LukeH
fonte
3

No Linq-To-Objects:

A classificação é bastante lenta e usa O(n)memória. Wherepor outro lado, é relativamente rápido e usa memória constante. Fazendo assimWhere primeiro será mais rápido e, para grandes coleções, significativamente mais rápido.

A pressão de memória reduzida também pode ser significativa, uma vez que as alocações no heap de objeto grande (junto com sua coleção) são relativamente caras na minha experiência.

CodesInChaos
fonte
1

Obviamente, os resultados ainda teriam que ser idênticos ...

Observe que isso não é realmente verdade - em particular, as duas linhas a seguir fornecerão resultados diferentes (para a maioria dos provedores / conjuntos de dados):

myCollection.OrderBy(o => o).Distinct();
myCollection.Distinct().OrderBy(o => o);
BlueRaja - Danny Pflughoeft
fonte
1
Não, o que eu quis dizer é que os resultados devem ser idênticos para até mesmo considerar a otimização. Não faz sentido "otimizar" algo e obter um resultado diferente.
Michael
1

É importante observar que você deve ter cuidado ao considerar como otimizar uma consulta LINQ. Por exemplo, se você usar a versão declarativa do LINQ para fazer o seguinte:

public class Record
{
    public string Name { get; set; }
    public double Score1 { get; set; }
    public double Score2 { get; set; }
}


var query = from record in Records
            order by ((record.Score1 + record.Score2) / 2) descending
            select new
                   {
                       Name = record.Name,
                       Average = ((record.Score1 + record.Score2) / 2)
                   };

Se, por algum motivo, você decidisse "otimizar" a consulta armazenando a média em uma variável primeiro, não obteria os resultados desejados:

// The following two queries actually takes up more space and are slower
var query = from record in Records
            let average = ((record.Score1 + record.Score2) / 2)
            order by average descending
            select new
                   {
                       Name = record.Name,
                       Average = average
                   };

var query = from record in Records
            let average = ((record.Score1 + record.Score2) / 2)
            select new
                   {
                       Name = record.Name,
                       Average = average
                   }
            order by average descending;

Não sei que muitas pessoas usam LINQ declarativo para objetos, mas é um bom alimento para reflexão.

myermian
fonte
0

Depende da relevância. Suponha que se você tiver muito poucos itens com Código = 3, o próximo pedido funcionará em um pequeno conjunto de coleta para obter o pedido por data.

Considerando que, se você tiver muitos itens com a mesma CreatedDate, o próximo pedido funcionará em um conjunto maior de coleta para obter o pedido por data.

Portanto, em ambos os casos, haverá uma diferença no desempenho

Pankaj Upadhyay
fonte