SQL od podstaw · część 18

GROUP BY i HAVING: podsumowania w grupach

Twórz raporty dla miast, klientów i produktów. Oddziel filtrowanie wierszy przez WHERE od filtrowania grup przez HAVING.

Część bazodanowa INF.03

Jedno podsumowanie dla każdej grupy

W poprzedniej części policzyliśmy wszystkich klientów naraz. Teraz chcemy wiedzieć, ilu mieszka w każdym mieście. GROUP BY tworzy grupy o jednakowych wartościach wskazanych wyrażeń, a agregacje obliczają wynik w każdej grupie osobno.

SELECT miasto, COUNT(*) AS liczba
FROM klienci
GROUP BY miasto
ORDER BY liczba DESC, miasto;

Gdansk ma 2 klientów, Krakow i Torun po 1, a grupa NULL także 1. Wszystkie braki miasta są jedną grupą; nie znaczy to, że zwykłe porównanie NULL = NULL jest prawdziwe. Położenie grupy NULL w sortowaniu zależy od silnika.

5 klientówwiersze wejściowe→GROUP BY miasto4 grupy→COUNT(*)2, 1, 1, 1

WHERE przed grupowaniem, HAVING po agregacji

SELECT klient_id, COUNT(*) AS liczba_zamowien
FROM zamowienia
WHERE status = 'oplacone'
GROUP BY klient_id
HAVING COUNT(*) >= 2
ORDER BY klient_id;

Najpierw WHERE zachowuje tylko opłacone zamówienia: id 1 i 3. Oba należą do klienta 1. GROUP BY tworzy jego grupę, COUNT daje 2, a HAVING przepuszcza tę grupę. Wynik: klient_id 1, liczba_zamowien 2. W tym miejscu nie potrzebujemy jeszcze imienia; jego dołączenie będzie tematem JOIN.

Nie zapisuj WHERE COUNT(*) >= 2. WHERE rozpatruje pojedyncze wiersze przed powstaniem tego agregatu. Warunek na policzoną grupę należy do HAVING. Warunek na status pojedynczego zamówienia należy do WHERE.

Wypróbuj zapytanie

SQLite — rzeczywisty silnik przeglądarki. MySQL / SQL Server — ograniczona emulacja składni. Wybierz tryb SQLite do natywnego LIKE. Pozostałe tryby nie są rzeczywistymi serwerami. Ścisłe typy, auto-ID i administrację ćwicz w prawdziwym SZBD.

Ładowanie edytora…

Kolejność zapisu i model logiczny

SELECT kolumna_grupy, COUNT(*) AS liczba
FROM tabela
WHERE warunek_wiersza
GROUP BY kolumna_grupy
HAVING COUNT(*) > 1
ORDER BY liczba DESC;

Pomocniczy model przetwarzania to FROM/JOIN → WHERE → GROUP BY/agregacje → HAVING → SELECT → ORDER BY → limit. To sposób rozumienia wyniku, a nie wymuszenie fizycznego planu wykonania: optymalizator może wykonywać operacje inaczej, zachowując znaczenie zapytania. Model wyjaśnia też, dlaczego alias z SELECT zwykle nie jest dostępny w WHERE.

Wiele kolumn w GROUP BY

SELECT klient_id, status, COUNT(*) AS liczba
FROM zamowienia
GROUP BY klient_id, status
ORDER BY klient_id, status;

Kluczem grupy jest cała para (klient_id, status), a nie każda kolumna oddzielnie. Otrzymasz (1, oplacone, 2), (2, nowe, 1), (3, nowe, 1). Gdy dodasz id zamówienia do GROUP BY, każde zamówienie utworzy osobną grupę i licznik zwykle spadnie do 1. Dobór kolumn określa poziom szczegółowości raportu.

W przenośnym zapytaniu wszystkie nieagregowane kolumny SELECT wpisz do GROUP BY. Nie dodawaj przypadkowego imienia do wyniku zliczania miast: grupa Gdansk ma Alę i O’Neila, więc pojedyncze imię nie jest jednoznaczne.

Ten sam raport w różnych bazach

Poniższy zapis działa w MySQL/MariaDB, SQL Server, Oracle 19c i Access. Celowo powtarza COUNT(*) w HAVING, zamiast polegać na aliasie:

SELECT miasto, COUNT(*) AS liczba
FROM klienci
WHERE miasto IS NOT NULL
GROUP BY miasto
HAVING COUNT(*) > 1
ORDER BY miasto;

Wynik: Gdansk, 2. MySQL dopuszcza aliasy SELECT w HAVING; SQL Server oraz Oracle 19c mają inne reguły dostępności aliasów. MySQL z ONLY_FULL_GROUP_BY sprawdza także zależności funkcyjne; nie wyłączaj tego trybu po to, by ukryć błąd projektu raportu. SQLite pozwala na pewne nieagregowane kolumny poza GROUP BY, lecz takie zapisy są nieprzenośne. Edytor w trybach porównawczych stosuje konserwatywną kontrolę grupowania. W trybie SQLite obowiązuje jego rzeczywista semantyka.

W Access to samo można zbudować w widoku projektu kwerendy za pomocą wiersza Sumy: dla miasta „Grupuj według”, dla licznika „Policz”. Sprawdź wygenerowany SQL, aby rozróżnić kryteria pojedynczych rekordów i grup. Oracle/Access nie są uruchamiane w przeglądarkowym edytorze.

GROUP BY to nie ORDER BY

Grupowanie nie gwarantuje kolejności prezentacji. Nawet jeśli wynik wygląda na posortowany, dopisz ORDER BY, gdy zadanie wymaga kolejności. DISTINCT wybiera różne wiersze wyniku, a GROUP BY daje miejsce na obliczenia dla każdej grupy. Nie stosuj DISTINCT jako automatycznej poprawki błędnego JOIN — najpierw znajdź przyczynę powielenia.

Ćwiczenia

  1. Policz zamówienia w każdym statusie.
  2. Policz łączną liczbę sztuk każdego produktu na pozycjach zamówień i zachowaj tylko produkty z co najmniej 3 sztukami.
  3. Wyklucz NULL miasta przed grupowaniem. Jak wpłynie to na liczbę grup?
Rozwiązania
SELECT status, COUNT(*) AS liczba FROM zamowienia
GROUP BY status ORDER BY status;
SELECT produkt_id, SUM(ilosc) AS sztuki FROM pozycje
GROUP BY produkt_id HAVING SUM(ilosc) >= 3 ORDER BY produkt_id;
SELECT miasto, COUNT(*) AS liczba FROM klienci
WHERE miasto IS NOT NULL GROUP BY miasto ORDER BY miasto;

1. nowe: 2, oplacone: 2. 2. produkt 2: 3 sztuki, produkt 4: 5 sztuk. 3. Zostają trzy grupy, bo grupa braku miasta nie powstanie. SUM(ilosc) liczy sztuki, a COUNT(*) liczyłoby pozycje — to różne miary.

Źródła: MySQL — grupowanie, SQL Server HAVING, Oracle — grupy, Access GROUP BY. INF.03.4: zestawienia i raportowanie. Dalej: INNER JOIN.

Przewijanie do góry