ОБНОВЛЕНИЕ: С добавлением InExpression в EF6 производительность обработки Enumerable.Contains значительно улучшилась.Подход, описанный в этом ответе, больше не нужен.
Вы правы, что большую часть времени тратится на обработку перевода запроса.Модель провайдера EF в настоящее время не включает выражение, представляющее предложение IN, поэтому провайдеры ADO.NET не могут поддерживать IN изначально.Вместо этого реализация Enumerable.Contains переводит его в дерево выражений OR, то есть для чего-то, что в C # выглядит примерно так:
new []{1, 2, 3, 4}.Contains(i)
... мы сгенерируем дерево DbExpression, которое можно представитькак это:
((1 = @i) OR (2 = @i)) OR ((3 = @i) OR (4 = @i))
(Деревья выражений должны быть сбалансированы, потому что, если бы у нас было все OR на одном длинном позвоночнике, было бы больше шансов, что посетитель выражения попадет в переполнение стека (да, мына самом деле это удалось в нашем тестировании))
Позже мы отправим подобное дерево провайдеру ADO.NET, который может распознать этот шаблон и сократить его до предложения IN во время генерации SQL.
Когда мы добавили поддержку Enumerable.Contains в EF4, мы подумали, что было бы желательно сделать это без необходимости введения поддержки выражений IN в модели провайдера, и, честно говоря, 10 000 - это намного больше, чем количество элементов.мы ожидали, что клиенты перейдут на Enumerable.Contains.Тем не менее, я понимаю, что это раздражает, и что манипулирование деревьями выражений делает вещи слишком дорогими в вашем конкретном сценарии.
Я обсуждал это с одним из наших разработчиков, и мы считаем, что в будущем мы могли бы изменитьреализация путем добавления первоклассной поддержки для IN.Я позабочусь о том, чтобы это было добавлено в наше отставание, но я не могу обещать, когда это будет сделано, поскольку есть много других улучшений, которые мы хотели бы внести.
К обходным путям, уже предложенным в потоке, я бы добавил следующее:
Подумайте о создании метода, который уравновешивает количество обращений к базе данных и количество элементов, которые вы передаете в Contains.Например, в моем собственном тестировании я заметил, что для вычисления и выполнения на локальном экземпляре SQL Server запрос из 100 элементов занимает 1/60 секунды.Если вы можете написать свой запрос таким образом, что выполнение 100 запросов с 100 различными наборами идентификаторов даст вам эквивалентный результат для запроса с 10 000 элементов, то вы можете получить результаты примерно за 1,67 секунды вместо 18 секунд.
Различные размеры блоков должны работать лучше в зависимости от запроса и задержки соединения с базой данных.Для определенных запросов, т. Е. Если переданная последовательность имеет дубликаты или Enumerable.Contains используется во вложенном состоянии, вы можете получить дублированные элементы в результатах.
Вот фрагмент кода (извините, если код, используемый для разбиения входных данных на куски, выглядит немного слишком сложным. Есть более простые способы добиться того же, но я пытался придумать шаблон, которыйсохраняет потоковую передачу для последовательности, и я не смог найти ничего подобного в LINQ, поэтому я, вероятно, перестарался с этой частью :)):
Использование:
var list = context.GetMainItems(ids).ToList();
Метод для контекста или хранилища:
public partial class ContainsTestEntities
{
public IEnumerable<Main> GetMainItems(IEnumerable<int> ids, int chunkSize = 100)
{
foreach (var chunk in ids.Chunk(chunkSize))
{
var q = this.MainItems.Where(a => chunk.Contains(a.Id));
foreach (var item in q)
{
yield return item;
}
}
}
}
Методы расширения для нарезки перечислимых последовательностей:
public static class EnumerableSlicing
{
private class Status
{
public bool EndOfSequence;
}
private static IEnumerable<T> TakeOnEnumerator<T>(IEnumerator<T> enumerator, int count,
Status status)
{
while (--count > 0 && (enumerator.MoveNext() || !(status.EndOfSequence = true)))
{
yield return enumerator.Current;
}
}
public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> items, int chunkSize)
{
if (chunkSize < 1)
{
throw new ArgumentException("Chunks should not be smaller than 1 element");
}
var status = new Status { EndOfSequence = false };
using (var enumerator = items.GetEnumerator())
{
while (!status.EndOfSequence)
{
yield return TakeOnEnumerator(enumerator, chunkSize, status);
}
}
}
}
Надеюсь, это поможет!