SQL JOIN w praktyce - dobierz złączenie i uniknij błędów

Bruno Krawczyk • 20 października 2026
Próba ataku SQL injection UNION w celu znalezienia kolumny z tekstem. Wpisany kod: '/?category=Pets' union select 'abc', null, null -- -

Spis treści

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ń.

FAQ - Najczęstsze pytania

Jeśli jedno zamówienie ma kilka pasujących płatności, LEFT JOIN powieli jego wiersz dla każdej wpłaty. W przykładzie suma zamówień rośnie z 600 zł do 700 zł. Sprawdź liczbę dopasowań i, jeśli raport ma mieć jeden wiersz na zamówienie, agreguj płatności przed złączeniem.

Umieść warunek p.status = 'zaksięgowana' w klauzuli ON, aby ograniczył dopasowania, ale pozostawił zamówienia bez takiej płatności. Umieszczenie go w WHERE odrzuci wiersze, w których kolumny płatności mają wartość NULL.

Użyj LEFT JOIN, a następnie sprawdź w WHERE, czy klucz główny płatności, na przykład p.id, ma wartość NULL. Wybierz kolumnę, która nie może być pusta w istniejącym rekordzie płatności.

MySQL 8.4 nie obsługuje natywnie FULL OUTER JOIN. Możesz połączyć wynik LEFT JOIN z niedopasowanymi rekordami z prawej tabeli przez UNION ALL, wybierając je warunkiem a.id IS NULL. Sprawdź dialekt bazy, bo na przykład PostgreSQL udostępnia FULL OUTER JOIN.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

join
mysql
sql server
agregacja
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