Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Abonnements

Entity Framework, soyez vigilant avec la méthode Include !

La méthode Include est très facile à utiliser ce qui est une bonne chose. Cependant son implémentation est loin d’être la plus optimale !

Pourquoi ? La méthode Include permet de récupérer des graphes d’entité en une seule requête SQL. L’idée est excellente mais…

Le problème c’est que pour faire cela, EF doit utiliser un join.

Qu’est-ce que ça implique ?

Ca implique une requête lente qui retourne trop d’octets.

A la place de :

context.Categories.Include("Products")

C’est souvent plus performant de faire cela :

foreach (var p in context.Products.Where(p => p.Category != null)) ;
context.Categories

Le problème avec ce moyen c’est qu’il implique plusieurs accès à la base de données. C’est dommageable.

Il est possible de corriger ce défaut en utilisant les MutiResultSets.

J’ai utilsé pour cela mon dernier post et j’ai rajouté la méthodes suivantes dans la classe ObjectContextExtension.

public static IEnumerable<T1> Include<T1, T2>(this ObjectContext context, IQueryable<T1> query1, IQueryable<T2> query2, MergeOption mergeOption = MergeOption.AppendOnly)
    where T1 : class, new()
    where T2 : class, new()
{
    if (mergeOption == MergeOption.NoTracking)
        throw new NotImplementedException();
    foreach (T1 item in from kvp in Execute(context, query2, query1, mergeOption).SkipWhile(elt => elt.Key != query1)
                        select kvp.Value)
        yield return item;
}

public static IEnumerable<T1> Include<T1, T2, T3>(this ObjectContext context, IQueryable<T1> query1, IQueryable<T2> query2, IQueryable<T3> query3, MergeOption mergeOption = MergeOption.AppendOnly)
    where T1 : class, new()
    where T2 : class, new()
    where T3 : class, new()
{
    if (mergeOption == MergeOption.NoTracking)
        throw new NotImplementedException();
    foreach (T1 item in from kvp in Execute(context, query2, query3, query1, mergeOption).SkipWhile(elt => elt.Key != query1)
                        select kvp.Value)
        yield return item;
}

Maintenant, exécutons quelques tests de performance sur la base Northwind :

test1:
context.Categories.Include("Products")

test2:
context.Categories.Include("Products").Where(c => c.CategoryName == "Beverages")

test3:
context.Categories.Include("Products.OrderDetails")

test4:
context.Categories.Include("Products.OrderDetails").Where(c => c.CategoryName == "Beverages")

test5:
context.Categories.Include("Products.OrderDetails").Where(c => c.CategoryName == "Unexisting")

Il est possible d’avoir les mêmes résultats avec plusieurs requêtes simples :

test1:
foreach (var p in context.Products.Where(p => p.CategoryID != null)) ;
context.Categories

test2:
foreach (var p in context.Products.Where(p => p.Category.CategoryName == "Beverages")) ;
context.Categories.Where(c => c.CategoryName == "Beverages")

test3:
foreach (var od in context.OrderDetails.Where(od => od.Product.CategoryID != null)) ;
foreach (var p in context.Products.Where(p => p.CategoryID != null)) ;
context.Categories

test4:
foreach (var od in context.OrderDetails.Where(od => od.Product.Category.CategoryName == "Beverages")) ;
foreach (var p in context.Products.Where(p => p.Category.CategoryName == "Beverages")) ;
context.Categories.Where(c => c.CategoryName == "Beverages")

test 5:
foreach (var od in context.OrderDetails.Where(od => od.Product.Category.CategoryName == "Unexisting")) ;
foreach (var p in context.Products.Where(p => p.Category.CategoryName == "Unexisting")) ;
context.Categories.Where(c => c.CategoryName == "Unexisting")

Ou avec ma méthode Include :

test 1:
context.Include(context.Categories, context.Products.Where(p => p.CategoryID != null))

test 2:
context.Include(
    context.Categories.Where(c => c.CategoryName == "Beverages"),
    context.Products.Where(p => p.Category.CategoryName == "Beverages"))

test 3:
context.Include(context.Categories,
    context.Products.Where(p => p.CategoryID != null),
    context.OrderDetails.Where(od => od.Product.CategoryID != null))

test 4 :
context.Include(
    context.Categories.Where(c => c.CategoryName == "Beverages"),
    context.Products.Where(p => p.Category.CategoryName == "Beverages"),
    context.OrderDetails.Where(od => od.Product.Category.CategoryName == "Beverages"))

test 5:
context.Include(
    context.Categories.Where(c => c.CategoryName == "Unexisting"),
    context.Products.Where(p => p.Category.CategoryName == "Unexisting"),
    context.OrderDetails.Where(od => od.Product.Category.CategoryName == "Unexisting"))

Les résultats sont les suivants :

test 1

Coeff d’amélioration

test 2

Coeff d’amélioration

test 3

Coeff d’amélioration

test 4

Coeff d’amélioration

test 5

Coeff d’amélioration

Temps d’exécution (ms)

L2E Include

222

95

342

163

93

Plusieurs select

159

1,40

61

1,56

124

2,76

66

2,47

24

3,88

Mon Include

57

3,89

22

4,32

433

0,79

128

1,27

22

4,23

Taille du paquet retourné par la base (octets)

L2E Include

847110

132699

2.375347E7

4462675

1534

Plusieurs select

95335

8,89

12619

10,52

145134

163,67

22065

202,25

663

2,31

Mon Include

95335

8,89

12619

10,52

145134

163,67

22065

202,25

663

2,31

A noter que, dans le cadre de mes tests, la base est locale. Les résultats auraient été encore pire pour la méthode Include de L2E sinon.

De plus, si Product.CategoryID n’était pas nullable, on améliorerait encore le coeff d’amélioration pour test1 et test3 parce qu’on éviterait un join dans la requête SQL générées.

Pour les tests test 3 et test 4, plusieurs select sont plus performant que mon Include. Je ne comprends pas pourquoi. La différence de performance se fait en base comme si SQL Server avait du mal avec les multi-resultset. Si quelqu’un peut m’expliquer, merci d’avance.

La méthode Include de L2E peut également s’avérer être la meilleure dans le cas suivant par exemple :

context.OrderDetails.Include("Product.Category").First()

Pour conclure, la méthode Include est très cool car très facile à utiliser. Cependant, dans certains cas, ce n’est pas la plus performante. Comme je viens de l’illustrer, il est parfois possible de l’améliorer de façon très significative.

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Publié lundi 12 juillet 2010 15:00 par Matthieu MEZIL

Classé sous : , , ,

Commentaires

# re: Entity Framework, soyez vigilant avec la méthode Include ! @ lundi 12 juillet 2010 15:23

Je suis assez surpris par tes résultats. Je suis confronté au problème d'une chaine d'include qui font produire à EF une requête longue comme le bras, mais les performances deviennent tout simplement ridicules (plusieurs minutes !) sans les include.

Ca dépend peut être du nombre d'objets. Dans mon cas j'en ramène 3000 depuis la base (et plus avec les objets liés car j'ai des relations 1-n). Tu ramènes combien de catégories dans ton exemple ?

En tout cas je creuserai la solution de ton include "custom", j'ai bon espoir que ça contribue à corriger mes problèmes.

KooKiz

# re: Entity Framework, soyez vigilant avec la méthode Include ! @ lundi 12 juillet 2010 17:22

Salut Mathieu,

Je serai très intéressé par davantage d'informations sur:

- le nombre de requêtes à la base de données pour chacun des tests réalisés (il me semble que la taille du paquet du L2E est plus élevé compte tenu du fait que les enregistrements sont retournés en 1 fois, ce qui économise le nombre de connexions à la base nécessaires pour retourner l'ensemble des enregistrements, cas des tests suivants. Peux-tu nous donner la taille cumulée des paquets-&gt; nb connexions*taille paquet)

- les traces observées au SQL Profiler pour chacun des tests.

J'aurai en tendance à penser que l'Include aurait été la solution offrant le meilleur ratio dans 99% des cas pour une forte volumétrie du fait de l'utilisation du JOIN et donc du bénéfice des index.

Très intéressant!  A suivre...

MPOWARE

# re: Entity Framework, soyez vigilant avec la méthode Include ! @ mardi 13 juillet 2010 13:58

Tout d'abord la raison des perfs du test3 et 4 (merci Denis)

Dans le cas des requêtes distinctes, la troisième requête tire parti des compilations préalables des deux requêtes précédentes (cache), alors que dans le cas du multi-resultset, elle est compilée en une seule fois par le moteur.

@ KooKiz et Frédéric, voici quelques infos supplémentaires :

J'ai 9 Categories, 77 Products dont 12 pour Beverages et 2155 OrderDetails dont 404 pour Beverages.

@ KooKiz : "mais les performances deviennent tout simplement ridicules (plusieurs minutes !) sans les include"

Bizarre. La première idée qui me vient est la suivante : n'est-ce pas un problème de Lazy Loading qui du coup te génère plein de requêtes pour charger les éléments ? As-tu tracé les requêtes qui passent ?

@Frédéric : "le nombre de requêtes à la base de données pour chacun des tests réalisés (il me semble que la taille du paquet du L2E est plus élevé compte tenu du fait que les enregistrements sont retournés en 1 fois, ce qui économise le nombre de connexions à la base nécessaires pour retourner l'ensemble des enregistrements, cas des tests suivants. Peux-tu nous donner la taille cumulée des paquets-&gt; nb connexions*taille paquet)"

Dans le cas de mon Include et du Include EF, il n'y a qu'une seule connection (c'est l'intérêt d'utiliser des multi-resultset dans mon Include). Dans le cas intermédiaire (plusieurs requêtes), il y en a deux dans les tests 1 et 2 et 3 ensuite.

La taille de ce qui est retournée est donnée dans le tableau. Donc je n'ai probablement pas compris ce que tu voulais. Peux-tu clarifier stp ?

"les traces observées au SQL Profiler pour chacun des tests."

C'est à dire ? Tu veux les requêtes SQL générées ? le temps d'exécution ? le Read ? l'utilisation CPU ?

"J'aurai en tendance à penser que l'Include aurait été la solution offrant le meilleur ratio dans 99% des cas pour une forte volumétrie du fait de l'utilisation du JOIN et donc du bénéfice des index"

Non car, d'une part, le temps d'exécution de la requête est plus long avec un JOIN (même avec un index) que celui pour deux select basiques. D'autre part, l'explosion de la taille des données retournées par la requêtes (ratio de 163,67 dans le test3 !!!) ralenti considèrablement la récupération des résultats (et encore dans le cadre de mes tests, ma base est locale, ça aurait été encore pire sinon). De plus, il faut également rajouter dans ce cas un temps de traitement, certe minime mais existant, pour permettre à EF de récupérer dans le jeu de résultat les Categories distinctes et les produits.

Matthieu MEZIL

# re: Entity Framework, soyez vigilant avec la méthode Include ! @ mardi 13 juillet 2010 14:04

"Bizarre. La première idée qui me vient est la suivante : n'est-ce pas un problème de Lazy Loading qui du coup te génère plein de requêtes pour charger les éléments ? As-tu tracé les requêtes qui passent ?"

Oui c'est le lazy loading. Mais il existe un moyen avec EF de ne pas faire de lazy loading sans utilise include ? Je pensais que c'était ce que faisait ton exemple sans include.

KooKiz

# re: Entity Framework, soyez vigilant avec la méthode Include ! @ mardi 13 juillet 2010 19:34

Tu peux désactiver le lazy loading

context.ContextOptions.LazyLoadingEnabled = false;

Matthieu MEZIL

# re: Entity Framework, soyez vigilant avec la méthode Include ! @ mardi 13 juillet 2010 19:49

Sauf que j'ai besoin des objets associés :p

Maintenant avec le recul je réalise. Sachant que je charge pratiquement tout, si je fais juste une requête pour chaque tables (dans ton exemple : foreach (var p in context.Products) ; foreach (var  c in context.Categories) ; ), logiquement il devrait ensuite être capable de recoller les morceaux tout seul sans repasser par la base quand je fais myProduct.Categories ? Si oui ça devrait régler mon problème.

KooKiz

# re: Entity Framework, soyez vigilant avec la méthode Include ! @ mardi 13 juillet 2010 20:20

Oui c'est exactement ça.

Et c'est pour ça que tu peux désactiver le Lazy Loading.

Matthieu MEZIL

# re: Entity Framework, soyez vigilant avec la méthode Include ! @ mardi 13 juillet 2010 20:44

Je tenterai ça dès que possible alors, merci pour les conseils !

KooKiz

Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01