IEnumerable vs IQueryable: Como a escolha da interface impacta a tradução de SQL e a performance no EF

A escolha entre IEnumerable vs IQueryable define onde uma consulta LINQ realmente executa: na memória da aplicação ou dentro do banco de dados. Por isso, essa decisão aparentemente pequena muda o SQL que o Entity Framework gera e, consequentemente, a performance da aplicação inteira.

Antes do LINQ

Antes do LINQ, desenvolvedores C# precisavam alternar constantemente entre paradigmas e sintaxes diferentes. Havia SQL para bancos de dados, XPath e XQuery para XML e APIs específicas para coleções em memória.

Cada fonte de dados exigia uma abordagem própria. Além disso, nada era verificado em tempo de compilação, então o risco de erros em runtime era alto.

O LINQ chegou com o .NET Framework 3.5, em 2007, para resolver exatamente esse problema. Com ele, a mesma sintaxe passou a consultar e manipular coleções de qualquer origem.

As vantagens apareceram logo: IntelliSense, verificação de tipos durante a compilação e uma sintaxe fortemente tipada e consistente. Na prática, o desenvolvedor escreve a consulta uma vez, em C#, e deixa a tradução para a infraestrutura.

O LINQ, porém, precisava atender dois mundos distintos. De um lado, fontes internas, já carregadas na memória, que implementam IEnumerable<T>.

Do outro lado, fontes externas, como bancos de dados e outros serviços, que implementam IQueryable ou IQueryable<T>. É dessa divisão que nasce o debate IEnumerable vs IQueryable.

O que muda entre IEnumerable vs IQueryable

Na comparação IEnumerable vs IQueryable, as duas interfaces servem de base para filtrar, agrupar, ordenar e projetar coleções de dados. Contudo, elas fazem isso de formas completamente diferentes.

Uma consulta LINQ sobre IEnumerable<T> usa os métodos da classe Enumerable. Esses métodos recebem delegates, ou seja, código compilado em IL (Intermediate Language) que roda diretamente na memória.

Já uma consulta sobre IQueryable<T> usa os métodos da classe Queryable. Esses métodos recebem árvores de expressão, uma estrutura de dados que descreve o código em vez de executá-lo.

Essa estrutura fica guardada até que um provider a leia e a traduza para a linguagem da fonte de dados. No caso do Entity Framework, a linguagem de destino é o SQL.

Hierarquia de IEnumerable vs IQueryable

Segundo a documentação oficial da interface IQueryable, ela herda de IEnumerable<T>. Assim, toda consulta IQueryable<T> também pode ser enumerada com um foreach.

Essa herança explica uma confusão comum. Como um IQueryable<T> também é um IEnumerable<T>, o compilador aceita atribuir um ao outro sem nenhum aviso.

Nesse ponto, a escolha IEnumerable vs IQueryable acontece sem que ninguém perceba.

O problema surge depois da atribuição. A partir dela, os próximos operadores passam a usar os métodos de Enumerable, e não mais os de Queryable.

Vale lembrar também que IQueryable não se restringe a fontes externas. O método AsQueryable transforma uma lista em memória em IQueryable<T>, com um provider que reescreve a árvore para chamadas comuns de Enumerable.

IEnumerable vs IQueryable e o impacto na memória

Pense em um banco de dados com milhões de registros. Com IEnumerable, mesmo que o código aplique filtros ou um limite de linhas depois da conversão, o banco devolve todos os dados e só então a aplicação filtra.

Com IQueryable, por outro lado, o filtro acontece dentro do banco. Dessa forma, a aplicação recebe apenas os dados já tratados e filtrados.

Dependendo da quantidade de dados, a diferença entre IEnumerable vs IQueryable decide se o servidor sobrevive ou não. Afinal, carregar uma tabela inteira para a RAM pode esgotar a memória e derrubar a aplicação como um todo.

Comparação IEnumerable vs IQueryable mostrando a filtragem em memória de um lado e a filtragem no banco de dados do outro
Onde cada interface aplica os filtros de uma consulta.

Exemplo prático de IEnumerable vs IQueryable no EF

A melhor forma de enxergar a diferença é comparar o mesmo código com os dois tipos de variável. Em seguida, basta observar o SQL que o Entity Framework envia ao banco.

Consulta usando IEnumerable

No primeiro lado da comparação IEnumerable vs IQueryable, o resultado do Where vai para uma variável do tipo IEnumerable<Student>. Depois disso, o código aplica um Take(2).

// O Where ainda é traduzido, mas o Take roda em memória
IEnumerable<Student> students = dbContext
    .Students
    .Where(x => x.Name == "Luis Felipe");

var result = students.Take(2);

O SQL gerado não usa a cláusula TOP. Portanto, o banco devolve todos os estudantes com o nome "Luis Felipe", e só então a aplicação descarta o excedente em memória.

Repare em um detalhe importante: o Where aconteceu antes da conversão. Por isso, ele ainda vira um WHERE no SQL, enquanto o Take chega tarde demais para entrar na consulta.

Com poucos registros, ninguém percebe o desperdício. No entanto, em uma tabela grande, a aplicação transfere e materializa milhares de objetos para usar apenas dois.

Consulta usando IQueryable

No segundo exemplo, a variável mantém o tipo IQueryable<Student>. Assim, cada operador seguinte continua compondo a mesma árvore de expressão.

// Todos os operadores entram na mesma consulta SQL
IQueryable<Student> students = dbContext
    .Students
    .Where(x => x.Gender == "Male");

var result = students.Take(2);

Agora o SQL gerado usa a cláusula TOP dentro do banco, e esse é o ganho central de IEnumerable vs IQueryable. Com isso, a aplicação recebe apenas os dois registros necessários, já filtrados.

A diferença entre os dois trechos é uma única palavra na declaração da variável. Mesmo assim, o efeito na performance pode ser enorme, e é por isso que a dúvida IEnumerable vs IQueryable aparece tanto em revisões de código.

Expression tree, a árvore de expressão

Para entender a escolha entre IEnumerable vs IQueryable, é preciso entender a árvore de expressão. Ela é o mecanismo que permite ao IQueryable adiar a execução e traduzir o código para outra linguagem.

Quando você escreve uma consulta LINQ sobre IQueryable<T>, como no Entity Framework, o compilador C# não gera código executável para a lambda. Em vez disso, ele constrói uma estrutura de dados hierárquica.

Nessa estrutura, cada nó representa uma operação do seu código. O repositório ExpressionTree demonstra essa montagem na prática.

Por exemplo, a lambda x => x > 5 vira uma árvore com três nós:

BinaryExpression (GreaterThan)
├─ Left: ParameterExpression (x)
└─ Right: ConstantExpression (5)

O nó raiz representa a comparação "maior que". Então, à esquerda fica o parâmetro x e, à direita, a constante 5.

Delegate e Expression lado a lado

A mesma lambda pode virar código executável ou árvore de expressão. O tipo da variável que a recebe decide qual dos dois o compilador gera.

using System.Linq.Expressions;

// Delegate: código compilado, pronto para executar
Func<int, bool> isGreaterThanFive = x => x > 5;

// Expression: estrutura de dados que descreve o código
Expression<Func<int, bool>> isGreaterThanFiveExpression = x => x > 5;

Console.WriteLine(isGreaterThanFive(10));          // True
Console.WriteLine(isGreaterThanFiveExpression.Body); // (x > 5)

O Func executa direto, porque é um delegate comum. Se quiser revisar esse conceito, o artigo sobre delegates em C# explica o tema em detalhes.

O Expression<Func<...>>, por sua vez, só descreve a operação. Para executá-lo, você precisaria chamar Compile(), mas um provider como o EF prefere ler a árvore e traduzi-la.

No fundo, IEnumerable vs IQueryable se resume a isso, ou seja, à diferença entre os métodos de Enumerable e de Queryable. Enquanto os primeiros recebem Func, os segundos recebem Expression<Func>.

Processo de tradução para SQL

O Entity Framework usa um pipeline de várias etapas para converter essa árvore em SQL executável. Primeiro, o compilador C# cria a Expression<Func<T, bool>> automaticamente quando você usa LINQ sobre IQueryable<T>.

Em seguida, as lambdas viram árvores de expressão compostas por nós como MethodCallExpression, BinaryExpression, MemberExpression e ConstantExpression. Nesse ponto, a árvore ainda não carrega nenhuma semântica de SQL.

A árvore existe em runtime, mas o compilador define a lógica de como ela é montada. Além disso, a consulta permanece inerte por execução diferida até que um operador terminal dispare a execução.

Operadores como ToList() ou First() acionam o provider, por meio de IQueryProvider.Execute() ou da enumeração. É esse motor que consome a árvore e "manda o comando" para o banco.

Depois, o Entity Framework usa o padrão ExpressionVisitor para percorrer recursivamente cada nó. Assim, ele identifica as operações, as propriedades acessadas e os valores constantes.

Na etapa seguinte, a árvore .NET se transforma em um modelo de consulta abstrato e independente de banco, o ShapedQueryExpression. Esse modelo representa conceitos genéricos como SELECT, WHERE e JOIN, algo que só existe no lado IQueryable da comparação IEnumerable vs IQueryable.

Então, o provider específico do banco, como o Microsoft.EntityFrameworkCore.SqlServer, converte o modelo abstrato em SQL concreto. Nessa conversão, ele respeita a sintaxe do banco (T-SQL, PL/SQL), os tipos nativos, as funções proprietárias e as otimizações específicas.

Por fim, vem a execução. Apenas nesse momento a consulta SQL chega ao banco de dados.

Pipeline do Entity Framework que transforma a árvore de expressão em SQL, do compilador C# até a execução no banco
As etapas que levam uma lambda LINQ até o comando SQL.

Segundo a documentação How Queries Work, o EF Core ainda guarda em cache o resultado desse processamento. Dessa forma, a mesma consulta não precisa passar por todo o pipeline a cada execução.

Execução diferida em IEnumerable vs IQueryable

Chamar operadores LINQ apenas monta uma representação da consulta em memória. A consulta só chega ao banco quando alguém consome os resultados.

As operações mais comuns que disparam a execução são iterar o resultado em um laço foreach e chamar ToList, ToArray, Single, First ou Count. O mesmo vale para as versões assíncronas, como ToListAsync.

Essa característica vale para os dois lados da comparação IEnumerable vs IQueryable. A diferença está no que acontece quando a execução finalmente ocorre.

No IQueryable, a execução gera um único comando SQL com tudo o que foi composto até ali. No IEnumerable, a execução roda o código em memória, operador por operador, sobre os dados já carregados.

Por isso, um erro comum consiste em chamar ToList() cedo demais. Ao fazer isso, você materializa a tabela e transforma todos os operadores seguintes em processamento local.

Avaliação no cliente e no servidor no EF Core

O EF Core também transforma o debate IEnumerable vs IQueryable em regra de execução.

Como regra geral, ele tenta avaliar o máximo possível da consulta no servidor. Contudo, nem todo código C# tem tradução para SQL.

Considere um método auxiliar que padroniza URLs. O provider do SQL Server não sabe como esse método funciona, então não consegue traduzi-lo.

// Lança exceção: o filtro não tem tradução para SQL
var blogs = await context.Blogs
    .Where(blog => StandardizeUrl(blog.Url).Contains("dotnet"))
    .ToListAsync();

Desde a versão 3.0, o EF Core bloqueia esse tipo de consulta e lança uma exceção em runtime. Antes disso, ele trazia a tabela inteira e filtrava no cliente, emitindo apenas um aviso.

A página Client vs. Server Evaluation explica a motivação. Uma tabela pequena em desenvolvimento esconde o problema, que só aparece em produção, com milhões de linhas.

A exceção à regra é a projeção final, ou seja, o último Select. Nela, o EF Core aceita avaliar partes no cliente, porque os filtros já rodaram no banco.

Quando usar AsEnumerable de propósito

Às vezes, a avaliação no cliente é exatamente o que você quer. Por exemplo, quando o volume de dados é pequeno ou quando o operador não tem tradução para SQL.

Nesses casos, a documentação recomenda optar explicitamente pela avaliação local com AsEnumerable ou AsAsyncEnumerable. Assim, a intenção fica clara no código.

// Filtro no banco, depois processamento local
var blogs = context.Blogs
    .Where(blog => blog.Rating > 3)
    .AsAsyncEnumerable()
    .Where(blog => StandardizeUrl(blog.Url).Contains("dotnet"));

await foreach (var blog in blogs)
{
    Console.WriteLine(blog.Url);
}

O primeiro Where vira SQL, enquanto o segundo roda em .NET. Em outras palavras, você escolhe conscientemente o ponto em que a consulta passa de IQueryable para IEnumerable.

Prefira AsEnumerable a ToList quando ainda houver operadores pela frente. O AsEnumerable faz streaming dos resultados, enquanto o ToList cria uma lista e ocupa memória adicional.

Boas práticas para escolher entre IEnumerable vs IQueryable

Algumas regras simples sobre IEnumerable vs IQueryable evitam a maior parte dos problemas no dia a dia. Primeiro, mantenha a consulta como IQueryable<T> enquanto ainda houver filtros, ordenação ou paginação a aplicar.

Além disso, cuidado com a palavra var em código legado. Ela preserva o tipo real, enquanto uma declaração explícita como IEnumerable<T> muda silenciosamente o comportamento.

Também vale observar as assinaturas de repositórios e serviços. Um método que devolve IEnumerable<T> impede que quem chama componha filtros no banco.

Por outro lado, expor IQueryable<T> para camadas externas tem seus custos. Nesse caso, qualquer parte do sistema pode montar consultas arbitrárias, o que dificulta prever o SQL gerado.

Por isso, muitas equipes encapsulam a consulta em métodos específicos e devolvem apenas o resultado final. O artigo sobre Repository Pattern com Entity Framework discute esse equilíbrio.

Por fim, confira sempre o SQL gerado. O método ToQueryString() do EF Core mostra o comando antes da execução, e os logs do provider confirmam o que chegou ao banco.

Se a dúvida for performance bruta entre ferramentas, o comparativo entre EF Core e Dapper complementa a análise. Ainda assim, a decisão IEnumerable vs IQueryable continua relevante em qualquer ORM baseado em LINQ.

Conclusão

Compreender a diferença entre IEnumerable vs IQueryable é fundamental para construir aplicações .NET performáticas e escaláveis. Afinal, uma única declaração de tipo decide se o filtro roda no banco ou na memória.

O IEnumerable é a ferramenta ideal para manipular dados que já estão em memória. Já o IQueryable brilha ao delegar a inteligência da consulta para o provider, usando árvores de expressão para traduzir C# em comandos nativos como o SQL.

Em resumo, o uso consciente de IEnumerable vs IQueryable evita gargalos críticos de memória e processamento. Com isso, o Entity Framework busca apenas o necessário.

Entender esse pipeline de tradução transforma o desenvolvedor em alguém capaz de otimizar a comunicação entre a aplicação e a camada de persistência. Dessa forma, cada decisão sobre manipulação de dados passa a ter base técnica, e não apenas hábito.

FAQ

Qual a principal diferença entre IEnumerable e IQueryable?

O IEnumerable executa os operadores em memória, com delegates compilados. Já o IQueryable monta uma árvore de expressão que o provider traduz, por exemplo, para SQL.

IQueryable herda de IEnumerable?

Sim, IQueryable<T> herda de IEnumerable<T>. Por isso, o compilador aceita a atribuição de um para o outro sem avisos.

Quando devo usar AsEnumerable em uma consulta do EF Core?

Use quando precisar de um operador sem tradução para SQL ou quando o volume de dados for pequeno. Aplique antes todos os filtros possíveis no banco.

Como vejo o SQL que o EF Core vai gerar?

Chame ToQueryString() na consulta IQueryable. Os logs do EF Core também mostram o comando enviado ao banco.

O EF Core ainda filtra dados no cliente automaticamente?

Não, desde a versão 3.0 ele lança uma exceção quando um filtro não tem tradução. A avaliação no cliente só acontece na projeção final ou quando você a pede explicitamente.