JOIN w SQL dla .NET developera — łączenie tabel bez błędów

Jedna tabela to rzadkość. W realnej bazie dane są rozbite: tabela Zamowienia trzyma KlientId, a nie imię klienta — bo powtarzanie imienia w każdym wierszu byłoby marnotrawstwem i źródłem niespójności. Żeby złożyć je z powrotem, używasz JOIN. To serce SQL-a i najczęstsze pytanie na rozmowie o bazę danych. Po opanowaniu podstaw zapytań to następny krok, bez którego nie napiszesz nic sensownego.
Schemat i klucze obce
-- Klienci(Id, Imie, Miasto)
-- Produkty(Id, Nazwa, Cena)
-- Zamowienia(Id, KlientId, ProduktId, Ilosc)Zamowienia.KlientId to klucz obcy — wskazuje na Klienci.Id. JOIN wykorzystuje tę relację, żeby połączyć wiersze z dwóch tabel tam, gdzie klucze się zgadzają.
INNER JOIN — tylko pasujące pary
Najczęstszy JOIN. Zwraca wiersze, dla których istnieje dopasowanie po obu stronach:
SELECT z.Id, k.Imie, p.Nazwa, z.Ilosc
FROM Zamowienia z
INNER JOIN Klienci k ON z.KlientId = k.Id
INNER JOIN Produkty p ON z.ProduktId = p.Id;Warunek po ON mówi, jak dopasować wiersze. Aliasy (z, k, p) skracają zapis i są obowiązkowe, gdy kolumny mają te same nazwy w różnych tabelach. Zamówienie bez pasującego klienta (osierocone) nie pojawi się w wyniku — to istota INNER.
LEFT JOIN — wszystko z lewej, dopasowania z prawej
LEFT JOIN zwraca wszystkie wiersze z lewej tabeli, a z prawej dokłada dopasowania albo NULL, gdy ich nie ma:
SELECT k.Imie, z.Id AS Zamowienie
FROM Klienci k
LEFT JOIN Zamowienia z ON z.KlientId = k.Id;To zapytanie pokaże także klientów bez żadnego zamówienia — z NULL w kolumnie Zamowienie. INNER JOIN by ich pominął. Stąd typowe zastosowanie: „znajdź klientów, którzy nic nie kupili”:
SELECT k.Imie
FROM Klienci k
LEFT JOIN Zamowienia z ON z.KlientId = k.Id
WHERE z.Id IS NULL;RIGHT i FULL JOIN
RIGHT JOIN to lustro LEFT — wszystkie wiersze z prawej tabeli. W praktyce prawie nikt go nie używa (zamiast tego odwraca się kolejność tabel i pisze LEFT). FULL JOIN zwraca wszystko z obu stron, uzupełniając brakujące dopasowania NULL-ami — przydatny do wykrywania rozbieżności między tabelami.
| JOIN | Zwraca |
|---|---|
INNER | tylko wiersze z dopasowaniem po obu stronach |
LEFT | wszystkie z lewej + dopasowania z prawej (lub NULL) |
RIGHT | wszystkie z prawej + dopasowania z lewej (lub NULL) |
FULL | wszystkie z obu stron, NULL gdzie brak dopasowania |
Self-join — tabela złączona sama ze sobą
Gdy wiersze odwołują się do innych wierszy tej samej tabeli (pracownik ma przełożonego, też pracownika), łączysz tabelę ze sobą pod dwoma aliasami:
SELECT prac.Imie AS Pracownik, szef.Imie AS Przelozony
FROM Pracownicy prac
LEFT JOIN Pracownicy szef ON prac.SzefId = szef.Id;Most do .NET — Include zamiast ręcznego JOIN
W Entity Framework Core rzadko piszesz JOIN ręcznie — relacje modelujesz właściwościami nawigacyjnymi, a EF generuje JOIN za Ciebie:
var zamowienia = db.Zamowienia
.Include(z => z.Klient) // EF Core dołączy dane klienta (JOIN pod spodem)
.Include(z => z.Produkt)
.ToList();Include odpowiada INNER/LEFT JOIN w wygenerowanym SQL. To wygoda, ale — jak każda abstrakcja — ma pułapkę, jeśli zapomnisz jej użyć.
Includeto nie ozdobnik. To jawna decyzja „pobierz te powiązane dane jednym zapytaniem”. Jej brak nie daje błędu — daje pułapkę N+1, o której niżej.
Pułapki rekrutacyjne
Pułapka #1 — N+1 zapytań. Najważniejsza pułapka .NET developera z bazą. Pętla po zamówieniach, która dla każdego z osobna sięga po klienta, generuje 1 zapytanie o listę + N zapytań o klientów = N+1 odwołań do bazy zamiast jednego JOIN-a:
foreach (var z in db.Zamowienia.ToList()) // 1 zapytanie
Console.WriteLine(z.Klient.Imie); // +1 zapytanie na KAŻDĄ iteracjęRozwiązanie: Include(z => z.Klient) — jeden JOIN zamiast setek zapytań. To niemal pewne pytanie na rozmowie i realna przyczyna wolnych aplikacji.
Pułapka #2 — WHERE na kolumnie z LEFT JOIN zamienia go w INNER. Warunek WHERE z.Ilosc > 5 na tabeli dołączonej przez LEFT JOIN odrzuci wiersze z NULL (bo NULL > 5 to nie prawda) — czyli właśnie te, które LEFT JOIN miał zachować. Jeśli warunek ma dotyczyć tylko dopasowanych wierszy, przenieś go do ON, nie do WHERE.
Pułapka #3 — iloczyn kartezjański. JOIN bez warunku ON (albo z błędnym) łączy każdy wiersz z każdym — 1000 × 1000 = milion wierszy. Objaw: zapytanie zwraca absurdalnie dużo danych i zawiesza się. Zawsze sprawdź, czy ON łączy po właściwych kluczach.
Pułapka #4 — duplikaty przy agregacji. JOIN „jeden do wielu” zwielokrotnia wiersze strony „jeden”. SUM(p.Cena) po złączeniu produktów z zamówieniami policzy cenę tyle razy, w ilu zamówieniach produkt wystąpił. Uważaj, co agregujesz po JOIN-ie — czasem trzeba DISTINCT albo podzapytania.
Podsumowanie
JOIN składa dane rozbite między tabele według kluczy obcych. INNER JOIN zwraca tylko pasujące pary, LEFT JOIN zachowuje wszystkie wiersze z lewej strony (z NULL tam, gdzie brak dopasowania), RIGHT i FULL to warianty rzadziej używane. W .NET zwykle nie piszesz JOIN-a ręcznie — robi to EF Core przez Include — ale musisz rozumieć, co generuje, bo brak Include to pułapka N+1: N+1 zapytań zamiast jednego złączenia. Reszta klasycznych błędów (warunek WHERE psujący LEFT JOIN, iloczyn kartezjański, zdublowane agregaty) sprowadza się do jednej zasady: wiedz, które wiersze JOIN zachowuje, a które odrzuca.
Co dalej
JOIN-y na dużych tabelach potrafią być wolne — i tu wchodzą indeksy i wydajność SQL, które decydują, czy złączenie trwa milisekundy czy sekundy. Zanim wyślesz zapytanie z aplikacji, poznaj SQL injection i parametryzację w .NET. A jak EF Core zamienia właściwości nawigacyjne na JOIN-y — Entity Framework Core od podstaw i budowanie API na Dapperze, gdzie JOIN piszesz już ręcznie.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →
1 comment