Start Debugging

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.

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.

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.

// .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:

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

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.

< Voltar