Любопытная медленность EF против SQL

В сильно многопоточном сценарии у меня проблемы с конкретным запросом EF. Это обычно дешево и быстро:

Context.MyEntity
  .Any(se => se.SameEntity.Field == someValue        
     && se.AnotherEntity.Field == anotherValue
     && se.SimpleField == simpleValue
     // few more simple predicates with fields on the main entity
     );

Это компилируется в очень разумный SQL-запрос:

SELECT 
CASE WHEN ( EXISTS (SELECT 
    1 AS [C1]
    FROM   (SELECT [Extent1].[Field1] AS [Field1]
        FROM  [dbo].[MyEntity] AS [Extent1]
        INNER JOIN [dbo].[SameEntity] AS [Extent2] ON [Extent1].[SameEntity_Id] = [Extent2].[Id]
        WHERE (N'123' = [Extent2].[SimpleField]) AND (123 = [Extent1].[AnotherEntity_Id]) AND -- further simple predicates here -- ) AS [Filter1]
    INNER JOIN [dbo].[AnotherEntity] AS [Extent3] ON [Filter1].[AnotherEntity_Id1] = [Extent3].[Id]
    WHERE N'123' = [Extent3].[SimpleField]
)) THEN cast(1 as bit) ELSE cast(0 as bit) END AS [C1]
FROM  ( SELECT 1 AS X ) AS [SingleRowTable1]

Запрос, в общем, имеет оптимальный план запроса, использует правильные индексы и возвращает в десятках миллисекунд, что вполне приемлемо.

Однако, когда критическое число потоков (< = 40) начинает выполнение этого запроса, производительность на нем падает до десятков секунд.

В базе данных нет блокировок, никакие запросы не записывают данные в эти таблицы, и они очень хорошо воспроизводятся с базой данных, которая практически изолирована от любых других операций. БД находится на одной физической машине и машина не перегружена в любой точке, то есть имеет множество запасных CPU, памяти и других ресурсов , когда процессор перегружен этой операцией.

Теперь, что действительно странно, когда я заменяю вызов EF Any() на Context.Database.ExecuteSqlCommand() с помощью скопированного SQL (также используя параметры), проблема магически исчезает. Опять же, это очень надежно воспроизводится - , заменяя вызов Any() с помощью вставного кода SQL, увеличивает производительность на 2-3 порядка.


Сэмплирование прикрепленного профайлера (dotTrace) показывает, что потоки, похоже, все проводят свое время по следующему методу:

dotTrace sample

Есть ли что-то, что я пропустил, или мы попали в какой-либо уголок ADO.NET/SQL Server?


БОЛЬШЕ КОНТЕКСТА

Код, выполняющий этот запрос, - это задача Hangfire. Для целей тестирования script ставит в очередь множество заданий, которые должны выполняться, и до 40 потоков продолжают обрабатывать задание. Каждое задание использует отдельный экземпляр DbContext, и он не очень много используется. Есть еще несколько запросов до и после проблемного запроса, и они принимают ожидаемое время для выполнения.

Мы используем много разных заданий Hangfire для подобных целей, и они ведут себя так, как ожидалось. То же самое с этим, за исключением случаев, когда он становится медленным при высоких concurrency (из тех же самых заданий). Кроме того, просто переключение на SQL по этому конкретному запросу устраняет проблему.

Снимок профилирования, приведенный выше, является репрезентативным, все потоки замедляют этот вызов метода и проводят большую часть времени на нем.


UPDATE

В настоящее время я повторно запускаю много проверок на предмет здравого смысла и ошибок. Легкое воспроизведение означает, что он все еще находится на удаленной машине, к которой я не могу подключиться, используя VS для отладки.

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

Повторно перепроверить все и вернуться сюда с обновлениями.

Ответ 1

Неисправные исходные предположения. SQL в вопросе был получен путем вставки кода в LINQPad и создания его SQL.

После присоединения профилировщика SQL к используемому фактическому БД он показал немного отличающийся SQL с внешними объединениями, которые являются субоптимальными и не имеют надлежащего индекса на месте.

Остается загадкой, почему LINQPad генерирует другой SQL, хотя он использует тот же EntityFramework.dll, но исходная проблема решена, и все, что остается, - это оптимизировать запрос.

Большое спасибо всем, кто участвует.

Ответ 2

Можете ли вы попробовать, как показано ниже, и посмотреть, есть ли улучшение производительности или нет...

Context.MyEntity.AsNoTracking()
  .Any(se => se.SameEntity.Field == someValue        
     && se.AnotherEntity.Field == anotherValue
     && se.SimpleField == simpleValue
    );

Ответ 3

Проверьте, используете ли вы контекст в цикле. Это может создать много объектов во время теста производительности и дать сборщику мусора много работы.