Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

O código de estrutura de entidade é lento ao usar Include() muitas vezes


tl;dr Vários Include s explodir o conjunto de resultados SQL. Logo fica mais barato carregar dados por várias chamadas de banco de dados em vez de executar uma mega instrução. Tente encontrar a melhor combinação de Include e Load declarações.

parece que há uma penalidade de desempenho ao usar Incluir

Isso é um eufemismo! Vários Include s rapidamente expandem o resultado da consulta SQL tanto em largura quanto em comprimento. Por que é que?

Fator de crescimento de Include


(Esta parte se aplica ao Entity Framework clássico, v6 e anterior)

Digamos que temos
  • entidade raiz Root
  • entidade pai Root.Parent
  • entidades filhas Root.Children1 e Root.Children2
  • uma instrução LINQ Root.Include("Parent").Include("Children1").Include("Children2")

Isso cria uma instrução SQL que tem a seguinte estrutura:
SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children1

UNION

SELECT *, <PseudoColumns>
FROM Root
JOIN Parent
JOIN Children2

Estes <PseudoColumns> consistem em expressões como CAST(NULL AS int) AS [C2], e servem para ter a mesma quantidade de colunas em todos os UNION -ed consultas. A primeira parte adiciona pseudo colunas para Child2 , a segunda parte adiciona pseudo colunas para Child1 .

Isto é o que significa para o tamanho do conjunto de resultados SQL:
  • Número de colunas no SELECT cláusula é a soma de todas as colunas nas quatro tabelas
  • O número de linhas é a soma dos registros nas coleções filhas incluídas

Como o número total de pontos de dados é columns * rows , cada Include adicional aumenta exponencialmente o número total de pontos de dados no conjunto de resultados. Deixe-me demonstrar isso pegando Root novamente, agora com um Children3 adicional coleção. Se todas as tabelas tiverem 5 colunas e 100 linhas, teremos:

Um Include (Root + 1 coleção filho):10 colunas * 100 linhas =1.000 pontos de dados.
Dois Include s (Root + 2 coleções filhas):15 colunas * 200 linhas =3.000 pontos de dados.
Três Include s (Root + 3 coleções filhas):20 colunas * 300 linhas =6000 pontos de dados.

Com 12 Includes isso equivaleria a 78.000 pontos de dados!

Por outro lado, se você obtiver todos os registros para cada tabela separadamente em vez de 12 Includes , você tem 13 * 5 * 100 pontos de dados:6500, menos de 10%!

Agora, esses números são um pouco exagerados, pois muitos desses pontos de dados serão null , portanto, eles não contribuem muito para o tamanho real do conjunto de resultados que é enviado ao cliente. Mas o tamanho da consulta e a tarefa do otimizador de consulta certamente são afetados negativamente pelo aumento do número de Include s.

Saldo


Então, usando Includes é um equilíbrio delicado entre o custo das chamadas de banco de dados e o volume de dados. É difícil dar uma regra prática, mas agora você pode imaginar que o volume de dados geralmente supera rapidamente o custo de chamadas extras se houver mais de ~3 Includes para coleções filhas (mas um pouco mais para as coleções pai Includes , que apenas ampliam o conjunto de resultados).

Alternativa


A alternativa para Include é carregar dados em consultas separadas:
context.Configuration.LazyLoadingEnabled = false;
var rootId = 1;
context.Children1.Where(c => c.RootId == rootId).Load();
context.Children2.Where(c => c.RootId == rootId).Load();
return context.Roots.Find(rootId);

Isso carrega todos os dados necessários no cache do contexto. Durante esse processo, o EF executa correção de relacionamento pelo qual ele preenche automaticamente as propriedades de navegação (Root.Children etc.) por entidades carregadas. O resultado final é idêntico à instrução com Include s, exceto por uma diferença importante:as coleções filhas não são marcadas como carregadas no gerenciador de estado da entidade, então o EF tentará acionar o carregamento lento se você as acessar. É por isso que é importante desativar o carregamento lento.

Na realidade, você terá que descobrir qual combinação de Include e Load declarações funcionam melhor para você.

Outros aspectos a serem considerados


Cada Include também aumenta a complexidade das consultas, de modo que o otimizador de consultas do banco de dados terá que se esforçar cada vez mais para encontrar o melhor plano de consulta. Em algum momento, isso pode não funcionar mais. Além disso, quando alguns índices vitais estão ausentes (especialmente em chaves estrangeiras), o desempenho pode ser prejudicado pela adição de Include s, mesmo com o melhor plano de consulta.

Núcleo do Entity Framework

Explosão cartesiana


Por algum motivo, o comportamento descrito acima, consultas UNIONed, foi abandonado a partir do EF core 3. Agora, ele cria uma consulta com junções. Quando a consulta é em forma de "estrela", isso leva a uma explosão cartesiana (no conjunto de resultados SQL). Só consigo encontrar uma nota anunciando essa mudança, mas não diz o porquê.

Consultas divididas


Para combater essa explosão cartesiana, o núcleo 5 do Entity Framework introduziu o conceito de consultas divididas que permite o carregamento de dados relacionados em várias consultas. Ele impede a construção de um conjunto de resultados SQL massivo e multiplicado. Além disso, devido à menor complexidade da consulta, pode reduzir o tempo necessário para buscar dados, mesmo com várias viagens de ida e volta. No entanto, pode levar a dados inconsistentes quando ocorrem atualizações simultâneas.

Vários relacionamentos 1:n fora da raiz da consulta.