Gdy porównujesz dane z dwóch tabel i nie chcesz zgubić rekordów występujących tylko po jednej stronie, zwykły INNER JOIN szybko okazuje się niewystarczający. W SQL full join pozwala zachować wszystkie wiersze z obu źródeł, a brakujące dopasowania oznacza wartościami NULL. Pokażę, jak działa to złączenie, kiedy ma sens, czym różni się od innych wariantów oraz jak uniknąć typowych błędów.
Pełne złączenie zachowuje dane z obu tabel
- Zwraca wszystkie wiersze z lewej i prawej tabeli.
- Dla niedopasowanych rekordów wstawia NULL po brakującej stronie.
- Najlepiej sprawdza się przy porównywaniu zestawów danych i wykrywaniu braków.
- Warunek WHERE może nieświadomie zmienić je w złączenie częściowe.
- W MySQL trzeba użyć alternatywy, ponieważ silnik nie obsługuje natywnego FULL OUTER JOIN.

Jak działa pełne złączenie tabel
Pełne złączenie, nazywane też FULL OUTER JOIN, łączy dwa zbiory według wskazanego warunku, ale nie usuwa rekordów bez pary. Wynik zawiera zarówno wiersze dopasowane, jak i te, które istnieją wyłącznie w tabeli po lewej albo po prawej stronie.
Wyobraźmy sobie dwie tabele z zamówieniami. W pierwszej znajdują się zamówienia z 2025 roku, a w drugiej zamówienia z 2026 roku. Po połączeniu po identyfikatorze klienta zobaczymy klientów obecnych w obu okresach, nowych klientów oraz tych, którzy nie złożyli zamówienia w nowszym zestawieniu.
| klienci_2025 | klienci_2026 | Wynik |
|---|---|---|
| Anna, ID 1 | Anna, ID 1 | Dopasowany rekord |
| Piotr, ID 2 | brak | NULL po stronie tabeli z 2026 roku |
| brak | Maria, ID 3 | NULL po stronie tabeli z 2025 roku |
To właśnie odróżnia ten wariant od zwykłego złączenia wewnętrznego. INNER JOIN pokazuje tylko wspólną część danych, natomiast pełne złączenie pokazuje także rozbieżności, które często są najcenniejszą informacją podczas analizy.
Jak zapisać zapytanie i poprawnie odczytać wynik
Podstawowa składnia jest prosta. W klauzuli ON określamy kolumny, które mają wskazywać odpowiadające sobie rekordy.
SELECT
COALESCE(a.customer_id, b.customer_id) AS customer_id,
a.total AS total_2025,
b.total AS total_2026
FROM orders_2025 AS a
FULL OUTER JOIN orders_2026 AS b
ON a.customer_id = b.customer_id;
Jeżeli klient występuje w obu tabelach, otrzymamy jeden wiersz z dwiema wartościami sprzedaży. Gdy pojawia się wyłącznie w jednej tabeli, kolumna po drugiej stronie będzie miała wartość NULL. Funkcja COALESCE wybiera pierwszą niepustą wartość, dlatego pozwala pokazać identyfikator bez powielania go w raporcie.
Trzeba uważać na kolumny użyte w warunku. Jeśli połączysz tabele po nazwie klienta zamiast po stabilnym identyfikatorze, drobna różnica w zapisie może stworzyć pozornie brakujące rekordy. W praktyce preferuję klucze główne i klucze obce, bo ograniczają ryzyko błędnego dopasowania.
Przykład wykrywania różnic między tabelami
Pełne złączenie dobrze nadaje się do kontroli migracji danych. Załóżmy, że chcesz sprawdzić, czy katalog produktów w systemie produkcyjnym odpowiada katalogowi w środowisku testowym.
SELECT
COALESCE(prod.product_id, test.product_id) AS product_id,
prod.name AS production_name,
test.name AS test_name
FROM products_production AS prod
FULL OUTER JOIN products_test AS test
ON prod.product_id = test.product_id
WHERE prod.product_id IS NULL
OR test.product_id IS NULL
OR prod.name <> test.name;
Takie zapytanie zwróci produkty brakujące po jednej stronie oraz te, których nazwa się różni. To praktyczny wzorzec do audytu, migracji i synchronizacji danych, a nie tylko akademicki przykład składni.
Czym różni się od INNER, LEFT i RIGHT JOIN
Najłatwiej zrozumieć działanie pełnego złączenia przez porównanie go z pozostałymi podstawowymi wariantami. Różnica sprowadza się do tego, które niedopasowane rekordy zostają zachowane.
| Rodzaj złączenia | Co zwraca | Typowe zastosowanie |
|---|---|---|
| INNER JOIN | Tylko rekordy obecne w obu tabelach | Praca na wspólnej części danych |
| LEFT JOIN | Wszystkie rekordy z lewej tabeli i dopasowania z prawej | Lista klientów wraz z opcjonalnymi zamówieniami |
| RIGHT JOIN | Wszystkie rekordy z prawej tabeli i dopasowania z lewej | Analiza, gdy ważniejszym źródłem jest tabela prawa |
| FULL OUTER JOIN | Wszystkie rekordy z obu tabel | Porównywanie źródeł i wyszukiwanie braków |
Jeżeli interesuje Cię wyłącznie część wspólna, pełne złączenie będzie niepotrzebnie szerokie. Gdy jednak raport ma odpowiedzieć na pytanie „co występuje tylko tutaj, a czego brakuje tam?”, FULL OUTER JOIN jest najbardziej bezpośrednim narzędziem.
NULL, filtry i duplikaty mogą zmienić wynik
Najczęstszy błąd polega na odfiltrowaniu wartości NULL w klauzuli WHERE. Przykładowo warunek WHERE b.status = 'aktywny' usunie rekordy, które nie mają dopasowania w tabeli b, ponieważ porównanie z NULL nie zwraca wartości TRUE.
SELECT a.id, b.status
FROM table_a AS a
FULL OUTER JOIN table_b AS b
ON a.id = b.id
WHERE b.status = 'aktywny';
Tak zapisane zapytanie w praktyce przestaje zachowywać część rekordów z lewej strony. Bezpieczniej przenieść warunek do klauzuli ON albo jawnie obsłużyć brak wartości, na przykład przez WHERE b.status = 'aktywny' OR b.id IS NULL.
Przeczytaj również: SQL Self Join - Jak łączyć tabelę z samą sobą?
Uważaj na relacje jeden do wielu
Jeśli jeden klient ma trzy zamówienia w pierwszej tabeli i dwa w drugiej, połączenie po samym customer_id może utworzyć nawet sześć wierszy. Nie jest to błąd silnika, lecz efekt dopasowania każdego rekordu z jednej strony do każdego pasującego rekordu z drugiej.
Przed użyciem złączenia sprawdzam, czy kolumna łącząca jest unikalna. Jeśli nie jest, najpierw agreguję dane w podzapytaniu lub CTE, a dopiero potem wykonuję połączenie. To zwykle daje czytelniejszy wynik i ogranicza niekontrolowane zwielokrotnienie rekordów.
Kiedy używać pełnego złączenia i co zrobić w MySQL
Największą wartość widzę w trzech sytuacjach: porównywaniu dwóch wersji danych, wykrywaniu rekordów utraconych podczas migracji oraz budowaniu raportu pokazującego pełny obraz z dwóch niezależnych źródeł. Nie używałbym go jednak automatycznie w każdej analizie, bo wynik może być większy i trudniejszy do interpretacji niż rezultat LEFT JOIN.
- Audyt danych - sprawdzenie, które rekordy różnią się między systemami.
- Synchronizacja - znalezienie elementów nowych, usuniętych i zmienionych.
- Raporty okresowe - porównanie sprzedaży, użytkowników lub produktów między okresami.
- Integracje - połączenie danych z dwóch źródeł, z których żadne nie jest nadrzędne.
PostgreSQL i SQL Server obsługują składnię FULL OUTER JOIN. MySQL nie udostępnia tego operatora natywnie, dlatego trzeba odtworzyć podobny efekt za pomocą połączenia lewostronnego i operatora UNION.
SELECT a.id, a.name, b.name AS other_name
FROM table_a AS a
LEFT JOIN table_b AS b
ON a.id = b.id
UNION
SELECT b.id, a.name, b.name AS other_name
FROM table_b AS b
LEFT JOIN table_a AS a
ON a.id = b.id;
UNION usuwa identyczne duplikaty, natomiast UNION ALL zachowuje wszystkie wiersze. Wybór zależy od danych, ale nie traktowałbym tych operatorów jako zamienników bez sprawdzenia, czy oba zapytania nie zwracają powtórzeń.
Przy dużych tabelach znaczenie ma indeks na kolumnach użytych w warunku ON. Sam indeks nie rozwiąże każdego problemu, lecz brak indeksowania, szerokie kolumny i niepotrzebne SELECT * mogą wyraźnie zwiększyć czas wykonania oraz zużycie pamięci.
Pełne złączenie pomaga zobaczyć dane bez luk
Najprostsza zasada brzmi tak: wybierz ten wariant, gdy potrzebujesz rekordów wspólnych i jednostronnych jednocześnie. Jeśli zależy Ci tylko na dopasowaniach, użyj INNER JOIN, a gdy jedna tabela ma być źródłem nadrzędnym, zwykle wystarczy LEFT JOIN.
Przed uruchomieniem zapytania sprawdź trzy rzeczy: czy kolumna łącząca jest właściwa, czy może zawierać duplikaty oraz czy filtr nie usuwa wartości NULL. Te trzy kontrole robią większą różnicę niż sama znajomość składni, szczególnie gdy złączenie zasila raport lub proces migracji.
Dobrze użyty FULL OUTER JOIN nie służy tylko do łączenia tabel. To także proste narzędzie do znajdowania braków, niespójności i różnic, które w codziennej pracy z bazą danych łatwo przeoczyć.