LEFT JOIN w SQL bez pułapek z NULL i filtrami

Sekwencje zer i jedynek, z wyróżnioną zieloną linią. Jak w bazie danych, gdzie `left join` łączy dane.

Napisano przez

Tymoteusz Sobczak

Opublikowano

30 wrz 2026

Spis treści

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.

Wynik operacji left join z danymi zawierającymi NULL. NULL = NULL daje UNKNOWN, nie TRUE.

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 ON mają 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 w WHERE,
  • 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.

FAQ - Najczęstsze pytania

Połącz tabelę klienci z zamowienia przez LEFT JOIN, a następnie dodaj warunek WHERE z.id IS NULL. Otrzymasz klientów, dla których nie istnieje dopasowany rekord zamówienia. Nie używaj zapisu = NULL, ponieważ NULL wymaga operatora IS NULL.

Warunek w WHERE filtruje gotowy wynik, więc zapis WHERE z.status = 'opłacone' usunie także klientów bez zamówienia. Jeśli chcesz zachować wszystkich klientów i dołączyć tylko opłacone zamówienia, umieść ten warunek w ON. Wtedy ograniczasz dopasowania prawej tabeli, ale nie usuwasz rekordów z lewej.

Jeśli jeden rekord z lewej tabeli pasuje do wielu rekordów po prawej, zostanie powtórzony dla każdego dopasowania. Klient z pięcioma zamówieniami pojawi się więc pięć razy. Aby uzyskać jeden wiersz na klienta, użyj agregacji, na przykład COUNT(z.id) wraz z GROUP BY.

INNER JOIN zachowuje wyłącznie rekordy z dopasowaniem po obu stronach. LEFT JOIN zachowuje wszystkie rekordy lewej tabeli, a brakujące dane z prawej oznacza jako NULL. FULL OUTER JOIN zachowuje rekordy z obu tabel, także te bez dopasowania, ale nie każdy silnik bazy danych go obsługuje.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi:

left join null indeksy agregacja klucze obce

Udostępnij artykuł

Tymoteusz Sobczak

Tymoteusz Sobczak

Nazywam się Tymoteusz Sobczak i od 7 lat zajmuję się programowaniem webowym. Moje zainteresowanie tą dziedziną zaczęło się od prostych projektów, które realizowałem w wolnym czasie. Z czasem odkryłem, jak fascynujące jest tworzenie aplikacji, które mogą ułatwiać życie innym. Chętnie dzielę się swoją wiedzą, pomagając czytelnikom zrozumieć złożone zagadnienia związane z programowaniem, od podstawowych technik po bardziej zaawansowane rozwiązania. Pisząc na temat programowania, staram się dostarczać informacje, które są nie tylko użyteczne, ale także zrozumiałe. Zawsze dokładam starań, aby moje źródła były wiarygodne, a treści aktualne. Lubię upraszczać trudne tematy i organizować wiedzę w sposób, który ułatwia naukę. Wierzę, że każdy, kto ma chęci, może stać się dobrym programistą, a ja jestem tutaj, aby wskazać drogę.

Napisz komentarz