GROUP BY służy do grupowania wierszy według jednej lub wielu kolumn, aby potem policzyć wyniki zbiorcze (agregaty), np. COUNT, SUM, AVG, MIN, MAX.
Przykład (ile zamówień ma każdy user):
SELECT user_id, COUNT(*) AS ile_zamowien
FROM orders
GROUP BY user_id;
Minimalny schemat zapytania z GROUP BY
SELECT
kolumny_grupujace,
agregaty(...)
FROM tabela
[WHERE ...]
GROUP BY kolumny_grupujace
[HAVING ...]
[ORDER BY ...]
[LIMIT ...];
Ważne zasady:
W SELECT przy GROUP BY możesz mieć kolumny grupujące (te same, co w GROUP BY) oraz funkcje agregujące
Nie wolno (logicznie) wybierać „zwykłej kolumny” nieujętej w GROUP BY, bo z grupy jest wiele wartości i nie wiadomo którą pokazać.
MariaDB czasem pozwala na to zależnie od ustawień SQL mode, ale to daje nieokreślone/losowe wyniki — lepiej tego unikać.
Najważniejsze funkcje agregujące
COUNT(*) / COUNT(kolumna) – liczenie
COUNT(*) – liczy wiersze w grupie
COUNT(kolumna) – liczy tylko te, gdzie kolumna IS NOT NULL
SELECT user_id, COUNT(*) AS n
FROM orders
GROUP BY user_id;
SUM(kolumna) – suma wartości w grupie.
SELECT user_id, SUM(total) AS suma
FROM orders
GROUP BY user_id;
AVG(kolumna) – średnia.
SELECT user_id, ROUND(AVG(total), 2) AS srednia
FROM orders
GROUP BY user_id;
MIN(…), MAX(…) – minimum / maksimum.
SELECT user_id, MIN(total) AS min_total, MAX(total) AS max_total
FROM orders
GROUP BY user_id;
GROUP_CONCAT(…) – Skleja wartości tekstowe z grupy do jednej kolumny (bardzo przydatne).
SELECT user_id, GROUP_CONCAT(total ORDER BY total SEPARATOR ', ') AS totals
FROM orders
GROUP BY user_id;
HAVING vs WHERE (kluczowe)
WHERE – Filtruje wiersze przed grupowaniem.
SELECT user_id, COUNT(*) AS n
FROM orders
WHERE total >= 10
GROUP BY user_id;
HAVING – Filtruje grupy po zliczeniu/agregacji.
SELECT user_id, COUNT(*) AS n
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 2;
Często używa się obu naraz:
SELECT user_id, COUNT(*) AS n, ROUND(AVG(total),2) AS avg_total
FROM orders
WHERE created_at >= '2026-02-01'
GROUP BY user_id
HAVING AVG(total) > 20;
Grupowanie po wielu kolumnach
Możesz grupować po więcej niż 1 kolumnie.
Przykład: użytkownik + data (zamówienia dziennie na użytkownika)
SELECT user_id, DATE(created_at) AS d, COUNT(*) AS n
FROM orders
GROUP BY user_id, DATE(created_at);
DISTINCT w agregatach
Możesz liczyć/sumować unikalne wartości w grupie, np. ile różnych dat zamówień ma user.
SELECT user_id, COUNT(DISTINCT DATE(created_at)) AS ile_dni_z_zamowieniami
FROM orders
GROUP BY user_id;
WITH ROLLUP (sumy częściowe i suma końcowa)
Daje wiersze „podsumowań” (subtotal/total). W kolumnach grupujących pojawiają się NULL.
SELECT user_id, COUNT(*) AS n, ROUND(SUM(total),2) AS suma
FROM orders
GROUP BY user_id WITH ROLLUP;
Sortowanie wyników grup
Sortujesz normalnie ORDER BY (po kolumnie grupującej albo agregacie).
SELECT user_id, ROUND(SUM(total),2) AS suma
FROM orders
GROUP BY user_id
ORDER BY suma DESC;
Typowe schematy z GROUP BY
„Policz ile … na …”
„Suma / średnia / min / max … na …”
„Pokaż tylko te grupy, gdzie COUNT/SUM/AVG …”
„Grupuj po dacie (DATE(created_at))”
„Sklej listę wartości w grupie (GROUP_CONCAT)”
„Zrób sumy końcowe (WITH ROLLUP)”
baza do ćwiczeń –pobierz plik txt
per – oznacza dla każdego / w podziale na
Ćwiczenia:
- Policz liczbę zamówień dla każdego user_id.
- Policz sumę wartości zamówień dla każdego user_id (zaokrąglij do 2 miejsc po przecinku).
- Policz średnią wartość zamówienia dla każdego user_id (zaokrąglij do 2 miejsc po przecinku).
- Dla każdego user_id podaj min i max total.
- Dla każdego user_id policz ile ma zamówień >= 10 (użyj WHERE).
- Pokaż tylko tych userów i liczbę ich zamówień, którzy mają co najmniej 2 zamówienia (tabela orders, wykorzystaj HAVING).
- Pokaż tylko tych userów i liczbę ich zamówień, którzy mają tylko 1 zamówienie (tabela orders, wykorzystaj HAVING)
- Pokaż tylko tych userów, których suma zamówień > 30.
- Pokaż user_id i liczbę różnych dni, w których składali zamówienia.
- Policz liczbę zamówień per data (bez usera).
- Policz sumę total per data.
- Policz zamówienia per user per data.
- Pokaż per user per data: suma total (2 miejsca).
- Pokaż per user: GROUP_CONCAT wartości total (posortowane rosnąco).
- Pokaż per user: ile total jest NULL (u nas raczej nie ma, ale ćwiczenie: COUNT(*)-COUNT(total)).
- Pokaż user_id i sumę, posortuj malejąco po sumie.
- Pokaż top 2 userów po sumie zamówień (LIMIT 2).
- Pokaż user_id, średnią zamówień jako srednia_zamowien zaokrąglone do 2 miejsc po przecinku, ale tylko dla zamówień z lutego 2026.
- Pokaż user_id, sumę zamówień jako suma_zamowien zaokrągloną do 2 miejsc po przecinku, ale tylko dla zamówień złożonych przed 20 marca 2026.
- Pokaż per user: liczba zamówień z marca 2026.
- Pokaż per user: suma zamówień z marca 2026.
- Pokaż per user: max total, ale tylko grupy gdzie max >= 50.
- Produkty: policz ile produktów ma dany vat_rate.
- Produkty: suma price_net per vat_rate.
- Produkty: średnia price_net per vat_rate.
- Produkty: min/max price_net per vat_rate.
- Produkty: policz ile produktów ma cenę netto >= 10 (WHERE) per vat_rate.
- Produkty: policz sumę brutto per vat_rate (price_net*(1+vat_rate)).
- Produkty: tylko te stawki VAT, gdzie suma brutto > 30 (HAVING).
- Produkty: grupuj po dacie dodania (DATE(created_at)) i policz ile produktów dodano dziennie.
- Produkty: grupuj po miesiącu (YYYY-MM) i policz liczbę produktów.
- Produkty: GROUP_CONCAT nazw produktów per vat_rate.
- Users: policz ilu userów ma active=1 i active=0 (GROUP BY active).
- Users: policz ilu userów ma NULL w age (grupuj po IF(age IS NULL,’brak’,’jest’)).
- Users: policz ilu userów ma jaką długość imienia (CHAR_LENGTH(first_name)).
- Users: policz ilu userów ma jaką pierwszą literę emaila (LOWER(SUBSTRING(email,1,1))).
- Users: policz ilu userów ma jaki “display_name” (COALESCE(nickname, first_name)).
- Users: policz ilu userów ma jaki “phone_or_brak” (IFNULL(phone,’brak’)).
- Users: grupuj po dacie utworzenia konta i policz rejestracje dziennie.
- Users: grupuj po miesiącu rejestracji (YYYY-MM) i policz ile kont/miesiąc.
- Users: policz ilu userów jest adult/minor/unknown (CASE + GROUP BY).
- Users: tylko te grupy age_type, gdzie COUNT(*) >= 2 (HAVING).
- Employees: policz ilu pracowników ma/nie ma telefonu (IF(phone IS NULL,…)).
- Employees: średnia pensja (AVG) per “tel” (ma/nie ma).
- Employees: minimalna i maksymalna pensja.
- Employees: grupuj po dacie zatrudnienia (DATE(created_at)) i policz ilu zatrudniono dziennie.
- Employees: grupuj po miesiącu (YYYY-MM) i policz ilu zatrudniono.
- Tabela t: policz ile rekordów ma każdy status.
- Tabela t: policz statusy, ale pusty string potraktuj jako 'pusty’ (IF(status=”,’pusty’,status)).
- Tabela t: policz parzyste i nieparzyste id (CASE + MOD).
- Orders: zrób ROLLUP (sumy per user i suma końcowa).
- Products: ROLLUP per vat_rate (liczba + suma netto).
Odpowiedzi
- SELECT user_id, COUNT(*) AS n FROM orders GROUP BY user_id;
- SELECT user_id, ROUND(SUM(total), 2) AS suma FROM orders GROUP BY user_id;
- SELECT user_id, ROUND(AVG(total), 2) AS avg_total FROM orders GROUP BY user_id;
- SELECT user_id, MIN(total) AS min_total, MAX(total) AS max_total FROM orders GROUP BY user_id;
- SELECT user_id, COUNT(*) AS n_ge_10 FROM orders WHERE total >= 10 GROUP BY user_id;
- SELECT user_id, COUNT(*) AS n FROM orders GROUP BY user_id HAVING COUNT(*) >= 2;
- SELECT user_id, COUNT(*) AS n FROM orders GROUP BY user_id HAVING COUNT(*) = 1;
- SELECT user_id, ROUND(SUM(total), 2) AS suma FROM orders GROUP BY user_id HAVING SUM(total) > 30;
- SELECT user_id, COUNT(DISTINCT DATE(created_at)) AS dni FROM orders GROUP BY user_id;
- SELECT DATE(created_at) AS d, COUNT(*) AS n FROM orders GROUP BY DATE(created_at);
- SELECT DATE(created_at) AS d, ROUND(SUM(total), 2) AS suma FROM orders GROUP BY DATE(created_at);
- SELECT user_id, DATE(created_at) AS d, COUNT(*) AS n FROM orders GROUP BY user_id, DATE(created_at);
- SELECT user_id, DATE(created_at) AS d, ROUND(SUM(total), 2) AS suma FROM orders GROUP BY user_id, DATE(created_at);
- SELECT user_id, GROUP_CONCAT(ROUND(total,2) ORDER BY total SEPARATOR ’, ’) AS totals FROM orders GROUP BY user_id;
- SELECT user_id, (COUNT(*) – COUNT(total)) AS null_totals FROM orders GROUP BY user_id;
- SELECT user_id, ROUND(SUM(total), 2) AS suma FROM orders GROUP BY user_id ORDER BY suma DESC;
- SELECT user_id, ROUND(SUM(total), 2) AS suma FROM orders GROUP BY user_id ORDER BY suma DESC LIMIT 2;
- SELECT user_id, ROUND(AVG(total), 2) AS srednia_zamowien FROM orders WHERE created_at >= ’2026-02-01′ AND created_at < ’2026-03-01′ GROUP BY user_id;
- SELECT user_id, ROUND(SUM(total), 2) AS suma_zamowien FROM orders WHERE created_at < ’2026-03-20′ GROUP BY user_id;
- SELECT user_id, COUNT(*) AS n FROM orders WHERE created_at >= '2026-03-01′ AND created_at < '2026-04-01′ GROUP BY user_id;
- SELECT user_id, ROUND(SUM(total), 2) AS suma FROM orders WHERE created_at >= '2026-03-01′ AND created_at < '2026-04-01′ GROUP BY user_id;
- SELECT user_id, MAX(total) AS max_total FROM orders GROUP BY user_id HAVING MAX(total) >= 50;
- SELECT vat_rate, COUNT(*) AS n FROM products GROUP BY vat_rate;
- SELECT vat_rate, ROUND(SUM(price_net), 2) AS suma_net FROM products GROUP BY vat_rate;
- SELECT vat_rate, ROUND(AVG(price_net), 2) AS avg_net FROM products GROUP BY vat_rate;
- SELECT vat_rate, MIN(price_net) AS min_net, MAX(price_net) AS max_net FROM products GROUP BY vat_rate;
- SELECT vat_rate, COUNT(*) AS n_ge_10 FROM products WHERE price_net >= 10 GROUP BY vat_rate;
- SELECT vat_rate, ROUND(SUM(price_net * (1 + vat_rate)), 2) AS suma_gross FROM products GROUP BY vat_rate;
- SELECT vat_rate, ROUND(SUM(price_net * (1 + vat_rate)), 2) AS suma_gross FROM products GROUP BY vat_rate HAVING SUM(price_net * (1 + vat_rate)) > 30;
- SELECT DATE(created_at) AS d, COUNT(*) AS n FROM products GROUP BY DATE(created_at);
- SELECT DATE_FORMAT(created_at, '%Y-%m’) AS ym, COUNT(*) AS n FROM products GROUP BY DATE_FORMAT(created_at, '%Y-%m’);
- SELECT vat_rate, GROUP_CONCAT(name ORDER BY name SEPARATOR ’, ’) AS nazwy FROM products GROUP BY vat_rate;
- SELECT active, COUNT(*) AS n FROM users GROUP BY active;
- SELECT IF(age IS NULL, 'brak’, 'jest’) AS age_info, COUNT(*) AS n FROM users GROUP BY IF(age IS NULL, 'brak’, 'jest’);
- SELECT CHAR_LENGTH(first_name) AS len_first, COUNT(*) AS n FROM users GROUP BY HAR_LENGTH(first_name) ORDER BY len_first;
- SELECT LOWER(SUBSTRING(email, 1, 1)) AS first_letter, COUNT(*) AS n FROM users GROUP BY LOWER(SUBSTRING(email, 1, 1)) ORDER BY first_letter;
- SELECT COALESCE(nickname, first_name) AS display_name, COUNT(*) AS n FROM users GROUP BY COALESCE(nickname, first_name) ORDER BY n DESC;
- SELECT IFNULL(phone, 'brak’) AS phone_or_brak, COUNT(*) AS n FROM users GROUP BY IFNULL(phone, 'brak’) ORDER BY n DESC;
- SELECT DATE(created_at) AS d, COUNT(*) AS n FROM users GROUP BY DATE(created_at) ORDER BY d;
- SELECT DATE_FORMAT(created_at, '%Y-%m’) AS ym, COUNT(*) AS n FROM users GROUP BY DATE_FORMAT(created_at, '%Y-%m’) ORDER BY ym;
- SELECT CASE WHEN age IS NULL THEN 'unknown’ WHEN age >= 18 THEN 'adult’ ELSE 'minor’ END AS age_type, COUNT(*) AS n FROM users GROUP BY CASE WHEN age IS NULL THEN 'unknown’ WHEN age >= 18 THEN 'adult’ ELSE 'minor’ END;
- SELECT CASE WHEN age IS NULL THEN 'unknown’ WHEN age >= 18 THEN 'adult’ ELSE 'minor’ END AS age_type, COUNT(*) AS n FROM users GROUP BY CASE WHEN age IS NULL THEN 'unknown’ WHEN age >= 18 THEN 'adult’ ELSE 'minor’ END HAVING COUNT(*) >= 2;
- SELECT IF(phone IS NULL, 'brak’, 'jest’) AS tel, COUNT(*) AS n FROM employees GROUP BY IF(phone IS NULL, 'brak’, 'jest’);
- SELECT IF(phone IS NULL, 'brak’, 'jest’) AS tel, ROUND(AVG(salary), 2) AS avg_salary FROM employees GROUP BY IF(phone IS NULL, 'brak’, 'jest’);
- SELECT MIN(salary) AS min_salary, MAX(salary) AS max_salary FROM employees;
- SELECT DATE(created_at) AS d, COUNT(*) AS n FROM employees GROUP BY DATE(created_at);
- SELECT DATE_FORMAT(created_at, '%Y-%m’) AS ym, COUNT(*) AS n FROM employees GROUP BY DATE_FORMAT(created_at, '%Y-%m’);
- SELECT status, COUNT(*) AS n FROM t GROUP BY status;
- SELECT IF(status = ”, 'pusty’, status) AS status2, COUNT(*) AS n FROM t GROUP BY IF(status = ”, 'pusty’, status);
- SELECT CASE WHEN MOD(id,2)=0 THEN 'parzyste’ ELSE 'nieparzyste’ END AS parzystosc, COUNT(*) AS n FROM t GROUP BY CASE WHEN MOD(id,2)=0 THEN 'parzyste’ ELSE 'nieparzyste’ END;
- SELECT user_id, COUNT(*) AS n, ROUND(SUM(total), 2) AS suma FROM orders GROUP BY user_id WITH ROLLUP;
- SELECT vat_rate, COUNT(*) AS n, ROUND(SUM(price_net), 2) AS suma_net FROM products GROUP BY vat_rate WITH ROLLUP;