DICAS PARA ESCREVER CONSULTAS COM ENTITY FRAMEWORK CORE

Rafael Arthur Rocha Miranda
#LocalizaLabs
Published in
10 min readMay 8, 2023

Consultas a uma base de dados fazem parte do dia a dia de um desenvolvedor de software e sabemos que muitas vezes essa pode ser uma tarefa um tanto quanto difícil, dependendo da forma e tecnologia adotada para isso. No entanto, temos uma ferramenta que pode nos ajudar nessa missão. Estou falando dos ORMs — Objetc-Relational Mapping e, nesse artigo vamos falar especificamente do Entity Framework Core — EF CORE.

Quando necessitamos realizar nossas consultas sabemos que devemos preparar nossa Structured Query Language — SQL de modo que possa realizar as condicionais, as junções, agrupamentos e até ordenações que desejamos e, quando estamos de fato escrevendo SQL, isso fica mais explícito e de maneira mais fácil de visualizar as coisas que não são necessárias na consulta, como por exemplo, as colunas desnecessárias ou até mesmo o famoso “ * from”.

Quando utilizamos o EF Core ele faz uma abstração de todo o processo de acesso a dados e até mesmo de escrita de SQL. Isso nos ajuda no dia a dia, proporcionando uma certa produtividade, uma vez que facilita essa árdua tarefa de acesso a dados. Porém, nem tudo são flores. A mesma abstração que nos ajuda também pode nos atrapalhar no momento de realizar as consultas na base de dados quando não prestamos atenção no que o EF Core está fazendo “por de baixo dos panos”. Esse artigo tem o objetivo de apresentar algumas dicas simples, mas que podem te ajudar a ter um melhor desempenho no momento de fazer suas consultas no banco de dados.

Esse artigo divide-se como se segue: A seção materialização das queries nos apresenta algumas dicas de como o EF Core gera a consulta SQL, materializando as informações na memória do computador; Na seção comparação de strings é apresentado algumas formas de melhorar a performance quando estamos trabalhando com comparação de strings; A seção tracking do EF Core aborda a forma com que o EF Core faz o tracking dos objetos em memória durante uma consulta; Na seção consultas projetadas são apresentadas algumas formas de realizar a projeção de consultas e, assim, melhorar a performance delas. Por fim, na seção benchmark é discutido sobre os resultados obtidos com as consultas projetadas.

MATERIALIZAÇÃO DAS QUERIES

Ao utilizar o EF Core devemos ter em mente que suas coleções de dados são do tipo IQueryable, ou seja, o objeto final contendo as instruções será compilado para uma saída SQL. É importante destacar que essa consulta só é materializada (gerado o SQL) quando executamos os comandos .ToList(), .Fisrt(), .FisrtOrDefault(), .Any() (Aqui considerem também os métodos assíncronos. Ex: .ToListAsync() ). Só após a execução desses métodos é que de fato a sua consulta é executada e os dados são materializados na memória do computador.

Devemos ter o cuidado de sempre invocar os métodos citados por último e não colocar nenhum where após eles, por exemplo, e assim fazer com que todo o conteúdo de uma tabela seja retornado e as condições de filtro sejam executadas na memória do computador de forma não intencional e prejudicando a performance. A Figura 1 ilustra uma coleção IQueryable do EF Core.

Figura 1 -Coleção de autor IQueryable.

COMPARAÇÃO DE STRINGS

Em alguns casos podemos precisar fazer uma comparação de strings onde os caracteres são todos maiúsculos ou minúsculos e, no CSharp — C#, isso é feito utilizando os métodos ToUpper() ou ToLower() respectivamente. No entanto, se colocar esses métodos em uma consulta, eles podem ser transferidos para a consulta SQL (ou seja, o banco fazer a conversão) e, em alguns casos, isso pode prejudicar a performance, dependendo da consulta executada.

public void ConsultaDeAutoresComUpper()
{
var nomeFiltro = "Rafael";
using var db = new LivrariaContext();
var autores = db.Autores.Where(x => x.Nome.ToUpper() == nomeFiltro.ToUpper()).ToList();
}
Figura 2 — SQL com operação Upper.
public void ConsultaDeAutoresSemUpper()
{
var nomeFiltro = "Rafael";
using var db = new LivrariaContext();
var autores = db.Autores.Where(x => x.Nome == nomeFiltro.ToUpper()).ToList();
}
Figura 3 — SQL sem operaçãoUpper.

Na Figura 2, podemos observar que foi transferido para a instrução SQL a operação UPPER, que será executada para cada linha da base de dados, degradando assim a performance da consulta. Uma possível solução é: na sua classe de domínio, já transformar a string em maiúsculo, como mostra a classe autor abaixo, e assim quando for consultar será necessário aplicar a transformação apenas no filtro como podemos ver na ConsultaDeAutoresSemUpper acima, obtendo um resultado mais limpo como na Figura 3. É importante destacar que essa solução demostrada na classe autor é uma abordagem didática, e que devemos sempre considerar persistir os dados em sua forma original, a menos que regras de negócio levem a tal modificação.

public class Autor
{
public Autor()
{
Livros = new List<Livro>();
}

public int Id { get; set; }
private string _nome = string.Empty;
public string Nome
{
get
{
return _nome;
}
set
{
_nome = value.ToUpper();
}
}

public string CPF { get; set; } = default!;
public string Email { get; set; } = default!;
public IEnumerable<Livro> Livros { get; set; }
}

TRACKING DO EF CORE

Quando utilizamos o EF Core e realizamos a leitura de um ou mais registros de uma base de dados, eles são trackeados por padrão pelo Change Tracker. Essa ferramenta é bastante útil quando estamos atualizando alguma informação na base, pois ela fica monitorando modificações no objeto em memória. Porém, quando estamos apenas realizando leitura de dados, ela pode apresentar uma redução de desempenho. Podemos desabilitar essa função para uma consulta, utilizando a opção AsNoTracking(). Sendo assim, sua consulta não será mais rastreada pelo EF Core. Contudo, isso não implica dizer que todas as consultas AsNoTracking têm maior performance, pois quando uma consulta é rastreada o EF Core pode utilizar o mesmo endereço de memória para o mesmo objeto caso ele se repita nos resultados, o que não vai acontecer se ela não for rastreada. Você realmente deve testar o resultado das duas consultas para ver qual tem a melhor performance para o seu caso.

Com o EF Core 5, esse problema foi solucionado com a disponibilização do método AsNoTrackingWithIdentityResolution() o qual não faz o tracker da consulta e também resolve a questão do apontamento em memoria caso algum objeto se repita, tornando a consulta mais performática. As consultas ConsultaDeAutoresNaoRastreada e ConsultaDeAutoresNaoRastreadaComResolucao abaixo ilustram a utilização dos métodos ASNoTracking e AsNoTrackingWithIdentityResolution respectivamente.

public void ConsultaDeAutoresNaoRastreada()
{
var nomeFiltro = "Rafael";
using var db = new LivrariaContext();
var autores = db.Autores
.AsNoTracking()
.Where(x => x.Nome == nomeFiltro.ToUpper()).ToList();
}
public void ConsultaDeAutoresNaoRastreadaComResolucao()
{
var nomeFiltro = "Rafael";
using var db = new LivrariaContext();
var autores = db.Autores
.AsNoTrackingWithIdentityResolution()
.Where(x => x.Nome == nomeFiltro.ToUpper()).ToList();
}

CONSULTAS PROJETADAS

Quando utilizamos um ORM e não nos atentamos aos detalhes de sua abstração, podemos degradar (sem a intenção é claro, eu mesmo já fiz isso) a performance das consultas, simplesmente por não dar atenção aos campos que a consulta vai gerar no select, ou seja, devemos realizar a projeção dos campos que eu necessito trazer na consulta. Se não for especificado, o EF Core vai fazer o tal do “ * from” e, se na consulta tiver ainda uma junção, a coisa pode piorar ainda mais, dependendo do tamanho da base de dados.

Poxa Rafael, muito simples isso que você está falando, quem não sabe disso? Pois é, “todos” sabemos disso, mas não nos atentamos a esse detalhe nas consultas e um pequeno detalhe pode ajudar a sua consulta a ter uma performance um pouco melhor.

A Figura 4 demonstra uma consulta realizada com EF Core, a qual não foi projetada e, assim, selecionando todos os campos das tabelas Autor e Livro respectivamente.

using var db = new LivrariaContext();
var autores = db.Autores.Include(x => x.Livros).ToList();
Figura 4 — Consulta EF Core não projetada.
public class Livro
{
public Livro()
{

}

public int Id { get; set; }
public string Titulo { get; set; } = default!;
public int AnoPublicacao { get; set; }
public int Edicao { get; set; }
public string Editora { get; set; } = default!;
public int AutorId { get; set; }
public Autor Autor { get; set; }
}

public class Autor
{
public Autor()
{
Livros = new List<Livro>();
}

public int Id { get; set; }
private string _nome = string.Empty;
public string Nome
{
get
{
return _nome;
}
set
{
_nome = value.ToUpper();
}
}

public string CPF { get; set; } = default!;
public string Email { get; set; } = default!;
public IEnumerable<Livro> Livros { get; set; }
}

As consultas projetadas visam uma execução mais rápida, uma vez que selecionamos somente o que realmente desejamos retornar de informações do banco de dados e, dessa forma, tornando as nossas consultas mais eficientes. O código a seguir exemplifica uma consulta projetada por um Data Trasnfer Objetc — DTO informando apenas os campos necessários.

using var db = new LivrariaContext();
var autores = db.Autores
.Include(x => x.Livros)
.Select(x => new AutorDto()
{
Id = x.Id,
Nome = x.Nome,
Livros = x.Livros.Select(y => new LivroDto()
{
Id = y.Id,
Titulo = y.Titulo
})
.ToList()
})
.ToList();
Figura 5 — SQL gerado pela consulta projetada.

Podemos realizar a consulta projetada com LINQ e AutoMapper também, as consultas a seguir ilustram essa utilização.

using var db = new LivrariaContext();
var autores = from aut in db.Autores
select new AutorDto()
{
Id = aut.Id,
Nome = aut.Nome,
Livros = from listAutLiv in aut.Livros
select new LivroDto()
{
Id = listAutLiv.Id,
Titulo = listAutLiv.Titulo
}
};

autores.ToList();
 using var db = new LivrariaContext();
var autores = mapper.ProjectTo<AutorDto>(db.Autores.Include(x => x.Livros)).ToList();

Para as consultas projetadas com o AutoMapper (ProjectTo), devemos ficar atento para o DTO que informamos, pois essa forma vai projetar a classe como um todo, e caso ele contenha mais informações do que o necessário, vai acabar se tornando uma consulta não projetada. Assim, talvez é preferível que se crie um DTO especifico para a consulta que se deseja fazer, como no caso da consulta abaixo.

using var db = new LivrariaContext();
var autores = mapper.ProjectTo<AutorAutoMapperDto>(db.Autores.Include(x => x.Livros)).ToList();
public class AutorAutoMapperDto
{
public AutorAutoMapperDto()
{
Livros = new List<LivroAutoMapperDto>();
}

public int Id { get; set; }
public string Nome { get; set; } = default!;
public IEnumerable<LivroAutoMapperDto> Livros { get; set; }
}

BENCHMARK

Por curiosidade executei todas as formas de consultas projetadas apresentadas nesse artigo em uma base de dados contendo 3000 registros para observar o desempenho. Lembrando que o cenário de livraria adotado é bem simples e sem complexidade, apenas para fins didáticos. Para o teste é utilizado a biblioteca benchmark para o .NET 6 com o intuito de analisar a performance das consultas. Basicamente é executado uma consulta não projetada e uma projetada para a “técnica” apresentada anteriormente.

Consultas Não Projetadas:

· ConsultarAutoresEFCore

· ConsultarAutoresLINQ

· ConsultarAutoresAutoMapperProjetada (cujo DTO é igual ao banco e no fim retorna todos os campos)

Consultas Projetadas:

· ConsultaeAutoresEFCoreProjetada

· ConsultaeAutoresEFCoreProjetadaAsNoTracking

· ConsultaeAutoresEFCoreProjetadaComResolucaoIdentidade

· ConsultarAutoresLINQProjetada

· ConsultarAutoresAutoMapperProjetadaEspecifica

Figura 6 — Resultado benchmark

Olhando para os resultados, podemos observar que todas as consultas projetadas tiveram um resultado melhor (veja as colunas Rank na Figura 6), desde a alocação de memória até tempo de execução (colunas Allocated e Mean da mesma figura), o que já era esperado. Dessa forma, a projeção que utiliza LINQ ficou em primeiro lugar com uma pequena diferença no tempo. O que me chamou a atenção foi a performance das consultas com AutoMapper (ProjectTo), que para esse cenário teve um bom resultado, ficando em segundo lugar. Outro detalhe notado foi que a consulta projetada rastreada pelo EF Core teve um desempenho muito parecido com a consulta não rastreada, e foi melhor em comparação com a consulta com resolução de identidade. Acredito que isso se dá pelo fato de ser um cenário simples e de não haver muitos itens repetidos na base de dados. Como esse caso do tracker é relativamente simples de se testar em casos mais complexos, é sempre bom testar os três para ver qual apresenta o melhor desempenho para o seu problema, uma vez que a alocação de memória foi a mesma e a diferença de tempo foi pequena.

Para finalizar, deixo o link de dois vídeos bem interessantes para complementar o assunto de consultas a base de dados com EF Core. O primeiro é um vídeo do Bruno Brito, do Desenvolvedor.io onde ele mostra como ter mais efetividade em consultas com vários campos utilizando o EF Core. O Segundo é do Milan Jovanović, onde ele mostra como conseguiu otimizar uma consulta em 233x.

O link do github do projeto e os links de ambos os vídeos podem ser encontrados na seção de referências.

REFERÊNCIAS

https://balta.io/blog/sql-server-docker

https://renatogroffe.medium.com/performance-de-c%C3%B3digo-em-net-implementando-testes-com-benchmarkdotnet-4c6cabb82607

https://learn.microsoft.com/pt-br/dotnet/csharp/programming-guide/concepts/linq/basic-linq-query-operations

https://docs.automapper.org/en/stable/Queryable-Extensions.html

https://learn.microsoft.com/pt-br/ef/core/performance/efficient-querying

https://www.macoratti.net/Cursos/ef_curb1/ef_curb11.htm
https://macoratti.net/21/09/efc_iqueryassinc1.htm

Vídeo Bruno Brito: https://www.youtube.com/watch?v=zcTVRP7kKBY

Vídeo Milan Jovanović: https://www.youtube.com/watch?v=jSiGyPHqnpY

Github do projeto: https://github.com/rafael-miranda10/consultas-projetadas-efcore

--

--

Rafael Arthur Rocha Miranda
#LocalizaLabs

Bacharel em sistemas de informação pela Universidade do Oeste Paulista — UNOESTE e Mestre em ciências pela Universidade Tecnológica Federal do Paraná — UTFPR