Подтвердить что ты не робот

Почему это происходит? Самый эффективный способ получения нескольких объектов с помощью первичного ключа?

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

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?

}

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

4b9b3361

Ответ 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 сокращает время на одну или две секунды.

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

Ответ 2

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

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

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

Ответ 3

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

return db.PERSON.Find(id);

а не:

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

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

Ответ 4

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

ids.Select(id => Images.Find(id));     
    return Images.toArray().Where( im => ids.Contains(im.Id));  

Ответ 5

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

private List<Foo> GetFoos(IEnumerable<long> ids)
{
    var sb = new StringBuilder();
    sb.Append("DECLARE @Temp TABLE (Id bitint PRIMARY KEY)\n");

    foreach (var id in ids)
    {
        sb.Append("INSERT INTO @Temp VALUES ('");
        sb.Append(id);
        sb.Append("')\n");
    }

    sb.Append("SELECT f.* FROM [dbo].[Foo] f inner join @Temp t on f.Id = t.Id");

    return this.context.Database.SqlQuery<Foo>(sb.ToString()).ToList();
}

Это не очень красиво, но для больших списков он очень эффективен.