EF Core 5 Performance — Dividir Consultas
O Entity Framework Core 5 está focado em várias novidades, e uma delas que diz respeito à performance é dividir as consultas a serem executadas no banco de dados.
Muitas empresas tem o DBA como especialista em gerar o T-SQL / PL-SQL que será executado no banco em si, e no EF Core 5 isto tem ajudado muito os desenvolvedores, afim de submeter ao DBA tais sintaxes e testar quais são melhores para determinadas situações.
Vamos a um exemplo prático que farei com o SQL Server. Crie um projeto de Console App em C#, instale os seguintes pacotes via NUGET: SqlServer para o banco de dados e o Tools para o Migration:
<PackageReference Include=”Microsoft.EntityFrameworkCore.SqlServer” Version=”5.0.4" />
<PackageReference Include=”Microsoft.EntityFrameworkCore.Tools” Version=”5.0.4">
Em seguida crie a classe Models.cs contendo as seguintes classes que identificam uma Etapa de corrida de motos e os pilotos participantes. Portanto, uma etapa (classe Etapa) pode conter vários pilotos (List<Piloto>).
Agora precisamos do contexto da classe, então crie um novo arquivo chamado BancoContexto que herda de DbContext com os dois DbSets<T>. O DbContext é o reponsável por todas as operações de banco de dados (criar, deletar, abrir e fechar a conexão) e os DbSets<T> se encarregam de fazer o CRUD dinâmico via EF Core.
Note que no OnConfiguring usei no UseSqlServer o banco de dados EFCoreSplitQuery a ser criado via Migration. E já habilitei o LOG de comandos no banco para que possamos visualizar as consultas.
E, no OnModelCreating já configurei alguns registros a serem adicionados ao banco via Migration para que tenhamos dados para rodar a aplicação.
Compile o projeto, e se tudo estiver sem erros, execute o Migration com os dois comandos a seguir a serem executados na janela do Package Manage Console. O primeiro cria o Script e o segundo cria o banco em si.
PM> Add-Migration SetupInicial
PM> Update-Database
Listar os Dados
Agora vamos listar os dados e ver como são geradas as consultas no banco de dados. No Program.cs digite o código a seguir, no bloco do USING está contido o contexto, então temos acesso a todas as propriedades do mesmo, ou seja, qualquer referência às Etapas e Pilotos basta usar o CTX.
Note que há dois loopings foreach para listar as Etapas e os Pilotos separadamente.
Execute (F5) o código e veja o resultado com o LOG exibido. Observe que o comando ExecuteReader (linha 3) levou apenas 172ms (linha 5) para executar o T-SQL da linha 9. Veja o T-SQL criado para pilotos na linha 22. Os resultados das listagens estão nas linhas 11 e 24.
Agora vamos incluir na mesma consulta as duas tabelas. Para isto usaremos o INCLUDE e é preciso adicionar o using Microsoft.EntityFrameworkCore;
Comente o bloco de código anterior e escreva dois loopings aninhados. Veja que como Pilotos (List<Piloto>) é uma propriedade da classe Etapa, podemos navegar sem problemas nesta lista de pilotos de uma etapa.
Execute F5 o código e veja o LOG gerado. Temos agora apenas uma consulta T-SQL sendo executada no banco usando o LEFT JOIN (linha 10).
Split Query
Agora vem a novidade do EF Core 5 com o recurso de declarar que toda consulta DEVERÁ ser dividida, afim de não usar o LEFT JOIN, RIGHT JOIN, etc. Ou seja, cada execução será apenas o Select.
Isto é permitido usando a declaração AsSplitQuery() de uma instrução de consulta. No exemplo a seguir, note que ctx.Etapas continua usando o Include de pilotos, porém está com o AsSplitQuery. Desta forma, a execução ocorrerá separadamente.
Veja o código gerado no LOG, na linha 8 o Select de Etapas e na linha 17 o Select dos pilotos usando o INNER JOIN.
SplitQuery / SingleQuery para todas as execuções
Há uma forma de definir que todas as consultas a serem executadas no banco sejam do tipo SplitQuery ou SingleQuery. Basta no arquivo BancoContexto.cs, no método OnConfiguring adicionar o UseQuerySplittingBehavior no optionBuilder.
E mesmo que esteja setado para SplitQuery globalmente, você pode sobrepor uma determinada consulta como SingleQuery. Para isto, é preciso declarar AsSingleQuery(), conforme o exemplo a seguir:
Execute e veja o resultado no LOG que a consulta gerou apenas um Select (linha 8).
Veja o código completo do Program.cs, note que você pode controlar como que as consultas serão geradas no banco, permitindo executar vários testes de performance e aprender o que o EF Core 5 gera de T-SQL. E, isto claro, vale para qualquer provider do EF Core 5.
Apesar de sabermos que dividir as consultas evita questões de performance, há algumas desvantagens:
- Quando uma consulta única está sendo executada, via de regra o banco garante a consistência de dados. Mas em casos de updates simultâneos, isto nem sempre ocorre, só controlando com transação.
- Toda execução de consulta é uma ida e volta no banco, o que pode implicar diretamente no desempenho, especialmente onde a latência para o banco de dados é alta (por exemplo, serviços de nuvem).
- Bancos como SQL Server com MARS, SQLite, entre outros permitem várias consultas ao mesmo tempo, mas a maioria permite apenas uma única consulta ativa em qualquer ponto específico. Neste caso, os resultados das outras consultas deverão ficar em cache na memória, implicando em requisitos de memória cada vez mais alto.
Então, cabe a você simular os cenários e ver qual melhor se adapta ao seu, pois abranger tudo não existe milagre.
Conclusão
O EF Core 5 nos oferece este recurso de gerenciamento de como que as consultas serão geradas no banco, e você deverá testar para chegar ao melhor desempenho x recursos.
Bons estudos e sucesso nos projetos.
Repositório de dados:
rehaddad/EFCore5_SplitQuery (github.com)
Renato Haddad
Microsoft Most Valuable Professional
rehaddad@msn.com