GROUP BY w SQL: skąd biorą się błędne sumy i jak robić agregacje, które mają sens

Dowiedz się, jak działa GROUP BY w SQL, czym różnią się WHERE i HAVING oraz jak unikać błędnych sum po JOIN. Praktyczny przewodnik po agregacjach: SUM, COUNT, AVG, MIN i MAX.
31 sierpnia 2026
blog

Do czego służy GROUP BY i kiedy warto go używać w analizie danych?

GROUP BY w SQL służy do grupowania wierszy o tych samych wartościach w wybranych kolumnach, aby policzyć dla każdej grupy jedną wartość zbiorczą, na przykład sumę, średnią, liczbę rekordów, minimum lub maksimum. Zamiast analizować każdy rekord osobno, można dzięki temu otrzymać podsumowanie na poziomie kategorii, daty, klienta, produktu, regionu albo innego wymiaru danych.

W praktyce używa się go wtedy, gdy interesuje Cię odpowiedź typu: ile było zamówień na klienta, jaki był przychód według miesiąca, jaka jest średnia wartość transakcji dla każdego produktu. Jeśli w zapytaniu pojawiają się funkcje agregujące, takie jak SUM(), COUNT(), AVG(), MIN() czy MAX(), a wynik ma być rozbity na konkretne kategorie, to zwykle potrzebne jest właśnie GROUP BY.

Najważniejsza zasada jest taka, że po grupowaniu jedna grupa daje jeden wiersz wyniku. Dlatego kolumny zwracane w SELECT powinny być albo ujęte w GROUP BY, albo objęte agregacją. To wymusza spójność logiczną: jeśli liczysz sumę sprzedaży według miesiąca, to wynik powinien być pokazany dla miesięcy, a nie jednocześnie dla pojedynczych wierszy sprzedaży.

W analizie danych GROUP BY warto stosować wtedy, gdy chcesz przejść z poziomu danych szczegółowych do poziomu syntetycznego i porównywać grupy między sobą. Nie jest natomiast potrzebne, gdy chcesz jedynie filtrować lub przeglądać pojedyncze rekordy bez podsumowań. Krótko mówiąc: GROUP BY służy do tworzenia sensownych agregacji, czyli takich, które odpowiadają na pytania o wynik dla określonych grup danych, a nie dla każdego rekordu z osobna.

Jak działają funkcje agregujące SUM, COUNT, AVG, MIN i MAX w SQL?

Funkcje agregujące w SQL liczą jedną wartość na podstawie wielu wierszy. Mogą działać na całym zbiorze danych albo osobno dla każdej grupy utworzonej przez GROUP BY. Innymi słowy: zamiast zwracać jeden wynik na każdy wiersz, zwracają jeden wynik na całą grupę lub cały zestaw.

Najważniejsza zasada praktyczna jest taka, że większość funkcji agregujących pomija wartości NULL. To wpływa na wynik i trzeba to rozumieć poprawnie, zwłaszcza przy liczeniu średnich i liczby rekordów.

FunkcjaCo robiJak traktuje NULL
SUM(kolumna)Sumuje wartości liczbowePomija NULL
COUNT(*)Zlicza wszystkie wierszeLiczy także wiersze z NULL
COUNT(kolumna)Zlicza niepuste wartości w kolumniePomija NULL
AVG(kolumna)Liczy średnią arytmetycznąPomija NULL
MIN(kolumna)Zwraca najmniejszą wartośćPomija NULL
MAX(kolumna)Zwraca największą wartośćPomija NULL

SUM działa tylko na danych liczbowych i zwraca ich sumę. Jeśli w danej grupie są wartości 10, 20 i NULL, wynik to 30, a nie błąd i nie NULL. AVG działa podobnie, ale dzieli sumę przez liczbę nie-NULL-owych wartości. Dla 10, 20, NULL średnia to 15, a nie 10.

COUNT ma dwa podstawowe warianty, które często są mylone. COUNT(*) liczy wszystkie wiersze, niezależnie od zawartości kolumn. COUNT(kolumna) liczy tylko te wiersze, w których wskazana kolumna nie jest NULL. To istotna różnica, bo oba zapytania mogą dać inny wynik dla tej samej tabeli.

MIN i MAX zwracają odpowiednio najmniejszą i największą wartość w grupie. Działają nie tylko na liczbach, ale też na typach takich jak daty czy tekst, zgodnie z regułami porównywania obowiązującymi w danym silniku SQL. Jeśli wszystkie wartości w agregowanej kolumnie są NULL, wynik takich funkcji będzie zwykle NULL.

Przykład: SELECT department_id, COUNT(*), AVG(salary), MAX(salary) FROM employees GROUP BY department_id; zwróci po jednym wierszu na każdy dział. Dla każdego działu SQL policzy liczbę pracowników, średnią pensję i najwyższą pensję w obrębie tej grupy.

W praktyce trzeba pamiętać, że wynik agregacji zależy od tego, co dokładnie jest agregowane i na jakim poziomie grupowania. Same funkcje SUM, COUNT, AVG, MIN i MAX są proste, ale dają poprawne wyniki tylko wtedy, gdy zbiór wejściowy odpowiada temu, co naprawdę chcesz policzyć.

Dlaczego dostaję błąd, gdy w SELECT mam kolumny bez agregacji i bez GROUP BY?

Bo zapytanie miesza dwa różne poziomy szczegółowości danych. Funkcje agregujące, takie jak SUM(), COUNT() czy AVG(), zwracają jedną wartość dla całej grupy wierszy. Z kolei zwykła kolumna w SELECT, na przykład customer_id albo order_date, wskazuje konkretną wartość z konkretnego wiersza. Jeśli nie użyjesz GROUP BY, silnik SQL nie wie, który pojedynczy customer_id lub którą datę ma zwrócić obok jednej zagregowanej sumy.

Przykładowo w zapytaniu SELECT customer_id, SUM(amount) FROM orders suma może zostać policzona dla wszystkich rekordów, ale kolumna customer_id nie ma jednej oczywistej wartości. W tabeli może być wielu klientów, więc wynik byłby niejednoznaczny. Właśnie dlatego większość baz danych zgłasza błąd: każda kolumna w SELECT musi albo uczestniczyć w agregacji, albo znaleźć się w GROUP BY.

Poprawna logika jest prosta: jeśli chcesz wynik na poziomie klienta, piszesz GROUP BY customer_id; jeśli chcesz jedną sumę dla całej tabeli, usuwasz customer_id z SELECT. Błąd nie jest więc ograniczeniem „składni dla zasady”, tylko ochroną przed wynikiem, który byłby przypadkowy albo semantycznie niepoprawny.

Warto też wiedzieć, że niektóre silniki SQL w określonych ustawieniach dopuszczają takie zapytania, ale wtedy zwracana wartość nieagregowanej kolumny może być arbitralna lub zależna od implementacji. To zachowanie bywa mylące, dlatego w praktyce należy trzymać się zasady: każda kolumna nieobjęta funkcją agregującą musi być ujęta w GROUP BY.

Czym różni się WHERE od HAVING i kiedy używać którego?

WHERE filtruje wiersze przed grupowaniem i agregacją, a HAVING filtruje wynik po wykonaniu GROUP BY i obliczeniu funkcji agregujących. To podstawowa różnica: WHERE działa na pojedynczych rekordach, a HAVING na całych grupach.

Jeśli chcesz odrzucić dane wejściowe, użyj WHERE. Przykładowo: ograniczenie do zamówień z 2024 roku albo tylko do aktywnych klientów powinno znaleźć się właśnie tam. Dzięki temu do agregacji trafia mniej wierszy, a wynik jest logicznie poprawny i zwykle wydajniejszy.

HAVING stosujesz wtedy, gdy warunek dotyczy już wartości zagregowanej, na przykład sumy, liczby lub średniej. Przykład: jeśli chcesz pokazać tylko te działy, które mają SUM(sprzedaz) > 10000, taki warunek nie może trafić do WHERE, bo suma jeszcze nie istnieje na etapie filtrowania wierszy.

Typowy zapis wygląda tak: SELECT dzial, SUM(kwota) FROM sprzedaz WHERE rok = 2024 GROUP BY dzial HAVING SUM(kwota) > 10000. W tym przykładzie WHERE najpierw ogranicza rekordy do 2024 roku, potem GROUP BY tworzy grupy według działu, a HAVING zostawia tylko te grupy, których suma przekracza 10000.

W praktyce zasada jest prosta: warunki na kolumnach źródłowych dawaj do WHERE, a warunki na wynikach agregacji do HAVING. Nie warto przenosić do HAVING czegoś, co można odfiltrować wcześniej w WHERE, bo to zaciemnia logikę zapytania i może niepotrzebnie zwiększać ilość danych przetwarzanych podczas grupowania.

💡 Protip: Zapamiętaj prostą regułę: WHERE służy do filtrowania danych wejściowych, a HAVING do filtrowania już policzonych grup. Jeśli warunek nie używa agregacji, zwykle powinien trafić do WHERE — zapytanie będzie czytelniejsze i często wydajniejsze.

Jaka jest różnica między COUNT(*), COUNT(kolumna) i COUNT(DISTINCT kolumna)?

Różnica dotyczy tego, co dokładnie jest liczone i jak traktowane są wartości NULL. COUNT(*) zlicza wszystkie wiersze w wyniku zapytania lub w każdej grupie po GROUP BY, niezależnie od tego, jakie są wartości w kolumnach. Jeśli grupa ma 10 wierszy, to COUNT(*) zwróci 10.

COUNT(kolumna) zlicza tylko te wiersze, w których wskazana kolumna nie jest NULL-em. To oznacza, że wynik może być mniejszy niż COUNT(*). Przykładowo, jeśli w grupie jest 10 wierszy, ale w 3 z nich kolumna ma wartość NULL, to COUNT(kolumna) zwróci 7.

COUNT(DISTINCT kolumna) zlicza liczbę unikalnych, nie-NULL-owych wartości w kolumnie. Powtórzenia są liczone tylko raz, a NULL nie jest uwzględniany. Jeśli więc wartości w grupie to 10, 10, 20, NULL, 20, 30, to COUNT(DISTINCT kolumna) zwróci 3.

WyrażenieCo liczyCzy liczy NULLCzy usuwa duplikaty
COUNT(*)Wszystkie wierszeTak, bo liczy wiersze, nie wartościNie
COUNT(kolumna)Niepuste wartości w kolumnieNieNie
COUNT(DISTINCT kolumna)Unikalne niepuste wartościNieTak

W praktyce to rozróżnienie ma duże znaczenie w agregacjach. COUNT(*) odpowiada na pytanie „ile jest wierszy”, COUNT(kolumna) — „ile wierszy ma uzupełnioną wartość”, a COUNT(DISTINCT kolumna) — „ile różnych wartości występuje”. Błędny wybór jednej z tych wersji często prowadzi do pozornie dziwnych wyników, mimo że zapytanie działa poprawnie składniowo.

💡 Protip: Jeśli wynik COUNT wydaje się „za mały” albo „za duży”, najpierw zadaj sobie pytanie, czy liczysz wiersze, niepuste wartości czy unikalne wartości. COUNT(*) liczy wszystkie rekordy, COUNT(kolumna) pomija NULL-e, a COUNT(DISTINCT kolumna) dodatkowo usuwa duplikaty.

Jak JOIN wpływa na agregacje i jak uniknąć zawyżonych sum?

JOIN wpływa na agregacje przez to, że najpierw powstaje wynik połączenia tabel, a dopiero potem wykonywane są funkcje agregujące, takie jak SUM(), COUNT() czy AVG(). Jeśli relacja po stronie dołączanej tabeli nie jest 1:1, lecz 1:wiele, jeden wiersz z tabeli głównej może zostać powielony tyle razy, ile ma dopasowań. W efekcie suma liczona po takim złączeniu bywa zawyżona, bo agregujesz już nie dane źródłowe, tylko ich zwielokrotnioną reprezentację.

Typowy błąd wygląda tak: masz tabelę zamówień i tabelę pozycji zamówień. Jeśli połączysz je przez JOIN i potem policzysz SUM(orders.total_amount), to wartość zamówienia zostanie dodana tyle razy, ile ma pozycji. Samo GROUP BY tego nie naprawia, jeśli grupowanie odbywa się po kolumnach, które nadal obejmują zwielokrotnione wiersze.

Aby uniknąć zawyżonych sum, trzeba agregować na właściwym poziomie szczegółowości. Najbezpieczniej najpierw zsumować dane w tabeli „wiele” do poziomu klucza, po którym łączysz, a dopiero potem wykonać JOIN. Przykładowo, zamiast łączyć zamówienia z każdą pozycją osobno, najpierw liczysz sumę pozycji dla każdego order_id, a następnie dołączasz już jeden zagregowany wiersz na zamówienie. Dzięki temu nie mnożysz wartości z tabeli nadrzędnej.

Trzeba też uważać na COUNT(). Po JOIN COUNT(*) liczy wiersze wyniku połączenia, a nie liczbę unikalnych rekordów z tabeli głównej. Jeśli chcesz policzyć encje z jednej tabeli, często właściwsze jest COUNT(DISTINCT ...) albo wcześniejsza agregacja w podzapytaniu. DISTINCT bywa pomocne, ale nie jest uniwersalnym lekarstwem: może ukryć problem modelu zapytania, a przy sumach nie zawsze oddaje poprawny wynik biznesowy.

Praktyczna zasada jest prosta: przed użyciem agregacji sprawdź, czy JOIN nie zwiększa liczby wierszy względem tabeli, z której pochodzi mierzona wartość. Jeśli zwiększa, to suma po złączeniu może być błędna. Wtedy należy albo agregować przed JOIN, albo tak przebudować zapytanie, by każda wartość była liczona dokładnie raz na docelowym poziomie grupowania.

💡 Protip: Przed dodaniem SUM() albo COUNT() po JOIN sprawdź, czy liczba wierszy nie wzrosła względem tabeli źródłowej — to najszybszy sygnał, że wynik może być zawyżony. Gdy łączysz relację 1:wiele, najpierw agreguj tabelę „wiele” do poziomu klucza, a dopiero potem wykonuj JOIN.

Najczęściej zadawane pytania i odpowiedzi odnośnie GROUP BY w SQL: skąd biorą się błędne sumy i jak robić agregacje, które mają sens

Jak sprawdzić, czy GROUP BY działa na właściwym poziomie szczegółowości danych?

Najprościej sprawdzisz to, zadając sobie pytanie: jeden wiersz wyniku ma oznaczać dokładnie jaką jednostkę biznesową. Jeśli ma to być klient, miesiąc albo produkt, właśnie te kolumny powinny definiować grupę. Gdy wynik wygląda zbyt szczegółowo lub zbyt ogólnie, zwykle oznacza to, że grupujesz na niewłaściwym poziomie i mieszasz różne poziomy analizy.

Skąd mam wiedzieć, że JOIN zawyża SUM() albo COUNT() w moim zapytaniu?

Najczęstszy sygnał to wzrost liczby wierszy po JOIN względem tabeli, z której pochodzi mierzona wartość. Jeśli po połączeniu jednej tabeli z relacją 1:wiele pojawia się więcej rekordów, agregacja może liczyć te same dane wiele razy. Pomocne są szybkie testy:

  • porównanie liczby wierszy przed i po JOIN,
  • sprawdzenie, czy jedna wartość z tabeli głównej powtarza się wiele razy,
  • weryfikacja, czy agregujesz przed czy po połączeniu tabel.
Czy samo DISTINCT wystarczy, żeby naprawić błędne sumy po JOIN?

Nie, DISTINCT nie zawsze rozwiązuje problem zawyżonych agregacji. Może usunąć część duplikatów w wyniku, ale nie gwarantuje poprawnej logiki biznesowej sumowania. Jeśli źródłem błędu jest relacja 1:wiele, bezpieczniej najpierw zagregować tabelę po stronie „wiele” do właściwego klucza, a dopiero potem wykonać JOIN i dalsze obliczenia.

Kiedy lepiej użyć podzapytania przed GROUP BY niż liczyć wszystko w jednym zapytaniu?

Podzapytanie warto zastosować wtedy, gdy najpierw musisz uporządkować poziom danych, a dopiero potem agregować wynik końcowy. To szczególnie przydatne po JOIN, gdy jedna tabela zwielokrotnia wiersze drugiej. Najpierw możesz policzyć wartości na poziomie klucza, a w zewnętrznym zapytaniu wykonać właściwe GROUP BY bez ryzyka zawyżonych sum lub błędnych liczników.

Dlaczego AVG() może dawać inny wynik, niż intuicyjnie się spodziewam?

AVG() liczy średnią tylko z wartości niebędących NULL. To oznacza, że nie dzieli sumy przez liczbę wszystkich wierszy, lecz przez liczbę rzeczywiście uzupełnionych wartości. Jeśli w danych jest dużo NULL-i, średnia może być wyższa albo niższa od oczekiwanej. Dlatego przed interpretacją wyniku dobrze sprawdzić, ile rekordów faktycznie weszło do obliczenia.

Czy mogę używać GROUP BY bez funkcji agregujących?

Tak, GROUP BY może działać także bez SUM(), COUNT() czy AVG(), ale wtedy pełni rolę porządkującą wynik do unikalnych grup. W praktyce takie użycie ma sens tylko wtedy, gdy naprawdę chcesz otrzymać jeden wiersz na każdą kombinację wskazanych kolumn. W analizie danych częściej stosuje się je razem z agregacjami, bo wtedy grupowanie daje realne podsumowanie.

Jakie pytania warto zadać sobie przed napisaniem agregacji w SQL?

Najpierw trzeba ustalić, co dokładnie liczysz i na jakim poziomie ma być pokazany wynik. To pomaga uniknąć błędów logicznych jeszcze przed pisaniem zapytania. Dobrze przejść przez krótką checklistę:

  • co jest jednostką wyniku: klient, miesiąc, produkt czy zamówienie,
  • czy JOIN nie powiela danych,
  • czy liczysz wiersze, wartości niepuste czy wartości unikalne,
  • czy warunek powinien być w WHERE czy w HAVING.
Po czym poznać, że wynik agregacji w SQL jest logicznie podejrzany, mimo że zapytanie działa?

Podejrzany wynik zwykle rozpoznasz po liczbach, które są technicznie poprawne, ale nie zgadzają się z logiką danych. Alarmujące sygnały to nagły wzrost sum po dodaniu JOIN, inna liczba rekordów niż oczekiwana liczba grup albo różnice między COUNT(*) i COUNT(kolumna), których nie potrafisz wyjaśnić. W takich sytuacjach trzeba sprawdzić poziom grupowania i zbiór wejściowy do agregacji.

icon

Formularz kontaktowyContact form

Imię *Name
NazwiskoSurname
Adres e-mail *E-mail address
Telefon *Phone number
UwagiComments