Warum ist LINQ JOIN so viel schneller als die Verknüpfung mit WHERE?

99

Ich habe kürzlich ein Upgrade auf VS 2010 durchgeführt und spiele mit LINQ to Dataset herum. Ich habe ein stark typisiertes Dataset für die Autorisierung, das sich in HttpCache einer ASP.NET-Webanwendung befindet.

Also wollte ich wissen, was eigentlich der schnellste Weg ist, um zu überprüfen, ob ein Benutzer berechtigt ist, etwas zu tun. Hier ist mein Datenmodell und einige andere Informationen, wenn jemand interessiert ist.

Ich habe 3 Möglichkeiten überprüft:

  1. Direkte Datenbank
  2. LINQ-Abfrage mit Where Bedingungen als "Join" - Syntax
  3. LINQ-Abfrage mit Join - Syntax

Dies sind die Ergebnisse mit 1000 Aufrufen für jede Funktion:

1.Iteration:

  1. 4,2841519 Sek.
  2. 115.7796925 Sek.
  3. 2,024749 Sek.

2.Iteration:

  1. 3,1954857 Sek.
  2. 84.97047 Sek.
  3. 1,5783397 Sek.

3.Iteration:

  1. 2,7922143 Sek.
  2. 97,8713267 sek.
  3. 1,8432163 Sek.

Durchschnittlich:

  1. Datenbank: 3,4239506333 Sek.
  2. Wo: 99.5404964 Sek.
  3. Beitritt: 1.815435 Sek.

Warum ist die Join-Version so viel schneller als die Where-Syntax, was sie unbrauchbar macht, obwohl sie als LINQ-Neuling am besten lesbar zu sein scheint. Oder habe ich etwas in meinen Fragen verpasst?

Hier sind die LINQ-Abfragen, ich überspringe die Datenbank:

Wo :

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

Beitreten:

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

Vielen Dank im Voraus.


Bearbeiten : Nach einigen Verbesserungen an beiden Abfragen, um aussagekräftigere Leistungswerte zu erhalten, ist der Vorteil von JOIN sogar um ein Vielfaches größer als zuvor:

Mitmachen :

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

Wo :

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

Ergebnis für 1000 Anrufe (auf einem schnelleren Computer)

  1. Beitreten | 2. Wo

1.Iteration:

  1. 0,0713669 Sek.
  2. 12.79595299 Sek.

2.Iteration:

  1. 0,0492458 Sek.
  2. 12.3885925 Sek.

3.Iteration:

  1. 0,0501982 Sek.
  2. 13.3474216 Sek.

Durchschnittlich:

  1. Beitritt: 0,0569367 Sek.
  2. Wo: 12.8251813 Sek.

Join ist 225-mal schneller

Schlussfolgerung: Vermeiden Sie, wo Beziehungen angegeben werden sollen, und verwenden Sie nach Möglichkeit JOIN (definitiv in LINQ to DataSet und Linq-To-Objectsallgemein).

Tim Schmelter
quelle
Für andere, die dies lesen und LinqToSQL verwenden und der Meinung sind, dass es sinnvoll sein könnte, alle Ihre WHEREs in JOINs zu ändern, lesen Sie bitte den Kommentar von THomas Levesque, in dem er sagt: "Es gibt eine solche Optimierung, wenn Sie Linq to SQL oder verwenden Linq to Entities, da die generierte SQL-Abfrage vom DBMS als Join behandelt wird. In diesem Fall wird Linq to DataSet jedoch nicht übersetzt. " Mit anderen Worten, ändern Sie nichts, wenn Sie linqtosql als WHERE-Übersetzung für Joins verwenden.
JonH
@ JonH: Es tut nicht weh, Joinirgendetwas zu verwenden . Warum sollten Sie sich auf ein Optimierungsprogramm verlassen, wenn Sie den optimierten Code von Anfang an schreiben können? Es macht auch Ihre Absichten klarer. Also die gleichen Gründe, warum Sie JOIN in SQL bevorzugen sollten .
Tim Schmelter
Kann ich zu Recht davon ausgehen, dass dies bei EntityFramework nicht der Fall ist?
Mafii

Antworten:

76
  1. Ihr erster Ansatz (SQL-Abfrage in der Datenbank) ist sehr effizient, da die Datenbank weiß, wie ein Join ausgeführt wird. Es ist jedoch nicht wirklich sinnvoll, es mit den anderen Ansätzen zu vergleichen, da sie direkt im Speicher arbeiten (Linq to DataSet).

  2. Die Abfrage mit mehreren Tabellen und einer WhereBedingung führt tatsächlich ein kartesisches Produkt aller Tabellen aus und filtert dann die Zeilen, die die Bedingung erfüllen. Dies bedeutet, dass die WhereBedingung für jede Kombination von Zeilen ausgewertet wird (n1 * n2 * n3 * n4).

  3. Der JoinOperator nimmt die Zeilen aus den ersten Tabellen, dann nur die Zeilen mit einem übereinstimmenden Schlüssel aus der zweiten Tabelle, dann nur die Zeilen mit einem übereinstimmenden Schlüssel aus der dritten Tabelle und so weiter. Dies ist viel effizienter, da nicht so viele Vorgänge ausgeführt werden müssen

Thomas Levesque
quelle
4
Vielen Dank für die Klärung des Hintergrunds. Der DB-Ansatz war nicht wirklich Teil dieser Frage, aber es war für mich interessant zu sehen, ob der Speicheransatz wirklich schneller ist. Ich ging davon aus, dass .net die whereAbfrage in gewisser Weise genauso wie eine Datenbank optimieren würde . Eigentlich JOINwar das sogar 225 mal schneller als das WHERE(letzte Bearbeitung).
Tim Schmelter
19

Das Joinist viel schneller, weil die Methode weiß , wie man die Tabellen zu kombinieren , um das Ergebnis zu den entsprechenden Kombinationen zu reduzieren. Wenn Sie Wheredie Beziehung angeben, muss jede mögliche Kombination erstellt und anschließend die Bedingung getestet werden, um festzustellen, welche Kombinationen relevant sind.

Die JoinMethode kann eine Hash-Tabelle einrichten, die als Index verwendet wird, um zwei Tabellen schnell zusammen zu komprimieren, während die WhereMethode ausgeführt wird, nachdem alle Kombinationen bereits erstellt wurden. Daher kann sie keine Tricks verwenden, um die Kombinationen im Voraus zu reduzieren.

Guffa
quelle
Danke dir. Gibt es keine impliziten Optimierungen von Compiler / Laufzeit wie in DBMS? Es sollte nicht unmöglich sein zu sehen, dass die Wo-Beziehung tatsächlich eine Verknüpfung ist.
Tim Schmelter
1
Ein gutes RDBMS sollte in der Tat erkennen, dass die WHERE-Bedingung ein Gleichheitstest für zwei EINZIGARTIGE Spalten ist, und sie als JOIN behandeln.
Simon Richter
6
@ Tim Schelter, es gibt eine solche Optimierung, wenn Sie Linq to SQL oder Linq to Entities verwenden, da die generierte SQL-Abfrage vom DBMS als Join behandelt wird. Aber in diesem Fall, wenn Sie Linq to DataSet verwenden, gibt es keine Übersetzung nach SQL
Thomas Levesque
@Tim: LINQ to DataSets verwendet tatsächlich LINQ to Objects. Infolgedessen können echte Verknüpfungen nur mit dem joinSchlüsselwort erfasst werden , da es keine Laufzeitanalyse der Abfrage gibt, um etwas Analoges zu einem Ausführungsplan zu erstellen. Sie werden auch feststellen, dass LINQ-basierte Joins nur einspaltige Equijoins aufnehmen können.
Adam Robinson
2
@ Adam, das ist nicht genau richtig: Sie können Equijoins mit mehreren Schlüsseln unter Verwendung anonymer Typen ausführen:... on new { f1.Key1, f1.Key2 } equals new { f2.Key1, f2.Key2 }
Thomas Levesque
7

Was Sie wirklich wissen müssen, ist die SQL, die für die beiden Anweisungen erstellt wurde. Es gibt einige Möglichkeiten, um dorthin zu gelangen, aber die einfachste ist die Verwendung von LinqPad. Direkt über den Abfrageergebnissen befinden sich mehrere Schaltflächen, die in SQL geändert werden. Das gibt Ihnen viel mehr Informationen als alles andere.

Tolle Informationen, die Sie dort geteilt haben.

Phillips
quelle
1
Vielen Dank für den LinqPad-Hinweis. Tatsächlich sind meine beiden Abfragen in Speicherabfragen linQ to Dataset, daher gehe ich davon aus, dass kein SQL generiert wird. Normalerweise würde es von den DBMS optimiert.
Tim Schmelter