CodeGym /Kursy /SQL SELF /CTE vs podzapytania: kiedy co wybrać?

CTE vs podzapytania: kiedy co wybrać?

SQL SELF
Poziom 28 , Lekcja 1
Dostępny

Już wiemy, że CTE robi kod bardziej czytelnym. Ale czy zawsze warto ich używać? Czasem zwykłe podzapytanie ogarnia temat lepiej i szybciej. Sprawdźmy, kiedy każde narzędzie działa na plus i nauczmy się wybierać świadomie.

Podzapytania: szybko i prosto

Pewnie pamiętasz, że podzapytanie to SQL w SQL-u. Wstawiasz je bezpośrednio w głównym zapytaniu i wykonuje się "na miejscu". Świetnie nadaje się do prostych, jednorazowych operacji:

-- Znajdź produkty droższe niż średnia cena
SELECT product_name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);

Tutaj podzapytanie liczy średnią cenę raz i gotowe. Zero zbędnych konstrukcji.

Wydajność: kto szybszy?

Podzapytania często wygrywają szybkością przy prostych operacjach. PostgreSQL potrafi je zoptymalizować "w locie", szczególnie gdy podzapytanie wykonuje się tylko raz:

-- Szybko: podzapytanie wykona się raz
SELECT customer_id, order_total
FROM orders
WHERE order_date = (SELECT MAX(order_date) FROM orders);

CTE domyślnie są materializowane — PostgreSQL najpierw liczy wynik CTE, zapisuje go jako tymczasową tabelę, a potem używa. To może spowolnić proste zapytania:

-- Wolniej: CTE materializuje się do tymczasowej tabeli
WITH latest_date AS (
    SELECT MAX(order_date) AS max_date FROM orders
)
SELECT customer_id, order_total
FROM orders, latest_date
WHERE order_date = max_date;

Ale! Od PostgreSQL 12 możesz sterować materializacją:

-- Wymuś brak materializacji
WITH latest_date AS NOT MATERIALIZED (
    SELECT MAX(order_date) AS max_date FROM orders
)
SELECT customer_id, order_total
FROM orders, latest_date
WHERE order_date = max_date;

Wielokrotne użycie: tu CTE rządzi

Kiedy ten sam wynik pośredni potrzebny jest kilka razy, CTE jest niezastąpione:

-- Z podzapytaniem: powtarzamy tę samą logikę dwa razy
SELECT
    (SELECT COUNT(*) FROM orders WHERE status = 'completed') AS zakonczone_zamowienia,
    (SELECT COUNT(*) FROM orders WHERE status = 'completed') * 100.0 / COUNT(*) AS wspolczynnik_zakonczenia
FROM orders;

-- Z CTE: liczymy raz, używamy dwa razy
WITH zakonczone_zamowienia AS (
    SELECT COUNT(*) AS count FROM orders WHERE status = 'completed'
)
SELECT
    co.count AS zakonczone_zamowienia,
    co.count * 100.0 / (SELECT COUNT(*) FROM orders) AS wspolczynnik_zakonczenia
FROM zakonczone_zamowienia co;

Zaawansowana analiza: CTE wygrywa na punkty

Przy wieloetapowej analizie CTE zamienia chaos w porządek. Porównaj raport sprzedaży:

Z podzapytaniami (mętlik w głowie):

SELECT 
    category,
    revenue,
    revenue * 100.0 / (
        SELECT SUM(p.price * oi.quantity)
        FROM order_items oi
        JOIN products p ON oi.product_id = p.product_id
        JOIN orders o ON oi.order_id = o.order_id
        WHERE EXTRACT(year FROM o.order_date) = 2024
    ) AS udzial_przychodu
FROM (
    SELECT 
        p.category,
        SUM(p.price * oi.quantity) AS revenue
    FROM order_items oi
    JOIN products p ON oi.product_id = p.product_id
    JOIN orders o ON oi.order_id = o.order_id
    WHERE EXTRACT(year FROM o.order_date) = 2024
    GROUP BY p.category
) category_revenue;

Z CTE (wszystko na swoim miejscu):

WITH roczna_sprzedaz AS (
    SELECT 
        p.category,
        p.price * oi.quantity AS kwota_sprzedazy
    FROM order_items oi
    JOIN products p ON oi.product_id = p.product_id
    JOIN orders o ON oi.order_id = o.order_id
    WHERE EXTRACT(year FROM o.order_date) = 2024
),
przychod_kategorii AS (
    SELECT 
        category,
        SUM(kwota_sprzedazy) AS revenue
    FROM roczna_sprzedaz
    GROUP BY category
),
calkowity_przychod AS (
    SELECT SUM(kwota_sprzedazy) AS total FROM roczna_sprzedaz
)
SELECT 
    cr.category,
    cr.revenue,
    cr.revenue * 100.0 / tr.total AS udzial_przychodu
FROM przychod_kategorii cr, calkowity_przychod tr;

Rekursja: monopol CTE

Dla struktur hierarchicznych podzapytania są bezradne.

Tylko rekurencyjne CTE ogarniają zadania typu "znajdź wszystkich podwładnych menedżera":

WITH RECURSIVE hierarchia_pracownikow AS (
    -- Zaczynamy od CEO
    SELECT employee_id, manager_id, name, 1 AS poziom
    FROM employees
    WHERE manager_id IS NULL

    UNION ALL

    -- Dodajemy podwładnych każdego poziomu
    SELECT e.employee_id, e.manager_id, e.name, eh.poziom + 1
    FROM employees e
    JOIN hierarchia_pracownikow eh ON e.manager_id = eh.employee_id
)
SELECT * FROM hierarchia_pracownikow ORDER BY poziom, name;

Debugowanie i utrzymanie kodu

CTE łatwo debugować etapami:

-- Sprawdzamy pierwszy etap
WITH aktywni_klienci AS (
    SELECT customer_id FROM customers WHERE status = 'active'
)
SELECT COUNT(*) FROM aktywni_klienci; -- Upewniamy się, że logika jest OK

-- Dodajemy drugi etap
WITH aktywni_klienci AS (...),
ostatnie_zamowienia AS (
    SELECT customer_id, COUNT(*) as liczba_zamowien
    FROM orders
    WHERE order_date >= '2024-01-01'
    GROUP BY customer_id
)
SELECT COUNT(*) FROM ostatnie_zamowienia; -- Sprawdzamy i ten etap

Podzapytania trudniej debugować — trzeba je wyciągać z kontekstu.

Praktyczne wskazówki

Używaj podzapytania gdy:

  • Logika jest prosta i mieści się w jednej linijce
  • Potrzebujesz maksymalnej wydajności dla prostych operacji
  • Wynik pośredni używasz tylko raz
  • Pracujesz na małych ilościach danych

Używaj CTE gdy:

  • Zapytanie jest złożone i dzieli się na logiczne etapy
  • Musisz wielokrotnie użyć wyników pośrednich
  • Liczy się czytelność i łatwość utrzymania kodu
  • Pracujesz z hierarchiami (rekurencyjne CTE)
  • Debugujesz złożoną logikę etapami

Złota zasada

Zacznij od podzapytania. Jeśli robi się nieczytelnie albo logika się powtarza — przejdź na CTE. Twój przyszły kolega (albo Ty sam za pół roku) podziękuje Ci za to!

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION