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)

image

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 :

image

Ok, j’analyse la requête et quelque chose me tracasse : Le calcul du row_number :

image

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 :

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

image

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 :

imageLes 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: image

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

,