Entity Framework + LINQ + "Содержит" == Супер медленное?
Попытка рефакторинга некоторого кода, который недавно стал очень медленным, и я наткнулся на блок кода, который выполняет 5 + секунд для выполнения.
Код состоит из двух операторов:
IEnumerable<int> StudentIds = _entities.Filters
.Where(x => x.TeacherId == Profile.TeacherId.Value && x.StudentId != null)
.Select(x => x.StudentId)
.Distinct<int>();
и
_entities.StudentClassrooms
.Include("ClassroomTerm.Classroom.School.District")
.Include("ClassroomTerm.Teacher.Profile")
.Include("Student")
.Where(x => StudentIds.Contains(x.StudentId)
&& x.ClassroomTerm.IsActive
&& x.ClassroomTerm.Classroom.IsActive
&& x.ClassroomTerm.Classroom.School.IsActive
&& x.ClassroomTerm.Classroom.School.District.IsActive).AsQueryable<StudentClassroom>();
Итак, это немного грязно, но сначала я получаю отдельный список идентификаторов из одной таблицы (фильтры), затем я запрашиваю другую таблицу, используя ее.
Это относительно небольшие таблицы, но это еще 5 + секунд времени запроса.
Я поместил это в LINQPad, и он показал, что сначала выполняет нижний запрос, а затем запускает 1000 "разных" запросов.
По прихоти я изменил код "StudentIds", просто добавив в конец .ToArray(). Это улучшило скорость 1000x... для выполнения одного и того же запроса теперь требуется 100 мс.
Какая сделка? Что я делаю неправильно?
Ответы
Ответ 1
Это одна из ловушек отложенного выполнения в Linq: в вашем первом подходе StudentIds
действительно является IQueryable
, а не сборкой в памяти. Это означает, что использование этого во втором запросе будет выполнять запрос снова в базе данных - каждый раз.
Принудительное выполнение первого запроса с помощью ToArray()
делает StudentIds
сборку в памяти, а часть Contains
во втором запросе будет выполняться над этой коллекцией, которая содержит фиксированную последовательность элементов. Это сопоставляется с что-то эквивалентное запросу SQL where StudentId in (1,2,3,4)
.
Этот запрос, конечно, будет намного быстрее, поскольку вы определили эту последовательность сразу, а не каждый раз, когда выполняется предложение Where
. Второй запрос без использования ToArray()
(я бы подумал) был бы сопоставлен с SQL-запросом с подзапросом where exists (...)
, который оценивается для каждой строки.
Ответ 2
ToArray()
Переносит исходный запрос в память сервера.
Я предполагаю, что поставщик запросов не сможет проанализировать выражение StudentIds.Contains(x.StudentId)
. Следовательно, вероятно, он считает, что studentIds
- это массив, уже загруженный в память. Поэтому он, вероятно, снова и снова запрашивает базу данных во время фазы синтаксического анализа. Единственный способ узнать наверняка - настроить профайлер.
Если вам нужно сделать это на сервере db, используйте соединение, а не "содержит". Если вам нужно использовать contains, чтобы делать то, что похоже на проблему соединения, скорее всего, вы не увидите первичный ключ суррогата или внешний ключ.
Вы также можете объявить studentIds
как IQueryable вместо IEnumerable. Это может дать поставщику запросов подсказку, которая должна интерпретировать studentIds
как выражение aka. данные еще не загружены в память. Я как-то сомневаюсь в этом, но стоит попробовать.
Если все остальное не работает, используйте ToArray()
. Это загрузит исходный studentIds
в память.