Ответ 1
ОБНОВЛЕНИЕ: с добавлением InExpression в EF6 производительность обработки Enumerable.Contains значительно улучшилась. Анализ в этом ответе велик, но в значительной степени устарел с 2013 года.
Использование Contains
в Entity Framework на самом деле очень медленное. Это правда, что он переводится в предложение IN
в SQL и что сам SQL-запрос выполняется быстро. Но проблема и узкое место в производительности связаны с переводом вашего запроса LINQ в SQL. Дерево выражений, которое будет создано, разворачивается в длинную цепочку конкатенаций OR
потому что не существует нативного выражения, которое представляет IN
. Когда SQL создается, это выражение многих OR
распознается и сворачивается обратно в предложение SQL IN
.
Это не означает, что использование Contains
хуже, чем выдача одного запроса на элемент в вашей коллекции ids
(ваш первый вариант). Вероятно, это еще лучше - по крайней мере, для не слишком больших коллекций. Но для больших коллекций это действительно плохо. Я помню, что некоторое время назад я проверил запрос " Contains
с примерно 12 000 элементов, которые работали, но занимали около минуты, даже если запрос в SQL выполнялся менее чем за секунду.
Возможно, стоит проверить производительность комбинации нескольких обращений к базе данных с меньшим количеством элементов в выражении Contains
для каждого обратного перехода.
Этот подход, а также ограничения использования Contains
with Entity Framework показаны и объяснены здесь:
Почему оператор Contains() настолько резко ухудшает производительность Entity Framework?
Возможно, что dbContext.Database.SqlQuery<Image>(sqlString)
команда SQL будет работать лучше всего в этой ситуации, что означает, что вы вызываете dbContext.Database.SqlQuery<Image>(sqlString)
или dbContext.Images.SqlQuery(sqlString)
где sqlString
- это SQL, показанный в ответе @Rune.
редактировать
Вот несколько измерений:
Я сделал это на столе с 550000 записей и 11 столбцов (идентификаторы начинаются с 1 без пробелов) и выбраны случайным образом 20000 идентификаторов:
using (var context = new MyDbContext())
{
Random rand = new Random();
var ids = new List<int>();
for (int i = 0; i < 20000; i++)
ids.Add(rand.Next(550000));
Stopwatch watch = new Stopwatch();
watch.Start();
// here are the code snippets from below
watch.Stop();
var msec = watch.ElapsedMilliseconds;
}
Тест 1
var result = context.Set<MyEntity>()
.Where(e => ids.Contains(e.ID))
.ToList();
Результат → msec = 85,5 с
Тест 2
var result = context.Set<MyEntity>().AsNoTracking()
.Where(e => ids.Contains(e.ID))
.ToList();
Результат → msec = 84,5 с
Этот крошечный эффект AsNoTracking
очень необычен. Это указывает на то, что узкое место не является материализацией объекта (а не SQL, как показано ниже).
Для обоих тестов в SQL Profiler можно увидеть, что SQL-запрос поступает в базу данных очень поздно. (Я точно не измерял, но это было позже 70 секунд.) Очевидно, что перевод этого запроса LINQ в SQL очень дорог.
Тест 3
var values = new StringBuilder();
values.AppendFormat("{0}", ids[0]);
for (int i = 1; i < ids.Count; i++)
values.AppendFormat(", {0}", ids[i]);
var sql = string.Format(
"SELECT * FROM [MyDb].[dbo].[MyEntities] WHERE [ID] IN ({0})",
values);
var result = context.Set<MyEntity>().SqlQuery(sql).ToList();
Результат → msec = 5,1 с
Тест 4
// same as Test 3 but this time including AsNoTracking
var result = context.Set<MyEntity>().SqlQuery(sql).AsNoTracking().ToList();
Результат → msec = 3,8 с
На этот раз эффект отключения отслеживания более заметен.
Тест 5
// same as Test 3 but this time using Database.SqlQuery
var result = context.Database.SqlQuery<MyEntity>(sql).ToList();
Результат → msec = 3,7 с
Я понимаю, что context.Database.SqlQuery<MyEntity>(sql)
совпадает с context.Set<MyEntity>().SqlQuery(sql).AsNoTracking()
, поэтому между тестом 4 и тестом 5 нет никакой разницы.
(Длина наборов результатов не всегда была одинаковой из-за возможных дубликатов после выбора случайного идентификатора, но всегда была между 19600 и 19640 элементами.)
Изменить 2
Тест 6
Даже 20000 обращений к базе данных быстрее, чем использование Contains
:
var result = new List<MyEntity>();
foreach (var id in ids)
result.Add(context.Set<MyEntity>().SingleOrDefault(e => e.ID == id));
Результат → msec = 73,6 с
Обратите внимание, что я использовал SingleOrDefault
вместо Find
. Используя тот же код с Find
очень медленно (я отменил тест через несколько минут), потому что Find
вызывает DetectChanges
внутри. Отключение автоматического обнаружения изменений (context.Configuration.AutoDetectChangesEnabled = false
) приводит к примерно той же производительности, что и SingleOrDefault
. Использование AsNoTracking
сокращает время на одну или две секунды.
Тесты выполнялись с клиентом базы данных (консольное приложение) и сервером базы данных на одном компьютере. Последний результат может значительно ухудшиться благодаря "удаленной" базе данных из-за большого количества обращений.