Почему LINQ JOIN намного быстрее, чем связывание с WHERE?

99

Я недавно обновился до VS 2010 и играю с LINQ to Dataset. У меня есть строго типизированный набор данных для авторизации, который находится в HttpCache веб-приложения ASP.NET.

Поэтому я хотел знать, как на самом деле самый быстрый способ проверить, имеет ли пользователь право что-то делать. Вот моя модель данных и другая информация, если кому-то интересно.

Я проверил 3 способа:

  1. прямая база данных
  2. Запрос LINQ с условиями Where как «Присоединиться» - синтаксис
  3. Запрос LINQ с объединением - синтаксис

Это результаты с 1000 вызовами каждой функции:

1.Итерация:

  1. 4,2841519 сек.
  2. 115,7796925 сек.
  3. 2,024749 сек.

2.Итерация:

  1. 3,1954857 сек.
  2. 84,97047 сек.
  3. 1,5783397 сек.

3.Итерация:

  1. 2,7922143 сек.
  2. 97,8713267 сек.
  3. 1,8432163 сек.

Средний:

  1. База данных: 3,4239506333 сек.
  2. Где: 99,5404964 сек.
  3. Присоединяйтесь: 1,815435 сек.

Почему версия Join намного быстрее, чем синтаксис where, что делает ее бесполезной, хотя для новичка в LINQ она кажется наиболее разборчивой. Или я что-то упустил в своих запросах?

Вот запросы LINQ, я пропускаю базу данных:

Где :

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

Присоединиться:

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

Заранее спасибо.


Изменить : после некоторых улучшений в обоих запросах для получения более значимых значений производительности преимущество JOIN даже во много раз больше, чем раньше:

Присоединяйтесь :

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

Результат за 1000 звонков (на более быстром компьютере)

  1. Присоединяйтесь | 2. Где

1.Итерация:

  1. 0,0713669 сек.
  2. 12,7395299 сек.

2.Итерация:

  1. 0,0492458 сек.
  2. 12,3885925 сек.

3.Итерация:

  1. 0,0501982 сек.
  2. 13,3474216 сек.

Средний:

  1. Присоединиться: 0,0569367 сек.
  2. Где: 12,8251813 сек.

Присоединяйтесь в 225 раз быстрее

Заключение: избегайте WHERE для указания отношений и по возможности используйте JOIN (обязательно в LINQ to DataSet и Linq-To-Objectsв целом).

Тим Шмельтер
источник
Для тех, кто читает это и использует LinqToSQL и думает, что было бы неплохо изменить все ваши WHERE на JOIN, пожалуйста, убедитесь, что вы прочитали комментарий Томаса Левеску, где он говорит: «Такая оптимизация существует, когда вы используете Linq to SQL или Linq to Entities, потому что сгенерированный SQL-запрос обрабатывается СУБД как соединение. Но в этом случае вы используете Linq to DataSet, нет перевода в SQL ". Другими словами, не пытайтесь ничего менять, когда вы используете linqtosql в качестве перевода WHERE в соединения.
JonH
@JonH: это не помешает использовать Join, зачем полагаться на оптимизатор, если вы можете написать оптимизированный код с самого начала? Это также проясняет ваши намерения. Итак, по тем же причинам, по которым вы должны предпочесть JOIN в sql .
Тим Шмелтер
Правильно ли я предполагаю, что это не относится к EntityFramework?
Mafii

Ответы:

76
  1. Ваш первый подход (SQL-запрос в БД) довольно эффективен, потому что БД знает, как выполнить соединение. Но сравнивать его с другими подходами не имеет смысла, поскольку они работают непосредственно в памяти (Linq to DataSet).

  2. Запрос с несколькими таблицами и Whereусловием фактически выполняет декартово произведение всех таблиц, а затем фильтрует строки, удовлетворяющие условию. Это означает, что Whereусловие оценивается для каждой комбинации строк (n1 * n2 * n3 * n4)

  3. JoinОператор берет строку из первых таблиц, а затем принимает только те строки , с помощью ключа согласования из второй таблицы, то только строки с ключом согласующего из третьей таблицы, и так далее. Это намного эффективнее, потому что не нужно выполнять столько операций.

Томас Левеск
источник
4
Спасибо за разъяснение предыстории. Подход db на самом деле не был частью этого вопроса, но мне было интересно посмотреть, действительно ли подход с памятью работает быстрее. Я предполагал, что .net оптимизирует where-запрос каким-то образом, как dbms. Фактически, он JOINбыл даже в 225 раз быстрее, чем WHERE(последняя редакция).
Тим Шмелтер
19

Это Joinнамного быстрее, потому что метод знает, как объединить таблицы, чтобы уменьшить результат до соответствующих комбинаций. Когда вы используете Whereдля указания отношения, он должен создать все возможные комбинации, а затем проверить условие, чтобы увидеть, какие комбинации являются релевантными.

JoinМетод может создать хэш - таблицу для использования в качестве индекса Quicky пронестись две таблицы вместе, в то время как Whereметод работает после всех комбинаций уже созданы, поэтому он не может использовать любые уловки , чтобы заранее уменьшить комбинации.

Гуффа
источник
Спасибо. Нет ли неявных оптимизаций компилятора / среды выполнения, как в dbms? Не должно быть невозможным увидеть, что отношение where на самом деле является соединением.
Тим Шмелтер
1
Хорошая СУБД действительно должна определять, что условие WHERE является проверкой равенства двух столбцов UNIQUE, и рассматривать его как JOIN.
Саймон Рихтер
6
@Tim Schelter, такая оптимизация есть при использовании Linq to SQL или Linq to Entities, потому что сгенерированный запрос SQL обрабатывается СУБД как соединение. Но в этом случае вы используете Linq для DataSet, нет перевода на SQL
Томас Левеск
@Tim: LINQ to DataSets фактически использует LINQ to Objects. В результате настоящие объединения могут быть зафиксированы только с помощью joinключевого слова, поскольку во время выполнения запроса нет анализа для создания чего-либо, аналогичного плану выполнения. Вы также заметите, что объединения на основе LINQ могут поддерживать равные соединения только с одним столбцом.
Адам Робинсон
2
@ Адам, это не совсем так: вы можете выполнять равные соединения с несколькими ключами, используя анонимные типы:... on new { f1.Key1, f1.Key2 } equals new { f2.Key1, f2.Key2 }
Томас Левеск
7

что вам действительно нужно знать, так это sql, созданный для двух операторов. Есть несколько способов добраться до него, но самый простой - использовать LinqPad. Прямо над результатами запроса есть несколько кнопок, которые изменятся на sql. Это даст вам гораздо больше информации, чем что-либо еще.

Тем не менее, вы там поделились отличной информацией.

филипп
источник
1
Спасибо за подсказку LinqPad. На самом деле мои два запроса - это linQ to Dataset в запросах памяти, поэтому я предполагаю, что SQL не генерируется. Обычно это оптимизируется с помощью dbms.
Тим Schmelter