Pourquoi LINQ JOIN est-il tellement plus rapide que la liaison avec WHERE?

99

J'ai récemment mis à niveau vers VS 2010 et je joue avec LINQ to Dataset. J'ai un ensemble de données fortement typé pour l'autorisation qui se trouve dans HttpCache d'une application Web ASP.NET.

Je voulais donc savoir quel est en fait le moyen le plus rapide de vérifier si un utilisateur est autorisé à faire quelque chose. Voici mon datamodel et quelques autres informations si quelqu'un est intéressé.

J'ai vérifié 3 façons:

  1. base de données directe
  2. Requête LINQ avec des conditions Where comme "Join" - Syntaxe
  3. Requête LINQ avec Join - Syntaxe

Voici les résultats avec 1000 appels sur chaque fonction:

1. itération:

  1. 4,2841519 sec.
  2. 115,7796925 sec.
  3. 2,024749 sec.

2. itération:

  1. 3,1954857 sec.
  2. 84 97047 sec.
  3. 1,5783397 sec.

3. itération:

  1. 2,7922143 sec.
  2. 97,8713267 sec.
  3. 1,8432163 sec.

Moyenne:

  1. Base de données: 3,4239506333 sec.
  2. Où: 99,5404964 sec.
  3. Rejoindre: 1,815435 sec.

Pourquoi la version Join est-elle tellement plus rapide que la syntaxe where, ce qui la rend inutile alors qu'en tant que débutant LINQ, elle semble être la plus lisible. Ou ai-je manqué quelque chose dans mes requêtes?

Voici les requêtes LINQ, je saute la base de données:

:

Public Function hasAccessDS_Where(ByVal accessRule As String) As Boolean
    Dim userID As Guid = DirectCast(Membership.GetUser.ProviderUserKey, Guid)
    Dim query = From accRule In Authorization.dsAuth.aspnet_AccessRule, _
                roleAccRule In Authorization.dsAuth.aspnet_RoleAccessRule, _
                role In Authorization.dsAuth.aspnet_Roles, _
                userRole In Authorization.dsAuth.aspnet_UsersInRoles _
                Where accRule.idAccessRule = roleAccRule.fiAccessRule _
                And roleAccRule.fiRole = role.RoleId _
                And userRole.RoleId = role.RoleId _
                And userRole.UserId = userID And accRule.RuleName.Contains(accessRule)
                Select accRule.idAccessRule
    Return query.Any
End Function

Joindre:

Public Function hasAccessDS_Join(ByVal accessRule As String) As Boolean
    Dim userID As Guid = DirectCast(Membership.GetUser.ProviderUserKey, Guid)
    Dim query = From accRule In Authorization.dsAuth.aspnet_AccessRule _
                Join roleAccRule In Authorization.dsAuth.aspnet_RoleAccessRule _
                On accRule.idAccessRule Equals roleAccRule.fiAccessRule _
                Join role In Authorization.dsAuth.aspnet_Roles _
                On role.RoleId Equals roleAccRule.fiRole _
                Join userRole In Authorization.dsAuth.aspnet_UsersInRoles _
                On userRole.RoleId Equals role.RoleId _
                Where userRole.UserId = userID And accRule.RuleName.Contains(accessRule)
                Select accRule.idAccessRule
    Return query.Any
End Function

Merci d'avance.


Edit : après quelques améliorations sur les deux requêtes pour obtenir des valeurs de performance plus significatives, l'avantage du JOIN est même plusieurs fois plus important qu'avant:

Rejoindre :

Public Overloads Shared Function hasAccessDS_Join(ByVal userID As Guid, ByVal idAccessRule As Int32) As Boolean
    Dim query = From accRule In Authorization.dsAuth.aspnet_AccessRule _
                   Join roleAccRule In Authorization.dsAuth.aspnet_RoleAccessRule _
                   On accRule.idAccessRule Equals roleAccRule.fiAccessRule _
                   Join role In Authorization.dsAuth.aspnet_Roles _
                   On role.RoleId Equals roleAccRule.fiRole _
                   Join userRole In Authorization.dsAuth.aspnet_UsersInRoles _
                   On userRole.RoleId Equals role.RoleId _
                   Where accRule.idAccessRule = idAccessRule And userRole.UserId = userID
             Select role.RoleId
    Return query.Any
End Function

:

Public Overloads Shared Function hasAccessDS_Where(ByVal userID As Guid, ByVal idAccessRule As Int32) As Boolean
    Dim query = From accRule In Authorization.dsAuth.aspnet_AccessRule, _
           roleAccRule In Authorization.dsAuth.aspnet_RoleAccessRule, _
           role In Authorization.dsAuth.aspnet_Roles, _
           userRole In Authorization.dsAuth.aspnet_UsersInRoles _
           Where accRule.idAccessRule = roleAccRule.fiAccessRule _
           And roleAccRule.fiRole = role.RoleId _
           And userRole.RoleId = role.RoleId _
           And accRule.idAccessRule = idAccessRule And userRole.UserId = userID
           Select role.RoleId
    Return query.Any
End Function

Résultat pour 1000 appels (sur un ordinateur plus rapide)

  1. Rejoindre | 2. Où

1. itération:

  1. 0,0713669 sec.
  2. 12,7395299 sec.

2. itération:

  1. 0,0492458 sec.
  2. 12,3885925 sec.

3. itération:

  1. 0,0501982 sec.
  2. 13,3474216 sec.

Moyenne:

  1. Joindre: 0,0569367 sec.
  2. Où: 12,8251813 sec.

L'adhésion est 225 fois plus rapide

Conclusion: évitez WHERE de spécifier des relations et utilisez JOIN dans la mesure du possible (certainement dans LINQ to DataSet et Linq-To-Objectsen général).

Tim Schmelter
la source
Pour les autres qui lisent ceci et utilisent LinqToSQL et pensent qu'il pourrait être bon de changer tous vos WHERE en JOIN, veuillez vous assurer de lire le commentaire de THomas Levesque où il dit "il y a une telle optimisation lorsque vous utilisez Linq to SQL ou Linq to Entities, car la requête SQL générée est traitée comme une jointure par le SGBD. Mais dans ce cas, vous utilisez Linq to DataSet, il n'y a pas de traduction en SQL ". En d'autres termes, ne vous souciez pas de changer quoi que ce soit lorsque vous utilisez linqtosql pour traduire les WHERE en jointures.
JonH
@JonH: ça ne fait pas de mal d'utiliser Joinanywhy, pourquoi compter sur un optimiseur si vous pouvez écrire le code optimisé depuis le début? Cela rend également vos intentions plus claires. Donc, les mêmes raisons pour lesquelles vous devriez préférer JOIN en sql .
Tim Schmelter
Ai-je raison de supposer que ce ne serait pas le cas avec EntityFramework?
Mafii

Réponses:

76
  1. Votre première approche (requête SQL dans la base de données) est assez efficace car la base de données sait comment effectuer une jointure. Mais cela n'a pas vraiment de sens de le comparer avec les autres approches, car ils fonctionnent directement en mémoire (Linq to DataSet)

  2. La requête avec plusieurs tables et une Wherecondition effectue en fait un produit cartésien de toutes les tables, puis filtre les lignes qui satisfont à la condition. Cela signifie que la Wherecondition est évaluée pour chaque combinaison de lignes (n1 * n2 * n3 * n4)

  3. L' Joinopérateur prend les lignes des premières tables, puis prend uniquement les lignes avec une clé correspondante de la deuxième table, puis uniquement les lignes avec une clé correspondante de la troisième table, et ainsi de suite. C'est beaucoup plus efficace, car il n'est pas nécessaire d'effectuer autant d'opérations

Thomas Levesque
la source
4
Merci d'avoir clarifié le contexte. L'approche db ne faisait pas vraiment partie de cette question, mais c'était intéressant pour moi de voir si l'approche mémoire est vraiment plus rapide. J'ai supposé que .net optimiserait la where-query d'une manière ou d'une autre comme un dbms. En fait, JOINc'était même 225 fois plus rapide que la WHERE(dernière modification).
Tim Schmelter
19

Le Joinest beaucoup plus rapide, car la méthode sait combiner les tableaux pour réduire le résultat aux combinaisons pertinentes. Lorsque vous utilisez Wherepour spécifier la relation, il doit créer toutes les combinaisons possibles, puis tester la condition pour voir quelles combinaisons sont pertinentes.

La Joinméthode peut configurer une table de hachage à utiliser comme index pour compresser rapidement deux tables ensemble, tandis que la Whereméthode s'exécute une fois que toutes les combinaisons sont déjà créées, de sorte qu'elle ne peut utiliser aucune astuce pour réduire les combinaisons au préalable.

Guffa
la source
Je vous remercie. N'y a-t-il pas d'optimisations implicites du compilateur / du runtime comme dans dbms? Il ne devrait pas être impossible de voir que la relation où est en fait une jointure.
Tim Schmelter
1
Un bon SGBDR devrait en effet repérer que la condition WHERE est un test d'égalité sur deux colonnes UNIQUE et la traiter comme une JOIN.
Simon Richter
6
@Tim Schelter, il existe une telle optimisation lorsque vous utilisez Linq to SQL ou Linq to Entities, car la requête SQL générée est traitée comme une jointure par le SGBD. Mais dans ce cas, vous utilisez Linq vers DataSet, il n'y a pas de traduction en SQL
Thomas Levesque
@Tim: LINQ to DataSets utilise en fait LINQ to Objects. Par conséquent, les vraies jointures ne peuvent être capturées qu'avec le joinmot - clé, car il n'y a pas d'analyse d'exécution de la requête pour produire quelque chose d'analogue à un plan d'exécution. Vous remarquerez également que les jointures basées sur LINQ ne peuvent accueillir que des équidés à une seule colonne.
Adam Robinson
2
@Adam, ce n'est pas tout à fait vrai: vous pouvez faire des équijointures avec plusieurs clés, en utilisant des types anonymes:... on new { f1.Key1, f1.Key2 } equals new { f2.Key1, f2.Key2 }
Thomas Levesque
7

ce que vous devez vraiment savoir, c'est le SQL qui a été créé pour les deux instructions. Il existe plusieurs moyens d'y parvenir, mais le plus simple est d'utiliser LinqPad. Il y a plusieurs boutons juste au-dessus des résultats de la requête qui changeront en sql. Cela vous donnera beaucoup plus d'informations qu'autre chose.

Excellente information que vous avez partagée là-bas.

Phillip
la source
1
Merci pour l'indice LinqPad. En fait, mes deux requêtes sont linQ to Dataset dans les requêtes de mémoire, donc je suppose qu'il n'y a pas de SQL généré. Normalement, il serait optimisé par les dbms.
Tim Schmelter