Sqlserver
 sql >> Base de données >  >> RDS >> Sqlserver

Le code Entity-framework est lent lors de l'utilisation multiple de Include()

tl;dr Plusieurs Include s explose le jeu de résultats SQL. Bientôt, il devient moins cher de charger des données par plusieurs appels de base de données au lieu d'exécuter une méga instruction. Essayez de trouver le meilleur mélange de Include et Load déclarations.

il semble qu'il y ait une pénalité de performance lors de l'utilisation d'Include

C'est un euphémisme! Plusieurs Include s explose rapidement le résultat de la requête SQL à la fois en largeur et en longueur. Pourquoi est-ce ?

Facteur de croissance de Include s

(Cette partie applique Entity Framework classique, v6 et versions antérieures)

Disons que nous avons

  • entité racine Root
  • entité parente Root.Parent
  • entités enfants Root.Children1 et Root.Children2
  • une instruction LINQ Root.Include("Parent").Include("Children1").Include("Children2")

Cela crée une instruction SQL qui a la structure suivante :

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

UNION

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

Ces <PseudoColumns> se composent d'expressions telles que CAST(NULL AS int) AS [C2], et ils servent à avoir le même nombre de colonnes dans tous les UNION -ed requêtes. La première partie ajoute des pseudo-colonnes pour Child2 , la deuxième partie ajoute des pseudo-colonnes pour Child1 .

Voici ce que cela signifie pour la taille du jeu de résultats SQL :

  • Nombre de colonnes dans le SELECT clause est la somme de toutes les colonnes des quatre tables
  • Le nombre de lignes est la somme des enregistrements dans les collections enfants incluses

Étant donné que le nombre total de points de données est de columns * rows , chaque Include supplémentaire augmente de façon exponentielle le nombre total de points de données dans le jeu de résultats. Permettez-moi de le démontrer en prenant Root encore une fois, maintenant avec un Children3 supplémentaire le recueil. Si tous les tableaux ont 5 colonnes et 100 lignes, nous obtenons :

Un Include (Root + 1 collection enfant) :10 colonnes x 100 lignes =1 000 points de données.
Deux Include s (Root + 2 collections enfants) :15 colonnes x 200 lignes =3 000 points de données.
Trois Include s (Root + 3 collections enfants) :20 colonnes x 300 lignes =6 000 points de données.

Avec 12 Includes cela reviendrait à 78000 points de données !

Inversement, si vous obtenez tous les enregistrements de chaque table séparément au lieu de 12 Includes , vous avez 13 * 5 * 100 points de données :6500, moins de 10 % !

Maintenant, ces chiffres sont quelque peu exagérés dans la mesure où nombre de ces points de données seront null , ils ne contribuent donc pas beaucoup à la taille réelle du jeu de résultats envoyé au client. Mais la taille de la requête et la tâche de l'optimiseur de requête sont certainement affectées négativement par l'augmentation du nombre de Include s.

Solde

Donc, en utilisant Includes est un équilibre délicat entre le coût des appels de base de données et le volume de données. Il est difficile de donner une règle empirique, mais vous pouvez maintenant imaginer que le volume de données dépasse généralement rapidement le coût des appels supplémentaires s'il y a plus de ~3 Includes pour les collections enfants (mais un peu plus pour le parent Includes , qui ne font qu'élargir le jeu de résultats).

Alternative

L'alternative à Include consiste à charger des données dans des requêtes distinctes :

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);

Cela charge toutes les données requises dans le cache du contexte. Au cours de ce processus, EF exécute la correction de la relation par lequel il remplit automatiquement les propriétés de navigation (Root.Children etc.) par les entités chargées. Le résultat final est identique à la déclaration avec Include s, à l'exception d'une différence importante :les collections enfants ne sont pas marquées comme chargées dans le gestionnaire d'état d'entité, donc EF essaiera de déclencher le chargement différé si vous y accédez. C'est pourquoi il est important de désactiver le chargement différé.

En réalité, vous devrez déterminer quelle combinaison de Include et Load les déclarations fonctionnent le mieux pour vous.

Autres aspects à prendre en compte

Chaque Include augmente également la complexité des requêtes, de sorte que l'optimiseur de requête de la base de données devra faire de plus en plus d'efforts pour trouver le meilleur plan de requête. À un moment donné, cela peut ne plus réussir. De plus, lorsque certains index vitaux sont manquants (en particulier sur les clés étrangères), les performances peuvent souffrir en ajoutant Include s, même avec le meilleur plan de requête.

Cœur d'Entity Framework

Explosion cartésienne

Pour une raison quelconque, le comportement décrit ci-dessus, les requêtes UNIONed, a été abandonné à partir de EF core 3. Il crée désormais une requête avec des jointures. Lorsque la requête est en forme d'"étoile", cela conduit à une explosion cartésienne (dans le jeu de résultats SQL). Je ne peux trouver qu'une note annonçant ce changement radical, mais elle ne dit pas pourquoi.

Fractionner les requêtes

Pour contrer cette explosion cartésienne, Entity Framework core 5 a introduit le concept de requêtes fractionnées qui permet de charger des données associées dans plusieurs requêtes. Cela empêche la création d'un ensemble de résultats SQL massif et multiplié. De plus, en raison de la moindre complexité des requêtes, cela peut réduire le temps nécessaire pour récupérer les données, même avec plusieurs allers-retours. Cependant, cela peut entraîner des données incohérentes lors de mises à jour simultanées.

Plusieurs relations 1:n en dehors de la racine de la requête.