J’ai été confronté à un problème de performances avec EF4.
Le principe de base de l’exercice est de “simplement” faire une pagination sur une table, pour réduire le nombre d’éléments à requêter. J’en avais déjà parlé ici, sans passer par EF4
Juste un point important pour la suite : Il s’agit aussi de faire quelques jointures pour ramener des informations nécessaires à l’affichage.
Je vais faire l’exemple sur la base AdventureWorks, même si celle ci est tout de même bien petite
(en terme de nombre d’enregistrements)
Juste pour info, je veux afficher les informations de la table des détails de factures (+ les infos du produit et du client)
Etape 1 : Le premier jet
Bref, naturellement, je pars sur un truc du genre :
using (AdventureWorks2008R2Entities context = new AdventureWorks2008R2Entities()) { var skipNumber = IndexPage * PageSize; var items = (from sod in context.SalesOrderDetails join soh in context.SalesOrderHeaders on sod.SalesOrderID equals soh.SalesOrderID join p in context.Product on sod.ProductID equals p.ProductID join c in context.Customers on soh.CustomerID equals c.CustomerID orderby sod.SalesOrderDetailID select new { sod.SalesOrderDetailID, c.AccountNumber, soh.ShipDate, p.Name, sod.OrderQty, sod.UnitPrice, sod.LineTotal }).Skip(skipNumber).Take(PageSize); dataGrid1.ItemsSource = items.ToList(); }
Et là WOW ! des performances déplorables ! (Bon sur AdventureWorks, c’est pas flagrant, mais sur une base avec quelques millions de lignes, c’est le drame …)
Ok, n’écoutant que mon courage, je sors mes meilleurs outils : SQL Server management studio et SQL Server Profiler
Le profiler m’indique un truc en substance :
Ok, j’analyse la requête et quelque chose me tracasse : Le calcul du row_number :
Bizarre… il calcul le row_number (donc un bon gros SCAN) et en plus il fait les jointures en même temps… C’est dramatique ça !!
Je vous passe le plan d’exécution :
On remarque bien qu’il remonte via son scan, tous les header, product, et customer.
Etape 2 : Le fait main, sans EF4
Bon pour ne pas en rester là, je repars sur une bonne vieille CTE , à la “mano”, pour voir de quoi il en retourne, quand on ne passe pas par de la génération …
La requête ressemble alors à ça :
;with mCTE (RowNumber, SalesOrderID, SalesOrderDetailID, OrderQty, UnitPrice, LineTotal, ProductID) as ( SELECT row_number() OVER (ORDER BY [SalesOrderDetailID] ASC) as RowNumber, [SalesOrderID] , [SalesOrderDetailID], [OrderQty], [UnitPrice], [LineTotal], [ProductID] From Sales.SalesOrderDetail ) SELECT [mCTE].[SalesOrderDetailID] AS [SalesOrderDetailID], [mCTE].[SalesOrderID] AS [SalesOrderID], [Extent4].[AccountNumber] AS [AccountNumber], [Extent2].[ShipDate] AS [ShipDate], [Extent3].[Name] AS [Name], [mCTE].[OrderQty] AS [OrderQty], [mCTE].[UnitPrice] AS [UnitPrice], [mCTE].[LineTotal] AS [LineTotal] From [mCTE] INNER JOIN [Sales].[SalesOrderHeader] AS [Extent2] ON [mCTE].[SalesOrderID] = [Extent2].[SalesOrderID] INNER JOIN [Production].[Product] AS [Extent3] ON [mCTE].[ProductID] = [Extent3].[ProductID] INNER JOIN [Sales].[Customer] AS [Extent4] ON [Extent2].[CustomerID] = [Extent4].[CustomerID] Where RowNumber between 50000 and 50100
Ok, allons voir du coté du Profiler (ou le résultat est sans appel)
Bon ben on divise les temps d’exécution par 10 (et sur une grosse base, c’est exponentiel) Le nombre de lecture est divisé par 100, bref, là on sent qu’’il y a quelque chose de différent (et performant)
Etape 3 : L’optimisation avec EF4
Alors on peut en rester là, mais on peut aussi tenter d’optimiser la requête générée par Linq to Entities.
Le truc, finalement, c’est de demander à EF4 de D’ABORD calculer le row_number et ENSUITE de faire ses jointures :
On va se la faire en 2 étapes:
using (AdventureWorks2008R2Entities context = new AdventureWorks2008R2Entities()) { var skipNumber = IndexPage * PageSize; var items2 = (from sod in context.SalesOrderDetails orderby sod.SalesOrderDetailID select new { sod.SalesOrderDetailID, sod.SalesOrderID, sod.ProductID, sod.OrderQty, sod.UnitPrice, sod.LineTotal }).Skip(skipNumber).Take(PageSize); var items = from sod in items2.AsQueryable() join soh in context.SalesOrderHeaders on sod.SalesOrderID equals soh.SalesOrderID join p in context.Product on sod.ProductID equals p.ProductID join c in context.Customers on soh.CustomerID equals c.CustomerID select new { sod.SalesOrderDetailID, c.AccountNumber, soh.ShipDate, p.Name, sod.OrderQty, sod.UnitPrice, sod.LineTotal }; dataGrid1.ItemsSource = items.ToList(); }
Notez qu’il n’y a PAS deux exécutions de deux requêtes hein, on utilise bien un IQueryable dans la deuxième requête LINQ.
Bref, le requête, comme d’hab, va être générée et exécutée lors de l’appel du ToList()
Résultat :
Les performances sont bien là, on a quasiment au même niveau que la requête fait main !
et je vous garantis que ça se sent au niveau du “lag” de l’interface utilisateur
Voilà voilà, que du bon !
Conclusion
Un petit tableau récapitulatif de ce qu’on vient de voir:
Note : On peut aussi faire la requête optimisée en “Une Passe” comme ça, mais bon c’est un peu moins maintenable je pense :
var items = from sod in (from sod in context.SalesOrderDetails orderby sod.SalesOrderDetailID select new { sod.SalesOrderDetailID, sod.SalesOrderID, sod.ProductID, sod.OrderQty, sod.UnitPrice, sod.LineTotal }).Skip(skipNumber).Take(PageSize) join soh in context.SalesOrderHeaders on sod.SalesOrderID equals soh.SalesOrderID join p in context.Product on sod.ProductID equals p.ProductID join c in context.Customers on soh.CustomerID equals c.CustomerID select new { sod.SalesOrderDetailID, c.AccountNumber, soh.ShipDate, p.Name, sod.OrderQty, sod.UnitPrice, sod.LineTotal };
Voilà, LINQ ça poutre, mais faut savoir regarder un peu plus loin, des fois