Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Geen leerstof en/of in opbouw
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Informatica is het verwerken van informatie. Het beheer van gegevens is dus vaak het eigenlijke doel van een informaticasysteem en is de bestandsorganisatie minstens zo belangrijk dan de eigenlijke programmatuur. Met gegevens bedoelen we hier informatie die voor kortere of langere tijd wordt opgeslagen en die onontbeerlijk is voor de werking van de organisatie/onderneming/…
Indien bepaalde hoofdstukken of onderdelen niet duidelijk zijn of je hebt suggesties ter verbetering/aanvulling, aarzel dan niet om ons te contacteren.
Veel leer- en leesplezier.
Peter Spaas, Vincent Van Camp en Vincent Nys
Installatiehandleiding
Basisscherm MySQL Workbench:
Stap 1: Klik op het plusje.
Stap 2: Vul de juiste gegevens in.
Voor jouw gebruikersnaam en paswoord verwijzen we naar de e-mail die je hebt ontvangen van lector V. Nys.
Je klikt na het invullen van de juiste gegevens op de knop "Store in Vault" en vult het paswoord dat je via vermelde mail hebt ontvangen in.
Stap 3: Connectie testen.
Je moet volgende boodschap krijgen.
In alle lessen (hoorcolleges en practica) hebben we volgende zaken nodig:
Deze cursus
Deze cursus wordt gebruikt als handboek binnen de opleiding .
Download:
Een laptop met daarop Windows 10 (Pro/Education/Enterprise) en MySQL Workbench. Je vindt gedetailleerde filmpjes en schriftelijke instructies op .
, Peter Spaas, LannooCampus
Een klassiek voorbeeld daarvan is een lijst met de namen en adressen. Die lijst kan er als volgt uitzien:
Naam
Voornaam
Adres
Postcode
Gemeente
Telefoon
Verdieping
PIETERS
PIeter
Tralalastraat 25
2660
Hoboken
03 333 33 33
1
JANSSENS
Jan
Jasstraat 2
2000
Antwerpen
03 222 22 22
1
DEBONDT
Ron
Jopstraat 5
2100
Deurne
03 111 11 11
3
JORIS
Joost
Boedreef 25
2600
Berchem
03 444 44 44
2
VOET
Bart
Plopstraat 9
2630
Aartselaar
03 888 88 88
3
In bovenstaand voorbeeld is de eerste rij de aanduiding van de kolommen (veldnamen).
Bovenstaand voorbeeld bevat dus vijf rijen (records) die telkens bepaalde waarden bevatten, hier gaat het over de "Naam", "Voornaam", "Adres", "Postcode", "Gemeente", "Telefoon" en "Verdieping".
Een bedrijf in de productiesector zal onder andere volgende gegevens willen bijhouden, nl.:
Personeelsgegevens
De zgn. productiegegevens. Hierbij kunnen we denken aan bv. de productsamenstelling
De gegevens betreffende de noodzakelijke bestellingen
De gegevens van de bestaande klanten
De gegevens van de leveranciers
De gegevens m.b.t. de goederen die nog in stock zijn en/of de goederen die besteld moeten worden
In dit geval is het best om voor elk bullet point een aparte tabel te voorzien. Groepjes gegevens die sterk aan elkaar gelinkt zijn (bijvoorbeeld "voornaam werknemer" en "familienaam werknemer" komen in dezelfde tabel). Groepjes gegevens die losser gelinkt zijn komen in verschillende tabellen. Er zijn nog andere redenen om verschillende tabellen te gebruiken. Die komen later.
Een bank houdt per rekening van een bepaalde klant geen box bij met daarin het geld van een klant. Het bedrag op de rekening van die klant is een cijfer dat wordt bijgehouden in een database. Er zou bijvoorbeeld een tabel Rekeningen
kunnen zijn, die je je zo kan voorstellen:
123456789
15.000
spaarrekening
456789123
2.124.000
belegging
789123456
11.000
spaarrekening
Daarnaast heeft de bank nog allerlei informatie, bijvoorbeeld over hypotheken,... Die zullen vermoedelijk in andere tabellen staan. De volledige verzameling tabellen vormt de database van de bank.
De "relationele" database is waarschijnlijk de meestgebruikte soort database. Dit type database is uitgewerkt in de jaren 1970 en steunt op de wiskundige verzamelingenleer.
Zonder deze wiskundige fundering in veel detail te behandelen: het basisidee is dat "interessante gehelen" van informatie in verzamelingen van één soort data worden opgedeeld. Tussen deze verzamelingen bestaan verbanden.
Onderstaande figuur illustreert dit. Op deze figuur zijn de interessante gehelen personen en boeken. Deze twee verzamelingen bestaan los van elkaar, maar er is een verband: sommige personen hebben sommige boeken gelezen.
Er kunnen veel verbanden zijn. We kunnen bijvoorbeeld een tweede verband hebben dat uitdrukt dat een bepaalde persoon een bepaald boek heeft geschreven:
Om dit in een elektronisch systeem voor te stellen, maken we geen tekeningen, maar gebruiken we tabellen. We doen dit zowel voor de "interessante gehelen" (dus de elementen van de verzamelingen) als voor de verbanden tussen elementen van deze verzamelingen.
Voor de eerste tekening zou een tabelvoorstelling van de verzamelingen er zo kunnen uitzien:
Voornaam
Familienaam
Geboortejaar
Vincent
Nys
1987
Esther
Nys
1990
Michiel
Nys
1984
William
Shakespeare
1564
Ted
Chiang
1967
Kazuo
Ishiguro
1954
Titel
Jaar van uitgave
The Tempest
1623
Exhalation: Stories
2019
Never Let Me Go
2005
Ook het verband zou kunnen worden uitgedrukt met een tabel. Hoe dat precies werkt is voor iets verder, maar een vereenvoudiging die een goed beeld geeft is als volgt:
Lezer
Titel
Michiel
The Tempest
Vincent
Exhalation: Stories
Esther
Never Let Me Go
Zowat alle DBMS'en voor relationele databases spreken (ongeveer) dezelfde taal: de "Structured Query Language" of SQL. Deze taal is ook gebaseerd op de relationele algebra's en ze maakt dat je vrij snel kan leren werken met één relationele database als je al met een andere relationele database overweg kan. Elk DBMS spreekt wel een eigen "dialect" van SQL, dus je kan niet verwachten dat code voor bijvoorbeeld PostgreSQL (een type relationele database) letterlijk kan worden uitgevoerd in SQLite (een ander type relationele database). Maar normaal moet je er niet veel aan aanpassen.
Typisch voor relationele databases is dat je op voorhand moet vastleggen wat de structuur van je database is: welke tabellen er zijn en wat voor gegevens hier in mogen worden geplaatst. Deze structuur noemen we het "schema". Als je nieuwe gegevens wil bijhouden die niet passen binnen de structuur, moet je eerst de structuur aanpassen. Dit is niet bij alle soorten databanken zo. Andere types databanken hebben soms een heel losse structuur. Dit geeft hen soms meer flexibiliteit, maar zorgt er soms ook voor dat ze minder kunnen waken over de integriteit van hun gegevens.
De basiswoordenschat van relationele databanken is als volgt:
entiteittypes: de verzamelingen van interessante gehelen waarover we informatie bijhouden. Op de figuur onderaan is dit bijvoorbeeld de hele verzameling met personen of de hele verzameling met boeken.
entiteiten: de elementen van de verzameling. Elke persoon in de linkse verzameling, bijvoorbeeld William Shakespeare, is een entiteit. Elke persoon en elk boek is een entiteit.
relatietypes: een soort verband tussen verzamelingen. Op de figuur is "heeft gelezen" een relatietype.
relaties: een concreet verband tussen entiteiten. Op de figuur is bv. "Michiel heeft The Tempest gelezen" een relatie.
tabellen: de structuur waarin een volledig entiteittype of een relatietype wordt opgeslagen. Deze tabel heeft steeds een naam, of tabelnaam.
records: een tabel bestaat uit rijen of records. Eén record bevat alle gegevens van de rij en stelt dus één entiteit voor.
kolommen (of "velden"): iedere kolom omvat één aanduiding van een gegeven binnen een tabel. Iedere kolom heeft een unieke naam.
Ter herinnering, de verzameling met personen stelden we zo voor:
Voornaam
Familienaam
Geboortejaar
Vincent
Nys
1987
Esther
Nys
1990
Michiel
Nys
1984
William
Shakespeare
1564
Ted
Chiang
1967
Kazuo
Ishiguro
1954
Om een beter overzicht van het databaseschema te krijgen, maken we vaak gebruik van een diagram. Dit wordt een entity-relationship diagram (of "ERD") genoemd. Dit lijkt wat op de verzamelingennotatie die we eerder zagen, maar werkt op een ander niveau.
Neem volgend voorbeeld:
Dit zegt volgende zaken:
In het algemeen kunnen we dus stellen: personen hebben een lievelingsboek. Als we verder vastleggen dat personen altijd exact één lievelingsboek hebben, kunnen we dit grafisch uitdrukken als volgt:
Hier staat niets over Vincent, Esther of Michiel. Maar er staat dat er een relatietype is dat personen en boeken verbindt. De exacte betekenis van de getallen, de icoontjes en de stippellijn laten we voor iets later, maar onthoud alvast dat de rechthoeken de entiteiten voorstellen (met hun kolommen) en de ruiten relaties.
Let op: er zijn veel stijlen van notatie voor het entity-relationship diagram. Wij hebben dit met MySQL Workbench getekend door eerst volgende knop te gebruiken:
Daarna hebben we de plusknop gebruikt en op "Add diagram" geklikt.
Ten slotte hebben we via de menuknop "Model" gekozen voor "Object Notation" → "Workbench (Simplified)" en voor "Relationship Notation" → "Classic". Dit is ook de afspraak die we zullen hanteren voor de rest van de cursus.
Als je een ERD tegenkomt op het internet, ga dan eerst na welke notatie gebruikt is. Anders kan je niet weten wat het ERD precies voorstelt.
MySQL is opgedeeld in een aantal deeltalen. Elke deeltaal dient voor een bepaald soort gebruik. Het is nuttig (sommige van) deze deeltalen meteen te kunnen herkennen, want ze maken het makkelijker de syntax te onthouden.
De volledige lijst deeltalen vind je terug in de (uitstekende) MySQL documentatie. Hij ziet er zo uit:
Je hoeft deze niet uit het hoofd te leren, maar het is wel nuttig om vanaf het begin een onderscheid te maken tussen data definition statements en data manipulation statements.
Data definition statements (ook de "Data Definition Language" of "DDL") dienen om vast te leggen hoe je gegevens er zullen uitzien. Met andere woorden, de structuur van de data. Ze bieden een antwoord op vragen zoals:
Welke tabellen zijn er?
Welk type gegevens mag ik in een bepaalde kolom bijhouden?
Is het toegestaan bepaalde cellen leeg te laten?
Data manipulation statements (of de "Data Manipulation Language" of "DML") dient om specifieke data te beheren en niet de structuur van de data. Voorbeelden hiervan zijn:
Maak een nieuwe rij aan in de tabel Personen.
Pas Said aan naar Saïd.
Verwijder Sanae uit de tabel Personen.
Als je jezelf steeds de vraag stelt of iets thuishoort in de DML of in de DDL, zal je veel makkelijker de juiste syntax terugvinden.
MySQL-instructies zijn vrij leesbaar in vergelijking met veel programmeertalen. Hieronder volgen enkele voorbeelden. Je hoeft deze nog niet zelf te kunnen gebruiken. Ze staan er alleen om je een idee te geven van hoe MySQL syntax er uitziet.
Volgend stukje code voegt een nieuwe persoon, Zadie Smith, toe aan de tabel met personen: INSERT INTO Personen(Voornaam,Familienaam,Geboortejaar) VALUES ('Zadie','Smith',1975);
Hier betekent INSERT dat er iets nieuws wordt toegevoegd.
Volgend stukje code voegt een nieuwe tabel, Liedjes, toe aan de database. Elk liedje heeft een titel en een duurtijd. De titel is tekst met maximum 100 symbolen en de duurtijd is een geheel getal: CREATE TABLE Liedjes(Titel VARCHAR(100), Duurtijd INT);
Instructies zoals deze zal je intypen in je MySQL client (typisch MySQL Workbench).
Een database (of gegevensbank) is een gestructureerde verzameling elektronische gegevens (of data) die door één of meerdere gebruikers (of users) gelijktijdig kunnen gemanipuleerd worden. De enorme hoeveelheden gegevens van banken worden beheerd in ultramoderne databases.
De database zelf, dat zijn dus de geordende gegevens. Maar gegevens zijn pas waardevol als er iets mee gedaan wordt. Het is de taak van een database management system (DBMS) om gegevens op te vragen, aan te passen, enzovoort.
Het DBMS moet er bovendien over waken dat de integriteit van de database behouden blijft. "Waken over integriteit" betekent bijvoorbeeld dat het juiste telefoonnummer bij de juiste persoon blijft horen, dat er geen personen zonder adres in het systeem geplaatst worden,...
Maar het DBMS doet veel meer dan alleen waken over integriteit. Zonder DBMS is het onmogelijk gegevens in een database onder andere te bekijken, te wijzigen, te verwijderen... Alleen het DBMS weet waar en hoe de gegevens in de database verwerkt moeten worden.
Een DBMS zal uiteraard nooit uit zichzelf de gegevens in een database opzoeken, wijzigen of verwijderen. Om dit te doen is steeds een specifieke opdracht van de gebruiker nodig. Bijvoorbeeld: "Verander het telefoonnummer van Jan Claessens van (02) 568 95 65 in (03) 574 23 84".
Dit soort instructie wordt echter niet in een natuurlijke taal gegeven. De meeste DBMS beschikken over een eigen taal voor instructies. Die taal heeft een specifieke "syntax", een stel regels om instructies te ontleden in hun bestanddelen. Deze regels gelijken een beetje op de regels die wij hanteren wanneer we Nederlands spreken. "Ga op die bank zitten" is een verstaanbare opdracht. We kunnen zeggen wat de taak is van elk woordje in deze Nederlandse zin. "Bank daar zitten ga op" is geen geldige zin. We kunnen als mensen nog wel raden naar de betekenis van deze zin, maar hij volgt de regels van de taal niet. Een machine zou het dan ook veel moeilijker hebben om er iets van te maken.
De regels van de talen waarmee men opdrachten kan geven aan een DBMS zijn dus veel strikter dan de regels voor het Nederlands: ze laten geen ruimte voor fouten. Een komma verkeerd volstaat om de opdracht onverstaanbaar en dus onuitvoerbaar te maken.
DROP DATABASE
Om een database te verwijderen, gebruik je een statement van de vorm DROP DATABASE
, gevolgd door de naam van de databank. Als je niet zeker bent dat deze database bestaat en je een foutmelding wil vermijden, gebruik dan drop database if exists
.
DROP TABLE
Hier is de syntax gelijkaardig, maar je moet de database kiezen waaruit je een tabel laat vallen.
Bijvoorbeeld:
De belangrijkste commando's van de DDL zijn:
CREATE
: Hiermee maak je nieuwe structuren aan.
ALTER
: Hiermee verander je bestaande structuren.
DROP
: Hiermee wis je bestaande structuren.
Hier zien we iets meer in verband met het aanmaken van structuren voor je data. We starten onze database met behulp van onderstaand calibratiescript, dat je 0013__Calibratie.sql mag noemen:
ALTER TABLE
ALTER TABLE
verandert de structuur van een tabel, zonder bestaande data te beschadigen. Je kan het gebruiken om kolommen toe te voegen of te verwijderen. Je kan het ook gebruiken om het soort data in een kolom aan te passen, specifieker of juist breder te maken. Voor deze taken heb je binnenin een ALTER TABLE
statement extra clausules nodig.
Sla het script om de voornaam te verwijderen op wanneer je klaar bent. Geef het de naam 0014__AlterBoeken.sql.
Om een kolom toe te voegen maak je gebruik van de ADD
clausule bij het ALTER statement. Je zou hier misschien het woordje CREATE
verwachten. Zoals je ondertussen weet, gebruik je in sql namelijk het keyword CREATE
om een structuur aan te maken. Maar binnenin ALTER
is het dus ADD
! Je moet m.a.w. aangeven dat je iets, in dit geval een kolom, wil toevoegen aan de tabel.
Je merkt in bovenstaand script dat er opgave wordt gegeven van een CHAR SET
. Standaard is utf8mb4
van toepassing voor MySql, maar we vermelden ze hier uitdrukkelijk, omdat er soms ook andere karaktersets worden gebruikt.
Pas dit script aan om naast de kolom Commentaar
ook de kolom Voornaam
terug toe te voegen en een kolom Familienaam toe te voegen, beide VARCHAR(100)
en niet verplicht. Sla je script opnieuw op wanneer je klaar bent. Geef het de naam 0015__AlterBoeken.sql.
Het is goed om van meet af aan de integriteit van de database te denken. We gaan ervan uit dat de familienaam moet ingevuld worden. Zelfs al is de auteur onbekend, moet dan zoiets als "onbekend" worden ingevuld. Om een kolom verplicht te maken voegen we een constraint toe. In volgende code zijn OldColumnName
, NewColumnName
en NewColumnType
placeholders voor de namen van twee kolommen en een datatype met de nodige constraints:
Via de NOT NULL
constraint kunnen we zorgen dat een kolom een waarde moet bevatten, maar die regel mogen we enkel opleggen als er momenteel geen rijen zonder waarde in die kolom zijn.
Eerst moeten we ervoor zorgen dat de nieuw toegevoegde kolom voor iedere rij een waarde krijgt, tot nu is deze waarde NULL
.
Vervolgens gaan we de kolom Familienaam qua structuur wijzigen en een beperking opleggen.
Sla een script met bovenstaande UPDATE
en ALTER
achter elkaar opnieuw op wanneer je klaar bent. Geef het de naam 0016__AlterBoeken.sql.
Het zou misschien te gemakkelijk geweest zijn als het wijzigen van tabelnamen in MySQL met het ALTER statement kon worden uitgevoerd. Om de naam van een tabel te wijzigen kunnen we het ALTER
statement niet gebruiken. Er bestaat daarvoor een apart RENAME
statement. De generieke vorm is als volgt. Let op het gebruik van backticks rond de naam van de tabellen. Die zijn verplicht in het geval dat de naam van de tabel overeenkomt met een gereserveerd woord van MySQL:
In ons voorbeeld:
Voorbeeld:
De tabellen die je tot hiertoe gemaakt hebt in MySQL, groeperen heel veel informatie in één record. Informatie groeperen is een belangrijk doel van databanken, maar het gebeurt beter pas wanneer de groepering zelf nodig is. Anders krijg je databanken die meer plaats innemen dan nodig, die makkelijker fouten zullen bevatten en die moeilijker te navigeren zijn. Het belangrijkste principe om de data pas te groeperen wanneer dat nodig is, is dat van een sleutel. Hier bekijken we het binnen MySQL.
Met een sleutel kan je elke rij in een tabel aanduiden zonder te verwijzen naar de eigenlijke informatie in die rij. We wijzen eerst op enkele problemen met de aanpak die we tot hier gehanteerd hebben en leggen dan uit hoe sleutels deze problemen kunnen oplossen.
Soms hebben twee rijen dezelfde waarden in bepaalde velden, maar gaat het toch over verschillende data. Dit is een eerste probleem dat we kunnen oplossen met attributen die we aanduiden als sleutels.
Een quizvraag voor de gamers: wat hebben de games God of War, Doom en Tomb Raider met elkaar gemeen? Antwoord: het zijn allemaal remakes van oude games met dezelfde titel en dezelfde ontwikkelaar.
Als je een tabel van (oude en nieuwe) games aanmaakt, met als kolommen hun titel en ontwikkelaar, zou een stukje van de tabel er dus als volgt kunnen uitzien:
Titel
Ontwikkelaar
Doom
ID Software
Doom
ID Software
Tomb Raider
Crystal Dynamics
Tomb Raider
Crystal Dynamics
God of War
SCE Santa Monica
God of War
SCE Santa Monica
Voor SQL is er geen verschil tussen de oude en de nieuwe versies van deze games, maar het gaat in werkelijkheid wel om verschillende zaken. Je zou het jaar van uitgave als kolom kunnen toevoegen om elke rij uit elkaar te houden. Of de spelconsole waarop het spel uitkwam. Of misschien is de uitgever steeds veranderd. Dan kan je elke rij uniek maken door die toe te voegen. Maar niets verhindert een ontwikkelaar om twee gelijknamige games meteen na elkaar uit te geven bij eenzelfde uitgever.
Het idee om elke rij uniek te maken is goed, maar je volgt best een voorzichtige aanpak. Dit kan door kolommen toe te voegen die een rij uniek kunnen identificeren. Een stel kolommen waarmee je een rij uniek kan identificeren, kan dan aangeduid worden als primaire sleutel of primary key. Deze verzameling kolommen (of ene kolom) hoeft niet noodzakelijk "leesbare" informatie te bevatten. Vaak is het gewoon een getal, zoals in dit voorbeeld, waarin de kolom Id
een primaire sleutel is:
Titel
Ontwikkelaar
Id
Doom
ID Software
1
Doom
ID Software
2
Tomb Raider
Crystal Dynamics
3
Tomb Raider
Crystal Dynamics
4
God of War
SCE Santa Monica
5
God of War
SCE Santa Monica
6
Je kan in SQL uitdrukken dat een bepaalde kolom wordt gebruikt als primaire sleutel, zodat je nooit per ongeluk twee rijen kan aanmaken met eenzelfde waarde in deze kolom.
Tabellen zoals we ze eerder soms hebben gezien, zijn ook niet bruikbaar voor (middel)grote systemen omwille van een efficiëntieprobleem. Sleutels zullen je ook toestaan op grotere schaal te werken.
Veronderstel dat je, in opdracht van Game Mania, een databank met videogames moet opstellen. Ze hebben momenteel volgende producten in hun inventaris en willen deze in hun nieuwe databank opslaan:
Titel
Platform
Anthem
PS4
Anthem
XBox One
Anthem
Windows
Sekiro: Shadows Die Twice
PS4
Sekiro: Shadows Die Twice
XBox One
Sekiro: Shadows Die Twice
Windows
Devil May Cry 5
PS4
Devil May Cry 5
XBox One
Mega Man 11
PS4
Mega Man 11
XBox One
Mega Man 11
Nintendo Switch
Mega Man 11
Windows
Mass Effect: Andromeda
PS4
Mass Effect: Andromeda
XBox One
Mass Effect: Andromeda
Windows
Dark Souls 3
PS4
Dark Souls 3
XBox One
Dark Souls 3
Windows
Hier is elke rij wel verschillend, maar toch is er een probleem. Denk eraan dat we in onze definities zo precies mogelijk uitdrukken of iets CHAR, VARCHAR,... is, hoe veel karakters er in passen,... Dat is omdat een databank zuinig moet zijn voor goede performantie. Bovenstaande tabel is dat niet: er zijn heel veel stukken lange tekst die regelmatig terugkomen en die nemen elke keer heel wat bytes in.
Zuinig zijn is bovendien niet alleen belangrijk voor performantie, maar helpt ook fouten te voorkomen. Hoe vaker we een waarde volledig moeten uitschrijven, hoe groter de kans dat we eens een fout maken.
Het zou al zuiniger zijn elke game en elk platform aan te duiden met een uniek identificatienummer. Dat bespaart heel veel ruimte tegenover wanneer we telkens de volledige tekst uit te schrijven. We kunnen bijvoorbeeld het volgende afspreken voor de titels:
Anthem: 1
Sekiro: Shadows Die Twice: 2
Devil May Cry 5: 3
Mega Man 11: 4
Mass Effect: Andromeda: 5
Dark Souls 3: 6
Voor de platformen:
PS4: 1
XBox One: 2
Windows: 3
Nintendo Switch: 4
Dan krijgen we voor de hele tabel:
Titel
Platform
1
1
1
2
1
3
2
1
2
2
2
3
3
1
3
2
4
1
4
2
4
4
4
3
5
1
5
2
5
3
6
1
6
2
6
3
Merk op dat we onze mappings van games / platformen op getallen ook in twee tabelvoorstellingen met telkens 2 kolommen (de game/het platform en het volgnummer) kunnen gieten. Dat is dan ook wat we zullen doen. We zullen de volgnummers aanduiden als primary keys die automatisch ophogen. Eens we dat gedaan hebben, kunnen we bijna op een heel efficiënte wijze data gaan combineren (met behulp van JOIN
-operaties). We zullen tabellen gelijkaardig aan die hierboven gebruiken om te verwijzen naar primaire sleutels in andere tabellen. Zo'n verwijzing zal een vreemde sleutel (foreign key) heten.
Onder Unix (macOS en Linux) zijn databasenamen hoofdlettergevoelig (in tegenstelling tot SQL trefwoorden). Dit geldt overigens ook voor de tabelnamen. Onder Windows is deze beperking standaard niet van toepassing, maar het is een goede gewoonte te doen alsof het wel zo is. Volg daarom de gemaakte afspraken wat betreft naamgeving heel nauwkeurig. Als de code op jouw Windowsmachine werkt maar niet op onze server omdat de code de afspraken niet volgt, bevat ze een fout!
Je zou het volgende SQL statement gebruiken om een database te maken:
Het creëren van een database volstaat niet om die vervolgens te kunnen gebruiken. Je moet in een script expliciet opgeven dat je een bepaalde database wilt gebruiken met de instructie USE
:
Een database moet slechts één keer gemaakt worden, maar je moet vooraleer die te gebruiken ze telkens weer selecteren. Dat doe je met de USE
instructie zoals in het voorgaande voorbeeld.
Het niveau onder dat van de databank is het niveau van de tabel. Een tabel bevat typisch informatie over één entiteit, d.w.z. één soort interessante data. Dat is bijvoorbeeld bijvoorbeeld een tabel Boeken
in een bibliotheeksysteem. We vertrekken van een voorstelling voor boeken en personen.
We willen volgende gegevens in het systeem bijhouden:
Gebruik eerst USE
om je database te activeren. Eerst leggen we vast we welke tabellen en welke datatypes we nodig hebben:
Negeer de "1 more" onder "Geboortejaar". Negeer ook het gele sleuteltje. Die zaken komen later. Om de tabel "Personen" aan te maken, schrijven we:
Via het CREATE
-commando maak je een nieuwe structuur aan met een bepaalde naam. Als die naam al bestaat, levert dat een foutmelding. Daarom moeten we voorzichtig omspringen met het CREATE
commando. We doen dit door onze CREATE
enkel uit te voeren als de naam die we willen gebruiken (voor een database of een tabel of een andere structuur) nog niet gebruikt wordt. Hiervoor vervangen we bijvoorbeeld CREATE TABLE MyTable (MyColumn VARCHAR(100));
door CREATE TABLE IF NOT EXISTS MyTable (MyColumn VARCHAR(100));
. Dit vermijdt dat we op een foutmelding botsen. Het kan wel een waarschuwing opleveren, maar dat is op zich niet erg.
We vertrekken hier van volgend script, 0048__CalibrateDB.sql:
We voegen een Id
kolom toe aan de tabel Boeken
die we als primaire sleutel gaan gebruiken.
Om een primaire sleutel toe te voegen aan een reeds bestaande tabel, gebruik je de DDL ALTER TABLE
instructie in combinatie met een DDL ADD
instructie:
Sla die instructie op in 0049__AlterBoeken.sql.
Je kan ook nagaan of de primaire sleutel is toegevoegd door het volgende statement uit te voeren:
Het feit dat een kolom een primaire sleutel is, is een constraint. Men spreekt van een constraint als iets een beperking is. Als je een rij zou toevoegen met een Id
waarvan de waarde reeds in een andere rij bestaat, krijg je een foutmelding. Een ander (en reeds gekend) voorbeeld van een constraint is NOT NULL
. Deze constraint is zwakker dan de PRIMARY KEY
constraint (omdat NULL niet geschikt is om een rij te identificeren), dus je hoeft ze nooit toe te voegen aan een kolom die dient als primaire sleutel.
AUTO_INCREMENT
Het is beter om de Id
door SQL zelf te laten toekennen. Zo hoef je niet telkens na te kijken welke waarde beschikbaar is voor Id
. Om dat te doen, gebruik je de eigenschap AUTO_INCREMENT
. Als je een nieuwe tabel maakt voeg je de eigenschap toe na de declaratie van de kolom. Zorg ervoor dat je op die kolom een primary key constraint hebt staan:
Sla op als 0050__CreatePersonen.sql.
Je kan de beginwaarde zelf bepalen. Bijvoorbeeld, als je de boeken wil nummeren vanaf 5 in plaats van 1 (de default):
Dit kan van pas komen als je al wat data hebt en SQL alleen voor de nieuwe data zelf de nummers wil laten genereren.
Een constraint behoort tot de definitie van de tabel, dus moet je DROP gebruiken:
MySQL ondersteunt drie brede soorten datatypes, met per soort verschillende concretere vormen. Deze drie soorten zijn:
string types (ofwel "tekst")
numerieke types (ofwel "getallen")
temporele types (datums en/of tijdstippen)
In deze cursus beperken we ons tot vier brede types: varchar
, int
, float
en datetime
. Dit is een vereenvoudigde weergave, bedoeld om de concepten aan te leren. Er zijn nog andere basistypes en je kan ook bepaalde extra beperkingen opleggen. Je kan alle details terugvinden in de (uitstekende) officiële MySQL documentatie.
VARCHAR
VARCHAR
stelt een stuk tekst met een bepaalde maximumlengte voor. Zo is een kolom met type VARCHAR(50)
beperkt tot maximum 50 karakters.
Als je data van dit type hebt, zet je het tussen enkele of dubbele aanhalingstekens. Bijvoorbeeld 'Do,Re,Mi,Fa,Sol,La,Si'
. Als je data zelf aanhalingstekens bevat, gebruik dan het andere type voor de gehele string.
Let op! Alles wat tussen aanhalingstekens staat is tekst. Zelfs cijfers. Met andere woorden: '42' is tekst en 42 is een getal. Met tekst kan je niet rekenen, met een getal wel. MySQL probeert wel te raden wat je bedoelt als je bijvoorbeeld '42' + '1' schrijft, maar niet elke taal doet dit. Daarom spreken we af dat je MySQL niet laat raden, maar altijd het juiste datatype gebruikt.
INT
en variantenINT
dient voor de opslag van gehele getallen. Standaard kan een INT
positieve en negatieve gehele getallen voorstellen. Je kan met INT
geen kleiner getal opslaan dan -2147483648
en geen groter getal dan 2147483647.
FLOAT
Dit type dient om kommagetallen bij benadering op te slaan. Dit wil zeggen dat (vaak héél kleine) afrondingen toegestaan zijn wanneer je data in het systeem plaatst.
Deze data zet je niet tussen quotes. Je gebruikt een punt in plaats van een komma.
Temporele types worden gebruikt om tijdstippen voor te stellen. Hierbij wordt volgende notatie gebruikt om een formaat voor te stellen:
Y: cijfer van een jaar
M: cijfer van een maand
D: cijfer van een dag
H: cijfer van een uur
m: cijfer van een minuut
S: cijfer van een seconde
DATETIME
Een DATETIME
is een waarde die een specifiek ogenblik in de tijd voorstelt. Met andere woorden ongeveer een combinatie van een dag en een moment van de dag. Het formaat is YYYY-MM-DD HH:MM:SS
en het bereik ligt tussen het jaar 1000 en 9999.
Je schrijft datums alsof het strings waren in een afgesproken formaat, dus wel tussen quotes.
Je kan er bij het aanmaken of wijzigen van een kolom voor zorgen dat je een kolom niet uitdrukkelijk hoeft in te vullen. Dat doe je door, na de naam van de kolom, het woordje default
noteren, gevolgd door een waarde van dat type. Bijvoorbeeld:
Dit voegt een nieuwe kolom toe voor het ISBN-nummer aan een bestaande tabel boeken. Dit is een tekstkolom van maximum 25 symbolen. Als de gebruiker géén ISBN invult, krijgt een boek automatisch de waarde ABC123
in die kolom.
Vaak kan je in een bepaalde kolom maar een beperkt aantal mogelijke waarden invullen. Een datatype zoals INT
of VARCHAR
is dan niet op zijn plaats, omdat die heel veel mogelijke waarden toelaten. Dat zorgt voor inefficiëntie en een grotere kans op foute invoer.
Voor dergelijke kolommen is het beter enumeraties te gebruiken, of ENUM
s, om het met de woordenschat van MySQL te zeggen. Een ENUM
is een stuk tekst met een waarde uit een op voorhand aangegeven stel mogelijkheden.
Veronderstel dat je een database met stukken kledij bijhoudt, bijvoorbeeld deze:
Als de winkel alleen polo's, broeken en truien verkoopt en als er maar drie formaten bestaan, stel je de tweede en derde kolom best voor met een enumeratie.
Dat gaat als volgt:
Noem die code 0026__CreateKledingstukken.sql.
Vervolgens kan je data aanmaken alsof de tweede en derde kolom tekst bevatten, met 0027__InsertKledingstukken.sql:
Wat niet gaat, is dit (hoef je niet op te slaan):
Dat komt omdat we alle mogelijke waarden al hebben vastgelegd.
Enumeraties hebben verschillende voordelen:
Ze zijn zuiniger in gebruik van opslagruimte dan strings die dezelfde tekst voorstellen.
Ze zorgen voor meer leesbare invoer en uitvoer dan getallen.
Let wel op! Enumeraties lijken op strings, maar ze worden anders gesorteerd. De volgorde waarin waarden van een enum gesorteerd worden, is de volgorde waarin de elementen gedeclareerd zijn.
Dit kan je afleiden uit volgend script (0028__SelectKledingstukken.sql):
Wat zou dit geven als formaat een VARCHAR
was?
artikelnummer
soort kledij
formaat
1
polo
small
2
polo
medium
3
polo
large
4
broek
small
5
broek
medium
6
broek
large
7
trui
small
8
trui
medium
9
trui
large
Het is niet erg handig om telkens de primaire sleutel achteraf toe te voegen. Als je een nieuwe tabel maakt, kan je in één keer een kolom aanduiden als primaire sleutel:
Deze hoef je niet uit te voeren.
De belangrijkste commando's van de DML zijn:
INSERT
: Hiermee voeg je data in
SELECT
: Hiermee vraag je data op
UPDATE
: Hiermee pas je data aan
DELETE
: Hiermee verwijder je data
Wanneer we in één record via een bepaalde kolom verwijzen naar (de sleutel van) een ander record, spreken we over een vreemde sleutel of foreign key. Hij is "vreemd" omdat hij verwijst naar een ander record, mogelijk (maar niet noodzakelijk) in een andere tabel.
Als de primary key van een tabel waar je naar verwijst een INT
is, definieer je een kolom met een foreign key ook als INT
. Na de oplijsting van de kolommen voeg je dan een constraint toe. Hierbij spelen drie zaken mee:
de naam van de constraint
de kolom die moet dienen als foreign key
de kolom die dienst doet als primary key in de tabel waarnaar verwezen wordt
Dat ziet er bijvoorbeeld zo uit, als je een tabel met boeken koppelt aan hun auteur (veronderstel even dat een boek één auteur heeft):
Deze hoef je niet uit te voeren.
Dit wil zeggen: "maak een tabel voor boeken met een uniek identificatienummer (de primary key); boeken kunnen ook verwijzen naar specifieke records in de tabel die personen voorstelt; deze verwijzingen worden voorgesteld met de kolom Personen_Id
in de tabel voor boeken"
Hierbij gebruiken we enkele afspraken:
De naam van de kolom die dienst doet als foreign key is de naam van de tabel waarnaar verwezen wordt, gevolgd door _Id
.
De naam van een foreign key constraint is altijd fk_
, gevolgd door de naam van de tabel waarop de constraint toegepast is, gevolgd door de naam van de tabel waarnaar verwezen wordt.
Prent het heel goed in: een kolom met als naam Id dient om een rij uit de tabel te identificeren waartoe de kolom behoort. Een kolom met als naam (Tabelnaam)_Id (waarbij de tabelnaam vanalles kan zijn) dient om een rij uit een andere tabel te identificeren, die hoort bij een rij uit deze tabel. Je zou nooit mogen twijfelen over hoe je een sleutelkolom moet noemen. Dit is een afspraak die wij steeds zullen volgen.
Als je al een tabel hebt en deze wil uitbreiden met een foreign key, doe je dat via de DDL ALTER
-instructie. Als je bijvoorbeeld al een tabel voor boeken had zoals hierboven, maar zonder de kolom Personen_Id
, zou je dit schrijven:
Sla op als 0051__AlterBoeken.sql en voer uit.
Als je tabellen maakt, zit het niet altijd meteen juist. Vaak probeer je een bepaalde structuur voor je data en verbeter je deze regelmatig. Om je de structuur van je data te bekijken, moet je in MySQL Workbench eerst je databanken refreshen. Daarna kan je je databank openklappen en via rechtermuisklik de structuur van je tabel controleren.
Schrijf een script dat een tabel Nummers
voor muzieknummers toevoegt aan je databank met volgende karakteristieken:
"Variabele lengte" betekent dat de tekst niet altijd de maximale hoeveelheid ruimte in beslag neemt.
Sla op als 0019__Oefening.sql.
Doe hetzelfde voor een tabel Huisdieren
met huisdieren, met volgende kenmerken:
Sla op als 0020__Oefening.sql.
Plaats volgende data in je tabel met muzieknummers:
1. het nummer "John the Revelator" van de groep "Larkin Poe". Het genre is "Blues" en het verschijningsjaar is 2017.
2. het nummer "Missionary Man" van de groep "Ghost". Het genre is "Metal" en het verschijningsjaar is 2016.
Sla op als 0021__Oefening.sql.
Plaats volgende data in je tabel met huisdieren:
Ming, 9 jaar oud, is de hond van Christiane
Bientje, 12 jaar oud, is de kat van Esther
Misty, 7 jaar oud, is de hond van Vincent
Sla op als 0022__Oefening.sql.
Toon met een SELECT
de duo's met huisdieren en baasjes in het formaat H+B
, gerangschikt volgens de leeftijd van het huisdier.
De juiste oplossing toont volgende rijen in MySQL Workbench, in exact deze volgorde:
Sla op als 0023__Oefening.sql.
Klassificatie van boeken is een wetenschap op zich. Er zijn veel systemen voor, maar ze gebruiken allemaal een code van een vrij klein aantal letters om aan te geven in welke cluster een bepaald boek thuishoort.
Schrijf zelf een script dat een verplichte kolom met naam Categorie
toevoegt aan de bestaande tabel met boeken. De categorie van een boek wordt uitgedrukt in maximaal 40 lettertekens. Dit zijn "gewone" lettertekens die je zou aantreffen in een typische Engelstalige tekst. Deze kolom heeft ook een defaultwaarde, namelijk "Zonder categorie".
Noem je script 0017__Oefening.sql.
Het is handig om bij te houden wie een boek in het systeem heeft geplaatst. Daarom willen we een extra kolom met de naam IngevoegdDoor
die maximaal 255 karakters lang is en zeker internationale karakters moet kunnen bevatten. Deze kolom is niet verplicht.
Noem je script 0018__Oefening.sql.
We willen wetenschappelijke observaties van het grondwaterniveau bijhouden. Maak hiervoor een tabel GrondwaterObservaties met twee verplichte kolommen: één kolom voor het tijdstip van de observatie (dag, maand, jaar, uur, minuten én seconden) en één kolom voor het grondwaterniveau uitgedrukt als kommagetal. Het grondwaterniveau hoeft niet met perfecte precisie voorgesteld te worden en je hoeft er niet veel opslagruimte voor te gebruiken. Bepaal zelf het meest geschikte kolomtype.
Noem je script 0024__Oefening.sql
Plaats volgende informatie in je nieuwe tabel:
Noem je script 0025__Oefening.sql
In een winkelsysteem willen we alle aankopen registreren. Maak daarom een tabel Aankopen
met drie stukjes informatie, die allemaal verplicht zijn:
de naam van het artikel, uitgedrukt als verplichte tekst van maximum 100 karakters
het aantal stuks, uitgedrukt als positief getal dat past in één byte
de aankoopprijs per stuk, uitgedrukt als exact kommagetal dat bestaat uit vijf cijfers, waarvan twee na de komma
Plaats, in hetzelfde script, volgende gegevens in deze tabel:
Noem je script 0029__Oefening.sql
Maak een tabel Bestellingen
die kan dienen voor het bestelsysteem van een autodealer. Deze bevat drie (verplichte) zaken:
het tijdstip waarop de bestelling geplaatst is
het model - dit kan alleen een van onderstaande opties zijn (tip: gebruik een enum datatype)
Qivic
Nexus
Yugo
de kleur - dit kan alleen een van onderstaande opties zijn
zwart
geel
rood
Noem je script 0030__Oefening.sql
In heb je gezien dat er verschillende redenen zijn om rijen uniek te identificeren. Dit is alleen zinvol als we de primaire sleutelattributen ook ergens anders vermelden. Met andere woorden, als we in kolom A van tabel B een primaire sleutel plaatsen, is het logisch dat we in kolom C van tabel D over deze sleutel spreken.
We hebben dat ook gedaan in het tweede voorbeeld op (het voorbeeld met games), door een tabel te maken die de dubbele voorkomens van lange stukken tekst vervangt door hun identificatienummers.
Je hebt hier de concat
functie nodig, uitgelegd op .
Kolomnaam
Type
Maximale lengte
Variabele lengte
Internationale tekens
Verplicht
Titel
tekst
100
JA
zeker nodig
JA
Artiest
tekst
100
JA
zeker nodig
JA
Genre
tekst
50
JA
niet zeker nodig
NEE
Jaar
tekst
4
NEE
niet zeker nodig
NEE
Kolomnaam
Type
Maximale lengte
Variabele lengte
Internationale tekens
Verplicht
Naam
tekst
100
JA
zeker nodig
JA
Leeftijd
een geheel positief getal
tot 300 jaar
NVT
NVT
JA
Soort
tekst
50
JA
niet zeker nodig
JA
Baasje
tekst
100
JA
zeker nodig
JA
(wat hier staat is niet belangrijk)
M+V
M+C
B+E
tijdstip
meting
4 februari 2020 om 8u35m9s
80.2
7 februari 2020 om 10u12m13s
76.4
12 februari 2020 om 11u13m52s
83.5
naam
aantal stuks
aankoopprijs per stuk
paar schoenen
2
60.99
waterfles
1
8.99
Aanmaken van je eerste concrete data
Aan alleen structuur heb je niets. We willen ook data bijhouden in ons systeem. Met andere woorden: rijen, niet alleen tabellen.
Er is een vaste syntax om data in je databank te plaatsen, waar je even aan zal moeten wennen. Het is een goed idee om met een stub te beginnen, een skelet statement. Dit zorgt er voor dat je fouten in de syntax snel opmerkt. Voor een tabel Boeken ziet dit er zo uit, voor een enkel nieuw record:
Je hoeft geen INSERT statement te schrijven per stukje data. Je kan meerdere rijen tegelijk invoegen door ze te scheiden met komma's:
Net zoals het DROP statement verwijdert het DELETE statement objecten uit de database. Het DROP statement verwijdert een tabel uit de database, het DELETE statement verwijdert hele rijen uit de tabel maat laat de tabelstructuur staan.
Meestal willen we specifieke records verwijderen en geen volledige tabellen leegmaken. Daarom ondersteunt DELETE
dezelfde WHERE
-clausule als SELECT
. Als je geen WHERE clausule gebruikt, worden alle rijen uit de tabel verwijderd en blijft alleen de structuur van de tabel over. Wees daar dus voorzittig mee want als de rijen gedeletet zijn kan je ze niet meer terughalen. Net als bij UPDATE
verbiedt MySQL standaard bepaalde "onveilige" operaties. Ook hier gebruik je SET SQL_SAFE_UPDATES = 0
en SET SQL_SAFE_UPDATES = 1
voor, respectievelijk na de operatie.
De syntax van DELETE
lijkt erg op die van SELECT
, maar in plaats van bepaalde rijen te tonen, zal MySQL ze gewoon wissen. Je kan ook geen specifieke kolommen wissen, dus je schrijft DELETE FROM Boeken
en niet DELETE * FROM Boeken
of DELETE Voornaam FROM Boeken
.
Afhankelijk van hoe entiteiten aan elkaar gekoppeld kunnen zijn, delen we de koppelingen tussen deze entiteiten op in categorieën:
een-op-een-relaties, d.w.z. één entiteit van een entiteittype hoort bij exact één entiteit van een gekoppeld type
een-op-veel-relaties, d.w.z. één entiteit van een entiteittype hoort niet bij 0, 1 of meerdere entiteiten van een gekoppeld type
veel-op-veel-relaties (ook wel M-op-N relaties genoemd)
Er is nog een indeling in relaties: identificerende tegenover niet-identificerende relaties. Dit onderscheid heeft niet veel impact op het basisgebruik van een databank. Met de werkwijze die wij volgen, kan je steeds niet-identificerende relaties gebruiken, tenzij het om veel-op-veel relaties gaat.
De simpelste verbanden zijn één-op-één verbanden. Dat wil zeggen: precies twee rijen nemen deel aan de relatie. Normaal zijn dit rijen van verschillende entiteittypes, al is het niet verplicht.
Een voorbeeld: een sportclub organiseert een jaarlijks etentje en alle leden krijgen precies één taak. Eén lid zorgt bijvoorbeeld voor bestek, een ander voor frisdrank, een ander voor onderleggers, enzovoort. De club gebruikt een database om de taken en de leden bij te houden en de taken zijn elk jaar dezelfde. Bijvoorbeeld:
taken:
bestek voorzien
frisdrank meebrengen
aardappelsla maken
leden:
Yannick
Bavo
Max
Onderstaande figuur stelt deze indeling voor in een ERD getekend in de editor van MySQL Workbench:
In dit geval is het logisch om een aparte tabel (Taken
) voor taken en een aparte tabel (Leden
) voor leden te gebruiken. Het is onhandig om uit te leggen dat één rij in de databank een lid en een taak voorstelt, omdat de tabellen best zo goed mogelijk overeenstemmen met duidelijke concepten. Het is logischer de leden en de taken als aparte entiteiten te beschouwen en een relatie tussen beide vast te leggen.
Er zijn wel systemen die je dwingen om dit soort relatie voor te stellen door Taken en Personen samen te smelten tot één tabel.
Om een relaties tussen rijen van de tabellen vast te leggen, maken we de rijen eerst identificeerbaar met een primaire sleutel. Bijvoorbeeld, voor de taken:
Omschrijving
Id
bestek voorzien
1
frisdrank meebrengen
2
aardappelsla maken
3
Voor de leden:
Naam
Id
Yannick
1
Bavo
2
Max
3
Zet de structuur van de twee tabellen om in SQL-tabellen met een script 0052__CreateTakenLeden.sql. Elke normale kolom bestaat uit een reeks van maximaal 45 karakters en is verplicht. De Id
-kolom stel je voor met een INT
die automatisch ophoogt. Elke kolom heeft dezelfde naam die gebruikt wordt in de hoofdingen hierboven.
Vul de twee tabellen, Taken
en Leden
in met een script 0053__InsertTakenLeden.sql.
Als Bavo bestek voorziet, Yannick frisdrank meebrengt en Max aardappelsla maakt, kunnen we dat als volgt bijhouden in een aparte tabel die alleen vreemde sleutels bevat:
Leden_Id
Taken_Id
2
1
1
2
3
3
In de praktijk wordt er normaal niet voor gekozen om deze relatie in een aparte tabel vast te leggen. Dat zou gaan, maar het is gewoon een beetje te veel van het goede. Je hebt geen aparte tabel nodig. Eén van de twee tabellen wordt uitgebreid met een foreign key. Er zijn twee mogelijkheden:
Omschrijving
Id
Leden_Id
bestek voorzien
1
2
frisdrank meebrengen
2
1
aardappelsla maken
3
3
of
Naam
Id
Taken_Id
Yannick
1
2
Bavo
2
1
Max
3
3
Beide zijn even goed. Typisch wordt (in geval van een niet-identificerende relatie en die gebruiken wij altijd) gekozen om de foreign key in de tabel te zetten met het kleinste aantal kolommen, om alles een beetje in evenwicht te houden. Hier hebben beide even veel kolommen dus het maakt helemaal niet uit.
Pas je tabel Leden aan zodat ze de tweede mogelijkheid van hierboven implementeert in een script 0054__AlterLeden.sql. Je moet eerst de kolom toevoegen, dan invullen, dan verplicht maken.
Dit ziet er zo uit:
Je zou de data nu kunnen combineren, maar daar heb je een JOIN-operatie voor nodig. Die komt later.
Een een-op-veel (of 1-op-N) verband is een verband dat je heel vaak tegenkomt op websites met een achterliggende databank. Bij dit soort verband stemt een rij uit een bepaalde tabel A overeen met meerdere rijen uit een tabel B. In de omgekeerde richting stemt een rij uit tabel B maar met één rij van tabel A overeen. Bijvoorbeeld, als je tweets bijhoudt in een databank, kan één persoon meerdere tweets hebben, maar één tweet kan oorspronkelijk slechts van één persoon komen. Hier moet je je inbeelden dat personen bijgehouden worden in tabel A en tweets in tabel B.
In een database van een webshop kan één persoon meerdere bestellingen plaatsen, maar één bestelling kan slechts van één klant komen. Hier geldt: personen in A, bestellingen in B.
Hier zijn enkele tweets die we als voorbeeld zullen gebruiken, voorafgegaan door de handle van de gebruiker die ze geschreven heeft:
Zoals in het geval van de 1-op-1 relatie, kunnen we deze relatie tussen gebruikers en tweets voorstellen in een tabel:
user
tweet
1
1
1
2
1
3
2
4
2
5
2
6
Dit is opnieuw iets meer dan we nodig hebben. We kunnen een foreign key van één tabel toevoegen aan een andere. Maar, in tegenstelling tot de precieze 1-op-1-relatie, mogen we niet kiezen. We zetten de foreign key in de tabel die niet aan de "exact-1"-kant van de relatie zit. Zorg er ook voor dat de vreemde sleutel nooit NULL is met een constraint.
Voer dit zelfstandig uit voor de reeks tweets hierboven. Volg de reeds afgesproken afspraken: één tabel Users
voor users (met een kolom Handle
), één tabel Tweets
voor tweets (met een kolom Bericht), beide voorzien van primaire sleutels, met de vreemde sleutel aan de "N-kant". Stel gebruikersnamen en tweets voor met kolommen van variabele lengte (tot 144 tekens), zonder internationale tekens. De @ maakt geen deel uit van een gebruikersnaam. Zet de SQL-code die je nodig hebt om de (lege) tabellen te maken in een script 0055__CreateUsersTweets.sql. Zet de code die je nodig hebt om de vreemde sleutel toe te voegen in 0056__AlterTweets.sql. Zet ten slotte de code om de tabellen in te vullen in een script 0057__InsertUsersTweets.sql. Begin met een INSERT
voor de users, doe dan pas die voor de tweets.
Voor het laatste script bespaart onderstaande gedeeltelijke SQL je het copy-pasten van de tweets.
Een een-op-max-een relatie is een relatie waarbij één entiteit A gelinkt is aan hooguit één andere entiteit B. Het kan ook zijn dat A aan geen enkele B gelinkt is. Deze stel je voor zoals een 1-op-N relatie, dus met de vreemde sleutel in de tabel aan de niet-exact-1-kant.
Een auteur kan meerdere boeken hebben en een boek kan verschillende auteurs hebben. Een game kan op verschillende platformen uitgebracht zijn en voor elk platform zijn er verschillende games beschikbaar. Een student volgt verschillende vakken en in elk vak zitten verschillende studenten. Dit zijn allemaal voorbeelden waar één rij uit een tabel A gekoppeld kan zijn aan meerdere rijen uit een tabel B en één rij uit dezelfde tabel B gekoppeld kan zijn aan meerdere rijen uit dezelfde tabel A. We zeggen dan ook dat er een veel-op-veel of M-op-N-relatie bestaat tussen de entiteiten A en B.
Bij 1-op-1-relaties mochten we de vreemde sleutel in tabel A of B zetten (en sommige systemen vereisen zelfs dat je de tabellen gewoon samensmelt). Bij 1-op-max-1 of 1-op-N relaties zetten we de vreemde sleutel in de tabel die niet precies één keer gekoppeld was. Dit werd vooral gedaan om geen overbodige tabellen toe te voegen. We konden in principe de takenverdeling voor het etentje ook als volgt voorstellen, met een aparte tabel:
Leden_Id
Taken_Id
2
1
1
2
3
3
We deden dit alleen anders omdat we het met een tabel minder (en dus in totaal ook een kolom minder) konden. Voor een M-op-N-relatie is deze voorstelling echter onze beste optie.
Een game kan beschikbaar zijn op meerdere platformen en op elk platform zijn er natuurlijk meerdere games beschikbaar. Bijvoorbeeld:
Anthem: beschikbaar op PS4, XBox One, Windows
Sekiro: beschikbaar op PS4, XBox One, Windows
Devil May Cry 5: beschikbaar op PS4, XBox One
Mega Man 11: beschikbaar op PS4, XBox One, Windows, Nintendo Switch
Veronderstel dat Anthem ID 1 heeft, Sekiro 2, enzovoort. Veronderstel ook dat PS4 ID 1 heeft, Xbox One ID 2, Windows ID 3 en Nintendo Switch ID 4. Dan kunnen we voorstellen welke games uitgebracht zijn op welke platformen als volgt:
Games_Id
Platformen_Id
1
1
1
2
1
3
2
1
2
2
2
3
3
1
3
2
4
1
4
2
4
3
4
4
Je hebt hier drie tabellen nodig: een voor games, een voor platformen, een voor de koppeling. De tabel Games
heeft naast de Id
één kolom: Titel
, een stuk tekst van maximaal 50 karakters dat nooit leeg mag zijn en mogelijk Unicode karakters bevat. Voor Platformen
is er een gelijkaardige structuur, maar de naam van de kolom die niet als sleutel wordt gebruikt is Naam
. Noem de tabel die de koppeling afhandelt Releases
. Volg de conventie voor de naam van de kolommen die naar beide andere tabellen verwijzen. Sla de DDL-instructies op als 0059__CreateGamesPlatformenReleases.sql. Voeg de DML-instructies toe als 0060__InsertGamesPlatformenReleases.sql.
Voor 0059 zou je dit moeten hebben:
Dit stemt overeen met een diagram in Workbench dat er zo uitziet:
Voor 0060:
Attributen horen meestal bij entiteiten, maar kunnen ook bij relaties horen. Bovenstaande tabel Releases
geeft bijvoorbeeld aan welk spel op welk platform verschenen is, maar wat als we de releasedatum willen bijhouden? Deze hoort niet in de tabel Games
. Hij hoort ook niet in de tabel Platformen. Hij hoort bij de combinatie van een game en een platform, d.w.z. bij de relatie die wordt voorgesteld met de tabel Releases. Daarom kunnen we de tabel ook uitbreiden met een kolom Releasedatum
. Voor het leesgemak stellen we de games en de platformen niet voor via hun Id
-attribuut.
Titel
Naam
Releasedatum
Anthem
PS4
22 februari 2019
Anthem
XBox One
22 februari 2019
Anthem
Windows
22 februari 2019
Sekiro: Shadows Die Twice
PS4
22 maart 2019
Sekiro: Shadows Die Twice
XBox One
22 maart 2019
Sekiro: Shadows Die Twice
Windows
22 maart 2019
Devil May Cry 5
PS4
8 maart 2019
Devil May Cry 5
XBox One
8 maart 2019
Mega Man 11
PS4
2 oktober 2018
Mega Man 11
XBox One
2 oktober 2018
Mega Man 11
Windows
2 oktober 2018
Mega Man 11
Nintendo Switch
2 oktober 2018
In een ERD stellen we dit als volgt voor:
In dit geval is Releases
niet gewoon een tabel die een relatie voorstelt, maar wel een associative entity: een relatie tussen Games
en Platformen
die eigen kenmerken bezit, zodat je ze eigenlijk ook als een entiteit zou kunnen zien.
Voeg zelf de nodige info toe. Hiervoor volg je volgende stappen:
Voeg een kolom van type DATE
toe aan de tabel Releases
. Deze kan nog niet verplicht zijn. Noem het script 0062__AlterReleases.sql.
Kopieer het script dat games en hun releaseplatform weergeeft naar een nieuw script, 0063__UpdateReleases.sql.
Pas voor de gecombineerde tabel de datum aan volgens de gegevens hierboven. Je kan in deze tabel een SET
uitvoeren op Releasedatum
.
Gebruik WHERE Games.Titel = ... AND Platformen.Naam = ...
in plaats van eerst de sleutels af te lezen!
Maak de kolom voor de releasedatum verplicht via 0064__AlterReleases.sql, zodat nieuwe games altijd een releasedatum moeten krijgen. Doe dit door de kolom aan te passen naar DATE NOT NULL
.
Hier komen geen nieuwe ideeën aan bod, maar je moet de eerdere stappen goed begrijpen om deze stappen te doorlopen.
Tabellen kunnen meer dan twee entiteiten verbinden. Voor releases van games kan je bijvoorbeeld een spel, een uitgever en een platform aan elkaar linken met een M-op-N-op-K relatie. Dit is wel niet vaak nodig, dus denk altijd even goed na voor je dit doet. De keuze berust vooral op een goede analyse en goed overleg met de klant! In deze cursus zal je nooit een ternaire (d.w.z. tussen drie entiteiten) of hogere relatie nodig hebben. En, zoals eerder gezegd, gaan we niet verder in op het onderscheid tussen identificerende en niet-identificerende relaties.
Om waarden te vergelijken, maakt MySQL gebruik van enkele operatoren die erg lijken op wat je mogelijk kent uit andere programmeertalen:
Symbool
Omschrijving
=
Gelijk aan
<
Kleiner dan
>
Groter dan
<=
Kleiner of gelijk
>=
Groter of gelijk
<>
Verschillend
Voor "gelijk aan" heb je maar één =
nodig, waar veel programmeertalen ==
gebruiken. Dat komt omdat =
in die andere talen gebruikt wordt voor toekenning. In SQL is duidelijk uit de context dat je met =
een vergelijking bedoelt.
Voor getallen betekenen bovenstaande operatoren precies wat je zou verwachten. Je moet in het achterhoofd houden dat FLOAT
en DOUBLE
niet helemaal precies zijn, maar voor de rest betekenen ze wat je verwacht uit de wiskunde.
Voor tekst is het wat complexer. Je kan wel degelijk iets als het volgende schrijven:
Dit SELECT
statement retourneert alle rijen uit de tabel Boeken
waarvan de Familienaam
een waarde heeft die "kleiner" is dan 'B'
. Wanneer is een string "kleiner" dan een andere string? Om op die vraag te antwoorden moet je eerst weten dat de namen in een bepaalde, gewoonlijk alfabetische volgorde (in het Engels collation order) worden gerangschikt. "Alle namen kleiner dan 'B'" wil dan zeggen "alle namen die na het sorteren voor 'B' komen."
De collation is een stel regels voor het vergelijken van tekens in een tekenset. De tekenset bepaalt dat A een symbool is dat je kan weergeven en bepaalt welke bytes nodig zijn om dit symbool voor te stellen. De collation legt vast dat "A" voor "B" komt en of "A" voor, na of op dezelfde plaats als "a" komt.
Als de familienaam Beth in je tabel voorkomt, wordt deze niet in de selectie opgenomen. Om ook die rij te selecteren, zou je kunnen denken om de expressie Familienaam <= 'B'
te gebruiken:
Dit zal niet werken. Dat komt omdat we de boeken opvragen waarvan de familienaam van de auteur kleiner of gelijk is aan 'B'. De tekst 'Beth'
is niet gelijk aan 'B'
en is zeker niet kleiner! In het algemeen mag je dit onthouden: als een string A een prefix is van een string B, d.w.z. als string B letterlijk begint met de inhoud van A en dan nog meer tekst bevat, dan zal A altijd voor B gesorteerd worden. Zo wordt 'schoen' gesorteerd voor 'schoenmaker', omdat 'schoen' een prefix is van 'schoenmaker'. Maar er wordt éérst naar de letters gekeken en dan pas naar de lengte van de tekst. Zo wordt 'schoeisel' voor 'schoen' gesorteerd, omdat 'i' voor 'n' komt.
Ter verduidelijking: als we 'B'
als familienaam toevoegen, wordt het resultaat wel geselecteerd:
Stel dat je alle titels wilt van de auteurs wilt waarvan de familienaam begint met een' A' of een 'B'. Dan moet je de WHERE clausule verfijnen. Je zou dit kunnen proberen:
Maar hier zijn verschillende problemen mee. Als 'Bz'
een prefix is van de naam van een auteur (niet erg waarschijnlijk, maar het zou kunnen), zal dit niet werken. Nog lastiger: dit is erg afhankelijk van de collation. Het is niet in elke collation zo dat 'Bz' voor 'Bé' komt, bijvoorbeeld. Sommige collations sorteren eerst de gebruikelijke 26 letters van het alfabet en pas daarna de letters met accenten.
Volgend script is beter (je mag er in het algemeen wel vanuit gaan dat 'B'
voor 'C'
komt):
Hoe weet je precies wat er met accenten en hoofdletters gebeurt? Dat hangt ervan af of de gebruikte collation hoofdlettergevoelig (case sensitive) en/of accentgevoelig (accent sensitive) is voor de kolom die je aan het beschouwen bent.
Probeer dit eens uit:
Als de collation voor Familienaam
accentongevoelig is, zal je sowieso beide nieuwe records zien. Anders worden de zaken complexer. Dan moet je kijken naar het gebruikte collation algoritme en de accentgevoeligheid. Dan moet je bijvoorbeeld weten of de é
pas ergens na de z
komt of na de e maar voor de f
. Als je de collation van de kolommen van de tabel Boeken te weten wil komen, kan je in MySQL Workbench rechtsklikken op de tabel in kwestie en dan via "Table Inspector" naar "Columns" gaan om de collation te achterhalen. Er zijn véél collations, maar onthoud vooral volgende vuistregels:
als ze _bin
bevat (al dan niet met hoofdletters geschreven), worden de bytevoorstellingen van de karakters vergeleken
dit is zowat de strengste definitie van gelijkheid die je kan hebben
als ze _as
bevat, is de collation accentgevoelig
als ze _ai
bevat, is de collation accentongevoelig
als ze _cs
bevat, is de collation hoofdlettergevoelig
als ze _ci
bevat, is de collation hoofdletterongevoelig
Als je dus bijvoorbeeld enkel een boek van Breton wil, zonder boeken van "Bréton" of "breton", schrijf je:
In veel gevallen weten we maar half wat we willen zoeken. We kennen een deel van de naam, het begin of het einde of iets tussenin. Met de LIKE
operator kan je in SQL naar patronen zoeken (pattern matching). Je kan met de LIKE
operator naar patronen in tekst zoeken. Je combineert de LIKE
operator met jokers (wildcards) om een booleaanse expressie te vormen. De set van jokers bestaat uit symbolen die één of meer ontbrekende tekens voorstellen.
De LIKE operator vergelijkt een tekstuitdrukking in het linkerlid, dat eventueel een wildcard bevat, met een patroon tussen aanhalingstekens in het rechterlid. Een patroon is een veralgemeende tekstuitdrukking. d.w.z. een tekst waarin nog enige vrijheid bestaat.
De betekenis van de wildcards in SQL is als volgt:
%
: nul, één of meer willekeurige tekens
_
: exact één willekeurig teken
Om alle boeken te selecteren waarvan de familienaam van de auteur begint met B als de collation niet hoofdlettergevoelig is:
Om alle boeken te selecteren waarvan de familenaam van de auteur eindigt op een s:
Om boeken waarvoor in de titel het woord economie voorkomt, te selecteren:
De LIKE
wordt door beginners vaak vergeten. Je komt soms dingen tegen als WHERE Voornaam = 'ma%ijs'
. Dat zal geen resultaten opleveren, want niemand heeft letterlijk de voornaam "Ma%ijs".
Soms maken we fouten bij het ingeven van data. Soms verouderen gegevens. In beide situaties willen we bestaande rijen wel bewaren, maar bepaalde kolomwaarden aanpassen. We kunnen het UPDATE
statement hier voor gebruiken.
MySQL staat dit om veiligheidsredenen niet zomaar toe, maar voorlopig zijn we aan het verkennen. Om dit dus toch mogelijk te maken, moet je SET SQL_SAFE_UPDATES = 0
toevoegen voor een "onveilig" commando en achteraf SET SQL_SAFE_UPDATES = 1
toevoegen.
Bijvoorbeeld:
Dit zet de kolom Categorie van alle boeken op "Metafysica".
Je kan ook de inhoud van meer dan één kolom aanpassen. Dat zou je als volgt doen (maar hoef je niet uit te voeren of op te slaan):
Zonder verdere specificatie zet het eerste stukje code de kolom Categorie
van alle rijen op 'Metafysica'.
Het zou kunnen dat alle boeken in de categorie Metafysica thuishoren. En in dat geval doet het statement precies wat je er van verwacht. Maar meestal willen wie niet alle rijen op dezelfde manier aanpassen. We moeten dus in het UPDATE statement specifiëren in welke rij we de kolom Categorie
willen updaten. We moeten één bepaalde rij eruit filteren. Dat doen we met de WHERE
clausule. We beperken de update tot de records die we met een SELECT zouden laten zien:
De WHERE
clausule bepaalt een voorwaarde die door de database als waar of vals geëvalueerd wordt. De voorwaarde Titel = 'Logicaboek'
wordt voor elke rij in de tabel geëvalueerd. Als de evaluatie van de logische expressie voor een bepaalde rij waar oplevert wordt die rij geüpdatet.
De vergelijking van strings in MySQL is standaard niet hoofdlettergevoelig! Je zou dus wel eens rijen kunnen aanpassen zonder dat dat je bedoeling is.
Je kan ook meerdere rijen in één keer updaten. Dat doe je door bijvoorbeeld de logische operator OR
te gebruiken:
Waar je een nieuwe waarde instelt, mag je ook weer een expressie gebruiken die een waarde oplevert. Net als in programmeren. Je kan bijvoorbeeld een waarde instellen die berekend wordt door strings aan elkaar te hangen of een substring te bepalen:
Hoe je de tweets terug koppelt aan de juiste account, lees je ook bij de uitleg rond . We tonen hier alleen dat het mogelijk is. Koppel users aan de juiste tweets met dit script, 0058__SelectUsersTweets.sql:
legt uit hoe je nu toont welke games op welk platform verschenen zijn. Dit wordt hier gedaan in 0061__SelectReleases.sql:
We willen niet alleen rijen kunnen selecteren gebaseerd op één kolom die gelijk is aan een bepaalde waarde. We moeten ook complexere voorwaarden kunnen opstellen. Daarom beschikken over we een hele reeks operatoren die we bovendien kunnen combineren.
Een expressie is een waarde of iets dat je kan uitrekenen om een waarde te krijgen. Een booleaanse expressie is een expressie met een uiteindelijke waarde TRUE
, FALSE
of (in SQL) NULL
.
De bouwstenen zijn:
TRUE
, FALSE
en NULL
haakjes (voor groepering)
de logische operatoren AND
, OR
en NOT
(en XOR
)
en andere constructies die een voorwaarde uitdrukken (deze komen later aan bod)
Het gebruik van de ronde haakjes dient om de volgorde van de evaluatie van de operatoren te bepalen. Wat tussen haakjes staat, werk je eerst uit als één geheel, vooraleer je de onderdelen buiten deze haakjes uitrekent.
Voor de logische operatoren gebruiken we waarheidstabellen:
AND
TRUE
FALSE
NULL
TRUE
TRUE
FALSE
NULL
FALSE
FALSE
FALSE
FALSE
NULL
NULL
FALSE
NULL
OR
TRUE
FALSE
NULL
TRUE
TRUE
TRUE
TRUE
FALSE
TRUE
FALSE
NULL
NULL
TRUE
NULL
NULL
NOT
TRUE
FALSE
NULL
FALSE
TRUE
NULL
XOR
TRUE
FALSE
NULL
TRUE
FALSE
TRUE
NULL
FALSE
TRUE
FALSE
NULL
NULL
NULL
NULL
NULL
Je kan de waarheidstabellen ook nabouwen in Workbench, bv.:
Het is niet genoeg gegevens uit een tabel te kunnen opvragen, je moet ze ook kunnen sorteren of ordenen. We willen lijsten in alfanumerieke volgorde weergeven omdat het gemakkelijker is in een geordende lijst te zoeken.
Om te sorteren gebruiken we de clausule ORDER BY
. Het sorteren kan op 2 manieren:
oplopend
aflopend
In het Engels is dat ascending en descending. In SQL wordt dat afgekort naar ASC
en DESC
. Stel dat je de namen uit de tabel boeken wil opvragen gesorteerd op de voornaam, de familienaam en titel van de boeken. De query ziet er dan zo uit:
Het eerste veld in de ORDER BY
clause wordt dan eerst bekeken, dan het tweede, enzovoort. Als niet aangegeven is of de sortering voor een bepaald veld oplopend of aflopend is, is ze standaard oplopend. Dat wil zeggen dat als je ASC
en DESC
niet vermeldt na een kolom, de code zich gedraagt alsof er ASC
staat.
De eerste bovenstaande SELECT
clausule toont dus eerst boeken in alfabetische volgorde van de familienamen van de auteurs. Als twee auteurs dezelfde familienaam hebben (althans volgens de gebruikte collation!), wordt hun voornaam gebruikt om de knoop door te hakken. Dan komt het boek met de voornaam die alfabetisch eerst komt ook eerst in de lijst. Als twee auteurs dezelfde naam hebben, of als het gewoon om twee boeken van dezelfde auteur gaat, wordt de titel gebruikt om de knoop door te hakken.
Voor de tweede query komen de familienamen met een 'Z' voor die met een 'A'. Let op: de voornamen met een 'A' komen wel voor de voornamen met een 'Z'!
Merk op uit bovenstaand voorbeeld: de volgorde van de kolommen in de ORDER BY
clausule hoeft niet dezelfde te zijn als de volgorde van de weergave in de SELECT
clausule. Het kan perfect zijn dat je wil sorteren op familienaam, maar wel eerst de voornaam wil tonen en dan pas de achternaam. Sterker nog: je kan zelfs ordenen op kolommen die je niet toont in de uitvoer.
Het is niet voldoende om rijen te kunnen opvragen. Je moet ook in staat zijn om specifieke informatie op te vragen, statistische gegevens te genereren, enz. Dit kan door uitdrukkingen en extra clausules te gebruiken in SELECT
statements.
De algemene vorm van een SELECT
statement is:
Hierbij heb je al redelijk wat flexibiliteit: je kan gewoon kolomnamen gebruiken als expressies; je kan functies toepassen op kolomnamen; je kan vaste tekst of getallen mee opnemen in de resultaten. Maar er zijn nog meer mogelijkheden. In het algemeen ziet het SELECT
statement er zo uit:
Deze volgorde is vast: FROM
komt altijd voor WHERE
, enzovoort.
Hiermee kan je:
enkel bepaalde rijen weergeven
rijen met eenzelfde waarde voor een bepaalde eigenschap groeperen
statistische eigenschappen van de getoonde data afdwingen
de uitvoer sorteren
In de loop van deze cursus komen al deze clausules aan bod.
Hoewel bovenstaande volgorde bepaalt hoe je een query moet uitschrijven, is dat niet hoe een query wordt uitgevoerd. De verschillende onderdelen worden verwerkt als volgt:
FROM
WHERE
GROUP BY
HAVING
ORDER BY
SELECT
Waarschijnlijk ken je al deze clausules nog niet, maar dat is niet erg. Je kan terug kijken naar deze uitleg telkens je een nieuwe clausule leert. Het idee is als volgt:
eerst geef je aan waar je data vandaag komt
dan bepaal je, per record, of dat record mee verwerkt zal worden of niet
de behouden records kan je groeperen om informatie over een bepaald groepje te tonen
per groepje kan je opnieuw bepalen of dat groepje verwerkt zal worden
je legt een volgorde vast
je toont het eindresultaat
In de meeste gevallen zijn we niet geïnteresseerd om alle rijen uit een tabel te selecteren. We willen over de mogelijkheid beschikken om alleen de rijen, die aan een bepaalde voorwaarde voldoen, te kunnen selecteren.
De oplossing bestaat erin de WHERE
-clausule te gebruiken. De WHERE
-clausule bevat een booleaanse expressie. Dit is een expressie die als resultaat voor een bepaalde rij in principe TRUE
of FALSE
moet opleveren. In tegenstelling tot de meeste programmeertalen gebruikt SQL ook een derde mogelijke waarde: NULL
. Deze waarde komt voor wanneer we niet kunnen bepalen of iets waar of niet waar is.
Om de WHERE
te gebruiken, zet je hem na de FROM Tabel
.
Bijvoorbeeld:
Ofwel (sla volgend script op als 0032__SelectBoeken.sql):
Je krijgt NULL
wanneer je bijvoorbeeld vergelijkt met een niet-ingevulde waarde, ook geschreven als NULL
. Zelfs NULL
is niet gelijk aan NULL
. Probeer maar eens alle boeken zonder titel op te vragen met een vergelijking (0033__SelectBoeken.sql):
Maar probeer ook deze eens (0034__SelectBoeken.sql):
Het ligt niet aan de data! Vergelijkingen met NULL
via =
en <>
zijn zinloos! Als je wil controleren of de waarde in een bepaalde kolom ontbreekt, schrijf dan IS NULL
in plaats van = NULL
!
Functies in SQL staan je toe een waarde te berekenen in plaats van een vaste waarde of de waarde in een of andere kolom.
Deze functie gebruik je om een deel van een stuk tekst over te houden. Je kan bijvoorbeeld dit doen om de eerste twee letters van de familienaam van een auteur te tonen:
Je bent ook niet beperkt tot kolomwaarden, je mag ook gewone constanten gebruiken:
Je kan wel net zo goed dit doen:
Als je alleen het begin van een string wil, kan je ook LEFT
gebruiken:
Deze functie gebruik je om stukken tekst aan elkaar te hangen. Je kan dus dit doen om de volledige naam van auteurs te tonen:
Je kan dit ook duidelijker laten weergeven in Workbench met:
Hiermee bereken je de lengte van een stuk tekst. Je kan bijvoorbeeld dit doen:
Dit zal je niet de familienaam van elke auteur tonen, maar wel het aantal letters in hun familienaam.
Net zo kan je dit doen:
Dan zal je als resultaat 3
krijgen.
Ook standaard wiskundige operaties zijn functies. Bijvoorbeeld:
Je kan in MySQL Workbench wel rechtstreeks naar je tabellen kijken, maar om gerichter te zoeken, moet je het commando kennen dat achter de schermen wordt uitgevoerd. Dat is het SELECT
-commando.
De simpelste vorm van het SELECT
commando toont gewoon alle data in een bepaalde tabel, bijvoorbeeld:
De asterisk is een "wildcard" of "joker". Ze geeft aan dat we alle kolommen willen zien. We kunnen ook kijken naar specifieke kolommen:
Tip: als je de namen van de kolommen niet uit het hoofd kent, typ dan eerst de naam van de tabel, gevolgd door een punt. Dan kan Workbench slim aanvullen:
Er zijn veel manieren om geselecteerde data te groeperen, maar de simpelste is ORDER BY
. Als je dit toevoegt aan een SELECT
-statement, kan je de rijen in een andere volgorde weergeven. Bijvoorbeeld:
Je kan ook een tweede (en derde,...) kolom gebruiken om knopen door te hakken:
De kolomnamen die je op deze manier gebruikt, hoeven niet getoond te worden in je resultaat.
Let op: de sorteervolgorde zal niet altijd overeenstemmen met wat je intuïtief verwacht. Ze hangt af van de gebruikte methode om stukken tekst te vergelijken, de zogenaamde "collation".
Er is ook een kortere, maar beperktere manier om te sorteren:
Dit sorteert eerst volgens de eerste kolom en dan volgens de tweede.
Voor je deze oefeningen maakt, gebruik je onderstaand script om zeker te zijn dat je databank in de gewenste toestand is.
Sla dit op als 0509__CalibrateDB.sql in je map met scripts.
Schrijf een script dat volgende nummers toevoegt aan je tabel, Liedjes
. Ze worden gegeven in het formaat "titel / artiest / album / jaar":
Stairway to Heaven / Led Zeppelin / Led Zeppelin IV / 1971
Good Enough / Molly Tuttle / Rise / 2017
Outrage for the Execution of Willie McGee / Goodnight, Texas / Conductor / 2018
They Lie / Layla Zoe / The Lily / 2013
It Ain't You / Danielle Nicole / Wolf Den / 2015
Unchained / Van Halen / Fair Warning / 1981
Sla op als 0510__Oefening.sql.
Doe hetzelfde voor een tabel Huisdieren
, met volgende gegevens. Ze worden gegeven in het formaat "baasje / naam / leeftijd / soort":
Vincent / Misty / 6 / hond
Christiane / Ming / 8 / hond
Esther / Bientje / 6 / kat
Jommeke / Flip / 75 / papegaai
Villads / Berto / 1 / papegaai
Bert / Ming / 7 / kat
Thaïs / Suerta / 2 / hond
Lyssa / Фёдор / 1 / hond
Sla op als 0511__Oefening.sql.
Schrijf een script, 0512__Oefening.sql, dat alle info over liedjes toont.
Schrijf een script, 0513__Oefening.sql, dat de naam en soort van elk dier toont, maar niet de leeftijd of het baasje.
Schrijf een script, 0514__Oefening.sql, dat (alleen!) de titels van liedjes toont, gaande van het oudste nummer tot het recentste nummer.
Schrijf een script, 0515__Oefening.sql, dat alle info over huisdieren toont, maar de huisdieren alfabetisch ordent volgens naam en, indien de naam dezelfde is, alfabetisch sorteert volgens soort.
Schrijf een script, 0516__Oefening.sql, dat per dier een tabel met één kolom, een omschrijving, toont. Deze heeft volgende vorm, inclusief de juiste hoofding:
Wanneer je klaar bent, voer dan de gewoonlijke commando's uit om je scripts te delen:
Ze hierna je database terug zoals ze was na de theorieles met het script van het einde van vorige labosessie.
Schrijf een script, 0517__Oefening.sql, dat alle nummers (alle kolommen) toont die uitgekomen zijn in een jaar dat eindigt op 1
. Tip: gebruik WHERE
in combinatie met een functie.
Schrijf een script, 0518__Oefening.sql, dat volgende zaken doet:
een niet-verplichte kolom Geluid
toevoegen aan de tabel met huisdieren, die tekst (tot 20 karakters uit het Engels) kan bevatten
tip: dit is een DDL-statement, dus kijk terug naar de DDL indien nodig
het geluid van alle honden aanpast naar "WAF!"
het geluid van alle katten aanpast naar "miauwww..."
Schrijf een script, 0519__Oefening.sql, dat volgende zaken doet:
de niet-verplichte kolom Genre
terug toevoegt aan de tabel met nummers (tot 20 karakters uit het Engels)
met maximum één UPDATE
-operatie alle nummers van Led Zeppelin en Van Halen aanduidt als Hard Rock
Schrijf een script, 0520__Oefening.sql, dat alle nummers wist (maar de tabelstructuur bewaart).
Schrijf een script, 0521__Oefening.sql, dat alle papegaaien wist.
Wanneer je klaar bent, voer dan de gewoonlijke commando's uit om je scripts te delen:
Ze hierna je database terug zoals ze was na de theorieles met het script van het einde van vorige labosessie.
Omschrijving
Misty de hond
Ming de hond
Bientje de kat
Flip de papegaai
Berto de papegaai
Ming de kat
Suerta de hond
Фёдор de hond
Aggregaatfuncties lijken op de functies die je al kent (substring
, concat
, length
, round
). Het verschil is dat, als je een kolomnaam invult, de aggregaatfunctie op heel de kolom wordt toegepast. Anders gezegd, aggregaatfuncties worden niet binnen een rij toegepast, maar over meerdere rijen heen. Voor elke rij wordt er een waarde geproduceerd en een aggregaatfunctie gebruikt al deze waarden samen als invoer.
Op deze pagina geven we een overzicht van de belangrijkste aggregatiefuncties, maar er bestaan er nog.
Voor deze voorbeelden gebruiken we volgende calibratiestap, die je moet opslaan als 0041__CreateHonden.sql
:
COUNT
COUNT
krijgt als invoer een lijst rijen en zegt hoeveel niet-NULL
elementen er in deze lijst zitten.
De simpelste toepassing is het tellen van al je records, zoals hier in 0042__SelectHonden.sql
:
Dit levert slechts één resultaat, ongeacht hoe veel honden er in de database staan. Dit is anders dan bijvoorbeeld LENGTH(Naam)
. Die laatste zou één resultaat per hond geven. De reden is dat COUNT
een aggregaatfunctie is. Ze verzamelt alle waarden in de gegeven kolom Naam
, telt hoe veel dat er zijn en toont dat resultaat. Namen die twee keer voorkomen, worden twee keer geteld. Let op: COUNT
telt geen NULL
-waarden.
Soms wil je gewoon weten hoe veel rijen er in een tabel zijn. Dan gebruik je COUNT(*)
. Bijvoorbeeld:
Dit geeft hetzelfde resultaat, want Naam
was toch een verplichte kolom.
SUM
Deze aggregaatfunctie krijgt een expressie en berekent voor de som van het toepassen van deze expressie voor elk record. Een eenvoudig voorbeeld van een expressie is hier (in 0043__SelectHonden.sql
) een kolomnaam:
Merk op: hier zal je niet zomaar *
in plaats van Leeftijd
kunnen invullen. Je kan de som nemen van getallen, maar niet van om het even welk soort data. SUM
werkt alleen met getallen.
Je kan wel iets algemener een expressie die een getal oplevert invullen, niet gewoon een kolomnaam. Dus je kan ook dit doen:
MIN
en MAX
Deze aggregaatfuncties krijgen een expressie en berekenen het minimum of het maximum voor het toepassen van deze expressie voor elk record. Een eenvoudig voorbeeld van een expressie is hier (in 0044__SelectHonden.sql
) een kolomnaam:
Dit vertelt je wat de leeftijd van de oudste hond in het systeem is.
AVG
Deze aggregaatfunctie krijgt een expressie en berekent het gemiddelde voor het toepassen van deze expressie voor elk record. Een eenvoudig voorbeeld van een expressie is hier (in 0045__SelectHonden.sql
) een kolomnaam:
Onder normale omstandigheden kan je geen combinatie maken van geaggregeerde resultaten en niet-geaggregeerde resultaten. Iets als het volgende gaat dus niet:
Dit komt omdat je uitvoer nog steeds een tabel is, bestaande uit één aantal rijen en één aantal kolommen. Als je COUNT(*)
zou toepassen, zou je één rij overhouden en als je Naam
zou opvragen, zou je even veel rijen overhouden als er honden zijn. Dus als deze query zou werken, zou je op basis van de eerste kolom maar één rij mogen hebben en op basis van de tweede kolom zou je er meerdere hebben. Dat gaat niet. Beide kolommen moeten leiden tot hetzelfde aantal rijen.
Eerst hebben we een relationele databank vooral gebruikt om individuele records bij te houden en op te vragen. Via aggregaatfuncties hebben we informatie over alle records gebundeld. Soms zoeken we iets dat in het midden ligt: informatie die niet handelt over individuele records, maar over groepen records. Typisch zijn dit records met dezelfde waarde in bepaalde velden. De oplossing schuilt in een GROUP BY
clausule.
Met de GROUP BY operator kan je rijen "samenpersen" en de gewenste informatie uit de samengeperste rijen halen. Dit "samenpersen" gebeurt eerst, voor de gewenste informatie wordt geselecteerd. Beeld je in dat er een tussenliggende tabel wordt aangemaakt op basis van de tabel waarin je wenst te zoeken.
Veronderstel dat je onderstaande tabel Honden
hebt:
In het eerdere calibratiescript staat veel meer data, maar voor een voorbeeld is het handiger minder data te tonen:
naam
leeftijd
geslacht
Ming
10
mannelijk
Swieber
14
mannelijk
Misty
7
vrouwelijk
Informatie die over groepen records gaat kan dan zijn: "hoeveel mannelijke honden zijn er in het systeem?" of "wat is de gemiddelde leeftijd per geslacht?" Deze vragen kan je niet meteen beantwoorden met de eerdere DML-commando's, maar wel met behulp van GROUP BY
. Honden GROUP BY Honden.Geslacht
moet je zien als een tijdelijke tabel die er als volgt uitziet:
namen per geslacht
leeftijd per geslacht
geslacht
[Ming,Swieber]
[10,14]
mannelijk
[Misty]
[7]
vrouwelijk
Je moet dit enkel zien als een hulpmiddel om over GROUP BY
na te denken! Je kan deze tussenliggende tabel niet zomaar produceren. Meer uitleg volgt verderop.
De kolom na GROUP BY
neemt geen nieuwe vorm aan, maar komt in de resultaten nog één keer voor per waarde. Er is dus precies één rij met de waarde "mannelijk"
en één rij met de waarde "vrouwelijk"
. De andere kolommen veranderen eigenlijk van datatype: de kolom voor de naam bevat een sequentie van VARCHAR(50)
per rij in plaats van een VARCHAR(50)
per rij. De kolom voor de leeftijd bevat een sequentie van TINYINT
in plaats een TINYINT
per rij, wat hier aangegeven is door de verschillende waarden tussen rechte haakjes te zetten. Er is gekozen voor deze notatie omdat dit lijkt op het gebruik van lijsten in de meeste programmeertalen. De kolom voor het geslacht bevat nog steeds waarden uit een ENUM
, omdat GROUP BY
nu juist zo werkt dat er precies één waarde is voor elke rij.
Je kan bij gebruik van GROUP BY
de data die in het voorbeeld tussen rechte haakjes staat (onder default omstandigheden) niet rechtstreeks tonen. Je kan er wel een aggregatiefunctie op toepassen. Als je wil weten wat de gemiddelde leeftijd per geslacht is, schrijf je dit (0046__SelectHonden.sql
):
Als je GROUP BY
gebruikt, wordt een aggregatiefunctie dus niet meer over heel de kolom toegepast, maar per groep. Geslacht
mag je wel tonen want daar heb je op gegroepeerd, dus die data staan niet tussen rechte haakjes.
Een speciaal geval is COUNT(*)
. Dit vertelt je hoe veel elementen er gegroepeerd zijn.
De tussenliggende tabel is een hulpmiddel om hierover na te denken; volgende code zou niet werken:
Ze levert: "Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'ApDB.Honden.Leeftijd' which is not functionally dependent on columns in GROUP BY clause". Het probleem is dus dat je data groepeert en dan geen aggregaat toepast op de data die in het voorbeeld hierboven tussen rechte haakjes wordt getoond.
GROUP BY
hoeft niet gevolgd te worden door één kolom, maar kan door meerdere kolommen gevolgd worden. In dat geval groepeer je records per unieke combinatie van kolomwaarden. Je kan bijvoorbeeld dit doen (0047__SelectHonden.sql
):
Dit toont je hoeveel mannelijke en hoeveel vrouwelijke honden er zijn van elke leeftijd die in het systeem voorkomt. Er zijn na uitvoering van het calibratiescript bijvoorbeeld 6 vrouwelijke honden van 1 jaar oud en 4 mannelijke honden van 1 jaar oud. We kunnen ter controle ook dit even doen (0048__SelectHonden.sql
):
Dit toont ons dat er 10 (dus 6 vrouwelijke en 4 mannelijke) honden zijn van 1 jaar oud. Anders gezegd: hoe meer kolommen je vermeldt na GROUP BY
, hoe meer onderverdelingen je zal zien.
Eerder zeiden we: "Honden GROUP BY Honden.geslacht
moet je zien als een tijdelijke tabel die er als volgt uitziet:"
namen per geslacht
leeftijd per geslacht
geslacht
[Ming,Swieber]
[10,14]
mannelijk
[Misty]
[7]
vrouwelijk
Je kan deze tabel niet echt op deze manier tonen, maar je kan ze wel bijna verkrijgen door middel van de group_concat (aggregaat)functie. Deze aggregeert alle waarden in één groep door middel van concatentie (met andere woorden: ze plakt ze allemaal achter elkaar).
Probeer maar eens volgende query: select group_concat(Naam), group_concat(Leeftijd), Geslacht from Honden group by Honden.Geslacht
Met de WHERE
-clausule kon je voorwaarden uitdrukken voor het selecteren (of updaten of wissen) van records. Je kan met WHERE
echter geen voorwaarden uitdrukken op kolommen die vermeld zijn in de GROUP BY
-clause of op geaggregeerde waarden. Met andere woorden, dit gaat niet:
Ook dit zal niet werken:
Dat komt omdat WHERE
rij per rij bekijkt en beslist of die rij behouden of verworpen wordt.
Als je voorwaarden op gegroepeerde data wil uitdrukken, maak je gebruik van HAVING
(0050__SelectHonden.sql
) en (0051__SelectHonden.sql
):
In het kort: voor voorwaarden op kolommen in een GROUP BY
of op geaggregeerde waarden gebruik je HAVING
, voor andere voorwaarden gebruik je WHERE
.
Er bestaan queries die je hetzelfde resultaat opleveren, of je nu WHERE
of HAVING
gebruikt. Dat betekent niet dat ze inwisselbaar zijn. Wat achter de schermen gebeurt, is wel degelijk verschillend. Soms zijn er gewoon verschillende werkwijzen die tot hetzelfde resultaat leiden.
IN
voor verkorte notatieSoms wordt de SQL-basissyntax wat langdradig. Een voorbeeld daarvan is als je verschillende waarden tegelijk wil onderzoeken. Het kan bijvoorbeeld zijn dat je enkel in bepaalde honden geïnteresseerd bent, waarvan je de naam al kent. Bijvoorbeeld Bailey, Cookie, Lola, Iggy, Snoopy en Leo.
Een logge manier om hun gegevens op te vragen is deze, die je al kent:
We moeten steeds het gedeelte Naam =
herhalen. Dit komt omdat de WHERE
-clausule een booleaanse expressie moet bevatten.
Gelukkig is er een andere manier om sneller de gewenste booleaanse expressie te vormen (0052__SelectHonden.sql
):
Je schrijft dus een lijst van alle mogelijkheden met ronde haakjes en met elementen die gescheiden worden door komma's. De IN
-operator wil dan zeggen dat de gevraagde waarde voorkomt in de lijst. Dit doet hetzelfde als de eerdere code, maar kost veel minder typwerk.
Als we uitdrukkelijk rekening willen houden met hoofdletters en accenten, kunnen we ook hier de gebruikte collation aanpassen (0053__SelectHonden.sql
):
De IN
is niet specifiek gelinkt aan WHERE
, maar is gewoon een andere operator om een booleaanse expressie te bekomen. Je kan dus ook dit doen (0054__SelectHonden.sql
), met HAVING
in plaats van WHERE
:
Je kan zelfs gewoon dit doen:
(ondertiteling beschikbaar)
(ondertiteling beschikbaar)
(ondertiteling beschikbaar)
In MySQL kan je in een HAVING
-clausule ook verwijzen naar bepaalde andere kolommen, maar wordt dit afgeraden.
Vaak zijn we geïnteresseerd in een specifieke deel data. Bijvoorbeeld alle personen in een bepaalde leeftijdscategorie. We kunnen dit al schrijven als volgt (in dit geval voor grote puppy's in plaats van personen):
Een beetje zoals bij de syntax die we hebben ingekort met IN
zitten we hier met overbodig schrijfwerk. We kunnen dit oplossen met BETWEEN ... AND ...
(0055__SelectHonden.sql
):
Let op: hier is AND
op zich geen booleaanse operator, maar BETWEEN ... AND ...
is wel een tweedelige booleaanse operator. Met andere woorden, 1 AND 2
hierboven levert je geen booleaanse waarde op, terwijl de meeste voorkomens van AND
dat wel doen. Omdat BETWEEN ... AND ...
een booleaanse operator is, kan je ook dit doen:
En je kan (zoals overal waar je een string gebruikt) de gebruikte collation wijzigen:
In dit deel leer je data over verschillende tabellen heen te combineren. Er is nog veel meer te leren over de DML, maar dit is het meest gevorderde gebruik van de DML in deze cursus.
Met GROUP BY
kan je er voor zorgen dat bepaalde waarden worden samengenomen. Op die manier kan je bijvoorbeeld (voor de dataset met honden) elk geslacht één keer tonen. Dat is nuttig, want vaak zijn we niet geïnteresseerd in dubbels. We hebben echter gezien dat na groepering enkel gegroepeerde of geaggregeerde waarden kunnen tonen.
Er is een andere (en meer expliciete) manier om dubbels te vermijden: SELECT
laten volgen door het sleutelwoordje DISTINCT
. Hierdoor worden alle dubbele resultaten verwijderd. "Dubbel" wordt hier geïnterpreteerd als "gelijk volgens de collation van de kolom in kwestie".
Volgende query geeft je alle hondennamen (0056__SelectHonden.sql) precies één keer:
Na DISTINCT
kan je schrijven wat je bij een gewone SELECT
zou kunnen schrijven, dus dit gaat ook:
De "domste" manier om data uit meerdere tabellen te combineren tot data die uit één tabel lijkt te komen, is de "cross join". Deze "plakt" elke rij uit tabel A aan elke rij uit tabel B. Veronderstel bijvoorbeeld volgende tabellen voor taken en voor personen:
omschrijving
Id
bestek voorzien
1
frisdrank meebrengen
2
aardappelsla maken
3
voornaam
Id
Taken_Id
Yannick
1
2
Bavo
2
1
Max
3
3
Deze kunnen we op deze manier combineren en tonen:
Dit levert dan een resultaat dat er als volgt uitziet:
omschrijving
Taken.Id
voornaam
Leden.Id
Taken_Id
bestek voorzien
1
Yannick
1
2
bestek voorzien
1
Bavo
2
1
bestek voorzien
1
Max
3
3
frisdrank meebrengen
2
Yannick
1
2
frisdrank meebrengen
2
Bavo
2
1
frisdrank meebrengen
2
Max
3
3
aardappelsla maken
3
Yannick
1
2
aardappelsla maken
3
Bavo
2
1
aardappelsla maken
3
Max
3
3
Dit bevat nuttige informatie, maar ook rijen waar we niets aan hebben. De interessante rijen zijn die, die een persoon koppelen aan een taak. Dat zijn de rijen waarin Taken.Id
gelijk is aan Taken_Id
(afkomstig uit Leden
).
Je kan dus personen koppelen aan hun taak via:
Hier moet je Taken.Id
schrijven omdat zowel Taken
als Leden
een kolom Id
hebben. Door de tabelnaam toe te voegen, maak je duidelijk over welke kolom het precies gaat.
Dit laatste voorbeeld werkt in MySQL, maar het wordt typisch anders geschreven. Meestal zal CROSS JOIN
vervangen worden door INNER JOIN
, terwijl WHERE
vervangen wordt door ON
. Wanneer we twee tabellen willen koppelen zodat samenhorende rijen uit tabel A en tabel B één nieuwe rij opleveren, zullen we deze conventie volgen.
Het resultaat zal er dus zo uitzien:
Het resultaat is hetzelfde, maar in dit scenario wordt INNER JOIN
verkozen. ON
is ook geen synoniem voor WHERE
, want het kan alleen gebruikt worden in een JOIN
-statement.
Je kan DISTINCT
ook gebruiken binnen sommige aggregatiefuncties. Bijvoorbeeld: COUNT(DISTINCT Naam)
om het aantal verschillende namen te weten te komen. Over welke aggregatiefuncties het gaat, vind je terug.
Om de integriteit van de database te bewaren, normaliseren we de gegevens in de database. Door normalisering geraakt de informatie echter verspreid over meer dan één tabel. De gebruiker heeft daar echter niet veel aan. Primaire en vreemde sleutels zeggen de gebruiker niets. Anders gesteld, een tabel vol getallen zoals onderaan op is op zich niet leesbaar. We moeten dus een manier vinden om de informatie weer voor te stellen alsof die uit één tabel komt.
In MySQL is er eigenlijk tussen een CROSS JOIN
met een WHERE
clause en een INNER JOIN
met een ON
clause, die we dadelijk zullen bekijken. We kiezen voor de ene of de andere vorm om uit te drukken wat we bedoelen, maar het resultaat zal hetzelfde zijn.
We weten wel hoe we leden aan taken kunnen koppelen door middel van INNER JOIN, maar die aanpak werkt niet wanneer er meer dan twee tabellen nodig zijn om de data terug te combineren, zoals in het geval van releases van games op platformen.
Om entiteiten gekoppeld via een M-op-N-relatie aan elkaar te koppelen, moet je eerst de informatie langs de M-kant koppelen met de tabel die de relatie voorstelt en vervolgens de informatie langs de N-kant koppelen.
Onze tabel met info over releases van games was:
Games_Id
Platformen_Id
1
1
1
2
1
3
2
1
2
2
2
3
3
1
3
2
4
1
4
2
4
3
4
4
We kunnen deze tonen met informatie over de game als volgt:
Dit voegt langs de rechterkant gewoon de details over de uitgebrachte game. Het is dus een meer uitgebreide versie van de tabel Releases. Daarom kunnen we dezelfde techniek herhalen en enkel de interessante info tonen:
We werken voortaan met het relationele databankmodel
Het is belangrijk dat wanneer een database wordt ontworpen er voldoende doordacht wordt nagedacht over de gewenste structuur van de te onwikkelen database.
Enkele aandachtspunten kunnen dan zijn:
Welke gegevens wenst u op te slaan?
Moeten die gegevens aan bepaalde criteria voldoen?
Waarvoor moeten die gegevens achteraf gebruikt worden?
...
Tabel __Gegevens worden opgeslagen in een tabel. Deze tabel heeft steeds een naam, of tabelnaam. Eén tabel bevat normaal gesproken samenhorende gegevens. (bv. personen, facturen, …) of entiteiten.
Record __Een tabel bestaat uit rijen of records. Eén record bevat alle gegevens van de zgn. rij. (bv. alle gegevens betreffende een persoon)
Kolommen __Iedere kolom omvat één aanduiding van een gegeven binnen een tabel. (bv. voornaam van een persoon, adres, enz...) Iedere kolom heeft uiteraard een unieke naam, of veldnaam.
Sleutels __Ieder record behoeft één veld (kolom) dat ervoor zorgt dat dat record uniek kan worden aangegeven. Dit veld noemt men de primaire sleutel. Bijvoorbeeld een persoons ID. Alleen de familienaam is waarschijnlijk niet voldoende. Daarentegen zouden we kunnen veronderstellen dat de combinatie van achternaam, voornaam, geboortedatum waarschijnlijk wel uniek is, maar dit is evenwel veel te omslachtig. Het is wel duidelijk dat de velden, hierboven aangehaald, een samengestelde of alternatieve sleutel vormen. Laten we volgend voorbeeld nemen, nl. de postcodes en gemeenten. Deze twee gegevens zijn samenhorend en het is niet echt zinvol om steeds de twee gegevens in een hele reeks tabellen te gaan bijhouden. Daarom kunnen we een afzonderlijke tabel aanmaken waarin de postcodes en gemeenten vervat zijn. Door dan in alle andere tabellen enkel de postcode bij te houden, kunnen we door die postcode perfect de gemeente opzoeken in de tabel met alle postcodes en gemeenten. Men noemt een dergelijke sleutel in een andere tabel dan die van de postcodes en gemeenten een vreemde sleutel.
Wanneer je bv. naam en adresgegevens wil bewaren in een database, mag je dit niet doen door gewoonweg in één veld de naam, voornaam, adres, postcode en gemeente in die volgorde te vermelden. De oplossing ligt er namelijk in dat je per gegeven een afzonderlijke kolom gaat voorzien.
Ook is het niet aan te raden om één veld voor evt. meerdere doeleinden te gebruiken, bv. het veld telefoonnummer gebruik je best niet voor de vermelding van een sis-nummer.
Het komt nog vaak voor dat de opbouw of normalisatie van een gegevensverzameling niet voldoende doordacht is. Volgend voorbeeld zal dit verduidelijken.
Een firma wil de gegevens betreffende zijn uitgeschreven facturen bijhouden in een database. Hiervoor zijn volgende gegevens noodzakelijk nl.
Factuurnummer: het nummer van de factuur
Factuurdatum: de datum van de factuur
Artikelnummer: de code van het betreffende artikel dat gefactureerd werd
Artikelomschrijving: de omschrijving van het gefactureerde artikel
Artikelprijs: de prijs van het gefactureerde artikel
Aantal: aantal eenheden van één artikel
BTW-percentage: het doorgerekende BTW-tarief uitgedrukt in percentage
Klantnummer: het nummer van de gefactureerde klant
Klantnaam: de naam van de klant vermeld op de factuur
Klantadres: het adres van de klant vermeld op de factuur
Bovenvermelde gegevens kunnen we dus in één tabel onderbrengen, hetgeen hetvolgende geeft.
Zoals je wel zal bemerken zijn er een aantal zgn. problemen, nl.
Er worden een aantal gegevens herhaald, nl. de factuurnummer, de factuurdatum, klantnummer, klantnaam en klanadres worden evenwel per gefactureerd artikel herhaald. Dit is dus een overtolligheid van gegevens.
Zonder dat er een factuur werd opgesteld kan geen nieuwe klant worden toegevoegd.
Wanneer we bijvoorbeeld de artikelomschrijving van een bepaald artikelnummer willen aanpassen, dienen we het hele bestand te doorlopen om deze aanpassingen door te voeren.
Wanneer we de inhoud van een bepaalde factuur zouden verwijderen (dit bv. ter annulatie), dan kan het zijn dat alle informatie van een klant mee wordt verwijderd. Bijvoorbeeld, wanneer we de inhoud van factuurnummer 112 zouden wissen, dan verdwijnt ook alle informatie aangaande de klant JANSSENS Jan.
Om ervoor te zorgen dat o.a. bovenstaande problemen worden opgelost, kunnen we de onderscheiden gegevens gaan spreiden over verschillende tabellen. Let wel, we moeten er wel voor zorgen dat er steeds geen verbanden tussen de onderliggende gegevens verloren gaat.
Zoals hierboven vermeld, bekomen we bij de uitsplitsing tot vier afzonderlijke tabellen. Niettegenstaande de uitsplisting in deze vier tabellen ontstaat er geen gegevensverlies. Geen gevensverlies, even verklaren, o.a.
bij tabel A is er een verband tussen de factuurnummer en de klantnummer. We weten hierbij dus perfect welke factuur voor welke klant bestemd is omdat we de informatie aangaande de
betreffende klantnummer kunnen gaan opzoeken in tabel B en dit d.m.v. de klantnummer.
Ook weten we welke artikels er gefactureerd werden. In tabel D wordt de factuurnummer in combinatie met de artikelnummer vermeld en hierdoor weten we welk artikel(s) moet gefactureerd
worden. De prijs van het artikel en omschrijving vinden we dan waar terug in tabel C, waarbij we door middel van het artikelnummer betreffend item kunnen opzoeken.
Zoals je wel zal bemerken zijn de problemen opgelost, nl.
Er worden geen gegevens meer herhaald per gefactureerd artikel . De overtolligheid van gegevens is dus opgelost.
Een nieuwe klant kan zonder problemen worden opgenomen in de tabel van de klanten (tabel B).
Wanneer we bijvoorbeeld de artikelomschrijving van een bepaald artikelnummer willen aanpassen, dienen we dat maar op één plaats door te voeren, nl. in de tabel van de artikelen (tabel C).
Wanneer we de inhoud van een bepaalde factuur zouden verwijderen (dit bv. ter annulatie), dan wordt de informatie van een bepaalde klant niet mee gewist.
JOIN
is niet alleen handig om informatie samen te tonen. Deze operatie kan ook gebruikt worden om aanpassingen door te voeren waarvoor we informatie uit een andere tabel nodig hebben.
Volgend script werd in de oefeningen gebruikt om nummers via een ID te linken aan een artiest, nadat artiesten in een aparte tabel waren geplaatst. De liedjes hebben dus nog géén ID, maar bevatten wel nog de naam van de artiest:
Eerst moet je dus kijken naar wat een gewone SELECT
van deze samengevoegde tabel zou opleveren. Hij zou informatie over een nummer plaatsen naast informatie over een artiest met dezelfde naam.
Onthoud heel goed: het is niet omdat we vaak schrijven ON X.Id = X_Id
dat dat de manier is om JOIN te gebruiken. Dit is niet meer of niet minder dan een vergelijking van de waarden in twee kolommen.
Voor uitvoering zien rijen uit de tabel met liedjes er dus zou uit:
Rijen uit de tabel met artiesten zien er zo uit:
Een combinatie van de twee tabellen kan je bekijken via (dus zoals in de update hierboven, maar de informatie wordt alleen getoond):
Deze ziet er zo uit:
Dus de kolom Artiesten_Id
is nog leeg, maar we kunnen het volgende zeggen:
ze is afkomstig uit Nummers
de waarde die we wensen in te vullen is wel zichtbaar in deze JOIN
, want ze staat in de kolom Id
afkomstig uit Artiesten
We weten dus wat we zouden moeten invullen voor al deze NULL
-waarden en we weten waar die info dan zou moeten worden opgeslagen. De UPDATE
is dan ook toegelaten en werkt zoals gewenst.
Compactheid: Normaal gesproken (zeer waarschijnlijk) neemt een database minder plaats in dan bijvoorbeeld al de gegevens op papier.
Snelheid: De gegevens worden in normale omstandigheden sneller aangepast/opgezocht/gewijzigd (o.a. door de gecentraliseerde opslag).
Up to date: Doordat de aanpassingen normaal minder werk vragen dan wanneer men nog zou spreken van een papieren opslag van gegevens, zullen deze gegevens sneller (en waarschijnlijk ook correcter) worden aangepast.
Uniek: Het is de bedoeling van een database om gegevens zo uniek mogelijk bij te houden. Anders gezegd, het is de bedoeling dat bepaalde gegevens slechts één keer worden bijgehouden in de database. Dit heeft uiteraard als bijzonder voordeel dat er in die gevallen zeker geen verwarring zal bestaan omtrent hun betekenis/waarde.
Gemeenschappelijk: Afhankelijk van een hele reeks factoren kunnen bepaalde of alle gegevens gemeenschappelijk zijn. Meerdere gebruikers kunnen er dus tegelijkertijd mee werken.
Veilig: Een database kan beveiligd worden, bv. enkel de DBA (database administrator) kan alles. De andere gebruikers zijn in bepaalde mate beperkt in hun toegang en mogelijkheden op de database; Aan de gewone gebruikers kunnen bijvoorbeeld enkel leesrechten worden toegestaan of beperkte schrijfrechten. Vaak gaat het dan nog enkel om schrijfrechten voor bepaalde gegevens.
Backup van de gegevens: Door het veilig stellen van gegevens op externe dragers (harde schijf, cassette,...) is het makkelijk een kopie te maken.
Standaardisatie: De voorstelling van gegevens ligt in veel soorten databanken vast. Het is dus niet aan de gebruiker om zelf een geschikt formaat te kiezen bijvoorbeeld een datum voor te stellen.
Integriteit: Dit door een controle op de invoer van gegevens in te bouwen. Een databank kan bijvoorbeeld vereisen dat een bepaalde waarde een getal voorstelt tussen 1 en 50, een datum, een stuk tekst van maximaal 100 karakters, enzovoort.
Relationele integriteit: Dit is een specifiekere vorm van integriteit. Doordat er bepaalde relaties (verbindingen/verbanden) bestaan tussen de onderliggende gegevens, kunnen deze verbanden gecontroleerd worden. "Relationele integriteit" betekent dat al deze verbanden gerespecteerd worden. Bijvoorbeeld: elke aankoop is gelinkt aan een klant; er is geen aankoop die niet door een klant is uitgevoerd.
Let op: er bestaan verschillende soorten databanken en deze hebben niet allemaal dezelfde voordelen in dezelfde mate. Maar in het algemeen kan je wel stellen dat dit voordelen zijn ten opzichte van een papieren administratie of een verzameling Office bestanden.
Entity Relationschip Model
Relationele databases vormen regelmatig de ruggegraat van (web)applicaties. We zullen dat in de verdere labo-oefeningen demonstreren. Voor de verdere oefeningen zullen we voornamelijk werken met een database genaamd apTunes
. Dit is een database die je zou kunnen gebruiken om een streamingdienst (vergelijkbaar met Spotify, Deezer,...) te ondersteunen.
apTunes
moet allerlei functionaliteit ondersteunen die je gewend bent van echte streamingdiensten, onder meer:
voor de eindgebruiker:
zoeken op titel, artiest, genre, etc. van een nummer
bijhouden van een persoonlijke collectie nummers, albums, playlists
nummers opslaan als favorieten
enzovoort
voor de eigenaar:
de populairste nummers binnen een bepaalde periode opvragen
per artiest het aantal afgespeelde nummers in een bepaalde periode opvragen
enzovoort
Tegen het einde van de cursus ken je genoeg MySQL om de databasekant van een prototype van een dergelijke streamingdienst te schrijven.
We zeggen uitdrukkelijk "prototype". Je zal de nodige data kunnen bijhouden en opzoeken, maar in een echte applicatie moet je ook rekening houden met performantie, beveiliging en stabiliteit. Die zaken laten we voor later.
We starten vanaf een voorstelling die we steeds beter en beter zullen maken:
Schrijf zelf code die deze twee tabellen aanmaakt en noem ze aptunes__0001.sql. Merk op dat de duurtijd uitgedrukt wordt in seconden.
Voer je een foute instructie uit? Voer dan in volgorde elk script uit dat in het vet en cursief staat tot vlak voor het script waarbij het fout liep en corrigeer dan je script.
Voer bovenstaand script uit en zet het in je map met scripts met de naam aptunes__0002.sql.
Schrijf een script, aptunes__0003.sql, dat alle nummers van Led Zeppelin in het systeem toont volgens titel.
Schrijf een script, aptunes__0004.sql, dat alle nummers van Ghostface Killah en alle nummers van Blues Pills samen toont, eerst volgens artiest en dan volgens titel. Dus eerst komen alle nummers van Blues Pills in alfabetische volgorde en dan alle nummers van Ghostface Killah in alfabetische volgorde.
Wis alle nummers die geen releasedatum hebben. Noem je script aptunes__0005.sql.
Schrijf een script, aptunes__0006.sql, dat alle nummers die geen klassieke muziek zijn toont. Bekijk de data om te achterhalen hoe je klassieke nummers kan herkennen.
Schrijf een script aptunes__0007.sql dat de titels van alle nummers toont die strikt minder dan 2 minuten duren en daarnaast ook toont hoe veel minder. Je zou dus iets als dit kunnen zien als "Kortjakje" 55 seconden duurt en "'k Zag twee beren" 40 seconden duurt:
Denk nu even na over een aantal zoekopdrachten die je met dit model nog niet zou kunnen doen. Noteer ze voor jezelf. Bespreek tijdens het labo klassikaal.
Voor onze streamingdienst is het handiger te werken met een vaste set van genres. Anders krijgen we zo veel subgenres dat het niet meer handig is genres te gebruiken. Vandaar volgende wijziging:
Voeg met script aptunes__0008.sql een kolom GenreEnum toe. Deze is voorlopig niet verplicht en kan alleen volgende waardes bevatten: Klassiek
, Pop
, Jazz
, Metal
, Rap
, Electro
, Folk
, Wereldmuziek
, Blues
, Rock
. Het is niet de bedoeling dat je de oude kolom Genre
verwijdert of dat je de nieuwe kolom al invult! Verderop zullen we vertalen van de oude kolom naar de nieuwe!
Vul met script aptunes__0009.sql de kolom GenreEnum in. Doe dit door gebruik te maken van de LIKE operator om volgende "vertalingen" toe te passen:
alles met de (hoofdletterongevoelige) substring "klass" in de oude kolom wordt "Klassiek" in de nieuwe kolom
alles dat eindigt op de (hoofdletterongevoelige) substring "rock" in de oude kolom wordt "Rock" in de nieuwe kolom
"rap" en "hiphop" (hoofdletterongevoelig) worden gegroepeerd onder "Rap"
alles met het woord "metal" (hoofdletterongevoelig) in wordt "Metal"
alles dat eindigt op "blues" (hoofdletterongevoelig) wordt "Blues"
Test met script aptunes__0010.sql of alle nummers nu een (nieuw) genre hebben door de nummers zonder genre te selecteren.
Verwijder met script aptunes__0011.sql de oude kolom Genre
en hernoem GenreEnum
naar Genre
. Maak hierbij het nieuwe genre ook een verplichte kolom.
We merken ook dat het niet erg nuttig is om een exacte datum bij te houden per nummer. We willen alleen het jaar. Je kan het jaar uit een datum halen door er de YEAR
-functie op toe te passen.
Maak met script aptunes__0012.sql een kolom ReleaseJaar
aan, vul deze automatisch in op basis van de releasedatum die je al hebt, maak de nieuwe kolom verplicht en verwijder ten slotte de kolom ReleaseDatum
. Dit zal niet werken voor de nummers van Debussy, omdat ze te oud zijn om met het YEAR-datatype voor te stellen. Vul daarom het jaar van de uitvoering in voor deze nummers: 1985.
Nu willen we weten wat de prijs is die de artiest verdient (in eurocent) wanneer een nummer gestreamd wordt. We zullen dit bijhouden met een kolom Royalties
. Deze heeft type TINYINT
en is unsigned. Er is een vuistregel voor het vastleggen van royalties, maar voor sommige nummers is er een speciale regeling. Omdat er alleen in gehele bedragen in eurocent wordt gewerkt, heb je de functie ROUND
nodig om een getal af te ronden.
Voeg met script aptunes__0013.sql deze kolom toe. Vul ze in als volgt:
Klassieke nummers krijgen normaal 1 eurocent per 60 seconden, dus je stelt de royalties in als de duurtijd gedeeld door 60 (afgerond).
Rocknummers en metalnummers krijgen 1 eurocent per 20 seconden.
Rap krijgt 1 eurocent per 15 seconden.
Nummers van Led Zeppelin krijgen 1 eurocent per 10 seconden.
Er is een wijziging in de prijsstructuur! Verhoog met script aptunes__0014.sql alle royalties met 20%. Rond opnieuw af om een geheel bedrag te krijgen.
De eigenaars van onze streamingdienst willen statistieken over de artiesten.
Gebruik eerst volgend script aptunes__0015.sql om meer data in het systeem te plaatsen:
Nu er redelijk wat data is, moeten we die gaan samenvatten eerder dan rij per rij te bekijken.
Schrijf een script aptunes__0016.sql dat toont hoe veel rocknummers er in het systeem zijn.
Schrijf een script aptunes__0017.sql dat voor elk genre toont hoe veel nummers er zijn. Het formaat van de uitvoer is als volgt en de genres zijn gesorteerd volgens de volgorde van de enum die de genres voorstelt:
Toon het releasejaar van het oudste nummer in het systeem. De uitvoer is één rij met één kolom. De titel van deze kolom maakt niet uit. Noem je script aptunes__0018.sql.
Herschrijf volgende query zonder DISTINCT
zodat je toch nog hetzelfde resultaat krijgt en noem je script aptunes__0019.sql: SELECT DISTINCT Artiest FROM Nummers;
Toon alle nummers die minstens 5 minuten duren volgens titel. Noem je script aptunes__0020.sql.
Toon alle genres waarvan de nummers gemiddeld minstens 5 minuten duren, in de volgorde die is vastgelegd voor de genre enum. Noem je script aptunes__0021.sql.
Toon per decennium het aantal uitgebrachte nummers, maar enkel als dat aantal hoger ligt dan 450. Om dit klaar te spelen, moet je afronden tot het dichtste tiental. Dat kan je doen met TRUNCATE(getal,-1), bijvoorbeeld TRUNCATE(1973,-1) levert 1970. Je output moet ook stijgen volgens het decennium. Je resultaat zou er bijvoorbeeld zo moeten uitzien:
Het blijkt dat erg lange klassieke nummers niet erg winstgevend zijn voor onze dienst. Toon daarom alfabetisch alle artiesten die klassieke nummers hebben, maar enkel als hun klassieke nummers ook gemiddeld langer dan 8 minuten duren. Noem je script aptunes__0023.sql. Tip: afhankelijk van hoe je dit doet, heb je de meeste of zelfs alle clausules nodig.
De apTunes database, zoals ze ontwikkeld is met scripts 1 tot 23, is niet erg efficiënt ontworpen. We zullen ze herstructureren met relationele concepten.
We willen ons ontwerp verbeteren, zodat we uiteindelijk volgende relaties krijgen:
Op deze figuur staat "Liedjes" in plaats van "Nummers". De eerstvolgende opdrachten gebruiken nog "Nummers", maar dat wordt verderop aangepast.
Dit ERD toont alleen de entiteittypes en de relatietypes. De precieze velden ervan moet je zelf kunnen afleiden uit de informatie waarvan je vertrekt.
We zullen eerst de 1-op-N relaties voorstellen.
Maak een script dat een tabel Artiesten
aanmaakt voor artiesten. Noem dit aptunes__0024.sql
. Zorg ervoor dat elke artiest die we toevoegen automatisch genummerd wordt.
Maak een script om de data over artiesten in te vullen in deze tabel. Je mag veronderstellen dat twee artiesten met dezelfde naam dezelfde artiest zijn.
Hiervoor kan je in een INSERT
query het gedeelte VALUES (...)
vervangen door een subquery waarvan de resultaten overeenstemmen met de values die je zou invullen. Met andere woorden: INSERT INTO EenTabel (IngevuldeKolom1, IngevuldeKolom2) (select KolomWaarde1, KolomWaarde2 from EenAndereTabel);
Noem je script aptunes__0025.sql
.
Maak een script om Nummers
te voorzien van een foreign key kolom die verwijst naar de tabel Artiesten
. Volg hierbij alle afspraken rond foreign keys! Voorlopig kan je deze niet verplicht maken. Noem je script aptunes__0026.sql
.
het script om de artiesten te linken hoef je op dit moment niet te begrijpen, maar krijg je hieronder (aptunes__0027.sql
)
het script om de verwijzing naar een Artiest
verplicht te maken en kolom Artiest
uit Nummers
te verwijderen is aptunes__0028.sql
het script om Albums
te maken is aptunes__0029.sql
(schrijf je zelf, moet alleen info zuiver over het album bevatten die nu in Nummers staat)
het script om data te migreren naar Albums
is aptunes__0030.sql
(schrijf je zelf)
het script om Albums
te voorzien van een foreign key waarmee je naar de artiest verwijst is aptunes__0031.sql
(schrijf je zelf)
het script om de albums te linken aan artiesten krijg je hieronder (aptunes__0032.sql
)
het script om gebruikers toe te voegen krijg je hieronder (aptunes__0033.sql
)
Vanaf hier spreken we over "Liedjes" in plaats van "Nummers". "Nummers" werd verwarrend omdat we ook getallen ("nummers") als sleutelwaarden gebruiken.
Fris het vorige deel op indien je ergens niet kan volgen, want deze leerstof bouwt rechtstreeks voort op de vorige. We werken hier verder naar het ontwerp volgens het ERD.
Het script om GebruikerHeeftAlbum
toe te voegen noem je aptunes__0035.sql.
Naast de sleutelkolommen voorzie je ook een kolom DatumToevoeging
om het tijdstip toe te voegen waarop dit album toegevoegd is aan de bibliotheek. Dit is een verplichte datetime.
Het script om de data toe te voegen (aptunes__0036.sql
) vind je hieronder terug.
Het script om LiedjeOpAlbum
toe te voegen noem je aptunes__0037.sql
. Het tracknummer hou je bij in een kolom TrackNummer
, het past in een int
.
In deze tabel sla je volgende informatie op met een eigen script aptunes__0038.sql
. (Om dit te doen zoek je met de hand de Id
van het nummer en van het album op in hun tabellen en INSERT
je hun combinatie in de nieuwe tabel LiedjeOpAlbum
. De werkwijze is dus dezelfde als in script 36.)
Het nummer met titel Stairway to Heaven is het vierde nummer op het album Led Zeppelin IV
Het nummer met titel Problem Child is het tweede nummer op het album met titel Let There Be Rock
Het script om GebruikerHeeftLiedje
toe te voegen noem je aptunes__0039.sql.
Je hebt ook een kolom Favoriet
nodig (van type int) om bij te houden of een nummer een favoriet nummer van die gebruiker is. Als het een favoriet nummer is, vul je 1 in, anders 0.
In deze tabel sla je volgende informatie op met een eigen script aptunes__0040.sql
. De werkwijze is dezelfde als eerder.
tuneBoY5 heeft het nummer Little Sun, maar het is geen favoriet
musicfan111 heeft het nummer Eat the Rich en het is een favoriet
Toon de titel en de artiest van alle nummers. Maak hierbij gebruik van een inner join. Noem dit script aptunes__0041.sql
.
Toon deze zelfde gegevens nu enkel voor nummers waarvan de titel begint met de letter "A". Noem dit script aptunes__0042.sql
.
Toon de titel van elk album naast de naam van de artiest. Noem dit script aptunes__0043.sql
.
Toon hoe veel nummers Led Zeppelin heeft. Noem dit script aptunes__0044.sql
.
Toon de titel van elk nummer naast het Id
van het album waarop dat nummer staat. Noem dit script aptunes__0045.sql
. Hiervoor heb je de tabel NummerOpAlbum
nodig. Je hebt nog maar één join nodig.
Toon per Id
van een album hoe veel nummers er op dat album staan. Je hoeft nog niet de titel te tonen van het album. Noem dit script aptunes__0046.sql
. Je kan dit zonder join.
Toon elk nummer naast de titel van het album waarop het nummer staat. Noem dit script aptunes__0047.sql
. Nu heb je twee joins nodig.
Toon hoe veel albums elke gebruiker heeft. Noem dit script aptunes__0048.sql
. Je moet dus naast de naam van elke gebruiker een getal zien staan (het aantal albums dat hij heeft).
Toon alle combinaties van een gebruiker en een album in de collectie van die gebruiker. Toon hierbij ook de datum waarop de gebruiker het album heeft aangekocht. Noem dit script aptunes__0049.sql
. Je moet dus een gebruikersnaam, titel en datum naast elkaar zien.
Toon alle combinaties van gebruikers en favoriete losse nummers. In deze dataset heeft maar één gebruiker een favoriet los nummer, maar je query zou ook moeten werken als er veel meer data is. Noem dit aptunes__0050.sql
. Je moet dus een gebruikersnaam en liedjestitel naast elkaar zien.
Het entity-relationship-model of entity-relationship diagram (ERD) is een of voor het inzichtelijk maken van een . Het is een visuele weergave van de , en regels die gelden of aanwezig zijn in een logisch . Deze diagrammen worden gemaakt bij het ontwerpen van een om inzicht te krijgen in de benodigde informatie en de verbanden tussen de gegevens.
titel
(wat hier staat is niet belangrijk)
Kortjakje
65
'k Zag twee beren
80
Genre
(titel maakt niet uit)
Genre 1
(aantal nummers in genre 1)
Genre 2
(aantal nummers in genre 2)
...
...
1970
...
1990
...
2000
...
één op één
één op n
n op één
n op n
Attributen die horen bij een één op n-, of bij een n op één-relatie, kunnen, indien gewenst, verschoven worden naar de entity set aan de n-kant van de relationship set. Bij een n op n-relatie kan een attribuut dat bij de relatie hoort, niet verschoven worden naar één van de entity-sets. Bij een één op één relatie kan men een attribuut verschuiven naar keuze verschuiven naar een deelnemende entity set.
De "ondergeschikte" entiteit moet steeds horen bij een "dominante" entiteit.
Voorbeeld: de entity set lening en de entity set afbetalingen. Elke afbetaling moet bij een concrete lening horen.
Gevolg: als we een lening uit de database verwijderen, moeten we alle afbetalingen voor die lening mee verwijderen. Het is wel mogelijk om een bepaalde afbetaling te verwijderen, zonder de lening waarvoor ze bedoeld was, mee te verwijderen.
Gevolg: de deelname van de entity set afbetalingen aan de relatie leningen-afbetalingen, is "totaal". De deelname van de entity set leningen aan de relatie leningen-afbetalingen is "partieel": het is mogelijk een lening te hebben waarvoor (nog) geen afbetalingen gebeurd zijn.
Constraints dwingen verschillende vormen van "integriteit" af.
Cardinaliteit en existence dependencies worden in de eerste plaats gelinkt aan "referentiële integriteit". Dit wil zeggen dat entiteiten op een correcte manier naar elkaar verwijzen. Het is om referentiële integriteit te bewaren dat een database management systeem niet toestaat dat je een vreemde sleutelwaarde ingeeft die verwijst naar een niet-bestaande entiteit. Let op: het is vaak wel mogelijk om een vreemde sleutelwaarde NULL
te gebruiken.
Breder dan alleen referentiële integriteit is data-integriteit. Dit wil zeggen dat de data geen ongeldige waarden bevat. Niets kan je volledig beschermen tegen foutieve invoer, maar (onder andere) volgende types checks bieden een betere data-integriteit:
checks op NULL
checks op toegelaten waarden
Er zijn nog specifieke vormen van data-integriteit naast referentiële integriteit. Deze worden behandeld in de cursus Databanken.
voorbeeld: neem de entity set "employee" en het attribuut "telephonenumber". Men kan evengoed twee entity sets maken, "employee" en "telephone" met daartussen een relationship set.
"Telephone" kan dan bovendien extra attributen krijgen (plaats, type, kleur,...).
Voordeel van afzonderlijke entity set:
aantal is niet naar onder of boven begrensd
telefoon kan gemakkelijker toegewezen worden aan verschillende personen tegelijk
eenvoudig toevoegen van extra attributen
Voordelen van attribuut: eenvoud
Opmerking: een zwakke entity set is steeds equivalent met een samengesteld, herhalend attribuut van de overeenkomstige sterke entity set.
Voorbeeld: een lening kunnen we zien als een entity set op zich, maar ook als een relationship set tussen klanten en filialen, met ln_nr en bedrag als attributen van de relationship set.
Nadelen: als de relationship set geen 1 op 1-relatie is, en attributen heeft, dan ontstaat duplicatie van gegevens.
In het geval een begrip duidelijk twee entiteittypes verbindt, maar ook eigen attributen bezit, wordt gekozen voor een "associatieve entiteit". Een voorbeeld hiervan kan bijvoorbeeld "lesmoment" zijn, wanneer er entiteittypes voor leerkrachten en voor vakken zijn. Een lesmoment verbindt een leerkracht en een vak, maar voegt toe op welk tijdstip de leerkracht in kwestie het vak geeft.
Een relationship tussen meer dan twee entity sets kan steeds herleid worden tot meerdere relationship sets die binaire zijn (dwz. tussen twee entity sets).
Elk E-R Diagram kan steeds herleid worden tot een aantal tabellen. Deze herleiding kan de basis vormen van het ontwerp voor een relationele database.
De namen van de enkelvoudige attributen vormen de titels voor de kolommen in een tabel.
Elke rij is de voorstelling van een entity uit de entity set, met in elke kolom de waarde voor het betreffende attribuut.
Berekenbare attributen schrappen we.
Samengestelde attributen splitsen we eerst op in enkelvoudige attributen.
Herhalende attributen plaatsen we in een aparte tabel. We zullen deze tabel van een vreemde sleutel voorzien om naar de tabel voor het entiteittype te verwijzen.
De attributen die de sleutel van de entity set vormen, worden ook de sleutel voor de tabel.
Wanneer we SQL schrijven, zullen we typisch een eenvoudig sleutelattribuut Id
voorzien omdat dat makkelijker werkt, maar wanneer we met ERD's bezig zijn, proberen we eerst een sleutel te vormen aan de hand van een verzameling attributen.
Als kolomtitels nemen we de attributen van de zwakke entity set, aangevuld met (de attributen van) de primaire sleutel van de bijhorende sterke entity set.
Samengestelde en herhalende attributen handelen we af zoals eerder.
Als kolomtitels nemen we de attributen van de sleutels van de participerende entity sets, aangevuld met de attributen van de relationship zelf.
Om botsingen te vermijden gebruiken we de naam van de participerende entitye set als prefix, bijvoorbeeld Klant_Nummer
en Bank_Nummer
in een relatie KlantBij
.
De tabel die de relatie voorstelt tussen een sterke en een zwakke entity set, is reeds vervat in de tabel die de zwakke entity set voorstelt. We kunnen die tabel weglaten.
Als de cardinaliteit van een relationship set 1 op n is, kunnen we de tabel die de relationship set voorstelt weglaten, door de sleutel van de tabel aan de 1-kant op te nemen als foreign key in de tabel aan de n-kant.
Als de cardinaliteit van een relationship set 1 op 1 is, kunnen we kiezen in welke tabel we een foreign key opnemen.
Eventuele attributen van de relationship set zelf moeten mee opgenomen worden in de tabel waar de foreign key geplaatst wordt.
Als de relatie recursief was (d.w.z. tussen entiteiten van hetzelfde type, bijvoorbeeld om uit te drukken dat een persoon baas is over een andere persoon), moet de titel van de foreign key-kolom verwijzen naar de rolnaam.
Totale participatie: dan mag de foreign key niet onbepaald (NULL
) zijn.
Berekenbare attributen moeten we schrappen.
Samengestelde attributen moeten we opsplitsen in enkelvoudige.
Voor een herhalend attribuut maken we een nieuwe tabel aan, bestaande uit de primaire sleutel van de oorspronkelijke entity set en het herhalende attribuut.
Er is geen "beste", algemene oplossing. Men kan wel verschillende strategieën volgen:
Eén tabel voor de algemene entity set, met enkel de algemene attributen; per gespecialiseerde entity set een nieuwe tabel bestaande uit de sleutel van de algemene entity set, en de gespecialiseerde attributen.
Op deze manier kan men niet garanderen dat de specialisatie gescheiden of totaal is.
Eén tabel met kolommen voor alle attributen die voorkomen; praktisch zal dit veronderstellen dat er een bepaald attribuut bestaat dat aanduidt tot welke gespecialiseerde entity set een entiteit behoort.
Nadeel: er kunnen veel onbepaalde waarden zijn, en men moet hun aanwezigheid programmatorisch afdwingen.
Eén tabel voor elk van de gespecialiseerde entity sets, zonder tabel voor de algemene entity set; de tabellen van de gespecialiseerde entity sets bevatten naast de gespecialiseerde attributen, alle algemene attributen.
Dit kan alleen als de specialisatie totaal is.
Wanneer de specialisatie niet gescheiden is, treedt redundantie op van de gegevens uit de algemene entity set.
Voordeel: alle gegevens over een entiteit zitten samen in één tabel.
Vandenbulcke, J. en Lemahieu, W. O., Databasesystemen voor de praktijk. Zevende druk, Den Haag, Ten Hagen en Stam, 2000, 559 p.
Ullman, Jeffrey D., Principles of Database Systems. Rockville, Computer Science Press, 1982.
Silberschatz, Abrahan; Korth, Henry; Sudanshar, S., Database System Concepts. New York, McGraw-Hill 1996.
Gillenson, Mark L., Fundamentals of database management systems, John Wiley & Sons. Nederlandse vertaling bij Academic Service, 2004.
Van Der Lans, Rick, Introduction to SQL. Addison-Wesley, 2000. Kent, William, ‘A simple guide to five normal forms in relational database theory’. Computing Practices, February 1983, vol. 26, Nr. 2.
Elke entity set moet een verzameling attributen hebben die toelaat om een entity uit de entity set uniek te onderscheiden.
De kleinste verzameling van attributen waarmee dit kan, noemen we de "sleutel".
Er kunnen meerdere sleutels mogelijk zijn, we noemen die dan "kandidaat-sleutels".
We kiezen dan een "primaire sleutel" uit deze kandidaat-sleutels.
Een relatie tussen twee entity sets wordt uniek bepaald door de sleutels van elk van de twee entiteiten waartussen de relatie ligt. Voorbeeld: (klantnummer, rekeningnr) is een sleutel voor de relatie "houder van" tussen klanten en rekeningen.
Indien we een één op n-relatie hebben, volstaat de sleutel van de entity set aan de n-kant
Als we een één op één-relatie hebben, volstaat de sleutel van een van beide entity sets naar keuze.
De sleutel van een relationship set wordt niet aangeduid in het diagram.
Als een entity set onvoldoende attributen heeft om een sleutel te vormen, noemen we de entity set "zwak".
Dit kan alleen als de zwakke entity set ondergeschikt is aan een dominante sterke entity set.
De kleinste verzameling attributen die toelaat om onderscheid te maken tussen de verschillende ondergeschikte entiteiten van dezelfde dominante entiteit, noemen we de "discriminator".
De sleutel van de zwakke entity set kan dan gevormd worden door de primaire sleutel van de sterke entity set, en de "discriminator" van de zwakke entity set.
Opmerking:existence dependency leidt niet automatisch tot een zwakke entity set.
Een voorbeeld is een entity type Hotelkamer
met een kolom Nummer
. Nummer
op zich kan niet als sleutel dienen. Een entiteit van dit type kan enkel uniek geïdentificeerd worden wanneer ze gelinkt wordt aan een hotel.
Entity (entiteit): een reëel "ding" of "object", dat we kunnen onderscheiden van alle andere dingen of objecten.
Entiteiten kunnen concreet zijn (personeelslid, klant, product,...) of abstract (lening, afspraak, op te nemen verlofdag,...)
Een "entity set" (letterlijk vertaald: verzameling van entiteiten, misschien beter: "entiteitstype") beschrijft alle mogelijke entiteiten van dezelfde soort.
De concrete samenstelling van een entity set zal regelmatig wijzigen (personeelsleden bij of weg, klanten
Voorstelling:
Een entiteit wordt voorgesteld door een verzameling "attributes", beschrijvende eigenschappen, die gedeeld worden door alle entiteiten uit de gegeven entity set.
Het feit dat alle entiteiten een zelfde attribuut hebben, wijst op informatie van hetzelfde type (gewicht, kleur, personeelsnummer,...). Maar elke entiteit kan wel een andere "value" (waarde) hebben voor dit attribuut (65Kg, groen, INF7089,...).
De toegelaten values voor een gegeven attribuut uit een entity set, vormen het "domain" (domein).
Attributen kunnen samengesteld zijn. Bv. adres kan als een samengesteld attribuut gezien worden.
Attributen kunnen herhalend zijn. Bv. telefoonnummer, persoon ten laste,...
Null value. Dit is een speciale waarde, gebruikt voor "onbekend" of "niet toepasbaar". De null is verschillend van een 0 (nul) of een lege string ("").
Berekenbaar attribuut: bv. als het attribuut geboortedatum bestaat, kan het attribuut leeftijd berekend worden uit geboortedatum en huidige datum.
Voorstelling:
Een relation is een verband tussen verschillende entiteiten. bv. de relatie "ontlener" tussen de entiteit "Janssens" uit de entity set klanten en de entiteit lening nr 0001 uit de entity set leningen.
Een relationship set (relatie) is de verzameling van alle gelijkaardige relaties tussen entiteiten uit dezelfde entity sets.
In bovenstaande relatie speelt Janssens de rol van "lener" en 0001 de rol van "lening". Rollen zijn alleen echt van belang bij relaties van een entity set naar zichzelf, "recursieve" relaties.
Voorbeeld: de relatie "is manager van" van de entity set "personeel" naar zichzelf.
We moeten hier een onderscheid kunnen maken tussen de entiteit die de rol van manager heeft, en de entiteit(en) die de rol van "gemanaged personeelslid" hebben. Ander voorbeeld: de relatie "is een onderdeel van" in de entity set "wisselstukken".
Een relatie kan eveneens attributen hebben.
Voorbeeld: de relatie "houder" tussen "klanten" en "rekening" kan het attribuut "laatste verwerking" hebben, met de datum van de laatste opname of storting.
Relaties hoeven niet steeds binair te zijn, dwz. tussen twee entity sets. Er zijn ook relaties mogelijk tussen drie, vier,... entity sets. Dit noemt men de "graad" van een relationship set.
Voorstelling:
Let op! Deze tool ondersteunt verschillende notaties! Gebruik de icoontjes vanaf dat voor "Entity" tot en met dat voor "Recursive Relationship (M:N)". Deze notatie lijkt sterk op die in [dit voorbeeld op Wikipedia], maar gebruikt stippellijnen om entiteittypes en relatietypes te verbinden (met als uitleg "optional participation") wanneer het voorbeeld op Wikipedia gewone lijnen gebruikt. Het gebruikt volle lijnen waar het voorbeeld op Wikipedia dubbele lijnen gebruikt 😩.
Op draw.io trek je een stippellijn tussen een entiteittype en een relatietype als een entiteit niet hoeft deel te nemen aan een relatie. Bijvoorbeeld: regio in een game hoeft geen personages te bevatten, want het kan zijn dat alle spelers in een andere regio zitten. Langs de andere kant bevindt elk personage zich altijd wel in een regio, dus je trekt op draw.io een volle lijn tussen het entiteittype voor personages en het relatietype dat personages en regio's verbindt.
Afspraken:
Alle tekstattributen (voornamen, familienamen, namen van vakken en opleidingen) definieer je als VARCHAR(100)
.
De semesters definieer je als TINYINT UNSIGNED
.
Alle attributen op dit schema zijn verplicht.
Gebruik de onderlijnde attributen als primary keys. Studentennummer en personeelsnummer mag je als automatisch ophogende INT
definiëren; de sleutelattributen Naam
definieer je als tekst met daarop de constraint PRIMARY KEY
. In dit geval hoef je de primary key dus niet Id
te noemen.
Vreemde sleutels volgen dezelfde afspraken als eerder.
Noem het script dat deze tabellen toevoegt 0564__Oefening.sql.
Teken een entity relationship diagram (ERD) voor volgende situatie.
Vakantieclub SummerParks wil zijn klanten regelmatig mailen met promoties en informatie over vakantieverblijven, en dit gericht op het voorkeurprofiel van de klant.
Van elke klant houden ze naam, voornaam, adres, telefoon en een uniek klantennummer bij.
Van elk vakantieverblijf houden ze naam, regio, prijscategorie en profielen (sportief, kindvriendelijk, cultureel, ...) bij. Er is ook een unieke code per vakantieverblijf.
Van elke klant houden ze ook bij welk profiel zijn voorkeur heeft.
Teken een entity relationship diagram (ERD) voor volgende situatie.
In een bedrijf moeten personeelsleden regelmatig vergaderingen organiseren. Hiervoor zijn een aantal lokalen voorzien. Men moet deze vooraf reserveren. Men kan ook bepaalde apparatuur (beamers, overheadprojectoren, laptops, geluidsinstallatie,...) lenen.
Elk lokaal heeft een uniek lokaalnummer en een omschrijving. Per lokaal houden we verder ook bij voor hoeveel personen het maximaal geschikt is, en in welke bedrijfsvestiging het lokaal gelegen is.
Elk apparaat heeft een uniek nummer en een omschrijving. We houden ook het serienummer van de producent bij en een aankoopwaarde.
Personeelsleden hebben een uniek personeelsnummer, een naam en voornaam, en een telefoonnummer. We houden ook bij op welke dienst ze werkzaam zijn.
Het secretariaat wil voor elke reservatie bijhouden wie welk lokaal en welke apparatuur gereserveerd heeft, de datum met begin- en eindtijdstip, en een korte motivatie.
Voor apparatuur moet de ontlener bovendien een borg moet betalen bij het secretariaat. De borg is voor elk afzonderlijk apparaat. Het secretariaat ontvangt en registreert de borg. Bij terugbrengen van het apparaat krijgt men de borg terug. Ook die terugbetaling wordt geregistreerd.
Informatisering van een bank
Onze bank heeft verschillende filialen, één per stad. Elk filiaal heeft een unieke naam. De bank houdt van elk filiaal bij, welke eigen bezittingen het heeft.
Klanten van de bank hebben een uniek nummer. De bank bewaart volgende gegevens over haar klanten: naam, adres.
Klanten kunnen rekeningen openen bij de bank, of leningen afsluiten.
Sommige klanten hebben een persoonlijke contactpersoon bij die bank.
Personeelsleden bij de bank hebben een uniek nummer. De bank bewaart volgende gegevens over haar personeelsleden: naam, adres, telefoon, namen van de personen ten laste, manager van het personeelslid, aanvangsdatum van tewerkstelling, ancieniteit, filiaal van tewerkstelling.
Er zijn twee soorten rekeningen, zichtrekening en spaarrekening.
Elke rekening kan meerdere houders hebben, en een klant kan meerdere rekeningen hebben.
Elke rekening heeft een uniek rekeningnummer. De bank bewaart per rekening volgende gegevens: saldo en datum van laatste verrichting per rekeninghouder.
Voor een spaarrekening wordt ook de interestvoet bewaard, voor een zichtrekening het maximum krediet.
Een lening wordt steeds toegestaan door een bepaald filiaal. Meerdere klanten kunnen samen een lening afsluiten, en één klant kan meerdere leningen hebben.
Een lening wordt gekenmerkt door een uniek nummer. Per lening bewaart de bank volgende gegevens: geleend bedrag en aflossingen. Elke aflossing heeft een uniek volgnummer, althans voor de lening waarvoor ze dient. Een aflossing van een andere lening kan hetzelfde volgnummer hebben. Per aflossing bewaart de bank volgende gegevens: datum, bedrag.
Voorbeeld: zichtrekening en spaarrekening. Deze hebben de gemeenschappelijke attributen van elke rekening (rekeningnummer, saldo, datum laatste verrichting) maar ook een specifiek attribuut: krediet en interestvoet. Dit noemen we "specialisatie". Het omgekeerde proces is generalisatie.
De entity sets zichtrekening en spaarrekening "erven" de attributen van de entity set rekeningen.
attribuut-bepaald: een entity kan tot een specialisatie behoren op basis van een attribuut in de veralgemeende entity set, bv. het attribuut "rekeningstype", dat de waarde "zicht" of "spaar" kan hebben.
user-bepaald: de gebruiker kan zelf een entity indelen in de specialisatie naar keuze.
gescheiden of overlappend: het kan zijn dat een entiteit slechts tot één gespecialiseerde entity set mag behoren, of tot meerdere tegelijk. Het is bijvoorbeeld niet mogelijk dat een rekening tegelijkertijd een spaarrekening én een zichtrekening is, maar dat wil niet zeggen dat dit voor andere entiteittypes ook altijd zo is.
totaal of partieel: het kan zijn dat een entiteit moet behoren tot een gespecialiseerde set, maar het kan ook zijn dat dit niet hoeft.
Aggregatie in een ERM houdt in dat een relatie tussen twee entiteittypes wordt behandeld als een entiteit op zich. Aggregatie wordt oa. gebruikt om relaties tussen relaties te leggen.
Dit is een n op n relatie, het attribuut filiaal hoort dus bij de relationship set, en kan niet verplaatst worden naar een van de entity sets.
Door aggregatie kunnen we het attribuut filiaal verzelfstandigen tot entity set.
In deze laatste afbeelding is er een associatieve entiteit Lening
aanwezig.
Om zelf tekeningen te maken, kan je gebruik maken van . Kies bij het aanmaken voor een blanco diagram en kies dan het onderdeel "Entity Relation".
Zet onderstaand ERD om naar databasetabellen volgens .
Soorten databanken
Hebben een boomstructuur
Dit geeft redelijk wat beperkingen.
Een parent kan meerdere children hebben
Maar een child slechts 1 parent.
Dit leidt tot kopieën en problemen in integriteit
Steeds minder gebruikt
Vb: windows registry
Bestaat uit tabellen die met elkaar gekoppeld worden dmv relaties
Relaties worden gemaakt door ‘sleutels’(keys) Primary Key(PK)
Elke tabel heeft 1 PK (deze identificeert een entry)
Foreign Key(FK) Elke tabel kan 0 of meerdere FK hebben
Deze legt een relatie naar een PK in een andere tabel
Niet bepaald overzichtelijker, maar wel veel flexibeler en efficiënter
Relationele databanken hebben enkele nadelen Schaalbaarheid voor heel grote databanken Opsplitsen van DB in verschillende DB’s werkt moeilijk Complexe SQL code omdat objecten in OO niet overeenkomen met tabellen in DB Bv. personeel(object) heeft adres en bedrijfswagen waarbij de data verspreid zijn over verschillende tabellen
Er zijn 4 soorten no-sql databanken:
Document databank Bv. MongoDB
Key Value databank Bv. Redis
Graph databank Bv. Neo4J
Column databank Bv. Google bigtable
Database bevat ‘documenten’ (json objecten) Bv.: { Id:1, Name: ‘Janssens’ FirstName:’Jan’}
We kunnen queries schrijven, maar niet in SQL Query taal afhankelijk van databank Bv.: db.Klant.Find(Name:’Janssens’)
Documenten zullen veel data bevatten Bv. in aankoop db zullen alle aankopen in klanten document zitten en niet in aparte tabellen
Als server maximale capaciteit bereikt gewoon een extra server Geen schaal problemen omdat er geen relaties zijn over databanken
Vergelijkbaar met document databank Maar geen queries mogelijk Enkel informatie opvragen via de sleutel(Key) Dit gaat bijzonder snel
Bv.: Twitter geschiedenis
Ontworpen om ‘sociale eigenschappen’ van personen op te slaan, bvb: persoon a is bevriend met persoon b Persoon a houd van product x
Zeer handig voor sociale media
Gebruikt tabellen, rijen en kolommen, maar anders dan een relationele databank.
De namen en formaat van de kolommen kan verschillen van rij tot rij in binnen dezelfde tabel.
Er wordt ook wel eens gesproken over een twee dimenionale key-value store.
NoSQL Vaak worden combinaties gebruikt van meerdere soorten DB’s en dit afhankelijk van het doel.
Dit is geen te kennen leerstof.
Om de integriteit van een database te bewaren, normaliseren we de gegevens in de database. Ruwweg betekent dit dat we tabellen splitsen in kleinere, zinvollere tabellen om ontdubbeling te vermijden. Door de normalisering geraakt de informatie echter verspreid over meer dan één tabel. De gebruiker heeft daar echter niet veel aan. Primaire - en vreemde sleutels zeggen de gebruiker niets. We moeten dus een manier vinden om de informatie weer voor te stellen alsof die uit één tabel komt.
INNER JOIN
: wanneer we gebruik gaan maken van de INNER JOIN zullen de records uit beide tabellen worden gehaald welke aan elkaar zijn verbonden.
LEFT [OUTER] JOIN
: met deze clausule kunnen we alle records ophalen uit de tabel die aan de linkerkant van de JOIN staat gespecificeerd, dus ook de rijen uit de linkse tabel die niet aan de join voorwaarde voldoen worden ook in het resultaat weergegeven.
RIGHT [OUTER] JOIN
: met deze clausule kunnen we alle records ophalen uit de tabel die aan de rechterkant van de JOIN staat gespecificeerd, dus ook de rijen uit de rechtse tabel die niet aan de join voorwaarde voldoen worden ook in het resultaat weergegeven.
FULL [OUTER] JOIN
: met deze clausule kunnen we alle records ophalen uit de tabel die zowel aan de linkerkant en aan de rechterkant van de JOIN staan gespecificeerd, dus ook de rijen uit de linkse en rechtse tabel die niet aan de join voorwaarde voldoen worden ook in het resultaat weergegeven
Als je wil mee volgen, gebruik dan onderstaand calibratiescript. Let op: dit maakt dezelfde tabelstructuur aan als de voorbeelden, maar gebruikt andere data.
De functie COALESCE
(Engels voor "samensmelten") in SQL retourneert de eerste niet-NULL expressie tussen de argumenten. Met deze functie kan je eenvoudig aangeven wat erin een NULL
kolom moet worden getoond:
Ter info, hieronder een lijstje met ranking van de meest populaire databases met bijzondere aandacht voor het database model ().
Verschil tussen een no sql / document database en een relationele database wordt heel duidelijk uitgelegd in volgend filmpje.
Verdiepend Introduction to NoSQL (Martin Fowler)
Als je alle personen wilt tonen ongeacht of ze een boek hebben geschreven of niet kan je een LEFT JOIN
gebruiken. In tegenstelling tot bij INNER JOIN
maakt het een groot verschil of je A LEFT JOIN B
schrijft of B LEFT JOIN A
. Met een LEFT JOIN
worden alle rijen uit de linkse tabel geselecteerd, of er nu een match is of niet op basis van de sleutelkolommen.
Terug naar het voorbeeld, maar dan met LEFT JOIN:
Simone is nu wel geselecteerd. Maar er is geen boek van haar aanwezig in de tabel Boeken
, dus staat Titel
op NULL
.
De LEFT JOIN
wordt ook wel LEFT OUTER JOIN
genoemd. Er is geen verschil.
Deze query retourneert alle records in de rechtse tabel (tabel B) die niet overeenkomen met records in de linkertabel (tabel A). Deze join wordt als volgt geschreven:
De OUTER JOIN
ziet er als volgt uit:
MySQL ondersteunt deze constructie niet, in tegenstelling tot sommige andere databanken. Maar je kan ze wel nabootsen door een (gewone) left join boven een excluding right join te plaatsen met behulp van UNION ALL
, dat resultaten samen neemt:
Selecteer dezelfde kolommen uit beide joins! Het is mogelijk om dat niet te doen, maar dan krijg je mogelijk onzinnige resultaten.
Je kan ook een left excluding join combineren met een gewone right join.
Er wordt ook wel gesproken over FULL OUTER JOIN
.
Deze is in het eerste semester al aan bod gekomen. We herhalen hem hier en we introduceren een Venn diagram als visueel geheugensteuntje. Let op: het Venn diagram mag je niet lezen als twee verzamelingen records en hun doorsnede. Het heeft hier een andere betekenis!
Lees ook de commentaar in elk gegeven stukje code! Deze bevat zeer belangrijke informatie.
We combineren informatie uit twee tabellen: de linkertabel (A
) en de rechtertabel (B
). We zeggen dat er overlapping is tussen de tabellen als de gebruikte sleutelkolommen dezelfde waarde bevatten. Het stuk van A
dat we bekijken (A.Id
) matcht met het stuk van B
dat we bekijken (B.A_Id
) en dat tekenen we als een overlappend gedeelte.
Het diagram met de naam A
staat voor de tabel Boeken
en met de naam B voor Personen
. We veronderstellen dat één auteur meerdere boeken kan hebben maar niet omgekeerd, dus de vreemde sleutel moet wel in Boeken
staan. Bij elk boek staat dus een nummer dat uitdrukt: "dit boek is geschreven door persoon nummer ..."
Selecteer alle boeken en toon de voornaam en de familienaam van de auteur.
Elke rij uit de tabel Boeken
wordt gecombineerd met elke rij uit de tabel Personen
en de combinaties waarin een match wordt gevonden tussen Personen_Id
en Personen.Id
worden overgehouden. Indien er geen match is wordt de gecombineerde rij genegeerd.
Het resultaat is een nieuwe tabel, dus je kan er verder mee werken zoals je altijd gedaan hebt. We ordenen de lijst op Familienaam
, Voornaam
, Titel
.
Je mag trouwens na FROM
een haakje openen en voor ORDER BY
het haakje weer sluiten als je dat makkelijker vindt.
We voegen een nieuwe persoon toe in de tabel Personen
:
We selecteren alle personen geordend op Familienaam
en Voornaam
:
En we zien dat Simone De Beauvoir is toegevoegd.
Alle auteurs en hun boeken te selecteren, maar ordenen de lijst nu op Familienaam
, Voornaam
en Titel
:
Nu merken we dat Simone De Beauvoir niet meer in de lijst voorkomt. Dat komt doordat er voor Simone de Beauvoir geen boeken in de boekentabel zijn opgenomen.
Een INNER JOIN
gaat alleen die personen tonen waarvoor een match in de tabel Boeken wordt gevonden.
Deze query retourneert alle records in de linkertabel (tabel A) en alle records in de rechtertabel (tabel B) die niet overeenkomen. Net als de gewone full outer join is deze standaard niet beschikbaar in MySQL, maar je kan hem krijgen door een left excluding join en een right excluding join te combineren. De code daarvan zou je intussen zelf moeten kunnen schrijven op basis van die voor de gewone outer join.
Deze query retourneert alle records in de linkertabel (tabel A) die niet overeenkomen met records in de rechtse tabel (tabel B). Eventuele kolommen uit de tabel B die vermeld worden in de select list bevatten sowieso de waarde NULL
. Deze join wordt als volgt geschreven:
Syntax
Als ik de tabellen in de select statement switch en Boeken
links en Personen
rechts zet, gaat Simone de Beauvoir getoond worden?
Geen Simone de Beauvoir te zien. Hoe kan ik Simone tonen (zonder de volgorde opnieuw te switchen naar het origineel):
Door een RIGHT JOIN
:
Simone De Beauvoir wordt getoond en de coalesce functie doet haar werk:
Hier merk je wel dat de derde kolomhoofding niet elegant is. Je kan die zelf opgeven in het select statement door een alias mee te geven:
Onderstaande flowchart kan je helpen de juiste JOIN toe te passen.
Een view is een zogeheten virtuele tabel die gebaseerd is op het resultaat van een query.
Een view zou je dus als een zogeheten opgeslagen query kunnen bekijken. De data van een view worden evenwel niet opgeslagen in de database, de gegevens worden telkens opnieuw uit tabellen opgehaald op basis van de query en dit wanneer betreffende view wordt geconsulteerd.
Een view, gebaseerd op een query, kun je op exact dezelfde manier benaderen als een gewone tabel en je kan hierbij tevens join statements hanteren. M.a.w. wanneer je een view hebt gecreëerd, kan je deze view benaderen als een gewone tabel en een nieuwe query schrijven die bv. data uit de view en een andere 'echte' tabel haalt.
Een view bevat rijen en kolommen zoals een 'echte' tabel.
Een view is dus het resultaat van een query en kan kolommen bevatten dewelke afkomstig zijn uit verschillende tabellen.
Views gebruiken de sleutelwoorden die we kennen uit de DDL. Ze worden gecreëerd als "virtuele tabellen", dus het is logisch dat ze syntax gebruiken die gelijkaardig is aan die voor het creëren van gewone tabellen.
We baseren ons voorbeeld op onderstaande tabellen Taken
en Leden
.
Taken
omschrijving
Id
Leden_Id
bestek voorzien
1
2
frisdrank meebrengen
2
1
aardappelsla maken
3
3
Leden
voornaam
Id
Yannick
1
Bavo
2
Max
3
Het uitganspunt van een view is bepaalde data die afkomstig is uit verschillende tabellen samen te brengen en te benaderen als één nieuwe virtuele tabel.
Onderstaande query haalt bepaalde data op uit de tabellen Taken
en Leden
.
Het resultaat van deze query is:
voornaam
omschrijving
Yannick
frisdrank meebrengen
Bavo
bestek voorzien
Max
aardappelsla maken
Als we het resultaat van bovenstaande query willen opslaan in een view, doen we dit op volgende manier:
Eens je bovenstaande query CREATE VIEW
hebt uitgevoerd wordt de view bewaard in de database.
In MySQL Workbench kan je een view vinden onder deze rubriek:
Nu kan je deze view bevragen zoals een gewone tabel.
Wel belangrijk te weten dat een view de data niet fysisch bevat, maar telkens de query zal uitvoeren. Dat kan ons verder een heleboel schrijfwerk besparen bij complexe queries.
Start vanaf onderstaand script, 0500__CalibrateDB.sql:
Merk op dat er tabellen zijn met de namen Boeken
en tabel Auteurs
. Gebruik volgende informatie om de tabel BoekenNaarAuteurs
in te vullen met 0501__Oefening.sql:
Haruki Murakami schreef Norwegian Wood en Kafka on the Shore
Neil Gaiman schreef American Gods en The Ocean at the End of the Lane
Terry Pratchett en Neil Gaiman schreven samen Good Omens
Noem volgend script 0502__Oefening.sql en voer uit:
Noem volgend script het 0503__Oefening.sql en voer uit. Je hoeft het niet helemaal te begrijpen.:
Schrijf zelf een tabel, Uitleningen
, die leden koppelt aan boeken die ze hebben uitgeleend in de bibliotheek. Een uitlening heeft een verplichte startdatum en eventueel (dus niet verplicht) een einddatum. Deze datums stel je voor met het DATE
-datatype. Noem het script dat deze tabel aanmaakt 0504__Oefening.sql.
Schrijf een script, 0505__Oefening.sql, dat volgende informatie in de databank plaatst:
Max heeft Norwegian Wood geleend van 1 februari 2019 tot 15 februari 2019.
Bavo heeft Norwegian Wood geleend van 16 februari 2019 tot 2 maart 2019.
Bavo heeft Pet Sematary geleend van 16 februari 2019 tot 2 maart 2019.
Yannick heeft Pet Sematary geleend van 1 mei 2019 en heeft het boek nog niet teruggebracht.
We hebben een tabel die bijhoudt wanneer een bepaalde videogame is uitgebracht op een bepaald platform. Die ziet er zo uit:
Games.Id
Platformen.Id
Releasedatum
1
1
22 februari 2019
1
2
22 februari 2019
1
3
22 februari 2019
2
1
22 maart 2019
2
2
22 maart 2019
2
3
22 maart 2019
3
1
8 maart 2019
3
2
8 maart 2019
4
1
2 oktober 2018
4
2
2 oktober 2018
4
3
2 oktober 2018
4
4
2 oktober 2018
Als we deze tabel willen ontcijferen, d.w.z. als we de namen van de games en de platformen willen zien, combineren we deze tabel Releases
eerst met de tabel Platformen
en vervolgens met de tabel games
. Dat ziet er zo uit:
Toon nu zelf het overzicht van wie welk boek heeft uitgeleend en in welke periode. Doe dit in script 0506__Oefening.sql. Gebruik coalesce
om duidelijk aan te geven wanneer een boek nog niet is teruggebracht en zorg dat de kolomtitel leesbaar is met as
.
Persoon
Boek
Startdatum
Einddatum
Max
Norwegian Wood
1 februari 2019
15 februari 2019
...
...
...
...
Yannick
Pet Sematary
1 mei 2019
nog niet teruggebracht
Toon alle taken, met het lid dat de taak uitvoert. Als de taak door niemand wordt uitgevoerd, staat er "taak niet toegewezen". Maak voor jezelf eerst de tekening met overlappende cirkels zodat je weet hoe je dit kan uitschrijven, zoals in de theorie. Gebruik COALESCE
en AS om
je output leesbaar te maken, zoals in de voorbeeldoutput hieronder:
Voornaam
Omschrijving
Yannick
frisdrank meebrengen
Bavo
bestek voorzien
Max
aardappelsla maken
Taak niet toegewezen
papieren bordjes meebrengen
Noem het script 0507__Oefening.sql.
Toon alle titels van games met hun bijbehorend platform, als er een is. Toon ook games waarvoor het platform niet meer ondersteund wordt (d.w.z. waarvoor geen info in Releases
staat). Gebruik hiervoor een samenstelling van twee JOINs. Gebruik opnieuw COALESCE
en AS
voor mooiere output. Noem het script 0508__Oefening.sql.
Voorbeeldoutput:
Titel
Naam
Anthem
PS4
Anthem
XBox one
Anthem
Windows
...
...
Mega Man 11
Switch
Oregon Trail
Platform niet meer ondersteund
Er is een tabel Taken
en een tabel Leden
. Bij taken staat (door middel van een verwijzing) welk lid een bepaalde taak uitvoert. Toon nu alle taken die niet aan iemand zijn toegewezen. Noem je script 0509__Oefening.sql. (Doe dit met stof die tot hiertoe aan bod is gekomen, niet met constructies die we nog niet gezien hebben.)
Toon alle platformen waarvoor games beschikbaar zijn. Een platform wordt dus niet getoond als er geen games beschikbaar voor zijn, maar verschijnt zodra er minstens één game voor is. Noem je script 0510__Oefening.sql. (Doe dit met stof die tot hiertoe aan bod is gekomen, niet met constructies die we nog niet gezien hebben.)
Toon alle games waarvoor het platform onbekend is, samen met alle platformen waarvoor geen games beschikbaar zijn. Het resultaat ziet er als volgt uit:
Oregon Trail
Geen platformen gekend
Geen games gekend
Master System
Noem je script 0511__Oefening.sql. (Doe dit met stof die tot hiertoe aan bod is gekomen, niet met constructies die we nog niet gezien hebben.)
Stel: Je wil alle personen selecteren die ouder zijn dan de gemiddelde persoon in je tabel Personen
(met een kolom Leeftijd
). Hoe zou je zoiets aanpakken? Je kan eerst AVG(Leeftijd)
opvragen, noteren en dan een SELECT
met een WHERE
uitvoeren. Het nadeel hiervan is dat hier twee queries moeten plaatsvinden en dat jij als programmeur het resultaat van die eerste query moet verwerken in de tweede. In dit onderdeel bekijken we een oplossing voor dit probleem en gelijkaardige problemen: geneste queries (ook wel subqueries) genoemd.
Een subquery is een query die wordt uitgevoerd als onderdeel van een grotere query. Deze kleinere query zal geen zichtbare uitvoer produceren, maar zal wel informatie leveren die je anders met de hand had moeten intypen in de grotere query. Dat laatste is vaak niet eens haalbaar!
Views bieden heel wat voordelen, o.a.
Vereenvoudiging van complexe queries Views zijn vooral nuttig ingeval je vaak eenzelfde complexe query moet uitvoeren.
Business logica blijft consistent Indien je een query hebt die een complexe logica bevat, kunnen views helpen deze logica consistent te maken en de complexiteit zogeheten te verbergen.
Afscherming van gevoelige data Als een of meer tabellen gevoelige data bevatten, dan kan je opteren om enkel de relevante en niet gevoelige data in een view op te slaan. Je kan bepaalde gebruikers dan ook toegang geven tot enkel de views, maar niet tot de oorspronkelijke tabellen.
Onafhankelijke subqueries zijn subqueries die je op zich kan uitschrijven en uitvoeren. Dat is handig, want het maakt onafhankelijke subqueries makkelijk te testen. Dit zijn de enige soorten die wij in deze cursus zullen behandelen.
Veronderstel je hebt een tabel Werknemers
van volgende vorm:
Id
Voornaam
Familienaam
Kantoornummer
1
Peter
Spaas
04.04
2
Vincent
Nys
03.128
3
Jef
Inghelbrecht
04.03
Stel dat je ook een tabel Kantoorruimtes
hebt van deze vorm. Beamer
geeft aan of er een beamer geïnstalleerd is in het kantoor:
Id
Kantoornummer
Beamer
1
04.04
0
2
03.128
1
3
04.03
1
Wat als je nu wil weten wie er in een kantoor zit waarin een beamer beschikbaar is? Dan is een geneste query een optie:
Start van binnen (de haakjes) en kijk dan naar buiten: select Kantoornummer from Kantoorruimtes where Beamer
toont je alle nummers van kantoren met een beamer. Het sleutelwoordje in
ken je: het zegt of een waarde aanwezig is in een lijst met waarden. In dit geval is die lijst het resultaat van de geneste query. Op deze manier krijgen we te zien wie in een kantoor met beamer zit. We noemen select Kantoornummer from Kantoorruimtes where Beamer
een geneste query of subquery omdat het op zich een SQL-query is, maar wel een die ingebed is in een grotere query. Meerbepaald gaat het om een onafhankelijke subquery: een subquery die je zo kan uitvoeren. Inderdaad, als je een tabel Kantoorruimtes
hebt, kan je select Kantoornummer from Kantoorruimtes where Beamer
op zich schrijven.
Wat je hier ziet is trouwens een algemene regel: subqueries zetten we tussen haakjes!
Het is mogelijk om de naam van de view te wijzigen.
Je kent het principe van een index waarschijnlijk uit alledaagse situaties.
In studieboeken staat vaak achteraan een alfabetisch geordende woordenlijst met paginanummers. Hiermee raak je veel sneller aan de gewenste informatie dan wanneer je een boek woord per woord leest vanaf het begin.
In het Engels wordt het woord "index" soms gebruikt om een telefoongids aan te duiden. Hiermee kan je heel snel iemand terugvinden als je de juiste plaatsnaam kent en een stukje informatie over de gezochte persoon kent (vaak een achternaam of beroep).
Indexen kunnen ook gebruikt worden om heel snel een databasetabel te doorlopen. Net zoals je zelf tijd verliest door altijd vanaf de eerste pagina te beginnen lezen, verliest een database engine tijd door altijd elk databaserecord te controleren.
We maken database indexen aan om deze inefficiëntie te vermijden. Net als woordenlijsten of telefoonboeken zijn ze gespecialiseerd in slechts een stukje van de aanwezige informatie, maar maken ze het wel mogelijk sneller informatie terug te vinden. Door op een verstandige manier om te springen met indexen kunnen we onze database veel efficiënter gebruiken zonder te investeren in sterkere hardware.
In eerste instantie is een tabel geïndexeerd op basis van zijn primaire sleutel, maar we kunnen ook indexeren op andere kolommen of zelfs op een combinatie van kolommen.
Indexeren verhoogt de snelheid waarmee gegevens worden geselecteerd, maar vertraagt de werking bij het opslaan van gegevens. Wanneer gegevens worden toegevoegd, wordt niet alleen de tabel bijgewerkt, maar moet ook de index worden bijgewerkt. Vergelijk met de registratie van een nieuwe persoon in een gemeente. Als deze persoon zich aanmeldt op het stadhuis en er wordt een telefoonboek bijgehouden, moeten de gegevens van de personendienst en de gegevens van het telefoonboek worden aangepast. Dat is meer werk dan alleen het aanpassen van de persoonsgegevens.
Een index kun je op elk willekeurig moment creëren/wijzigen/verwijderen, zonder dat dit invloed heeft op de opgeslagen data in de bijhorende tabellen. Anders gesteld: het is niet omdat het telefoonboek niet meer wordt uitgegeven, dat je plots geen telefoonnummer meer hebt.
Start vanaf volgend calibratiescript, 0512__Calibratie.sql
:
Maak een view aan met naam AuteursBoeken
waarmee je makkelijk een overzicht kan vragen van welke auteur welk(e) boek(en) heeft geschreven.
Je oplossing zou er exact zo moeten uitzien:
Noem het script om deze view aan te maken 0513__Oefening.sql
.
Pas, niet rechtstreeks in de tabel Boeken
maar wel via de view AuteursBoeken
, de titel Pet Sematary aan naar Pet Cemetery.
Noem het script hiervoor 0514__Oefening.sql
.
Wat gebeurt er als je de naam "Stephen King" via deze view in "Steven King" wil veranderen? Waarom, denk je?
We willen een complexere versie van de bestaande view AuteursBoeken
. We zullen hierbij ook de gemiddelde rating van elk boek plaatsen in een uitgebreide versie van de view, namelijk AuteursBoekenRatings
. We zullen dit in stappen doen. Lees eerst de stappen, bekijk dan de figuren, voer dan de stappen uit.
Gebruik een ALTER VIEW
om je bestaande view AuteursBoeken
te voorzien van het Id
uit de tabelBoeken
. Toon Id
hier wel als Boeken_Id
. Noem het script dat de view aanpast 0515__Oefening.sql
.
Maak een view GemiddeldeRatings
aan op basis van Reviews
. Noem de kolom met het gemiddelde Rating
. Noem het script dat de view maakt 0516__Oefening.sql
.
Maak de viewAuteursBoekenRatings
aan door een nieuwe view te maken gebaseerd op AuteursBoeken
en GemiddeldeRatings
. Noem het script dat de view maakt 0517__Oefening.sql
.
Vraag via de metadatabank op welke van je views updatable zijn en welke niet. Schrijf voor de meest complexe van de updatable views een UPDATE
per kolom om te testen of je die kolom kan updaten. Je hoeft het script niet te bewaren, maar je moet in een gelijkaardige situatie (bv. op het examen) wel kunnen zeggen welke kolommen updatable zijn.
Uit de laatste oefening zie je dat er meerdere redenen zijn om een view op te bouwen uit andere views. Enerzijds om stap per stap het resultaat op te bouwen. Anderzijds omdat een view die niet-updatable views bevat zelf toch nog updatable kan zijn. Uiteraard zijn niet alle kolommen dan noodzakelijk updatable, maar het kan beter zijn dan het opbouwen van één grote niet-updatable view.
Views kunnen onder bepaalde omstandigheden worden aangepast, ttz. de inhoud van de view kan op dezelfde wijze worden benaderd als een gewone tabel.
Er zijn hierbij wel enkele uitzonderingen in die zin dat bij de creatie van de view de query o.a. geen van volgende statements mag bevatten.
MIN
, MAX
, SUM
, AVG
en COUNT
DISTINCT
GROUP BY
HAVING
UNION
LEFT JOIN
of RIGHT JOIN
(en dus ook de EXCLUDING
versies en OUTER JOIN
via de workaround)
Merk op: INNER JOIN
mag wel!
Waarom zijn juist deze clausules niet toegelaten? De meeste ervan groeperen informatie op zo'n manier dat je de groepering niet altijd ongedaan kan maken. De niet-toegelaten JOIN
s combineren informatie met NULL
-waarden. Het zou niet zinvol zijn deze NULL
-waarden aan te passen, want de aanpassingen zouden verloren gaan bij het herberekenen van de view.
Inspecteer TakenLeden
en Leden
nadat je deze opdracht hebt uitgevoerd. Wat zie je?
In een updatable view kan bepaald worden waar dergelijke aanpassingen echt moeten plaatsvinden. Volgende figuur toont het idee:
Er zijn helaas grenzen aan wat mogelijk is. Experimenteer met enkele DML-operaties met TakenLeden
. Probeer o.a. een INSERT
en een DELETE
van telkens één rij.
Je kan nagaan of een view aanpasbaar is door in de metadatabank te kijken.
Start vanaf het gegeven calibratiescript, 0526__CalibrateDB.sql
. Dit vult de database ApDB
in met onderstaande structuren:
De database die je hier krijgt is niet elegant ontworpen, maar ze maakt de subqueries die we hier zoeken makkelijker uit te schrijven dan een beter opgesplitste database.
Schrijf een script, 0527__Oefening.sql
, dat je alle voornamen van studenten toont die korter zijn dan gemiddeld.
Het formaat is:
Schrijf een script, 0528__Oefening.sql
, dat je al de recentste evaluaties toont. Dat wil zeggen: alle evaluaties die hebben plaatsgevonden op de laatste dag dat er nog evaluaties waren.
Schrijf een script, 0529__Oefening.sql
, dat je alle studenten (enkel de Id
's) toont die een hoger persoonlijk puntengemiddelde hebben dan het algemene gemiddelde. Het persoonlijk gemiddelde is dus het gemiddelde over alle evaluaties van enkel die student en het algemene gemiddelde is het gemiddelde over alle evaluaties in het algemeen.
Tips:
evaluaties van één student neem je samen door te groeperen op het ID van de student aan wie de evaluatie toebehoort
gebruik daarna having
om het gemiddelde van één student te vergelijken met het algemene gemiddelde
het algemene gemiddelde vraag je me een scalaire query
Schrijf een script, 0530__Oefening.sql
, dat je de voornamen en familienamen toont van alle studenten die aangesloten zijn bij de studentenvereniging. Doe dit zonder gebruik te maken van JOIN. (Tip: een student is aangesloten bij de vereniging als er een rol bestaat die naar die student verwijst...)
Het formaat:
Schrijf een script, 0531__Oefening.sql
, dat je de voornamen toont die voorkomen bij studenten en personeel en directie.
Toon in een script 0532__Oefening.sql
de namen van directieleden die meer verdienen dan om het even welke werknemer, zonder gebruik te maken van max
.
Het formaat:
Toon in een script 0533__Oefening.sql
de namen van directieleden die minder verdienen dan minstens één werknemer, zonder gebruik te maken van max
.
Het formaat:
Toon in een script 0534__Oefening.sql
het laagste puntengemiddelde van alle studenten. Je hoeft de naam van de student er niet bij te tonen.
Het formaat:
Subqueries komen vaak van pas om één waarde te berekenen die dan gebruikt wordt in een grotere query. Wanneer we een query gebruiken om precies één waarde te produceren, noemen we die waarde een scalar.
Volgende query produceert een scalar:
Hier is geen group
by aanwezig, dus het gemiddelde wordt over alle personen beschouwd. We zijn ook alleen geïnteresseerd in de leeftijd. Bijgevolg hebben we uiteindelijk maar één rij en één kolom, dus één uiteindelijk waarde. Met andere woorden: een scalar.
Scalars zijn belangrijk omdat een belangrijke groep subqueries specifiek bedoeld is om één resultaat te produceren. We spreken dan over scalaire subqueries. In oude databasesystemen konden subqueries zelfs alleen gebruikt worden om te vergelijken met een scalar! In nieuwere systemen kan je een scalaire subquery gewoonweg gebruiken waar je een enkele waarde kan gebruiken.
We kunnen een subquery gebruiken om de oudste personen in onze database te tonen, zonder dat we op voorhand weten over welke leeftijdsgroep we het dan hebben.
Als de oudste persoon in onze database 91 jaar oud is, krijgen we info over iedereen die 81 jaar of ouder is. We kunnen ook kijken naar iedereen die "ongeveer de gemiddelde leeftijd" heeft:
Er is hier niets speciaals aan max
of avg
. Je kan ook andere functies gebruiken die je al kent. Je kan ook andere vergelijkingen gebruiken: =
, >
, <
, >=
, <=
, <>
, !=
, <=>
en zelfs LIKE
zijn mogelijk.
Nog een voorbeeldje:
"Studenten die minder hbben behaald dan het gemiddelde" gaat zo, bij een tabel Studenten
met onder andere een kolom Cijfer
:
Je kan in principe gebruik maken van subqueries in om het even welke WHERE
, niet alleen in een WHERE
die hoort bij een SELECT
. Let op, in MySQL heb je hier een belangrijke beperking: je kan de tabel niet aanpassen die je gebruikt voor de geneste SELECT
. Iets als dit, waarbij we de jongste personen willen wissen, gaat dus niet:
Dit kan je wel oplossen door middel van sessievariabelen.
Een sessievariabele is vergelijkbaar met een variabele uit een general purpose programmeertaal zoals C# of TypeScript. Het is met andere woorden een koppeling tussen een naam en een waarde. Je kan een sessievariabele als volgt een waarde geven:
Een sessievariabele begint altijd met @
. Hiermee kan je bovenstaande beperking op subqueries dus ook omzeilen, want je kan het volgende doen:
Wanneer je verbinding met de MySQL-databank verbroken wordt, verdwijnen al je sessievariabelen.
Scalaire subqueries zijn niet de enige subqueries die we hebben. Als je subquery één kolom als resultaat produceert, kan je deze kolom gebruiken als een lijst waarden waarmee je wil vergelijken. De meest gebruikte manier om een waarde en een lijst te vergelijken is door na te gaan of de waarde gewoonweg voorkomt in die lijst. Dat is ook wat we eerder deden met het sleutelwoordje IN
, dus het zal niet verbazen dat IN
gevolgd mag worden door een subquery die een kolom produceert.
Subqueries van bovenstaande vorm kunnen nog meer doen, maar op het eerste gezicht zijn er veel beperkingen waar we hier niet dieper op in gaan. Om hier rond te werken, kan je gebruik maken van temporary tables of tijdelijke tabellen. Deze lijken op sessievariabelen, maar ze dienen niet om een scalar op te slaan. Ze dienen om één of meerdere kolommen tijdelijk op te slaan. Ze verdwijnen ook wanneer je de connectie verbreekt.
Een tijdelijke tabel aanmaken doe je via CREATE TEMPORARY TABLE
. De syntax is verder dezelfde als voor een gewone tabel. Een tijdelijke tabel is ook alleen maar zichtbaar binnen je eigen verbinding. Er is dus geen risico dat iemand anders tegelijk dezelfde naam voor een tijdelijke tabel gebruikt als jij.
Tijdelijke tabellen kunnen complexere queries behapbaar maken. Veronderstel bijvoorbeeld dat we voor een demografische analyse willen weten welke naam gemiddeld de "jongste" is. We willen dus per naam de gemiddelde leeftijd berekenen en daarvan willen we het minimum.
Dit gaat niet:
Dat komt omdat we met onze group by
hebben aangegeven dat we onze functies willen toepassen over groepjes. avg
kan je toepassen op een groepje leeftijden, maar dat levert dan één uitkomst. Daar kan je niet nog een keer min
op toepassen. Je bedoelt dat min
over heel de resulterende kolom moet worden toegepast, maar dat wordt zo niet geïnterpreteerd door MySQL.
Wat wel werkt, is dit:
Hier zijn kortere oplossingen voor, meerbepaald materialized subqueries, maar deze oplossing is makkelijker verstaanbaar en heeft minder beperkingen.
M.a.w. de zijn ook hier te gebruiken (met bepaalde technische beperkingen).
We baseren ons op de view die we onder de rubriek hebben aangemaakt. Deze gebruikt een INNER JOIN
, maar dat verbiedt niet dat we updates doen.
Voornaam
korte voornaam 1
korte voornaam 2
korte voornaam 3
...
Studenten_Id
Vakken_Id
Cijfer
Id 1
Id 4
Cijfer 1
Id 2
Id 5
Cijfer 2
Id 3
Id 6
Cijfer 3
Id
Id 1
Id 2
Id 3
...
...
Voornaam
Familienaam
Voornaam 1
Familienaam 1
Voornaam 2
Familienaam 2
Voornaam 3
Familienaam 3
Voornaam
Voornaam 1
Voornaam 2
Voornaam 3
Voornaam
Familienaam
Voornaam 1
Familienaam 1
Voornaam 2
Familienaam 2
Voornaam 3
Familienaam 3
Voornaam
Familienaam
Voornaam 1
Familienaam 1
Voornaam 2
Familienaam 2
Voornaam 3
Familienaam 3
Gemiddelde
Een cijfer
(Hier staat maar één rij!)
Indexen belasten het systeem als deze niet nodig zijn, daarom is het raadzaam om indexen die niet (meer) nodig zijn te verwijderen. De reden van deze belasting kun je vinden in het feit dat indexen moeten worden bijgewerkt zodra de data in een tabel worden aangepast. Bovendien kun je indexen niet aanpassen of wijzigen, hiervoor moet je de index verwijderen en opnieuw aanmaken volgens de nieuwe ingevingen.
Om een bestaande index te verwijderen van een tabel gebruik je het drop index
statement.
De primary key is een index en door middel van dit commando kan je deze index verwijderen.
Een index toevoegen doe je in het algemene geval met volgende syntax:
Dit maakt een index op de combinatie van de kolommen Column1
en Column2
. Als er maar één kolom is, laat je de tweede gewoon achterwege. Meer dan twee kolommen kan ook.
De volgorde is hier van belang. Het telefoonboek is bijvoorbeeld een soort index op gemeente, familienaam en voornaam. Je kan het alleen maar efficiënt gebruiken als je de eerste kolom kent en het wordt efficiënter naarmate je meer kolommen kent.
Wanneer je een secundaire index voor een kolom maakt, dus een index die niet over de primaire sleutel gaat, slaat MySQL de waarden van de kolommen op in een afzonderlijke gegevensstructuur. Dit zijn structuren waarin je snel kan opzoeken.
In het geval dat de kolommen de tekenreekskolommen zijn (zoals VARCHAR
), zal de index veel schijfruimte in beslag nemen en mogelijk de INSERT-bewerkingen vertragen.
Om dit probleem aan te pakken, kan je met MySQL een index maken voor het leidende deel van de kolomwaarden van de tekenreekskolommen met behulp van de volgende syntax:
Je kan een index op twee manieren creëren.
1. bij creatie van de tabel zelf
2. nadien toevoegen aan een bestaande tabel
In bovenstaande voorbeelden is de lengte het aantal tekens voor de datatypes zoals VARCHAR (en het aantal bytes voor binaire datatypes).
Taken
omschrijving
Id
bestek voorzien
1
frisdrank meebrengen
2
aardappelsla maken
3
...
...
pasta koken
4000
Als we onderstaande query uitvoeren, dan zal heel de tabel worden doorlopen omdat er geen index is bepaald op het veld waarop het zoekcriterium werd ingesteld.
Als we deze query met het EXPLAIN
statement uitvoeren, krijgen we volgende resultaat.
Als je de tabel taken veel bevraagt terwijl je filtert op de omschrijving, is het zinvol om voor deze kolom een index te creëren waardoor de uitvoering een heel stuk sneller zal verlopen.
De grootte van de kolom Omschrijving
is tijdens het ontwerp ingesteld op 50 karakters, maar dat wil niet zeggen dat we in de index 50 karakters per mogelijkheid moeten bijhouden. Voor de index moet je de lengte van het voorvoegsel bepalen. Soms wordt er gezegd dat je dit zo efficiënt mogelijk dient te doen door de prefixlengte zo kort mogelijk te houden. Hier schuilt wel een gevaar in: wanneer de tabel met nieuwe data wordt uitgebreid, is de index misschien niet meer zo uniek. Dat leidt niet tot fouten maar mogelijk wel tot performantieverlies.
Hoe zoek je nu de ideale lengte van de prefix op? Een vuistregel: zorg dat de index meteen naar een uniek resultaat leidt, maar dat hij niet groter is dan nodig om dit te bereiken.
Stap 1: zoek het aantal verschillende waarden in de kolom op
Stap 2: bepaal de maximale lengte van een waarde in de kolom
Stap 3: zoek de minimale lengte waarbij je geen informatie verliest. Dit kan nooit meer zijn dan het resultaat van stap 2. Als de langste kolomwaarde bijvoorbeeld 21 is, start je door te kijken of 20 karakters al dan niet zorgen voor informatieverlies. Via LEFT
krijg je alleen de eerste (aantal) tekens uit een string.
Zodra er minder resultaten zijn, heb je de prefix te kort gekozen. Het antwoord is dus één meer dan de lengte waarbij het aantal resultaten begint te krimpen.
Indien 20 de perfecte lengte van de prefix is, dan gaan we de index opbouwen.
Binnen de schema navigation kan je nu de index zien. Als je nu bovenstaande select-query opnieuw uitvoert zal deze efficiënter en sneller verlopen.
De lengte van het "ideale" prefix kan wijzigen naarmate je meer data toevoegt aan een tabel.
Zelfs als je de ideale prefixlengte gebruikt, houdt het gebruik van een prefix gevolgen in. MySQL kan bij gebruik van een prefix niet garanderen dat twee waarden op dezelfde plaats in de index identiek zijn. Sommige queries kunnen hierdoor trager lopen bij een prefixindex dan bij een index zonder prefix.
Als je een index op meerdere kolommen aanmaakt, is de volgorde van deze kolommen belangrijk. Vergelijk met het telefoonboek. Daar heb je een index op plaatsnaam en familienaam. Dit staat je toe snel iemand terug te vinden. Als je geen informatie kent over hoe de plaatsnamen geordend zijn (stel je voor dat alle gemeentes willekeurig in het telefoonboek stonden in plaats van alfabetisch), moet je toch elke pagina doorzoeken om zeker te zijn dat je de persoon in kwestie niet over het hoofd ziet.
Wat betekent dit in de praktijk? Je kan een index op meerdere kolommen (bijvoorbeeld A, B en C) gebruiken om snel opzoekingen te doen als je werkt met een doorlopende reeks van de eerste kolommen (bv. A of A en B of A, B en C). Maar je kan niet sneller zoeken met deze index als je bijvoorbeeld enkel B en C betrekt in je zoekopdracht.
We gebruiken de MySQL ER-editor om het ER-diagram aan te maken en dit dan vervolgens naar een tabelstructuur om te zetten.
Met de juiste instellingen kan je MySQL queries laten onthouden die langer duren dan een zekere drempelwaarde. Als het gaat om queries die regelmatig terugkomen, is het de moeite waard indexen aan te maken die deze queries kunnen versnellen.
Je ziet in MySQL Workbench hoe lang het duurt om een query uit te voeren:
Je voert de queries in kwestie opnieuw uit, maar je laat ze voorafgaan door het sleutelwoordje EXPLAIN
. Je krijgt dan één rij per doorzochte tabel in een uitgebreide versie van volgend formaat:
table
key
rows
naam van een doorzochte tabel
eventueel gebruikte index
aantal rijen doorzocht
naam van een andere doorzochte tabel
eventueel gebruikte index
aantal rijen doorzocht
Als er voor een index NULL
staat, betekent dat dat alle rijen van de eerste tot de laatste zijn beschouwd, wat typisch erg inefficiënt is (zeker als het aantal rijen groot is). Door een index toe te voegen aan een kolom met een gezochte waarde (bv. een kolom vermeld in een WHERE
), kan je de NULL
wegwerken en het aantal doorzochte rijen drastisch verlagen.
Volgende werkwijze is alleen mogelijk als je script één statement bevat. Laat in dit geval USE <naam-van-je-databank>;
achterwege, want dat is op zich al een statement.
MySQL Workbench heeft ook een grafische weergave van je uitvoeringsplan. Deze kan je bekijken na het uitvoeren van je statement via onderstaande knop:
Als je op een element van het diagram gaat staan met je muis, krijg je ook een technische uitleg over waarom dat onderdeel van je query waarschijnlijk niet efficiënt is. Let op! Het betreft hier nog steeds vuistregels. Een "full table scan" is bijvoorbeeld geen ramp als je tabel erg klein is. Dat wordt ook vermeld in de kleine lettertjes.
Als je een bepaalde SELECT
-query wil optimaliseren, kan je onderstaand stappenplan volgen:
Als er een WHERE
voorkomt in je SELECT
, voorzie dan één index op alle kolommen samen die via AND
verbonden zijn en die rechtstreeks vergeleken worden met een constante waarde.
Voeg aan je index ook de eerste van de volgende mogelijkheden toe die je hieronder krijgt.
a. Een kolom die niet rechtstreeks wordt vergeleken met een waarde, maar wel in een bereik moet liggen (via BETWEEN
, >
, <
, LIKE
waarbij het eerste karakter geen wildcard is)
b. Alle kolommen die in een GROUP BY
worden gebruikt, in de volgorde waarin ze in de GROUP BY
worden vermeld
c. Alle kolommen die in een ORDER BY
worden gebruikt, in de volgorde waarin ze in de ORDER BY
worden vermeld, maar enkel als er geen mix van ASC
en DESC
voorkomt in de ORDER BY
De primary key is een unieke index bij creatie. Er kan echter slechts één primary key per tabel worden gedefinieerd en indien je toch op een andere kolom ook een unieke index wil leggen kan je dit door een index te bepalen die uniek is. Je kan meerdere unieke indexen binnen één tabel hebben, maar slechts één primary key. De data zullen dan niet fysiek opgeslagen zijn in volgorde van deze unieke index, maar er zal wel een hulpstructuur bijgehouden worden die voor elke unieke waarde zegt waar je ze kan terugvinden.
Er zijn twee manieren om een unieke index te creëren, de eerste bij creatie van de tabel of in het tweede geval nadien.
Bij creatie van de tabel doe je dit als volgt.
Deze syntax is algemeen en zegt dat een combinatie van waarden uniek moet zijn. Als je index maar één kolom gebruikt, kan je dit doen met een UNIQUE
constraint op die kolom, bijvoorbeeld:
We creëren een tabel met een primary key en een unieke index.
Met het commando SHOW INDEXES FROM table_name
kan je zien dat de unieke index werd aangemaakt.
Vervolgens voegen wat data toe aan de tabel People
.
Omdat we op mail een unieke index hebben gelegd is het niet meer mogelijk personen toe te voegen met opgave van een mailadres dat al bestaat. Concreet, wanneer we Jef Doe met als mailadres john.doe@modernways.be zouden willen toevoegen, zal dit resulteren in een fout, want het opgegeven mailadres komt reeds voor.
Indexen kunnen nuttig zijn in vaak voorkomende scenario's:
in queries met een WHERE-clause die de waarde van een geïndexeerd attribuut controleert kunnen veel rijen op voorhand worden uitgesloten (bijvoorbeeld WHERE Personen.Voornaam = 'Elias'
sluit al veel mogelijkheden uit)
om snel minima en maxima te bepalen (zoals hoe een woordenlijst vaak alfabetisch is, zijn ook indexen op kolommen met getalwaarden vaak gesorteerd)
om JOIN-operaties op geïndexeerde attributen te versnellen (herinner je dat INNER JOIN ON
in MySQL net hetzelfde doet als een CROSS JOIN WHERE
)
...
Niet alle indexen zijn hetzelfde. Zoals eerder aangegeven kunnen indexen gebaseerd zijn op één kolom, of op meerdere kolommen. Een index met strengere voorwaarde zal typisch een grotere snelheidswinst opleveren dan een index met zwakkere eisen, omdat je sneller weet of je klaar bent met zoeken. Volgende indexen zijn voorzien in MySQL (en stemmen in sommige gevallen overeen met constraints op je data zoals UNIQUE
):
Primaire sleutels: deze heb je meestal sowieso al aangemaakt. Primaire sleutelkolommen worden vaak gebruikt voor zoekoperaties en WHERE-clauses, dus het is logisch om ze automatisch in een index op te nemen. MySQL houdt rijen intern bij in een gesorteerde volgorde op basis van de primaire sleutel en dankzij deze sortering kan er efficiënt gezocht worden.
Unieke indexen: deze kan je definiëren op (verzamelingen van) kolommen met unieke waarden. De data kunnen niet fysiek opgeslagen worden in een volgorde bepaald door deze index (omdat de primaire sleutel de volgorde al bepaalt), maar er kan een extra datastructuur met de unieke waarden worden bijgehouden.
Gewone indexen: deze kan je definiëren op kolommen met waarden die niet noodzakelijk uniek zijn. Ze mogen ook NULL bevatten.
Fulltext indexen: deze dienen voor kolommen die een (lang) blok tekst bevatten. De tekst wordt opgesplitst in individuele woorden, die dan geïndexeerd worden. Zo kan je efficiënt blokken tekst zoeken die één specifiek woord bevatten.
Je zal waarschijnlijk pas indexen nodig hebben als je met grotere databases gaat werken. Langs de andere kant: eens je op dat punt zit, worden enkele zorgvuldig gekozen indexen snel onmisbaar. Het is wanneer je database stilaan onder het gewicht van bepaalde queries begint te kreunen, dat je een echt geïnformeerde beslissing kan nemen. Je doet dit door twee dingen op te meten:
welke soorten queries vaak uitgevoerd worden
hoe "duur" dergelijke queries zijn
Als je weet dat bepaalde queries vaak voorkomen en/of veel zoekwerk doorheen kolommen vereisen, is het zinvol de betrokken kolommen te indexeren op een manier die goed past bij hun typische gebruik. In de praktijk zal je ook de eventuele snelheidswinsten die je indexen opleveren opmeten. Hiervoor gebruik je het EXPLAIN
-statement, dat een overzicht geeft van hoe een bepaald statement is uitgevoerd.
Als twee entiteiten gelinkt zijn, kan een aanpassing van de ene soms een aanpassing van de andere noodzakelijk maken.
Een voorbeeld: een gebruiker van een webshop schrijft zich uit en vraagt (zoals dat ook kan onder GDPR) dat al zijn/haar persoonlijke informatie uit de webshop verwijderd wordt. Dit omvat mogelijk rijen in een tabel Bezorgadressen
(want één gebruiker kan meerdere geregistreerde adressen hebben), Bestellingen
,... Meerbepaald: alle rijen die via een foreign key verwijzen naar de te wissen gebruiker.
Probeer volgende code:
Het lukt om Claude te wissen, maar niet om Edelgard of Dimitri te wissen. Dit komt omdat er (in andere tabellen) nog data met deze records geassocieerd is. Er is hier een referentiële beperking, dit wil zeggen een beperking die opgelegd wordt omdat er een verwijzing tussen data is. Die verhindert dat Edelgard en Dimitri gewist worden. Voor Claude is er geen verwijzende data, dus hij kan gewist worden.
Er zijn in MySQL verschillende veelgebruikte referentiële beperkingen. Deze worden allemaal genoteerd na de defnitie van een foreign key. Ze drukken dus uit wat er moet gebeuren wanneer de waarde waarnaar verwezen wordt wijzigt.
Ze worden allemaal met volgende syntax genoteerd:
of
ON UPDATE RESTRICT
/ ON DELETE RESTRICT
Dit is de defaultoptie in MySQL. Ze betekent dat deze instructies gewoonweg niet toegelaten zijn als er een verwijzing bestaat. Dit is de reden dat het in bovenstaand voorbeeld wel mogelijk is Claude te wissen, maar niet Edelgard of Dimitri. Om dezelfde reden kan men het Id
van Edelgard en Dimitri niet aanpassen.
ON DELETE CASCADE
Dit is geschikt als het geen zin heeft om de gekoppelde data te bewaren. Met deze constraint zou (in het voorbeeld hierboven) een DELETE
van "Edelgard" ook inhouden dat "Adrestia street 1" gewist wordt.
ON UPDATE CASCADE
Dit zorgt ervoor dat een verwijzing mee wijzigt met de brondata. Een aanpassing van het Id
van "Edelgard" van 1 naar 4 zou dan automatisch inhouden dat de kolom Customers_Id
van "Adrestia street 1" mee gewijzigd zou worden naar 4.
ON DELETE SET NULL
Dit kan zinvol zijn als een verwijzing niet noodzakelijk is. Zo zou het adres van een klant bewaard kunnen worden wanneer de klant zelf verwijderd wordt. Merk op dat dit in bovenstaand voorbeeld niet kan werken, omdat Customers_Id
nooit NULL
mag zijn.
ON UPDATE SET NULL
Dit gedraagt zich hetzelfde als ON DELETE SET NULL
, maar dan bij een UPDATE
. Dit is technisch mogelijk, maar zelden zinvol. Ook hier wordt verondersteld dat de foreign key NULL
mag bevatten.
Als je op deze knop klikt, krijg je een diagram waarin wordt uitgelegd welke stappen nodig zijn om je query uit te voeren. Een volledig overzicht van de betekenis van de elementen in zo'n diagram vind je . Je hoeft niet elk onderdeel te kennen, maar bekijk de pagina kort. Belangrijk is dat je rode boxes met daarin "full table scan" zo veel mogelijk vermijdt, zeker als het gaat om tabellen met veel data in.
Bovenstaand stappenplan is afkomstig uit . Dit is een zeer nauwe verwant van MySQL. Als je de pagina bekijkt, zie je dat het laatste woord over indexen nog niet gezegd is. Voor deze cursus volstaat het algoritme, maar als je ooit in je carrière geconfronteerd wordt met performantieproblemen in een database, raadpleeg dan deze uitleg voor je nieuwe (virtuele) hardware aankoopt!
Wat met NULL
-waarden in combinatie met unieke indexen? Zie . De waarden zijn dus niet noodzakelijk 100% uniek, dit in tegenstelling tot sommige andere databasesystemen.
Je vindt een officiële lijst terug van scenario's waarin MySQL zinvol gebruik kan maken van indexen. Let wel op: indexen hebben niet alleen voordelen. Anders zouden we op elk attribuut en op elke combinatie van attributen een index definiëren. Elke index neemt plaats in en moet onderhouden worden, wat betekent dat de schrijfperformantie van het systeem een beetje daalt.