Por que LINQ JOIN é muito mais rápido do que vincular com WHERE?

99

Recentemente, atualizei para o VS 2010 e estou brincando com LINQ to Dataset. Eu tenho um forte conjunto de dados tipado para autorização que está em HttpCache de um ASP.NET WebApplication.

Queria saber qual é a maneira mais rápida de verificar se um usuário está autorizado a fazer algo. Aqui está meu modelo de dados e algumas outras informações caso alguém esteja interessado.

Eu verifiquei 3 maneiras:

  1. banco de dados direto
  2. Consulta LINQ com condições Where como "Join" - Sintaxe
  3. Consulta LINQ com Join - Sintaxe

Estes são os resultados com 1000 chamadas em cada função:

1.Iteração:

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

2.Iteração:

  1. 3,1954857 seg.
  2. 84.97047 seg.
  3. 1.5783397 seg.

3.Iteração:

  1. 2,7922143 seg.
  2. 97.8713267 seg.
  3. 1,8432163 seg.

Média:

  1. Banco de dados: 3,4239506333 seg.
  2. Onde: 99.5404964 seg.
  3. Unir: 1.815435 seg.

Por que a versão Join é muito mais rápida do que a sintaxe where, o que a torna inútil, embora, para um novato do LINQ, ela pareça ser a mais legível. Ou eu perdi algo em minhas consultas?

Aqui estão as consultas LINQ, eu pulo o banco de dados:

Onde :

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

Junte-se:

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

Agradeço antecipadamente.


Editar : depois de algumas melhorias em ambas as consultas para obter valores de desempenho mais significativos, a vantagem do JOIN é ainda muitas vezes maior do que antes:

Junte - se :

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

Onde :

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 para 1000 chamadas (em um computador mais rápido)

  1. Junte-se | 2. Onde

1.Iteração:

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

2.Iteração:

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

3.Iteração:

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

Média:

  1. União: 0,0569367 seg.
  2. Onde: 12,8251813 seg.

Participe 225 vezes mais rápido

Conclusão: evite WHERE para especificar relações e use JOIN sempre que possível (definitivamente em LINQ to DataSet e Linq-To-Objectsem geral).

Tim Schmelter
fonte
Para outras pessoas que lêem isso e estão usando LinqToSQL e pensam que pode ser bom mudar todos os seus WHEREs para JOINs, por favor, certifique-se de ler o comentário de THomas Levesque onde ele diz "existe uma otimização quando você usa Linq para SQL ou Linq to Entities, porque a consulta SQL gerada é tratada como uma junção pelo DBMS. Mas, nesse caso, você está usando Linq to DataSet, não há conversão para SQL ". Em outras palavras, não se preocupe em mudar nada quando estiver usando linqtosql, pois o WHERE é traduzido para joins.
JonH
@JonH: não Joincusta nada usar , por que confiar em um otimizador se você pode escrever o código otimizado desde o início? Também torna suas intenções mais claras. Portanto, as mesmas razões pelas quais você deve preferir JOIN no sql .
Tim Schmelter
Estou correto em supor que esse não seria o caso com EntityFramework?
Mafii

Respostas:

76
  1. Sua primeira abordagem (consulta SQL no banco de dados) é bastante eficiente porque o banco de dados sabe como realizar uma junção. Mas realmente não faz sentido compará-lo com as outras abordagens, uma vez que funcionam diretamente na memória (Linq para DataSet)

  2. A consulta com várias tabelas e uma Wherecondição realmente executa um produto cartesiano de todas as tabelas e, em seguida , filtra as linhas que satisfazem a condição. Isso significa que a Wherecondição é avaliada para cada combinação de linhas (n1 * n2 * n3 * n4)

  3. O Joinoperador obtém as linhas das primeiras tabelas, em seguida, obtém apenas as linhas com uma chave correspondente da segunda tabela e, a seguir, apenas as linhas com uma chave correspondente da terceira tabela e assim por diante. Isso é muito mais eficiente, porque não precisa realizar tantas operações

Thomas Levesque
fonte
4
Obrigado por esclarecer o contexto. A abordagem db não fazia parte dessa questão, mas foi interessante para mim ver se a abordagem da memória é realmente mais rápida. Presumi que .net otimizaria a where-query de alguma forma, assim como um dbms. Na verdade, o JOINfoi até 225 vezes mais rápido que o WHERE(última edição).
Tim Schmelter,
19

o Join é muito mais rápido, pois o método sabe combinar as tabelas para reduzir o resultado às combinações relevantes. Quando você usa Wherepara especificar a relação, é necessário criar todas as combinações possíveis e, em seguida, testar a condição para ver quais combinações são relevantes.

O Joinmétodo pode configurar uma tabela hash para usar como um índice para compactar rapidamente duas tabelas, enquanto o Wheremétodo é executado depois que todas as combinações já foram criadas, então ele não pode usar nenhum truque para reduzir as combinações de antemão.

Guffa
fonte
Obrigado. Não há otimizações implícitas do compilador / tempo de execução como no dbms? Não deveria ser impossível ver que a relação onde realmente é uma junção.
Tim Schmelter,
1
Um bom RDBMS deve realmente detectar que a condição WHERE é um teste de igualdade em duas colunas UNIQUE e tratá-la como um JOIN.
Simon Richter,
6
@Tim Schelter, existe essa otimização quando você usa Linq para SQL ou Linq para Entidades, porque a consulta SQL gerada é tratada como uma junção pelo DBMS. Mas, nesse caso, você está usando Linq para DataSet, não há tradução para SQL
Thomas Levesque,
@Tim: LINQ to DataSets realmente usa LINQ to Objects. Como resultado, as junções verdadeiras só podem ser capturadas com a joinpalavra - chave, pois não há análise de tempo de execução da consulta para produzir algo análogo a um plano de execução. Você também notará que as junções baseadas em LINQ só podem acomodar equijoins de coluna única.
Adam Robinson,
2
@Adam, isso não é exatamente verdade: você pode fazer equijoins com várias chaves, usando tipos anônimos:... on new { f1.Key1, f1.Key2 } equals new { f2.Key1, f2.Key2 }
Thomas Levesque,
7

o que você realmente precisa saber é o sql que foi criado para as duas instruções. Existem algumas maneiras de fazer isso, mas a mais simples é usar o LinqPad. Existem vários botões logo acima dos resultados da consulta que serão alterados para sql. Isso lhe dará muito mais informações do que qualquer outra coisa.

Ótima informação que você compartilhou lá.

phillip
fonte
1
Obrigado pela dica do LinqPad. Na verdade, minhas duas consultas são linQ to Dataset em consultas de memória, portanto, presumo que nenhum SQL gerado. Normalmente seria otimizado pelo dbms.
Tim Schmelter,