• Bazy danych i SQL
  • IEnumerable czy IQueryable? Gdzie wykonuje się zapytanie LINQ

IEnumerable czy IQueryable? Gdzie wykonuje się zapytanie LINQ

Bruno Krawczyk 31 sierpnia 2026
Tabela porównuje IEnumerable i IQueryable, pokazując różnice w zapytaniach do danych lokalnych i zewnętrznych.

Spis treści

Jedno pozornie niewinne ToList() potrafi zmienić szybkie zapytanie SQL w operację, która pobiera z bazy tysiące niepotrzebnych rekordów. Rozstrzygnięcie dylematu IEnumerable vs IQueryable pomaga zrozumieć, gdzie faktycznie wykonuje się kod LINQ, kiedy filtr trafia do SQL i jak uniknąć problemów z wydajnością w aplikacjach .NET.

Najważniejsze różnice między IEnumerable i IQueryable w praktyce

  • IEnumerable wykonuje operatory LINQ w pamięci aplikacji.
  • IQueryable buduje wyrażenie, które dostawca, na przykład EF Core, może przetłumaczyć na SQL.
  • ToList(), First() i podobne metody materializują zapytanie i kończą jego dalsze składanie po stronie bazy.
  • AsEnumerable() świadomie przenosi kolejne operacje do pamięci.
  • Najlepszy wybór zależy od miejsca wykonania filtrów, sortowania i projekcji, a nie od samej nazwy interfejsu.

Najkrótsza odpowiedź brzmi prosto, ale ma ważne konsekwencje

IEnumerable opisuje sekwencję elementów, po której aplikacja może iterować. Gdy używam na niej metod LINQ, takich jak Where, Select czy OrderBy, operacje wykonują się zwykle w pamięci procesu .NET.

IQueryable reprezentuje zapytanie, które może zostać zinterpretowane przez zewnętrznego dostawcę. W przypadku Entity Framework Core dostawca analizuje drzewo wyrażeń i tłumaczy je na zapytanie SQL wykonywane w bazie.

Najważniejsza różnica nie polega więc na tym, że jeden interfejs jest „lepszy”. Chodzi o to, gdzie wykonywana jest praca. Dla małej kolekcji w pamięci nie ma to większego znaczenia. Dla tabeli z milionem rekordów może zdecydować o czasie odpowiedzi, zużyciu pamięci i obciążeniu serwera.

IEnumerable pracuje na danych dostępnych w pamięci

Najłatwiej zobaczyć działanie IEnumerable na zwykłej kolekcji:

var products = new List
{
    new() { Name = "Laptop", Price = 4500 },
    new() { Name = "Mysz", Price = 120 }
};

IEnumerable result =
    products.Where(p => p.Price > 500);

W tym przykładzie filtr jest wykonywany przez kod aplikacji. Lista już znajduje się w pamięci, więc nie ma żadnego SQL ani zewnętrznego dostawcy, który mógłby zoptymalizować zapytanie. To właśnie klasyczny scenariusz dla LINQ to Objects.

W praktyce podobny efekt pojawia się po wcześniejszym pobraniu danych z EF Core:

var products = await db.Products.ToListAsync();

var expensiveProducts = products
    .Where(p => p.Price > 500)
    .OrderByDescending(p => p.Price)
    .ToList();

Pierwsze wywołanie pobiera całą tabelę lub cały wcześniejszy zakres zapytania. Dopiero później aplikacja filtruje i sortuje wynik. Przy kilkunastu rekordach nie będzie to problemem, ale przy dużej tabeli oznacza niepotrzebny transfer danych i większe zużycie RAM.

IQueryable pozwala przenieść logikę do SQL

Jeżeli źródłem danych jest DbSet, mogę budować zapytanie bez natychmiastowego pobierania rekordów:

var query = db.Products
    .Where(p => p.Price > 500)
    .OrderByDescending(p => p.Price)
    .Select(p => new
    {
        p.Name,
        p.Price
    })
    .Take(20);

var products = await query.ToListAsync();

Do momentu wywołania ToListAsync() powyższy kod tworzy opis zapytania. EF Core może przełożyć go na SQL zawierający filtr, sortowanie, wybór konkretnych kolumn i limit wyników. W efekcie baza zwraca tylko 20 potrzebnych rekordów i wybrane pola.

To opóźnione wykonanie nazywa się deferred execution. Samo zdefiniowanie zapytania nie musi jeszcze powodować połączenia z bazą. Zapytanie wykonuje się dopiero przy materializacji, iteracji albo wywołaniu metody zwracającej pojedynczy wynik, na przykład FirstAsync() lub AnyAsync().

Nie oznacza to jednak, że każde wyrażenie C# da się zamienić na SQL. Dostawca musi rozumieć użyte operatory i metody. Jeżeli w filtrze wywołam własną metodę, EF Core może zgłosić wyjątek albo wymusić wykonanie fragmentu po stronie klienta, zależnie od miejsca i rodzaju wyrażenia.

Porównanie zachowania obu interfejsów

Kryterium IEnumerable IQueryable
Miejsce wykonania Pamięć aplikacji Baza danych lub inny dostawca
Rodzaj delegata Func Expression>
Źródło danych Listy, tablice, kolekcje EF Core, LINQ to SQL i inni dostawcy
Moment wykonania Podczas iteracji lub operacji LINQ Przy materializacji albo iteracji
Optymalizacja SQL Nie dotyczy Możliwa po stronie dostawcy
Ryzyko Duże zużycie pamięci przy dużych kolekcjach Nieprzewidywalne zapytania lub trudna translacja

IQueryable dziedziczy po IEnumerable, dlatego oba typy wyglądają podobnie w kodzie. To podobieństwo bywa mylące. Wywołanie tej samej metody LINQ może prowadzić do innego miejsca wykonania, zależnie od statycznego typu źródła.

Największą różnicę widać po ToList i AsEnumerable

Przedwczesna materializacja

Ten kod najpierw pobiera dane z bazy, a dopiero potem wykonuje filtr:

var users = await db.Users.ToListAsync();

var activeUsers = users
    .Where(u => u.IsActive)
    .Take(50)
    .ToList();

Lepsza wersja przesuwa wszystkie operacje możliwe do wykonania w SQL przed materializację:

var activeUsers = await db.Users
    .Where(u => u.IsActive)
    .Take(50)
    .ToListAsync();

Różnica jest szczególnie duża, gdy filtr redukuje wynik z setek tysięcy rekordów do kilkudziesięciu. Moja praktyczna zasada jest prosta: ToList wywołuję dopiero wtedy, gdy naprawdę potrzebuję kolekcji w pamięci.

Świadome przejście do LINQ to Objects

AsEnumerable() nie jest błędem. Czasem jest potrzebne, gdy część logiki nie ma odpowiednika w SQL:

var products = db.Products
    .Where(p => p.IsPublished)
    .AsEnumerable()
    .Where(p => CalculateScore(p) > 10);

W tym przykładzie pierwszy filtr może trafić do bazy, a drugi wykona się już w aplikacji. To rozsądny kompromis, o ile wcześniejszy warunek ogranicza liczbę rekordów. Przeniesienie do pamięci przed filtrem selektywnym może natomiast szybko stać się wąskim gardłem.

Przeczytaj również: MySQL subquery w praktyce - IN, EXISTS i wydajne zapytania

Przypadkowa zmiana operatorów LINQ

Problem może pojawić się także wtedy, gdy zapytanie zostanie przekazane jako IEnumerable:

IEnumerable products = db.Products;

var result = products
    .Where(p => p.Price > 500)
    .ToList();

Od tego momentu kompilator wybiera operatory z klasy Enumerable, a nie Queryable. W efekcie filtr może zostać wykonany w pamięci. Sam typ zmiennej potrafi więc wpłynąć na zachowanie kodu, nawet jeśli obiekt pod spodem pochodzi z EF Core.

Typowe błędy przy zapytaniach do bazy

Najczęstszy błąd to traktowanie IQueryable jak zwykłej kolekcji i wielokrotne wykonywanie tego samego zapytania:

var query = db.Orders.Where(o => o.Status == OrderStatus.Open);

var count = await query.CountAsync();
var first = await query.FirstOrDefaultAsync();

W tym przypadku baza może otrzymać dwa osobne zapytania. To może być poprawne, ale przy kosztownych filtrach lub dużym ruchu warto świadomie zdecydować, czy oba wyniki są potrzebne. Wielokrotna enumeracja niezmaterializowanego zapytania często oznacza dodatkowe połączenia z bazą.

Drugi błąd to pobieranie całych encji, gdy ekran potrzebuje trzech pól. Projekcja przez Select ogranicza liczbę kolumn i często upraszcza działanie pamięci śledzenia zmian:

var summaries = await db.Orders
    .Where(o => o.CreatedAt >= startDate)
    .Select(o => new OrderSummary
    {
        Id = o.Id,
        Total = o.Total,
        CustomerName = o.Customer.Name
    })
    .ToListAsync();

Przy zapytaniach tylko do odczytu warto rozważyć także AsNoTracking(). Nie przyspieszy każdej operacji w zauważalny sposób, ale ogranicza narzut śledzenia encji, gdy aplikacja nie zamierza ich modyfikować.

Trzeci problem to przekonanie, że IQueryable zawsze zapewni lepszą wydajność. Złożone zapytanie z wieloma złączeniami, sortowaniem i funkcjami może wygenerować trudny SQL. Dlatego w krytycznych miejscach sprawdzam rzeczywiste zapytanie przez ToQueryString() oraz analizuję plan wykonania w bazie.

Kiedy wybrać IEnumerable, a kiedy IQueryable

Po IEnumerable sięgam, gdy dane są już w pamięci, kolekcja jest niewielka albo filtr wymaga kodu, którego dostawca nie potrafi przetłumaczyć. Dobrze pasuje też do metod, które mają działać niezależnie od konkretnego źródła danych.

IQueryable wybieram przy budowaniu zapytań do relacyjnej bazy, szczególnie gdy użytkownik może filtrować, sortować i stronicować wynik. W takim scenariuszu jak najwięcej pracy powinno zostać wykonane po stronie SQL, zanim dane opuszczą serwer bazy.

Nie wystawiam jednak bezrefleksyjnie IQueryable z każdej warstwy aplikacji. Przekazanie go przez cały system daje odbiorcy dużą kontrolę, ale może też ujawnić szczegóły dostępu do danych, utrudnić testowanie i spowodować zapytania trudne do przewidzenia. W wielu projektach lepiej zamknąć budowanie zapytania w warstwie dostępu do danych i zwracać gotowy model albo zmaterializowaną kolekcję.

  • Mała kolekcja w pamięci - wybierz IEnumerable.
  • Duża tabela w SQL - filtruj i sortuj na IQueryable.
  • Stronicowanie - użyj Skip i Take przed materializacją.
  • Własna logika C# - przenieś ją do pamięci dopiero po ograniczeniu danych.
  • Granica warstwy aplikacji - zwróć konkretny model zamiast przypadkowo ujawniać IQueryable.

Decyzję warto podejmować na podstawie miejsca wykonania

Najlepszym testem nie jest samo sprawdzenie typu zmiennej, lecz odpowiedź na trzy pytania. Ile danych pobieram? Które operacje wykona baza, a które aplikacja? Czy zapytanie wykonuje się dokładnie tyle razy, ile zakładam?

Jeżeli potrafię odpowiedzieć na te pytania i obejrzeć wygenerowany SQL, wybór interfejsu przestaje być kwestią przyzwyczajeń. IEnumerable jest świetne dla danych lokalnych, a IQueryable dla zapytań, które powinny zostać zoptymalizowane przez dostawcę, jednak oba narzędzia mają sens tylko wtedy, gdy świadomie kontroluję moment materializacji.

Na koniec rozdzieliłbym jeszcze IQueryable od IAsyncEnumerable. To drugie służy przede wszystkim do asynchronicznego, strumieniowego odczytu elementów i nie oznacza automatycznie, że filtr zostanie przetłumaczony na SQL. W aplikacjach opartych na EF Core najważniejsze pozostaje to, co dzieje się przed ToListAsync() i gdzie znajduje się granica między bazą a pamięcią aplikacji.

FAQ - Najczęstsze pytania

Filtr może trafić do SQL, gdy zapytanie pozostaje typu IQueryable, na przykład jako DbSet<T>, aż do momentu materializacji. Po użyciu ToList(), AsEnumerable() albo przypisaniu źródła do IEnumerable<T> operatory Enumerable wykonują filtr w pamięci aplikacji.

ToListAsync() pobiera do pamięci wszystkie rekordy objęte wcześniejszym zakresem zapytania. Dopiero potem Where filtruje dane, co zwiększa transfer z bazy i zużycie RAM. Wydajniej jest umieścić filtr i inne możliwe operacje przed materializacją.

AsEnumerable() warto zastosować, gdy dalsza logika, na przykład własna metoda C#, nie ma odpowiednika możliwego do przetłumaczenia na SQL. Najpierw należy ograniczyć wynik filtrami wykonywanymi w bazie, a dopiero potem przenieść pozostałe operacje do pamięci.

Filtruj, sortuj i stronicuj przed materializacją, używając między innymi Where, OrderBy, Skip i Take. Projekcja przez Select pozwala pobrać tylko potrzebne kolumny, a przy odczycie bez modyfikacji encji można rozważyć AsNoTracking().

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

linq
iqueryable
entity framework
ienumerable
materializacja
Autor Bruno Krawczyk
Bruno Krawczyk
Mam na imię Bruno i od 8 lat zgłębiam tajniki programowania w ekosystemie .NET, chmury Azure oraz sztucznej inteligencji. Moja przygoda z technologią zaczęła się od ciekawości, jak złożone systemy mogą ułatwiać codzienne życie i rozwiązywać realne problemy. Dziś moją misją jest dzielenie się tą wiedzą, starając się przybliżyć nawet najbardziej skomplikowane zagadnienia w sposób zrozumiały i przystępny dla każdego. W moich artykułach na kursdotnet.pl skupiam się na praktycznych aspektach, analizuję najnowsze trendy i weryfikuję informacje, aby dostarczyć Wam treści, które są nie tylko dokładne i aktualne, ale przede wszystkim użyteczne w Waszej własnej ścieżce rozwoju technologicznego.

Udostępnij artykuł

Napisz komentarz