Raport sprzedaży w aplikacji .NET pokazuje identyfikatory klientów, ale bez ich nazw trudno go czytać. Złączenie tabel przez JOIN pozwala pobrać dane z obu miejsc, choć wynik zależy od tego, czy rekordy bez pary mają zniknąć, czy pozostać w raporcie.
O wyniku złączenia decydują warunek i liczba dopasowań
- INNER JOIN zwraca tylko wiersze, dla których warunek złączenia znajduje parę.
- LEFT JOIN zachowuje każdy wiersz z lewej tabeli, a brak dopasowania po prawej oznacza wartości
NULL. - Wiele dopasowań powiela wiersz z lewej strony i może zawyżyć sumy.
- Filtr w WHERE na prawej tabeli może usunąć wiersze, które miały zostać zachowane przez
LEFT JOIN.
Wybierz typ złączenia według tego, które rekordy mają przetrwać
Wyobraź sobie listę zamówień i osobną tabelę klientów. Gdy raport ma zawierać tylko zamówienia z przypisanym klientem, wybór pada na INNER JOIN; gdy ma pokazać także błędne lub nieuzupełnione przypisania, użyj LEFT JOIN.
W typowym zapytaniu warunek opisuje relację między kluczem głównym a kluczem obcym. Klucz obcy to kolumna wskazująca rekord w innej tabeli, na przykład Zamowienia.klient_id odwołujące się do Klienci.id.
SELECT z.id, z.data_zamowienia, k.nazwa
FROM Zamowienia AS z
LEFT JOIN Klienci AS k
ON k.id = z.klient_id;
RIGHT JOIN zachowuje wszystkie wiersze z prawej tabeli, ale zwykle można odwrócić kolejność tabel i zapisać tę samą intencję jako LEFT JOIN. FULL OUTER JOIN zachowuje niedopasowane wiersze z obu stron, co przydaje się na przykład przy porównywaniu dwóch eksportów danych.
Moja praktyczna reguła jest prosta: jeśli brak powiązanego rekordu oznacza błąd lub brak interesującego przypadku, wybierz INNER JOIN; jeśli brak pary sam w sobie ma być widoczny w wyniku, wybierz LEFT JOIN. Przy porównaniu obu tabel naraz rozważ FULL OUTER JOIN, o ile obsługuje go używany silnik.
Dwa dopasowania do jednego zamówienia mogą zawyżyć sumę
Najbardziej podstępny błąd złączenia wygląda jak poprawny raport. Załóżmy, że tabela zamówień zawiera trzy pozycje o wartościach 100, 200 i 300 zł, a tabela płatności ma dwie częściowe wpłaty dla pierwszego zamówienia i jedną dla drugiego.
Po połączeniu przez LEFT JOIN wynik ma cztery wiersze: pierwsze zamówienie pojawia się dwa razy, drugie raz, a trzecie raz z NULL po stronie płatności. Jeśli zsumujesz w nim kwotę zamówienia, otrzymasz 700 zł zamiast 600 zł, bo kwota pierwszego zamówienia powtórzyła się wraz z każdą wpłatą.
SELECT SUM(z.kwota) AS suma_zamowien
FROM Zamowienia AS z
LEFT JOIN Platnosci AS p
ON p.zamowienie_id = z.id;
Nie naprawiaj tego automatycznie przez DISTINCT. Może ukryć powielone wiersze, ale nie odpowiada na pytanie, czy płatności są częściowe, czy w tabeli istnieje nieoczekiwany duplikat. Najpierw ustal, jakiej liczności oczekujesz po każdej stronie relacji.
| Sytuacja | Prawdopodobna przyczyna lub skutek | Co zrobić |
|---|---|---|
| Wynik ma więcej wierszy niż tabela zamówień | Jedno zamówienie pasuje do wielu płatności | Sprawdź liczbę płatności dla każdego zamówienia i agreguj je osobno przed złączeniem, jeśli raport wymaga jednego wiersza na zamówienie. |
Po LEFT JOIN brakuje zamówień bez płatności |
Filtr na kolumnie płatności odrzuca wiersze z NULL |
Przenieś warunek dotyczący dopasowania do ON albo świadomie zmień typ złączenia. |
| Kolumna w zapytaniu jest niejednoznaczna | Obie tabele zawierają kolumnę o tej samej nazwie | Poprzedź nazwę aliasem tabeli, np. z.id lub p.id. |
Gdy potrzebujesz wyłącznie sprawdzić, czy zamówienie ma jakąkolwiek płatność, rozważ EXISTS zamiast dołączania wszystkich wpłat. Zapytanie nie powieli wtedy zamówienia tylko dlatego, że znalazło kilka pasujących płatności.
Warunek w WHERE może zmienić LEFT JOIN w filtr INNER JOIN
Wiersz z lewej tabeli bez pary po prawej dostaje po złączeniu wartości NULL. Jeśli później wpiszesz w WHERE warunek typu p.status = 'zaksięgowana', taki wiersz nie przejdzie filtra i zniknie z wyniku.
-- Zachowuje zamówienia bez zaksięgowanej płatności
SELECT z.id, p.status
FROM Zamowienia AS z
LEFT JOIN Platnosci AS p
ON p.zamowienie_id = z.id
AND p.status = 'zaksięgowana';
W tej wersji warunek ogranicza dopasowania płatności, ale nadal zachowuje zamówienie bez takiego dopasowania. Gdy warunek w WHERE ma celowo odrzucić zamówienia bez zaksięgowanej płatności, pozostaw go tam; miejsce filtra wynika z oczekiwanego wyniku, nie z kosmetyki zapisu.
Do znalezienia rekordów bez pary przydaje się test IS NULL na kolumnie, która po stronie tabeli dołączanej nie może być pusta, na przykład na jej kluczu głównym. Prawdę mówiąc, to właśnie rozdzielenie „brak dopasowania” od „dopasowanie niespełniające filtra” najczęściej porządkuje zapytanie, które na pierwszy rzut oka wyglądało poprawnie.
SELECT z.id
FROM Zamowienia AS z
LEFT JOIN Platnosci AS p
ON p.zamowienie_id = z.id
WHERE p.id IS NULL;
Składnia JOIN różni się między MySQL a SQL Server
Zapytanie może działać w jednej bazie i odrzucić składnię w drugiej. SQL Server obsługuje między innymi INNER, LEFT, RIGHT, FULL OUTER i CROSS JOIN, a dokumentacja Microsoft Learn opisuje ich zachowanie w klauzuli FROM.
MySQL 8.4 ma złączenia LEFT i RIGHT, ale nie oferuje natywnego FULL OUTER JOIN. Dokumentacja MySQL opisuje też różnicę między ON i USING: przy SELECT * ten drugi zapis scala wspólną kolumnę w pojedynczą kolumnę wyniku, więc wynik nie musi mieć takiego samego układu.
Jeśli potrzebujesz pełnego złączenia w MySQL, połącz wynik lewego złączenia z niedopasowanymi rekordami prawej tabeli przez UNION ALL. Warunek IS NULL sprawdzaj na kluczu głównym tabeli lewej, aby dobrać tylko wiersze, których nie znalazł pierwszy fragment.
SELECT a.id AS a_id, b.id AS b_id
FROM A AS a
LEFT JOIN B AS b ON b.a_id = a.id
UNION ALL
SELECT a.id AS a_id, b.id AS b_id
FROM A AS a
RIGHT JOIN B AS b ON b.a_id = a.id
WHERE a.id IS NULL;
Nie przenoś takiego wzorca do innego silnika bez sprawdzenia jego dialektu, czyli odmiany składni SQL obsługiwanej przez konkretną bazę. W PostgreSQL dostępny jest FULL OUTER JOIN, więc ręczne składanie wyniku niepotrzebnie komplikuje zapytanie.
Przed wdrożeniem sprawdź, ile wierszy ma powstać
Przed dodaniem kolejnej tabeli zapisz sobie oczekiwaną liczbę wierszy na poziomie raportu: jeden rekord na zamówienie, na płatność czy na klienta. Potem sprawdź, czy klucz po stronie dołączanej rzeczywiście jest unikalny; jeśli nie jest, wynik może prawidłowo zawierać kilka wierszy dla jednego rekordu nadrzędnego.
Przy raportach finansowych liczę sumę na właściwym poziomie danych, zanim połączę tabele o relacji jeden do wielu. Taki porządek pozwala odróżnić prawdziwe wielokrotne płatności od błędnego złączenia, zanim zawyżona kwota trafi do dalszych obliczeń.
