Podzapytania i CTE w T-SQL – jak pisać czytelne i wydajne zapytania
Dowiedz się, jak używać podzapytań i CTE w T-SQL, porównywać ich zastosowania, poprawiać czytelność kodu i wybierać wydajne rozwiązania w praktycznych przykładach.
Wprowadzenie: czym są podzapytania i CTE w T-SQL oraz typowe zastosowania
W T-SQL bardzo często zachodzi potrzeba rozbicia bardziej złożonego problemu na mniejsze etapy. Właśnie w tym celu stosuje się podzapytania oraz CTE (Common Table Expressions). Oba mechanizmy pozwalają budować zapytania warstwowo, dzięki czemu łatwiej wyrazić logikę filtrowania, agregacji, porównywania wyników czy przygotowania danych do dalszego przetwarzania.
Podzapytanie to zapytanie umieszczone wewnątrz innego zapytania. Może służyć do wyliczenia pojedynczej wartości, zwrócenia zestawu rekordów albo sprawdzenia, czy określony warunek jest spełniony. To naturalne rozwiązanie wszędzie tam, gdzie wynik jednego zapytania staje się wejściem dla drugiego.
CTE to nazwana, tymczasowa definicja zbioru wyników używana w obrębie pojedynczej instrukcji. Pozwala nadać fragmentowi logiki własną nazwę i potraktować go jak czytelny etap przetwarzania danych. W praktyce CTE bywa wybierane wtedy, gdy zapytanie staje się trudne do czytania lub gdy logikę warto zapisać w bardziej uporządkowanej formie.
Najważniejsza różnica na poziomie ogólnym polega na tym, że podzapytanie jest zwykle osadzone bezpośrednio w konkretnym miejscu instrukcji, natomiast CTE pozwala najpierw zdefiniować logiczny zbiór danych, a dopiero potem odwołać się do niego w głównej części zapytania. Z punktu widzenia programisty oznacza to przede wszystkim różnicę w czytelności, organizacji logiki oraz sposobie rozwijania bardziej złożonych operacji.
Typowe zastosowania podzapytań i CTE w T-SQL obejmują między innymi:
- filtrowanie danych na podstawie wartości obliczonych w innym zapytaniu,
- porównywanie rekordów z wartościami zagregowanymi, takimi jak średnia, suma, minimum lub maksimum,
- wydzielanie pośrednich etapów przetwarzania danych,
- upraszczanie złożonych instrukcji zawierających wiele warunków i zależności,
- przygotowanie danych do rankingów, numeracji i analiz opartych na funkcjach okna,
- budowanie zapytań rekurencyjnych, zwłaszcza przy pracy ze strukturami hierarchicznymi.
W praktyce nie chodzi tylko o to, aby zapytanie zwróciło poprawny wynik. Równie ważne jest, by było zrozumiałe, łatwe w utrzymaniu i możliwie wydajne. Podzapytania dobrze sprawdzają się w wielu krótszych i lokalnych scenariuszach, natomiast CTE często ułatwia uporządkowanie bardziej rozbudowanej logiki. Wybór między nimi nie zawsze wynika wyłącznie z wydajności — bardzo często decyduje również przejrzystość zapisu oraz wygoda dalszej pracy z zapytaniem.
Znajomość obu podejść jest podstawą pisania dojrzałych zapytań w T-SQL. Pozwala lepiej modelować logikę biznesową w SQL, ograniczać nadmierne zagnieżdżanie oraz budować rozwiązania, które są jednocześnie poprawne i czytelne dla innych osób pracujących z kodem.
Podzapytania w T-SQL: skorelowane vs nieskorelowane
Podzapytanie to zapytanie umieszczone wewnątrz innego zapytania. W T-SQL pełni rolę pomocniczego źródła danych lub warunku logicznego, który pozwala zawęzić wynik, porównać wartości albo obliczyć wartość pośrednią potrzebną w głównym zapytaniu. Najczęściej pojawia się w klauzulach SELECT, WHERE, HAVING oraz FROM.
Najważniejszy podział dotyczy tego, czy podzapytanie działa niezależnie od zapytania zewnętrznego, czy też odwołuje się do jego bieżącego wiersza. Na tej podstawie rozróżniamy podzapytania nieskorelowane i skorelowane. Różnica nie jest wyłącznie składniowa — wpływa też na sposób myślenia o logice zapytania, jego semantykę oraz często na wydajność.
Podczas szkoleń Cognity ten temat wraca regularnie — dlatego zdecydowaliśmy się omówić go również tutaj.
Podzapytanie nieskorelowane
Podzapytanie nieskorelowane można traktować jako zapytanie pomocnicze, które da się uruchomić samodzielnie. Nie odwołuje się ono do kolumn z zapytania zewnętrznego, więc jego wynik jest ustalany niezależnie od przetwarzanych wierszy głównego zbioru.
Takie podejście jest typowe wtedy, gdy potrzebna jest:
- jedna wartość porównawcza, na przykład średnia, minimum lub maksimum,
- lista wartości używana do filtrowania,
- zbiór pośredni traktowany jako źródło danych.
Semantycznie oznacza to zwykle: najpierw wyznacz wynik pomocniczy, a następnie użyj go w zapytaniu głównym. Dzięki temu nieskorelowane podzapytania są często łatwiejsze do zrozumienia, zwłaszcza gdy opisują prostą regułę biznesową, taką jak „pokaż rekordy większe od średniej” albo „wybierz wiersze należące do zestawu spełniającego określony warunek”.
Typowy przykład to sytuacja, w której filtrujemy wiersze na podstawie jednej wartości obliczonej globalnie dla całej tabeli, a nie osobno dla każdego rekordu.
Podzapytanie skorelowane
Podzapytanie skorelowane odwołuje się do kolumn z zapytania zewnętrznego. Nie jest więc samodzielne — jego znaczenie zależy od bieżącego wiersza analizowanego przez zapytanie główne. W praktyce oznacza to logikę typu: dla każdego wiersza zewnętrznego sprawdź warunek lub oblicz wynik na podstawie powiązanych danych.
To rozwiązanie jest przydatne, gdy decyzja o włączeniu rekordu do wyniku zależy od kontekstu konkretnego wiersza, na przykład:
- sprawdzenia, czy istnieją powiązane rekordy spełniające warunek,
- porównania wartości wiersza z danymi z jego grupy,
- wyszukania rekordu najlepszego, najnowszego lub wyróżnionego w obrębie powiązanego zbioru.
Semantyka podzapytania skorelowanego jest bardziej „wierszowa” niż „zbiorcza”. Zamiast jednego wspólnego wyniku pomocniczego dla całego zapytania, powstaje zależność między rekordem zewnętrznym a wynikiem sprawdzenia wykonywanego w jego kontekście. To bardzo wygodne przy warunkach opartych na relacjach między danymi, ale jednocześnie wymaga większej ostrożności przy interpretacji i optymalizacji.
Najważniejsze różnice
- Niezależność wykonania: podzapytanie nieskorelowane można rozumieć jako odrębne zapytanie pomocnicze, natomiast skorelowane zależy od zapytania zewnętrznego.
- Kontekst obliczeń: nieskorelowane operuje zwykle na całym zbiorze lub ustalonym podzbiorze, a skorelowane działa w kontekście bieżącego wiersza.
- Typowe użycie: nieskorelowane częściej służy do porównań z jedną wartością albo listą wartości, skorelowane zaś do sprawdzania istnienia relacji lub wyliczeń zależnych od konkretnego rekordu.
- Czytelność logiki: nieskorelowane bywa prostsze do odczytania przy ogólnych warunkach, podczas gdy skorelowane lepiej oddaje logikę „dla każdego rekordu sprawdź…”.
- Wpływ na wykonanie: skorelowane podzapytania częściej budzą pytania o koszt wykonania, ponieważ ich zależność od wiersza zewnętrznego może prowadzić do mniej oczywistego planu zapytania.
Przykładowe zastosowania w praktyce
Podzapytanie nieskorelowane sprawdza się wtedy, gdy chcemy porównać wartość rekordu z wartością odniesienia obliczoną raz dla całego zbioru. Dobrym przykładem jest filtrowanie rekordów powyżej średniej albo wybór elementów należących do listy zdefiniowanej przez osobne kryterium.
Podzapytanie skorelowane jest naturalnym wyborem, gdy trzeba odpowiedzieć na pytania w rodzaju: czy dla tego rekordu istnieje powiązany wpis, czy ten wynik jest największy w swojej grupie, czy bieżący wiersz spełnia warunek względem danych z tego samego obszaru biznesowego.
Na co uważać
Podzapytania są bardzo użyteczne, ale łatwo doprowadzić do sytuacji, w której zapis staje się mniej czytelny niż alternatywne podejście. Szczególnie dotyczy to zagnieżdżania wielu warstw warunków lub łączenia kilku podzapytań o podobnym celu. Warto też pamiętać, że podobny rezultat można często osiągnąć na różne sposoby, a wybór między nimi powinien uwzględniać zarówno przejrzystość logiki, jak i charakter danych.
W praktyce najważniejsze jest poprawne rozpoznanie semantyki problemu: jeśli potrzebny jest jeden wspólny wynik pomocniczy, zwykle lepiej pasuje podzapytanie nieskorelowane; jeśli warunek zależy od konkretnego rekordu i jego powiązań, częściej właściwe będzie podzapytanie skorelowane.
CTE (Common Table Expressions): składnia, działanie, czytelność i ograniczenia
CTE (Common Table Expression) to nazwany, tymczasowy zestaw wyników zdefiniowany bezpośrednio w treści zapytania za pomocą klauzuli WITH. W praktyce działa jak „lokalny widok” dostępny tylko dla pojedynczej instrukcji SELECT, INSERT, UPDATE, DELETE lub MERGE, która występuje bezpośrednio po definicji CTE.
CTE stosuje się przede wszystkim wtedy, gdy zapytanie warto podzielić na logiczne etapy: najpierw przygotować dane, potem je przefiltrować, pogrupować albo połączyć z innymi źródłami. Dzięki temu kod bywa bardziej czytelny niż rozbudowane zagnieżdżenia podzapytań, zwłaszcza gdy ten sam pośredni wynik ma być użyty w dalszej części instrukcji w bardziej przejrzysty sposób.
Podstawowa składnia CTE
Najprostsza postać wygląda następująco:
WITH CTE_Nazwa AS
(
SELECT kolumna1, kolumna2
FROM dbo.Tabela
WHERE warunek
)
SELECT *
FROM CTE_Nazwa;CTE można też definiować wielokrotnie w jednej instrukcji, oddzielając kolejne definicje przecinkami:
WITH DaneBazowe AS
(
SELECT Id, Kategoria, Wartosc
FROM dbo.Tabela
),
DaneFiltrowane AS
(
SELECT Id, Kategoria, Wartosc
FROM DaneBazowe
WHERE Wartosc > 100
)
SELECT *
FROM DaneFiltrowane;Warto pamiętać o praktycznej zasadzie składniowej: jeśli przed WITH znajduje się inna instrukcja, to zwykle musi ona zostać zakończona średnikiem.
Jak działa CTE w T-SQL
CTE nie jest trwałym obiektem w bazie danych. Nie zapisuje się jako osobna struktura, jak widok czy tabela. Istnieje tylko w kontekście pojedynczego polecenia, które następuje po jego definicji.
Z perspektywy autora zapytania CTE pełni rolę warstwy pośredniej, która:
- nadaje nazwę fragmentowi logiki,
- upraszcza odczyt bardziej złożonych zapytań,
- pozwala budować zapytanie etapami,
- ułatwia pracę z funkcjami okna, agregacją i filtrowaniem wyników pośrednich.
W praktyce CTE często jest traktowane jako sposób na uporządkowanie kodu, a nie jako mechanizm „przechowywania” danych do dalszego wielokrotnego użycia w różnych miejscach sesji.
Dlaczego CTE poprawia czytelność
Największą zaletą CTE jest zwykle czytelność. Zamiast budować jedną długą instrukcję z wieloma poziomami nawiasów i zagnieżdżeń, można wydzielić etapy obliczeń pod nazwami opisującymi ich sens biznesowy lub techniczny.
Przykładowo, zamiast pisać jedno złożone zapytanie z podzapytaniem w klauzuli FROM, można rozdzielić logikę na dwa kroki:
WITH SprzedazNaKlienta AS
(
SELECT KlientID, SUM(Kwota) AS SumaSprzedazy
FROM dbo.Sprzedaz
GROUP BY KlientID
)
SELECT KlientID, SumaSprzedazy
FROM SprzedazNaKlienta
WHERE SumaSprzedazy > 1000;Taki zapis ułatwia:
- szybsze zrozumienie intencji autora,
- łatwiejsze debugowanie,
- bezpieczniejsze rozwijanie zapytania w przyszłości,
- oddzielenie etapu obliczeń od etapu filtrowania lub prezentacji wyników.
Typowe zastosowania CTE
CTE sprawdza się szczególnie dobrze w kilku sytuacjach:
- porządkowanie złożonych zapytań – gdy jedna instrukcja staje się zbyt trudna do czytania,
- praca na wynikach pośrednich – gdy najpierw trzeba coś policzyć, a potem to przefiltrować lub połączyć,
- łączenie wielu kroków analitycznych – np. agregacja, a potem dalsza analiza,
- zapytania rekurencyjne – np. dla struktur hierarchicznych, takich jak drzewo kategorii czy relacje przełożony–podwładny.
Szczególnym przypadkiem jest CTE rekurencyjne, które odwołuje się do samego siebie. Pozwala ono przechodzić po strukturach hierarchicznych bez ręcznego rozpisywania wielu poziomów złączeń.
WITH Hierarchia AS
(
SELECT Id, ParentId, Nazwa, 0 AS Poziom
FROM dbo.Elementy
WHERE ParentId IS NULL
UNION ALL
SELECT e.Id, e.ParentId, e.Nazwa, h.Poziom + 1
FROM dbo.Elementy e
JOIN Hierarchia h ON e.ParentId = h.Id
)
SELECT *
FROM Hierarchia;To ważne zastosowanie CTE, ale samo projektowanie wydajnych zapytań rekurencyjnych wymaga już osobnej analizy.
CTE a inne konstrukcje – najważniejsze różnice
| Cecha | CTE | Podzapytanie | Tabela tymczasowa |
|---|---|---|---|
| Zakres działania | Jedna instrukcja | Fragment jednej instrukcji | Szerszy, w ramach sesji lub procedury |
| Czytelność | Wysoka przy złożonej logice | Dobra przy prostych przypadkach | Dobra przy etapowym przetwarzaniu |
| Nadawanie nazw etapom | Tak | Zwykle ograniczone | Tak |
| Możliwość rekurencji | Tak | Nie w tej formie | Nie jako cecha samej konstrukcji |
| Trwałość | Tylko na czas jednej instrukcji | Tylko wewnątrz zapytania | Tymczasowa, ale materializowana |
W uproszczeniu: CTE wybiera się głównie dla przejrzystości i organizacji logiki zapytania, a nie dlatego, że zawsze będzie szybsze.
Ograniczenia CTE
Mimo wygody CTE ma też kilka istotnych ograniczeń:
- działa tylko dla jednej bezpośrednio następującej instrukcji – nie można zdefiniować CTE i użyć go później w osobnym zapytaniu,
- nie jest obiektem fizycznym – nie można na nim tworzyć indeksów jak na tabeli tymczasowej,
- nie zawsze poprawia wydajność – jego główną zaletą jest struktura kodu,
- przy bardzo rozbudowanych łańcuchach CTE kod nadal może stać się trudny do utrzymania,
- rekurencja ma ograniczenia wykonania – domyślnie obowiązuje limit poziomów rekurencji, który można kontrolować opcją
MAXRECURSION.
Warto też unikać traktowania CTE jako uniwersalnego zamiennika dla każdej innej techniki. Jeśli dane pośrednie mają być używane wielokrotnie, wymagają indeksowania albo potrzebne są statystyki pomocne optymalizatorowi, samo CTE może nie być najlepszym wyborem.
Kiedy CTE jest dobrym wyborem
- Gdy zapytanie warto rozpisać na czytelne etapy.
- Gdy logika jest zbyt złożona, by zostawić ją w jednym podzapytaniu.
- Gdy potrzebna jest rekurencja.
- Gdy priorytetem jest zrozumiałość i utrzymanie kodu.
CTE w T-SQL to więc przede wszystkim narzędzie porządkujące. Pozwala budować zapytania w sposób bardziej deklaratywny, z wyraźnym podziałem na kroki, ale nie zwalnia z myślenia o planie wykonania, kosztach operacji i faktycznym sposobie przetwarzania danych przez silnik bazy.
CTE vs tabela tymczasowa: kiedy wybrać które rozwiązanie
CTE i tabela tymczasowa często służą do „rozbicia” złożonego zapytania na mniejsze etapy, ale nie są rozwiązaniami równoważnymi. CTE najlepiej sprawdza się wtedy, gdy chcemy poprawić czytelność pojedynczego zapytania i logicznie wydzielić jego fragment. Tabela tymczasowa jest lepszym wyborem, gdy wynik pośredni ma być używany wielokrotnie, wymaga dodatkowego indeksowania albo warto dać optymalizatorowi osobny etap przetwarzania danych.
Najprościej ująć to tak: CTE jest wygodnym sposobem zapisu logiki zapytania, a tabela tymczasowa jest fizycznym obiektem w tempdb, który można dalej przetwarzać. Ta różnica wpływa zarówno na czytelność kodu, jak i na wydajność. W czasie szkoleń Cognity ten temat bardzo często budzi ożywione dyskusje między uczestnikami, bo w praktyce wybór między tymi podejściami często decyduje nie tylko o estetyce zapisu, ale też o stabilności planu wykonania.
| Cecha | CTE | Tabela tymczasowa |
|---|---|---|
| Zakres użycia | Jedno bezpośrednio następujące zapytanie | Cała sesja lub blok procedury |
| Cel | Uproszczenie i uporządkowanie zapisu | Przechowanie wyniku pośredniego do dalszego użycia |
| Ponowne użycie | Ograniczone | Możliwe wielokrotnie |
| Indeksy | Brak możliwości indeksowania jak osobnego obiektu | Można tworzyć indeksy |
| Statystyki | Brak jak dla tabeli tymczasowej | Silnik może korzystać ze statystyk |
| Wpływ na plan wykonania | Część jednego planu | Często osobny etap, co bywa korzystne przy większych zbiorach |
| Zastosowanie | Krótsze, jednorazowe przekształcenia | Wieloetapowe przetwarzanie i cięższe operacje |
Kiedy wybrać CTE
- Gdy wynik pośredni jest potrzebny tylko raz.
- Gdy najważniejsza jest czytelność i logiczny podział zapytania.
- Gdy chcemy uniknąć tworzenia dodatkowych obiektów w tempdb.
- Gdy operujemy na umiarkowanie prostym przepływie danych i nie potrzebujemy osobnego strojenia etapu pośredniego.
CTE jest więc dobrym wyborem do zapytań analitycznych, raportowych i wszędzie tam, gdzie jeden blok SQL da się sensownie podzielić na kolejne nazwy logicznych kroków.
Kiedy wybrać tabelę tymczasową
- Gdy ten sam zestaw danych będzie używany w kilku instrukcjach.
- Gdy wynik pośredni jest duży i warto go najpierw zapisać, a potem filtrować lub łączyć dalej.
- Gdy potrzebne są indeksy przyspieszające kolejne operacje.
- Gdy ważne są statystyki, które pomagają optymalizatorowi lepiej oszacować liczność danych.
- Gdy chcemy rozdzielić złożone przetwarzanie na etapy i łatwiej diagnozować wydajność.
W praktyce tabele tymczasowe są często korzystniejsze w procedurach składowanych, w bardziej rozbudowanych procesach ETL oraz w zapytaniach, gdzie ten sam wynik pośredni uczestniczy w kilku joinach, agregacjach lub aktualizacjach.
Wydajność: nie zawsze „krócej” znaczy szybciej
Z perspektywy wydajności CTE nie powinno być automatycznie traktowane jako „lżejsze” rozwiązanie tylko dlatego, że zapis jest krótszy. Ponieważ CTE jest częścią jednego zapytania, optymalizator analizuje całość naraz. To bywa bardzo dobre przy prostych scenariuszach, ale przy bardziej złożonych operacjach może utrudnić uzyskanie stabilnego planu wykonania.
Tabela tymczasowa pozwala natomiast „zamrozić” wynik etapu pośredniego. Dzięki temu kolejne instrukcje operują już na konkretnym zbiorze danych, często z lepszymi oszacowaniami liczby wierszy. W efekcie rozwiązanie z tabelą tymczasową bywa szybsze mimo dodatkowego kroku zapisu do tempdb.
Ponowne użycie danych pośrednich
To jedna z najważniejszych praktycznych różnic. Jeśli potrzebujesz tego samego wyniku tylko w jednej instrukcji, CTE zwykle wystarczy. Jeśli jednak wynik ma zostać użyty kilka razy, tabela tymczasowa jest naturalnym wyborem.
Przykładowo, gdy najpierw wyliczasz zbiór rekordów spełniających określone warunki, a następnie:
- łączysz go z inną tabelą,
- tworzysz na jego podstawie raport,
- wykonujesz dodatkową agregację,
- albo aktualizujesz dane w kilku krokach,
to zapisanie tego zbioru do tabeli tymczasowej zwykle upraszcza kod i ogranicza powtarzanie tej samej logiki.
Indeksy i statystyki
CTE nie daje możliwości potraktowania wyniku jak niezależnej struktury, na której można założyć indeks dla kolejnych operacji. To ważne ograniczenie przy dużych zbiorach danych. Tabela tymczasowa może być natomiast przygotowana tak, aby dalsze łączenia i filtrowanie działały sprawniej.
Równie istotne są statystyki. Przy tabelach tymczasowych SQL Server może je wykorzystać do lepszych decyzji optymalizatora. W praktyce oznacza to, że przy bardziej złożonych zapytaniach plan wykonania dla tabeli tymczasowej może być trafniejszy niż dla jednego dużego zapytania z CTE.
Krótki przykład porównawczy
CTE jako jednorazowy etap logiczny:
WITH SprzedazKlientow AS (
SELECT CustomerID, SUM(TotalDue) AS SumaSprzedazy
FROM Sales.SalesOrderHeader
GROUP BY CustomerID
)
SELECT *
FROM SprzedazKlientow
WHERE SumaSprzedazy > 10000;Tabela tymczasowa, gdy wynik pośredni ma być używany dalej:
SELECT CustomerID, SUM(TotalDue) AS SumaSprzedazy
INTO #SprzedazKlientow
FROM Sales.SalesOrderHeader
GROUP BY CustomerID;
CREATE INDEX IX_SprzedazKlientow_CustomerID ON #SprzedazKlientow(CustomerID);
SELECT *
FROM #SprzedazKlientow
WHERE SumaSprzedazy > 10000;W pierwszym wariancie nacisk położony jest na prostotę zapisu. W drugim pojawia się dodatkowy krok, ale zyskujemy możliwość dalszego wykorzystania danych i ich strojenia.
Praktyczna zasada wyboru
- Wybierz CTE, jeśli chcesz uporządkować pojedyncze zapytanie i używasz wyniku tylko raz.
- Wybierz tabelę tymczasową, jeśli wynik pośredni jest duży, ma być użyty wielokrotnie albo wymaga indeksów i lepszej kontroli nad wydajnością.
W codziennej pracy nie chodzi więc o to, które rozwiązanie jest „lepsze” ogólnie, lecz które lepiej pasuje do konkretnego scenariusza: CTE wygrywa prostotą i czytelnością, tabela tymczasowa elastycznością i kontrolą nad przetwarzaniem.
Przykład 1: ranking klientów po sprzedaży (podzapytanie vs CTE z funkcjami okna)
Jednym z najczęstszych zadań w T-SQL jest przygotowanie rankingu klientów według wartości sprzedaży. Taki raport może służyć do wyświetlenia najlepszych klientów, podziału na pozycje w rankingu albo wskazania klientów z takim samym wynikiem sprzedażowym. To dobry przykład pokazujący praktyczną różnicę między klasycznym podzapytaniem a podejściem opartym o CTE i funkcje okna.
Załóżmy, że chcemy policzyć łączną sprzedaż dla każdego klienta i na tej podstawie nadać mu miejsce w rankingu. Najprostsze rozwiązanie można zbudować przy pomocy podzapytania, które najpierw agreguje dane, a potem wynik jest wykorzystywany w zapytaniu zewnętrznym.
SELECT s.CustomerID, s.TotalSales
FROM (
SELECT CustomerID, SUM(Amount) AS TotalSales
FROM Sales
GROUP BY CustomerID
) AS s
ORDER BY s.TotalSales DESC;Taki wariant dobrze sprawdza się wtedy, gdy potrzebujemy jedynie posortowanej listy klientów. Problem pojawia się w momencie, gdy chcemy dodać numer pozycji w rankingu albo rozróżnić sytuacje, w których kilku klientów ma identyczną wartość sprzedaży. Wtedy konstrukcja oparta wyłącznie na podzapytaniu staje się mniej wygodna i zwykle mniej czytelna.
W praktyce ranking znacznie lepiej zapisuje się z użyciem CTE oraz funkcji okna, takich jak ROW_NUMBER(), RANK() lub DENSE_RANK().
WITH CustomerSales AS (
SELECT CustomerID, SUM(Amount) AS TotalSales
FROM Sales
GROUP BY CustomerID
)
SELECT
CustomerID,
TotalSales,
RANK() OVER (ORDER BY TotalSales DESC) AS SalesRank
FROM CustomerSales
ORDER BY SalesRank;W tym podejściu logika jest podzielona na dwa czytelne kroki:
- najpierw liczymy sprzedaż per klient,
- następnie na gotowym zbiorze wyznaczamy pozycję rankingową.
Dzięki temu zapytanie jest łatwiejsze do zrozumienia i prostsze do rozwijania, na przykład o filtrowanie tylko pierwszych 10 klientów albo dodanie kolejnych miar.
| Podejście | Kiedy jest wygodne | Główna cecha |
|---|---|---|
| Podzapytanie | Proste agregacje i jednorazowe użycie wyniku | Zwięzłe, ale przy rankingu szybko robi się mniej czytelne |
| CTE + funkcje okna | Rankingi, top N, porównania między wierszami | Bardziej naturalny zapis z wyraźnym podziałem etapów |
W kontekście rankingu klientów szczególnie ważne jest zrozumienie różnicy między funkcjami okna:
ROW_NUMBER()nadaje unikalny numer każdemu wierszowi, nawet gdy wartości sprzedaży są takie same,RANK()przyznaje to samo miejsce dla remisów, ale pozostawia luki w numeracji,DENSE_RANK()również obsługuje remisy, ale bez luk.
Przykładowo, jeśli dwóch klientów ma identyczną sprzedaż i zajmuje pierwsze miejsce, to:
RANK()nada im pozycję 1, a kolejny klient dostanie pozycję 3,DENSE_RANK()nada im pozycję 1, a kolejny klient dostanie pozycję 2.
W raportach biznesowych wybór między tymi funkcjami ma znaczenie, ponieważ wpływa na interpretację wyników. Samo podzapytanie zwykle nie daje tak wygodnego i przejrzystego sposobu budowy rankingu jak funkcje okna użyte na wyniku CTE.
Jeśli celem jest tylko zebranie sum sprzedaży, podzapytanie bywa wystarczające. Jeśli jednak potrzebny jest pełny ranking klientów, wskazanie pozycji, remisów i łatwe rozszerzanie logiki, podejście z CTE + funkcjami okna jest zazwyczaj bardziej praktyczne i czytelniejsze.
6. Przykład 2: filtr „powyżej średniej” (AVG w podzapytaniu i warianty z CTE)
Jednym z najczęstszych wzorców w T-SQL jest filtrowanie rekordów na podstawie wartości agregowanej, na przykład znalezienie pozycji, których wartość jest większa od średniej. To dobry przykład pokazujący praktyczną różnicę między prostym podzapytaniem a podejściem opartym o CTE.
Załóżmy, że chcemy zwrócić wiersze z tabeli sprzedaży, w których wartość zamówienia jest wyższa niż średnia wartość wszystkich zamówień. Najprostsza wersja zwykle wykorzystuje podzapytanie z funkcją AVG().
SELECT SalesOrderID, TotalDue
FROM Sales.SalesOrderHeader
WHERE TotalDue > (
SELECT AVG(TotalDue)
FROM Sales.SalesOrderHeader
);Takie rozwiązanie jest zwięzłe i czytelne wtedy, gdy warunek jest prosty, a obliczenie średniej występuje tylko raz. Podzapytanie zwraca pojedynczą wartość, która następnie służy jako punkt odniesienia w klauzuli WHERE.
W praktyce często pojawia się jednak potrzeba pokazania także samej średniej w wyniku albo wykorzystania jej w kilku miejscach tego samego zapytania. Wtedy wygodniejszy może być wariant z CTE.
WITH AvgValue AS (
SELECT AVG(TotalDue) AS AvgTotalDue
FROM Sales.SalesOrderHeader
)
SELECT s.SalesOrderID, s.TotalDue, a.AvgTotalDue
FROM Sales.SalesOrderHeader s
CROSS JOIN AvgValue a
WHERE s.TotalDue > a.AvgTotalDue;W tym wariancie logika obliczenia średniej została nazwana i wydzielona do osobnego fragmentu. Dzięki temu łatwiej zobaczyć, skąd bierze się próg filtrowania, a samo zapytanie staje się bardziej przejrzyste, szczególnie gdy warunków lub obliczeń zaczyna przybywać.
Najprostsze porównanie podejść
| Podejście | Kiedy sprawdza się najlepiej | Główna zaleta |
|---|---|---|
| Podzapytanie z AVG() | Prosty filtr z jedną wartością agregowaną | Krótka i bezpośrednia składnia |
| CTE z AVG() | Gdy wynik agregacji ma być użyty także w SELECT lub logicznie wydzielony | Lepsza czytelność i łatwiejsza rozbudowa |
Wariant „powyżej średniej” w obrębie grupy
Często nie chodzi o średnią globalną, ale o średnią w ramach określonej grupy, na przykład dla klienta, kategorii lub miesiąca. W takim przypadku można użyć CTE, które najpierw oblicza średnią dla każdej grupy, a następnie łączy wynik z danymi źródłowymi.
WITH AvgByCustomer AS (
SELECT CustomerID, AVG(TotalDue) AS AvgTotalDue
FROM Sales.SalesOrderHeader
GROUP BY CustomerID
)
SELECT s.SalesOrderID, s.CustomerID, s.TotalDue, a.AvgTotalDue
FROM Sales.SalesOrderHeader s
JOIN AvgByCustomer a
ON s.CustomerID = a.CustomerID
WHERE s.TotalDue > a.AvgTotalDue;Taki zapis dobrze oddziela dwa etapy:
- najpierw obliczenie średniej dla grup,
- potem filtrowanie rekordów względem tej średniej.
To szczególnie przydatne wtedy, gdy warunek „powyżej średniej” jest tylko częścią większej logiki biznesowej.
Na co zwrócić uwagę
- Jeśli potrzebny jest tylko prosty próg porównania, podzapytanie bywa najbardziej naturalnym wyborem.
- Jeśli obliczenie średniej ma własne znaczenie logiczne, CTE poprawia czytelność.
- W przypadku średniej liczonych per grupa, CTE często pozwala lepiej uporządkować zapytanie niż zagnieżdżanie kolejnych podzapytań.
- Sam zapis nie gwarantuje lepszej wydajności — równie ważne są dane, indeksy i plan wykonania.
Wzorzec „powyżej średniej” jest prosty, ale bardzo reprezentatywny: pokazuje, że podzapytanie dobrze sprawdza się w krótkich filtrach, natomiast CTE ułatwia budowanie zapytań, które mają być nie tylko poprawne, ale też wygodne w utrzymaniu i rozwijaniu.
Przykład 3: wykrywanie duplikatów (GROUP BY/HAVING, ROW_NUMBER, CTE)
Wykrywanie duplikatów to jeden z najczęstszych scenariuszy, w których łączy się kilka technik T-SQL. W praktyce duplikatem może być zarówno wiersz identyczny w całości, jak i rekord powielony według wybranych kolumn, na przykład adresu e-mail, numeru dokumentu czy zestawu pól opisujących tę samą encję biznesową. Kluczowe jest więc najpierw ustalenie, co dokładnie oznacza duplikat w danym przypadku.
Najprostsze podejście opiera się na GROUP BY z HAVING. Taki wariant dobrze sprawdza się wtedy, gdy celem jest szybkie ustalenie, które wartości występują więcej niż raz. Jest to rozwiązanie zwięzłe i czytelne, szczególnie gdy interesuje nas sama informacja o istnieniu powtórzeń albo liczba ich wystąpień. Ograniczeniem tej metody jest jednak to, że skupia się ona na grupach, a nie na pojedynczych rekordach, więc sama w sobie nie wskazuje jeszcze, który konkretny wiersz należy zachować, a który uznać za nadmiarowy.
Gdy potrzebna jest praca na poziomie konkretnych rekordów, lepiej sprawdza się ROW_NUMBER. Funkcja ta pozwala nadać numer każdemu wierszowi wewnątrz grupy zdefiniowanej przez zestaw kolumn uznanych za kryterium duplikatu. Dzięki temu można odróżnić pierwszy rekord od kolejnych i łatwo wskazać te, które są kandydatami do usunięcia, archiwizacji albo dalszej analizy. To podejście jest szczególnie przydatne tam, gdzie trzeba zachować jeden „właściwy” rekord według określonej reguły, na przykład najnowszy lub najstarszy.
W takich przypadkach często pojawia się CTE jako warstwa porządkująca logikę zapytania. Samo CTE nie służy do wykrywania duplikatów, ale bardzo dobrze porządkuje rozwiązanie oparte na numerowaniu wierszy. Pozwala najpierw przygotować zbiór z wyliczoną numeracją, a następnie w czytelny sposób odfiltrować rekordy z numerem większym niż jeden. Dzięki temu zapytanie jest zwykle prostsze do zrozumienia niż rozbudowana konstrukcja zapisana w jednej instrukcji.
- GROUP BY/HAVING — dobre do szybkiego znalezienia wartości powtarzających się i policzenia skali problemu.
- ROW_NUMBER — dobre do wskazywania konkretnych nadmiarowych wierszy w obrębie duplikatów.
- CTE — dobre do poprawy czytelności rozwiązania, zwłaszcza gdy wykrywanie duplikatów łączy się z dalszym filtrowaniem.
Warto też pamiętać, że wykrywanie duplikatów nie zawsze oznacza fizyczne usuwanie danych. Czasem chodzi jedynie o raport kontrolny, czasem o oznaczenie rekordów do przeglądu, a czasem o przygotowanie danych do dalszego czyszczenia. Z tego powodu wybór techniki zależy nie tylko od samego wykrycia powtórzeń, ale również od tego, co chcemy zrobić z wynikiem.
Najbezpieczniejsze podejście wygląda zwykle tak: najpierw zidentyfikować grupy duplikatów, potem określić regułę wyboru rekordu, który ma pozostać, a dopiero na końcu wykonywać operacje modyfikujące dane. T-SQL daje do tego kilka dróg, ale w praktyce najczęściej używa się właśnie kombinacji agregacji, funkcji okna i CTE, bo zapewnia ona dobry balans między prostotą a kontrolą nad wynikiem.
Wskazówki i pułapki: wielokrotne użycie CTE, złożoność planu, optymalizacja i czytelność kodu
CTE i podzapytania pomagają porządkować logikę zapytań, ale sama poprawa czytelności nie gwarantuje jeszcze dobrej wydajności. W praktyce warto pamiętać, że zapis „ładniejszy” dla człowieka nie zawsze oznacza zapis „tańszy” dla optymalizatora. Dlatego przy wyborze między CTE, podzapytaniem, tabelą tymczasową czy innym podejściem trzeba brać pod uwagę nie tylko estetykę, lecz także sposób wykonania zapytania.
Jedna z najczęstszych pułapek dotyczy wielokrotnego użycia CTE. CTE bywa traktowane jak tymczasowy, gotowy wynik, do którego można odwoływać się wielokrotnie bez kosztu. W praktyce nie należy tego zakładać. Jeśli ta sama logika jest wykorzystywana kilka razy w jednym zapytaniu, może to prowadzić do powtórnego przetwarzania tych samych danych albo do bardziej złożonych planów wykonania. Przy większych zbiorach danych taki zapis potrafi stać się trudny do przewidzenia pod względem wydajności.
Drugim problemem jest narastająca złożoność planu wykonania. Rozbudowane, wielopoziomowe CTE lub głęboko zagnieżdżone podzapytania mogą wyglądać logicznie na etapie pisania, ale po stronie silnika bazy danych przełożyć się na trudniejszy do optymalizacji plan. Im więcej warstw abstrakcji, filtrów i obliczeń, tym większe ryzyko, że końcowe zapytanie będzie trudniejsze do diagnozy, a jego zachowanie mniej intuicyjne.
Warto też uważać na sytuacje, w których czytelność zostaje osiągnięta kosztem nadmiernego rozbicia logiki. Dzielenie jednego zapytania na wiele kolejnych CTE może poprawić odbiór kodu, ale czasem prowadzi do sztucznego komplikowania prostego problemu. Jeżeli każda kolejna część wnosi tylko niewielką zmianę, kod może stać się bardziej rozwlekły niż przejrzysty. Dobra praktyka polega na takim podziale, który oddziela wyraźne etapy przetwarzania, a nie tworzy warstwy po warstwie bez realnej korzyści.
- Nie zakładaj, że CTE materializuje wynik. To wygodna konstrukcja składniowa, ale nie zastępuje automatycznie fizycznego zapisania danych pośrednich.
- Sprawdzaj plan wykonania. Nawet bardzo czytelne zapytanie może wykonywać zbędne skany, sortowania lub powtórne przeliczenia.
- Unikaj nadmiernego zagnieżdżania. Jeśli logika staje się trudna do prześledzenia, rośnie ryzyko błędów i problemów wydajnościowych.
- Dbaj o jednoznaczne nazewnictwo etapów. Dobrze nazwane CTE lub logiczne bloki zapytania ułatwiają utrzymanie kodu i analizę działania.
- Nie myl czytelności z optymalnością. Kod łatwy do przeczytania powinien nadal być weryfikowany pod kątem kosztu wykonania.
W kontekście optymalizacji szczególnie ważne jest, aby patrzeć na zapytanie całościowo. Problem wydajności często nie wynika z samego użycia CTE lub podzapytania, lecz z filtrowania danych zbyt późno, łączenia zbyt dużych zbiorów albo wykonywania tych samych obliczeń wielokrotnie. Czytelny kod powinien wspierać optymalizację: ułatwiać zrozumienie przepływu danych, pokazywać miejsce agregacji i pozwalać szybciej wykryć zbędne operacje.
Z perspektywy utrzymania kodu najlepiej sprawdza się podejście pragmatyczne. Jeśli proste podzapytanie dobrze oddaje intencję i nie komplikuje planu, nie ma potrzeby zastępować go bardziej rozbudowaną konstrukcją. Jeśli natomiast CTE porządkuje wieloetapową logikę i poprawia możliwość analizy zapytania, warto z niego skorzystać — ale bez automatycznego zakładania, że będzie to wariant najlepszy wydajnościowo. Najważniejsza zasada brzmi: najpierw czytelna logika, potem weryfikacja rzeczywistego kosztu wykonania.
Jeśli chcesz poznać więcej takich przykładów, zapraszamy na szkolenia Cognity, gdzie rozwijamy ten temat w praktyce.