• Bazy danych
  • FULL OUTER JOIN w SQL - NULL, duplikaty i MySQL

FULL OUTER JOIN w SQL - NULL, duplikaty i MySQL

Tymoteusz Maciejewski • 25 października 2026
Wynik zapytania LEFT JOIN: dane z Tabeli A (produkty) połączone z cenami z Tabeli B, gdzie brakujące ceny oznaczono jako NULL.

Spis treści

Porównujesz raport sprzedaży z eksportem z systemu płatności i część zamówień znika, bo występuje tylko w jednym źródle. FULL OUTER JOIN zachowa wiersze z obu tabel, a brakujące dane uzupełni wartościami NULL, choć wynik może zaskoczyć, gdy klucz łączenia się powtarza.

Pełne złączenie zachowuje dopasowane i niedopasowane wiersze

  • FULL OUTER JOIN zwraca dopasowania oraz wiersze bez pary z obu tabel.
  • Powtórzone klucze mogą zwielokrotnić wynik, nawet gdy każda tabela ma niewiele rekordów.
  • Warunek w WHERE może odrzucić wiersze z wartościami NULL i zmienić efekt złączenia.
  • MySQL nie obsługuje składni FULL OUTER JOIN; da się ją odtworzyć przez LEFT JOIN i UNION ALL.

FULL OUTER JOIN zachowuje rekordy z obu stron

Wyobraź sobie dwa pliki z identyfikatorami zamówień: jeden z aplikacji sklepu, drugi od operatora płatności. FULL OUTER JOIN łączy rekordy, które spełniają warunek złączenia, ale pozostawia też te bez odpowiednika po dowolnej stronie. Brakujące kolumny z drugiej tabeli przyjmują wtedy wartość NULL.

W prostym przykładzie możesz zestawić zamówienia po ich identyfikatorze:

SELECT
    COALESCE(s.order_id, p.order_id) AS order_id,
    s.status AS shop_status,
    p.status AS payment_status
FROM shop_orders AS s
FULL OUTER JOIN payment_orders AS p
    ON s.order_id = p.order_id;

COALESCE zwraca pierwszą wartość różną od NULL, więc kolumna order_id pozostaje widoczna również dla wiersza obecnego tylko w jednej tabeli. Według dokumentacji PostgreSQL pełne złączenie zwraca dopasowane wiersze oraz niedopasowane rekordy z lewej i prawej tabeli. Ten opis brzmi prosto, ale nie gwarantuje jednego wyniku na każdy identyfikator.

Powtórzony klucz może zamienić dwa rekordy w cztery

Przy złączeniu wiele-do-wielu każdy pasujący rekord z jednej tabeli łączy się z każdym pasującym rekordem z drugiej. Jeśli tabela sklepu zawiera dwa wpisy z identyfikatorem 42, a tabela płatności trzy, wynik dla tego identyfikatora będzie miał sześć wierszy, a nie jeden. To często wygląda jak duplikaty wygenerowane przez bazę, choć silnik poprawnie wykonał warunek.

Przed dodaniem agregacji sprawdź, czy kolumna łączenia jest rzeczywiście unikalna po obu stronach. Jeśli płatności mają kilka zdarzeń dla jednego zamówienia, zestawienie warto najpierw sprowadzić do poziomu zamówienia, na przykład wybierając ostatni status albo sumując kwoty. Inaczej suma z raportu może urosnąć przez kombinacje wierszy.

Moim zdaniem najbezpieczniej najpierw ustalić oczekiwaną ziarnistość wyniku: czy jeden wiersz ma oznaczać zamówienie, płatność, czy pojedyncze zdarzenie. Bez tej decyzji łatwo pomylić poprawne mnożenie rekordów z błędem w danych.

Warunek w WHERE potrafi ukryć wiersze bez pary

Załóżmy, że chcesz zachować wszystkie zamówienia, ale uwzględnić tylko płatności o statusie „rozliczona”. Umieszczenie filtra po stronie płatności w WHERE może usunąć wiersze, dla których płatności nie ma, ponieważ ich status jest NULL. W efekcie pełne złączenie przestaje w praktyce zachowywać część niedopasowanych rekordów.

Jeżeli filtr ma ograniczać dopasowywane płatności, przenieś go do warunku ON:

SELECT s.order_id, s.status, p.status
FROM shop_orders AS s
FULL OUTER JOIN payment_orders AS p
    ON s.order_id = p.order_id
   AND p.status = 'rozliczona';

Przy takim zapisie wiersz płatności z innym statusem nie spełni warunku dopasowania, ale nadal może pojawić się jako rekord bez pary po prawej stronie. Jeśli filtr ma celowo odrzucić takie rekordy, WHERE jest właściwym miejscem. Różnica polega na tym, czy warunek definiuje parę, czy filtruje już gotowy wynik.

Objaw w wyniku podpowiada, gdzie szukać błędu

Pełne złączenie często służy do uzgadniania danych, ale samo pojawienie się NULL nie mówi jeszcze, czy rekord nie ma odpowiednika, czy brakowało wartości już w tabeli źródłowej. Tabela pomaga rozdzielić typowe przypadki:

Objaw Możliwa przyczyna Co zrobić
Kolumny jednej tabeli są puste Brak dopasowania albo klucz po tej stronie ma NULL Dodaj wskaźnik obecności rekordu i sprawdź klucz w źródle
Jeden identyfikator występuje w wielu wierszach wyniku Powtarzające się klucze po jednej lub obu stronach Policz wystąpienia przed złączeniem i ustal poziom agregacji
Znikają rekordy bez odpowiednika Warunek w WHERE odrzuca wiersze z NULL Przenieś warunek dopasowania do ON albo zmień filtr
NULL trudno odróżnić od braku dopasowania Kolumna biznesowa może sama zawierać NULL Sprawdź osobną kolumnę oznaczającą obecność rekordu

Najpewniejszy test to nie sprawdzanie dowolnej kolumny biznesowej, lecz stałego identyfikatora, który nie może być pusty, albo dodatkowej flagi obecności. Dokumentacja Microsoft Learn zwraca uwagę, że wartości NULL pochodzące z tabeli mogą być trudne do odróżnienia od tych wstawionych przez złączenie zewnętrzne. W praktyce właśnie dlatego jawny znacznik źródła ułatwia kontrolę raportu.

MySQL wymaga emulacji pełnego złączenia

Składnia z przykładu nie zadziała w każdym silniku. MySQL nie obsługuje FULL OUTER JOIN, co potwierdza jego dokumentacja klauzuli JOIN. W tym systemie można połączyć lewostronne złączenie z rekordami, które występują wyłącznie po prawej stronie:

SELECT
    s.order_id,
    s.status AS shop_status,
    p.status AS payment_status
FROM shop_orders AS s
LEFT JOIN payment_orders AS p
    ON s.order_id = p.order_id

UNION ALL

SELECT
    p.order_id,
    NULL AS shop_status,
    p.status AS payment_status
FROM payment_orders AS p
LEFT JOIN shop_orders AS s
    ON s.order_id = p.order_id
WHERE s.order_id IS NULL;

Druga część wybiera tylko te rekordy płatności, dla których nie znaleziono zamówienia. UNION ALL zachowuje powtórzenia, co odpowiada zachowaniu złączenia, natomiast zwykłe UNION usuwa identyczne wiersze i może po cichu zmienić wynik. Warunek s.order_id IS NULL zakłada, że klucz zamówienia nie jest pusty w dopasowanym rekordzie; jeśli nie możesz tego zagwarantować, użyj innej kolumny, która identyfikuje obecność wiersza.

Wybierz złączenie według pytania, na które odpowiadasz

Jeśli analizujesz kompletność dwóch źródeł, pełne złączenie pokaże także braki po obu stronach. Gdy raport ma obejmować wyłącznie zamówienia ze sklepu, prostszy i czytelniejszy będzie LEFT JOIN; gdy potrzebujesz tylko wspólnych rekordów, wybierz INNER JOIN. Dla samej kontroli różnic często wystarczy pełne złączenie z osobnym wskaźnikiem źródła.

Traktuj NULL jako sygnał do interpretacji, nie jako automatyczny dowód błędu. Najpierw potwierdź warunek łączenia i unikalność kluczy, a dopiero potem poprawiaj dane lub filtr. Dzięki temu w raporcie rozdzielisz brak pary od powielonego dopasowania, zamiast naprawiać efekt uboczny zapytania.

FAQ - Najczęstsze pytania

FULL OUTER JOIN zwraca dopasowane rekordy oraz te bez odpowiednika z lewej i prawej tabeli. Brakujące kolumny przyjmują wartość NULL, ale taki wynik może też oznaczać NULL obecny już w źródle. Aby rozróżnić te przypadki, sprawdź stały identyfikator rekordu albo użyj osobnego wskaźnika obecności.

Przy złączeniu wiele-do-wielu każdy rekord z jednej tabeli łączy się z każdym pasującym rekordem z drugiej. Jeśli klucz 42 występuje dwa razy w tabeli sklepu i trzy razy w tabeli płatności, wynik zawiera sześć wierszy. Przed agregacją sprawdź unikalność kluczy i ustal, czy jeden wiersz ma oznaczać zamówienie, płatność czy zdarzenie; dane można wcześniej sprowadzić do właściwego poziomu, na przykład wybierając ostatni status lub sumując kwoty.

Filtr przenieś do ON, gdy ma ograniczać płatności dopasowywane do zamówień, ale nadal chcesz zachować rekordy bez płatności. Warunek w WHERE może odrzucić wiersze z NULL po stronie płatności i usunąć niedopasowane rekordy. Użyj WHERE, jeśli celowo chcesz odfiltrować takie wiersze z gotowego wyniku.

MySQL nie obsługuje składni FULL OUTER JOIN. Możesz połączyć LEFT JOIN z drugiej strony z pierwszym złączeniem za pomocą UNION ALL, a następnie wybrać rekordy występujące wyłącznie po prawej stronie przez warunek s.order_id IS NULL. UNION ALL zachowuje powtórzenia, ale wskazany warunek zakłada, że klucz zamówienia nie jest pusty w dopasowanym rekordzie.

Oceń artykuł

Ocena: 4.50 Liczba głosów: 6

Tagi

full outer join
null
mysql
union all
agregacja
Autor Tymoteusz Maciejewski
Tymoteusz Maciejewski
Moje dni wypełnia zgłębianie kodu i analiza nowych technologii, zawsze z myślą o tym, jak najlepiej przekazać tę wiedzę dalej. Nazywam się Tymoteusz Maciejewski i od 12 lat aktywnie działam w świecie programowania, technologii i kariery w IT. Interesuje mnie nie tylko samo tworzenie oprogramowania, ale także proces, który prowadzi do zrozumienia złożonych zagadnień i uporządkowania ich w przystępny sposób. Na lukasz-zielinski.pl staram się dzielić się tym, co sam odkrywam, sprawdzając informacje u podstaw i dbając o to, by treści były aktualne i zrozumiałe dla każdego, kto chce rozwijać się w tej dynamicznej branży.

Udostępnij artykuł

Napisz komentarz