Возвращая IEnumerable <T> против IQueryable <T>

1086

В чем разница между возвращением и IQueryable<T>временем IEnumerable<T>, когда один должен быть предпочтительнее другого?

IQueryable<Customer> custs = from c in db.Customers
where c.City == "<City>"
select c;

IEnumerable<Customer> custs = from c in db.Customers
where c.City == "<City>"
select c;
stackoverflowuser
источник

Ответы:

1779

Да, оба дадут вам отсроченное исполнение .

Разница в том, что IQueryable<T>это интерфейс, который позволяет LINQ-to-SQL (LINQ.-to-everything) работать. Поэтому, если вы дополнительно уточните свой запрос в IQueryable<T>, этот запрос будет выполнен в базе данных, если это возможно.

В данном IEnumerable<T>случае это будет LINQ-to-object, то есть все объекты, соответствующие исходному запросу, должны быть загружены в память из базы данных.

В коде:

IQueryable<Customer> custs = ...;
// Later on...
var goldCustomers = custs.Where(c => c.IsGold);

Этот код будет выполнять SQL для выбора только золотых клиентов. Следующий код, с другой стороны, выполнит исходный запрос в базе данных, а затем отфильтрует не-золотых клиентов в памяти:

IEnumerable<Customer> custs = ...;
// Later on...
var goldCustomers = custs.Where(c => c.IsGold);

Это довольно важное различие, и работа над ним IQueryable<T>во многих случаях может спасти вас от возвращения слишком большого количества строк из базы данных. Еще один яркий пример делают подкачки: Если вы используете Takeи Skipна IQueryable, вы получите только количество строк просили; это IEnumerable<T>приведет к тому, что все ваши строки будут загружены в память.

driis
источник
32
Отличное объяснение. Существуют ли ситуации, когда IEnumerable предпочтительнее IQueryable?
FJXX
8
Таким образом, мы можем сказать, что если мы используем IQueryable для запроса объекта памяти, то они не будут иметь никакого различия между IEnumerable и IQueryable?
Тарик
11
ПРЕДУПРЕЖДЕНИЕ. Хотя IQueryable может быть заманчивым решением из-за указанной оптимизации, его нельзя допускать за пределы уровня хранилища или службы. Это необходимо для защиты вашей базы данных от накладных расходов, вызванных «укладкой выражений LINQ».
Йорро
48
@fjxx Да. Если вы хотите повторную фильтрацию по вашему исходному результату (несколько конечных результатов). Выполнение этого на интерфейсе IQueryable сделает несколько обращений к базе данных, в то время как выполнение этого на IEnumerable выполнит фильтрацию в памяти, делая ее быстрее (если объем данных не ОГРОМНЫЙ)
Per Hornshøj-Schierbeck
34
Еще одна причина предпочесть IEnumerableв IQueryableтом , что не все операции LINQ поддерживаются всеми поставщиками LINQ. Поэтому, пока вы знаете, что делаете, вы можете использовать IQueryableдля передачи как можно большей части запроса поставщику LINQ (LINQ2SQL, EF, NHibernate, MongoDB и т. Д.). Но если вы позволите другому коду делать то, что он хочет с вашим, IQueryableвы в конечном итоге окажетесь в беде, потому что некоторый клиентский код где-то использовал неподдерживаемую операцию. Я согласен с рекомендацией не выпускать IQueryable«в дикую природу» за пределы хранилища или эквивалентного слоя.
Авиш
303

Верхний ответ хорош, но он не упоминает деревья выражений, которые объясняют, «как» эти два интерфейса различаются. По сути, есть два идентичных набора расширений LINQ. Where(), Sum(), Count(), FirstOrDefault()И т.д. все имеют две версии: одна , которая принимает функции и один , который принимает выражения.

  • IEnumerableВерсия подписи:Where(Func<Customer, bool> predicate)

  • IQueryableВерсия подписи:Where(Expression<Func<Customer, bool>> predicate)

Вы, вероятно, использовали оба из них, не осознавая этого, потому что оба вызываются с использованием идентичного синтаксиса:

например , Where(x => x.City == "<City>")работает на обоих IEnumerableиIQueryable

  • При использовании Where()в IEnumerableколлекции компилятор передает скомпилированную функциюWhere()

  • При использовании Where()в IQueryableколлекции компилятор передает дерево выражений в Where(). Дерево выражений похоже на систему отражения, но для кода. Компилятор преобразует ваш код в структуру данных, которая описывает, что ваш код делает в формате, который легко усваивается.

Зачем с этим выражением дерева вещь? Я просто хочу Where()отфильтровать свои данные. Основная причина в том, что ORM EF и Linq2SQL могут преобразовывать деревья выражений непосредственно в SQL, где ваш код будет выполняться намного быстрее.

О, это похоже на бесплатное повышение производительности, я должен использовать AsQueryable()повсеместно в этом случае? Нет, IQueryableполезно, только если базовый поставщик данных может что-то с этим сделать. Преобразование чего-то вроде регулярного Listв IQueryableне даст вам никакой выгоды.

Иаков
источник
9
ИМО это лучше чем принятый ответ. Тем не менее, я не понимаю одно: IQueryable не дает никакой пользы для обычных объектов, хорошо, но разве это хуже? Потому что, если это просто не дает никаких преимуществ, недостаточно просто выбрать IEnumerable, поэтому идея использования IQueryable повсюду остается в силе.
Сергей Таченов
1
Сергей, IQueryable расширяет IEnumerable, поэтому при использовании IQueryable вы загружаете в память больше, чем это делает экземпляр IEnumerable! Итак, вот аргумент. ( stackoverflow.com/questions/12064828/… c ++, хотя я думал, что смогу экстраполировать это)
Viking
Согласиться с Сергеем о том, что это лучший ответ (хотя принятый ответ - хорошо). Я хотел бы добавить , что в моем опыте, IQueryableне анализирует функции, а также IEnumerableделает: например, если вы хотите знать , какие элементы DBSet<BookEntity>не в List<BookObject>, dbSetObject.Where(e => !listObject.Any(o => o.bookEntitySource == e))бросает исключение: Expression of type 'BookEntity' cannot be used for parameter of type 'BookObject' of method 'Boolean Contains[BookObject] (IEnumerable[BookObject], BookObject)'. Я должен был добавить .ToList()после dbSetObject.
Жан-Давид Ланц
80

Да, оба используют отложенное выполнение. Давайте проиллюстрируем разницу, используя профилировщик SQL Server ....

Когда мы запускаем следующий код:

MarketDevEntities db = new MarketDevEntities();

IEnumerable<WebLog> first = db.WebLogs;
var second = first.Where(c => c.DurationSeconds > 10);
var third = second.Where(c => c.WebLogID > 100);
var result = third.Where(c => c.EmailAddress.Length > 11);

Console.Write(result.First().UserName);

В профилировщике SQL Server мы находим команду, равную:

"SELECT * FROM [dbo].[WebLog]"

Примерно 90 секунд требуется для запуска этого блока кода в таблице WebLog, содержащей 1 миллион записей.

Итак, все записи таблицы загружаются в память как объекты, а затем с каждым .Where () это будет еще один фильтр в памяти против этих объектов.

Когда мы используем IQueryableвместо IEnumerableприведенного выше примера (вторая строка):

В профилировщике SQL Server мы находим команду, равную:

"SELECT TOP 1 * FROM [dbo].[WebLog] WHERE [DurationSeconds] > 10 AND [WebLogID] > 100 AND LEN([EmailAddress]) > 11"

Примерно четыре секунды, чтобы запустить этот блок кода с помощью IQueryable.

У IQueryable есть свойство, Expressionкоторое хранит выражение дерева, которое начинает создаваться, когда мы использовали его resultв нашем примере (которое называется отложенным выполнением), и в конце это выражение будет преобразовано в запрос SQL для запуска на ядре базы данных.

Каспер Рома
источник
5
Это учит меня при приведении к IEnumerable, базовый IQueryable теряет свой метод расширения IQueryable.
Yiping
56

Оба дадут вам отсроченное исполнение, да.

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

Возврат IEnumerableавтоматически заставит среду выполнения использовать LINQ to Objects для запроса вашей коллекции.

Возврат IQueryable(который реализует IEnumerable, между прочим) предоставляет дополнительную функциональность для преобразования вашего запроса во что-то, что могло бы работать лучше на базовом источнике (LINQ to SQL, LINQ to XML и т. Д.).

Джастин Нисснер
источник
30

В общих чертах я бы порекомендовал следующее:

  • Вернитесь, IQueryable<T>если вы хотите, чтобы разработчик использовал ваш метод для уточнения запроса, который вы возвращаете перед выполнением.

  • Вернитесь, IEnumerableесли вы хотите перенести набор объектов для перечисления.

Вообразите IQueryableкак то, что это - «запрос» к данным (который вы можете уточнить, если хотите). An IEnumerable- это набор объектов (которые уже были получены или были созданы), которые вы можете перечислить.

sebastianmehler
источник
2
"Может перечислять", а не "может IEnumerable".
Кейси
29

Многое было сказано ранее, но вернемся к корням более техническим способом:

  1. IEnumerable это коллекция объектов в памяти, которую вы можете перечислить - последовательность в памяти, которая позволяет выполнять итерации (упрощает foreachцикл в цикле, хотя вы можете работать IEnumeratorтолько с ним). Они хранятся в памяти как есть.
  2. IQueryable это дерево выражений , которое в какой-то момент преобразуется во что-то другое с возможностью перечисления окончательного результата . Я думаю, это то, что смущает большинство людей.

Они, очевидно, имеют разные значения.

IQueryableпредставляет собой дерево выражений (просто запрос), которое будет преобразовано во что-либо еще базовым поставщиком запросов при вызове API-интерфейсов выпуска, например агрегатных функций LINQ (Sum, Count и т. д.) или ToList [Array, Dictionary ,. ..]. И IQueryableобъекты также реализуют IEnumerable, IEnumerable<T>так что если они представляют запрос, результат этого запроса может быть повторен. Это означает, что IQueryable не должен быть только запросом. Правильный термин - это деревья выражения .

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

В мире Entity Framework (который является мистическим базовым поставщиком источника данных или поставщиком запросов) IQueryableвыражения транслируются в собственные запросы T-SQL . Nhibernateделает подобные вещи с ними. Вы можете написать свой собственный, следуя принципам, довольно хорошо описанным в LINQ: например, создание ссылки на провайдера IQueryable , и вы можете захотеть иметь пользовательский API запросов для службы поставщика вашего магазина продуктов.

Таким образом, в основном IQueryableобъекты создаются все время, пока мы явно не освободим их и не попросим систему переписать их в SQL или что-то еще и отправить цепочку выполнения для дальнейшей обработки.

Что же касается отложенного выполнения, то это LINQвозможность удерживать схему дерева выражений в памяти и отправлять ее в выполнение только по требованию, всякий раз, когда определенные API-интерфейсы вызываются для последовательности (тот же Count, ToList и т. Д.).

Правильное использование обоих в значительной степени зависит от задач, стоящих перед вами в конкретном случае. Для хорошо известного шаблона репозитория я лично предпочитаю возвращать IList, то есть IEnumerableсписки (индексаторы и тому подобное). Поэтому мой совет использовать IQueryableтолько в репозиториях и IEnumerable где-либо еще в коде. Не говоря о проблемах, связанных с проверяемостью, которые IQueryableразрушают и разрушают принцип разделения интересов . Если вы возвращаете выражение из репозиториев, потребители могут поиграть со слоем персистентности так, как им хотелось бы.

Небольшое дополнение к беспорядку :) (из обсуждения в комментариях)) Ни один из них не является объектами в памяти, так как они не являются реальными типами как таковыми, это маркеры типа - если вы хотите углубиться в это. Но имеет смысл (и именно поэтому даже MSDN так выразился) рассматривать IEnumerables как коллекции в памяти, а IQueryables - как деревья выражений. Дело в том, что интерфейс IQueryable наследует интерфейс IEnumerable, так что, если он представляет запрос, результаты этого запроса могут быть перечислены. Перечисление вызывает выполнение дерева выражений, связанного с объектом IQueryable. Таким образом, на самом деле вы не можете вызвать любой член IEnumerable, не имея объекта в памяти. В любом случае он попадет туда, если вы это сделаете, если он не пустой. IQueryables - это просто запросы, а не данные.

Арман МакХитариан
источник
3
Комментарий о том, что IEnumerables всегда находятся в памяти, не обязательно верен. Интерфейс IQueryable реализует интерфейс IEnumerable. По этой причине вы можете передать необработанный IQueryable, представляющий запрос LINQ-to-SQL, прямо в представление, которое ожидает IEnumerable! Вы можете быть удивлены, обнаружив, что ваш контекст данных истек или у вас возникли проблемы с MARS (несколько активных наборов результатов).
так что, на самом деле, вы не можете вызвать любой член IEnumerable, не имея объекта в памяти. В любом случае он попадет туда, если вы это сделаете, если он не пустой. IQueryables - это просто запросы, а не данные. Но я действительно понимаю вашу точку зрения. Я добавлю комментарий к этому.
Арман Макхитариан
@AlexanderPritchard - ни один из них не является объектом в памяти, поскольку сам по себе он не является реальным типом, он является маркером типа - если вы хотите углубиться в это. Но имеет смысл (и именно поэтому даже MSDN так выразился) рассматривать IEnumerables как коллекции в памяти, а IQueryables - как деревья выражений. Дело в том, что интерфейс IQueryable наследует интерфейс IEnumerable, так что, если он представляет запрос, результаты этого запроса могут быть перечислены. Перечисление вызывает выполнение дерева выражений, связанного с объектом IQueryable.
Арман Макхитариан
24

Как правило, вы хотите сохранить исходный статический тип запроса, пока он не будет иметь значения.

По этой причине вы можете определить свою переменную как 'var' вместо либо, IQueryable<>либо, IEnumerable<>и вы будете знать, что вы не меняете тип.

Если вы начинаете с символа IQueryable<>, вы, как правило, хотите сохранить его, IQueryable<>пока не появится веская причина для его изменения. Причина этого в том, что вы хотите предоставить обработчику запросов как можно больше информации. Например, если вы собираетесь использовать только 10 результатов (вы звонили Take(10)), вы хотите, чтобы SQL Server знал об этом, чтобы он мог оптимизировать свои планы запросов и отправлять вам только те данные, которые вы будете использовать.

Убедительная причина изменить тип с IQueryable<>на IEnumerable<>может быть в том, что вы вызываете некоторую функцию расширения, которую реализация IQueryable<>вашего конкретного объекта либо не может обработать, либо обрабатывает неэффективно. В этом случае вам может потребоваться преобразовать тип в IEnumerable<>(путем присвоения переменной типа IEnumerable<>или, например, с помощью AsEnumerableметода расширения), чтобы вызываемые вами функции расширения в конечном итоге были теми, которые в Enumerableклассе, а не в Queryableклассе.

AJS
источник
18

Есть блог с кратким примером исходного кода о том, как неправильное использование IEnumerable<T>может существенно повлиять на производительность запросов LINQ: Entity Framework: IQueryable против IEnumerable .

Если мы покопаемся глубже и посмотрим на источники, то увидим, что существуют явно разные методы расширения IEnumerable<T>:

// Type: System.Linq.Enumerable
// Assembly: System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// Assembly location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Core.dll
public static class Enumerable
{
    public static IEnumerable<TSource> Where<TSource>(
        this IEnumerable<TSource> source, 
        Func<TSource, bool> predicate)
    {
        return (IEnumerable<TSource>) 
            new Enumerable.WhereEnumerableIterator<TSource>(source, predicate);
    }
}

и IQueryable<T>:

// Type: System.Linq.Queryable
// Assembly: System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// Assembly location: C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Core.dll
public static class Queryable
{
    public static IQueryable<TSource> Where<TSource>(
        this IQueryable<TSource> source, 
        Expression<Func<TSource, bool>> predicate)
    {
        return source.Provider.CreateQuery<TSource>(
            Expression.Call(
                null, 
                ((MethodInfo) MethodBase.GetCurrentMethod()).MakeGenericMethod(
                    new Type[] { typeof(TSource) }), 
                    new Expression[] 
                        { source.Expression, Expression.Quote(predicate) }));
    }
}

Первый возвращает перечисляемый итератор, а второй создает запрос через поставщика запросов, указанного в IQueryableисточнике.

Александр Иваницкий
источник
11

Недавно я столкнулся с проблемой с IEnumerableст. IQueryable. Используемый алгоритм сначала выполнил IQueryableзапрос для получения набора результатов. Затем они были переданы в foreachцикл с элементами, созданными как класс Entity Framework (EF). Этот класс EF затем использовался в fromпредложении запроса Linq to Entity, в результате чего был получен результат IEnumerable.

Я довольно новичок в EF и Linq для Entities, поэтому потребовалось время, чтобы выяснить, в чем заключалось узкое место. Используя MiniProfiling, я нашел запрос, а затем преобразовал все отдельные операции в один IQueryableзапрос Linq for Entities. Выполнение IEnumerableзаняло 15 секунд, а выполнение - IQueryable0,5 секунды. Было задействовано три таблицы, и после прочтения я считаю, что IEnumerableзапрос фактически формировал перекрестный продукт из трех таблиц и фильтровал результаты.

Попробуйте использовать IQueryables как практическое правило и профилируйте свою работу, чтобы сделать ваши изменения измеримыми.

sscheider
источник
причина была в том, что выражения IQueryable преобразуются в собственный SQL в EF и выполняются прямо в БД, в то время как списки IEnumerable являются объектами в памяти. Они извлекаются из БД в какой-то момент, когда вы вызываете агрегатные функции, такие как Count, Sum или любое To ... и впоследствии работаете в памяти. IQueryableОни также застряли в памяти после вызова одного из этих API, но если нет, вы можете передать выражение в стек слоев и поэкспериментировать с фильтрами до вызова API. Хорошо спроектированный DAL, как хорошо спроектированный репозиторий, решит такие проблемы;)
Арман МакХитариан,
10

Я хотел бы прояснить некоторые вещи из-за, казалось бы, противоречивых ответов (в основном это касается IEnumerable).

(1) IQueryableрасширяет IEnumerableинтерфейс. (Вы можете отправить IQueryableчто-то, что ожидает IEnumerableбез ошибок.)

(2) Оба IQueryableи IEnumerableLINQ пытаются ленивой загрузки при итерации по набору результатов. (Обратите внимание, что реализацию можно увидеть в методах расширения интерфейса для каждого типа.)

Другими словами, IEnumerablesне являются исключительно "в памяти". IQueryablesне всегда выполняются в базе данных. IEnumerableдолжен загружать вещи в память (после извлечения, возможно, лениво), потому что у него нет абстрактного поставщика данных. IQueryablesполагаться на абстрактный поставщик (например, LINQ-to-SQL), хотя это также может быть поставщик .NET в памяти.

Пример использования

(а) Извлечь список записей IQueryableиз контекста EF. (Нет записей в памяти.)

(б) Передать в IQueryableпредставление, чья модель IEnumerable. (Действит. IQueryableРасширяется IEnumerable.)

(c) Выполните итерацию и получите доступ к записям набора данных, дочерним объектам и свойствам из представления. (Может вызвать исключения!)

Возможные проблемы

(1) IEnumerableПопытки ленивой загрузки и вашего контекста данных истекли. Исключение, потому что провайдер больше не доступен.

(2) Прокси-объекты сущности Entity Framework включены (по умолчанию), и вы пытаетесь получить доступ к связанному (виртуальному) объекту с просроченным контекстом данных. То же, что (1).

(3) Несколько активных наборов результатов (MARS). Если вы перебираете блок IEnumerablein foreach( var record in resultSet )и одновременно пытаетесь получить доступ record.childEntity.childProperty, вы можете получить MARS из-за отложенной загрузки как набора данных, так и реляционной сущности. Это вызовет исключение, если оно не включено в строке подключения.

Решение

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

Выполните запрос и сохраните результаты, вызвав resultList = resultSet.ToList() это. Похоже, это самый простой способ убедиться, что ваши объекты находятся в памяти.

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


источник
9

Основное различие между «IEnumerable» и «IQueryable» заключается в том, где выполняется логика фильтра. Один выполняется на стороне клиента (в памяти), а другой - в базе данных.

Например, мы можем рассмотреть пример, в котором у нас есть 10000 записей для пользователя в нашей базе данных и, скажем, только 900 из них являются активными пользователями, поэтому в этом случае, если мы используем «IEnumerable», то сначала он загружает все 10000 записей в память и затем применяет фильтр IsActive, который в итоге возвращает 900 активных пользователей.

С другой стороны, в том же случае, если мы используем «IQueryable», он напрямую применяет фильтр IsActive к базе данных, который непосредственно оттуда вернет 900 активных пользователей.

Ссылка Ссылка

Табиш Усман
источник
Какой из них оптимизирован и легкий вес с точки зрения производительности?
Сайт Сэм
@Sam "IQueryable" является более предпочтительным с точки зрения оптимизации и легкого веса.
Табиш Усман
6

Мы можем использовать оба одинаково, и они отличаются только по производительности.

IQueryable эффективно работает только с базой данных. Это означает, что он создает весь запрос выбора и получает только связанные записи.

Например, мы хотим взять 10 лучших клиентов, чье имя начинается с «Nimal». В этом случае запрос на выборку будет сгенерирован как select top 10 * from Customer where name like ‘Nimal%’.

Но если бы мы использовали IEnumerable, запрос был бы таким, select * from Customer where name like ‘Nimal%’и первая десятка будет отфильтрована на уровне кодирования C # (он получает все записи о клиентах из базы данных и передает их в C #).

user3710357
источник
5

В дополнение к первым двум действительно хорошим ответам (автор Driis & Jacob):

Интерфейс IEnumerable находится в пространстве имен System.Collections.

Объект IEnumerable представляет собой набор данных в памяти и может перемещаться по этим данным только вперед. Запрос, представленный объектом IEnumerable, выполняется немедленно и полностью, поэтому приложение получает данные быстро.

Когда запрос выполняется, IEnumerable загружает все данные, и если нам нужно отфильтровать их, сама фильтрация выполняется на стороне клиента.

Интерфейс IQueryable находится в пространстве имен System.Linq.

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

Что выбрать?

Если вам нужен весь набор возвращаемых данных, то лучше использовать IEnumerable, который обеспечивает максимальную скорость.

Если вам НЕ нужен весь набор возвращаемых данных, а только некоторые отфильтрованные данные, то лучше использовать IQueryable.

Глеб Б
источник
0

В дополнение к вышесказанному, интересно отметить, что вы можете получить исключения, если вы используете IQueryableвместо IEnumerable:

Следующее прекрасно работает, если productsэто IEnumerable:

products.Skip(-4);

Однако, если productsесть IQueryableи он пытается получить доступ к записям из таблицы БД, то вы получите эту ошибку:

Смещение, указанное в предложении OFFSET, не может быть отрицательным.

Это потому, что был создан следующий запрос:

SELECT [p].[ProductId]
FROM [Products] AS [p]
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS

и OFFSET не может иметь отрицательное значение.

Дэвид Клемпфнер
источник