Confusion is part of programming.
—Felienne Hermans, The Programmer's Brain
De GROUP BY en HAVING clauses in detail
Rijen groeperen
In het hoofdstuk over het importeren van een CSV-bestand kwam je een eerste keer in contact met het groeperen van rijen. Vaak gaat het over een vraag waarin het woord ‘per’ een rol speelt. “Geef per lector het aantal OPO’s waarvoor deze lector coördinator is”. “Geef het totaal aantal studiepunten van alle OPO’s die nu of vroeger in die taal gegeven zijn”. Toegegeven, die laatste zin bevat het woordje ‘per’ niet, maar je kan ze ook herformuleren als “Geef per taal het totaal aantal studiepunten van alle OPO's die in die taal gegeven worden of zijn.”
Het beeld dat we gebruiken is: groepeer de rijen op eenzelfde waarde van een bepaald veld (of velden) in een doosje. Van dat doosje kan je dan geen individuele rijen meer bekijken. Je moet je dan beperken tot samenvattende informatie.
Laten we als voorbeeld bovenstaande vraag behandelen: “Geef per lector het aantal OPO’s waarvoor deze lector coördinator is of was”.
Je kan de query's nog steeds uitvoeren op de tabel ‘opleidingsonderdeel’ in het schema ‘uclloket’ van de databank ‘df’ of in je eigen versie hiervan.
In een eerste stap schrijf je als oefening de query die onderstaande figuur genereert, nl. een lijst van alle coördinatoren met opocode, naam van het opo en semester, geordend volgens stijgende lectorcode (u-nummer):

Niet zo'n moeilijke query, toch?
SELECT coordinator, opocode, naam, semester
FROM opleidingsonderdeel
ORDER BY coordinator
Je leest in deze figuur dat lector ‘u0012047’ coördinator is (of was) van ‘Probleemoplossend denken’, ‘Webontwikkeling 1’ en ‘Front-end Development’. Als je nu groepeert per lector dan gaan alle rijen met dezelfde waarde voor de kolom ‘coordinator’ samen in één doosje. Het eerste doosje bevat zo drie rijen. Op het doosje staat de naam van het veld dat voor al deze rijen gemeenschappelijk is, dus ‘u0012047’.
Het tweede doosje met als label ‘u0015529’ bevat twee rijen. Het derde doosje (‘u0032987’) bevat maar één rij, enz.
Doordat deze rijen samen in een doosje zitten kan je geen individuele
gegevens in de SELECT
clause meer opvragen. Als je dat probeert
krijg je een typische foutmelding, zoals volgend stukje code laat zien:
SELECT coordinator, opocode, naam, semester
FROM opleidingsonderdeel
GROUP BY coordinator
-- het resultaat van deze query is deze foutmelding:
ERROR: column "opleidingsonderdeel.opocode" must appear in the GROUP BY clause
or be used in an aggregate function
LINE 2: select coordinator, opocode, naam, semester
^
SQL state: 42803
Character: 50
Deze query toont enkel een overzicht van alle ‘etiketten’ van de doosjes en wordt probleemloos uitgevoerd:
SELECT coordinator
FROM opleidingsonderdeel
GROUP BY coordinator
Je mag enkel in het doosje kijken en bepaalde samenvattende informatie
geven in de SELECT
, zoals het aantal rijen, de som van alle
rijen wat een bepaalde kolom betreft. We noemen deze samenvattende functies ‘aggregatiefuncties’ en spreken
over data ‘aggregeren’. In de volgende sectie bekijken we verschillende van deze functies.
Aggregatiefuncties
Aantal rijen tellen
De vraag die we nog steeds trachten te beantwoorden is “Geef per lector het aantal OPO’s waarvoor deze lector coördinator is of was”. In deze zin
weet je ondertussen al dat het stukje ‘per lector’ betekent dat je de rijen
moet groeperen die dezelfde waarde voor coordinator hebben.
Het aantal rijen tellen dat in één doosje zit
doe je m.b.v. de functie COUNT()
(documentatie). Als je volledige rijen wilt tellen, gebruik je COUNT(*)
.
De uiteindelijke query die het antwoord op de vraag oplevert is:
SELECT coordinator, COUNT(*)
FROM opleidingsonderdeel
GROUP BY coordinator
ORDER BY coordinator
Som van bepaalde gegevens in een groep
Een tweede aggregatiefunctie is SUM()
. Je telt hiermee waarden in een bepaalde kolom op. Opgepast: we zien geregeld studenten COUNT()
en SUM()
verwisselen! Maak nu volgende oefening.
Schrijf een SQL query die het overzicht genereert van het totaal aantal studiepunten waarvoor een lector coördinator is. Orden het resultaat volgens dalend totaal aantal studiepunten. Je moet onderstaande figuur bekomen. Zoals altijd: let je ook op de juiste hoofding van elke kolom?

SELECT coordinator, SUM(studiepunten) AS "totaal aantal SP"
FROM opleidingsonderdeel
GROUP BY coordinator
ORDER BY 2 DESC
Minimum, maximum en gemiddelde
De laatste drie aggregatiefuncties zijn MIN()
, MAX()
en AVG()
, respectievelijk voor het minimum, maximum of
rekenkundig gemiddelde van waarden in een bepaalde kolom. Maak
volgende oefeningen.
Maak een lijst die voor elk semester het gemiddeld aantal studiepunten toont. Orden de antwoordrijen op semester van klein naar groot. Je moet onderstaande figuur bekomen. Kan je ook de naam van het OPO vermelden?

Niet elke opgave zal het woordje ‘per’ bevatten. Taal kent heel wat
alternatieven om toch hetzelfde te vragen. Hier zal je dus moeten
groeperen per semester en moet je de aggregatiefunctie AVG()
gebruiken. Vanzelfsprekend kan je geen informatie van individuele OPO's
opvragen (zoals de naam) want dat is ‘informatie die in het doosje zit’.
Volgende query is een mogelijke oplossing:
SELECT semester, AVG(studiepunten) AS "gemiddeld aantal SP"
FROM opleidingsonderdeel
GROUP BY semester
ORDER BY 1
Iets moeilijker … Geef een overzicht per taal van het aantal OPO’s van minstens 4 SP dat in die taal onderwezen wordt. Je hoeft niet te ordenen.
SELECT taal, COUNT(*) AS aantal
FROM opleidingsonderdeel
WHERE studiepunten >= 4
GROUP BY taal
Groeperen op een expressie
Meestal groepeer je op een bepaalde kolom, maar het is ook mogelijk om te
groeperen op een ‘berekende kolom’ (expressie). Elk OPO heeft een
startdatum. Uit deze startdatum kan je gemakkelijk het jaar halen met EXTRACT
. We geven bij wijze van voorbeeld de query die volgende vraag
beantwoordt: “Geef een overzicht voor elk jaar hoeveel OPO’s er nieuw in
de opleiding gekomen zijn in dat jaar”.
SELECT EXTRACT(year FROM van), COUNT(*) AS "aantal OPOs"
FROM opleidingsonderdeel
GROUP BY EXTRACT(year FROM van)
ORDER BY 1
Groeperen op meerdere kolommen
De volgende query groepeert op twee kolommen: semester en studiepunten:
SELECT semester, studiepunten, COUNT(*)
FROM opleidingsonderdeel
GROUP BY semester, studiepunten
ORDER BY 1, 2
Deze query maakt 15 ‘doosjes’ (zie figuur). Het eerste doosje (oranje rand) toont op het etiket “semester 1, OPO’s met 3 SP”. In dit doosje zitten er vier rijen. Het tweede doosje (blauwe rand) is “semester 1, OPO’s met 4 SP”. Zo is er maar één OPO. enz.

HAVING
De HAVING
clause werd al behandeld in het hoofdstuk over CSV-bestanden. Studenten vinden het vaak moeilijk om het onderscheid te maken met de
WHERE
clause en dat is begrijpelijk want ze doen iets vergelijkbaars.
De
WHERE
selecteert direct na de uitvoering van de FROM component
welke rijen behouden mogen blijven. Daarmee wordt dan verder
gerekend.
De HAVING
component echter wordt pas uitgevoerd na het maken van
de ‘doosjes’ bij het groeperen. Deze conditie beslist welke doosjes mogen blijven en welke uit het
resultaat zullen verdwijnen.
Oefeningen
Maak nu volgende oefeningen.
Geef voor oneven semesters een overzicht van het aantal studiepunten en het aantal OPO’s in dat semester. Sorteer volgens dalend aantal studiepunten. Zorg dat je de volgende figuur bekomt.
Een tip om de oneven semester te vinden: bekijk hiervoor de mogelijkheden van de rest bij gehele deling (de zogenaamde ‘modulo’ bewerking), doe een zoekopdracht naar ‘Modulo’ op de pagina https://www.postgresql.org/docs/current/functions-math.html.

SELECT semester, SUM(studiepunten) AS "Totaal aantal SP", COUNT(*) AS "aantal OPOs"
FROM opleidingsonderdeel
WHERE semester %2 != 0
GROUP BY semester
ORDER BY 2 DESC
Hoeveel verschillende coordinatoren zijn er per semester?
SELECT SEMESTER, COUNT(DISTINCT(coordinator)) AS AANTAL_VERSCH_COORDINATOREN
FROM opleidingsonderdeel
GROUP BY semester
Geef alle semesters met twee of meer opvolgvakken (vakken die eindigen op ‘2’) per semester.
SELECT semester, COUNT(naam) AS AANTAL_OPVOLG_VAKKEN
FROM opleidingsonderdeel
WHERE naam LIKE '%2'
GROUP BY semester
HAVING COUNT(naam) >= 2;
Dit geeft een ‘verkeerd’ resultaat omdat vak “Communicatie in het Frans 2 (sem 2)” de (onuitgesproken?) conventie qua naam niet volgt. Volgende versie geeft mogelijk een beter resultaat:
SELECT semester, COUNT(naam) AS AANTAL_OPVOLG_VAKKEN
FROM opleidingsonderdeel
WHERE naam LIKE '%2' OR naam LIKE '%2 (%'
GROUP BY semester
HAVING COUNT(naam) >= 2;
Oefeningen SQLzoo
Maak oefeningenreeks 5 op aggregatiefuncties (tabel ‘world’).