UNION, INTERSECT, EXCEPT w SQL


Ważne: to nie jest JOIN.
JOIN łączy tabele „w bok” (dokłada kolumny).
Operacje zbiorowe łączą wyniki „w dół” (dokładają wiersze).

1) Co to są operacje zbiorowe w SQL?

Operacje zbiorowe działają tak, jak w matematyce na zbiorach/licie danych:

  • masz wynik zapytania A (np. lista e-maili uczniów),
  • masz wynik zapytania B (np. lista e-maili nauczycieli),
  • i chcesz:
    • połączyć te listy (UNION),
    • znaleźć część wspólną (INTERSECT),
    • albo odjąć jedną listę od drugiej (EXCEPT).

W SQL wygląda to tak, że piszesz kilka SELECT, a między nimi wstawiasz operator.

2) UNION — wyniki jako jedna lista

UNION (bez duplikatów)

UNION łączy wyniki dwóch (lub więcej) zapytań i usuwa powtórki.

Czyli: jeśli ten sam wiersz pojawi się w A i w B, to w wyniku będzie tylko raz.

SELECT kolumny
FROM ...

UNION

SELECT kolumny
FROM ...;

UNION ALL (z duplikatami)

UNION ALL robi to samo, ale nie usuwa duplikatów.

Często jest szybszy i bywa po prostu wygodniejszy, jeśli powtórki Ci nie przeszkadzają albo chcesz je zobaczyć.

SELECT kolumny
FROM ...

UNION ALL

SELECT kolumny
FROM ...;

3) INTERSECT — pokazuje tylko to, co jest w obu wynikach

INTERSECT to część wspólna: zostają tylko te wiersze, które występują jednocześnie w A i w B.

Brzmi super, ale… musimy być szczerzy:

Ważne o MySQL / MariaDB (phpMyAdmin)

  • UNION działa normalnie w MySQL/MariaDB.
  • INTERSECT i EXCEPT w wielu szkolnych instalacjach MySQL/MariaDB nie działają (bo nie są obsługiwane albo są wyłączone / zależy od wersji).

Dlatego na lekcjach i egzaminach często uczy się:

  • idei INTERSECT,
  • ale wykonania w MySQL przez INEXISTSJOIN.

I to jest naprawdę praktyczne, bo działa zawsze.

4) EXCEPT — „A bez B” (różnica zbiorów)

EXCEPT zwraca wiersze, które są w A, ale nie ma ich w B.

Podobnie jak przy INTERSECT:

  • w MySQL/MariaDB często nie ma EXCEPT,
  • więc robimy to przez NOT EXISTSNOT IN albo czasem LEFT JOIN ... IS NULL (ale na tej stronie skupimy się na NOT EXISTS, bo jest najpewniejsze).

5) Najważniejsze zasady składni (to się myli najczęściej)

Zasada 1: ta sama liczba kolumn

Każdy SELECT w UNION/INTERSECT/EXCEPT musi zwrócić dokładnie tyle samo kolumn.

Czyli to zadziała:

SELECT email FROM uczniowie
UNION
SELECT email FROM nauczyciele;

A to nie:

SELECT id, email FROM uczniowie
UNION
SELECT email FROM nauczyciele; -- błąd: różna liczba kolumn

Zasada 2: pasujące typy danych

Kolumny powinny mieć sensownie zgodne typy.
Np. nie mieszaj liczb z datą albo tekstu z ceną (chyba że wiesz, co robisz i użyjesz CAST).

Zasada 3: kolejność kolumn ma znaczenie

SQL dopasowuje kolumny po kolejności, nie po nazwach.

Zasada 4: ORDER BY dajemy na końcu

Jeśli chcesz sortować wynik, ORDER BY dajesz na końcu całej konstrukcji:

SELECT email FROM uczniowie
UNION
SELECT email FROM nauczyciele
ORDER BY email;
BAZA DO CWICZEŃ
DROP DATABASE IF EXISTS szkolenie_sql;
CREATE DATABASE szkolenie_sql
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_general_ci;

USE szkolenie_sql;

-- UCZNIOWIE
CREATE TABLE uczniowie (
  id INT PRIMARY KEY AUTO_INCREMENT,
  imie VARCHAR(40) NOT NULL,
  nazwisko VARCHAR(60) NOT NULL,
  email VARCHAR(120) NOT NULL UNIQUE,
  klasa VARCHAR(10) NOT NULL
);

INSERT INTO uczniowie (imie, nazwisko, email, klasa) VALUES
('Ala', 'Nowak', 'ala.nowak@szkola.pl', '2A'),
('Bartek', 'Kowalski', 'bartek.kowalski@szkola.pl', '2A'),
('Celina', 'Zielińska', 'celina.zielinska@szkola.pl', '3B'),
('Damian', 'Wiśniewski', 'damian.wisniewski@szkola.pl', '3B');

-- NAUCZYCIELE
CREATE TABLE nauczyciele (
  id INT PRIMARY KEY AUTO_INCREMENT,
  imie VARCHAR(40) NOT NULL,
  nazwisko VARCHAR(60) NOT NULL,
  email VARCHAR(120) NOT NULL UNIQUE,
  przedmiot VARCHAR(60) NOT NULL
);

INSERT INTO nauczyciele (imie, nazwisko, email, przedmiot) VALUES
('Ewa', 'Maj', 'ewa.maj@szkola.pl', 'Informatyka'),
('Filip', 'Lis', 'filip.lis@szkola.pl', 'Matematyka'),
-- celowo: ten sam e-mail co uczeń, żeby było widać sens INTERSECT/duplikatów
('Celina', 'Zielińska', 'celina.zielinska@szkola.pl', 'Zajęcia dodatkowe');

-- KURSY
CREATE TABLE kursy (
  id INT PRIMARY KEY AUTO_INCREMENT,
  nazwa VARCHAR(80) NOT NULL
);

INSERT INTO kursy (nazwa) VALUES
('SQL od podstaw'),
('Python podstawy'),
('Sieci komputerowe');

-- ZAPISY UCZNIÓW NA KURSY (relacja wiele-do-wielu)
CREATE TABLE zapisy_uczniow (
  uczen_id INT NOT NULL,
  kurs_id INT NOT NULL,
  PRIMARY KEY (uczen_id, kurs_id),
  FOREIGN KEY (uczen_id) REFERENCES uczniowie(id),
  FOREIGN KEY (kurs_id) REFERENCES kursy(id)
);

INSERT INTO zapisy_uczniow (uczen_id, kurs_id) VALUES
(1, 1), -- Ala -> SQL
(2, 1), -- Bartek -> SQL
(3, 2), -- Celina -> Python
(4, 3); -- Damian -> Sieci

-- KLIENCI (żeby pokazać przykład "sklepowy")
CREATE TABLE klienci (
  id INT PRIMARY KEY AUTO_INCREMENT,
  nazwa VARCHAR(120) NOT NULL,
  email VARCHAR(120) NOT NULL UNIQUE
);

INSERT INTO klienci (nazwa, email) VALUES
('Firma Alfa', 'kontakt@alfa.pl'),
('Firma Beta', 'kontakt@beta.pl'),
('Osoba Prywatna', 'celina.zielinska@szkola.pl'); -- celowo ten sam mail co wcześniej

-- ZAMOWIENIA
CREATE TABLE zamowienia (
  id INT PRIMARY KEY AUTO_INCREMENT,
  klient_id INT NOT NULL,
  data_zamowienia DATETIME NOT NULL,
  status ENUM('nowe','oplacone','wyslane','anulowane') NOT NULL,
  FOREIGN KEY (klient_id) REFERENCES klienci(id)
);

INSERT INTO zamowienia (klient_id, data_zamowienia, status) VALUES
(1, '2026-05-01 10:00:00', 'oplacone'),
(2, '2026-05-03 12:30:00', 'nowe'),
(1, '2026-05-10 09:15:00', 'wyslane');

Przykłady UNION I UNION ALL

Przykład 1 (UNION): jedna lista maili: uczniowie + nauczyciele (bez powtórek)

SELECT email
FROM uczniowie

UNION

SELECT email
FROM nauczyciele
ORDER BY email;

Co się dzieje krok po kroku?

  1. Pierwszy SELECT bierze wszystkie maile uczniów.
  2. Drugi SELECT bierze wszystkie maile nauczycieli.
  3. UNION skleja te wyniki w jedną kolumnę.
  4. Jeśli jakiś mail powtórzył się w obu tabelach (u nas celina.zielinska@szkola.pl) → w wyniku będzie tylko raz.
  5. ORDER BY sortuje całość.

Przykład 2 (UNION ALL): to samo, ale chcemy widzieć powtórki

SELECT email
FROM uczniowie

UNION ALL

SELECT email
FROM nauczyciele
ORDER BY email;

Różnica w praktyce:
celina.zielinska@szkola.pl pojawi się dwa razy, bo jest w obu tabelach.

Przykład 3: „ładniejszy wynik” — dodajemy etykietę skąd jest rekord

SELECT email, 'uczen' AS rola
FROM uczniowie

UNION ALL

SELECT email, 'nauczyciel' AS rola
FROM nauczyciele
ORDER BY email, rola;

Dlaczego UNION ALL, a nie UNION?
Bo nawet jeśli e-mail się powtarza, to wiersze nie są identyczne (różni się rola), więc i tak by nie zniknęły. A ALL jest po prostu uczciwsze i szybsze.

Przykłady INTERSECT

Pokaż e-maile, które są i w uczniach, i w nauczycielach

W SQL z INTERSECT wyglądałoby to tak (ale może nie działać w MySQL):

SELECT email FROM uczniowie
INTERSECT
SELECT email FROM nauczyciele;

Obejście 1: IN (proste)

SELECT email
FROM uczniowie
WHERE email IN (SELECT email FROM nauczyciele);

Krok po kroku:

  1. Wewnętrzny SELECT tworzy listę maili nauczycieli.
  2. Zewnętrzny SELECT bierze tylko tych uczniów, których email jest na tej liście.
  3. Wynik to „część wspólna” — czyli INTERSECT.

Wynik w naszej bazie: celina.zielinska@szkola.pl

Obejście 2: EXISTS (bardziej „bezpieczne” i często polecane)

SELECT u.email
FROM uczniowie u
WHERE EXISTS (
  SELECT 1
  FROM nauczyciele n
  WHERE n.email = u.email
);

Przykłady EXCEPT 

Pokaż e-maile uczniów, których NIE ma w nauczycielach

SELECT email FROM uczniowie
EXCEPT
SELECT email FROM nauczyciele;

Obejście: NOT EXISTS

SELECT u.email
FROM uczniowie u
WHERE NOT EXISTS (
  SELECT 1
  FROM nauczyciele n
  WHERE n.email = u.email
)
ORDER BY u.email;

Krok po kroku:

  1. Bierzemy każdego ucznia.
  2. Sprawdzamy, czy istnieje nauczyciel z takim samym mailem.
  3. Jeśli nie istnieje → uczeń zostaje w wyniku.

Typowe błędy

  • różna liczba kolumn – oba SELECT-y muszą zwracać tyle samo kolumn.
  • ORDER BY w złym miejscu – dajemy na samiutkim końcu, po wszystkich zapytaniach
  • INTERSECT/EXCEPT nie działa, MySQL krzyczy, że nie zna składni (You have an error in your SQL syntax...). Użyj INTERSECT → IN / EXISTS; EXCEPT → NOT EXISTS (najlepiej)
  • mieszanie typów danych: np. SELECT id UNION SELECT email – nawet jeśli przejdzie, wynik jest bez sensu.

Tabela porównawcza

OperatorCo robi „po ludzku”DuplikatyWsparcie w MySQL/MariaDB
UNIONA + Busuwatak
UNION ALLA + Bzostawiatak
INTERSECTA i Bzależy od silnikaczęsto brak → IN/EXISTS
EXCEPTA bez Bzależy od silnikaczęsto brak → NOT EXISTS

Ćwiczenia

Zadania rób na bazie szkolenia.sql (nad „Przykłady UNION I UNION ALL”)

  1. Zrób jedną listę maili: uczniowie + nauczyciele (bez powtórek).
  2. Zrób jedną listę maili: uczniowie + nauczyciele (z powtórkami).
  3. Zrób listę maili: uczniowie + nauczyciele + klienci, ale dodaj kolumnę zrodlo.
  4. Pokaż tylko te e-maile uczniów, które występują też u nauczycieli (INTERSECT → IN).
  5. Pokaż e-maile uczniów, których nie ma u nauczycieli (EXCEPT → NOT EXISTS).
  6. Pokaż e-maile klientów, które nie występują ani w uczniach, ani w nauczycielach.
  7. Zrób listę osób (email + typ_osoby) dla uczniów i nauczycieli (UNION ALL), posortuj po email rosnąco, a gdy ten sam email — niech „nauczyciel” będzie nad „uczen” (podpowiedź: sortowanie po drugiej kolumnie).
  8. Pokaż e-maile, które występują jednocześnie w tabeli klienci i uczniowie (INTERSECT → EXISTS).
  9. Pokaż e-maile nauczycieli, którzy nie występują w klienci (EXCEPT → NOT EXISTS).
  10. Zrób raport: email oraz ile_razy_wystepuje w całym systemie (uczniowie+nauczyciele+klienci).
    Podpowiedź: użyj UNION ALL, a potem policz (tu będzie potrzebne GROUP BY).