Почему. Содержит медленно?Самый эффективный способ получить несколько объектов по первичному ключу? - PullRequest
56 голосов
/ 13 ноября 2011

Какой самый эффективный способ выбора нескольких объектов по первичному ключу?

public IEnumerable<Models.Image> GetImagesById(IEnumerable<int> ids)
{

    //return ids.Select(id => Images.Find(id));       //is this cool?
    return Images.Where( im => ids.Contains(im.Id));  //is this better, worse or the same?
    //is there a (better) third way?

}

Я понимаю, что могу провести некоторые тесты производительности для сравнения, но мне интересно, есть лина самом деле лучше, чем оба, и я ищу некоторое понимание того, в чем разница между этими двумя запросами, если таковые имеются, после того, как они были «переведены».

Ответы [ 5 ]

129 голосов
/ 13 ноября 2011

ОБНОВЛЕНИЕ: С добавлением 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 сокращает время на одну или две секунды.

Тесты проводились с клиентом базы данных (консольное приложение) и сервером базы данных на одном компьютере.Последний результат может значительно ухудшиться с «удаленной» базой данных из-за большого количества обращений.

4 голосов
/ 13 ноября 2011

Второй вариант определенно лучше первого.Первая опция приведет к ids.Length запросам к базе данных, тогда как вторая опция может использовать оператор 'IN' в запросе SQL.Это в основном превратит ваш запрос LINQ во что-то вроде следующего SQL:

SELECT *
FROM ImagesTable
WHERE id IN (value1,value2,...)

, где value1, value2 и т. Д. Являются значениями вашей переменной id.Имейте в виду, однако, что я думаю, что может быть верхний предел количества значений, которые могут быть сериализованы в запрос таким образом.Я посмотрю, смогу ли я найти документацию ...

0 голосов
/ 02 июня 2018

Преобразование списка в массив с помощью toArray () повышает производительность. Вы можете сделать это так:

ids.Select(id => Images.Find(id));     
    return Images.toArray().Where( im => ids.Contains(im.Id));  
0 голосов
/ 21 апреля 2017

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

0 голосов
/ 06 февраля 2015

Я использую Entity Framework 6.1 и узнал, используя код , что лучше использовать:

return db.PERSON.Find(id);

вместо:

return db.PERSONA.FirstOrDefault(x => x.ID == id);

Производительность Find () по сравнению с FirstOrDefault некоторые мысли по этому поводу.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...