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

,

Quel rapport entre DataSet, une architecture multi-tiers et Linq to SQL ?

Pas facile ? Si si, juste un Designer qui génère tout pour nous dans Visual Studio 2008, que ce soit pour un DataSet typé ou pour un DataContext Linq To SQL.

Oui mais voilà, là où ça se complique (si on peut dire) c' est lorsque qu' on veut faire une petite architecture 3 tiers (soyons pas fou, restons simple)

 

Dans Visual Studio 2008, le Designer du DataSet est capable de se générer dans 2 projets distincts.

  1. Un projet contenant les DataTables typés.
  2. Un projet contenant les TableAdapters typés.

Tout ça grâce à la petite option magique : DataSet Project

image

 

Voilà à  quoi ressemble ma solution :

image 

C' est beau, c' est propre et ça marche.

 

Alors, je vous le demande, Pourquoi ils n'ont pas fait pareil pour Linq To SQL !!!

J' avais espéré secrètement que le SP1 de Visual Studio 2008 apporte cette feature, mais ce n'est pas le cas (si je me trompe, montrez moi, je serai super heureux si c'était le cas)

Alors comment on fait aujourd'hui ? eh ben à la main, on passe le DataContext en internal et on copie colle le DataContext dans l'assembly service en le repassant en public.

Et si on rajoute des tables au schéma, bah on recommence l'opération.

C'est pas critique, mais je trouve ça dommage …

 

Voilà mon coup de gueule du jour :)

Bonjour à tous,

linqpadlogo

Je viens de découvrir un tool absolument "Must Have" pour tout bon développeur qui se respecte : LinqPad

Cet utilitaire est grosso-modo un Notepad Pour Linq.

J'adore !

Vous lancez et vous faites du LINQ :

image

J'ai trouvé tout de suite, un cas d'utilisation direct de cet outil.
Je veux faire du Linq To Sql dans une base de donnée, vite fait pour voir le contenu de la table Client.

Et bien avec LinqPad, pas besoin de créer un projet VS.Net, de créer un DataContext, une classe console, ou une winforms, blah blah blah …

Je lance juste LinqPad, je me connecte à ma base de donnée (si ça n'a pas été déjà fait dans une session antérieure) et j'interroge directement ma table client… en LINQ !

En plus vous trouverez plein d'exemples, que ce soit LINQ simple ou LINQ to SQL ou encore LINQ To XML.

Enormissime !

Je fais un billet vite fait, pour répondre à une question qu'on m'a posée et à laquelle je n'ai pas tout à fait répondu Smile

La question était : Comment créer un DataContext qui pointe sur SQL SERVER 2008.
Il faut dire que, pour le moment, le designer n'est pas capable de créer votre fichier dbml pour générer le code correspondant à une table SQL SERVER 2008.

Ce qu'il faut savoir c'est que Visual Studio utilise en fait un utilitaire en ligne de commandes, appellé SqlMetal.

Il suffit donc de lancer cet utilitaire "à la main" pour créer vos classes correspondant à vos tables stockées sur SQL SERVER 2008.

Un petit exemple pour la route :
sqlmetal /Server:.\sql2008 /database:Demo /Code:DemoClass.cs

Une petite ressource, au hasard d'internet :
http://www.simple-talk.com/dotnet/.net-tools/exploring-linq,-sqlmetal-and-sqltac/

Bah voilà j'espère avoir répondu à la question posée Wink

Allez je retourne réviser mes slides !