410f37a008
Introduce a simpler training-based workflow for the OnlineAcademy Playwright runner. Changes included: - support --training-dir and automatic loading of content.md and training.json - support --env-file so OA credentials no longer need manual shell sourcing - resolve asset paths relative to the training directory - improve human-readable review and execute output with step-by-step progress - keep the browser open after execute when no save flag is used - add optional --save flow that clicks 'Opslaan als' and then chooses 'Concept' - add a concrete user guide for preparing and running training imports - update handover documentation to reflect the current repo structure and workflow - align the repo around trainings/<name>/content.md, training.json and assets/ - remove reliance on older pilot/test material in the documented main flow
380 lines
16 KiB
Markdown
380 lines
16 KiB
Markdown
---
|
|
training: SQL met FitWorks
|
|
---
|
|
|
|
# Module: Hoofdstuk 4 - Basisqueries
|
|
|
|
## Pagina: Terugblik en opening
|
|
|
|
### Tekst
|
|
**Titel:** Terugblik
|
|
|
|
In hoofdstuk 3 heb je kennisgemaakt met SQL als declaratieve taal: je schrijft wat je wilt, niet hoe de database dat uitrekent. Je hebt de drie groepen van SQL leren kennen - DML, DDL en DCL - en de fitworks-database geinstalleerd. Die database staat nu klaar.
|
|
|
|
Neem even 30 seconden. In hoofdstuk 3 stond al een korte `SELECT`-query als voorbeeld. Kon je op dat moment al raden wat hij deed? En nu, na het lezen van de beschrijving van DDL en DML, zou je dan anders antwoorden? Bewaar dat gevoel; aan het einde van dit hoofdstuk kijk je terug op je eigen redenering.
|
|
|
|
### Tekst
|
|
**Titel:** Opening
|
|
|
|
Je bent systeembeheerder bij FitWorks. De vestigingsmanager loopt op je af: "Kun jij me snel een lijst geven van alle leden die geen telefoonnummer hebben ingevuld? We willen ze via e-mail aanschrijven."
|
|
|
|
Een simpele vraag. Maar hoe stel je die aan een database?
|
|
|
|
Je kunt niet op een knop klikken. Er bestaat geen menu-optie "geef me leden zonder telefoon". Je hebt een instructie nodig die de database begrijpt: precies, ondubbelzinnig en in de juiste volgorde. Dat is een query. En er zit een addertje onder het gras in die vraag over telefoonnummers. Dat addertje heet `NULL`. Je ontdekt het in sectie 4.3.
|
|
|
|
## Pagina: Leerdoelen
|
|
|
|
### Tekst
|
|
**Titel:** Leerdoelen
|
|
|
|
Na dit hoofdstuk kun je:
|
|
|
|
- een query schrijven met `SELECT`, `FROM` en `WHERE` en uitleggen wat elk onderdeel doet;
|
|
- resultaten filteren met vergelijkingsoperatoren, `BETWEEN`, `IN` en logische operators `AND`, `OR` en `NOT`;
|
|
- uitleggen wat `NULL` is, waarom je er niet op kunt filteren met `=`, en hoe je dat correct doet met `IS NULL`;
|
|
- resultaten sorteren met `ORDER BY` en dubbele waarden verwijderen met `DISTINCT`;
|
|
- kolomaliassen toewijzen met `AS` en vaste tekst, literals, in een query opnemen;
|
|
- voor- en achternamen samenvoegen met `CONCAT` en datatypes omzetten met `CAST`.
|
|
|
|
## Pagina: 4.1 SELECT en FROM
|
|
|
|
### Tekst
|
|
**Titel:** De balie vraagt, de database antwoordt
|
|
|
|
Het is dinsdagochtend bij FitWorks en een trainer vraagt aan de receptie: "Wie staat er allemaal ingeschreven?" De receptioniste opent phpMyAdmin, typt een instructie en krijgt binnen een seconde een lijst op het scherm. Die instructie is klein maar compleet:
|
|
|
|
SELECT voornaam, achternaam
|
|
FROM leden;
|
|
|
|
Dit is een volledige SQL-query. Ze bestaat uit twee onderdelen. `SELECT` zegt welke kolommen je wilt zien, gescheiden door komma's. `FROM` zegt uit welke tabel die kolommen komen. Zonder `FROM` weet de database niet waar hij moet zoeken. Zonder `SELECT` weet hij niet wat hij moet tonen. Ze zijn onlosmakelijk verbonden.
|
|
|
|
De volgorde is vastgelegd: `SELECT` altijd eerst, dan `FROM`, dan pas eventuele aanvullingen zoals `WHERE`. SQL klaagt direct als je die volgorde omdraait. Het is geen aanbeveling, maar een syntaxregel.
|
|
|
|
### Afbeelding
|
|
**Bestand:** assets/hoofdstuk_04_scherm_01_select_from_anatomie.png
|
|
**Titel:** Anatomie van een SELECT-query
|
|
**Onderschrift:** Overzicht van de onderdelen `SELECT`, `FROM` en later `WHERE`.
|
|
**Alt:** Diagram van een SELECT-query met de onderdelen SELECT, FROM en WHERE.
|
|
|
|
### Tekst
|
|
**Titel:** Alle kolommen tegelijk
|
|
|
|
Soms wil je snel alle kolommen zien die een tabel bevat, zonder ze een voor een op te noemen. Dat doe je met de asterisk `*`:
|
|
|
|
SELECT *
|
|
FROM leden;
|
|
|
|
De `*` is een verkorting voor "geef me alles". Handig bij het verkennen van een tabel. In de praktijk gebruik je `*` spaarzaam: het haalt onnodig veel data op, maakt je query minder leesbaar en kan vertragen als een tabel tientallen kolommen heeft. Maar als leergereedschap bij het doorzoeken van fitworks is het prima.
|
|
|
|
Kolomnamen in fitworks zijn altijd lowercase. Schrijf dus `voornaam`, niet `Voornaam` of `VOORNAAM`. MySQL maakt op de meeste systemen geen onderscheid tussen hoofd- en kleine letters in kolomnamen, maar consequent lowercase schrijven voorkomt verwarring.
|
|
|
|
### Open vraag
|
|
**Vraag:** Je wilt de e-mailadressen en geboortedatums van alle leden zien. Welke query schrijf je?
|
|
**Toelichting:** Modelantwoord: `SELECT email, geboortedatum FROM leden;`
|
|
|
|
## Pagina: 4.2 WHERE
|
|
|
|
### Tekst
|
|
**Titel:** Filteren op voorwaarden
|
|
|
|
De tabel `leden` bevat zo'n 400 rijen. Als je de manager een overzicht stuurt van actieve leden, heb je die 400 rijen niet nodig. Je wilt alleen de rijen waarbij `actief` gelijk is aan `1`. Daarvoor gebruik je `WHERE`.
|
|
|
|
`WHERE` is een filter. De database loopt alle rijen van de opgegeven tabel langs en geeft alleen de rijen terug die voldoen aan de voorwaarde die jij opgeeft. Rijen die er niet aan voldoen, worden stilletjes weggelaten.
|
|
|
|
SELECT voornaam, achternaam, email
|
|
FROM leden
|
|
WHERE actief = 1;
|
|
|
|
`WHERE` staat altijd na `FROM`. De volgorde `SELECT`, `FROM`, `WHERE` is onwrikbaar. Typ je `WHERE` voor `FROM`, dan krijg je een foutmelding.
|
|
|
|
### Tekst
|
|
**Titel:** Vergelijkingsoperatoren
|
|
|
|
In een `WHERE`-clausule vergelijk je een kolom met een waarde. Voor die vergelijking gebruik je een operator.
|
|
|
|
`BETWEEN` filtert op een bereik, inclusief de grenswaarden zelf:
|
|
|
|
SELECT naam, prijs_per_maand
|
|
FROM abonnementen
|
|
WHERE prijs_per_maand BETWEEN 20 AND 40;
|
|
|
|
`IN` laat je meerdere toegestane waarden opgeven als een lijst:
|
|
|
|
SELECT betaling_id, bedrag, status
|
|
FROM betalingen
|
|
WHERE status IN ('betaald', 'openstaand');
|
|
|
|
Dit is identiek aan `WHERE status = 'betaald' OR status = 'openstaand'`, maar compacter en beter leesbaar.
|
|
|
|
### Afbeelding
|
|
**Bestand:** assets/hoofdstuk_04_scherm_02_where_operatoren.png
|
|
**Titel:** WHERE-operatoren
|
|
**Onderschrift:** Overzicht van veelgebruikte vergelijkingsoperatoren in fitworks.
|
|
**Alt:** Overzicht van WHERE-operatoren met voorbeelden uit de fitworks-database.
|
|
|
|
### Tekst
|
|
**Titel:** Logische operators
|
|
|
|
Voorwaarden kun je combineren.
|
|
|
|
Met `AND` moeten beide voorwaarden gelden:
|
|
|
|
SELECT voornaam, achternaam
|
|
FROM leden
|
|
WHERE actief = 1
|
|
AND locatie_id = 2;
|
|
|
|
Met `OR` hoeft minimaal een voorwaarde te kloppen:
|
|
|
|
SELECT betaling_id, bedrag, status
|
|
FROM betalingen
|
|
WHERE status = 'mislukt'
|
|
OR status = 'openstaand';
|
|
|
|
`NOT` keert een voorwaarde om:
|
|
|
|
SELECT voornaam, achternaam
|
|
FROM leden
|
|
WHERE NOT actief = 0;
|
|
|
|
### Tekst
|
|
**Titel:** Tekst en getallen
|
|
|
|
Getallen schrijf je zonder aanhalingstekens: `actief = 1`.
|
|
|
|
Tekst schrijf je tussen enkele aanhalingstekens: `status = 'betaald'`.
|
|
|
|
Gebruik in MySQL geen dubbele aanhalingstekens voor tekstwaarden. In de fitworks-database zijn kolomnamen nooit geciteerd; ze staan altijd zonder aanhalingstekens in de query.
|
|
|
|
### Open vraag
|
|
**Vraag:** Welke query schrijf je om trainers op locatie 1 te tonen die ook een specialisatie hebben opgegeven?
|
|
**Toelichting:** Mogelijk modelantwoord: `SELECT voornaam, achternaam, specialisatie FROM trainers WHERE locatie_id = 1 AND specialisatie IS NOT NULL;` Met `AND` moeten beide voorwaarden tegelijk waar zijn. Een trainer zonder specialisatie of van een andere locatie valt dan af.
|
|
|
|
## Pagina: 4.3 NULL
|
|
|
|
### Tekst
|
|
**Titel:** Een waarde die geen waarde is
|
|
|
|
Terug naar de vraag van de vestigingsmanager: leden zonder telefoonnummer. Wat staat er in de database als iemand geen nummer heeft opgegeven? Geen nul. Geen lege string. Er staat iets anders: `NULL`.
|
|
|
|
`NULL` is de afwezigheid van een waarde. Het is geen getal, geen tekst en geen spatie. Het betekent: er is geen informatie. In fitworks kom je `NULL` op meerdere plekken tegen, bijvoorbeeld bij `tussenvoegsel`, `telefoon` en `specialisatie`.
|
|
|
|
### Tekst
|
|
**Titel:** Waarom werkt = NULL niet
|
|
|
|
Dit is een veelgemaakte fout:
|
|
|
|
SELECT voornaam, achternaam
|
|
FROM leden
|
|
WHERE telefoon = NULL;
|
|
|
|
De query wordt uitgevoerd zonder foutmelding, maar geeft nul resultaten terug. In SQL is `NULL` geen gewone waarde die je kunt vergelijken. De uitdrukking `NULL = NULL` is niet waar, maar onbekend. En een onbekende vergelijking telt niet als waar.
|
|
|
|
### Tekst
|
|
**Titel:** De juiste aanpak
|
|
|
|
Gebruik speciale `NULL`-controles:
|
|
|
|
SELECT voornaam, achternaam
|
|
FROM leden
|
|
WHERE telefoon IS NULL;
|
|
|
|
SELECT voornaam, achternaam
|
|
FROM leden
|
|
WHERE telefoon IS NOT NULL;
|
|
|
|
`IS NULL` is de enige correcte manier om te controleren of een kolom geen waarde bevat. `IS NOT NULL` doet het omgekeerde.
|
|
|
|
### Afbeelding
|
|
**Bestand:** assets/hoofdstuk_04_scherm_03_null_afhandeling.png
|
|
**Titel:** NULL-afhandeling
|
|
**Onderschrift:** Vergelijking tussen `= NULL` en `IS NULL`.
|
|
**Alt:** Diagram dat het verschil laat zien tussen = NULL en IS NULL.
|
|
|
|
### Tekst
|
|
**Titel:** Wat gaat er mis als je dit negeert
|
|
|
|
Het probleem met `= NULL` is verraderlijk: de query geeft geen foutmelding en lijkt normaal te werken, maar geeft stilletjes een leeg resultaat. Daardoor kan een rapport foutieve conclusies oproepen, bijvoorbeeld dat alle leden een telefoonnummer hebben ingevuld.
|
|
|
|
Onthoud: `NULL` vergelijk je nooit met `=`. Altijd met `IS NULL` of `IS NOT NULL`.
|
|
|
|
### Open vraag
|
|
**Vraag:** Schrijf de query die alle lestypes toont waarvan de beschrijving niet is ingevuld.
|
|
**Toelichting:** Modelantwoord: `SELECT naam, duur_minuten, niveau FROM lestypes WHERE beschrijving IS NULL;`
|
|
|
|
## Pagina: 4.4 ORDER BY en DISTINCT
|
|
|
|
### Tekst
|
|
**Titel:** ORDER BY
|
|
|
|
De database geeft rijen terug in de volgorde die hem uitkomt. Als je een ledenlijst wilt sorteren op inschrijfdatum, heb je `ORDER BY` nodig.
|
|
|
|
SELECT voornaam, achternaam, lid_sinds
|
|
FROM leden
|
|
ORDER BY lid_sinds;
|
|
|
|
Standaard sorteert `ORDER BY` oplopend. Dat heet `ASC`. Wil je de nieuwste inschrijvingen bovenaan, gebruik dan `DESC`:
|
|
|
|
SELECT voornaam, achternaam, lid_sinds
|
|
FROM leden
|
|
ORDER BY lid_sinds DESC;
|
|
|
|
Je kunt ook op meerdere kolommen sorteren:
|
|
|
|
SELECT achternaam, voornaam, lid_sinds
|
|
FROM leden
|
|
ORDER BY achternaam, voornaam;
|
|
|
|
`ORDER BY` staat altijd als laatste in de query, dus na `WHERE` als die ook aanwezig is.
|
|
|
|
### Afbeelding
|
|
**Bestand:** assets/hoofdstuk_04_scherm_04_order_by_distinct.png
|
|
**Titel:** ORDER BY en DISTINCT
|
|
**Onderschrift:** Sorteren van resultaten en verwijderen van duplicaten.
|
|
**Alt:** Diagram dat laat zien hoe ORDER BY sorteert en DISTINCT duplicaten verwijdert.
|
|
|
|
### Tekst
|
|
**Titel:** DISTINCT
|
|
|
|
Wil je alleen unieke specialisaties zien, dan gebruik je `DISTINCT`:
|
|
|
|
SELECT DISTINCT specialisatie
|
|
FROM trainers;
|
|
|
|
`DISTINCT` zet je direct na `SELECT`. Het verwijdert rijen die in alle geselecteerde kolommen identiek zijn. `SELECT DISTINCT specialisatie, voornaam` geeft dus unieke combinaties van specialisatie en voornaam, niet per se unieke specialisaties.
|
|
|
|
### Open vraag
|
|
**Vraag:** Welke query schrijf je om alle abonnementsnamen met maandprijs te tonen, gesorteerd van duurste naar goedkoopste? Wat verandert er als je DISTINCT toevoegt?
|
|
**Toelichting:** Modelantwoord: `SELECT naam, prijs_per_maand FROM abonnementen ORDER BY prijs_per_maand DESC;` `DISTINCT` verandert hier niets zolang namen uniek zijn. Het effect hangt af van de geselecteerde kolommen; DISTINCT werkt op de combinatie van alle geselecteerde velden.
|
|
|
|
## Pagina: 4.5 Kolomaliassen en literals
|
|
|
|
### Tekst
|
|
**Titel:** Kolomaliassen
|
|
|
|
Met een alias geef je een kolom in het resultaat een andere naam, zonder de tabel te veranderen.
|
|
|
|
SELECT voornaam AS Voornaam,
|
|
achternaam AS Achternaam,
|
|
lid_sinds AS 'Lid sinds'
|
|
FROM leden;
|
|
|
|
Het resultaat toont leesbaardere kolomkoppen. De alias bestaat alleen in dit queryresultaat. Als een alias een spatie bevat, zet je hem tussen enkele aanhalingstekens.
|
|
|
|
### Afbeelding
|
|
**Bestand:** assets/hoofdstuk_04_scherm_05_aliassen_literals.png
|
|
**Titel:** Aliassen en literals
|
|
**Onderschrift:** Kolomaliassen maken queryresultaten leesbaarder.
|
|
**Alt:** Voorbeeld van aliassen en literals in een SQL-resultaat.
|
|
|
|
### Tekst
|
|
**Titel:** Literals
|
|
|
|
Een literal is een vaste waarde die je direct in een query schrijft. Dat kan tekst of een getal zijn dat voor elke rij hetzelfde blijft.
|
|
|
|
SELECT voornaam,
|
|
achternaam,
|
|
'FitWorks lid' AS type
|
|
FROM leden
|
|
WHERE actief = 1;
|
|
|
|
Zo krijgt elke rij een extra kolom `type` met dezelfde vaste waarde.
|
|
|
|
### Open vraag
|
|
**Vraag:** Schrijf een query die de naam en maandprijs van alle abonnementen toont, met als kolomkoppen Abonnementsnaam en Maandprijs. Sorteer op prijs, laagste eerst.
|
|
**Toelichting:** Modelantwoord: `SELECT naam AS Abonnementsnaam, prijs_per_maand AS Maandprijs FROM abonnementen ORDER BY prijs_per_maand;`
|
|
|
|
## Pagina: 4.6 CONCAT en CAST
|
|
|
|
### Tekst
|
|
**Titel:** CONCAT
|
|
|
|
Bij FitWorks zijn voornaam en achternaam in aparte kolommen opgeslagen. Wil je een volledige naam als een kolom tonen, dan gebruik je `CONCAT`:
|
|
|
|
SELECT CONCAT(voornaam, ' ', achternaam) AS volledige_naam
|
|
FROM leden;
|
|
|
|
Sommige leden hebben ook een tussenvoegsel. Dan helpt `COALESCE` om `NULL` op te vangen:
|
|
|
|
SELECT CONCAT(
|
|
voornaam, ' ',
|
|
COALESCE(CONCAT(tussenvoegsel, ' '), ''),
|
|
achternaam
|
|
) AS volledige_naam
|
|
FROM leden;
|
|
|
|
`COALESCE` vervangt `NULL` door een standaardwaarde, hier een lege string.
|
|
|
|
### Tekst
|
|
**Titel:** CAST
|
|
|
|
Elke waarde in een database heeft een datatype. Soms wil je een waarde als een ander type gebruiken. Daarvoor is `CAST`.
|
|
|
|
SELECT naam,
|
|
CONCAT('Prijs: ', CAST(prijs_per_maand AS CHAR)) AS prijsweergave
|
|
FROM abonnementen;
|
|
|
|
`CAST(prijs_per_maand AS CHAR)` zet een getal om naar tekst, zodat je het veilig kunt combineren met andere tekst.
|
|
|
|
### Afbeelding
|
|
**Bestand:** assets/hoofdstuk_04_scherm_06_concatenatie_cast.png
|
|
**Titel:** CONCAT en CAST
|
|
**Onderschrift:** Kolommen samenvoegen en datatypes omzetten.
|
|
**Alt:** Diagram dat laat zien hoe CONCAT waarden samenvoegt en CAST datatypes omzet.
|
|
|
|
### Tabel
|
|
**Titel:** Veelgebruikte CAST-conversies
|
|
**Top header:** Aan
|
|
**Left header:** Uit
|
|
| Van | Naar | Gebruik |
|
|
|-----|------|---------|
|
|
| Getal | Tekst | `CAST(prijs_per_maand AS CHAR)` |
|
|
| Tekst | Geheel getal | `CAST('42' AS UNSIGNED)` |
|
|
| Datum | Tekst | `CAST(geboortedatum AS CHAR)` |
|
|
|
|
### Open vraag
|
|
**Vraag:** Schrijf een query die de volledige naam van elke trainer toont in een kolom genaamd Trainer.
|
|
**Toelichting:** Modelantwoord: `SELECT CONCAT(voornaam, ' ', achternaam) AS Trainer FROM trainers;`
|
|
|
|
## Pagina: Quiz
|
|
|
|
### Meerkeuze
|
|
**Vraag:** Je wilt alle betalingen zien met status `mislukt` of `openstaand`, gesorteerd van hoog naar laag bedrag. Welke query is correct?
|
|
- [ ] `SELECT betaling_id, bedrag, status FROM betalingen WHERE status = 'mislukt' OR 'openstaand' ORDER BY bedrag DESC;`
|
|
- [x] `SELECT betaling_id, bedrag, status FROM betalingen WHERE status IN ('mislukt', 'openstaand') ORDER BY bedrag DESC;`
|
|
- [ ] `SELECT betaling_id, bedrag, status FROM betalingen WHERE status IN ('mislukt', 'openstaand') ORDER BY bedrag ASC;`
|
|
- [ ] `SELECT betaling_id, bedrag, status FROM betalingen WHERE status = 'mislukt' OR status = 'openstaand' ORDER BY bedrag ASC;`
|
|
**Toelichting:** Optie B is correct. `IN` maakt de statusfilter compact en `ORDER BY bedrag DESC` sorteert van hoog naar laag. Optie A is syntactisch fout, en opties C en D sorteren in de verkeerde richting.
|
|
|
|
### Meerkeuze
|
|
**Vraag:** Welke query geeft correct alle leden terug die geen tussenvoegsel hebben?
|
|
- [ ] `SELECT voornaam, achternaam FROM leden WHERE tussenvoegsel = NULL;`
|
|
- [ ] `SELECT voornaam, achternaam FROM leden WHERE tussenvoegsel = '';`
|
|
- [x] `SELECT voornaam, achternaam FROM leden WHERE tussenvoegsel IS NULL;`
|
|
- [ ] `SELECT voornaam, achternaam FROM leden WHERE tussenvoegsel IS NOT NULL;`
|
|
**Toelichting:** Optie C is correct. `NULL` vergelijk je niet met `=`, maar met `IS NULL`. Een lege string is bovendien iets anders dan `NULL`.
|
|
|
|
### Meerkeuze
|
|
**Vraag:** Een collega ziet met `SELECT DISTINCT specialisatie, voornaam FROM trainers;` nog steeds herhaalde waarden in `specialisatie`. Wat is de oorzaak?
|
|
- [ ] `DISTINCT` werkt alleen op de eerste geselecteerde kolom.
|
|
- [x] `DISTINCT` verwijdert dubbele rijen op basis van alle geselecteerde kolommen tegelijk.
|
|
- [ ] `DISTINCT` moet altijd gecombineerd worden met `ORDER BY`.
|
|
- [ ] `DISTINCT` werkt niet op tekstkolommen zoals `specialisatie`.
|
|
**Toelichting:** `DISTINCT` kijkt naar de combinatie van alle geselecteerde kolommen. Twee trainers met dezelfde specialisatie maar een andere voornaam zijn dus nog steeds twee unieke rijen. Voor alleen unieke specialisaties moet je alleen die kolom selecteren.
|
|
|
|
## Pagina: Samenvatting en vooruitblik
|
|
|
|
### Tekst
|
|
**Titel:** Samenvatting
|
|
|
|
`SELECT` en `FROM` vormen het hart van elke query. Met `WHERE` filter je rijen, met `ORDER BY` sorteer je resultaten en met `DISTINCT` verwijder je duplicaten.
|
|
|
|
`NULL` is de afwezigheid van een waarde. Je filtert erop met `IS NULL` en `IS NOT NULL`, nooit met `=`.
|
|
|
|
Kolomaliassen met `AS` maken resultaten leesbaarder. Literals voegen vaste waarden toe. `CONCAT` plakt waarden aan elkaar en `CAST` zet waarden om naar een ander datatype.
|
|
|
|
### Tekst
|
|
**Titel:** Vooruitblik
|
|
|
|
Je hebt nu de basis in handen om de fitworks-database te bevragen. In hoofdstuk 5 ga je verder met tekstbewerkingen, zoals `LIKE` voor zoeken op patronen, en met datumfuncties. Je gaat de data dan niet alleen ophalen, maar er echt mee werken.
|