LINQ é lindo. Até você precisar descobrir por que aquela query está lenta
LINQ é uma das partes mais elegantes de C#. A mesma sintaxe de Where, Select e OrderBy funciona sobre uma lista em memória, sobre um banco de dados via Entity Framework Core e sobre outras fontes de dado completamente diferentes. É difícil não gostar dessa consistência.
O problema é que essa consistência de sintaxe esconde uma diferença de execução que importa bastante na hora de entender por que uma query específica ficou lenta.
Dois mundos atrás da mesma sintaxe
Quando você escreve LINQ sobre um IEnumerable<T>, está descrevendo código C# de verdade, executado em memória, item por item. LINQ to Objects não traduz nada, apenas encadeia delegates.
Quando você escreve LINQ sobre um IQueryable<T>, como o DbSet<T> do Entity Framework Core, não está descrevendo execução direta. Está construindo uma árvore de expressão que o provider, o Entity Framework nesse caso, traduz para outra linguagem, normalmente SQL, antes de qualquer dado ser buscado.
var ativos = dbContext.Usuarios
.Where(u => u.Ativo)
.Select(u => new { u.Nome, u.Email })
.ToList();
Até o ToList(), nada foi executado. A query só roda quando o resultado é efetivamente enumerado, e é o provider, não o C#, quem decide como transformar isso numa instrução real contra o banco.
Onde a abstração cobra o preço
O problema aparece quando alguma parte da query usa um método ou uma lógica que o provider não sabe traduzir para SQL. Métodos de C# sem equivalente direto no banco, lógica condicional complexa dentro do Select, chamadas a métodos de instância personalizados.
A partir do Entity Framework Core 3.0, esse tipo de situação deixou de ser resolvido silenciosamente do lado do cliente. Em vez de trazer a tabela inteira para memória e aplicar o filtro em C# sem avisar ninguém, o EF Core passou a lançar uma exceção quando encontra uma parte da query que não consegue traduzir. É mais barulhento, mas evita a armadilha de uma query que parece filtrada no banco e, na verdade, está trazendo dado demais antes de filtrar.
Um exemplo comum de comportamento inesperado
Um caso clássico é acessar uma propriedade de navegação dentro de um Select sem que ela esteja mapeada de forma direta:
var pedidos = dbContext.Pedidos
.Select(p => new { p.Id, TotalItens = p.Itens.Count })
.ToList();
Se Itens for uma coleção de navegação mapeada corretamente, o EF Core traduz Count para um COUNT dentro da própria consulta SQL. Mas se a expressão dentro do Select depender de alguma lógica que o provider não consegue mapear, o resultado pode ser uma exceção em tempo de execução, mesmo que o mesmo código compile perfeitamente e funcione contra uma lista em memória.
Lindo continua sendo, só não é grátis
A elegância do LINQ não é ilusão, é real. O que precisa de atenção é lembrar que a mesma linha de código significa coisas diferentes dependendo do tipo por trás dela ser IEnumerable<T> ou IQueryable<T>. Uma query lenta raramente é culpa da sintaxe. Geralmente é sinal de que alguma parte dela não está sendo traduzida do jeito que parecia óbvio à primeira vista.