BAZY DANYCH · INF.03
Podzapytania, UNION i widoki
Nie każde pytanie da się odpowiedzieć jednym prostym SELECT. Podzapytania pozwolą porównywać zbiory, UNION łączyć wyniki, a widoki zapisać gotowy raport, z którego będzie korzystać cała biblioteka.
Podzapytania i EXISTS — pytanie w pytaniu
Nie każde pytanie da się zadać w jednym prostym SELECT. Bibliotekarz może najpierw policzyć średnią cenę książek, a dopiero potem zapytać, które tytuły są od niej droższe. W SQL nie trzeba wykonywać dwóch osobnych zapytań: wystarczy umieścić jedno zapytanie w nawiasie wewnątrz drugiego. Takie zagnieżdżenie nazywamy podzapytaniem.
Podzapytanie skalarne zwraca jedną wartość: jeden wiersz i jedną kolumnę. Można go użyć wszędzie tam, gdzie w zwykłym zapytaniu stałaby liczba albo napis. W poniższym przykładzie wewnętrzny SELECT AVG(cena) liczy średnią dla całej tabeli, a zewnętrzny porównuje z nią cenę każdej książki.
SELECT tytul, cena
FROM ksiazki
WHERE cena > (SELECT AVG(cena) FROM ksiazki)
ORDER BY cena DESC, tytul;
Podzapytanie może też stać w liście wyboru. Wtedy jego wynik pojawia się jako dodatkowa kolumna w każdym wierszu. To wygodne w raportach, w których obok wartości chcemy pokazać punkt odniesienia, np. średnią cenę. Warto nadać kolumnie alias, inaczej nagłówek będzie trudny do odczytania.
SELECT tytul, cena,
ROUND((SELECT AVG(cena) FROM ksiazki), 2) AS srednia_cena
FROM ksiazki
WHERE cena > 80
ORDER BY cena DESC;
IN z podzapytaniem sprawdza, czy wartość należy do zbioru zwróconego przez wewnętrzne zapytanie. To wygodny sposób łączenia tabel bez jawnego JOIN. Jeśli chcemy uczniów, którzy wypożyczyli choć jedną książkę z kategorii Podręczniki, najpierw wybieramy identyfikatory takich książek, a potem szukamy wypożyczeń, które ich dotyczą. DISTINCT usuwa powtórzenia, bo jeden uczeń mógł wypożyczyć kilka podręczników.
SELECT DISTINCT u.imie, u.nazwisko
FROM uczniowie u
JOIN wypozyczenia w ON u.id = w.uczen_id
WHERE w.ksiazka_id IN (
SELECT id FROM ksiazki WHERE kategoria_id = 8
)
ORDER BY u.nazwisko;
NOT IN odwraca warunek i pozwala znaleźć wartości, których nie ma w zbiorze. Zapytanie o uczniów bez żadnego wypożyczenia jest krótkie, ale wymaga ostrożności: jeśli podzapytanie zwróci NULL, porównanie może nie zwrócić żadnego wiersza. W naszej bazie kolumna uczen_id jest kluczem obcym i nie może być NULL, więc wynik jest bezpieczny. W innych schematach trzeba o tym pamiętać.
SELECT imie, nazwisko
FROM uczniowie
WHERE id NOT IN (SELECT uczen_id FROM wypozyczenia)
AND aktywny = 1
ORDER BY nazwisko;
EXISTS zadaje pytanie „czy istnieje choć jeden wiersz spełniający warunek?”. W przeciwieństwie do IN nie porównuje wartości, lecz sprawdza sam fakt istnienia. Podzapytanie z EXISTS jest zwykle skorelowane: odwołuje się do kolumny z zewnętrznego zapytania, np. u.id. Baza może przerwać szukanie po pierwszym dopasowaniu, bo dalsze wiersze nie zmieniają odpowiedzi.
-- Wersja dla phpMyAdmin: EXISTS sprawdza, czy uczeń ma jakiekolwiek wypożyczenie
SELECT imie, nazwisko
FROM uczniowie u
WHERE EXISTS (
SELECT 1 FROM wypozyczenia w
WHERE w.uczen_id = u.id
);
W edytorze ćwiczeń tego kursu sprawdzamy podstawowy zakres SQL: SELECT, JOIN, grupowanie, podzapytania z IN oraz proste widoki. EXISTS uruchomisz w phpMyAdmin na XAMPP, gdzie masz pełny MySQL/MariaDB. W zadaniach automatycznych używamy równoważnego IN albo NOT IN, bo dają ten sam wynik i działają w obu środowiskach.
IN i EXISTS zwracają zbiór wierszy i odpowiadają na pytania o przynależność lub istnienie.| Rodzaj podzapytania | Co zwraca | Gdzie pasuje | Przykład |
|---|---|---|---|
| skalarne | jedna wartość | w porównaniu lub w SELECT | cena > (SELECT AVG(cena) ...) |
| lista z IN | kolumna wartości | w warunku WHERE | id IN (SELECT uczen_id ...) |
| EXISTS | prawda albo fałsz | w warunku WHERE | WHERE EXISTS (SELECT 1 ...) |
Przykłady z życia
1. Sprawdzenie, czy uczeń ma zaległości. Bibliotekarka nie potrzebuje listy wszystkich wypożyczeń. Wystarczy pytanie „czy istnieje choć jedno przeterminowane wypożyczenie tego ucznia?”. To zadanie dla EXISTS, bo liczy się fakt istnienia, a nie liczba rekordów.
2. Zakupy powyżej średniej. Szkoła porównuje ceny podręczników ze średnią ceną w całym katalogu. Podzapytanie skalarne liczy średnią raz, a zewnętrzne zapytanie wybiera droższe pozycje. Ten sam wzorzec działa w sklepie przy analizie paragonów.
3. Lista rezerwacji bez wypożyczenia. Biblioteka chce znaleźć książki, które są zarezerwowane, ale nie zostały jeszcze wypożyczone. Podzapytanie z IN zbiera identyfikatory rezerwacji, a zewnętrzne zapytanie sprawdza księgozbiór.
Matematyka: podzapytanie przypomina funkcję złożoną. Najpierw obliczasz wartość wewnętrzną, a potem używasz jej w wyrażeniu zewnętrznym. W teorii zbiorów IN odpowiada relacji przynależności, a EXISTS pytaniu, czy zbiór jest niepusty.
W module 04 liczyłeś agregaty, a w module 05 łączyłeś tabele. Tutaj używasz jednego zapytania jako danych dla drugiego. W następnej sekcji połączysz wyniki przez UNION, a w sekcji o widokach zapiszesz całe zapytanie pod nazwą. W module 09 podzapytania pojawią się w zadaniach egzaminacyjnych.
Informatyka: w kodzie programu zagnieżdżasz funkcje — wynik jednej staje się argumentem drugiej. Podzapytanie działa podobnie, ale przenosi obliczenia do bazy, która może wykorzystać indeksy i nie przesyła niepotrzebnych wierszy do aplikacji.
UNION i raporty przekrojowe
JOIN dodaje kolumny: z dwóch tabel powstaje jeden szerszy wiersz. Czasem potrzebujemy czegoś odwrotnego — połączyć dwa zbiory wierszy o tej samej strukturze. Biblioteka chce jedną listę kontaktów, na której znajdą się zarówno uczniowie, jak i pracownicy. Do dokładania wierszy służy operator UNION.
UNION łączy wyniki dwóch zapytań SELECT. Oba muszą zwracać tę samą liczbę kolumn, a odpowiadające sobie kolumny powinny mieć zgodne typy. Nazwy kolumn w wyniku pochodzą z pierwszego zapytania. Kolejność kolumn też musi być zgodna, bo baza porównuje je pozycyjnie, a nie po nazwach.
SELECT imie, nazwisko, email, 'uczen' AS rola
FROM uczniowie
WHERE aktywny = 1 AND email IS NOT NULL
UNION ALL
SELECT imie, nazwisko, email, 'pracownik' AS rola
FROM pracownicy
ORDER BY nazwisko, imie;
UNION usuwa duplikaty, a UNION ALL zostawia wszystkie wiersze. Usuwanie duplikatów wymaga dodatkowego porównania całych wierszy, więc UNION ALL jest zwykle szybszy. Sięgamy po UNION, gdy naprawdę chcemy uniknąć powtórzeń, a po UNION ALL, gdy każdy wiersz jest osobnym faktem — np. każdy kontakt ma swoją rolę i nie chcemy go scalać z innym.
Klauzula ORDER BY w zapytaniu z UNION występuje tylko raz, na samym końcu, i dotyczy całego połączonego wyniku. Nie można sortować każdego SELECT osobno, bo kolejność końcowa i tak byłaby ustalana na zewnątrz. Analogicznie LIMIT na końcu ogranicza całość, a nie pojedynczą część.
UNION a JOIN. JOIN łączy tabele w poziomie i wymaga warunku połączenia. UNION układa wyniki w pionie i wymaga zgodności kolumn. Jeśli pomylisz te operatory, otrzymasz albo zbyt wiele kolumn, albo zbyt wiele wierszy. Prosta zasada: JOIN odpowiada na pytanie „jakie informacje o tym samym obiekcie?”, a UNION — „jakie obiekty z różnych zbiorów chcę pokazać na jednej liście?”.
Raport przekrojowy to zestawienie wartości według dwóch wymiarów. Grupowanie po klasie i statusie wypożyczeń daje tabelę, w której każda klasa ma osobne wiersze dla zwrotów, wypożyczeń w trakcie i przeterminowań. Nie potrzebujemy do tego UNION: wystarczy GROUP BY po dwóch kolumnach. UNION przydaje się wtedy, gdy zestawiamy ze sobą dwa różne rodzaje obiektów, a nie dwa wymiary tego samego obiektu.
SELECT kl.nazwa AS klasa, w.status, COUNT(*) AS liczba
FROM klasy kl
JOIN uczniowie u ON kl.id = u.klasa_id
JOIN wypozyczenia w ON u.id = w.uczen_id
GROUP BY kl.nazwa, w.status
ORDER BY kl.nazwa, w.status;
| Operacja | Co robi z wierszami | Czego wymaga | Kiedy użyć |
|---|---|---|---|
UNION |
dokłada wiersze i usuwa duplikaty | ta sama liczba i typ kolumn | wspólna lista bez powtórzeń |
UNION ALL |
dokłada wszystkie wiersze | ta sama liczba i typ kolumn | każdy wiersz jest osobnym faktem |
JOIN |
łączy wiersze w szersze rekordy | warunek połączenia po kluczach | uzupełnienie danych o ten sam obiekt |
Przykłady z życia
1. Wspólna lista uczestników. Szkoła organizuje konkurs dla dwóch klas i chce jedną listę uczestników. Każda klasa ma osobną tabelę zgłoszeń, ale obie mają te same kolumny: imię, nazwisko i klasę. UNION ALL daje jedną listę gotową do wydruku.
2. Wyciąg bankowy. Bank pokazuje na jednym ekranie obciążenia i wpłaty. To dwa różne zbiory operacji o wspólnych kolumnach: data, kwota i opis. UNION łączy je w jedną historię, choć pochodzą z różnych tabel.
3. Wyszukiwarka biblioteki. Czytelnik szuka słowa „kosmos” w książkach i czasopismach. Wyniki z obu katalogów mają wspólne kolumny tytułu i autora, więc można je zestawić na jednej liście z kolumną „typ materiału”.
Geografia: zestawienie „wszystkie cieki wodne w gminie” może połączyć rzeki i kanały z dwóch rejestrów. Oba zbiory mają nazwę, długość i gminę, więc UNION tworzy jedną warstwę do analizy. JOIN byłby potrzebny dopiero wtedy, gdybyśmy chcieli dołączyć do każdego cieku dane gminy.
W poprzedniej sekcji podzapytania dostarczały wartości do warunku. Tutaj łączysz całe wyniki w pionie. W następnej sekcji zapiszesz zapytanie jako widok, a w module 04 przypomnisz sobie grupowanie, które jest podstawą raportów przekrojowych. W module 09 zestawienia przekrojowe pojawią się w pełnym scenariuszu egzaminacyjnym.
Ekonomia: raport „przychody według miesiąca i kanału sprzedaży” grupuje po dwóch wymiarach, a nie używa UNION. UNION przydaje się, gdy zestawiamy np. faktury sprzedaży i faktury zakupu, które są osobnymi zbiorami dokumentów o podobnej strukturze.
Biblioteka przygotowuje jedną listę osób do powiadomień e-mail. Mają się na niej znaleźć aktywni uczniowie, którzy podali adres e-mail, oraz wszyscy pracownicy. Kolumny: imię, nazwisko, e-mail i rola („uczen” albo „pracownik”). Posortuj listę po nazwisku i imieniu. Napisz zapytanie z UNION ALL i wyjaśnij, dlaczego w tym przypadku nie potrzebujesz usuwania duplikatów przez UNION.
Pokaż przykładowe rozwiązanie i kryteria
Przykład: SELECT imie, nazwisko, email, 'uczen' AS rola FROM uczniowie WHERE aktywny = 1 AND email IS NOT NULL UNION ALL SELECT imie, nazwisko, email, 'pracownik' AS rola FROM pracownicy ORDER BY nazwisko, imie; Rola odróżnia oba zbiory, więc nawet osoba o takim samym imieniu i nazwisku nie byłaby tym samym kontaktem. Każdy wiersz jest osobnym faktem, dlatego UNION ALL jest właściwy.
Kryteria: (1) struktura — oba zapytania mają cztery zgodne kolumny; (2) filtr — uczniowie są aktywni i mają e-mail; (3) operator — użyto UNION ALL, a nie UNION bez uzasadnienia; (4) wynik — lista jest posortowana po nazwisku i imieniu, a rola ma czytelną wartość.
Widoki — zapisane raporty
Biblioteka co tydzień pyta o te same aktywne wypożyczenia, które trzeba przypomnieć uczniom. Za każdym razem trzeba łączyć trzy tabele i filtrować status. Zamiast powtarzać długie zapytanie, można zapisać je pod nazwą i korzystać z niej jak z tabeli. Tak działa widok (ang. view).
Widok to zapisane zapytanie SELECT, które zachowuje się jak tabela wirtualna. Nie przechowuje własnej kopii danych — przy każdym odczycie pobiera je z tabel źródłowych. Dlatego po dodaniu nowego wypożyczenia widok od razu pokazuje aktualny stan, bez ręcznego odświeżania. W MySQL widok nie jest zmaterializowany, więc nie zajmuje miejsca na kopię wierszy.
CREATE VIEW v_aktywne_wypozyczenia AS
SELECT w.id, u.imie, u.nazwisko, ks.tytul, w.termin_zwrotu
FROM wypozyczenia w
JOIN uczniowie u ON w.uczen_id = u.id
JOIN ksiazki ks ON w.ksiazka_id = ks.id
WHERE w.status = 'w trakcie';
Po utworzeniu widoku można go używać w zwykłym SELECT, dodawać warunki, sortować i grupować. Nazwa widoku zajmuje miejsce tabeli w zapytaniu, ale pod spodem nadal wykonuje się zapisane połączenie. To wygodne w aplikacjach: programista pisze SELECT * FROM v_aktywne_wypozyczenia, a nie powtarza skomplikowanego JOIN w każdym miejscu.
SELECT nazwisko, tytul, termin_zwrotu
FROM v_aktywne_wypozyczenia
WHERE termin_zwrotu <= '2026-11-07'
ORDER BY termin_zwrotu, nazwisko;
Widoki świetnie nadają się do raportów z agregatami. Widok v_raport_kategorii może przechowywać zapytanie, które liczy książki i średnią cenę w każdej kategorii. Użytkownik widzi gotowy raport i nie musi znać szczegółów połączeń. To także sposób na ograniczenie dostępu: można przyznać pracownikowi prawo do widoku, a nie do wszystkich tabel źródłowych.
CREATE VIEW v_raport_kategorii AS
SELECT kat.nazwa AS kategoria,
COUNT(k.id) AS liczba_ksiazek,
ROUND(AVG(k.cena), 2) AS srednia_cena
FROM kategorie kat
JOIN ksiazki k ON kat.id = k.kategoria_id
GROUP BY kat.nazwa;
SELECT kategoria, liczba_ksiazek, srednia_cena
FROM v_raport_kategorii
ORDER BY srednia_cena DESC;
Widok można zmienić przez CREATE OR REPLACE VIEW albo usunąć przez DROP VIEW. Nazwy widoków warto poprzedzać przedrostkiem v_, żeby odróżnić je od tabel. Nie należy wkładać do widoku logiki, która ma się zmieniać przy każdym raporcie — lepiej zostawić parametry (daty, klasy) w zewnętrznym WHERE. Pamiętaj też, że ORDER BY w definicji widoku nie gwarantuje kolejności wyniku; sortuj przy odczycie.
DROP VIEW v_aktywne_wypozyczenia;
| Obiekt | Czy przechowuje dane | Co zawiera | Kiedy użyć |
|---|---|---|---|
| tabela | tak | wiersze i kolumny | przechowywanie danych źródłowych |
| widok | nie, tylko definicję zapytania | zapisany SELECT |
powtarzalny raport lub ograniczenie dostępu |
| zwykłe zapytanie | nie | wynik obliczony na bieżąco | jednorazowa analiza |
Przykłady z życia
1. Zapisany filtr w skrzynce e-mail. Reguła „wiadomości od wychowawcy” nie kopiuje maili, tylko za każdym razem wybiera je z całej skrzynki. Widok działa podobnie: przechowuje warunek, a nie kopię wiadomości.
2. Szablon raportu w arkuszu. Księgowa ma szablon, który pobiera dane z tabeli faktur i od razu pokazuje sumy. Gdy pojawi się nowa faktura, raport się aktualizuje. To odpowiednik widoku z agregacją, choć w arkuszu trzeba czasem odświeżyć dane ręcznie.
3. Tablica wyników na stronie szkoły. Strona pokazuje najlepszych uczniów w konkursie. Zamiast kopiować listę, pobiera ją z widoku, który sortuje i filtruje wyniki. Po dopisaniu nowego wyniku tablica zmienia się bez edycji strony.
Informatyka: w aplikacji internetowej widok bazy bywa warstwą pośrednią między tabelami a kodem. Programista nie musi powtarzać złożonego JOIN w każdym kontrolerze, a administrator może odebrać dostęp do tabeli, zostawiając prawo do widoku. To wzorzec podobny do funkcji w kodzie: jedna nazwa ukrywa szczegóły implementacji.
W module 04 nauczyłeś się grupować dane, a w module 05 łączyć tabele. Widok zapisuje efekt obu tych umiejętności pod jedną nazwą. W poprzedniej sekcji poznałeś UNION, który także może stać się częścią widoku. W module 08 zajmiesz się uprawnieniami, a w module 09 wykorzystasz widok w pełnym scenariuszu egzaminacyjnym.
Geografia: system GIS często udostępnia widok „szkoły w promieniu 5 km”, który łączy tabelę szkół z warstwą dróg i oblicza odległości. Użytkownik widzi gotową warstwę, a nie surowe tabele. Zmiana danych w tabeli źródłowej jest widoczna przy następnym odświeżeniu.
Biblioteka chce udostępnić wychowawcom gotowy raport. Utwórz widok v_raport_klas, który dla każdej klasy pokaże nazwę klasy, liczbę wypożyczeń i sumę kar. Następnie wyświetl wszystkie wiersze widoku posortowane malejąco po sumie kar, a przy równych sumach alfabetycznie po nazwie klasy. Wyjaśnij, dlaczego widok nie przechowuje kopii danych i co się stanie po dodaniu nowego wypożyczenia.
Pokaż przykładowe rozwiązanie i kryteria
Przykład: CREATE VIEW v_raport_klas AS SELECT kl.nazwa AS klasa, COUNT(w.id) AS wypozyczenia, SUM(w.kara) AS suma_kar FROM klasy kl JOIN uczniowie u ON kl.id = u.klasa_id JOIN wypozyczenia w ON u.id = w.uczen_id GROUP BY kl.nazwa; SELECT klasa, wypozyczenia, suma_kar FROM v_raport_klas ORDER BY suma_kar DESC, klasa; Widok przechowuje tylko definicję zapytania. Po dodaniu wypożyczenia do tabeli źródłowej następny odczyt widoku policzy je od nowa.
Kryteria: (1) definicja — widok łączy klasy, uczniów i wypożyczenia po właściwych kluczach; (2) agregaty — zawiera COUNT(w.id) i SUM(w.kara) oraz grupowanie po klasie; (3) odczyt — zapytanie korzysta z widoku i sortuje po sumie kar oraz nazwie klasy; (4) wyjaśnienie — uczeń odróżnia widok od kopii danych i opisuje aktualizację po zmianie tabel źródłowych.
Ćwiczenia SQL
Zadania do samodzielnego wykonania w edytorze SQL.
- 1Droższe niż średniaUżyj podzapytania skalarnego, aby znaleźć dostępne książki droższe od średniej ceny w katalogu.Zadanie i podpowiedzi →
- 2Czytelnicy podręcznikówZnajdź uczniów, którzy wypożyczyli choć jedną książkę z kategorii Podręczniki, używając podzapytania z IN.Zadanie i podpowiedzi →
- 3Książki bez aktywnej rezerwacjiUżyj NOT IN z podzapytaniem, aby znaleźć dostępne książki, które nie mają aktywnej rezerwacji.Zadanie i podpowiedzi →
- 4Widok raportu kategoriiUtwórz widok v_raport_kategorii z agregatami kategorii, a następnie wyświetl kategorie z co najmniej trzema książkami.Zadanie i podpowiedzi →