Podzapytania SQL – filtrowanie i analiza danych na podstawie innych zapytań

Cel lekcji

Uczeń potrafi:

  • wyjaśnić, czym jest podzapytanie SQL;
  • stosować podzapytania w klauzuli WHERE oraz HAVING;
  • wykorzystywać podzapytania zwracające pojedynczą wartość oraz wiele rekordów;
  • stosować operatory IN i NOT IN;
  • tworzyć podzapytania skorelowane;
  • łączyć podzapytania z JOIN, GROUP BY i funkcjami agregującymi;
  • rozwiązywać problemy, w których warunek zapytania zależy od wyniku innego zapytania.

1. Podzapytania SQL

Podzapytanie to zapytanie SQL umieszczone wewnątrz innego zapytania. Wynik podzapytania jest wykorzystywany przez zapytanie zewnętrzne, np. do filtrowania danych.

Podzapytania mogą zwracać:

  • jedną wartość, np. średnią cenę produktów;
  • jedną kolumnę zawierającą wiele wartości, np. identyfikatory klientów;
  • wiele kolumn i rekordów, które mogą być wykorzystane jako tabela tymczasowa.

W tej lekcji będziemy pracować na bazie danych sklepu internetowego.

1.1. Podzapytanie zwracające jedną wartość

Załóżmy, że chcemy wyświetlić produkty droższe od średniej ceny wszystkich produktów.

Najpierw można obliczyć średnią cenę:

SELECT AVG(cena)
FROM produkty;

Wynikiem jest jedna wartość liczbowa.

Możemy wykorzystać ją w innym zapytaniu:

SELECT nazwa, cena
FROM produkty
WHERE cena > (
    SELECT AVG(cena)
    FROM produkty
);

W tym przykładzie:

  1. Podzapytanie oblicza średnią cenę produktów.
  2. Zapytanie zewnętrzne pobiera dane z tabeli produkty.
  3. Warunek WHERE wybiera produkty droższe od obliczonej średniej.

Nie trzeba ręcznie przepisywać wyniku pierwszego zapytania do drugiego.

Podzapytanie znajduje się w nawiasach.

1.2. Podzapytanie zwracające wiele wartości

Podzapytanie może zwracać wiele rekordów.

Przykładowo chcemy wyświetlić klientów, którzy złożyli zamówienia.

SELECT imie, nazwisko
FROM klienci
WHERE id_klienta IN (
    SELECT id_klienta
    FROM zamowienia
);

Podzapytanie pobiera identyfikatory klientów występujących w tabeli zamowienia.

Zapytanie zewnętrzne wyświetla dane klientów, których identyfikator znajduje się w wyniku podzapytania.

W tym przypadku podzapytanie nie musi zwracać jednej wartości. Może zwrócić wiele identyfikatorów.

2. Operator IN

Operator IN sprawdza, czy dana wartość znajduje się w zbiorze wartości.

Można go stosować z listą wartości:

SELECT nazwa
FROM produkty
WHERE id_kategorii IN (1, 3, 4);

Można również wykorzystać podzapytanie:

SELECT nazwa
FROM produkty
WHERE id_kategorii IN (
    SELECT id_kategorii
    FROM kategorie
    WHERE nazwa IN ('Gaming', 'Komputery')
);

W tym przykładzie podzapytanie pobiera identyfikatory kategorii o nazwach Gaming i Komputery.

Zapytanie zewnętrzne wyświetla produkty należące do tych kategorii.

Operator IN jest szczególnie przydatny wtedy, gdy nie znamy wcześniej identyfikatorów rekordów, które chcemy wyszukać.

3. Operator NOT IN

Operator NOT IN działa odwrotnie niż IN.

Sprawdza, czy dana wartość nie znajduje się w zbiorze wartości.

Przykład:

SELECT nazwa
FROM produkty
WHERE id_kategorii NOT IN (
    SELECT id_kategorii
    FROM kategorie
    WHERE nazwa = 'Gaming'
);

Zapytanie wyświetli produkty, które nie należą do kategorii Gaming.

Operator NOT IN może być używany zarówno z listą wartości, jak i z podzapytaniem.

Ważne: jeżeli podzapytanie używane z NOT IN zwróci wartość NULL, wynik może być inny od oczekiwanego. Dlatego należy kontrolować, czy kolumna wykorzystywana w podzapytaniu nie zawiera wartości NULL.

4. Podzapytania skorelowane

Podzapytanie skorelowane korzysta z danych zapytania zewnętrznego.

Przykład:

SELECT nazwa, cena, id_kategorii
FROM produkty p
WHERE cena > (
    SELECT AVG(cena)
    FROM produkty p2
    WHERE p2.id_kategorii = p.id_kategorii
);

Zapytanie wyświetla produkty droższe od średniej ceny produktów w ich kategorii.

W podzapytaniu występuje warunek:

p2.id_kategorii = p.id_kategorii

Wartość p.id_kategorii pochodzi z zapytania zewnętrznego.

Dla każdego produktu podzapytanie oblicza średnią cenę produktów należących do tej samej kategorii.

To właśnie zależność podzapytania od danych zapytania zewnętrznego nazywamy korelacją.

4.1. Podzapytanie skorelowane z IN

Podzapytania skorelowane można również wykorzystywać z operatorem IN.

Przykład:

SELECT imie, nazwisko
FROM klienci k
WHERE id_klienta IN (
    SELECT z.id_klienta
    FROM zamowienia z
    JOIN pozycje_zamowien pz
        ON pz.id_zamowienia = z.id_zamowienia
    WHERE z.id_klienta = k.id_klienta
      AND z.status = 'zrealizowane'
);

Zapytanie wyświetla klientów, którzy mają przynajmniej jedno zrealizowane zamówienie.

Podzapytanie korzysta z identyfikatora klienta pobranego przez zapytanie zewnętrzne.

5. Podzapytania w HAVING

Podzapytania można stosować również w klauzuli HAVING.

Przykład:

SELECT id_klienta, COUNT(*) AS liczba_zamowien
FROM zamowienia
GROUP BY id_klienta
HAVING COUNT(*) > (
    SELECT AVG(liczba)
    FROM (
        SELECT COUNT(*) AS liczba
        FROM zamowienia
        GROUP BY id_klienta
    ) AS statystyki
);

Zapytanie wyświetla klientów, którzy złożyli więcej zamówień niż średnia liczba zamówień przypadająca na klienta posiadającego zamówienia.

W tym przykładzie:

  1. Najbardziej wewnętrzne zapytanie liczy zamówienia każdego klienta.
  2. Kolejne podzapytanie oblicza średnią z otrzymanych wyników.
  3. Zapytanie zewnętrzne wyświetla klientów, których liczba zamówień przekracza obliczoną średnią.

Podzapytanie w HAVING pozwala porównywać wyniki grupowania z innymi wartościami obliczonymi przez SQL.

6. Przygotowanie bazy danych

Podczas lekcji będziemy pracować na bazie danych przedstawiającej sklep internetowy.

Baza zawiera:

  • klientów;
  • kategorie produktów;
  • produkty;
  • zamówienia;
  • pozycje zamówień.

Uruchom cały skrypt w MySQL lub phpMyAdmin.

DROP DATABASE IF EXISTS sklep_podzapytania;
CREATE DATABASE sklep_podzapytania
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

USE sklep_podzapytania;

CREATE TABLE klienci (
    id_klienta INT PRIMARY KEY,
    imie VARCHAR(40),
    nazwisko VARCHAR(50),
    miasto VARCHAR(50),
    typ_klienta VARCHAR(20)
);

CREATE TABLE kategorie (
    id_kategorii INT PRIMARY KEY,
    nazwa VARCHAR(50)
);

CREATE TABLE produkty (
    id_produktu INT PRIMARY KEY,
    nazwa VARCHAR(100),
    id_kategorii INT,
    cena DECIMAL(10,2),
    stan_magazynowy INT,
    FOREIGN KEY (id_kategorii)
        REFERENCES kategorie(id_kategorii)
);

CREATE TABLE zamowienia (
    id_zamowienia INT PRIMARY KEY,
    id_klienta INT,
    data_zamowienia DATE,
    status VARCHAR(20),
    FOREIGN KEY (id_klienta)
        REFERENCES klienci(id_klienta)
);

CREATE TABLE pozycje_zamowien (
    id_pozycji INT PRIMARY KEY,
    id_zamowienia INT,
    id_produktu INT,
    ilosc INT,
    cena_sztuki DECIMAL(10,2),
    FOREIGN KEY (id_zamowienia)
        REFERENCES zamowienia(id_zamowienia),
    FOREIGN KEY (id_produktu)
        REFERENCES produkty(id_produktu)
);

INSERT INTO klienci VALUES
(1, 'Adam', 'Kowalski', 'Warszawa', 'indywidualny'),
(2, 'Anna', 'Nowak', 'Krakow', 'firmowy'),
(3, 'Piotr', 'Wisniewski', 'Gdansk', 'indywidualny'),
(4, 'Ewa', 'Wojcik', 'Warszawa', 'firmowy'),
(5, 'Tomasz', 'Kaminski', 'Poznan', 'indywidualny'),
(6, 'Katarzyna', 'Lewandowska', 'Krakow', 'indywidualny'),
(7, 'Michal', 'Zielinski', 'Lublin', 'firmowy'),
(8, 'Magdalena', 'Szymanska', 'Gdansk', 'indywidualny'),
(9, 'Pawel', 'Dabrowski', 'Warszawa', 'indywidualny'),
(10, 'Joanna', 'Kozlowska', 'Poznan', 'firmowy'),
(11, 'Marcin', 'Jankowski', 'Lublin', 'indywidualny'),
(12, 'Agnieszka', 'Mazur', 'Krakow', 'firmowy');

INSERT INTO kategorie VALUES
(1, 'Komputery'),
(2, 'Akcesoria'),
(3, 'Gaming'),
(4, 'Smartfony'),
(5, 'AGD');

INSERT INTO produkty VALUES
(1, 'Laptop Lenovo', 1, 3500, 12),
(2, 'Laptop Dell', 1, 4200, 8),
(3, 'Monitor 27 cali', 2, 1100, 18),
(4, 'Klawiatura mechaniczna', 2, 350, 25),
(5, 'Mysz Logitech', 2, 120, 50),
(6, 'Sluchawki gamingowe', 3, 280, 30),
(7, 'Konsola Xbox', 3, 2200, 10),
(8, 'Konsola PlayStation', 3, 2500, 6),
(9, 'Smartfon Samsung', 4, 2800, 20),
(10, 'Smartfon Xiaomi', 4, 1900, 15),
(11, 'Ekspres do kawy', 5, 1600, 9),
(12, 'Odkurzacz', 5, 850, 14),
(13, 'Router TP-Link', 2, 320, 19),
(14, 'Kamera internetowa', 2, 250, 35),
(15, 'Tablet Samsung', 1, 1800, 11),
(16, 'Glosnik Bluetooth', 2, 450, 0),
(17, 'Drukarka HP', 1, 750, 16),
(18, 'Smartwatch Apple', 4, 1700, 4),
(19, 'Blender Bosch', 5, 420, 13),
(20, 'Mikrofon USB', 2, 390, 22);

INSERT INTO zamowienia VALUES
(1, 1, '2026-01-10', 'zrealizowane'),
(2, 2, '2026-01-15', 'zrealizowane'),
(3, 3, '2026-01-20', 'wyslane'),
(4, 1, '2026-02-05', 'zrealizowane'),
(5, 4, '2026-02-10', 'anulowane'),
(6, 5, '2026-02-15', 'zrealizowane'),
(7, 6, '2026-02-20', 'wyslane'),
(8, 7, '2026-03-01', 'zrealizowane'),
(9, 8, '2026-03-05', 'zrealizowane'),
(10, 9, '2026-03-10', 'oczekujace'),
(11, 10, '2026-03-15', 'zrealizowane'),
(12, 2, '2026-03-20', 'wyslane'),
(13, 3, '2026-04-01', 'zrealizowane'),
(14, 5, '2026-04-05', 'anulowane'),
(15, 11, '2026-04-10', 'zrealizowane'),
(16, 12, '2026-04-15', 'oczekujace'),
(17, 4, '2026-04-20', 'zrealizowane'),
(18, 6, '2026-05-01', 'zrealizowane'),
(19, 7, '2026-05-05', 'wyslane'),
(20, 8, '2026-05-10', 'zrealizowane'),
(21, 1, '2026-05-15', 'zrealizowane'),
(22, 9, '2026-05-20', 'anulowane'),
(23, 10, '2026-06-01', 'zrealizowane'),
(24, 11, '2026-06-05', 'wyslane'),
(25, 12, '2026-06-10', 'zrealizowane'),
(26, 3, '2026-06-15', 'zrealizowane'),
(27, 4, '2026-06-20', 'oczekujace'),
(28, 5, '2026-07-01', 'zrealizowane'),
(29, 6, '2026-07-05', 'zrealizowane'),
(30, 7, '2026-07-10', 'wyslane'),
(31, 8, '2026-07-15', 'zrealizowane'),
(32, 9, '2026-07-20', 'zrealizowane'),
(33, 10, '2026-08-01', 'anulowane'),
(34, 11, '2026-08-05', 'zrealizowane'),
(35, 12, '2026-08-10', 'wyslane'),
(36, 1, '2026-08-15', 'zrealizowane');

INSERT INTO pozycje_zamowien VALUES
(1, 1, 1, 1, 3500),
(2, 1, 5, 1, 120),
(3, 2, 3, 2, 1100),
(4, 2, 4, 1, 350),
(5, 3, 2, 1, 4200),
(6, 3, 9, 1, 2800),
(7, 4, 7, 1, 2200),
(8, 4, 6, 2, 280),
(9, 5, 9, 1, 2800),
(10, 5, 18, 1, 1700),
(11, 6, 12, 2, 850),
(12, 6, 19, 1, 420),
(13, 7, 10, 2, 1900),
(14, 8, 13, 2, 320),
(15, 8, 14, 1, 250),
(16, 9, 6, 3, 280),
(17, 9, 8, 1, 2500),
(18, 10, 9, 1, 2800),
(19, 11, 2, 1, 4200),
(20, 11, 4, 1, 350),
(21, 12, 15, 1, 1800),
(22, 12, 5, 2, 120),
(23, 13, 1, 1, 3500),
(24, 13, 3, 1, 1100),
(25, 14, 9, 1, 2800),
(26, 14, 10, 1, 1900),
(27, 15, 17, 2, 750),
(28, 15, 14, 2, 250),
(29, 16, 18, 1, 1700),
(30, 17, 2, 1, 4200),
(31, 17, 3, 1, 1100),
(32, 17, 4, 2, 350),
(33, 18, 7, 1, 2200),
(34, 18, 6, 1, 280),
(35, 19, 13, 3, 320),
(36, 19, 20, 2, 390),
(37, 20, 12, 1, 850),
(38, 20, 19, 2, 420),
(39, 21, 1, 1, 3500),
(40, 21, 15, 1, 1800),
(41, 22, 8, 1, 2500),
(42, 22, 6, 2, 280),
(43, 23, 10, 2, 1900),
(44, 23, 5, 3, 120),
(45, 24, 17, 1, 750),
(46, 24, 13, 2, 320),
(47, 25, 11, 1, 1600),
(48, 25, 19, 2, 420),
(49, 26, 2, 1, 4200),
(50, 26, 9, 1, 2800),
(51, 27, 3, 2, 1100),
(52, 27, 14, 1, 250),
(53, 28, 7, 1, 2200),
(54, 28, 6, 2, 280),
(55, 29, 10, 1, 1900),
(56, 29, 18, 1, 1700),
(57, 30, 1, 1, 3500),
(58, 30, 4, 2, 350),
(59, 31, 12, 1, 850),
(60, 31, 5, 2, 120),
(61, 32, 9, 1, 2800),
(62, 32, 13, 2, 320),
(63, 33, 2, 1, 4200),
(64, 33, 20, 2, 390),
(65, 34, 15, 2, 1800),
(66, 34, 14, 1, 250),
(67, 35, 8, 1, 2500),
(68, 35, 6, 2, 280),
(69, 36, 1, 1, 3500),
(70, 36, 3, 2, 1100);

7. Ćwiczenia

Wykonaj wszystkie zadania samodzielnie.

Do każdego ćwiczenia przygotuj osobne zapytanie SQL. Nie zmieniaj danych w bazie.

Ćwiczenie 1 – Produkty droższe od średniej

Wyświetl nazwy, ceny i nazwy kategorii produktów, których cena jest większa od średniej ceny wszystkich produktów w sklepie.

Wyniki posortuj malejąco według ceny.

Wykorzystaj podzapytanie zwracające pojedynczą wartość.

Ćwiczenie 2 – Klienci, którzy złożyli zamówienia

Wyświetl imiona, nazwiska i miasta klientów, którzy złożyli przynajmniej jedno zamówienie o statusie zrealizowane.

Nie wyświetlaj tych samych klientów więcej niż jeden raz.

Posortuj wyniki alfabetycznie według nazwiska.

Wykorzystaj podzapytanie oraz operator IN.

Ćwiczenie 3 – Produkty, których nikt nie kupił

Wyświetl nazwy i ceny produktów, które nie wystąpiły w żadnej pozycji zamówienia.

Posortuj wynik alfabetycznie według nazwy produktu.

Wykorzystaj podzapytanie z operatorem NOT IN.

Ćwiczenie 4 – Produkty droższe od najdroższego produktu z kategorii Gaming

Wyświetl nazwy, ceny i nazwy kategorii produktów, których cena jest większa od najwyższej ceny produktu należącego do kategorii Gaming.

Nie wyświetlaj produktów z kategorii Gaming.

Wyniki posortuj malejąco według ceny.

Wykorzystaj podzapytanie zwracające pojedynczą wartość oraz funkcję MAX().

Ćwiczenie 5 – Klienci, którzy kupowali w różnych kategoriach

Wyświetl imiona, nazwiska i miasta klientów, którzy kupili produkty należące do co najmniej 3 różnych kategorii.

Uwzględnij wyłącznie zamówienia o statusie zrealizowane.

Wyniki posortuj alfabetycznie według nazwiska.

Wykorzystaj podzapytanie, grupowanie danych i operator IN.

Ćwiczenie 6 – Produkty droższe od średniej ceny w swojej kategorii

Wyświetl nazwy, ceny i nazwy kategorii produktów, których cena jest większa od średniej ceny produktów należących do tej samej kategorii.

Wyniki posortuj alfabetycznie według kategorii, a następnie malejąco według ceny.

Wykorzystaj podzapytanie skorelowane.

Ćwiczenie 7 – Klienci, którzy wydali więcej niż średnia

Wyświetl imiona, nazwiska i miasta klientów, których łączna wartość zrealizowanych zamówień jest większa od średniej wartości zrealizowanych zamówień wszystkich klientów.

Wartość każdego zamówienia oblicz na podstawie jego pozycji.

Uwzględnij tylko klientów, którzy złożyli przynajmniej jedno zamówienie zrealizowane.

Wyniki posortuj malejąco według łącznej wartości zakupów.

Wykorzystaj podzapytania, GROUP BY oraz HAVING.

Ćwiczenie 8 – Analiza klientów i kategorii

Przygotuj raport pokazujący klientów, którzy kupowali produkty z kategorii, w których łączna wartość sprzedaży jest większa od średniej wartości sprzedaży wszystkich kategorii.

Uwzględnij wyłącznie zamówienia zrealizowane.

Wynik powinien zawierać:

  • imię i nazwisko klienta;
  • nazwę kategorii;
  • liczbę różnych zamówień klienta w tej kategorii;
  • łączną wartość zakupów klienta w tej kategorii;
  • łączną wartość sprzedaży całej kategorii.

Wyświetl tylko te pary klient – kategoria, dla których wartość zakupów klienta jest większa niż 1000 zł.

Posortuj wynik malejąco według wartości zakupów klienta, a następnie alfabetycznie według kategorii.

Wykorzystaj podzapytania, JOIN, GROUP BY i HAVING.