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
etRoot.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.