Agregacje i grupowanie w SOQL

Licz, sumuj i grupuj dane bezpośrednio w zapytaniu: COUNT, SUM, AVG, MIN, MAX, GROUP BY i HAVING, także do szukania duplikatów.

Lekcja 5 z 710 min czytania

Zamiast eksportować tysiące rekordów i liczyć je w Excelu, możesz policzyć wynik od razu w SOQL. Zapytania agregujące zwracają nie pojedyncze rekordy, ale podsumowania: liczby, sumy, średnie.

Przykładowe dane

Wszystkie przykłady w tej lekcji działają na tych samych, małych danych z obiektów zbudowanych w module 2 kursu Podstawy Salesforce: 5 kontaktów, 6 nieruchomości i 8 ofert. Tych samych danych używamy w lekcjach o filtrowaniu i relacjach, więc wystarczy przygotować je raz.

Obiekt Contact (kontakty):

FirstNameLastNameEmail
AnnaNowakanna.nowak@example.com
PiotrKowalskipiotr.kowalski@example.com
EwaWiśniewskaewa.wisniewska@example.com
AniaNowakanna.nowak@example.com
JanZieliński(puste)

Ania Nowak to ta sama osoba co Anna Nowak, wprowadzona drugi raz przy imporcie. W tej lekcji nauczysz się wyszukiwać takie duplikaty.

Obiekt Nieruchomość (Property__c):

NamePrice__cArea_m2__cStatus__cSprzedający (Seller__c)
Lipowa 5/12, Gdańsk450 00050DostępnaAnna Nowak
Kwiatowa 3, Sopot1 250 000140ZarezerwowanaAnna Nowak
Słoneczna 8/4, Gdańsk380 00036SprzedanaPiotr Kowalski
Ogrodowa 15, Gdynia890 000120Dostępna(puste)
Morska 21/3, Gdynia640 00062NowaPiotr Kowalski
Polna 2/7, Gdańsk520 00055DostępnaEwa Wiśniewska

Pozostałe pola tych samych nieruchomości:

NameAmenities__cIs_Furnished__cAvailable_From__c
Lipowa 5/12, GdańskBalkon;Windatrue2026-11-01
Kwiatowa 3, SopotMiejsce parkingowe;Ogród;Piwnicafalse(puste)
Słoneczna 8/4, GdańskBalkontrue2026-03-01
Ogrodowa 15, GdyniaMiejsce parkingowe;Ogródfalse2026-10-01
Morska 21/3, GdyniaWinda;Miejsce parkingowetrue2026-12-15
Polna 2/7, GdańskPiwnicafalse2026-10-20

Obiekt Oferta (Offer__c), powiązany z nieruchomością relacją Master-Detail:

NameAmount__cNieruchomość (Property__c)
OF-00001395 000Lipowa 5/12, Gdańsk
OF-00002430 000Lipowa 5/12, Gdańsk
OF-000031 200 000Kwiatowa 3, Sopot
OF-00004360 000Słoneczna 8/4, Gdańsk
OF-00005375 000Słoneczna 8/4, Gdańsk
OF-00006380 000Słoneczna 8/4, Gdańsk
OF-00007850 000Ogrodowa 15, Gdynia
OF-00008505 000Polna 2/7, Gdańsk

Uwagi do tabel z wynikami. Kwoty są sformatowane dla czytelności, a Salesforce zwraca je jako zwykłe liczby, np. 450000. Bez ORDER BY Salesforce nie gwarantuje kolejności wierszy, więc w takich wynikach pokazujemy grupy w kolejności, w jakiej pierwszy raz pojawiają się w tabelach powyżej. Przykład z datami zakłada, że dziś jest czwartek, 15 października 2026.

Chcesz sprawdzić przykłady u siebie? Dodaj te rekordy w swoim orgu do nauki: ręcznie albo importem z pliku CSV, jak w lekcji Praca z rekordami i widokami list. Numery ofert nadaje Salesforce, więc u Ciebie mogą być inne. Pole Dostępna od ma wartość domyślną TODAY(), więc wpisz daty z tabeli ręcznie, a w Kwiatowej 3 wyczyść to pole.

Funkcje agregujące

FunkcjaCo zwracaPrzykład
COUNT()liczbę rekordów (sam wynik, bez wierszy)SELECT COUNT() FROM Contact
COUNT(pole)liczbę rekordów z wypełnionym polemCOUNT(Email)
COUNT_DISTINCT(pole)liczbę różnych wartościCOUNT_DISTINCT(Industry)
SUM(pole)sumęSUM(Amount)
AVG(pole)średniąAVG(Price__c)
MIN(pole) / MAX(pole)najmniejszą / największą wartośćMAX(CloseDate)

COUNT() z pustymi nawiasami zwraca samą liczbę i nie można go łączyć z innymi polami. Do raportów z grupowaniem używaj COUNT(Id).

Przykład 1. Sama liczba: COUNT()

SELECT COUNT()
FROM Property__c
WHERE Status__c = 'Dostępna'

Co zwróci: liczbę dostępnych nieruchomości i nic poza nią. W Apex taki wynik przypiszesz od razu do zmiennej typu Integer.

Wynik: liczba 3, bez żadnych wierszy z danymi.

Przykład 2. COUNT(Id), COUNT(pole) i COUNT_DISTINCT

SELECT COUNT(Id) wszystkie,
       COUNT(Email) z_emailem,
       COUNT_DISTINCT(Email) rozne_emaile
FROM Contact

Co zwróci: jeden wiersz z trzema liczbami. Kontaktów jest pięć, e-mail ma cztery, bo Jan Zieliński go nie ma, a różnych adresów są trzy, bo Anna i Ania Nowak mają ten sam.

Wynik (1 wiersz):

wszystkiez_emailemrozne_emaile
543

Przykład 3. Suma, średnia, minimum i maksimum

SELECT SUM(Amount__c) suma, AVG(Amount__c) srednia,
       MIN(Amount__c) najnizsza, MAX(Amount__c) najwyzsza
FROM Offer__c

Co zwróci: podsumowanie wszystkich ośmiu ofert w jednym wierszu. Bez aliasów kolumny wyniku nazywałyby się expr0, expr1 i tak dalej, więc aliasy warto nadawać zawsze.

Wynik (1 wiersz):

sumasrednianajnizszanajwyzsza
4 495 000561 875360 0001 200 000

Grupowanie: GROUP BY

GROUP BY dzieli rekordy na grupy i liczy agregaty dla każdej z nich:

SELECT StageName, COUNT(Id) liczba, SUM(Amount) suma
FROM Opportunity
WHERE IsClosed = false
GROUP BY StageName
ORDER BY SUM(Amount) DESC

Zasady:

  • Każde pole w SELECT, które nie jest agregatem, musi być w GROUP BY. Inaczej dostaniesz błąd Field must be grouped or aggregated.
  • liczba i suma to aliasy, czyli własne nazwy kolumn wyniku. W SOQL piszesz je bez słowa AS.
  • Sortując po agregacie, powtórz funkcję (ORDER BY SUM(Amount) DESC) zamiast aliasu.

Przykład na obiektach z modułu 2 kursu podstaw:

SELECT Status__c, COUNT(Id) liczba, AVG(Price__c) srednia_cena
FROM Property__c
GROUP BY Status__c

Przykład 4. Grupy według statusu, od najwyższej średniej

SELECT Status__c, COUNT(Id) liczba, AVG(Price__c) srednia_cena
FROM Property__c
GROUP BY Status__c
ORDER BY AVG(Price__c) DESC

Co zwróci: po jednym wierszu na każdy status, z liczbą nieruchomości i średnią ceną, od najwyższej średniej. Dla statusu Dostępna średnia to (450 000 + 890 000 + 520 000) / 3 = 620 000.

Wynik (4 wiersze):

Status__cliczbasrednia_cena
Zarezerwowana11 250 000
Nowa1640 000
Dostępna3620 000
Sprzedana1380 000

Przykład 5. Grupowanie po polu rodzica i grupa pustych

SELECT Seller__r.LastName nazwisko, COUNT(Id) liczba
FROM Property__c
GROUP BY Seller__r.LastName

Co zwróci: liczbę nieruchomości według nazwiska sprzedającego. Ogrodowa 15 nie ma sprzedającego, więc tworzy osobną grupę z pustym nazwiskiem. Rekordy z pustym polem nie znikają z grupowania, tylko trafiają do własnej grupy. W prawdziwym orgu grupowanie po nazwisku połączyłoby różne osoby o tym samym nazwisku, więc w raportach bezpieczniej grupować po Seller__c, czyli po Id kontaktu.

Wynik (4 wiersze):

nazwiskoliczba
Nowak2
Kowalski2
(puste)1
Wiśniewska1

Grupowanie po dacie

Funkcje dat pozwalają zbudować zestawienie miesięczne:

SELECT CALENDAR_MONTH(CloseDate) miesiac, COUNT(Id) liczba, SUM(Amount) suma
FROM Opportunity
WHERE StageName = 'Closed Won' AND CloseDate = THIS_YEAR
GROUP BY CALENDAR_MONTH(CloseDate)
ORDER BY CALENDAR_MONTH(CloseDate)

Inne przydatne funkcje to CALENDAR_YEAR(), CALENDAR_QUARTER() i DAY_ONLY(). Ostatnia wycina datę z pola Date/Time, np. DAY_ONLY(CreatedDate).

Przykład 6. Zestawienie miesięczne

SELECT CALENDAR_MONTH(Available_From__c) miesiac,
       COUNT(Id) liczba, SUM(Price__c) suma
FROM Property__c
WHERE Available_From__c = THIS_YEAR
GROUP BY CALENDAR_MONTH(Available_From__c)
ORDER BY CALENDAR_MONTH(Available_From__c)

Co zwróci: liczbę i łączną cenę nieruchomości według miesiąca, od którego są dostępne. Przyjmujemy, że dziś jest 15 października 2026, więc THIS_YEAR oznacza rok 2026. Kwiatowa 3 ma pustą datę i odpada już w WHERE. Miesiące bez żadnej nieruchomości nie pojawiają się w wyniku, nawet jako zera.

Wynik (4 wiersze):

miesiacliczbasuma
31380 000
1021 410 000
111450 000
121640 000

Filtrowanie grup: HAVING

WHERE filtruje rekordy przed grupowaniem, a HAVING filtruje gotowe grupy. Klasyczne zastosowanie to szukanie duplikatów:

SELECT Email, COUNT(Id) liczba
FROM Contact
WHERE Email != null
GROUP BY Email
HAVING COUNT(Id) > 1

Wynik to lista adresów e-mail, które występują w więcej niż jednym kontakcie. To dobry punkt startowy do porządkowania danych przed migracją albo kampanią.

Podobnie znajdziesz nieruchomości z wieloma ofertami:

SELECT Property__c, COUNT(Id) liczba_ofert, MAX(Amount__c) najwyzsza
FROM Offer__c
GROUP BY Property__c
HAVING COUNT(Id) >= 2

Przykład 7. Nieruchomości z co najmniej dwiema ofertami

SELECT Property__r.Name nieruchomosc,
       COUNT(Id) liczba_ofert, MAX(Amount__c) najwyzsza
FROM Offer__c
GROUP BY Property__r.Name
HAVING COUNT(Id) >= 2

Co zwróci: to samo co zapytanie powyżej, ale z nazwą nieruchomości zamiast jej Id. Z pięciu grup zostają dwie. Morskiej 21/3 nie ma nawet wśród grup, bo zapytanie liczy oferty, a do niej nikt nie złożył oferty.

Wynik (2 wiersze):

nieruchomoscliczba_ofertnajwyzsza
Lipowa 5/12, Gdańsk2430 000
Słoneczna 8/4, Gdańsk3380 000

Przykład 8. WHERE i HAVING w jednym zapytaniu

SELECT Property__r.Name nieruchomosc, COUNT(Id) liczba_ofert
FROM Offer__c
WHERE Amount__c >= 380000
GROUP BY Property__r.Name
HAVING COUNT(Id) >= 2

Co zwróci: nieruchomości z co najmniej dwiema ofertami od 380 000 wzwyż. WHERE najpierw odrzuca oferty OF-00004 i OF-00005, więc Słonecznej 8/4 zostaje jedna oferta i jej grupa odpada w HAVING. Lipowa 5/12 ma obie oferty powyżej progu.

Wynik (1 wiersz):

nieruchomoscliczba_ofert
Lipowa 5/12, Gdańsk2

Przykład 9. Duplikaty po dwóch polach

SELECT LastName, Email, COUNT(Id) liczba
FROM Contact
WHERE Email != null
GROUP BY LastName, Email
HAVING COUNT(Id) > 1

Co zwróci: pary nazwisko i e-mail, które powtarzają się w więcej niż jednym kontakcie. Anna i Ania Nowak różnią się imieniem, więc grupowanie po imieniu i nazwisku by ich nie wyłapało. Duplikat zdradza wspólny e-mail i nazwisko.

Wynik (1 wiersz):

LastNameEmailliczba
Nowakanna.nowak@example.com2

Agregaty czy Roll-Up Summary? Pole Roll-Up Summary z kursu podstaw liczy wynik na stałe i zapisuje go na rekordzie. Zapytanie agregujące liczy go na żądanie, z dowolnym filtrem i grupowaniem. Do jednorazowych analiz wystarczy zapytanie. Jeśli wynik ma być widoczny na rekordzie lub w dokumencie, lepsze jest pole.

Ćwiczenie

  1. Policz kontakty w każdej branży (Account.Industry) ich firm. Podpowiedź: grupuj po Account.Industry.
  2. Dla każdej nieruchomości policz oferty i podaj najniższą oraz najwyższą kwotę.
  3. Znajdź firmy (Account) o tej samej nazwie, czyli potencjalne duplikaty.
  4. Zestaw liczbę utworzonych zgłoszeń (Case) w podziale na miesiące bieżącego roku.

Sprawdź się

1. Zapytanie SELECT Name, COUNT(Id) FROM Account GROUP BY Industry zwraca błąd. Dlaczego?

Pokaż odpowiedź

Pole Name nie jest agregatem i nie ma go w GROUP BY. Trzeba je usunąć z SELECT albo dodać do grupowania.

2. Czym różni się WHERE od HAVING?

Pokaż odpowiedź

WHERE filtruje pojedyncze rekordy przed grupowaniem. HAVING filtruje gotowe grupy, np. tylko te, w których liczba rekordów jest większa niż 1.

3. Kiedy użyjesz COUNT(), a kiedy COUNT(Id)?

Pokaż odpowiedź

COUNT() zwraca samą liczbę rekordów i nie łączy się z innymi polami ani grupowaniem. COUNT(Id) działa razem z GROUP BY i innymi kolumnami, więc nadaje się do zestawień.

Następny krok: wykorzystasz SOQL w praktyce, przy budowaniu danych do dokumentów w PDF Butlerze.