W SQL agregacja pozwala zamienić duży zbiór wierszy w jeden sensowny wynik: sumę sprzedaży, średnią ocenę, liczbę zamówień albo listę unikalnych wartości. To właśnie te operacje najczęściej decydują o tym, czy raport pokazuje realny obraz danych, czy tylko przypadkową tabelę rekordów.
W tym artykule pokazuję, jak działają funkcje agregujące w SQL, kiedy używać `GROUP BY` i `HAVING`, czym różni się `COUNT(*)` od `COUNT(kolumna)` oraz kiedy lepiej przejść na funkcje okienkowe. Dorzucam też przykłady i pułapki, które w praktyce psują wyniki częściej, niż wielu osobom się wydaje.
Najważniejsze rzeczy, które warto zapamiętać
- Agregacja w SQL zamienia wiele wierszy w jeden wynik dla całej tabeli albo dla konkretnej grupy.
- `WHERE` filtruje wiersze przed agregacją, a `HAVING` filtruje już gotowe grupy.
- `COUNT(*)` liczy wiersze, a `COUNT(kolumna)` pomija wartości `NULL`.
- `GROUP BY` ma sens tylko wtedy, gdy grupujesz po kolumnach, które naprawdę odpowiadają pytaniu biznesowemu.
- Funkcje okienkowe zwracają wynik dla każdego wiersza, a nie jeden wynik dla całej grupy.
- Przy dużych tabelach największe znaczenie mają czytelność zapytania, poprawne grupowanie i kontrola duplikatów po `JOIN`.
Jak działają funkcje agregujące SQL
W praktyce traktuję je jako narzędzie do odpowiedzi na pytania typu: „ile?”, „jaka średnia?”, „który rekord jest najmniejszy lub największy?”, „ile mamy unikalnych wartości?”. Silnik SQL bierze wiele wierszy, przetwarza je według określonej reguły i zwraca jedną wartość albo jedną wartość na grupę.
Najczęściej chodzi o funkcje takie jak `COUNT`, `SUM`, `AVG`, `MIN` i `MAX`, ale w wielu bazach dostępne są też agregacje tekstowe, statystyczne czy geograficzne. Ważny szczegół: większość agregatów ignoruje `NULL`, więc brak danych nie zawsze jest liczony tak samo jak zero. To różnica, która potrafi całkowicie zmienić interpretację raportu.
Warto też pamiętać, że agregacja nie jest „magiczna”. To po prostu obliczenie wykonane na zbiorze wartości. Jeśli wiersze są zdublowane przez zły `JOIN`, agregat też pokaże zafałszowany wynik. Dlatego przed sięgnięciem po sumę lub średnią zawsze sprawdzam, czy źródłowe dane są w ogóle na poziomie, na którym chcę liczyć.
Od tego punktu najważniejsze staje się już nie samo „liczenie”, ale wybór właściwej funkcji i właściwego sposobu grupowania.
Najważniejsze funkcje i kiedy używać każdej z nich
| Funkcja | Co zwraca | Kiedy się przydaje | Na co uważać |
|---|---|---|---|
COUNT(*) |
Liczbę wszystkich wierszy | Gdy chcesz policzyć rekordy bez względu na wartości w kolumnach | Nie pomija wierszy z `NULL` |
COUNT(kolumna) |
Liczbę niepustych wartości w kolumnie | Gdy interesują Cię tylko faktycznie uzupełnione dane | Pomija `NULL`, więc wynik bywa niższy niż liczba wierszy |
COUNT(DISTINCT kolumna) |
Liczbę unikalnych wartości | Do policzenia klientów, produktów albo miast bez duplikatów | Może być kosztowniejsze niż zwykły `COUNT` |
SUM(kolumna) |
Suma wartości liczbowych | Do raportowania sprzedaży, kosztów, czasu pracy | Ignoruje `NULL`, ale nie naprawi błędów pochodzących z duplikatów |
AVG(kolumna) |
Średnią arytmetyczną | Do oceny średniego koszyka, czasu realizacji, oceny produktu | Łatwo zafałszować wynik przez skrajne wartości i błędne grupy |
MIN(kolumna) / MAX(kolumna)
|
Najmniejszą lub największą wartość | Do zakresów dat, cen, wyników, ocen | Na tekstach działa porządek leksykograficzny, nie „logiczny” |
STRING_AGG, GROUP_CONCAT, LISTAGG
|
Połączony tekst z wielu wierszy | Do list nazw, tagów, kategorii albo miast | Nazwy i składnia różnią się zależnie od bazy danych |
Jeśli mam wskazać jeden praktyczny wniosek, to ten: najpierw dobieram funkcję do pytania, a dopiero później myślę o formatowaniu wyniku. Inaczej łatwo zrobić zapytanie, które wygląda poprawnie, ale odpowiada na zupełnie inne pytanie niż to, które zadał biznes.
To prowadzi prosto do `GROUP BY`, bo bez niego większość agregacji pozostaje tylko liczeniem „po całości”, a nie na poziomie interesujących nas segmentów.
Dlaczego `GROUP BY` decyduje o sensie wyniku
`GROUP BY` dzieli wiersze na podzbiory według wybranych kolumn. Dzięki temu zamiast jednego wyniku dla całej tabeli dostajesz wynik osobno dla każdego miasta, klienta, kategorii produktu albo miesiąca. W analizie danych to zwykle ważniejsze niż sama funkcja agregująca, bo to właśnie grupa mówi, co porównujesz.
Najprostszy przykład wygląda tak:
SELECT
city,
SUM(amount) AS revenue
FROM orders
GROUP BY city;
Jeżeli tabela `orders` ma zamówienia z Warszawy, Krakowa i Gdańska, to wynik będzie miał trzy wiersze. Każdy z nich pokaże sumę dla jednego miasta. Właśnie dlatego `GROUP BY` jest tak istotny: bez niego nie dostaniesz porównań między segmentami, tylko jedną sumę dla wszystkiego.
Warto też odróżnić `WHERE` od `HAVING`:
| Element | Na czym działa | Moment użycia | Przykład zastosowania |
|---|---|---|---|
WHERE |
Na pojedynczych wierszach | Przed agregacją | Wykluczenie anulowanych zamówień |
HAVING |
Na gotowych grupach | Po agregacji | Pokazanie tylko miast ze sprzedażą powyżej 10 000 |
To rozróżnienie jest krytyczne. Jeśli filtrujesz źle, wynik może być formalnie poprawny, ale logicznie bezużyteczny. Następnie pokażę na konkretnych przykładach, jak to wygląda w praktyce, bo tu najlepiej widać różnicę między dobrą a przypadkową agregacją.

Przykłady, które od razu pokazują różnicę
Załóżmy prostą tabelę `orders`:
| order_id | customer | city | amount |
|---|---|---|---|
| 1 | Anna | Warszawa | 120 |
| 2 | Anna | Warszawa | 80 |
| 3 | Bartek | Kraków | 200 |
| 4 | Celina | Kraków | 150 |
| 5 | Damian | Gdańsk | 90 |
Jeśli chcę policzyć sprzedaż per miasto, użyję:
SELECT
city,
SUM(amount) AS revenue
FROM orders
GROUP BY city
ORDER BY revenue DESC;
Wynik będzie prosty: Warszawa 200, Kraków 350, Gdańsk 90. Taki raport jest wartościowy, bo od razu pokazuje, gdzie sprzedaż jest najwyższa, a gdzie najsłabsza.
Gdy interesuje mnie liczba zamówień i liczba klientów, lepszy będzie wariant z `COUNT`:
SELECT
COUNT(*) AS all_orders,
COUNT(amount) AS paid_orders,
COUNT(DISTINCT customer) AS unique_customers
FROM orders;
Tu różnica jest ważna: `COUNT(*)` policzy wszystkie wiersze, `COUNT(amount)` pominie puste wartości, a `COUNT(DISTINCT customer)` pokaże, ilu mamy unikalnych klientów. W praktyce to częsty zestaw do szybkiej kontroli jakości danych i skali ruchu.
Jeśli chcę wyłapać tylko te miasta, w których sprzedaż przekroczyła 200 zł, użyję `HAVING`:
SELECT
city,
SUM(amount) AS revenue
FROM orders
GROUP BY city
HAVING SUM(amount) > 200;
To już nie jest zwykłe filtrowanie rekordów. Tu decyduje wynik agregacji, a nie pojedyncza wartość w wierszu. Ten przykład dobrze pokazuje, jak szybko można przejść od prostego liczenia do naprawdę użytecznego raportowania.
Skoro widać już podstawy, pora przejść do błędów, które najczęściej niszczą wiarygodność takich zapytań.
Najczęstsze błędy, które psują raporty
| Błąd | Co się dzieje | Jak to naprawić |
|---|---|---|
| Mylenie `WHERE` z `HAVING` | Filtr działa w złym momencie i wynik przestaje odpowiadać pytaniu | Wybieraj `WHERE` dla wierszy, `HAVING` dla grup |
| Używanie `COUNT(kolumna)` zamiast `COUNT(*)` | Braki `NULL` znikają z liczenia | Sprawdź, czy liczysz rekordy, czy tylko niepuste wartości |
| Duplikaty po `JOIN` | `SUM` i `COUNT` rosną szybciej niż powinny | Agreguj na właściwym poziomie albo uprzednio zredukuj dane w CTE |
| Wybieranie kolumn nieujętych w `GROUP BY` | Zapytanie bywa odrzucone albo zwraca przypadkowy wiersz z grupy | Dodaj kolumnę do grupowania albo użyj osobnej agregacji |
| Agregowanie danych bez normalizacji tekstu | `Warszawa`, `warszawa` i ` Warszawa ` stają się trzema różnymi grupami | Oczyść dane przed raportowaniem |
| Zbyt wczesne zaokrąglanie średnich | Różnice rozchodzą się na kolejnych poziomach analizy | Zaokrąglaj na końcu, nie w środku obliczeń |
Najgroźniejszy błąd to zwykle nie sama składnia, tylko nieświadome powielenie rekordów. Widziałem raporty, w których jedna faktura pojawiała się cztery razy po połączeniu z tabelą pozycji i nagle sprzedaż wyglądała na 400% wyższą niż w rzeczywistości. SQL nie ostrzeże przed takim błędem, bo dla silnika to nadal poprawne dane.
Jeżeli Twoje zapytanie ma nie tylko liczyć, ale też pokazywać wynik obok każdego rekordu, wtedy często lepsze okazują się funkcje okienkowe. I właśnie to odróżnienie warto znać.
Kiedy lepiej użyć funkcji okienkowych
Funkcje okienkowe są bliskie agregacji, ale nie robią tego samego. Agregacja zmniejsza liczbę wierszy, a funkcja okienkowa zostawia wszystkie wiersze i dokleja do nich wynik obliczenia. Dzięki temu możesz mieć jednocześnie szczegół i kontekst.
Przykład:
SELECT
order_id,
city,
amount,
SUM(amount) OVER (PARTITION BY city) AS city_total
FROM orders;
Każdy wiersz nadal istnieje, ale obok niego pojawia się suma dla całego miasta. To bardzo przydatne, gdy chcesz pokazać udział pojedynczego zamówienia w całości albo policzyć ranking bez utraty szczegółów.
Jeszcze lepiej widać to przy sumach narastających:
SELECT
order_id,
city,
amount,
SUM(amount) OVER (
PARTITION BY city
ORDER BY order_id
) AS running_total
FROM orders;
Takie zapytanie daje wynik dla każdego kolejnego rekordu. W raportach sprzedażowych, finansowych i operacyjnych to często wygodniejsze niż klasyczne `GROUP BY`, bo nie tracisz informacji o kolejnych zdarzeniach. Ja zwykle sięgam po okna wtedy, gdy klasyczna agregacja zaczyna „zjadać” zbyt dużo kontekstu.
Na końcu zostaje jeszcze jedno pytanie: jak pisać takie zapytania, żeby były czytelne i nie sprawiały problemów przy rozbudowie.
Jak pisać agregacje, które da się utrzymać
- Używaj czytelnych aliasów, np. `revenue`, `orders_count`, `avg_rating` zamiast przypadkowych nazw.
- Oddziel filtrację od agregacji, żeby od razu było widać, co dzieje się przed `GROUP BY`, a co po nim.
- Jeśli zapytanie robi się długie, rozbij je na CTE lub podzapytania. To zwykle poprawia czytelność bardziej niż kolejne komentarze.
- Sprawdzaj, czy kolumny używane w `GROUP BY` mają sens biznesowy. Grupa po złym polu daje poprawną składnię, ale błędny raport.
- Przy dużych tabelach zwracaj uwagę na indeksy, ale nie zakładaj, że indeks rozwiąże każdy problem wydajnościowy. Czasem silnik i tak musi przeskanować duży fragment danych.
- Jeśli raport uruchamia się wielokrotnie, rozważ tabelę agregującą albo widok materializowany zamiast liczenia wszystkiego od zera.
W praktyce najlepsze zapytania agregujące są zwykle dość proste, tylko dobrze ułożone. Nadmierna komplikacja rzadko daje lepszy wynik, a dużo częściej utrudnia późniejszą kontrolę jakości danych.
Co warto wdrożyć od razu w swoich zapytaniach
Jeśli mam wybrać kilka nawyków, które naprawdę poprawiają jakość pracy z danymi, to byłyby to: jawne `GROUP BY`, świadome użycie `COUNT`, sprawdzanie `NULL` oraz kontrola duplikatów po `JOIN`. Te cztery rzeczy rozwiązują zaskakująco dużo problemów, zanim jeszcze zaczniesz myśleć o optymalizacji.
Druga rzecz to testowanie na małym wycinku danych. Zanim zaufam zapytaniu na całej tabeli, porównuję wyniki z ręcznie policzonym fragmentem albo z prostym przekrojem 3-5 rekordów. To szybki sposób na wyłapanie błędów, których sama składnia nie pokaże. W analizie danych taka dyscyplina oszczędza więcej czasu niż późniejsze poprawki w gotowym raporcie.
