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!
GO TO FULL VERSION