Podzapytania i CTE w SQL

Kacper Sieradziński
Kacper Sieradziński
27 sierpnia 2025SQL3 min czytania

Podstawowe SELECT-y wystarczają, dopóki dane są proste. Ale gdy zaczynasz pisać zapytania wieloetapowe — z filtrami, sumami, warunkami i porównaniami między zbiorami — szybko pojawia się potrzeba „myślenia w warstwach”.

Obraz główny Podzapytania i CTE w SQL

Zapisz się na nasz newsletter

Otrzymuj regularne aktualizacje, specjalne oferty i porady od ekspertów, które pomogą Ci osiągnąć więcej w krótszym czasie.

Podzapytania i CTE (Common Table Expressions) to właśnie sposób, by rozbić złożony problem SQL na mniejsze, logiczne etapy. Pozwalają tworzyć tymczasowe wyniki, łączyć je w łańcuchy i nadawać im czytelne nazwy zamiast pisać gigantyczne zapytania w jednej linii.

W tym artykule zobaczysz, jak działają podzapytania i CTE, kiedy warto ich używać, jak wpływają na wydajność i kiedy lepiej zastąpić je JOIN-em lub funkcjami okienkowymi. To kluczowy krok od „pisać SELECT-y” do „projektować zapytania”.

Czym są podzapytania i kiedy ich używać

Podzapytania w SQL (subquery) to zapytania zagnieżdżone w innym zapytaniu. Mogą występować w SELECT (podzapytanie skalarne), WHERE/HAVING (filtr), lub FROM (tabela pochodna). Sprawdzają się, gdy potrzebujesz pośredniego wyniku: doprecyzowania filtra, agregacji pomocniczej lub izolowania logiki bez trwałego widoku.

Przykład filtru opartego o podzapytanie w WHERE:

SQL
1 2 3 4 5 6 7 8 -- Klienci z zamówieniami powyżej 1000 SELECT c.id, c.name FROM customers c WHERE c.id IN ( SELECT o.customer_id FROM orders o WHERE o.total_amount > 1000 );

Jeśli dopiero zaczynasz, uzupełnij podstawy składni i operatorów w: Podstawy SQL: SELECT, WHERE, JOIN, GROUP BY.

MySQL — Jak zacząć? Darmowy e-book

MySQL — Jak zacząć? Darmowy e-book

Praktyczny przewodnik po świecie SQL. Poznaj typy danych, zapytania SELECT, JOIN, funkcje agregujące i nie tylko.

Podzapytania skorelowane i nieskorelowane

Podzapytania nieskorelowane nie odwołują się do tabel z zapytania zewnętrznego i mogą być wykonane niezależnie. Przykład skalarny: porównanie do globalnej średniej. Podzapytania skorelowane odwołują się do bieżącego wiersza zapytania zewnętrznego (np. przez alias). Te drugie bywają kosztowne, bo mogą wykonywać się dla każdego wiersza zewnętrznego.

SQL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 -- Nieskorelowane: zamówienia powyżej średniej SELECT o.id FROM orders o WHERE o.total_amount > (SELECT AVG(total_amount) FROM orders); -- Skorelowane: klienci, którzy mają przynajmniej jedno otwarte zamówienie SELECT c.id FROM customers c WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id AND o.status = 'OPEN' );

Gdy podzapytanie skorelowane zwraca wiele wierszy dla dużych tabel, rozważ przepisanie na JOIN lub użycie agregacji w podzapytaniu nieskorelowanym. Wydajność znacząco poprawią właściwe indeksy kluczy łączeń i filtrów. Zobacz: Indeksy w SQL: teoria i praktyka.

Wprowadzenie do CTE (Common Table Expressions)

CTE w SQL (Common Table Expressions) to nazwana, tymczasowa definicja wyniku zapytania zbudowana słowem kluczowym WITH w SQL. CTE poprawia czytelność, pozwala dzielić złożoną logikę na etapy i czasem ułatwia wielokrotne użycie tego samego wyniku w jednym zapytaniu.

SQL
1 2 3 4 5 6 7 8 WITH high_value_orders AS ( SELECT o.customer_id, o.id, o.total_amount FROM orders o WHERE o.total_amount > 1000 ) SELECT c.id, c.name, hvo.total_amount FROM customers c JOIN high_value_orders hvo ON hvo.customer_id = c.id;

CTE może być rekurencyjne (hierarchie, grafy), ale w codziennych analizach częściej używamy wariantu nierekurencyjnego jak wyżej.

Zagnieżdżanie i łańcuchy CTE

Możesz definiować wiele CTE w jednym WITH i budować łańcuch CTE, gdzie kolejne odwołują się do poprzednich. Zagnieżdżone CTE upraszczają złożone transformacje, ale unikaj przesadnie długich łańcuchów, które utrudniają debugowanie.

SQL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 WITH filtered_orders AS ( SELECT * FROM orders WHERE status = 'PAID' ), orders_by_customer AS ( SELECT customer_id, COUNT(*) AS cnt, SUM(total_amount) AS sum_amount FROM filtered_orders GROUP BY customer_id ), top_customers AS ( SELECT customer_id FROM orders_by_customer WHERE sum_amount > 10000 ) SELECT c.id, c.name, obc.sum_amount FROM customers c JOIN orders_by_customer obc ON obc.customer_id = c.id JOIN top_customers tc ON tc.customer_id = c.id;

Materiałowalność i wpływ na wydajność

Materiałowalność CTE oznacza, czy silnik wykona i utrzyma wynik CTE jako tymczasową strukturę, czy wstawi CTE w plan (inline). Różne silniki działają inaczej: w SQL Server i wielu systemach CTE zwykle jest inlinowane (to „sugar” nad podzapytaniem), w PostgreSQL 12+ CTE domyślnie jest inlinowane, ale można je wymusić lub zablokować.

SQL
1 2 3 4 5 6 7 8 9 -- PostgreSQL 12+: sterowanie materiałowalnością WITH expensive_calc AS MATERIALIZED ( SELECT * FROM big_table WHERE flag = true ), recent AS NOT MATERIALIZED ( SELECT * FROM expensive_calc WHERE created_at > now() - interval '7 days' ) SELECT COUNT(*) FROM recent;

Zawsze weryfikuj realny plan. Użyj EXPLAIN (ANALYZE, BUFFERS) i sprawdź koszty, skany, krotności oraz czy CTE zostało wstawione czy zmaterializowane. Więcej o tym, jak czytać plan i diagnozować wąskie gardła: Optymalizacja zapytań SQL i analiza planów wykonania.

Alternatywy: JOIN i funkcje okienkowe

Wiele podzapytań da się przepisać na JOIN, często szybciej i czytelniej. W przypadkach „top-N per grupa” lub „ostatni rekord” świetnie sprawdzają się funkcje okienkowe – to praktyczne alternatywy dla CTE w SQL lub podzapytania skorelowanego.

SQL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 -- Zamiast skorelowanego EXISTS: agregacja + JOIN SELECT c.id, c.name, x.cnt FROM customers c JOIN ( SELECT customer_id, COUNT(*) AS cnt FROM orders GROUP BY customer_id ) x ON x.customer_id = c.id WHERE x.cnt >= 3; -- Ostatnie zamówienie klienta: okno zamiast podzapytania WITH ranked AS ( SELECT o.*, ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.created_at DESC) AS rn FROM orders o ) SELECT * FROM ranked WHERE rn = 1;

Jeśli nie znasz okien, tu znajdziesz krótkie wprowadzenie i przykłady: Funkcje okienkowe w SQL.

Przykładowe przypadki użycia

CTE w SQL porządkuje złożone transformacje: filtrowanie, agregacje, a na końcu łączenia. Przykład: wyznacz „wartościowych klientów” na podstawie sumy zakupów i wybierz ich ostatnie zamówienie bez podzapytań skorelowanych.

SQL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 WITH paid AS ( SELECT * FROM orders WHERE status = 'PAID' ), agg AS ( SELECT customer_id, SUM(total_amount) AS sum_amount FROM paid GROUP BY customer_id ), valuable AS ( SELECT customer_id FROM agg WHERE sum_amount >= 10000 ), latest AS ( SELECT o.*, ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.created_at DESC) AS rn FROM paid o ) SELECT c.id, c.name, a.sum_amount, l.id AS last_order_id, l.created_at FROM valuable v JOIN customers c ON c.id = v.customer_id JOIN agg a ON a.customer_id = v.customer_id JOIN latest l ON l.customer_id = v.customer_id AND l.rn = 1;

Inne typowe przypadki użycia CTE: deduplikacja (ROW_NUMBER i filtr rn=1), przygotowanie etapów ETL (czyszczenie, wzbogacanie, agregacja), czy eksploracja hierarchii poprzez CTE rekurencyjne. W razie problemów z czasem wykonania pamiętaj o planie i indeksach – to zwykle większa różnica niż sam wybór CTE vs podzapytanie.

Kurs SkumajBazy — Czas w końcu nauczyć się SQLa

Kurs SkumajBazy — Czas w końcu nauczyć się SQLa

Kompleksowy kurs SQL dla programistów, analityków i wszystkich, którzy chcą efektywnie pracować z danymi. Od podstaw do zaawansowanych zapytań.