• C# i .NET
  • Join w LINQ w C# - składnia, GroupJoin i LeftJoin

Join w LINQ w C# - składnia, GroupJoin i LeftJoin

Bruno Krawczyk 22 czerwca 2026
Kod C# demonstrujący LINQ join. Diagram przedstawia typy joinów: Inner, Left Outer, Right Outer i Full Join.

Spis treści

Masz dwie kolekcje, które przechowują powiązane dane, ale nie mają bezpośrednich odwołań do siebie? W takiej sytuacji operacja Join w LINQ pozwala połączyć elementy na podstawie wspólnego klucza, podobnie jak INNER JOIN w SQL. Pokażę składnię metody i zapytania, różnice między Join, GroupJoin oraz LeftJoin, a także typowe błędy, które potrafią zmienić liczbę wyników albo ukryć brakujące dane.

Najważniejsze informacje o łączeniu kolekcji w C#

  • Join zwraca wyłącznie elementy mające dopasowanie po obu stronach.
  • GroupJoin zachowuje wszystkie elementy z pierwszej kolekcji i grupuje pasujące rekordy.
  • .NET 10 wprowadza bezpośrednie metody LeftJoin i RightJoin.
  • Przy wielu dopasowaniach powstaje osobny wynik dla każdej pary elementów.
  • Dla dwóch lub większej liczby właściwości można użyć klucza złożonego.

Na czym polega Join w LINQ

Join kojarzy elementy dwóch sekwencji, porównując wartości zwracane przez dwa selektory klucza. Pierwsza kolekcja jest traktowana jako outer, druga jako inner, ale sama kolejność ma znaczenie głównie dla czytelności i kolejności wyników.

Załóżmy, że mamy klientów i zamówienia. Zamówienie przechowuje tylko CustomerId, a dane klienta znajdują się w osobnej kolekcji. Łączenie pozwala utworzyć wynik zawierający numer zamówienia oraz nazwę klienta.

public record Customer(int Id, string Name);
public record Order(int Id, int CustomerId, decimal Total);

var customers = new List
{
    new(1, "Anna"),
    new(2, "Marek"),
    new(3, "Ola")
};

var orders = new List
{
    new(1001, 1, 249.99m),
    new(1002, 1, 89.00m),
    new(1003, 2, 499.00m),
    new(1004, 99, 120.00m)
};

Zamówienie o numerze 1004 nie ma klienta o identyfikatorze 99. Przy zwykłym Join nie pojawi się więc w wynikach. To najważniejsza cecha tego operatora: brak dopasowania usuwa element z rezultatu.

Jak używać Join w składni metod

Najczęściej korzystam ze składni metod, ponieważ dobrze pasuje do potoków LINQ i łatwo można do niej dopisać filtrowanie, sortowanie oraz projekcję. Metoda przyjmuje kolekcję wewnętrzną, funkcję pobierającą klucz z obu stron i funkcję budującą wynik.

var result = customers.Join(
    orders,
    customer => customer.Id,
    order => order.CustomerId,
    (customer, order) => new
    {
        OrderId = order.Id,
        CustomerName = customer.Name,
        order.Total
    });

foreach (var item in result)
{
    Console.WriteLine(
        $"{item.OrderId}: {item.CustomerName}, {item.Total:C}");
}

Wynik zawiera trzy rekordy, ponieważ tylko trzy zamówienia mają odpowiedniego klienta. Klient bez zamówień również nie pojawi się w tym zestawieniu. Jeżeli obie kolekcje mają po kilka elementów z tym samym kluczem, LINQ utworzy wszystkie pasujące kombinacje, a nie jedno przypadkowe dopasowanie.

To zachowanie często zaskakuje przy danych, które nie mają unikalnych kluczy. Jeżeli klient ma trzy zamówienia, otrzymamy trzy wiersze dla tego klienta. Dlatego przed użyciem Join trzeba ustalić, czy relacja jest typu jeden do jednego, jeden do wielu czy wiele do wielu.

Ta sama operacja w składni zapytania

Składnia zapytań jest bardziej zbliżona do SQL i przy dłuższych zapytaniach bywa łatwiejsza do czytania. W C# warunek łączenia zapisuje się za pomocą słowa equals, a nie operatora ==.

var result =
    from customer in customers
    join order in orders
        on customer.Id equals order.CustomerId
    select new
    {
        OrderId = order.Id,
        CustomerName = customer.Name,
        order.Total
    };

Obie wersje robią to samo. Ja wybieram składnię zapytań wtedy, gdy łączę kilka źródeł i chcę zachować czytelny układ relacji. Przy pojedynczym połączeniu składnia metod jest zwykle krótsza i naturalniej współgra z pozostałymi operatorami LINQ.

Łączenie kilku kolekcji

Można dołączać kolejne źródła, ale po każdym kroku trzeba pamiętać, że wynik często jest typem anonimowym zawierającym wcześniejsze obiekty. Przykład łączy zamówienia z klientami i płatnościami.

var result = orders
    .Join(
        customers,
        order => order.CustomerId,
        customer => customer.Id,
        (order, customer) => new { order, customer })
    .Join(
        payments,
        item => item.order.Id,
        payment => payment.OrderId,
        (item, payment) => new
        {
            OrderId = item.order.Id,
            Customer = item.customer.Name,
            payment.Status
        });

W praktyce przy trzech lub czterech połączeniach rozważam utworzenie osobnego typu wynikowego zamiast dalszego zagnieżdżania anonimowych obiektów. Kod staje się wtedy łatwiejszy do debugowania, testowania i przekazania do warstwy API.

Klucze proste i złożone

Najprostszy przypadek porównuje jedną właściwość, na przykład Id. Czasem jednak pojedyncza wartość nie identyfikuje rekordu. Typowym przykładem jest raport, w którym dopasowanie zależy jednocześnie od identyfikatora produktu i roku.

var result = sales.Join(
    targets,
    sale => new
    {
        sale.ProductId,
        sale.Year
    },
    target => new
    {
        target.ProductId,
        target.Year
    },
    (sale, target) => new
    {
        sale.ProductId,
        sale.Year,
        sale.Amount,
        Target = target.Value
    });

Anonimowe typy porównują swoje właściwości wartościowo, więc taki zapis działa dobrze dla klucza złożonego. W składni zapytania nazwy właściwości i ich kolejność muszą być zgodne po obu stronach wyrażenia equals.

Nie łączyłbym danych po nazwie wyświetlanej użytkownikowi, jeśli istnieje stabilny identyfikator. Nazwa może zmienić wielkość liter, format albo zostać powtórzona. Klucz techniczny, najlepiej jednoznaczny i niezmienny, ogranicza ryzyko fałszywych dopasowań.

GroupJoin i połączenia zachowujące brakujące dane

GroupJoin działa inaczej niż zwykły Join. Zamiast tworzyć osobny wynik dla każdej pary, zwraca jeden element z pierwszej kolekcji oraz grupę pasujących elementów z drugiej. Dzięki temu zachowuje także klientów, którzy nie mają żadnego zamówienia.

var result = customers.GroupJoin(
    orders,
    customer => customer.Id,
    order => order.CustomerId,
    (customer, customerOrders) => new
    {
        Customer = customer.Name,
        OrderCount = customerOrders.Count(),
        Total = customerOrders.Sum(order => order.Total),
        Orders = customerOrders
    });

foreach (var item in result)
{
    Console.WriteLine(
        $"{item.Customer}: {item.OrderCount} zamówień, {item.Total:C}");
}

Klient Ola pojawi się z liczbą zamówień równą zero. To ważne przy raportach, listach administracyjnych i ekranach, na których użytkownik powinien zobaczyć pełny katalog elementów, a nie tylko te, które mają powiązane dane.

Jeżeli chcemy spłaszczyć grupy do pojedynczych wierszy, możemy użyć SelectMany. Gdy grupa jest pusta, zwykłe spłaszczenie nie utworzy rekordu, więc otrzymamy zachowanie podobne do wewnętrznego połączenia.

var flattened = customers
    .GroupJoin(
        orders,
        customer => customer.Id,
        order => order.CustomerId,
        (customer, customerOrders) => new { customer, customerOrders })
    .SelectMany(
        item => item.customerOrders,
        (item, order) => new
        {
            Customer = item.customer.Name,
            OrderId = order.Id
        });

LeftJoin i RightJoin w .NET 10

W .NET 10 pojawiły się bezpośrednie metody LeftJoin i RightJoin. Wcześniej lewostronne połączenie najczęściej zapisywało się za pomocą GroupJoin, SelectMany oraz DefaultIfEmpty. Nowa składnia jest prostsza do odczytania i lepiej pokazuje intencję kodu.

var result = orders.LeftJoin(
    customers,
    order => order.CustomerId,
    customer => customer.Id,
    (order, customer) => new
    {
        OrderId = order.Id,
        CustomerName = customer?.Name ?? "brak klienta",
        order.Total
    });

W tym przypadku każde zamówienie zostanie zachowane, także to o numerze 1004. Gdy klienta nie ma, parametr po prawej stronie będzie miał wartość domyślną, dlatego trzeba bezpiecznie obsłużyć null.

RightJoin działa analogicznie, ale zachowuje wszystkie elementy z drugiej kolekcji. W wielu projektach można osiągnąć ten sam efekt, zamieniając kolejność źródeł i używając LeftJoin. Zwykle robię właśnie tak, bo lewostronne połączenia są częściej spotykane i dla zespołu łatwiej rozpoznać ich znaczenie.

Jeżeli aplikacja działa na starszej wersji .NET, pozostaje klasyczny wzorzec z DefaultIfEmpty.

var result = from order in orders
             join customer in customers
                 on order.CustomerId equals customer.Id into matches
             from customer in matches.DefaultIfEmpty()
             select new
             {
                 OrderId = order.Id,
                 CustomerName = customer?.Name ?? "brak klienta"
             };

Najczęstsze błędy przy łączeniu danych

Mylenie Join z wyszukiwaniem pojedynczego elementu

Join nie wybiera pierwszego pasującego rekordu. Jeżeli po prawej stronie znajdzie trzy elementy z tym samym kluczem, zwróci trzy wyniki. Gdy potrzebujesz dokładnie jednego obiektu, najpierw zadbaj o unikalność danych albo użyj świadomie Single, SingleOrDefault czy FirstOrDefault.

Brakujące rekordy znikają bez ostrzeżenia

Przy zwykłym Join brak dopasowania jest prawidłowym zachowaniem, a nie wyjątkiem. Jeżeli raport ma pokazywać także klientów bez zamówień, użyj GroupJoin lub LeftJoin, zamiast próbować naprawiać wynik po fakcie.

Niezgodne typy kluczy

Wartości kluczy muszą być porównywalne pod względem typu. int i long nie są tym samym typem, podobnie jak tekst z numerem klienta i liczba całkowita. Konwersję najlepiej wykonać przed łączeniem i w jednym, jasno opisanym miejscu.

Różnice między LINQ to Objects i bazą danych

Dla list w pamięci działają delegaty i standardowy komparer równości. Dla IQueryable, na przykład w Entity Framework Core, wyrażenie może zostać przetłumaczone na SQL, a możliwości zależą od dostawcy. To, co działa dla List, nie zawsze zostanie przetłumaczone do zapytania wykonywanego przez bazę.

Nie pobierałbym dużych tabel do pamięci tylko po to, aby wykonać Join lokalnie. Jeśli dane pochodzą z jednej bazy, lepiej pozwolić dostawcy przetłumaczyć operację na SQL. Gdy łączysz różne źródła, takie jak API i plik CSV, lokalne LINQ jest naturalnym rozwiązaniem, ale wtedy trzeba kontrolować ilość wczytanych danych.

Przeczytaj również: typeof w C# - różnice, generyki i refleksja

Opóźnione wykonanie

Większość zapytań LINQ nie wykonuje się w chwili przypisania do zmiennej. Wynik powstaje podczas iteracji, na przykład w foreach albo po wywołaniu ToList(). Jeśli źródło może się zmienić lub zapytanie ma być wykonane tylko raz, jawnie materializuję wynik przez ToList albo ToArray.

Jak dobrać właściwy wariant do zadania

Potrzeba Operator Co otrzymasz
Tylko rekordy z dopasowaniem po obu stronach Join Jedną parę wyniku dla każdego dopasowania
Każdy element z pierwszej kolekcji i jego pasujące elementy GroupJoin Element oraz kolekcję powiązanych danych
Każdy element z lewej strony, także bez dopasowania LeftJoin Pełną lewą kolekcję i wartości domyślne po prawej
Każdy element z prawej strony, także bez dopasowania RightJoin Pełną prawą kolekcję i wartości domyślne po lewej
Połączenie elementów według pozycji Zip Pary elementów o tych samych indeksach, nie wspólnym kluczu

Najczęściej zaczynam od pytania, czy brak dopasowania ma usunąć rekord, czy powinien zostać pokazany jako brak danych. Jeśli ma zniknąć, wybieram Join. Jeśli ma pozostać widoczny, wybieram połączenie lewostronne. Ta jedna decyzja rozwiązuje większość wątpliwości dotyczących operatora.

Przy danych w pamięci sprawdzam też, czy liczba elementów jest rozsądna i czy klucze są odpowiednio znormalizowane. Usunięcie przypadkowych spacji, ujednolicenie wielkości liter oraz wybór właściwego komparatora często daje większą poprawę jakości niż samo przepisanie składni zapytania.

Od poprawnego klucza zależy cały wynik

Join w C# jest prostym narzędziem, ale jego rezultat zawsze odzwierciedla jakość kluczy i relacji między kolekcjami. Join wybiera tylko dopasowane elementy, GroupJoin pomaga budować dane hierarchiczne, a LeftJoin i RightJoin upraszczają obsługę brakujących rekordów w .NET 10.

Jeżeli wynik ma zbyt wiele wierszy, sprawdź duplikaty kluczy. Jeżeli brakuje rekordów, sprawdź, czy naprawdę potrzebujesz połączenia wewnętrznego. W praktyce to właśnie świadomy wybór rodzaju połączenia, a nie sama znajomość składni, robi największą różnicę w kodzie LINQ.

FAQ - Najczęstsze pytania

Join zwraca tylko elementy z dopasowaniem po obu stronach. GroupJoin zachowuje wszystkie elementy pierwszej kolekcji i przypisuje im grupy dopasowań, a dostępne w .NET 10 LeftJoin i RightJoin służą do zachowania odpowiednio lewej lub prawej kolekcji wraz z brakującymi dopasowaniami.

Operator tworzy osobny wynik dla każdej pasującej pary elementów. Jeśli klient ma trzy zamówienia, otrzymasz trzy wiersze, a duplikaty kluczy po obu stronach mogą utworzyć wszystkie pasujące kombinacje. Przed użyciem Join warto sprawdzić, czy relacja jest jeden do jednego, jeden do wielu czy wiele do wielu.

Użyj klucza złożonego z anonimowego typu, na przykład new { sale.ProductId, sale.Year }, oraz analogicznego klucza po drugiej stronie. W składni zapytań nazwy właściwości i ich kolejność muszą być zgodne w obu częściach wyrażenia equals.

Dla List<T> LINQ działa w pamięci i korzysta z delegatów, natomiast dla IQueryable wyrażenie może zostać przetłumaczone na SQL zależnie od dostawcy, na przykład Entity Framework Core. Nie każda konstrukcja działająca dla list zostanie przetłumaczona przez bazę, dlatego dużych tabel nie warto pobierać do pamięci tylko po to, aby łączyć je lokalnie.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

join
groupjoin
iqueryable
leftjoin
klucze złożone
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