[Linq] Enumerable vs Queryable

Beaucoup de personnes ne savent pas réellement la différence entre l’utilisation des classes statiques Enumerable et Queryable. Elles offrent les mêmes méthodes d’extensions, elles produisent le même résultat, mais pourtant en interne elle se comportement très différemment.

Différence entre Enumerable et Queryable

La classe statique Enumerable contient des méthodes d’extensions qui s’utilisent sur des séquences implémentant l’interface IEnumerable<T>. Ces méthodes parcourt de manière différée la séquence qui leur sont passée en paramètre. Ce type de parcours est réalisé grâce à l’instruction yield return. L’exemple suivant illustre l’implémentation d’une méthode Ou() qui reproduit le comportement du Where() de la classe Enumerable.

public static class ExempleExtensions
{
    public static IEnumerable<T> Ou<T>(this IEnumerable<T> séquence, Func<T, bool> condition)
    {
        foreach (T élément in séquence)
        {
            if (condition(élément) == true)
            {
                yield return élément;
            }
        }
    }
}

Vous constatez que ce code ne fait que parcourir la séquence et retourne les différents éléments sous forme d’un itérateur IEnumerable<T>.

La classe statique Queryable offrent les mêmes méthodes d’extensions, mais sur des séquences qui implémentent IQueryable<T>. La différence, vient du fait que cette méthode retourne un objet IQueryable<T> qui construit un arbre à expression. Cette arbre est ensuite analysé par un provider et se charge d’exécuter la requête. Par exemple, Linq To Entities est un provider qui se charge d’analyser un arbre à expression Linq et de générer une requête SQL au format texte. Cette dernière sera automatiquement exécutée sur un serveur de base de données.

Dans la version 3.5 du .NET Framework, un provider Linq To Objects a été implémenté par Microsoft. Ce provider analyse un arbre à expression Linq, compile un algorithme de parcours en IL et l’exécute. Le résultat produit au final est identique aux méthodes d’extension de la classe Enumerable, mais le comportement est différent.

Pièges à éviter

Il faut savoir que l’interface IQueryable<T> hérite de l’interface IEnumerable<T>. Cela peut donc poser des problèmes de confusion lorsque vous utiliser une méthode d’extension Linq.

Si on prend la ligne suivante :

nouvelleSéquence = séquence.Where(e => e > 10);

Qu’elle est la méthode d’extension Linq qui sera appelée ? Et bien cela dépend du type apparent de la variable “séquence” (et non du type réel !). En effet, si “séquence” est déclarée comme une variable d’un type qui implémente IEnumerable<T>, c’est la méthode d’extension Enumerable.Where<T>() qui sera automatiquement appelée. Dans le cas d’une variable dont le type implémente l’interface IQueryable<T>, c’est la méthode Queryable.Where<T>() qui sera automatiquement appelée.

Voici un exemple qui illustre des appels à la méthode Where() sur un ObjectQuery d’un ObjectContext de Entity Framework (pour rappel ObjectQuery implémente l’interface IQueryable<T>) :

using (PersonnesContext dc = new PersonnesContext())
{
    var requête = from personne in dc.Personnes
                  select personne;

    IEnumerable<Personne> e = requête;
    IQueryable<Personne> q = requête;

    Console.WriteLine(e.Where(p => p.Id == 10).Count());  // Appel Enumerable.Where()
    Console.WriteLine(q.Where(p => p.Id == 10).Count());  // Appel Queryable.Where()
}

Une chose importante à rappeler : La requête n’est pas exécutée au moment de sa création, mais uniquement au moment du déclenchement du parcours de cette dernière (appel à la méthode Count() dans cet exemple). En exécutant ce code on obtient le même nombre de personnes, cependant une différence majeure existe et elle est non visible ! Pour s’en rendre compte, il suffit de regarder dans SQL Profiler les requêtes générées :

Pour la première requête Enumerable.Where() :

SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Nom] AS [Nom],
FROM [dbo].[Personne] AS [Extent1]

Pour la deuxième requête avec Queryable.Where() :

SELECT
[GroupBy1].[A1] AS [C1]
FROM ( SELECT
	COUNT(1) AS [A1]
	FROM [dbo].[Personne] AS [Extent1]
	WHERE 10 = [Extent1].[Id]
)  AS [GroupBy1]

Cette différence s’explique par le fait que l’appel à la méthode d’extension Enumerable.Where() engendre l’exécution de la requête SQL contenue dans la variable “requête” : c’est à dire récupérer toutes les personnes contenues dans la table “Personne”. Ensuite Linq To Object prend le relais et parcourt en mémoire toutes les personnes récupérées en base afin de récupérer celle qui détient l’identifiant égal à 10. On peut dire que les méthodes d’extensions contenues dans la classe Enumerable “cassent” l’arbre syntaxique IQueryable<T>.

Dans la deuxième requête, c’est au moment où l’on réalise un Count() que la requête SQL est générée. En effet, la création de la requête Linq et l’appel à la méthode Queryable.Where() ne fait que créer un arbre à expression Linq. Ce dernier sera analysé, converti en SQL et exécuté en base de données lors de l’appel à la méthode Count().

Bien évidemment, la deuxième requête est la plus performantes surtout si le nombre de personne contenu dans la table est important. Notez que la première requête récupère toutes les lignes contenues dans la table et les fait transiter sur le réseau si le client ne se trouve pas sur la même machine que le SGBD. Ces lignes se trouvent ensuite en mémoire côté client et une boucle (produite par Linq To Object) est ensuite exécutée. En résumé de quoi mettre à genoux une infrastructure du serveur jusqu’au client !

“Casser” une requête afin d’utiliser Linq To Object.

Dans certains cas, il est intéressant de casser une requête afin de forcer l’utilisation de Linq To Object. Par exemple on souhaiterait récupérer le dernier client contenu dans une table :

using (PersonnesContext dc = new PersonnesContext())
{
    var requête = from personne in dc.Personnes
                  orderby personne.Nom
                  select personne;

    Console.WriteLine(requête.Last());
}

La méthode Last() n’est pas supporté par Linq To Entities. Il est possible de remédier à ce problème en utilisant Linq To Object. Pour cela il suffit d’appeler la méthode AsEnumerable() afin d’arrêter la création de l’arbre à expression Linq et d’utiliser exclusivement les méthodes d’extensions contenues dans la classe Enumerable :

using (PersonnesContext dc = new PersonnesContext())
{
    var requête = from personne in dc.Personnes
                  orderby personne.Nom
                  select personne;

    Console.WriteLine(requête.AsEnumerable().Last());
}

Bien évidemment, au niveau performance, la meilleure solution consisterait à récupérer la première personne dont la table est triée par nom décroissant.

Conclusion

Faites donc très attention lorsque vous utilisez les méthodes d’extension Linq avec les classes Enumerable et Queryable sur des providers autre que Linq To Objects. Avec Entity Framework, surveillez vous requêtes qui sont générées avec SQL Profiler.

Publié dans la catégorie C#, Linq.
Tags : , . TrackBack URL.

7 Comments

  1. Article très intéressant. Je vais ajouter un lien vers ton blog

    Cordialement.

  2. krokolud dit :

    Super intéressant, merci.

  3. Michel Bruyère dit :

    La deuxième requête avec « Queryable.Where() » sera surtout plus rapide si le champ Id de la base de données est un index sinon la différence peut être relativement minimum.

  4. Michel Bruyère dit :

    La meilleure façon de résoudre cette demande est :

    var requête = where Id=10
    from personne in dc.Personnes
    select personne;

    La requête sera toujours exécutée dans la base de données.
    La requête Queryable est relativement « lourde » pour la base de données et est une mauvaise requête.

  5. Votre requête Linq est erronée car elle doit commencer par la clause from.
    Bien évidemment, un filtre sur l’ID sera beaucoup plus performant, mais le but de l’article est de montrer les différences entre Enumerable et Queryable.

  6. Oui, mais cet article ne traite pas de la recherche au niveau de la base de données. Il montre juste la différence entre les méthodes d’extensions Queryable et Enumerable…

  7. Michel Bruyère dit :

    C’est bizarre, cette formulation/ordre de la requête pour Linq mais ça n’a pas d’importance.

    Cet article est très intéressant.Mais, il serait préférable de démontrer la différence avec un exemple plus adéquat. Car si la majorité des requêtes SQL générées par le Queryable est …. FROM ( SELECT COUNT(1) FROM … WHERE …) AS [GroupBy1], je déconseille fortement d’utiliser « Queryable ».

    Un filtre sur l’ID sur la requête ne signifie pas qu’un index est présent pour le champ de la table.

Leave a comment