Vamos falar sobre o Entity Framework do ponto de vista do DBA.
Veja os demais artigos:
- Entity Framework: Code-First, Database-Never
- Entity Framework: Broken LINQ
- 7 Motivos para não usar o Entity Framework
Iniciei o SQL Profiler no projeto do ARDA (https://github.com/DXBrazil/Arda) para coletar as queries que estão rodando no banco de dados. Rapidamente encontrei algumas coisas interessantes.
Select TOP 2
"Quem escreve query assim?!?"
Não é errado usar a sintaxe TOP 2, mas é pouco comum especificar exatamente 2 registros a serem retornados.
Procurando no código, não há nenhuma referência de TOP 2. Após analisar um pouco as consultas envolvidas, descobri que as extensões LINQ Single ou SingleOrDefault adicionam esse limite. Esse é um efeito colateral causado pelo Entity Framework que, embora seja inofensivo, não é um comportamento normal.
Entretanto, nem sempre é assim. Veja a discussão anterior no artigo do Broken LINQ.
Stored Procedures
Não tem! "Como posso otimizar o código?"
Scan de tabela
Enquanto estava analisando o desempenho do servidor, encontrei um SCAN de tabela.
A query é simples.
Rapidamente identificamos a falta de índice adequado na tabela. A pergunta é “por que o desenvolvedor não criou índice?”.
Ao mesmo tempo, já deduzi que o desenvolvedor usou code-first e sequer passou pela cabeça dele a necessidade de índice. Sabe como sei disso? No caso, eu era o desenvolvedor dessa aplicação. Só fui enxergar o problema claramente quando saí da frente do Visual Studio e entrei no SQL Management Studio.
Maior carga no banco de dados
Coletando as consultas mais pesadas no banco de dados, identifiquei uma query que realizava operações de SORT e SCAN.
A mesma query apresentava planos de execução ligeiramente parecidos, mas diferentes:
Ao ver qual a consulta problemática, descobrimos outra coisa interessante: a tabela FiscalYear faz JOIN com ela mesma!
Uma das dificuldades do Entity Framework é encontrar o código com as queries LINQ correspondentes às consultas SQL. Quem faz join de FiscalYear com FiscalYear?
- Não há comentários.
- Não está encapsulado em uma procedure.
- Não tenho ideia.
"Por qual motivo o desenvolvedor faz isso?", pergunta o DBA.
Resolvendo o problema
A resolução é simples (mas não trivial).
O primeiro passo é encontrar qual o trecho que gera esse join duplicado da tabela de FiscalYear com ela mesma.
Nesse caso, eu como desenvolvedor, fui procurando por todas as classes C# que acessavam informações da tabela Metrics e FiscalYear. O projeto é pequeno, então não foi difícil descobrir que o problema estava na classe MetricRepository.
O trecho de código com problema possui um JOIN entre as tabelas Metrics e FiscalYear.
Analisando a query LINQ, (aparentemente) não tem referência da tabela FiscalYear com ela mesma.
Demorou um tempo para enxergar que sim, existe! FiscalYear está duplicada. Veja o código novamente até encontrar.
...
(Se você ainda não achou, veja que a consulta faz join entre 3 tabelas: _context.Metrics, _context.FiscalYear e m.FiscalYear).
Está muito claro! O código está forçando o JOIN do FiscalYear com FiscalYear. Como essa relação é expressa por um campo já existente na tabela Metrics, então não precisamos fazer esse JOIN. Podemos reescrever assim:
Aplicando o novo código, temos a query correta:
Os detalhes do problema estão registrados no GitHub Issue que acabei de abrir.
[GitHub Issue #100] Improve SQL query performance for Metrics
https://github.com/DXBrazil/Arda/issues/100
Conclusão
No final do dia, os pensamentos do DBA são:
- Quem escreve query assim? (ex: SELECT TOP2 e afins)
- Como posso otimizar o código? (ex: stored Procedures não tem!)
- Por que o desenvolvedor não criou índice? (ex: scan de tabela)
- Por qual motivo o desenvolvedor faz isso? (ex: maior carga no banco de dados)
Na maior parte dos casos, o DBA se sente incapaz de resolver problemas de desempenho causado pelo EF. Embora a resolução seja simples, ela não é trivial.