Łączenie danych z kilku tabel lub zapytań często wygląda podobnie do zwykłego łączenia tabel, ale działa według zupełnie innej zasady. Operator UNION w SQL, znany też jako sql union, pozwala zestawić wyniki kilku instrukcji SELECT w jeden wspólny wynik. Pokażę, jak działa, czym różni się od UNION ALL, kiedy lepiej użyć JOIN oraz jak unikać błędów związanych z kolumnami, duplikatami i sortowaniem.
Najważniejsze zasady łączenia wyników w SQL
- UNION łączy wyniki kilku zapytań i usuwa powtarzające się wiersze.
- UNION ALL zachowuje duplikaty i zwykle działa szybciej.
- Każde zapytanie musi zwracać taką samą liczbę kolumn w tej samej kolejności.
- Odpowiadające sobie kolumny muszą mieć zgodne lub konwertowalne typy danych.
- ORDER BY dla całego wyniku umieszcza się zazwyczaj na końcu konstrukcji.

Jak działa operator UNION
UNION traktuje wyniki zapytań jak dwa zbiory danych i dokłada wiersze jeden pod drugim. Nie łączy kolumn w poziomie, lecz tworzy jeden większy zestaw rekordów z kilku instrukcji SELECT.
SELECT imie, email
FROM klienci
WHERE miasto = 'Kraków'
UNION
SELECT imie, email
FROM klienci
WHERE miasto = 'Gdańsk';
Wynik zawiera klientów z obu miast. Jeżeli identyczny wiersz pojawi się w obu zapytaniach, standardowe UNION zwróci go tylko raz. Działa to podobnie do zastosowania unikalności całych wierszy, a nie pojedynczej kolumny.
Poszczególne zapytania nie muszą pobierać danych z tej samej tabeli. Istotne jest to, aby ich wyniki dało się ułożyć w ten sam schemat. W praktyce często wykorzystuję tę konstrukcję do połączenia danych z tabel archiwalnych, bieżących albo pochodzących z różnych źródeł.
Podstawowa składnia
SELECT kolumna_1, kolumna_2
FROM tabela_a
UNION
SELECT kolumna_1, kolumna_2
FROM tabela_b;
Można połączyć więcej niż dwa zapytania, dokładając kolejne operatory. Każdy następny SELECT musi jednak zachować ten sam układ kolumn. Nazwy kolumn w końcowym rezultacie zwykle pochodzą z pierwszego zapytania.
UNION czy UNION ALL
Najważniejszy wybór dotyczy tego, czy duplikaty mają zostać usunięte. UNION wykonuje dodatkową operację porównywania wierszy, natomiast UNION ALL zwraca wszystkie rekordy dokładnie tak, jak pojawiły się w zapytaniach źródłowych.
| Operator | Duplikaty | Wydajność | Typowe zastosowanie |
|---|---|---|---|
UNION |
Usuwa identyczne wiersze | Zwykle niższa | Lista unikalnych wartości z wielu źródeł |
UNION ALL |
Zachowuje wszystkie wiersze | Zwykle wyższa | Raporty, agregacje i łączenie pełnych danych |
Przykład z zamówieniami pokazuje różnicę najlepiej. Jeżeli ten sam produkt występuje w dwóch tabelach sprzedażowych i oba rekordy mają pozostać widoczne, użyję UNION ALL.
SELECT produkt, ilosc
FROM sprzedaz_styczen
UNION ALL
SELECT produkt, ilosc
FROM sprzedaz_luty;
Gdybym zastosował zwykły UNION, identyczne wiersze mogłyby zostać zredukowane. To nie zawsze jest korzystne. W raporcie sprzedaży dwa takie same rekordy mogą oznaczać dwie osobne transakcje, a nie błąd do usunięcia.
Moja praktyczna zasada jest prosta: zaczynam od UNION ALL, jeśli nie mam konkretnego powodu do deduplikacji. UNION wybieram świadomie, gdy usunięcie identycznych wierszy jest częścią logiki biznesowej.
Warunki poprawnego połączenia zapytań
Najczęstszy błąd polega na założeniu, że wystarczy pobrać „jakieś” kolumny z obu tabel. SQL wymaga zgodności struktury wyników. Każda część konstrukcji musi zwracać taką samą liczbę kolumn, a odpowiadające sobie pozycje powinny mieć kompatybilne typy.
Liczba i kolejność kolumn
SELECT imie, nazwisko, email
FROM klienci
UNION
SELECT nazwa, email
FROM firmy;
Ten przykład zakończy się błędem, ponieważ pierwsze zapytanie zwraca trzy kolumny, a drugie tylko dwie. Problemem może być również inna kolejność danych. SQL nie dopasowuje kolumn po nazwach, lecz po ich pozycji.
Jeżeli druga tabela przechowuje nazwę firmy i adres kontaktowy, trzeba jawnie dopasować wynik, na przykład przez dodanie wartości NULL:
SELECT imie AS nazwa, email, 'osoba' AS typ
FROM klienci
UNION ALL
SELECT nazwa, email, 'firma' AS typ
FROM kontrahenci;
Dodanie kolumny typ jest bardzo użyteczne. Dzięki niemu wiadomo, skąd pochodzi rekord, nawet gdy oba źródła zostały połączone w jeden wynik. Właśnie takie małe oznaczenie często oszczędza później sporo pracy podczas analizy danych.
Typy danych
Kolumny na tych samych pozycjach nie muszą mieć identycznego typu, ale baza musi móc je bezpiecznie połączyć. Liczba całkowita i liczba dziesiętna zwykle nie są problemem, natomiast zestawienie daty z tekstem albo liczby z niekonwertowalnym tekstem może zakończyć się błędem.
SELECT id, data_zamowienia
FROM zamowienia
UNION ALL
SELECT id, data_reklamacji
FROM reklamacje;
Jeżeli typy są niejednoznaczne, lepiej użyć jawnego CAST niż liczyć na automatyczną konwersję zależną od silnika. Takie podejście zwiększa przenośność zapytania między PostgreSQL, MySQL i SQL Server.
SELECT id, CAST(data_zdarzenia AS DATE) AS data
FROM logi_aplikacji
UNION ALL
SELECT id, CAST(data_zgloszenia AS DATE) AS data
FROM zgloszenia;
Aliasy i wartości stałe
Alias nadany kolumnie w drugim zapytaniu nie zmienia nazwy kolumny w końcowym wyniku. O tym decyduje pierwszy SELECT. Jeśli nazwa ma być czytelna, umieszczam alias właśnie tam.
SELECT nazwa AS element, 'produkt' AS zrodlo
FROM produkty
UNION ALL
SELECT nazwa, 'usluga'
FROM uslugi;
Wartości tekstowe, takie jak 'produkt' i 'usluga', pozwalają zachować informację o pochodzeniu danych. To lepsze rozwiązanie niż próba odgadywania źródła na podstawie samej wartości.
UNION a JOIN czyli dwie różne operacje
UNION i JOIN bywają mylone, bo oba służą do pracy z wieloma tabelami. Różnica jest jednak fundamentalna. Pierwszy operator dokłada wiersze pionowo, a drugi zestawia kolumny z powiązanych rekordów.
| Pytanie | Właściwe rozwiązanie | Co powstaje |
|---|---|---|
| Chcę połączyć klientów z dwóch tabel | UNION |
Więcej wierszy |
| Chcę dodać nazwę miasta do klienta | JOIN |
Więcej kolumn |
| Chcę znaleźć wspólne rekordy |
INTERSECT lub JOIN
|
Rekordy spełniające warunek dopasowania |
Załóżmy, że mamy tabelę klienci oraz zamowienia. Aby pobrać klientów wraz z ich zamówieniami, użyję połączenia:
SELECT k.imie, z.numer_zamowienia
FROM klienci k
JOIN zamowienia z ON z.klient_id = k.id;
Jeśli natomiast chcę stworzyć wspólną listę osób i firm, które mają konta w systemie, lepszy będzie operator zbiorowy:
SELECT nazwa, email
FROM osoby
UNION ALL
SELECT nazwa, email
FROM firmy;
Dobra diagnoza jest prosta. Gdy potrzebuję więcej rekordów, myślę o UNION. Gdy potrzebuję informacji z tego samego rekordu rozłożonych w kilku tabelach, wybieram JOIN. Ta różnica eliminuje sporą część nieporozumień już na etapie projektowania zapytania.
Filtrowanie sortowanie i ograniczanie wyniku
Warunki WHERE zwykle umieszcza się wewnątrz poszczególnych zapytań. Dzięki temu każde źródło może mieć własne kryteria filtrowania.
SELECT imie, email
FROM klienci
WHERE aktywny = 1
UNION ALL
SELECT imie, email
FROM pracownicy
WHERE email IS NOT NULL;
Jeśli filtr ma dotyczyć całego połączonego wyniku, można opakować konstrukcję w zapytanie zewnętrzne. To czytelniejsze niż powtarzanie identycznego warunku w każdej części.
SELECT *
FROM (
SELECT imie, email FROM klienci
UNION ALL
SELECT imie, email FROM pracownicy
) AS osoby
WHERE email LIKE '%@firma.pl';
Sortowanie całego rezultatu wykonuje się zazwyczaj jednym poleceniem ORDER BY na końcu:
SELECT imie, email
FROM klienci
UNION ALL
SELECT imie, email
FROM pracownicy
ORDER BY imie;
Sortowanie wewnątrz pojedynczego zapytania bez odpowiedniego opakowania często nie daje oczekiwanego efektu albo jest odrzucane przez konkretny silnik. Podobnie działa LIMIT lub TOP. Jeśli chcę pobrać pięć rekordów z każdego źródła, ograniczenie musi znaleźć się w obu zapytaniach, zwykle z użyciem nawiasów.
(SELECT imie FROM klienci ORDER BY imie LIMIT 5)
UNION ALL
(SELECT imie FROM pracownicy ORDER BY imie LIMIT 5);
Składnia szczegółowa zależy od bazy. PostgreSQL używa między innymi LIMIT, SQL Server zwykle TOP lub OFFSET ... FETCH, a MySQL również obsługuje LIMIT. Przed przeniesieniem zapytania między systemami sprawdzam właśnie te elementy.
Typowe błędy i wpływ na wydajność
Najbardziej zdradliwy błąd nie zawsze powoduje komunikat. Zapytanie może wykonać się poprawnie, ale zwrócić dane w złym układzie, jeśli kolumny zostały ustawione w niewłaściwej kolejności. Z tego powodu unikam SELECT * w konstrukcjach z UNION.
- Użycie SELECT * może zepsuć zapytanie po dodaniu kolumny do jednej z tabel.
- Niepotrzebny UNION usuwa duplikaty, choć raport powinien zachować wszystkie zdarzenia.
- Brak jawnego CAST może wywołać różne wyniki w różnych silnikach baz danych.
- ORDER BY w złym miejscu sortuje tylko fragment albo powoduje błąd składni.
- Łączenie nieporównywalnych danych utrudnia późniejszą analizę, nawet gdy baza zaakceptuje konwersję.
Wydajność zależy przede wszystkim od ilości danych i sposobu usuwania duplikatów. UNION może wymagać sortowania albo budowy struktury pomocniczej do sprawdzenia unikalności, więc przy milionach wierszy różnica względem UNION ALL potrafi być odczuwalna.
Nie oznacza to jednak, że zawsze należy wybierać szybszy wariant. Jeżeli wynik ma przedstawiać unikalne osoby, produkty lub identyfikatory, usunięcie duplikatów jest częścią poprawności, a nie optymalizacji. Najpierw ustalam znaczenie danych, a dopiero później patrzę na plan wykonania i indeksy.
Przeczytaj również: DDL w SQL - Jak bezpiecznie zmieniać strukturę bazy danych?
Jak poprawić zapytanie
W praktyce pomaga kilka prostych nawyków. Jawnie wypisuję kolumny, filtruję dane możliwie wcześnie i stosuję UNION ALL, gdy rekordy z różnych źródeł mają zachować swoją pełną liczność.
- Sprawdź liczbę i kolejność kolumn w każdym
SELECT. - Porównaj typy danych i dodaj
CAST, jeśli konwersja nie jest oczywista. - Zdecyduj, czy duplikat oznacza błąd, czy osobne zdarzenie.
- Przenieś wspólne filtrowanie do zapytania zewnętrznego, jeśli poprawia czytelność.
- Przy dużych tabelach sprawdź plan wykonania i rzeczywisty czas działania.
Jak wykorzystać UNION w codziennej pracy
Najbardziej praktyczne zastosowanie to łączenie tabel o podobnej strukturze, na przykład danych bieżących i archiwalnych. Takie rozwiązanie bywa przydatne w raportach, migracjach, konsolidacji danych oraz podczas budowania warstwy analitycznej.
SELECT id, kwota, data_platnosci, 'bieżące' AS zrodlo
FROM platnosci
UNION ALL
SELECT id, kwota, data_platnosci, 'archiwalne' AS zrodlo
FROM platnosci_archiwum;
Kolumna zrodlo jest tutaj ważniejsza, niż może się wydawać. Pozwala szybko wykryć, z której tabeli pochodzi rekord, a później porównać liczbę płatności lub sumy między źródłami.
Można też połączyć wyniki o różnym pochodzeniu, o ile przygotuje się wspólny format:
SELECT nazwa, cena, 'produkt' AS kategoria
FROM produkty
UNION ALL
SELECT nazwa, stawka AS cena, 'usługa' AS kategoria
FROM uslugi;
Nie próbowałbym jednak używać tego operatora jako sposobu na ukrywanie złego modelu danych. Jeżeli regularnie łączysz kilkanaście tabel o podobnej strukturze, być może lepszym rozwiązaniem będzie wspólna tabela, widok albo partycjonowanie. UNION jest świetnym narzędziem integracyjnym, ale nie zastąpi przemyślanego schematu bazy.
Świadomy wybór operatora zaczyna się od znaczenia wiersza
Najkrócej mówiąc, UNION buduje wspólny wynik z kilku zapytań, a UNION ALL robi to bez usuwania duplikatów. Właściwy wybór zależy od tego, czy dwa identyczne wiersze opisują ten sam fakt, czy dwa niezależne zdarzenia.
Przed uruchomieniem zapytania sprawdzam trzy rzeczy: układ kolumn, sens duplikatów i oczekiwaną kolejność danych. Ten krótki test zwykle daje więcej niż późniejsze poprawianie raportu, który wygląda poprawnie, ale zgubił część rekordów.
Jeżeli dane pochodzą z różnych źródeł, dodaję kolumnę opisującą ich pochodzenie i jawnie określam typy. Dzięki temu operator pozostaje prosty, wynik jest łatwiejszy do kontroli, a zapytanie można bezpieczniej wykorzystać także z poziomu aplikacji napisanej w Pythonie.
