Masz tabelę klientów i chcesz pokazać także tych, którzy nigdy nie złożyli zamówienia? Właśnie w takich sytuacjach przydaje się left join, czyli złączenie zachowujące wszystkie rekordy z tabeli znajdującej się po lewej stronie zapytania. Pokażę, jak działa ta konstrukcja, czym różni się od innych rodzajów JOIN oraz jak uniknąć błędów z filtrami, wartościami NULL i powielonymi wierszami.
Najważniejsze informacje o złączeniu lewostronnym
- Wszystkie rekordy z lewej tabeli pozostają w wyniku, nawet bez dopasowania.
- Brakujący rekord po prawej stronie jest oznaczany jako NULL.
- Warunki dotyczące prawej tabeli często trzeba umieszczać w ON, a nie w WHERE.
- Jedno dopasowanie do wielu rekordów może spowodować powielenie wierszy.
- Złączenie sprawdza się przy raportach, listach brakujących danych i relacjach opcjonalnych.

Jak działa złączenie lewostronne
Załóżmy, że mamy tabelę klienci oraz tabelę zamowienia. Tabela klientów jest po lewej stronie, więc wynik zachowa każdego klienta. Jeżeli konkretny klient nie ma zamówienia, kolumny pobrane z drugiej tabeli przyjmą wartość NULL.
SELECT
k.id,
k.imie,
z.data_zamowienia
FROM klienci AS k
LEFT OUTER JOIN zamowienia AS z
ON z.klient_id = k.id;
To ważne rozróżnienie. Złączenie nie mówi, że obie tabele muszą mieć pasujące rekordy. Ono mówi, że lewa tabela jest nadrzędna dla wyniku, a prawa dostarcza dodatkowe informacje, jeśli uda się je dopasować.
| Klient | Zamówienie | Wynik |
|---|---|---|
| Anna | Istnieje | Anna i dane zamówienia |
| Piotr | Brak | Piotr i NULL po stronie zamówienia |
Sam zapis jest prosty, ale kolejność tabel ma znaczenie. Zamiana ich miejscami zmieni rezultat, ponieważ wtedy zachowane zostaną wszystkie rekordy z innego źródła.
Składnia i praktyczny przykład
Podstawowa konstrukcja składa się z tabeli bazowej, słowa JOIN oraz warunku ON. Warunek określa, po jakich kolumnach mają zostać połączone rekordy, najczęściej po kluczu głównym i kluczu obcym.
SELECT
p.nazwa,
k.nazwa AS kategoria
FROM produkty AS p
LEFT OUTER JOIN kategorie AS k
ON p.kategoria_id = k.id;
Ten raport pokaże także produkty bez przypisanej kategorii. To częsty przypadek w panelach administracyjnych, ponieważ pozwala szybko znaleźć niekompletne dane, zamiast ukrywać je za pomocą zwykłego złączenia wewnętrznego.
Przeczytaj również: SQL Self Join - Jak łączyć tabelę z samą sobą?
Wyszukiwanie rekordów bez dopasowania
Jedno z najbardziej użytecznych zastosowań polega na znalezieniu rekordów, dla których nie istnieje powiązanie w drugiej tabeli. W tym celu sprawdzam kolumnę pochodzącą z prawej strony i wyszukuję wartość NULL.
SELECT
k.id,
k.imie
FROM klienci AS k
LEFT OUTER JOIN zamowienia AS z
ON z.klient_id = k.id
WHERE z.id IS NULL;
Wynik zawiera klientów bez zamówień. Trzeba użyć IS NULL, a nie = NULL, ponieważ NULL oznacza brak znanej wartości i nie porównuje się go zwykłym operatorem równości.
NULL, wiele dopasowań i powielone wiersze
Brak dopasowania nie oznacza pustego tekstu ani zera. SQL zwraca NULL, dlatego aplikacja powinna obsłużyć ten stan osobno, na przykład wyświetlając komunikat „brak zamówień” albo zastępując wartość funkcją COALESCE.
SELECT
k.imie,
COALESCE(z.liczba, 0) AS liczba_zamowien
FROM klienci AS k
LEFT OUTER JOIN statystyki_klientow AS z
ON z.klient_id = k.id;
Druga pułapka pojawia się wtedy, gdy jeden rekord z lewej tabeli pasuje do wielu rekordów z prawej. Klient z pięcioma zamówieniami pojawi się w wyniku pięć razy. To prawidłowe zachowanie, ale może zaskoczyć przy liczeniu klientów lub wyświetlaniu listy.
Jeżeli potrzebuję jednego wiersza na klienta, zwykle agreguję dane albo stosuję COUNT i GROUP BY.
SELECT
k.id,
k.imie,
COUNT(z.id) AS liczba_zamowien
FROM klienci AS k
LEFT OUTER JOIN zamowienia AS z
ON z.klient_id = k.id
GROUP BY k.id, k.imie;
W tym przykładzie COUNT(z.id) zwróci zero dla klienta bez zamówień. Użycie COUNT(*) mogłoby dać mylący rezultat, ponieważ policzyłoby także wiersz zachowany przez złączenie, mimo że po prawej stronie nie ma rzeczywistego zamówienia.
Dlaczego filtr w WHERE może zmienić wynik
Najczęstszy błąd polega na filtrowaniu prawej tabeli w klauzuli WHERE. Przykładowo poniższe zapytanie zachowa tylko klientów z zamówieniami z 2026 roku, więc rekordy bez zamówienia znikną z wyniku.
SELECT
k.imie,
z.status
FROM klienci AS k
LEFT OUTER JOIN zamowienia AS z
ON z.klient_id = k.id
WHERE z.status = 'opłacone';
Dla klienta bez zamówienia warunek z.status = 'opłacone' nie jest spełniony, bo status ma wartość NULL. W efekcie zapytanie zaczyna działać podobnie do złączenia wewnętrznego.
Jeżeli chcę zachować wszystkich klientów, a jednocześnie dołączyć tylko opłacone zamówienia, przenoszę warunek do ON.
SELECT
k.imie,
z.status
FROM klienci AS k
LEFT OUTER JOIN zamowienia AS z
ON z.klient_id = k.id
AND z.status = 'opłacone';
To drobna zmiana, ale ma duże znaczenie. Warunki w ON ograniczają dopasowanie prawej tabeli, natomiast warunki w WHERE filtrują gotowy wynik.
Porównanie z innymi rodzajami JOIN
Dobór rodzaju złączenia zależy od tego, które rekordy mają przetrwać bez dopasowania. Ja zaczynam od pytania, czy brak relacji ma być pokazany użytkownikowi, czy pominięty.
| Rodzaj złączenia | Co zachowuje | Typowe zastosowanie |
|---|---|---|
INNER JOIN |
Tylko rekordy z dopasowaniem po obu stronach | Raporty obejmujące wyłącznie kompletne relacje |
LEFT OUTER JOIN |
Wszystkie rekordy z lewej tabeli | Lista klientów, produktów lub użytkowników z opcjonalnymi danymi |
RIGHT OUTER JOIN |
Wszystkie rekordy z prawej tabeli | Rzadziej używana alternatywa zależna od układu zapytania |
FULL OUTER JOIN |
Rekordy z obu tabel, także bez dopasowania | Porównywanie dwóch niezależnych zbiorów danych |
W praktyce złączenie prawe często można zastąpić lewym, zamieniając kolejność tabel. Dzięki temu wiele zespołów trzyma się jednej, czytelniejszej konwencji. Z kolei wariant pełny nie jest dostępny w każdej bazie danych, dlatego przed użyciem trzeba sprawdzić możliwości konkretnego silnika.
Wydajność i kontrola poprawności zapytania
Samo złączenie lewostronne nie musi być wolne. Największą różnicę robią zwykle indeksy na kolumnach używanych w relacji, zwłaszcza na kluczu obcym tabeli po prawej stronie.
Przed optymalizacją sprawdzam kilka rzeczy:
- czy kolumny użyte w
ONmają zgodne typy danych, - czy po prawej stronie nie ma niepotrzebnie wielu rekordów,
- czy zapytanie zwraca oczekiwaną liczbę wierszy,
- czy filtr powinien znaleźć się w
ON, czy wWHERE, - czy agregacja nie ukrywa powielonych rekordów.
Do diagnozy używam planu wykonania, na przykład przez EXPLAIN. Nie zakładam z góry, że dodanie indeksu rozwiąże każdy problem. Przy małych tabelach koszt jego utrzymania może być większy niż korzyść, a przy dużych zbiorach znaczenie ma także selektywność warunków i kolejność operacji.
Jak nie zgubić sensu wyniku po złączeniu
Najbezpieczniej najpierw ustalić, która tabela ma być kompletna w raporcie. Jeżeli odpowiedź brzmi „każdy klient”, „każdy produkt” albo „każdy użytkownik”, ten zbiór powinien znaleźć się po lewej stronie.
Potem testuję osobno trzy przypadki: rekord z dopasowaniem, rekord bez dopasowania oraz rekord z wieloma dopasowaniami. Taki prosty zestaw kontrolny szybko ujawnia problemy z NULL, filtrowaniem i nieoczekiwanym powieleniem danych.
Dobrze napisane złączenie nie polega więc tylko na znajomości składni. Trzeba jeszcze świadomie określić, które rekordy mają zostać zachowane, gdzie umieścić warunki i jak interpretować wynik w aplikacji.