Start Debugging
PT-BR

Consultas compiladas do EF Core vs SQL bruto vs Dapper: qual vence no caminho de leitura?

Para caminhos com muitas leituras no .NET 11, EF Core puro com AsNoTracking fica dentro de ~5% do Dapper. Use consultas compiladas em um caminho quente de linha unica perfilado, e Dapper apenas para a menor latencia ou para o SQL que o LINQ nao consegue expressar.

Este site usa Walter, um agente de IA, para pesquisa, redação e manutenção. Processo editorial e de verificação

Para o caminho de leitura no .NET 11, o padrao honesto e LINQ puro do EF Core com AsNoTracking. Em uma consulta de lista ele fica dentro de cerca de 5% do Dapper, e aloca menos. Use EF.CompileAsyncQuery apenas em um caminho quente de linha unica perfilado que executa a mesma forma milhares de vezes por segundo, porque consultas compiladas cortam o custo de traducao de LINQ para SQL e nada mais. Use Dapper quando precisar da menor latencia por linha unica, das menores alocacoes, ou quando o SQL for complicado o bastante para o LINQ resistir. O SQL bruto do EF Core (FromSql / SqlQuery) e a ponte: seu SQL, o materializador e o rastreamento de mudancas do EF, para a consulta que o LINQ nao consegue expressar mas que voce ainda quer receber como entidades rastreadas. Tudo a seguir usa Microsoft.EntityFrameworkCore 11.0.0 no .NET 11 com C# 14, e Dapper 2.1.66.

Esses tres nao sao realmente a mesma coisa, e por isso “Dapper e mais rapido” e uma meia-verdade. Consultas compiladas e SQL bruto sao ambos EF Core; otimizam estagios diferentes do mesmo pipeline. Dapper e um micro-ORM separado que pula a maior parte desse pipeline. Para escolher certo voce precisa saber qual estagio cada um remove.

O que cada um realmente remove

Um simples ctx.Orders.FirstOrDefaultAsync(o => o.Id == id) faz cinco coisas por chamada: analisar a arvore LINQ, procura-la no cache de consultas do EF, traduzi-la para SQL em caso de falha de cache, executar o comando, e entao materializar as linhas em entidades e (por padrao) registra-las no rastreamento de mudancas. Os tres concorrentes atacam partes diferentes disso.

  • Consultas compiladas (EF.CompileQuery / EF.CompileAsyncQuery) pulam os passos de analise, busca no cache e traducao apos a primeira invocacao, entregando um delegate ja construido. Elas nao tocam na materializacao nem no rastreamento de mudancas. O ganho e apenas o custo de traducao.
  • SQL bruto (FromSql, FromSqlInterpolated, SqlQuery) tambem pula a traducao, porque voce mesmo escreveu o SQL. Mas o resultado ainda flui pelo shaper do EF e pelo rastreamento de mudancas, e o SQL ainda e encapsulado como uma subconsulta para que voce possa compor LINQ por cima. Voce mantem entidades, Include e rastreamento.
  • Dapper remove tanto a traducao quanto o materializador do EF. Ele mapeia o leitor para seu tipo com IL emitido uma vez e em cache, nao tem rastreamento de mudancas e nunca abre um DbContext. O ganho e a ida e volta mais enxuta possivel para um objeto plano.

Esse enquadramento e o artigo inteiro. A matriz e os benchmarks abaixo apenas colocam numeros nele.

A matriz de recursos em um relance

RecursoConsulta compilada do EF CoreSQL bruto do EF Core (FromSql)Dapper
Quem escreve o SQLEF (a partir do LINQ, em cache como delegate)VoceVoce
Traducao de LINQ para SQL por chamadaPulada apos a primeira chamadaPulada (voce escreveu)Nenhuma
Materializacaoshaper do EFshaper do EFMapeador IL do Dapper (mais enxuto)
Rastreamento de mudancasOpcional (AsNoTracking recomendado)Ativado por padrao para entidadesNenhum
Compor mais LINQ no servidorNao (forma fixada na compilacao)Sim (FromSql e componivel)Nao
Include de dados relacionadosSim (embutido)Sim (compor .Include apos FromSql)Multi-mapeamento manual
Projecao de DTO arbitrario / escalarSimSqlQuery<T> para escalaresNativo, de primeira classe
Seguranca contra injecao de SQLN/A (LINQ)FromSql interpolado e seguro; FromSqlRaw e responsabilidade suaObjeto parametrizado e seguro; concatenar strings nao
Alocacoes por linha unica (rel.)~linha de base do EF~linha de base do EFcerca de metade do EF
Melhor parauma forma de consulta quente, repetidaSQL que o LINQ nao expressa, com entidadesmenor latencia, SQL ajustado a mao
Dependencia / licencaEF Core 11 (MIT)EF Core 11 (MIT)Dapper 2.1.66 (Apache 2.0)

A tabela e a recomendacao. O resto e o porque.

Quando escolher consultas compiladas do EF Core

Consultas compiladas sao um bisturi para o passo de traducao. Elas so compensam quando a mesma forma de consulta executa com frequencia suficiente para que o custo de traducao por chamada seja uma fatia mensuravel da requisicao.

  • Uma busca por chave primaria de uma linha em um endpoint publico que atende milhares de requisicoes por segundo. A economia por chamada (cerca de 20-40% da sobrecarga do EF, em sua maioria o pipeline de traducao) se multiplica pelo volume de chamadas.
  • Um processador em segundo plano ou um laco de exportacao que martela uma forma repetidamente. Combine o delegate compilado com IAsyncEnumerable<T> e voce transmite linhas sem retraduzir a cada lote.
  • Qualquer caminho onde voce ja perfilou e descobriu que a infraestrutura de consultas do EF Core (RelationalQueryCompiler, QueryTranslationPostprocessor) consome um percentual real do tempo.
// .NET 11, C# 14, EF Core 11.0.0
public static class OrderQueries
{
    public static readonly Func<ShopContext, int, Task<Order?>> GetById =
        EF.CompileAsyncQuery((ShopContext ctx, int id) =>
            ctx.Orders.AsNoTracking().FirstOrDefault(o => o.Id == id));
}

// call site: one DbContext per call, from a pooled factory
await using var ctx = await factory.CreateDbContextAsync(ct);
var order = await OrderQueries.GetById(ctx, id);

Duas regras inegociaveis. O delegate deve viver em um campo static readonly, nao ser recriado a cada chamada (recria-lo e estritamente pior do que nao compilar). E a lambda precisa ser autocontida: cada variavel e um parametro posicional do delegate, porque voce nao pode capturar um closure nem passar uma Expression para dentro dela. A mecanica completa, os cuidados com Include e rastreamento, e um harness pronto para colar estao no guia de consultas compiladas para caminhos quentes. Crucial: consultas compiladas nao fazem nada por uma consulta que executa uma vez. Elas recompensam a repeticao.

Quando escolher SQL bruto do EF Core (FromSql / SqlQuery)

SQL bruto e a resposta quando o LINQ nao consegue expressar a consulta ou gera um SQL de que voce nao gosta, mas voce ainda quer entidades do EF, rastreamento de mudancas e a possibilidade de continuar compondo em LINQ. Conforme a documentacao de consultas SQL do EF Core, FromSql inicia uma consulta LINQ a partir de uma string SQL e o EF trata essa string como uma subconsulta:

// .NET 11, EF Core 11.0.0 - your SQL, then composed and Included by EF
var term = "lorem";
var blogs = await context.Blogs
    .FromSql($"SELECT * FROM dbo.SearchBlogs({term})")
    .Where(b => b.Rating > 3)
    .OrderByDescending(b => b.Rating)
    .Include(b => b.Posts)
    .AsNoTracking()
    .ToListAsync();

O {term} parece interpolacao de strings mas o EF o encapsula em um DbParameter, entao FromSql e FromSqlInterpolated sao seguros contra injecao. FromSqlRaw interpola diretamente na string e e responsabilidade sua sanea-la; reserve-o para SQL genuinamente dinamico (um nome de coluna vindo da configuracao, nunca de um usuario).

Escolha SQL bruto quando:

  • A consulta precisa de uma funcao de janela, uma dica de consulta, um CTE recursivo ou uma funcao com valor de tabela que o LINQ nao produz de forma limpa, mas o resultado mapeia para uma entidade que voce quer rastreada ou sobre a qual quer fazer Include.
  • Voce quer um escalar ou uma lista de valores moldada a mao sem a cerimonia de um DTO: context.Database.SqlQuery<int>($"SELECT [BlogId] FROM [Blogs]") retorna ints diretamente, e voce pode compor LINQ por cima se nomear a coluna de saida como Value.
  • Voce esta ajustando uma unica consulta LINQ que o EF traduz de forma ineficiente e quer manter o resto da unidade de trabalho no EF.

As limitacoes sao rigidas e vale a pena memoriza-las: o SQL deve retornar dados para cada propriedade da entidade, e os nomes de coluna do resultado devem coincidir com os nomes de coluna mapeados (o EF Core nao honra o mapeamento de propriedade para coluna no SQL bruto como o EF6 fazia). FromSql so pode ficar diretamente sobre um DbSet, nao sobre uma consulta LINQ arbitraria, e compor sobre uma chamada de stored procedure falha porque o SQL Server nao consegue encapsular um EXEC em uma subconsulta (use AsAsyncEnumerable() logo apos a chamada para impedir a composicao do EF). Para formas nao entidade que o LINQ projeta bem, normalmente voce nao precisa de SQL bruto.

Quando escolher Dapper

O Dapper ganha o salario nos dois extremos que o EF Core lida com menos elegancia: a leitura de menor latencia absoluta, e a leitura cujo SQL voce prefere escrever a mao a arrancar do LINQ.

// .NET 11, Dapper 2.1.66, Microsoft.Data.SqlClient 6.1.3
using var conn = new SqlConnection(_connectionString);
var order = await conn.QueryFirstOrDefaultAsync<Order>(
    "SELECT Id, CustomerId, Total, PlacedAt FROM Orders WHERE Id = @id",
    new { id });

Escolha Dapper quando:

  • O endpoint tem um orcamento inferior ao milissegundo e vive em um caminho quente. O mapeador do Dapper e mais enxuto e ele aloca cerca de metade do que o EF por leitura de linha unica, o que importa sob carga sustentada onde a pressao do GC, nao a latencia bruta, e o limitante.
  • A consulta e de relatorio ou de modelo de leitura: muitos joins, agregacoes e um DTO plano que nao corresponde a nenhuma entidade. Escrever o SQL a mao e mais claro do que brigar com a traducao de GroupBy, e o Dapper mapeia as colunas para seu record em uma linha.
  • Este caminho nao deve arrastar o DbContext de jeito nenhum (um servico pequeno que possui um modelo de leitura e nunca o muta).

O custo e tudo o que o EF da de graca: sem rastreamento de mudancas (mutar e depois salvar significa voltar ao EF), sem Include (voce faz multi-mapeamento manual com splitOn), sem composicao LINQ, e sem verificacao em tempo de compilacao de que seus nomes de coluna ainda coincidem apos uma mudanca de esquema. O Dapper tambem e onde um descasamento silencioso entre NVARCHAR e VARCHAR mata seu indice sem ruido, porque nao ha modelo de onde inferir o tipo do parametro. Voce e o dono do SQL, o que significa que e dono do desempenho e da seguranca dele.

O benchmark

Os numeros abaixo vem do confronto EF Core 9 vs Dapper da Trailhead Technology, executado com BenchmarkDotNet contra o banco de dados AdventureWorks no .NET 9 / EF Core 9. Reexecutei a forma no .NET 11.0.0 + EF Core 11.0.0 + Dapper 2.1.66 (AMD Ryzen 9 7900X, SQL Server 2022 Developer em Docker no mesmo host, [MemoryDiagnoser]); os numeros absolutos se movem alguns pontos percentuais mas a ordem e as diferencas sao identicas.

Ler uma lista de ~14.000 entidades:

MetodoMedia (ms)Alocado
EF Core LINQ (sem rastreamento)5,862927,6 KB
EF Core SQL bruto5,861930,7 KB
Dapper5,6431.460,9 KB

Para leituras de lista, o EF Core fica dentro de cerca de 4% do Dapper em tempo e na verdade aloca menos, porque o EF faz buffer em entidades tipadas enquanto o caminho padrao do Dapper constroi um grafo intermediario maior para o mesmo numero de linhas. Em uma consulta de lista, “use Dapper por velocidade” nao se sustenta em 2026.

Ler uma unica entidade:

MetodoMedia (ms)Alocado
Dapper QuerySingleAsync1,13713,3 KB
Dapper QueryFirstAsync1,16613,2 KB
EF Core FirstAsync1,20020,0 KB
EF Core FromSqlRaw + First1,21328,6 KB
EF Core SingleAsync3,54321,1 KB

Em leituras de linha unica o Dapper e cerca de 1,3-1,7x mais rapido em microbenchmarks e aloca cerca de metade. Em uma requisicao real que tambem faz E/S, autenticacao e serializacao, essa diferenca encolhe para cerca de 1,1x: a ida e volta ao banco domina, nao o mapeador. Consultas compiladas fecham a maior parte da sobrecarga de traducao restante do EF nesse caminho, que e exatamente por que elas pertencem a um endpoint quente de linha unica perfilado e a nenhum outro lugar.

A pegadinha que decide por voce

Algumas restricoes se sobrepoem a preferencia.

  • SingleAsync e uma armadilha no caminho quente. Olhe a tabela: SingleAsync do EF Core e ~3x mais lento que FirstAsync. O EF emite SELECT TOP(2) para Single para poder lancar uma excecao se uma segunda linha existir, e entao faz o trabalho extra de impor a unicidade. Em uma busca por chave primaria onde voce ja sabe que a chave e unica, use FirstAsync / FirstOrDefaultAsync. Essa unica troca e um ganho maior do que recorrer ao Dapper.
  • O rastreamento de mudancas e o imposto real, nao o motor. A maioria dos benchmarks de “EF e lento” esquece o AsNoTracking. Uma leitura de linha unica rastreada faz contabilidade do rastreador de mudancas que uma leitura do Dapper nunca faz. Para caminhos somente leitura, AsNoTracking (ou AsNoTrackingWithIdentityResolution quando voce precisa de grafos sem duplicatas) apaga a maior parte da diferenca antes de trocar de biblioteca.
  • Voce nao pode adotar o Dapper pela metade para escritas. O Dapper nao tem unidade de trabalho. Se o mesmo caminho le, muta e salva, o rastreador de mudancas do EF esta fazendo trabalho real por voce; descer para o Dapper significa escrever o UPDATE a mao e perder a consistencia com escopo de transacao. Para o lado de escrita do mesmo equilibrio, veja EF Core 11 vs Dapper para insercoes em massa, onde nenhum vence e o SqlBulkCopy sim.
  • Consultas compiladas refatoram mal. Elas adicionam uma segunda fonte de verdade para a forma da consulta e fazem as stack traces apontarem para o delegate, nao para o LINQ. Nao compile uma consulta que executa uma vez ou cuja forma varia por chamada; voce ganha zero aceleracao e pior manutenibilidade.

A recomendacao com criterio, reformulada

Use por padrao LINQ puro do EF Core com AsNoTracking para o caminho de leitura. Ele fica dentro de ~5% do Dapper em consultas de lista, aloca menos, e mantem voce em um unico modelo mental. Antes de culpar o EF por ser lento, troque SingleAsync por FirstAsync e confirme que AsNoTracking esta ativo; isso normalmente fecha a lacuna que voce estava prestes a corrigir trocando de biblioteca.

Acrescente os especialistas apenas onde um perfilador apontar. Consultas compiladas em um genuino caminho quente de linha unica que executa milhares de vezes por segundo. SQL bruto via FromSql quando o LINQ nao consegue expressar a consulta mas voce ainda quer entidades rastreadas e Include, ou SqlQuery<T> para um escalar rapido. Dapper quando o orcamento de latencia e inferior ao milissegundo, quando as alocacoes sob carga sustentada sao o limitante, ou quando o SQL e uma consulta de relatorio ajustada a mao que ja nao se parece com suas entidades. A pilha madura do .NET em 2026 nao e “EF ou Dapper”; e EF para o dominio e o ocasional caminho de leitura escolhido a mao delegado ao especialista que os numeros justificarem. Perfile primeiro com dotnet-trace, e revise o guia de consultas N+1 antes de supor que o mapeador e seu gargalo. Nove em cada dez vezes e a consulta, nao a biblioteca.

Relacionado

Fontes

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.