Programming, it turns out, is hard. The fundamental rules are typically simple and clear. But programs built on top of these rules tend to become complex enough to introduce their own rules and complexity. You’re building your own maze, in a way, and you might just get lost in it.
—Marijn Haverbeke
Subquery's
Schema studenten_campus
Bekijk even het ERD van de figuur. Je vindt het schema
‘studenten_campus’ in de databank ‘df’ via de pooling-connectie (of de
5… connectie). Doe even een SELECT *
van beide tabellen om een
idee te krijgen van de informatie die het schema bevat.

Het gaat concreet om een opsomming van alle acht campussen van UCLL en een tabel met 1000 studenten met per student enkele persoonlijke gegevens, naar welke campus ze gaan (een student kan in dit model maar tot één campus behoren) en hoe ver ze wonen van deze campus. Dat laatste veld is niet verplicht. We maakten dit schema trouwens als voorbeeld voor automatische datageneratie in DBSchema of met behulp van een een site zoals https://mockaroo.com.
Wie woont verder van een campus dan de gemiddelde afstand?
Er zijn meerdere soorten SQL-vragen. Eén soort vraagt enkel het antwoord te geven op een vraag. Dat kan een getal zijn, één of meerdere namen, een datum, ... Voor dit type vraag hoef je de gebruikte query(s) niet te geven. Enkel het antwoord is voldoende. Een voorbeeld van een dergelijke vraag is de volgende oefening. Maak die en controleer je antwoord.
Hoeveel studenten wonen verder van hun campus dan de gemiddelde afstand? Enkel het antwoord is gevraagd, niet de query’s die je gebruikte. Vind je het juiste antwoord: 401 studenten?
Dit is een typisch een vraag die je snel kan oplossen met twee eenvoudige query's. De eerste query zoekt het gemiddelde van alle afstanden:
SELECT AVG(afstand_tot_campus)
FROM student;
Het resultaat van deze query is 52.01658. Merk op dat null-waarden
niet meetellen om een gemiddelde te berekenen, zoals de documentatie kort vermeldt bij de functie ‘avg’.
Je kan nu dit getal gebruiken in een volgende query:
SELECT count(*). -- of eventueel gewoon * en dan aantal rijen aflezen in de query tool
FROM student
WHERE afstand_tot_campus > 52.01658;
Voor dit type van SQLvraag antwoord je dus gewoon ‘401’. Prima, punt verdiend!
Voor een ander type examenvraag moet je echter de query geven. Bovendien mag je maar één query geven, die je lector moet kunnen kopiëren en uitvoeren in pgAdmin. Een oplossing met twee aparte query’s, waarbij je dan het antwoord van de eerste moet kopiëren naar de tweede query is niet toegelaten.
Bovenstaande oefening heeft een elegante oplossing: een subquery gebruiken (ook wel ‘geneste query’ genoemd). Volgende query geeft de oplossing (401) van de oefening in één query:
SELECT COUNT(*)
FROM student
WHERE afstand_tot_campus > (
SELECT AVG(afstand_tot_campus)
FROM student
);
In de WHERE
clause wordt er een nieuwe SELECT
gestart.
Deze query noemen we de binnenquery (‘inner query’). De query errond wordt
dan de buitenquery (‘outer query’) genoemd. Je kan verschillende niveau’s diep
nesten. Een subquery kan één of meerdere rijen teruggeven aan de oproepende
query. Het bovenstaande is een voorbeeld van een scalaire subquery:
de subquery geeft immers enkel één getal terug. Je kan een subquery
nesten in de SELECT
, FROM
, WHERE
of
HAVING
clause.
Gemiddelde afstand van Proximus-studenten
Twee versies van dezelfde oefening
Los nu de volgende oefening op met een subquery:
Geef de gemiddelde afstand tot de campus van alle studenten die naar campus Proximus gaan. Gebruik één query met een subquery. Het antwoord is: 49.0319
SELECT avg(afstand_tot_campus)
FROM student
WHERE campus_id = (
SELECT id
FROM campus
WHERE naam = 'Campus Proximus'
);
Misschien dacht je bij het lezen van bovenstaande opgave ‘dat kan ik toch
ook gewoon met een JOIN
?’. Terecht! Los nu dezelfde oefening
op zonder subquery door enkel een JOIN
te gebruiken:
Geef de gemiddelde afstand tot de campus van alle studenten die naar
campus Proximus gaan m.b.v. een JOIN
.
SELECT avg(afstand_tot_campus)
FROM student S INNER JOIN campus C on S.campus_id = C.id
WHERE C.naam = 'Campus Proximus'
Kiezen tussen JOIN en subquery
Soms heb je geen keuze en moet je een subquery gebruiken, zoals bij de
eerste oefening. Die kan je niet oplossen met een JOIN
.
Omgekeerd heb je voor sommige vragen een JOIN
nodig. Vaak heb
je echter de keuze tussen een subquery en een JOIN
. Het is
moeilijk om eenduidig te omschrijven wat de beste keuze is. Voor complexe
queries is een JOIN
vaak efficiënter. Een subquery
is meestal wel eenvoudiger leesbaar.
Je moet in elk geval beide types van query kunnen gebruiken. Het is mogelijk dat je bij een bepaalde vraag op het opdracht expliciet één van beide zal moeten gebruiken.
Subquery met IN operator
We bekeken de IN
operator al in het hoofdstuk over de WHERE
clause. Achter de IN
kan het resultaat van een subquery komen. Maak
nu de volgende oefening.
Toon de vijf oudste studenten die op een campus in Heverlee zitten. Werk met een subquery.

SELECT voornaam || ' ' || familienaam AS naam, geboortedatum
FROM student
WHERE campus_id IN (
SELECT id
FROM campus
WHERE locatie = 'Heverlee'
)
ORDER BY geboortedatum
LIMIT 5
Ook dit voorbeeld kan perfect met een JOIN
opgelost worden.
Hermaak de oefening met een JOIN
.
SELECT voornaam || ' ' || familienaam AS naam, geboortedatum
FROM student S INNER JOIN campus C ON S.campus_id = C.id
WHERE locatie = 'Heverlee'
ORDER BY geboortedatum
LIMIT 5
Subquery met de ALL operator
De ALL
operator vergelijkt een waarde met elke waarde
van de resultaattabel. We illustreren dit met volgende voorbeeld.
Maak een lijst van alle studenten die jonger zijn dan alle studenten van wie de voornaam begint met ‘Al’. Dit zou je opnieuw met twee query’s kunnen oplossen. Vraag eerst de geboortedata op van alle studenten met de gevraagde voornaam:
SELECT geboortedatum
FROM student
WHERE (voornaam like 'Al%')
ORDER BY geboortedatum DESC
Het blijkt dat de jongste van deze studenten geboren is op 27 februari 2004. Je kan nu in een tweede query alle studenten opvragen met een geboortedatum groter dan (want ze moeten jonger zijn!) deze datum:
SELECT *
FROM student
WHERE geboortedatum > '2004-02-27'
ORDER BY geboortedatum DESC
Je vindt uiteindelijk een lijst van 22 studenten.
Deze twee opeenvolgende query’s kan je combineren in één query door gebruik te maken van een subquery. Je vindt dezelfde 22 studenten:
SELECT *
FROM student
WHERE geboortedatum > ALL (
SELECT geboortedatum
FROM student
WHERE voornaam LIKE 'Al%'
)
ORDER BY geboortedatum DESC
De volgende oefening ziet er op het eerste zicht niet zo moeilijk uit, maar er is een onverwacht probleem waar je misschien wel wat mee zal worstelen. Tips en oplossing vind je op de gebruikelijke plaats, maar probeer eerst zelf!
Geef een lijst van alle studenten die dichter wonen bij hun campus dan alle studenten die naar een campus in Diest gaan en waarvan de familienaam twee keer de letter ‘a’ bevat (hoofdletters mogen ook). Je mag maar één query gebruiken (die natuurlijk wel subquery’s mag bevatten). Rangschik volgens stijgend campus_id en dalende afstand.

Het is een goed idee om een dergelijke oefening eerst in stukjes op te lossen. Deelvraag 1: wat is het campus_id van de campus(sen) in Diest?
SELECT id
FROM campus
WHERE locatie = 'Diest'
Er blijkt maar één campus in Diest te zijn, nl. de campus met id = 2. Hiermee kunnen we nu deelvraag 2 beantwoorden: geef alle studenten uit deze campus die een familienaam met twee keer (hoofd- of kleine letter maakt niet uit) een ‘a’ hebben.
SELECT *
FROM student
WHERE lower(familienaam) LIKE '%a%a%' AND campus_id = 2
ORDER BY afstand_tot_campus ASC
Ga na dat het resultaat effectief alleen studenten bevat die naar campus 2 gaan en een familienaam met minstens 2 keer de letter ‘a’ hebben. De kleinste afstand blijkt 15 km te zijn. Tenslotte kunnen we het laatste deel van de vraag beantwoorden: alle studenten uit de student-tabel die minder dan 15 km van hun campus wonen:
SELECT *
FROM student
WHERE afstand_tot_campus < 15
ORDER BY campus_id ASC, afstand_tot_campus DESC
Dit blijkt een lijst van 106 studenten te zijn. Alles samenpuzzelen tot één grote query, met twee subquery's erin is nu niet meer zo moeilijk:
SELECT *
from student
WHERE afstand_tot_campus < ALL (
SELECT afstand_tot_campus
FROM student
WHERE lower(familienaam) LIKE '%a%a%' AND campus_id IN (
SELECT id
FROM campus
WHERE locatie = 'Diest'
)
)
ORDER BY campus_id ASC, afstand_tot_campus DESC
Toch blijkt deze query niet het juiste resultaat te geven. Het resultaat bevat immer geen enkele rij! Wat is er aan de hand?
Het probleem zit in deelvraag 2. De lijst met alle studenten uit campus 4 met twee keer een ‘a’ in de familienaam bevat immers een aantal null waarden in de afstand. Elke vergelijking van een getal met een null geeft altijd een null, zoals deze query aantoont:
SELECT null > 999 -- resultaat is niet true of false maar null
SELECT null <= 999 -- resultaat is niet true of false maar null
Het gevolg is dat elke test WHERE afstand_tot_campus < ALL ...
altijd zal mislukken en er dus geen enkele rij overblijft in het resultaat.
Gelukkig is de oplossing niet zo moeilijk:
vermijd in deelvraag 2 null waarden in het resultaat
. We voegen dus één enkele voorwaarde in de eerste subquery bij:
SELECT *
from student
WHERE afstand_tot_campus < ALL (
SELECT afstand_tot_campus
FROM student
WHERE lower(familienaam) LIKE '%a%a%'
AND afstand_tot_campus IS NOT null --Vermijd null in het deelresultaat
AND campus_id IN (
SELECT id
FROM campus
WHERE locatie = 'Diest'
)
)
ORDER BY campus_id ASC, afstand_tot_campus DESC
Subquery met de ANY operator
De ANY
operator vergelijkt een waarde met elke waarde in een tabel
en is voldaan als de vergelijking minstens voor één waarde uit de tabel geldt. De tabel waarmee vergeleken wordt, moet minstens één waarde bevatten.
Het voorbeeld hieronder legt het principe uit.
We willen een lijst van alle studenten van campus 7 die ouder zijn dan minstens één student van campus 4. Volgende code geeft een lijst van 135 studenten die voldoen aan de voorwaarde:
SELECT *
FROM student
WHERE campus_id = 7 AND geboortedatum < ANY(
SELECT geboortedatum
FROM student
WHERE campus_id = 4
);
Zoals altijd is het een goed idee om een query goed te ontleden om te snappen hoe alles werkt. Laten we even de lijst opvragen van alle studenten van campus 7:
SELECT *
FROM student
WHERE campus_id = 7
Die lijst bevat 136 studenten, dus eentje meer dan de eerste query! We onderzoeken even snel waar het verschil zit.
We zoeken eerst de jongste student van campus 4:
SELECT *
FROM student
WHERE campus_id = 4
ORDER BY 4 DESC
Dat blijkt Lisa Vargas te zijn, geboren op 30 september 2004. In campus 7
is er echter één student die nog jonger is dan Lisa Vargas, nl. Kate
Gonzales, geboren op 12 oktober 2004. Zij voldoet dus niet aan de
voorwaarde met de ANY
en staat bijgevolg niet in het resultaat.