¿Por qué LINQ JOIN es mucho más rápido que vincular con WHERE?

99

Recientemente me actualicé a VS 2010 y estoy jugando con LINQ to Dataset. Tengo un conjunto de datos de tipo fuerte para la autorización que está en HttpCache de una aplicación web ASP.NET.

Así que quería saber cuál es en realidad la forma más rápida de comprobar si un usuario está autorizado para hacer algo. Aquí está mi modelo de datos y alguna otra información si alguien está interesado.

He comprobado 3 formas:

  1. base de datos directa
  2. Consulta LINQ con condiciones Where como "Unir" - Sintaxis
  3. Consulta LINQ con Join - Sintaxis

Estos son los resultados con 1000 llamadas en cada función:

1.Iteración:

  1. 4.2841519 seg.
  2. 115,7796925 seg.
  3. 2.024749 seg.

2.Iteración:

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

3.Iteración:

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

Promedio:

  1. Base de datos: 3,4239506333 seg.
  2. Donde: 99,5404964 seg.
  3. Unirse: 1.815435 seg.

¿Por qué la versión Join es mucho más rápida que la sintaxis where, lo que la hace inútil, aunque como novato en LINQ parece ser la más legible? ¿O me he perdido algo en mis consultas?

Aquí están las consultas LINQ, me salto la base de datos:

Donde :

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

Unirse:

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

Gracias de antemano.


Editar : después de algunas mejoras en ambas consultas para obtener valores de rendimiento más significativos, la ventaja de JOIN es incluso muchas veces mayor que antes:

Únete :

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

Donde :

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

Resultado de 1000 llamadas (en una computadora más rápida)

  1. Únete | 2. Donde

1.Iteración:

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

2.Iteración:

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

3.Iteración:

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

Promedio:

  1. Unirse: 0,0569367 seg.
  2. Donde: 12,8251813 seg.

Unirse es 225 veces más rápido

Conclusión: evite WHERE para especificar relaciones y use JOIN siempre que sea posible (definitivamente en LINQ to DataSet y Linq-To-Objectsen general).

Tim Schmelter
fuente
Para otros que lean esto y estén usando LinqToSQL y piensen que podría ser bueno cambiar todos sus WHEREs a JOINs, asegúrese de leer el comentario de THomas Levesque donde dice "existe tal optimización cuando usa Linq to SQL o Linq a Entities, porque la consulta SQL generada es tratada como una unión por el DBMS. Pero en ese caso, estás usando Linq to DataSet, no hay traducción a SQL ". En otras palabras, no se moleste en cambiar nada cuando esté usando linqtosql como el DONDE se traduce a uniones.
JonH
@JonH: no está de más usar Joincualquier motivo, ¿por qué confiar en un optimizador si puedes escribir el código optimizado desde el principio? También aclara tus intenciones. Entonces, las mismas razones por las que debería preferir JOIN en sql .
Tim Schmelter
¿Estoy en lo correcto al asumir que este no sería el caso con EntityFramework?
Mafii

Respuestas:

76
  1. Su primer enfoque (consulta SQL en la base de datos) es bastante eficiente porque la base de datos sabe cómo realizar una combinación. Pero realmente no tiene sentido compararlo con los otros enfoques, ya que funcionan directamente en la memoria (Linq a DataSet)

  2. La consulta con varias tablas y una Wherecondición en realidad realiza un producto cartesiano de todas las tablas y luego filtra las filas que satisfacen la condición. Esto significa que la Wherecondición se evalúa para cada combinación de filas (n1 * n2 * n3 * n4)

  3. El Joinoperador toma las filas de las primeras tablas, luego toma solo las filas con una clave coincidente de la segunda tabla, luego solo las filas con una clave coincidente de la tercera tabla, y así sucesivamente. Esto es mucho más eficiente, porque no necesita realizar tantas operaciones

Thomas Levesque
fuente
4
Gracias por aclarar los antecedentes. El enfoque de db no era realmente parte de esta pregunta, pero fue interesante para mí ver si el enfoque de memoria es realmente más rápido. Supuse que .net optimizaría la consulta wherede alguna manera como un dbms. En realidad, JOINfue incluso 225 veces más rápido que la WHERE(última edición).
Tim Schmelter
19

El Joines mucho más rápido, ya que el método sabe cómo combinar las tablas para reducir el resultado de las combinaciones pertinentes. Cuando usa Wherepara especificar la relación, tiene que crear todas las combinaciones posibles y luego probar la condición para ver qué combinaciones son relevantes.

El Joinmétodo puede configurar una tabla hash para usar como índice para unir rápidamente dos tablas, mientras que el Wheremétodo se ejecuta después de que todas las combinaciones ya están creadas, por lo que no puede usar ningún truco para reducir las combinaciones de antemano.

Guffa
fuente
Gracias. ¿No hay optimizaciones implícitas del compilador / tiempo de ejecución como en dbms? No debería ser imposible ver que la relación donde en realidad es una combinación.
Tim Schmelter
1
Un buen RDBMS debería detectar que la condición WHERE es una prueba de igualdad en dos columnas UNIQUE y tratarla como JOIN.
Simon Richter
6
@Tim Schelter, existe una optimización de este tipo cuando usa Linq para SQL o Linq para Entidades, porque la consulta SQL generada es tratada como una combinación por el DBMS. Pero en ese caso, está usando Linq a DataSet, no hay traducción a SQL
Thomas Levesque
@Tim: LINQ to DataSets en realidad usa LINQ to Objects. Como resultado, las combinaciones verdaderas solo se pueden capturar con la joinpalabra clave, ya que no hay un análisis en tiempo de ejecución de la consulta para producir algo análogo a un plan de ejecución. También notará que las uniones basadas en LINQ solo pueden acomodar equijoins de una sola columna.
Adam Robinson
2
@ Adam, eso no es exactamente cierto: puede hacer equijoins con múltiples claves, usando tipos anónimos:... on new { f1.Key1, f1.Key2 } equals new { f2.Key1, f2.Key2 }
Thomas Levesque
7

lo que realmente necesita saber es el sql que se creó para las dos declaraciones. Hay varias formas de acceder a él, pero la más sencilla es utilizar LinqPad. Hay varios botones justo encima de los resultados de la consulta que cambiarán a sql. Eso le dará mucha más información que cualquier otra cosa.

Sin embargo, compartiste una gran información allí.

phillip
fuente
1
Gracias por la sugerencia de LinqPad. En realidad, mis dos consultas son linQ to Dataset en consultas de memoria, por lo tanto, supongo que no se genera SQL. Normalmente sería optimizado por dbms.
Tim Schmelter