ОБНОВЛЕНИЕ: С добавлением InExpression в EF6 производительность обработки Enumerable.Contains значительно улучшилась. Анализ в этом ответе велик, но в значительной степени устарел с 2013 года.
Использование Contains
в Entity Framework на самом деле очень медленно. Это правда, что он переводится в SQL-предложение IN
и сам запрос SQL выполняется быстро. Но проблема и узкое место в производительности заключается в переводе вашего запроса LINQ на SQL. Дерево выражений, которое будет создано, разворачивается в длинную цепочку OR
конкатенаций, потому что нет собственного выражения, представляющего IN
. Когда SQL создается, это выражение многих OR
s распознается и сворачивается обратно в предложение SQL IN
.
Это не означает, что использование Contains
хуже, чем отправка одного запроса на элемент в вашей коллекции ids
(ваш первый вариант). Это, вероятно, все еще лучше - по крайней мере для не слишком больших коллекций. Но для больших коллекций это действительно плохо. Я помню, что некоторое время назад я тестировал Contains
запрос с около 12.000 элементов, который работал, но занимал около минуты, хотя запрос в SQL выполнялся менее чем за секунду.
Возможно, стоит протестировать производительность комбинации нескольких обращений к базе данных с меньшим числом элементов в выражении Contains
для каждого обращения.
Этот подход, а также ограничения использования Contains
с Entity Framework показаны и объяснены здесь:
Почему оператор Contains () так резко снижает производительность Entity Framework?
Вполне возможно, что в этой ситуации лучше всего подойдет сырая команда 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();
Результат -> мс = 85,5 с
Тест 2
var result = context.Set<MyEntity>().AsNoTracking()
.Where(e => ids.Contains(e.ID))
.ToList();
Результат -> мс = 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();
Результат -> мс = 5,1 с
Тест 4
// same as Test 3 but this time including AsNoTracking
var result = context.Set<MyEntity>().SqlQuery(sql).AsNoTracking().ToList();
Результат -> мс = 3,8 с
На этот раз эффект отключения отслеживания более заметен.
Тест 5
// same as Test 3 but this time using Database.SqlQuery
var result = context.Database.SqlQuery<MyEntity>(sql).ToList();
Результат -> мс = 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));
Результат -> мс = 73,6 с
Обратите внимание, что я использовал SingleOrDefault
вместо Find
. Использование того же кода с Find
очень медленное (я отменил тест через несколько минут), потому что Find
вызывает DetectChanges
внутри. Отключение автоматического обнаружения изменений (context.Configuration.AutoDetectChangesEnabled = false
) приводит к примерно той же производительности, что и SingleOrDefault
. Использование AsNoTracking
сокращает время на одну или две секунды.
Тесты проводились с клиентом базы данных (консольное приложение) и сервером базы данных на одном компьютере.Последний результат может значительно ухудшиться с «удаленной» базой данных из-за большого количества обращений.