EF Core - Otimizando Consultas - II


 Hoje vou apresentar dicas de como otimizar alguns recursos do EF Core que podem ajudar a melhorar o desempenho das consultas.

 Vou apresentar 2 dicas para otimizar consultas no  EF Core , lembrando que atualmente a versão estável do EF Core é a versão 7.0.X)

1- A primeira dica refere-se a utilização do DbContext Pooling

Para melhorar o desempenho de aplicações web podemos considerar o uso do Context Pooling ou DbContext Pooling.

Usando o recurso DbContext Pooling – um pool de instâncias reutilizáveis pode ser criado, e, ao invés de criar uma nova instância toda vez, o código primeiro verificará se há uma instância disponível no pool. Com isso vamos garantir o reaproveitamento de um DbContext, ou seja, no momento que uma instância DbContext for solicitada, primeiro vamos verificar se existe uma instância já disponível no pool, e depois que o processamento da solicitação for finalizada, qualquer estado na instância será redefinido e a instância volta para o pool.

Usar este recurso vale a a pena apenas quando seu aplicativo tiver uma carga decente, pois ele apenas armazena em cache as instâncias do DbContext para não descartar após cada solicitação e recriar novamente. Você pode definir o tamanho máximo do pool, para que ele descarte aqueles que estiverem acima do limite.

1- Classe Startup

...

 services.AddDbContextPool<AppDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

...

2- Classe Program

...

string conexaoSQL = builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddDbContextPool<AppDbContext>(options =>
                    options.UseSqlServer(conexaoSQL);

...

Segundo a Microsoft os benchmarks mostram que esse recurso pode ter um impacto decisivo em aplicativos de alto desempenho e baixa latência. Para isso:

1- Certifique-se de que o maxPoolSize corresponda ao seu cenário de uso; se for muito baixo, as instâncias do DbContext serão constantemente criadas e descartadas, prejudicando o desempenho. Defini-lo muito alto pode consumir memória desnecessariamente, pois as instâncias de DbContext não utilizadas são mantidas no pool.

2- Para um aumento de desempenho mais refinado, considere usar PooledDbContextFactory em vez de ter instâncias de contexto de injeção de DI diretamente (suportado no EF Core 6 e superior). O gerenciamento de DI do pool de DbContext gera uma pequena sobrecarga.

Para mais detalhes consulte a documentação aqui e aqui.

2- A segunda dica refere-se usar o recurso Split Query (Consultas Divididas)

O EF Core permite especificar que uma determinada consulta LINQ deve ser dividida em várias consultas SQL. Em vez de JOINs, as consultas divididas geram uma consulta SQL adicional para cada navegação de coleção incluída:

Esse recurso foi introduzido no EF Core 5.0, onde só funcionava ao usar Include. O EF Core 6.0 adicionou suporte para consultas divididas ao carregar dados relacionados em projeções, sem usar o Include.

Dessa forma originalmente todas as entidades relacionadas carregadas com Include, ThenInclude e outras variações de junção produzem junções no lado do banco de dados. 

Considere a seguinte consulta:

var resultado  = await context.TareafaListas
                          .Include(l => l.TarefaItems)
                          .ThenInclude(i => i.TagLista)
                          .ToListAsync();
 

Isso significa que, quando carregamos TarefaListas e TarefaItems, os valores da lista serão duplicados em todos os TarefaItems, e ainda pior, quando nos aprofundarmos e carregarmos TagLista para todos as TarefasItems, então TarefaItem e os dados da lista serão duplicados para cada tag.

Esse problema de duplicação de carregamento profundo tem o nome de "explosão cartesiana" que ocorre quando você carrega mais e mais relacionamentos um para muitos, a quantidade de dados duplicados aumenta e um grande número de linhas é retornado. Isso terá um grande impacto no desempenho do seu aplicativo. Como você pode ver no exemplo, isso pode ficar muito grande muito rapidamente.

Devido à explosão cartesiana, houve a necessidade de dividir grandes consultas em várias chamadas, por isso foi introduzido .AsSplitQuery() para não fazer várias chamadas manualmente:

var resultado  = await context.TareafaListas
                          .Include(l => l.TarefaItems)
                          .ThenInclude(i => i.TagLista)
                          .AsSplitQuery()
                          .ToListAsync();
 

Outra maneira é habilitar o comportamento de consulta dividida globalmente e substituir o comportamento chamando .AsSingleQuery() quando necessário.

1- Classe Startup

...

 services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
            b=> b.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery))); 

...

2- Classe Program

...

string conexaoSQL = builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(conexaoSQL,
            b=> b.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery))); 

...

Quando as consultas divididas são configuradas como padrão, ainda é possível configurar consultas específicas para serem executadas como consultas únicas:

using (var context = new SplitQueriesBloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
      
 .AsSingleQuery()
        .ToList();
}

O EF Core usa o modo de consulta única por padrão na ausência de qualquer configuração. Como pode causar problemas de desempenho, o EF Core gera um aviso sempre que as seguintes condições são atendidas:

Para desativar o aviso, configure o modo de divisão de consulta globalmente ou no nível de consulta para um valor apropriado.

Embora a consulta dividida evite os problemas de desempenho associados aos JOINs e à explosão cartesiana, ela também apresenta algumas desvantagens:

Agora, nem todos os aplicativos da web precisariam dessa otimização, mas conhecer esse recurso economizaria seu tempo ao enfrentar esses problemas.

Saiba mais sobre este recurso aqui.  

"A ti, ó Deus, glorificamos, a ti damos louvor, pois o teu nome está perto, as tuas maravilhas o declaram"
Salmos 76:1

Referências:


José Carlos Macoratti