Relationele databases en SQL [Derde, geheel herziene druk, 4e oplage. ed.] 9789039527146, 9039527148 [PDF]


131 45 10MB

Dutch; Flemish Pages [630] Year 2017

Report DMCA / Copyright

DOWNLOAD PDF FILE

Table of contents :
Voorwoord
De software
Opbouw van het boek
Inhoud
1 Relationele databases: structuur
1.1 Informatiesystemen
1.2 Relationele informatiesystemen
1.2.1 SQL-server en SQL-clients
1.2.2 Client/server-architectuur
1.3 Reijnders’ Toetjesboek
Opgave 1.1
1.4 Alle gegevens in één tabel
1.4.1 Eerste stap naar relationele opslagstructuur
1.4.2 Eén tabel met subtabellen
1.4.3 Redundantie
1.4.4 Gecontroleerde redundantie
1.4.5 Herhalende groepen
1.4.6 Omkering van herhalende groepen
1.5 Verbeterde tabelstructuren
1.5.1 Elimineren van de herhalende groep
1.5.2 Elimineren van redundantie
1.5.3 Alternatief: eerst elimineren van redundantie
1.5.4 Elk entiteittype zijn eigen tabel
1.5.5 Normaliseren en standaardiseren
1.5.6 Structuur en populatie
1.5.7 Tabellen en relaties
Opgave 1.2
Opgave 1.3
Opgave 1.4
1.6 Gegevens en informatie
1.6.1 Informatie als verwoording van gegevens
1.6.2 Atomaire informatie
Opgave 1.5
Oefenopgaven
Opgave 1.6
Opgave 1.7
Opgave 1.8
2 Relationele databases: regels
2.1 Beperkingsregels
2.1.1 Optionele en verplichte kolommen
2.1.2 Uniciteit
2.1.3 Illustratieve populaties
2.1.4 Identificatie en verwijzing
2.1.5 Primaire sleutels en verwijssleutels
2.1.6 Kandidaatsleutels en alternatieve sleutels
2.1.7 Referentiële integriteitsregel
2.1.8 Multipliciteitsregels
2.1.9 Een ‘kip-ei’-probleem
Opgave 2.1
Opgave 2.2
Opgave 2.3
Opgave 2.4
2.1.10 Bijzondere beperkingsregels
2.1.11 Het bewaken van beperkingsregels
2.1.12 Kunstmatige sleutels
2.2 Gedragsregels
2.2.1 Refererende actieregels
2.2.2 Bijzondere gedragsregels
2.2.3 Transacties
Voorbeeld 2.1
Voorbeeld 2.2
Voorbeeld 2.3
Opgave 2.5
2.3 De OpenSchool-database (1)
2.4 Meer over uniciteitsregels
2.4.1 Combinaties van uniciteitsregels
Opgave 2.6
2.5 Meer over verwijzingsregels
2.5.1 Een samengestelde sleutel
Voorbeeld 2.4
2.5.2 Een recursieve verwijzing
Voorbeeld 2.5
2.5.3 Een veel-veel-associatie tussen een tabel en zichzelf
Voorbeeld 2.6
2.5.4 Een veel-veel-associatie tussen een tabel en een andere tabel
2.5.5 Niet-sleutelverwijzingen
Voorbeeld 2.7
Opgave 2.7
2.6 De OpenSchool-database (2)
Oefenopgaven
Opgave 2.8
Opgave 2.9
Opgave 2.10
Opgave 2.11
Opgave 2.12
Opgave 2.13
Opgave 2.14
3 Communiceren met een relationele database
3.1 Uitbreiding van Reijnders’ Toetjesboek
3.2 SQL als ‘universele gegevenstaal’
3.2.1 Wat kunnen we met SQL?
3.2.2 SQL-subtalen
3.2.3 Standaard-SQL en SQL-dialecten
3.3 De Boekverkenner en de Interactive Query Utility
3.3.1 Boekverkenner
Opgave 3.1
3.3.2 Interactive Query Utility
Opgave 3.2
3.4 Een eerste select-statement
3.4.1 Opvragen van volledige tabelinhoud
Voorbeeld 3.1
Opgave 3.3
3.4.2 Opmaak
3.4.3 Gereserveerde woorden en identifiers
3.4.4 Weergave van nulls
Voorbeeld 3.2
Opgave 3.4
3.4.5 Commentaarcode
Opgave 3.5
3.5 Projecties en selecties
3.5.1 De projectieoperatie
Voorbeeld 3.3
3.5.2 Constante en berekende resultaatkolommen
Voorbeeld 3.4
3.5.3 De selectieoperatie
Voorbeeld 3.5
Voorbeeld 3.6
3.5.4 Ordening
Voorbeeld 3.7
Opgave 3.6
Opgave 3.7
3.6 Operatoren en functies
3.6.1 Operatoren
Voorbeeld 3.8
3.6.2 Functies
Voorbeeld 3.9
3.7 Opvragingen uit meer dan één tabel: de join
3.7.1 De essentie van de join
Voorbeeld 3.10
3.7.2 De join in SQL
Voorbeeld 3.10
3.8 Tabelinhouden wijzigen
3.8.1 Rijen toevoegen
Voorbeeld 3.11
Voorbeeld 3.12
3.8.2 De automatische energieberekening
Voorbeeld 3.13
3.8.3 Scripts
3.8.4 Transacties
3.8.5 Rijen verwijderen
Voorbeeld 3.14
Voorbeeld 3.15
Voorbeeld 3.16
Voorbeeld 3.17
Opgave 3.8
3.8.6 Rijen wijzigen
Voorbeeld 3.18
Voorbeeld 3.19
Opgave 3.9
Opgave 3.10
Opgave 3.11
Opgave 3.12
Opgave 3.13
Opgave 3.14
Opgave 3.15
3.9 Databasestructuurdefinitie
3.9.1 Voorbereidende werkzaamheden
Opgave 3.16
3.9.2 Aanmaken van tabellen
Opgave 3.17
Opgave 3.18
Opgave 3.19
3.10 Reijnders’ Toetjesboek: de applicatie
3.10.1 Bladeren
3.10.2 Zoeken
3.10.3 Onderhoud
Oefenopgaven
Opgave 3.20
Opgave 3.21
Opgave 3.22
Opgave 3.23
Opgave 3.24
Opgave 3.25
Opgave 3.26
Opgave 3.27
Opgave 3.28
Opgave 3.29
Opgave 3.30
4 Nulls
4.1 De aard van nulls
4.1.1 Wat zijn nulls?
4.1.2 De SQL-constante null
Voorbeeld 4.1
4.1.3 Selecteren op null
Voorbeeld 4.2
Opgave 4.1
4.1.4 Rekenen met null
Voorbeeld 4.3
Voorbeeld 4.4
4.1.5 Null, 0 en de lege string
Voorbeeld 4.5
4.1.6 Onbekend of niet van toepassing?
Voorbeeld 4.6
4.1.7 Persoonsnamen
4.2 Codd-relationaliteit
Voorbeeld 4.7
Opgave 4.2
4.3 Logische algebra
4.3.1 Tweewaardige logica
Voorbeeld 4.8
Voorbeeld 4.9
4.3.2 Driewaardige logica
Opgave 4.3
Opgave 4.4
Opgave 4.5
Oefenopgaven
Opgave 4.6
Opgave 4.7
5 Normalisatie
5.1 De eerste normaalvorm
Opgave 5.1
5.2 Functionele afhankelijkheid
5.2.1 Functionele afhankelijkheid en sleutels
5.2.2 Functionele afhankelijkheid in een ongewenste structuur
5.3 De tweede normaalvorm
Opgave 5.2
5.4 De derde normaalvorm
Opgave 5.3
Opgave 5.4
Opgave 5.5
5.5 De Boyce-Codd-normaalvorm
5.5.1 De verbodsbepaling van BCNV
5.5.2 Een kritisch voorbeeld voor BCNV
Voorbeeld 5.1
Opgave 5.6
5.6 Kanttekeningen
5.6.1 1NV versus de hogere normaalvormen
5.6.2 De (on)wenselijkheid van redundantie
5.6.3 De vierde en de vijfde normaalvorm
Oefenopgaven
Opgave 5.7
Opgave 5.8
Opgave 5.9
6 Informatie uit één tabel
6.1 Projecties: select ... from
6.1.1 Projectie op kolomverzameling
6.1.2 Constante en berekende resultaatkolommen
6.1.3 Aliaskolomnamen
Voorbeeld 6.1
6.1.4 Distinct
Opgave 6.1
6.2 Datatypen
6.2.1 Datatypen van de SQL-standaard
6.2.2 Character sets
6.2.3 Typecasting
6.3 Operatoren
6.3.1 Numerieke operatoren
Voorbeeld 6.2
6.3.2 Alfanumerieke operatoren
6.3.3 Datum- en tijdoperatoren
Voorbeeld 6.3
6.3.4 Gemengde operatoren
Opgave 6.2
Opgave 6.3
6.4 Functies
6.4.1 Wat is een functie?
Voorbeeld 6.4
6.4.2 Datum- en tijdfuncties
Opgave 6.4
6.4.3 De cast-functie
Voorbeeld 6.5
6.4.4 De functies case en iif
Voorbeeld 6.6
Voorbeeld 6.7
6.4.5 User defined functions
6.5 Selecties: where
6.5.1 Voorwaarden aan rijen
Voorbeeld 6.8
6.5.2 Vergelijkingsoperatoren
6.5.3 De operator between ... and
6.5.4 De operator like
6.5.5 Logische expressies
6.5.6 Prioriteit van operatoren
Voorbeeld 6.9
6.5.7 De ‘is element van’-operator ‘in’
Voorbeeld 6.10
Opgave 6.5
6.5.8 Selecteren op nulls
Opgave 6.6
6.6 Ordening: order by
6.6.1 Klimmend en dalend ordenen
Voorbeeld 6.11
Voorbeeld 6.12
6.6.2 Ordenen op expressie
Voorbeeld 6.13
6.6.3 Verfijnd ordenen
Voorbeeld 6.14
Opgave 6.7
6.7 Verzamelingsoperatoren
6.7.1 Vereniging
Voorbeeld 6.15
Voorbeeld 6.16
6.7.2 Doorsnede en verschil
Voorbeeld 6.17
6.7.3 Verzamelingsexpressies en ordening
Opgave 6.8
Oefenopgaven
Opgave 6.9
Opgave 6.10
Opgave 6.11
Opgave 6.12
Opgave 6.13
7 Informatie uit meerdere tabellen: joins
7.1 Inner joins
7.1.1 Tabelverbreding op de inner-joinmanier
7.1.2 Joinen als denormaliseren
7.1.3 De inner join in SQL, via de productoperator
Voorbeeld 7.1
7.1.4 Projectie en selectie van join
Voorbeeld 7.2
Opgave 7.1
Opgave 7.2
Opgave 7.3
7.2 Outer joins
7.2.1 Tabelverbreding op de outer-joinmanier
Voorbeeld 7.3
7.2.2 De left outer join in SQL, via union
Voorbeeld 7.3
Opgave 7.4
Opgave 7.5
7.3 Joinoperatoren
7.3.1 De operator inner join
Voorbeeld 7.4
Voorbeeld 7.5
7.3.2 De operator left outer join
Voorbeeld 7.6
Voorbeeld 7.7
Opgave 7.6
Opgave 7.7
Opgave 7.8
7.4 Joins over een brede sleutel
Voorbeeld 7.8
Opgave 7.9
7.5 Samengestelde joins
7.5.1 Navigatie
7.5.2 Gegevens uit drie of meer tabellen
Voorbeeld 7.9
Voorbeeld 7.10
Voorbeeld 7.11
7.5.3 Joinen vanwege een selectieconditie
Voorbeeld 7.12
Opgave 7.10
Opgave 7.11
7.5.4 Gemengd gebruik van inner en outer joins
Voorbeeld 7.13
Voorbeeld 7.14
7.6 Autojoins
7.6.1 Inner autojoins
Voorbeeld 7.15
Voorbeeld 7.16
7.6.2 Outer autojoins
Voorbeeld 7.17
Opgave 7.12
Opgave 7.13
Opgave 7.14
7.7 Joins over een niet-sleutelverwijzing
Voorbeeld 7.18
7.8 De right outer join en de full outer join
Oefenopgaven
Opgave 7.15
Opgave 7.16
Opgave 7.17
Opgave 7.18
Opgave 7.19
Opgave 7.20
Opgave 7.21
Opgave 7.22
8 Statistische informatie
8.1 Statistische informatie: groeperen
8.2 Statistieken over één groep
8.2.1 Statistische functies
8.2.2 Tellen
Voorbeeld 8.1
Voorbeeld 8.2
Voorbeeld 8.3
8.2.3 Optellen en middelen
Voorbeeld 8.4
Voorbeeld 8.5
8.2.4 Extreme waarden
Voorbeeld 8.6
8.2.5 Foutieve ‘scheve’ query’s
Opgave 8.1
Opgave 8.2
Opgave 8.3
8.3 Statistieken over meerdere groepen
8.3.1 De group by-clausule
Voorbeeld 8.7
Voorbeeld 8.8
8.3.2 Verfijnd groeperen
Voorbeeld 8.9
Voorbeeld 8.10
8.3.3 Voorwaarden aan groepen: having
Voorbeeld 8.11
8.3.4 Kiezen voor having of where
8.3.5 Groeperen op berekende expressie
Voorbeeld 8.12
8.3.6 Groeperen op optionele kolom
Voorbeeld 8.13
8.3.7 Groeperen als denormaliseren
Voorbeeld 8.14
Opgave 8.4
Opgave 8.5
Opgave 8.6
8.4 Statistische joinquery’s
8.4.1 Joinen in navigatierichting één-veel
8.4.2 Statistische inner-joinquery’s
8.4.3 Statistische outer-joinquery’s
Opgave 8.7
Opgave 8.8
Opgave 8.9
8.5 Genest groeperen
Voorbeeld 8.15
Voorbeeld 8.16
Opgave 8.10
8.6 Het conceptuele algoritme
8.6.1 Stap voor stap volgens een denkmodel
Voorbeeld 8.17
Opgave 8.11
8.6.2 Niet-conceptuele aspecten: geheugen en performance
8.7 Groeperen en standaardisatie
Oefenopgaven
Opgave 8.12
Opgave 8.13
Opgave 8.14
Opgave 8.15
Opgave 8.16
Opgave 8.17
Opgave 8.18
Opgave 8.19
9 Subselects en views
9.1 Subselects als oplossing van deelproblemen
9.1.1 Subselect in where-clausule
Voorbeeld 9.1
Opgave 9.1
9.1.2 Subselect in select-clausule
Voorbeeld 9.2
Opgave 9.2
9.1.3 Subselect in from-clausule
Voorbeeld 9.3
9.1.4 Subselect in having-clausule
Voorbeeld 9.4
Opgave 9.3
9.1.5 Problemen stapsgewijs oplossen
9.2 Subselects en joins
9.2.1 Subselectnavigatie en joinnavigatie
9.2.2 Conditie op extra tabel
Voorbeeld 9.5
Opgave 9.4
9.2.3 Distinct als pseudo-groeperingsoperator
Voorbeeld 9.6
9.2.4 Navigatie over brede sleutel
Voorbeeld 9.7
Opgave 9.5
Voorbeeld 9.8
Opgave 9.6
Opgave 9.7
9.3 Gecorreleerde subselects
9.3.1 Niet-gecorreleerde subselect met gecorreleerde variant
Voorbeeld 9.9
Opgave 9.8
9.3.2 Gecorreleerde subselects met exists
Voorbeeld 9.10
Voorbeeld 9.11
Voorbeeld 9.12
Opgave 9.9
Opgave 9.10
9.3.3 Gecorreleerde subselects met not exists
Voorbeeld 9.13
Opgave 9.11
Voorbeeld 9.14
Opgave 9.12
Voorbeeld 9.15
9.4 Geneste subselects
9.4.1 Voorbeelden
Voorbeeld 9.16
Voorbeeld 9.17
9.4.2 Problemen met ‘alle’ en genest gebruik van (not) exists
Voorbeeld 9.18
Voorbeeld 9.19
Voorbeeld 9.20
Opgave 9.13
9.5 De closed world assumption
9.6 De operatoren all en any
Voorbeeld 9.21
Voorbeeld 9.22
Opgave 9.14
9.7 Views
9.7.1 Van subselect naar view
Voorbeeld 9.23
9.7.2 Het create view-statement
Voorbeeld 9.23
Opgave 9.15
9.8 Kiezen uit alternatieven
Voorbeeld 9.24
Oefenopgaven
Opgave 9.16
Opgave 9.17
Opgave 9.18
Opgave 9.19
Opgave 9.20
Opgave 9.21
Opgave 9.22
Opgave 9.23
Opgave 9.24
Opgave 9.25
Opgave 9.26
Opgave 9.27
10 Wijzigen van een database-inhoud
10.1 Levenswijzen van een database
10.1.1 Typen database
10.1.2 Online transaction processing
10.1.3 Transactionele bedrijfssystemen
10.1.4 Datawarehouses
10.2 Transacties
10.2.1 Commitmomenten
10.2.2 Transactiemodellen
10.3 Integriteitsregels
10.3.1 De referentiële integriteitsregel
10.3.2 Deleteregels
10.4 Het insert-statement
10.4.1 Enkelvoudige inserts
Voorbeeld 10.1
Voorbeeld 10.2
Opgave 10.1
10.4.2 Meervoudige inserts
Voorbeeld 10.3
Opgave 10.2
10.5 Het delete-statement
10.5.1 Eenvoudige deletes
Voorbeeld 10.4
Voorbeeld 10.5
Voorbeeld 10.6
10.5.2 Deletes met subselect
Voorbeeld 10.7
Opgave 10.3
Opgave 10.4
Opgave 10.5
10.6 Het update-statement
10.6.1 Eenvoudige updates
Voorbeeld 10.8
Voorbeeld 10.9
Voorbeeld 10.10
Voorbeeld 10.11
10.6.2 updates met subselect
Voorbeeld 10.12
Voorbeeld 10.13
Opgave 10.6
10.6.3 Update van primaire sleutel
Voorbeeld 10.14
Voorbeeld 10.15
Opgave 10.7
Voorbeeld 10.16
10.6.4 Update van verwijssleutel
Voorbeeld 10.17
Voorbeeld 10.18
Oefenopgaven
Opgave 10.8
Opgave 10.9
Opgave 10.10
11 Definitie van gegevensstructuren
11.1 Voorbeelddatabase: Ruimtereisbureau
11.2 Data definition language
11.2.1 Databases en databaseobjecten
11.2.2 Gebruikersinformatie en meta-informatie
11.2.3 DDL en transacties
11.2.4 Structuurwijzigingen: deltaproblematiek
11.3 Levenscyclus van een database
11.3.1 Creëren van een database
11.3.2 Een connectie met een database
11.3.3 Verwijderen van een database
Opgave 11.1
Opgave 11.2
Opgave 11.3
Opgave 11.4
11.4 Tabellen
11.4.1 Creëren van tabellen
11.4.2 Constraintnamen
11.4.3 Tabel met recursieve verwijzing
11.4.4 Verwijderen van tabellen en deltaproblemen
Voorbeeld 11.1
Voorbeeld 11.2
Opgave 11.5
Opgave 11.6
11.5 Kolomdefinities
11.5.1 Elementen van een kolomdefinitie
11.5.2 Datatypen
11.5.3 Not null-constraints
11.5.4 Default-specificaties
11.5.5 Afleidbare kolommen
11.5.6 Deltaproblemen op kolomniveau
Voorbeeld 11.3
Voorbeeld 11.4
11.6 Constraints
11.6.1 Primary key-constraints
11.6.2 Foreign key-constraints
11.6.3 Unique-constraints
11.6.4 Check-constraints
Voorbeeld 11.5
Opgave 11.7
Opgave 11.8
Opgave 11.9
11.7 Domeinen
11.7.1 Wat zijn domeinen?
11.7.2 Creëren van domeinen
11.7.3 Check-constraints op domeinen
Opgave 11.10
11.7.4 Verwijderen van domeinen en deltaproblemen
11.8 Views
11.8.1 Creëren van views
11.8.2 Gebruik van views
Voorbeeld 11.6
11.8.3 Verwijderen van views en deltaproblemen
11.9 Sequences
11.9.1 Kunstmatige sleutels
11.9.2 Creëren en gebruiken van sequences
Voorbeeld 11.7
11.9.3 Verwijderen van sequences en deltaproblemen
Opgave 11.11
Oefenopgaven
Opgave 11.12
Opgave 11.13
Opgave 11.14
12 Autorisatie
12.1 Gebruikers
12.1.1 Een ‘supergebruiker’: de DBA
12.1.2 Database-user-structuur
12.1.3 Gebruikersbeheer
Opgave 12.1
12.2 Privileges
12.2.1 Systeemprivileges
Opgave 12.2
12.2.2 Objectprivileges
12.2.3 Privileges verlenen
Voorbeeld 12.1
Voorbeeld 12.2
Voorbeeld 12.3
Voorbeeld 12.4
Voorbeeld 12.5
12.2.4 Privileges terugnemen
Voorbeeld 12.6
Voorbeeld 12.7
12.2.5 Privileges verlenen aan alle gebruikers
Voorbeeld 12.8
12.2.6 Privileges verlenen ‘with grant option’
Voorbeeld 12.9
Voorbeeld 12.10
Opgave 12.3
Opgave 12.4
Opgave 12.5
Opgave 12.6
Opgave 12.7
12.3 Views
12.3.1 Views op maat
Voorbeeld 12.11
Voorbeeld 12.12
12.3.2 Updatable views
Voorbeeld 12.13
12.3.3 Views ‘with check option’
Voorbeeld 12.14
Opgave 12.8
Opgave 12.9
Opgave 12.10
Opgave 12.11
12.4 Rollen
12.4.1 Bedrijfsprocessen, gebruikersgroepen en rollen
12.4.2 Privileges, rollen en gebruikers
Voorbeeld 12.15
12.4.3 Vereenvoudiging van privilegebeheer
12.4.4 Wijzigen en verwijderen van rollen
Voorbeeld 12.16
Voorbeeld 12.17
Opgave 12.12
Opgave 12.13
Oefenopgaven
Opgave 12.14
Opgave 12.15
Opgave 12.16
Opgave 12.17
Opgave 12.18
Opgave 12.19
Opgave 12.20
Opgave 12.21
Opgave 12.22
13 Query-optimalisatie
13.1 Voorbeelddatabase: GrootOrderdatabase
13.2 De optimizer
13.2.1 Stappen in het verwerken van een query
13.2.2 Performancefactoren
13.2.3 Hypothetische ‘conceptuele’ verwerking
13.2.4 Een verbeterd algoritme
13.2.5 Rule-based en cost-based optimizers
13.3 Indexen
13.3.1 Indexstructuur
13.3.2 Binair zoeken
13.3.3 Wanneer indexeren?
13.3.4 Creëren en verwijderen van een index
Voorbeeld 13.1
Voorbeeld 13.2
Voorbeeld 13.3
13.3.5 Unieke indexen
Voorbeeld 13.4
13.3.6 Standaardindexen
13.3.7 Queryplan
13.3.8 Verwerkingsstatistieken
13.3.9 Een index activeren en inactiveren
Voorbeeld 13.5
13.3.10 De selectiviteit van een index
13.3.11 Onderhoud van indexen
Opgave 13.1
13.3.12 Tips om lange wachttijden te voorkomen
Opgave 13.2
Opgave 13.3
Opgave 13.4
13.4 Performanceverbetering door queryaanpassing
Voorbeeld 13.6
Opgave 13.5
Opgave 13.6
13.5 Performanceverbetering door aanpassing ontwerp
13.5.1 De keuze van sleutels
Opgave 13.7
13.5.2 Redundantie en constraints
Oefenopgaven
Opgave 13.8
Opgave 13.9
Opgave 13.10
Opgave 13.11
Opgave 13.12
14 Aanpak van queryproblemen
14.1 Warming-up
Opgave 14.1
Opgave 14.2
Opgave 14.3
Opgave 14.4
Opgave 14.5
Opgave 14.6
Opgave 14.7
Opgave 14.8
Opgave 14.9
Opgave 14.10
14.2 Probleemaanpak door vragen en antwoorden
14.2.1 De belangrijkste vraag: waarover wordt informatie gevraagd?
Voorbeeld 14.1
Voorbeeld 14.2
Voorbeeld 14.3
Voorbeeld 14.4
14.2.2 De keuze tussen een subselect en een join
Voorbeeld 14.5
14.2.3 Een voorbeeld met groeperen
Voorbeeld 14.6
Voorbeeld 14.7
Opgave 14.11
14.2.4 Een voorbeeld met een recursieve verwijzing
Voorbeeld 14.8
14.2.5 Join in subselect of geneste subselect?
Voorbeeld 14.9
14.3 Stappenplan
Voorbeeld 14.10
Opgave 14.12
Oefenopgaven
Opgave 14.14
Opgave 14.15
Opgave 14.16
Opgave 14.17
Opgave 14.18
Opgave 14.19
15 Transacties en concurrency
15.1 Transacties
15.1.1 Nogmaals transacties
Voorbeeld 15.1
15.1.2 Transactiemodellen
Voorbeeld 15.2
Voorbeeld 15.3
Opgave 15.1
15.2 Transactiemanagement
15.2.1 Concurrency
15.2.2 Concurrency control
15.2.3 Recovery
15.2.4 ACID
15.2.5 Gedistribueerde databases
15.3 Vier klassieke problemen
15.3.1 Lost update
Voorbeeld 15.4
Opgave 15.2
15.3.2 Dirty read
Voorbeeld 15.5
15.3.3 Non-repeatable read
Voorbeeld 15.6
15.3.4 Phantom
Voorbeeld 15.7
Opgave 15.3
15.4 Isolation levels
15.4.1 Isolation levels in de SQL-standaard
15.4.2 Isolation levels in Firebird
Opgave 15.4
Voorbeeld 15.8
Opgave 15.5
Voorbeeld 15.9
Opgave 15.6
Opgave 15.7
Opgave 15.8
Oefenopgaven
Opgave 15.9
Opgave 15.10
Opgave 15.11
Opgave 15.12
16 Triggers en stored procedures
16.1 Triggertaal
16.2 Stored procedures
16.2.1 Wat is een stored procedure?
Voorbeeld 16.1
Opgave 16.1
Opgave 16.2
16.2.2 Uitvoeren van een stored procedure
Opgave 16.3
16.3 Triggers
16.3.1 Before-triggers en after-triggers
16.3.2 Events
Voorbeeld 16.2
16.3.3 Contextvariabelen old en new
16.3.4 Voorbeelden van after-triggers
Voorbeeld 16.3
Opgave 16.4
Opgave 16.5
Voorbeeld 16.4
Opgave 16.6
Voorbeeld 16.5
Opgave 16.7
16.3.5 Voorbeelden van before-triggers
Voorbeeld 16.6
Opgave 16.8
Opgave 16.9
16.3.6 Before-trigger met exception
Voorbeeld 16.7
Opgave 16.10
Voorbeeld 16.8
Opgave 16.11
Opgave 16.12
16.3.7 Multi-event-triggers
Voorbeeld 16.9
16.4 Meer over triggers en stored procedures
16.4.1 Deltaproblematiek
16.4.2 Executable procedures en select-procedures
16.4.3 Stored procedures en views
16.4.4 Executievolgorde van triggers
Oefenopgaven
Opgave 16.13
Opgave 16.14
Opgave 16.15
Opgave 16.16
17 De data dictionary
17.1 Metastructuren
17.1.1 Het objectniveau
17.1.2 Het metaniveau
17.2 Meta-informatie over tabellen
Opgave 17.1
Opgave 17.2
Opgave 17.3
Opgave 17.4
17.3 Meta-informatie over kolommen en domeinen
17.3.1 Lokale kolomkenmerken
17.3.2 Niet-lokale kolomkenmerken en domeinen
Opgave 17.5
Opgave 17.6
Opgave 17.7
17.4 Overige dictionarytabellen
Opgave 17.8
Opgave 17.9
17.5 Metaviews
17.5.1 Waarom metaviews?
17.5.2 ANSI / ISO-metaviews
Opgave 17.10
17.6 Van tabel ‘Tabel’ naar volledige database
17.6.1 Zelfbeschrijvende deelsystemen
17.6.2 Een meta-metaniveau?
Oefenopgaven
Opgave 17.11
Opgave 17.12
Opgave 17.13
Opgave 17.14
Bijlage 1
Firebird: functies en contextvariabelen
Stringfuncties
Rekenkundige functies
Wiskundige functies
Datumfuncties
Conditionele functies
Typeconversiefunctie
Null-vervangfunctie
Contextvariabelen
Datumconversiefuncties - user defined in IQU
Bijlage 2
Firebird: data dictionary
De database
Tabellen
Kolommen
Domeinen
Indexen
Constraints
Autorisatie
Sequences
Bijlage 3
Firebird: databasetools
Command line tools
Grafische tools
Bijlage 4
Voorbeelddatabases
OpenSchool
Orderdatabase
Ruimtereisbureau
Toetjesboek
Register
Papiere empfehlen

Relationele databases en SQL [Derde, geheel herziene druk, 4e oplage. ed.]
 9789039527146, 9039527148 [PDF]

  • 0 0 0
  • Gefällt Ihnen dieses papier und der download? Sie können Ihre eigene PDF-Datei in wenigen Minuten kostenlos online veröffentlichen! Anmelden
Datei wird geladen, bitte warten...
Zitiervorschau

Voorwoord Door alle hypes heen blijven relationele databases de basistechnologie om bedrijfsgegevens op een gestandaardiseerde manier te beheren en om aan de dagelijkse informatiebehoeften tegemoet te komen. De relationele taal SQL zal daarom voor onafzienbare tijd tot de basiskennis voor informatici blijven behoren. Uitgangspunten In deze derde, geheel herziene druk zijn de uitgangspunten dezelfde als in de vorige drukken:

De concepten staan centraal: die van de relationele theorie, en daaraan gerelateerd die van SQL . Dit impliceert aandacht voor de eisen van een ‘goede taal’.

Het ontwikkelen van probleemoplossend vermogen is belangrijker dan het leren van syntaxis, onder het motto ‘Het opstellen van SQL -query’s is een sport die je kunt leren’. Het stapsgewijs oplossen van vraagstukken, met tussenformuleringen in natuurlijke taal, is een daarbij regelmatig gehanteerde methode.

Vraagstellingen worden conceptueel benaderd en niet vanuit de beperkingen van een SQL -dialect. De relationele theorie van deel A wordt levend gehouden in deel B, en is daarin leidend voor oplossingsstrategieën en het opstellen van leesbare, professionele SQL code. Via talloze plaatjes wordt gevisualiseerd wat de essentie is.

De theorie wordt ondersteund door vele voorbeelden en opgaven, een krachtig databasemanagementsysteem, een mooie SQL-omgeving en een bijzondere elektronische leeromgeving. Didactiek De conceptuele aanpak is één van de pijlers van de didactiek, zodat steeds een verscheidenheid aan dingen onder de noemer wordt gebracht van eenzelfde ‘rode draad’. De uitleg wordt hierdoor vereenvoudigd en het inzicht verdiept. ‘Databasenavigatie’ is zo’n rode draad, die naar voren wordt gehaald bij de uitleg van sleutels, joins en subselects, maar ook centraal staat bij de richtlijnen voor probleemaanpak bij het opstellen van goede SQL -code. Een tweede rode draad is ‘normaliseren en denormaliseren’. Zo wordt niet alleen de join maar ook het groeperen in SQL gepresenteerd als een vorm van denormaliseren, wat conceptueel heel

zuiver is en (daardoor) de stof gemakkelijker maakt. Een derde rode draad is ‘single point of definition’, het principe om wát dan ook op niet meer dan één plek te definiëren. Dat geldt voor databasegegevens zoals plaatsnamen, maar de reikwijdte van het principe blijkt enorm veel uitgebreider. Een andere didactische pijler is onze overtuiging dat bij dit vak een hoog niveau niet gediend is bij een al te formele aanpak. Daarvoor kent de relationele theorie téveel varianten en losse eindjes. Door een nette, maar informele aanpak hebben we voor onszelf de ruimte geschapen om een klas-sieke theorie op een moderne manier te behandelen en waar nodig te becommentariëren. Dit boek maakt het relatief gemakkelijk om een heel goed niveau te bereiken. Doelgroepen Het boek is geschikt voor de eerste fase van (bedrijfskundige) informaticaopleidingen aan de hogeschool en universiteit. Mede door de elektronische leeromgeving (Boekverkenner) leent het zich ook goed voor zelfstudie. Daarnaast is de Boekverkenner een mooi instrument voor docenten, bij presentaties en instructies. Nieuw in de derde druk Deze derde druk is volledig herzien:

We zijn overgegaan op een nieuw databasemanagementsysteem, Firebird. Dit is ‘open source’, met een SQL die nauw aansluit bij de SQL -standaard SQL:2008 .

Er is een hoofdstuk toegevoegd over transacties en multi-usergebruik.

Databasenavigatie en, nauw daaraan verwant, de aanpak van queryproblemen via stapsgewijze verfijning hebben meer aandacht gekregen.

Twee van de voorbeelddatabases zijn vervangen door één nieuwe, die rijker is van structuur.

De naamgevingsconventies, met name voor sleutelkolommen, zijn veranderd, met veel compactere SQL -code tot gevolg.

De uitwerkingen van opgaven zijn nu te vinden in de ‘Boekverkenner’. De tekst is ingekort waar dat kon en uitgebreid waar dat nuttig was. Ook is de structuur herzien. Al met al zijn grote delen van de tekst volledig herschreven. Dit is géén ontwerpboek Dit boek beperkt zich bewust tot kant en klare databases. Er wordt gereflecteerd op de mogelijkheden en beperkingen van gegeven databases, maar er worden geen databases ontworpen. Alles op zijn tijd: we denken dat ervaring met bestaande databases een nodige (maar niet voldoende) voorwaarde is voor goed en effectief ontwerpen. De problematiek van het ontwerpen van informatiemodellen en databases komt uitgebreid aan de orde in het boek “Modelleren als ambacht – Informatiemodelleren met MDD ” van dezelfde auteurs. Over de auteurs Leo Wiegerink studeerde wiskunde aan de Universiteit van Leiden. Als opleider van wiskundeleraren, als informacadocent aan de Hogere Informaticaopleiding van de Hogeschool van Amsterdam, als docent/ cursusontwikkelaar in het bedrijfsleven en tenslotte als cursusontwikkelaar bij de Informatica-faculteit van de Open Universiteit heeft hij ruime ervaring opgedaan met lesgeven aan en cursusontwikkeling voor de doelgroepen van dit boek. Jeanot Bijpost en Marco de Groot begonnen hun informaticaloopbaan aan de Amsterdamse HIO . Vanuit hun bedrijf, Mattic Software, houden zij zich bezig met de ontwikkeling en het beheer van grote informatiesystemen binnen de Nederlandse overheid. Het ontwikkelen van tools voor systeemontwikkelaars (metasysteemontwikkeling) loopt als rode draad door hun werk. Dank aan ... Ook nu weer willen we iedereen bedanken die aan dit boek heeft bijgedragen: studenten die eerdere versies van commentaar hebben voorzien en onze achterbannen. In het bijzonder bedanken we drie medewerkers van de Open Universiteit: Harold Pootjes, Nikè van Vugt en Bart Pauw. Hun bijdrage in deze nieuwe druk aan o.a. de hoofdstukken Query-optimalisatie en Transacties en concurrency was zeer waardevol, evenals de terugkoppeling vanuit het onderwijs. Leo Wiegerink, Jeanot Bijpost en Marco de Groot Amsterdam, zomer 2000 / winter 2013.

De software De software omvat het open source databasemanagementsysteem Firebird 2.5, een interactieve SQL -omgeving IQU (Interactive Query Utility) en een elektronische leeromgeving (Boekverkenner) met een interactieve versie van het volledige boek. Ga naar http://www.relsql.nl/download voor download van alle software. U hebt hierbij een unieke code nodig, die u vindt op de colofonpagina van dit boek. Firebird Firebird is een open source SQL -databaseserver, die zich snel en simpel laat installeren en eenvoudig is in gebruik. Firebird- SQL is bijna volledig conform de SQL -standaard SQL:2008 . Firebird omvat een derdegeneratietaal voor het programmeren van triggers en stored procedures. Interactive Query Utility De Interactive Query Utility ( IQU ®) is een SQL -omgeving, dat wil zeggen een programma om SQL -opdrachten te versturen naar het databasemanagementsysteem. IQU is een zeer handige tool, die vanuit de praktijk is ontwikkeld. Boekverkenner De Boekverkenner is een elektronische leeromgeving waarmee het boek op allerlei manieren kan worden benaderd en geïntegreerd met het databasemanagementsysteem (Firebird). De Boekverkenner biedt onder meer:

de volledige tekst van het boek, te benaderen via de inhoudsopgave, een index, een historielijst, bookmarks en een zoekmachine

alle uitwerkingen van opgaven

alle voorbeelddatabases, met hun beschrijving, diagrammen en SQL scripts

een eindgebruikersapplicatie met ‘zichtbaar’ SQL -verkeer

schakelmogelijkheden tussen tekst, voorbeelddatabases en IQU

het uitvoeren van SQL -opdrachten direct vanuit de tekst van het boek

meting van de databasebelasting van SQL -opdrachten

verschillende transactiemodellen

het boek en de Boekverkenner zelf als één van de voorbeelddatabases. Door de snelle schakelmogelijkheden tussen tekst, voorbeelden en IQU , en het moeiteloos installeren en herinstalleren van de voorbeelddatabases, is de Boekverkenner een bijzonder handig en plezierig instrument. Website Het boek heeft een website: www.relsql.nl , met aanvullende informatie en diensten bij het boek.

Opbouw van het boek Het boek is opgebouwd uit vijf delen: Deel A ‘Relationele Systemen’ behandelt met tal van voorbeelden de theorie van relationele systemen. De hoofdstukken 1 en 2 gaan over de ‘achterkant’ van die systemen: relationele databases en hun beheerprogramma’s. Hoofdstuk 3 geeft een inleiding in de taal ( SQL ) waarmee met het beheerprogramma wordt gecommuniceerd. Met alle aandacht voor de ‘voorkant’ van het systeem: de programma’s (clients) waarmee eindgebruikers, databasebeheerders of studenten die SQL willen leren het beheerprogramma benaderen. Hoofdstuk 4 gaat over de problematiek van de ‘niet-ingevulde waarden’ (nulls). Dit deel eindigt met hoofdstuk 5 over de normalisatietheorie, een klassiek database-onderwerp over het tranformeren van ongewenste databasestructuren. Deel B ‘Opvragen en manipuleren van gegevens’ is het eerste deel van een grondige leergang SQL . Het is gewijd aan de SQL -querytaal, de subtaal om gegevens op te vragen en de gegevensinhoud van een database up-todate te houden. De hoofdstukken 6 tot en met 9 gaan over het opvragen van gegevens, om te voorzien in de informatiebehoeften van gebruikers. De relationele theorie van deel A wordt daarbij optimaal gebruikt om inzicht en verdieping te bereiken. Als vanzelf heeft dit effect op een goede probleemaanpak en een professionele programmeerstijl. Het leidt ook tot een kritische houding ten opzichte van de taal SQL , die om historische en

commerciële redenen niet de mooist denkbare relationele taal is. Hoofdstu k 10 behandelt hierna de SQL -commando’s om een database-inhoud bij te werken. Deel C ‘Structuur en beheer’ vervolgt de SQL -leergang. Hoofdstuk 11 behandelt het creëren, veranderen en verwijderen van databases met hun verschillende typen databaseobjecten. Hierna volgen twee beheerhoofdstukken: hoofdstuk 12 over autorisatie, het geven van rechten op maat aan gebruikers, en hoofdstuk 13 over het optimaliseren van query’s, met het oog op snelle verwerking. Deel D ‘Verdieping’ bevat vier verdiepende hoofdstukken die stuk voor stuk interessant zijn. Hoofdstuk 14 gaat over probleemaanpak bij het opstellen van SQL -query’s. In hoofdstuk 15 over multi-usergebruik wordt uitgelegd hoe wordt vermeden dat gelijktijdige gebruikers elkaar in de weg zitten. Hoofdstuk 16 behandelt het bewaken van regels en het programmeren van automatische acties via ‘triggers en stored procedures’, dat zijn programmaatjes in een (derde-generatie) uitbreidingstaal van SQL . Dit deel eindigt met hoofdstuk 17 over het hart van een relationeel systeem: de data dictionary. Het afsluitende deel E omvat vier bijlagen, drie met Firebird-specifieke informatie en één met een overzicht van alle voorbeelddatabases.

Inhoud Voorwoord De software Opbouw van het boek Inhoud Deel A: Relationele databases

1 Relationele databases: structuur 1.1 Informatiesystemen 1.2 Relationele informatiesystemen 1.3 Reijnders’ Toetjesboek 1.4 Alle gegevens in één tabel 1.5 Verbeterde tabelstructuren 1.6 Gegevens en informatie Oefenopgaven

2 Relationele databases: regels

2.1 Beperkingsregels 2.2 Gedragsregels 2.3 De OpenSchool-database (1) 2.4 Meer over uniciteitsregels 2.5 Meer over verwijzingsregels 2.6 De OpenSchool-database (2) Oefenopgaven

3 Communiceren met een relationele database 3.1 Uitbreiding van Reijnders’ Toetjesboek 3.2 SQL als ‘universele gegevenstaal’ 3.3 De Boekverkenner en de Interactive Query Utility 3.4 Een eerste select-statement 3.5 Projecties en selecties

3.6 Operatoren en functies 3.7 Opvragingen uit meer dan één tabel: de join 3.8 Tabelinhouden wijzigen 3.9 Databasestructuurdefinitie 3.10 Reijnders’ Toetjesboek: de applicatie Oefenopgaven

4 Nulls 4.1 De aard van nulls 4.2 Codd-relationaliteit 4.3 Logische algebra Oefenopgaven

5 Normalisatie 5.1 De eerste normaalvorm

5.2 Functionele afhankelijkheid 5.3 De tweede normaalvorm 5.4 De derde normaalvorm 5.5 De Boyce-Codd-normaalvorm 5.6 Kanttekeningen Oefenopgaven Deel B: Relationele databases bevragen en wijzigen

6 Informatie uit één tabel 6.1 Projecties: select ... from 6.2 Datatypen 6.3 Operatoren 6.4 Functies 6.5 Selecties: where

6.6 Ordening: order by 6.7 Verzamelingsoperatoren Oefenopgaven

7 Informatie uit meerdere tabellen: joins 7.1 Inner joins 7.2 Outer joins 7.3 Joinoperatoren 7.4 Joins over een brede sleutel 7.5 Samengestelde joins 7.6 Autojoins 7.7 Joins over een niet-sleutelverwijzing 7.8 De right outer join en de full outer join Oefenopgaven

8 Statistische informatie 8.1 Statistische informatie: groeperen 8.2 Statistieken over één groep 8.3 Statistieken over meerdere groepen 8.4 Statistische joinquery’s 8.5 Genest groeperen 8.6 Het conceptuele algoritme 8.7 Groeperen en standaardisatie Oefenopgaven

9 Subselects en views 9.1 Subselects als oplossing van deelproblemen 9.2 Subselects en joins

9.3 Gecorreleerde subselects 9.4 Geneste subselects 9.5 De closed world assumption 9.6 De operatoren all en any 9.7 Views 9.8 Kiezen uit alternatieven Oefenopgaven

10 Wijzigen van een database-inhoud 10.1 Levenswijzen van een database 10.2 Transacties 10.3 Integriteitsregels 10.4 Het insert-statement 10.5 Het delete-statement

10.6 Het update-statement Oefenopgaven Deel C: Relationele databases beheren

11 Definitie van gegevensstructuren 11.1 Voorbeelddatabase: Ruimtereisbureau 11.2 Data definition language 11.3 Levenscyclus van een database 11.4 Tabellen 11.5 Kolomdefinities 11.6 Constraints 11.7 Domeinen 11.8 Views 11.9 Sequences Oefenopgaven

12 Autorisatie 12.1 Gebruikers 12.2 Privileges 12.3 Views 12.4 Rollen Oefenopgaven

13 Query-optimalisatie 13.1 Voorbeelddatabase: GrootOrderdatabase 13.2 De optimizer 13.3 Indexen 13.4 Performanceverbetering door queryaanpassing 13.5 Performanceverbetering door aanpassing ontwerp Oefenopgaven Deel D: Verdieping

14 Aanpak van queryproblemen 14.1 Warming-up 14.2 Probleemaanpak door vragen en antwoorden 14.3 Stappenplan Oefenopgaven

15 Transacties en concurrency 15.1 Transacties 15.2 Transactiemanagement 15.3 Vier klassieke problemen 15.4 Isolation levels Oefenopgaven

16 Triggers en stored procedures

16.1 Triggertaal 16.2 Stored procedures 16.3 Triggers 16.4 Meer over triggers en stored procedures Oefenopgaven

17 De data dictionary 17.1 Metastructuren 17.2 Meta-informatie over tabellen 17.3 Meta-informatie over kolommen en domeinen 17.4 Overige dictionarytabellen 17.5 Metaviews 17.6 Van tabel ‘Tabel’ naar volledige database Oefenopgaven Deel E: Bijlagen

Bijlage 1 Firebird: functies en contextvariabelen Bijlage 2 Firebird: data dictionary Bijlage 3 Firebird: databasetools Bijlage 4 Voorbeelddatabases Register

1 Relationele databases: structuur Een relationele database is één component van een groter systeem, een informatiesysteem. De database bevat, gestructureerd in de vorm van tabellen, de gegevens die voor gebruikers van het informatiesysteem de informatie opleveren die zij nodig hebben. Dit hoofdstuk gaat over de structuur van een relationele database en legt daarmee de basis voor al wat volgt.

1.1 Informatiesystemen Onderwijs-informatiesystemen, een studiefinancierings-informatiesysteem, een belastingdienst-informatiesysteem, tandarts- en huisartsinformatiesystemen, ziekenhuis-informatiesystemen, een antiekdiefstalinformatiesysteem, enzovoort. Er zijn zóveel informatiesystemen dat we allemaal wel ongeveer weten wat het zijn: ‘systemen waar je informatie in kunt stoppen en informatie uit kunt halen’. We zullen de twee aspecten: informatie en systeem eens wat preciezer beschouwen. Informatie heeft te maken met gegevens die ergens zijn opgeslagen: op papier, in een computergeheugen of op een andere gegevensdrager. Die gegevens kunnen bestaan uit tekst of getallen, uit grafieken of andere plaatjes. Ook gesproken tekst op een bandje kun je opvatten als een verzameling gegevens. Informatie is echter méér dan die gegevens: het heeft te maken met de betekenis die aan de gegevens moet worden gehecht. En gegevens kunnen alleen maar wat betekenen als ze iets uitdrukken over een ‘wereld’. Bijvoorbeeld een wereld van patiënten, doctoren, medische ingrepen, enzovoort in een ziekenhuis. Of een wereld van antiek, diefstal en opsporing. Of een heel klein wereldje van recepten voor lekkere toetjes: het geautomatiseerde Toetjesboek dat we verderop als voorbeeld zullen gebruiken. Informatie drukt feiten uit over zo’n wereld. Een systeem is iets dat geordend is, waar een bepaalde structuur in zit. Een informatiesysteem is dus een systeem waarin informatie (betekenisvolle gegevens over een bepaalde ‘wereld’) gestructureerd is opgeslagen. Daarnaast is het een instrument voor gebruikers van het systeem, die er informatie uit moeten kunnen opvragen om antwoorden te krijgen op relevante vragen over de ‘wereld’ in kwestie. Een informatiesysteem hoeft niet geautomatiseerd te zijn, er hoeft helemaal geen computer aan te pas te komen. Een verzameling kaartenbakken kan een betrouwbaar informatiesysteem vormen, als het goed wordt beheerd. Maar zo’n papieren systeem heeft beperkingen: het opzoeken kan lang duren, en koppelen met andere systemen is veelal onuitvoerbaar. Een geautomatiseerd systeem kent dit soort beperkingen in mindere mate. Maar alleen als het goed is ontworpen.

1.2 Relationele informatiesystemen Veel moderne informatiesystemen maken gebruik van een manier van gegevensopslag die we relationeel noemen. Dit houdt – informeel uitgedrukt – in dat de gegevens in de vorm van tabellen zijn opgeslagen. Alle tabellen bij elkaar, doorgaans horend bij één ‘wereld’, vormen een relationele database . Een relatie is een wiskundige structuur die veel overeenkomst vertoont met wat we in het dagelijks leven een tabel noemen, vandaar de term ‘relationeel’. En database betekent ‘gegevensbank’, ofwel opslagplaats voor gegevens.

1.2.1 SQL-server en SQL-clients Bij grotere informatiesystemen met meerdere gebruikers is er meestal één centrale computer (de server ) waarop de database wordt bewaard en beheerd. Elke gebruiker van het systeem beschikt dan over een pc of vergelijkbare computer (de client ) waarop een applicatie (toepassingsprogramma) draait. Deze configuratie, waarbij een taakverdeling bestaat tussen een beheerprogramma op de servercomputer en programma’s op de clients, is de meest voorkomende fysieke verschijningsvorm van wat client/server-architectuur wordt genoemd, zie f iguur 1.1 . Het serverprogramma heet relationeel databasemanagementsysteem ( rdbms ), ofwel ‘relationeel gegevensbankbeheersysteem’.

Figuur 1.1 Client/server-architectuur met pc’s en centrale databaseserver

De server is doorgaans met de clients verbonden door een netwerkverbinding. De gebruikers sturen via hun applicaties opdrachten naar het rdbms. Die opdrachten worden geformuleerd in een gegevenstaal . Voor relationele systemen is de meest gebruikte gegevenstaal SQL . Het rdbms wordt daarom ook vaak een SQL -server genoemd, en de applicatie een SQL -client . Deze voert de opdrachten uit, bijvoorbeeld het wijzigen van een adres of het terugsturen van een overzicht met gegevens. Een eindgebruiker hoeft de taal SQL niet te kennen. Hun client ‘spreekt’ SQL zonder dat ze het merken. Maar ontwikkelaars en beheerders van relationele databases dienen een gedegen kennis van SQL te hebben. Daarom is het grootste deel van dit boek daaraan gewijd. Bij kleine systemen kunnen het rdbms en een applicatie op één computer draaien, bijvoorbeeld op een pc of een notebookcomputer. Al is de taakverdeling tussen serverprogramma (rdbms) en clientprogramma (applicatie) dan minder zichtbaar, in feite is er niets wezenlijks veranderd, zie figuur 1.2 . Wanneer je de voorbeelden en opgaven in dit boek zelf uitprobeert, zal dat waarschijnlijk op één machine gebeuren, ofwel: met client en server in één kastje. SQL-dialecten Hoewel standaardisatiecommissies de ontwikkeling van SQL hebben proberen te sturen (in hoofdstuk 3 vertellen we daar meer over) hebben de rdbms’en van verschillende leveranciers elk hun eigen SQL -dialect. Bij dit boek is Firebird meegeleverd, een open sourceproduct. Dit volgt zeer goed de belangrijke SQL:2008 -standaard. Hiermee kunt u de voorbeelden uitproberen en de opdrachten maken. Via de elektronische leeromgeving (Boekverkenner) wordt het u daarbij heel erg gemakkelijk gemaakt.

Figuur 1.2 Client/server op één computer

Een stukje geschiedenis In 1970 verscheen in de Communications of the Association for Computing Machinery een artikel van IBM-onderzoeker dr. E.F. Codd (1970): ‘A Relational Model of Data for Large Shared Data Banks’. Deze dr. Codd wordt sedertdien steevast aangeduid als de ‘vader van het relationele model’. In dit artikel wordt een theorie uiteengezet, gebaseerd op de wiskundige verzamelingenleer, over het opslaan en manipuleren van gegevens door middel van tabelstructuren. Het artikel bracht veel onderzoek op gang, met als doel een ‘relationeel’ alternatief te ontwikkelen voor de toen bestaande databasearchitecturen, waar men om verschillende redenen niet tevreden over was. IBM zelf startte een research project ‘System/R’, dat beoogde een werkend relationeel systeem op te leveren. Halverwege de 70’er jaren resulteerde dit in een prototype van een rdbms, dat in de jaren daarna verder werd ontwikkeld. Onderzoek aan relationele vraagtalen gaf het leven aan onder meer de taal SEQUEL, wat stond voor Structured English Query Language (‘gestructureerde Engelse vraagtaal’). Later werd deze naam om auteursrechtelijke redenen gewijzigd in SQL, Structured Query Language, wat vaak nog steeds als ‘seOnline’ wordt uitgesproken. Niet IBM zelf, maar een bedrijf genaamd ‘Relational Software, Inc.’ bouwde het eerste commerciële rdbms gebaseerd op SQL. Dit product, Oracle, kwam uit in 1979. Het bedrijf heet inmiddels al heel lang Oracle Corporation en is marktleider voor rdbms’en voor client/server-systemen. IBM zelf heeft zich een dominante positie verworven op de mainframemarkt. Het meest bekende IBM-rdbms is Database 2 (DB/2) geworden. Vermeldenswaardig is ook het bedrijf ‘Relational Technology, Inc.’ dat in 1981 een commercieel SQL-rdbms het leven liet zien, genaamd Ingres. Het prototype van Ingres en de eerste commerciële versies van Ingres waren niet gebaseerd op SQL maar op de taal QUEL, die veel minder dan SQL op Engels lijkt en een strakkere, logische structuur heeft. Wie in dit boek wat verder gevorderd is, zal het misschien mét ons betreuren dat QUEL uiteindelijk het onderspit heeft moeten delven tegenover SQL. In de jaren negentig heeft ook Microsoft zich op de databasemarkt begeven, en is momenteel met Ms Sql Server één van de belangrijke leveranciers. Een ander, bekend Microsoft-product is Ms Access dat echter maar beperkte mogelijkheden heeft en minder geschikt is voor grote databases in complexe omgevingen.

1.2.2 Client/server-architectuur Een zuivere benadering van de client/server-architectuur gaat niet uit van een fysieke hardwareconfiguratie, maar van een rolverdeling tussen programma’s. Daarbij verleent het ene programma (het serverprogramma) diensten aan een ander programma dat om die diensten vraagt (het clientprogramma). In de praktijk wordt de term vooral gebruikt voor een netwerkconfiguratie met aan de ‘achterkant’ een databaseserver en aan de ‘voorkant’ client-pc’s met grafisch georiënteerde applicatieprogrammatuur. Belangrijk hierbij zijn de softwarecomponenten die garanderen dat de gegevens aan bepaalde bedrijfsregels voldoen, of

die automatisch bepaalde acties veroorzaken. Tezamen worden deze vaak aangeduid met de wat verwarrende termen logica of business logic . Fat clients of thin clients De bedrijfslogica staat in principe volledig of bijna volledig op de server. Zoals we later zullen zien: als onderdeel van de database. Daarnaast kunnen óók al de nodige controles op de clients plaatsvinden. Denk bijvoorbeeld aan controles op geldige postcodes of e-mailadressen. Dat heeft als voordeel dat het netwerkverkeer wordt beperkt, immers de gegevens die worden verstuurd zijn dan al gevalideerd (getoetst aan de regels, en in orde bevonden). Een nadeel kan zijn dat de applicaties sneller bijgewerkt moeten worden (versiebeheer op de clients). Bij een fat client worden veel regels al in het clientprogramma bewaakt. Bij een thin client is dat niet of nauwelijks het geval. Daarbij is in het algemeen meer netwerkverkeer nodig, immers alle invoerpogingen worden pas op de server gecontroleerd. Doordat echter de bedrijfslogica voornamelijk op de server wordt beheerd, wordt het applicatiebeheer eenvoudiger. In een enkel geval vindt op alle applicaties uitgebreide regelbewaking plaats. Het is dan niet nodig dit ook nog eens op de server te doen. We hebben dan een thin server (tegenover, in het normale geval, een fat server ). Een belangrijk, bijkomend voordeel van zo’n thin server is dat er slechts de meest elementaire SQL -commando’s nodig zijn om de database aan te maken en te onderhouden. Die elementaire commando’s zijn voor vrijwel alle SQL -dialecten gelijk, waardoor de applicaties met slechts geringe aanpassingen kunnen worden geschiktgemaakt voor rdbms’en van andere leveranciers (Oracle, Microsoft, IBM , ...). Wanneer men het over client/server heeft, wordt meestal verondersteld dat de applicatieprogramma’s tenminste een deel van de business logic voor hun rekening nemen. Dat was immers juist de winst ten opzichte van de voorgangertechnologie, met mainframes. Van mainframe naar client/server De client/server-architectuur is de opvolger van een architectuur met één krachtige, centrale computer, mainframe genaamd, met ‘domme’ terminals. De terminals dienden uitsluitend voor weergave van resultaten en invoer van gegevens. Alle verwerking, zelfs de kleinste controle op correctheid van gegevens, vond op het mainframe plaats. Je zou kunnen zeggen: een extreem geval van een thin client. De stap naar client/server was een grote vooruitgang, vanwege de beperking van het netwerkverkeer. We moeten hierbij bedenken dat in de tijd waarin dit speelt (de jaren ’70) de bandbreedte van netwerken (de capaciteit in bits per tijdeenheid) gering was. Van client/server naar internet De opkomst van internet heeft nog niet het einde ingeluid van client/ server, maar heeft wel de thin client in ere hersteld. Het ligt wat subtiel: aan de clientkant is het eigenlijke applicatieprogramma nu de internetbrowser, die niet bepaald ‘thin’ is. Maar met het oog op een specifieke toepassing kunnen de via een webserver verstuurde webpagina’s als applicatie worden beschouwd. Denk bijvoorbeeld aan een

pagina met een bestelformulier. Hoewel dergelijke formulieren steeds intelligenter worden (door meer en meer ingebouwde logica, vaak met Javascript-programmacomponenten) zijn ze nog steeds als thin clients te beschouwen. Maar de nadelen van de vroegere thin clients gelden hier veel minder: de bandbreedte van het netwerkverkeer is enorm toegenoemen en vooral: de applicaties zijn zelf gegevens geworden die worden overgestuurd. Het beheerprobleem, voor zover samenhangend met verspreide locaties, bestaat niet meer. Minstens zo belangrijk als deze zuiver technische kwesties is de toegankelijkheid van serverapplicaties: nu ook van buiten een bedrijfsnetwerk. Behalve voor de eigen bedrijfsmedewerkers kunnen internetapplicaties ook voor externe doelgroepen (klanten of leveranciers!) worden ontwikkeld. De communicatie met een (relationele) database vindt in al deze gevallen plaats via een aantal softwarelagen ( tiers , uit te spreken met een ‘ie’), met aan de ene kant de clientlaag (internetbrowser) en aan de andere kant het rdbms. En het rdbms wordt nog steeds ‘toegesproken’ in SQL , maar de SQL -opdrachten worden nu verstuurd via een tussenlaag aan de serverkant.

1.3 Reijnders’ Toetjesboek In dit hoofdstuk en in beide volgende hoofdstukken staat één voorbeeldinformatiesysteem centraal: Reijnders’ Toetjesboek. Dit is een interactief receptenboek voor lekkere toetjes, op een computer die bij voorkeur is ingebouwd in het keukenaanrecht. Gezien deze ‘huiselijke’ context is het aannemelijk dat op die computer een lokale versie van het relationele databasemanagementsysteem (rdbms) draait. In principe echter kan het rdbms ook elders draaien, op een externe server. De applicatie op de ‘keukenclient’ communiceert daar dan mee via een netwerkverbinding. In Reijnders’ Toetjesboek kunt u recepten van toetjes opzoeken, u kunt ze veranderen en u kunt er uw eigen recepten aan toevoegen. U kunt zoeken volgens allerlei criteria, bijvoorbeeld snel klaar te maken en/of caloriearm en/of met aardbeien. Uit de ingrediëntinformatie wordt automatisch afgeleid hoeveel energie per persoon elk gerecht bevat, uitgedrukt in kilocalorieën (kcal). Er zijn beperkingen. Zo worden de ingrediënten in Reijnders’ Toetjesboek altijd gemeten in dezelfde maateenheid, ongeacht het gerecht. Bijvoorbeeld melk altijd in liters en dus nooit in eetlepels. Verder is de opgegeven bereidingstijd een vaste waarde, ongeacht het aantal personen. Figuur 1.3 toont een receptvenster, met informatie over het gerecht Coupe Kiwano. Dit is een voorbeeld van hoe een clientprogramma (applicatie) informatie op een grafische manier aan een gebruiker kan presenteren. De meeste van de gepresenteerde gegevens zijn opgehaald uit de (relationele)

database, waar ze in verschillende tabellen zijn opgeslagen. Een aantal echter zijn berekende waarden , die niet zijn opgeslagen, maar door de applicatie worden berekend. Eén waarde is ingevuld door de gebruiker.

Figuur 1.3 Receptvenster voor het gerecht Coupe Kiwano

Toelichting – Het aantal personen kan door de gebruiker worden ingesteld; de initiële waarde is 4. – De ingrediënthoeveelheden van een gerecht worden door de applicatie berekend uit de opgeslagen (maar niet afgebeelde) ingrediënthoeveelheden per persoon en het aantal personen.

– De energie per persoon van een gerecht wordt door de applicatie berekend uit de opgeslagen ingrediënthoeveelheden per persoon en de eveneens opgeslagen energiewaarden per eenheid. In de volgende paragrafen zullen we stap voor stap achterhalen wat de structuur is van de databasetabellen. Gaandeweg zullen we dan vertrouwd raken met de belangrijkste begrippen uit de relationele model , dat is de theorie over tabellen, de wijze waarop deze onderling samenhangen en de regels waaraan zij moeten voldoen.

Opgave 1.1 De energie per persoon van een gerecht is een afgeleide waarde die door het rdbms zelf wordt berekend uit de energiewaarden van de ingrediënten (per maateenheid) en de hoeveelheden per persoon. Reken dit (uitgaande van figuur 1.3 ) na voor Coupe Kiwano, als gegeven is dat in de database de volgende (in het venster niet zichtbare) energiewaarden zijn opgeslagen: ijs kiwano slagroom suiker tequila

1600 kcal per liter 40 kcal per stuk 336 kcal per deciliter 4 kcal per gram 30 kcal per eetlepel

Opmerking : de uitwerking van deze opgave en van alle andere opgaven vindt u in de Boekverkenner.

1.4 Alle gegevens in één tabel In deze paragraaf zullen we een opslagstructuur bespreken (en verwerpen) waarbij alle gegevens in één tabel worden ondergebracht. Daarbij komen enkele belangrijke begrippen aan de orde: redundantie en herhalende groep .

1.4.1 Eerste stap naar relationele opslagstructuur Figuur 1.4 bevat drie tabellen met gerechtinformatie: één voor Coupe Kiwano, één voor Glace Terrace en één voor Mango Plus Plus. Per recept zijn vijf kenmerken opgenomen:

– de naam van het gerecht – de energie per persoon (in kcal) – de bereidingstijd – de bereidingswijze – de ingrediëntinformatie. De eerste vier hiervan zijn enkelvoudige gegevens : tekst of getalwaarden. In andere gegevensverzamelingen zijn ook andere enkelvoudige waarden mogelijk, zoals kalenderdata, of een foto of video-fragment. Het criterium voor ‘enkelvoudig gegeven’ is dat het rdbms zich niet hoeft bezig te houden met de interne structuur ervan.

Figuur 1.4 Schematische weergave van drie receptvensters

Het vijfde kenmerk, de ingrediëntinformatie, is samengesteld : het is niet één waarde, maar een hele subtabel van waarden. Elke rij van zo’n subtabel bevat gegevens over één ingrediënt in het betreffende gerecht: – de product van het ingrediënt – de hoeveelheid per persoon

– de maateenheid waarin het product wordt gemeten – een kolom ‘energie per eenheid’, gemeten in kcal. Merk, afgezien van de subtabelweergave, de volgende verschillen op met f iguur 1.3 . – Het ‘aantal personen’ is verdwenen; de waarde hiervan wordt immers niet in de database bewaard.

– De ‘hoeveelheden’ (voor het boodschappenlijstje) komen niet meer voor. Deze worden door de applicatie berekend en hoeven dus niet te worden opgeslagen. De hoeveelheden per persoon zijn ervoor in de plaats gekomen. Hieruit en uit het aantal personen dat door een gebruiker wordt ingevuld kan de benodigde hoeveelheid van een ingrediënt worden berekend. – Bij de producten is de ‘energie per eenheid’ erbij gekomen; de Toetjesboek-applicatie heeft deze nodig om de energie per persoon van elk gerecht te kunnen berekenen. In tegenstelling tot de hoeveelheden wordt de energie per persoon wel in de database opgeslagen. Het rdbms bevat een mechanisme waardoor ze automatisch worden herberekend als dat nodig is. De ‘peper’ in Glace Terrace heeft wat bijzonders: de hoeveelheid ontbreekt, evenals de eenheid en de energie per eenheid. Welnu, dat kan: dat is gewoon ‘naar smaak’! Afleidbare gegevens: in database of applicatie? Het was mogelijk geweest de hoeveelheden en de energiewaarden per persoon gelijk te behandelen: – allebei opslaan in de database (met een mechanisme om hun waarden te laten herberekenen als dat nodig is)

– allebei berekenen in de applicatie (vlak voor ze worden getoond). Bewust hebben we het verschillend gedaan om beide mogelijkheden te illustreren. De vraag wanneer in de praktijk voor het één of voor het ander wordt gekozen, laten we in het midden.

1.4.2 Eén tabel met subtabellen Alle genoemde kenmerken voor de drie gerechten kunnen we onderbrengen in één grotere tabel, met drie rijen en vijf kolommen , zie fig uur 1.5 . Merk op dat alle gegevens van één gerecht nu in één rij staan, en overeenkomstige kenmerken van gerechten in één kolom. Elke kolom heeft een kolomnaam . Door deze wijze van weergeven hebben we bereikt dat de logische rijstructuur overeenkomt met de fysieke rijstructuur . Dat wil zeggen dat een logische rij (de gegevens over één ‘ding’) horizontaal wordt getekend, en een logische kolom (overeenkomstige gegevens van de ‘dingen’) verticaal. Enkele kolomkoppen zijn wat compacter genoteerd: energie PP voor ‘energie per persoon’, energie PE voor ‘energie per eenheid’ en hoeveelheid PP voor ‘hoeveelheid per persoon’. Het is belangrijk de samengestelde kolom ingrediënten als één enkele kolom te zien! Het ‘snijpunt’ van een rij en een kolom heet een cel . De cellen in de kolommen gerecht, energie PP , bereidingstijd en bereidingswijze bevatten een enkelvoudige waarde. De cellen in kolom ingrediënten bevatten een samengestelde waarde: elke cel bevat een subtabel met vier kolommen. De kolom ingrediënten is dan ook de enige kolom waarvan de naam een meervoudsvorm is. De lichtgrijze achtergrond in kolom energie PP duidt aan dat de gegevens in deze kolom afleidbaar zijn. Merk op dat de kolom met gerechtnamen ‘naam’ heet en niet ‘gerecht’. In het algemeen geven we een kolom nooit dezelfde naam (ongeacht hoofdof kleine letter) als een tabel. De kolomnaam ‘naam’ drukken we uit dat een gerecht een naam heeft. (Een gerecht heeft geen gerecht!)

Figuur 1.5 Alle gegevens in één tabel, met drie rijen en vijf kolommen

Is dit nu een geschikte tabelstructuur om de gegevens in de database op te slaan? Nee, want er kleven maar liefst drie bezwaren aan opslag volgens deze structuur: Bezwaar 1 : Bezwaar 2 : Bezwaar 3 :

bepaalde informatie wordt meer dan één keer opgeslagen een tabelstructuur met subtabellen maakt het databasebeheer erg ingewikkeld gegevens over producten worden alléén in de context van gerechten opgeslagen, terwijl producten een zelfstandig belang kunnen hebben.

Meervoudige informatieopslag ( redundantie ), kolommen met subtabellen ( herhalende groepen ) en het principe dat alle ‘soorten dingen’ een gelijkwaardige behandeling verdienen, worden in het vervolg van dit hoofdstuk uitgebreid behandeld.

1.4.3 Redundantie Op twee plaatsen wordt vermeld dat ijs wordt gemeten in ‘liter’. Dit is vaker dan nodig; producten worden immers in Reijnders’ Toetjesboek altijd in dezelfde eenheid gemeten. IJs bijvoorbeeld in liter en aardbeien in gram. Wanneer u dus bij Coupe Kiwano leest dat ijs in liter wordt gemeten, weet u zonder verder te kijken dat dit ook voor Glace Terrace geldt, en omgekeerd. We noemen dat: redundantie , ofwel redundante gegevensopslag . Redundant betekent overtollig: bepaalde gegevens zijn afleidbaar uit andere gegevens in de database. Zou u in de rij van Glace Terrace ‘liter’ bij ‘ijs’ wegstrepen, dan kon u het reconstrueren door bij Coupe Kiwano de eenheid van ‘ijs’ op te zoeken. Zie figuur 1.6 . Die reconstrueerbaarheid uit andere gegevens is hét kenmerk van redundante opslag. Ook de kolom energie PE geeft aanleiding tot redundantie: van ijs wordt twee keer vermeld wat zijn energiewaarde is.

Figuur 1.6 Redundante gegevensopslag: reconstrueerbaarheid uit de context

Redundante opslag kost extra opslagruimte, maar dat hier nauwelijks een bezwaar. Een echt bezwaar is het gevaar dat de gegevens onderling niet meer kloppen. Stel bijvoorbeeld dat bij Coupe Kiwano staat dat ijs 1600 kilocalorie per maateenheid bevat, en bij Glace Terrace dat het 1450 is, zie figuur 1.7 . Eén van de twee moet dan fout zijn. We spreken van inconsistentie ofwel inconsistente gegevensopslag . Inconsistente gegevens zijn gegevens waarvan de betekenissen onderling strijdig zijn. Redundantie brengt vaak het gevaar van inconsistentie met zich mee. In de volgende paragraaf zullen we zien dat dit niet altíjd het geval is.

Figuur 1.7 Inconsistente gegevensopslag

Merk op dat we in het voorgaande enerzijds spreken over gegevens , anderzijds over informatie . Bij gegevens gaat het om afzonderlijke waarden, bij informatie om de betekenis van die waarden, die veelal alléén is uit te drukken in combinatie met andere waarden. Hoewel redundantie altijd te maken heeft met de betekenis van gegevens, zullen we toch ook wel spreken over redundante gegevens . Dat zijn dan gegevens die reconstrueerbaar zijn uit andere gegevens, op grond van betekenissen. In paragraaf 1.6 gaan we dieper in op het onderscheid tussen gegevens en informatie.

1.4.4 Gecontroleerde redundantie Behalve de kolommen eenheid en energie PE bevat figuur 1.6 nóg een kolom die aanleiding geeft tot redundantie: de kolom energie PP , die de energie per persoon van een gerecht aangeeft, gemeten in kilocalorieën. Deze kolom bevat zelfs uitsluitend redundante gegevens; de waarden worden immers berekend uit energie PE en hoeveelheid PP (hoe precies?). Dit is een geval van gecontroleerde redundantie : het is de bedoeling dat de gebruiker de berekende energiewaarde niet kan veranderen. Daardoor bestaat er geen gevaar voor inconsistente gegevens. Het mechanisme om de juiste waarde steeds opnieuw te berekenen, is gebaseerd op triggers , een bepaald type programmaatjes waarvan de code in de database is opgenomen. Zou de gebruiker bijvoorbeeld een hoeveelheid per persoon veranderen, dan zal het systeem automatisch de juiste trigger aanroepen en deze een nieuwe waarde voor de energiewaarde van het gerecht laten berekenen. Wijzigt de gebruiker een energiewaarde van een product, dan zal automatisch een andere trigger worden aangeroepen, die de energiewaarden per persoon aanpast van alle gerechten waar dat product in voorkomt. Het programmeren van triggers (in een programmeertaal die een uitbreiding biedt op de mogelijkheden van SQL ) wordt besproken in hoofdstuk 12 .

1.4.5 Herhalende groepen Redundantie was één probleem van de tabelstructuur van figuur 1.5 . Het tweede probleem was de kolom ‘ingrediënten’, die voor elke gerechtrij een subtabel met ingrediëntgegevens bevat. Zo’n samengestelde kolom, die per rij van de hoofdtabel een subtabel bevat, wordt een herhalende groep genoemd (Engels: repeating group ). Met ‘groep’ worden de kolommen van de subtabellen bedoeld. Het predikaat ‘herhalend’ is helaas wat ongelukkig gekozen. Herhalende groepen en redundantie Wat is er eigenlijk mis met een herhalende groep? Op het eerste gezicht lijkt het dat de herhalende groep verantwoordelijk is voor de redundantie in de kolommen eenheid en energie PE . Dit is echter maar schijn, want we zullen nog zien dat we de tabel op twee manieren kunnen omvormen: – tot twee gerelateerde tabellen zonder redundantie, maar met herhalende groep

– tot twee gerelateerde tabellen met redundantie, maar zonder herhalende groep. Bezwaren van herhalende groepen ‘Herhalende groep’ en ‘redundantie’ staan dus los van elkaar. Echte bezwaren van herhalende groepen zijn: – het databasebeheer wordt er veel ingewikkelder door – ze leiden tot een ingewikkelder databasestaal – gebrek aan symmetrie: het ene ‘soort ding’ wordt heel anders behandeld dan het andere ‘soort ding’. Ingewikkelder databasebeheer Zouden de Toetjesboekgegevens volledig per gerecht worden opgeslagen, met een herhalende groep van ingrediënten, dan zou het invoegen van nieuwe rijen op twee niveaus moeten gebeuren, zie figuur 1.8 : 1 binnen een subtabel, bij het invoegen van een nieuw ingrediënt bij een bestaand gerecht 2 binnen de hoofdtabel, bij het invoegen van een nieuw gerecht; hierbij moeten de gewone, enkelvoudige gerechtgegevens worden ingevoerd, maar ook een subtabel van ingrediënten, wat een heel ander soort bewerking is.

Figuur 1.8 Invoegen van rijen op twee niveaus

Ingewikkelder gegevenstaal Ook het verwijderen van rijen wordt ingewikkelder: het verwijderen van een rij in een subtabel is een ander soort bewerking dan het verwijderen van een volledige gerechtrij. Net zoiets geldt voor het wijzigen van gegevens en het opvragen van overzichten. We zullen nog zien dat al die bewerkingen en opvragingen worden gerealiseerd door opdrachten die zijn geformuleerd in een gegevenstaal . Die taal zou een stuk ingewikkelder worden wanneer we tabellen met herhalende groepen toelieten. Gebrek aan symmetrie Producten die in gerechten als ingrediënt voorkomen, worden alleen in de context van een gerecht opgeslagen. Dat is jammer, want ze hebben ook een belang van zichzelf. Je zou bijvoorbeeld een calorieëntabel kunnen samenstellen uit alle productinformatie, los van gerechten. Een neutrale, contextvrije benadering van de verschillende ‘soorten van dingen’ zal een van de belangrijkste kenmerken blijken te zijn van een goedgestructureerde relationele database. Later zullen we hieraan het predikaat ‘genormaliseerd’ toekennen. In de volgende paragraaf zullen we de rollen eens omkeren: producten krijgen daar de hoofdrol, terwijl de gerechten daaraan ondergeschikt worden gemaakt.

1.4.6 Omkering van herhalende groepen Een gerecht kan meerdere producten als ingrediënt bevatten. Daarom is de samengestelde kolom ‘ingrediënten’ een herhalende groep, zie figuur 1.5 . Omgekeerd kunnen we stellen: een product kan als ingrediënt voorkomen in meerdere gerechten. De consequentie is dat we ‘gerechten’

moeten kunnen weergeven als herhalende groep ten opzichte van producten. Het is niet moeilijk die omkering uit te voeren, zie figuur 1.9 .

Figuur 1.9 Gerechten als herhalende groep bij ingrediënten

De tabel heet nu Product: er is één rij per product. Er zijn vier kolommen, waarvan er één samengesteld is: per product is er een subtabelletje van gerechtgegevens. De subtabelletjes hebben vier kolommen met ‘pure’ gerechtinformatie. De vijfde kolom echter, hoeveelheid PP , bevat informatie die betrekking heeft op de combinatie van een product en een gerecht. Vandaar dat deze zowel in figuur 1.5 als in figuur 1.9 deel uitmaakt van de herhalende groep. Merk op dat de tabelstructuur van Product, in tegenstelling tot die van Gerecht, toelaat om producten zonder gerechten op te nemen. Bij een product zonder gerecht wordt de gerechtencel leeg gelaten. De omkering illustreert het punt ‘gebrek aan symmetrie’ van paragraaf 1.2.5. We kunnen het ook ‘willekeur’ noemen. Een weergave met herhalende groepen behandelt de ‘dingen’ (gerechten en producten) waarvan eigenschappen in de tabel voorkomen, ongelijkwaardig. De kracht van een goed model is juist gelegen in een gelijkwaardige behandeling, onafhankelijk van de manier waarop een gebruiker later, via een applicatie, de gegevens gepresenteerd wil zien. Ook moet het mogelijk zijn later geheel nieuwe toepassingen bij dezelfde database te realiseren. In de volgende paragraaf zullen we transformaties uitvoeren waarbij we de herhalende groep en de redundantie kwijtraken.

1.5 Verbeterde tabelstructuren Herhalende groepen en redundantie komen voort uit het feit dat er informatie over meerdere ‘soorten dingen’ in één tabel zit. In de volgende paragrafen worden zowel de herhalende groep als de gesignaleerde redundantie geëlimineerd door een bepaalde vorm van tabelsplitsing (of decompositie ) toe te passen. Het uiteindelijke resultaat voldoet aan het principe ‘elk soort ding zijn eigen tabel’. Van nu af zullen we de ‘dingen’ in de werkelijkheid (concreet of abstract) aanduiden met entiteiten en ‘soorten van dingen’ met entiteittypen . We sluiten daarbij aan bij veelgebruikte terminologie uit de theorie en de praktijk van het informatiemodelleren. In deze terminologie luidt het principe: ‘elk entiteittype zijn eigen tabel’.

1.5.1 Elimineren van de herhalende groep Om de herhalende groep in de tabel van figuur 1.5 kwijt te raken, is een eenvoudige ingreep voldoende. Deze komt er ongeveer op neer dat we de herhalende groep er ‘afknippen’, zie figuur 1.10 . Zouden we het bij knippen laten, dan zou er informatie verloren gaan: hoe weten we dan nog bijvoorbeeld dat de bovenste rij (ijs; 0.15; liter; 1600) bij het gerecht Coupe Kiwano hoort? Vandaar dat nog een kolom is toegevoegd voor het bijbehorende gerecht.

Figuur 1.10 Structuurtransformatie: elimineren herhalende groep

De resultaatstructuur omvat twee tabellen zonder herhalende groep. Elke cel bevat een enkelvoudige waarde; er zijn geen cellen meer met een subtabel als waarde. De afgesplitste tabel heeft de passende naam Ingredient gekregen. De waarden in de toegevoegde kolom gerecht van Ingredient verwijzen naar de waarden in de kolom naam van Gerecht. De redundantie is echter door de nieuwe structuur niet geëlimineerd. Die moeten we dus nog zien kwijt te raken.

1.5.2 Elimineren van redundantie Het redundant voorkomen van eenheid- en energie PE -waarden in Ingredient wordt veroorzaakt doordat dat pure productkenmerken zijn, terwijl Ingredient geen pure producttabel is. De ingredienten in deze tabel zijn producten-in-gerechten, en producten komen daar meervoudig in voor. De oplossing is een ‘pure’ productentabel Product te maken. In Product wordt van elk product eenmalig de productnaam vermeld met (ook eenmalig!) de waarden van de bijbehorende eenheid en energie PE . In Ingredient kunnen nu de kolommen eenheid en energie PE worden geschrapt. Zie figuur 1.11 .

Figuur 1.11 Structuurwijziging: elimineren van redundantie

Er zijn nu twee kolommen met kolomnaam ‘naam’. We kunnen deze als volgt van elkaar onderscheiden: Gerecht.naam en Product.naam. We zullen deze puntnotatie (tabelnaam+punt+kolomnaam) vaak gebruiken, ook voor unieke kolomnamen. Ook in SQL zullen we ons van de puntnotatie bedienen.

De kolomwaarden van Ingredient.gerecht verwijzen naar de kolomwaarden van Gerecht.naam. En die van Ingredient.product verwijzen naar die van Product.naam. Deze verwijzingen vormen het ‘cement’ tussen de tabellen. De kolom Ingredient.hoeveelheid PP bevat het enige kenmerk dat echt bij de combinatie van een gerecht en een product hoort. De niet-gecontroleerde redundantie is verdwenen. Er is nog steeds de gecontroleerde redundantie van kolom energie PP , maar daar is geen bezwaar tegen, integendeel.

1.5.3 Alternatief: eerst elimineren van redundantie In paragraaf 1.2.5 hebben we betoogd dat herhalende groepen en redundantie losstaande fenomenen zijn. Ter illustratie zullen we nu éérst de redundantie elimineren en daarna de herhalende groep afsplitsen. De eerste stap is weergegeven in figuur 1.12 . De resultaatstructuur is vrij van redundantie, maar heeft nog wel de herhalende groep. Het enige verschil is dat deze nu twee (sub)kolommen minder heeft. Zo zien we wat eerder werd geclaimd: dat we redundantie kunnen hebben zonder herhalende groep, of een herhalende groep zonder redundantie. Deze twee fenomenen zijn dus niet zo nauw verbonden als op het eerste gezicht misschien lijkt. De herhalende groep van de onderste Gerecht-tabel in figuur 1.12 kunnen we eenvoudig kwijtraken volgens het in figuur 1.10 geïllustreerde recept. Het eindresultaat is dan hetzelfde als bij de eerdere werkwijze: de resultaatstructuur van figuur 1.11 .

Figuur 1.12 Elimineren van redundantie, vóór afsplitsen van herhalende groep

1.5.4 Elk entiteittype zijn eigen tabel In de oorspronkelijke tabel, met een volledig recept per rij, waren producten ondergeschikt aan gerechten. Dat kwam doordat die structuur vanuit één bijzonder gezichtspunt was ontworpen: het gezichtspunt van de recepten. Deze structuur maakt het onmogelijk om een product op te slaan

zonder tegelijk ook een gerecht op te nemen waarin dat product als ingrediënt voorkomt. Terwijl het heel aardig zou zijn om helemaal los van recepten een aparte lijst aan te leggen met producten als mogelijke ingrediënten , bijvoorbeeld om te gebruiken voor een calorieëntabel. In paragraaf 1.2.5 wezen we hier al op en op ‘gebrek aan symmetrie’. Er vindt een ongelijkwaardige behandeling plaats van verschillende typen informatie, die ook los van elkaar van belang kunnen zijn voor de gebruiker. De structuur van figuur 1.11 behandelt gerechten en producten op een meer neutrale en gelijkwaardige manier. Gerechten en producten worden als zelfstandige entiteiten beschouwd. Elk entiteittype heeft zijn eigen tabel. Ook Ingredient hoort tot zo’n entiteittype: dat van de ‘ingrediëntregels’ bij een gerecht. Merk op dat de database producten kan bevatten die in geen enkel gerecht voorkomen. Dit wordt geïllustreerd in figuur 1.13 : hier is de tabel Product uitgebreid met een rij voor het product banaan, dat in geen enkel gerecht als ingrediënt wordt gebruikt. Deze figuur bevat nóg een verandering: een aparte tabel voor (gestandaardiseerde) eenheden, zie hiervoor de volgende paragraaf. Hoe entiteiten en entiteittypen nog meer kunnen heten In dit boek gebruiken we de term ‘entiteittype’ als classificatieterm voor ‘dingen’ die vanuit het oogpunt van een informatiebehoefte overeenkomstige kenmerken hebben. Eigenlijk gaat het daarbij nooit om de concrete dingen zelf, maar om een abstractie daarvan: de kenmerken die ertoe doen. In andere teksten kan men voor ‘entiteittype’ andere termen tegenkomen, zoals ‘entiteit’, ‘objecttype’ of ‘klasse’. Een mooie omschrijving vinden we zelf ‘soort van ding’. Een ‘ding’ zelf kan behalve ‘entiteit’ ook ‘entiteitinstantie’ of ‘object’ heten.

1.5.5 Normaliseren en standaardiseren De drie tabellen in het onderste deel van figuur 1.11 vormen een zogenaamde volledig genormaliseerde structuur . Dat wil zeggen dat er geen herhalende groepen zijn en geen redundantie. Het verwijderen van herhalende groepen en redundantie, door tabellen van een grotere tabel af te splitsen, heet normaliseren . De theorie waarin dergelijke structuurtransformaties worden bestudeerd, heet normalisatietheorie ; het is een klassiek onderdeel van de theorie van relationele databases. In hoofdstuk 5 gaan we hier dieper op in. Nauw verbonden met normaliseren is standaardiseren van bepaalde gegevens. Ook dit kunnen we toelichten aan de hand van het Toetjesboek. We zullen zien hoe we het gebruik van de namen van eenheden in de tabel Product kunnen standaardiseren.

Allereerst moeten we vaststellen dat standaardisatie van de namen van eenheden door de huidige structuur niet wordt afgedwongen. Niets verbiedt ons immers om bijvoorbeeld ‘slagroom’ te meten in ‘deciliter’ en zure room in ‘dl’, hoewel dat dezelfde eenheid is. Een relationele structuur biedt een eenvoudige manier om standaardisatie van gegevens af te dwingen; we hoeven slechts een aparte tabel aan te leggen van de gestandaardiseerde namen of waarden. Voor het Toetjesboek geeft dit nogmaals een uitbreiding van de tabelstructuur: er komt een tabel Eenheid bij, met gestandaardiseerde eenheidnamen, zie figuur 1.13 .

Figuur 1.13 Genormaliseerde gerechten-en-productendatabase, met gestandaardiseerde eenheden

Als de verantwoordelijke gebruiker of beheerder nu in Eenheid toch zowel ‘dl’ als ‘deciliter’ opneemt, dan is dat de eigen verantwoordelijkheid: niet het rdbms, maar de verantwoordelijke gebruiker is de ‘baas van de waarheid’.

De verwijzing van Product naar Eenheid (aangegeven door de pijl) houdt in dat elke waarde in Product.eenheid (dat is kolom eenheid in Product) ook moet voorkomen in Eenheid.naam (kolom naam in Eenheid). We zien dat normalisatie alléén niet altijd standaardisatie afdwingt waar dat wel gewenst is. Gestandaardiseerde opslag volgens ‘single point of definition’ Samengevat heeft de structuur van figuur 1.13 de volgende voordelen boven de alles-in-één-tabelbenadering van figuur 1.5 . – Elk entiteittype heeft zijn eigen tabel, waardoor over alle dingen van een bepaald soort informatie kan worden opgeslagen los van andersoortige dingen. – Single point of definition – Informatie wordt als regel enkelvoudig opgeslagen, waardoor die maar op één plek hoeft te worden beheerd: single point of definition . Hierdoor bestaat geen gevaar voor ongecontroleerde redundantie en evenmin voor inconsistente gegevensopslag. – De structuur maakt standaardisatie van gegevens mogelijk.

1.5.6 Structuur en populatie De rijen van een tabel vormen met elkaar de populatie van die tabel. Alle rijen van alle tabellen vormen de populatie van de database. De populatie, of een deel ervan, kunnen we grafisch weergeven in een populatiediagram , zoals in figuur 1.13 . Wanneer we de populatie wegdenken, houden we de structuur over: de opbouw van tabellen uit hun kolommen en de onderlinge verwijzingen. Zie figuur 1.14 voor zo’n databasestructuurdiagram, dat in deze speciale grafische vorm een strokendiagram wordt genoemd.

Figuur 1.14 Strokendiagram voor Reijnders’ Toetjesboek (versie 1)

Afleidbaarheid geven we in een strokendiagram aan door een slash voor de kolomnaam: /energie PP geeft dus aan dat kolom energiePP gegevens bevat, die – in dit geval via een berekening – uit andere databasegegevens afleidbaar zijn. De structuur van een database is een tamelijk vast gegeven. Een eindgebruiker kan er geen veranderingen in aanbrengen. De populatie daarentegen verandert zodra de eindgebruiker nieuwe gerechten, producten of eenheden toevoegt, of ze juist verwijdert, of er veranderingen in aanbrengt. De populatie van de database, en dus ook de veranderingen die een gebruiker daarin wil aanbrengen, zijn onderworpen aan een flink aantal beperkingen. Deze beperkingen worden vastgelegd in allerlei soorten regels: beperkingsregels (ook constraints geheten) Hierover gaat het volgende hoofdstuk.

1.5.7 Tabellen en relaties In de term relationele database komt het woord relatie voor, dat hier een specifieke betekenis heeft. Wanneer we ons wat slordig uitdrukken, kunnen we zeggen dat relatie de wiskundige term is voor tabel. Iets preciezer: een relatie is een verzameling van rijen . En een verzameling (in de wiskundige betekenis) kent geen volgorde. Natuurlijk zijn we bij weergave op papier gedwongen één bepaalde volgorde te kiezen, maar die volgorde is als het ware ‘toevallig’ en van geen belang. Door een andere volgorde te kiezen, krijgen we geen andere relatie. Evenmin hebben de kolommen een speciale volgorde, want ook een rij wordt gezien als een

verzameling: een verzameling van aan een kolomnaam gekoppelde waarden. Het begrip relatie verschilt dus van het begrip tabel uit het dagelijks spraakgebruik: een relatie kent geen volgorde van rijen en kolommen. Er is nog een verschil: in een relatie zijn alle rijen verschillend , ook al noteren we ze meer dan één keer en ook al zouden we een rij fysiek meervoudig opslaan in een database. Als relatie zijn dus de tabellen in figu ur 1.15 (met vereenvoudigde versies van Gerecht) gelijk.

Figuur 1.15 Tabel als relatie

Wanneer we in dit boek de term tabel gebruiken, zal dat meestal synoniem zijn met ‘relatie’. Dan speelt de eventuele volgorde van rijen en kolommen, zoals we die op papier of beeldscherm zien, dus geen rol. Rijen en tupels Zoals de term rij zich verhoudt tot tabel , zo verhoudt de term tupel zich tot relatie : tupels zijn de ‘rijen’ van een relatie, in een meer formeel/wiskundige betekenis. De tupels van een relatie vormen een verzameling; ze zijn dus alle verschillend en hebben geen volgorde. Een veelgebruikte naam voor de ‘tupelpopulatie’ is extensie . Wanneer we de termen rij en tabel zuiver relationeel interpreteren, is er geen verschil met ‘tupel’ en ‘relatie’. Helaas maakt de aard van de taal SQL het wenselijk om toch onderscheid te maken. Al zullen we steeds ons best doen om SQL als een relationele taal te zien en SQL-resultaattabellen als relaties te interpreteren, dit zal niet altijd houdbaar zijn. SQL blijkt maar ten dele relationeel; we zullen meermalen voorbeelden zien waarbij volgorde wel degelijk een rol speelt en waarbij gelijke rijen voorkomen die los van elkaar worden behandeld.

Opgave 1.2 We willen Reijnders’ Toetjesboek aanvullen met een recept voor Reijnders Roem. Dit is een nagerecht dat, voor 10 personen, bestaat uit: 5 kiwano’s, 2 mango’s, 1 meloen en 10 eetlepels pernod. Gegeven is dat een meloen 150 kcal bevat. De bereidingswijze luidt: ‘Snijd het fruit in stukjes, vermeng alles met de pernod en laat dit een uurtje staan alvorens het op te dienen.’ De bereidingstijd is 12 minuten. Welke rijen komen erbij in de tabellen? Ga hierbij uit van figuur 1.13 en een energie PP die is afgerond op een gehele waarde. Opgave 1.3 Geef voor de acties a t/m d aan welke tabellen moeten worden gewijzigd wanneer een gebruiker (via het Toetjesboek) de actie uitvoert. Geef steeds

per tabel aan of het gaat om het toevoegen, het verwijderen, of het wijzigen van een rij (of van meerdere rijen). Ga voor elke actie uit van de populatie in figuur 1.13 . Laat wijziging van de bereidingswijze buiten beschouwing. a Verwijderen van het gerecht Mango Plus Plus. b Verwijderen van het product banaan. c Toevoegen van 1 kiwi per persoon aan het gerecht Mango Plus Plus (een kiwi bevat 30 kcal per stuk). d Wijzigen van de maateenheid van pernod: niet meer in eetlepels, maar in koffielepels (één eetlepel is 2.5 koffielepel).

Opgave 1.4 Een gebruiker wil het product mango verwijderen. Het Toetjesboekprogramma zal dit echter niet zomaar toelaten. Waarom niet?

1.6 Gegevens en informatie De termen ‘gegeven’ en ‘informatie’ worden vaak door elkaar gebruikt. In deze paragraaf proberen we een helder onderscheid te formuleren, via een taalkundige benadering.

1.6.1 Informatie als verwoording van gegevens Als voorbeeld nemen we weer het Toetjesboek en zullen laten zien dat de vragen ‘Welke gegevens bevat het Toetjesboek?’ en ‘Welke informatie bevat het Toetjesboek?’ hele verschillende vragen zijn. Gegevens Wanneer we de Toetjesboek-populatie zien als een verzameling gegevens , denken we primair aan de vorm van die ‘gegevens’ en aan de regels waaraan die vormen moeten voldoen. Zo kunnen we over het gegeven ‘Coupe Kiwano’ in kolom Gerecht.naam zeggen dat dit 12 tekens telt. Een gegevensaspect van de kolom als geheel

is bijvoorbeeld de vormregel dat het maximum aantal tekens in een cel 25 bedraagt. De vormaspecten van een taal vatten we samen onder de term syntaxis . Het begrip ‘gegeven’ is dus vooral een syntactisch begrip. Daarnaast denken we bij ‘gegeven’ ook aan dingen als opslag en verwerking. Informatie De Toetjesboekgegevens hebben naast een vorm ook betekenis ( semantiek ) voor iedereen die de Nederlandse taal beheerst en ‘deskundig’ is op het gebied van recepten. Wanneer we aan een gegeven of een combinatie van gegevens betekenis hechten, gebruiken we de term informatie . ‘Informatie’ is dus een semantisch begrip. Informatie is expliciet uit te drukken door zinnen in natuurlijke taal. Bijvoorbeeld, het gegeven ‘Coupe Kiwano’ in kolom Gerecht.naam betekent: “er bestaat een gerecht genaamd Coupe Kiwano”. Preciezer: “er bestaat één gerecht genaamd Coupe Kiwano”. Op grond van dat ‘één’ mogen we in andere zinnen spreken over “hét gerecht genaamd Coupe Kiwano” of kortweg “gerecht ‘Coupe Kiwano”. (Als er twee gerechten konden bestaan met die naam, zouden we dat niet mogen zeggen.) Vaak heeft een gegeven geen betekenis op zichzelf maar alleen in combinatie met één of meer andere gegevens. Bijvoorbeeld, het gegeven ‘gram’ in de vierde rij van tabel Product heeft alleen betekenis in combinatie met het gegeven ‘suiker’: ‘het product suiker wordt gemeten in gram’. Informatie via verwoording Dit vormen van natuurlijke-taalzinnen om de betekenis van gegevens (dus informatie) uit te drukken, wordt de verwoording van gegevens genoemd. Bij het ontwerpen of het begrijpen van een database zijn die verwoordingen van groot belang. De structuur van een database is immers nauw verbonden met de betekenissen van de gegevens. Zonder verwoording kan de betekenis van een gegeven vaag of dubbelzinnig zijn. In zulke gevallen moeten we ons afvragen of de structuur wel correct is. We geven voor één voorbeeldrij per tabel alle relevante verwoordingen, die we hebben opgetekend uit de mond van een denkbeeldige deskundige.

– er bestaat één eenheid met de naam ‘liter’

– er bestaat één gerecht met de naam ‘Coupe Kiwano’ – gerecht Coupe Kiwano heeft energiewaarde 431 kcal per persoon – gerecht Coupe Kiwano heeft bereidingstijd 20 minuten – gerecht Coupe Kiwano heeft bereidingswijze ‘Schil ...’

– er bestaat één product met de naam ‘ijs’ – product ijs wordt gemeten in eenheid liter – product ijs heeft energiewaarde 1600 kcal per eenheid

– er bestaat één ingrediënt ‘ product ijs in gerecht Coupe Kiwano ’

– het ingrediënt product ijs in gerecht Coupe Kiwano heeft een hoeveelheid van 0.15 producteenheden per persoon Toelichting – Elke eerste zin claimt het bestaan van een ‘ding’, via de manier waarop dat ‘ding’ uniek kan worden aangeduid. Op basis daarvan mogen die ‘dingen’ in andere zinnen voorkomen – In andere zinnen worden van die ‘bestaande dingen’ (cursief weergegeven) eigenschappen geformuleerd. Zo is het een eigenschap van Coupe Kiwano dat het een bereidingstijd heeft van 20 minuten. En een eigenschap van het (Coupe Kiwano, ijs)-ingrediënt dat de hoeveelheid 0.15 ijseenheden (liter) bedraagt.

1.6.2 Atomaire informatie De zinnen in de vorige paragraaf zijn alle atomair (of elementair ), dat wil zeggen dat ze niet zonder verlies aan informatie gesplitst kunnen worden in zinnen die elk minder ‘gegevensplaatsen’ hebben. Een voorbeeld van een niet-atomaire zin (met drie – vet gemarkeerde – gegevensplaatsen) is:



Het gerecht Coupe Kiwano heeft een bereidingstijd van 20 minuten en als bereidingswijze ‘ Schil ... ’

Deze zin kan immers worden gesplitst in twee ‘kleinere’ zinnen, die elk minder gegevensplaatsen hebben:





Het gerecht Coupe Kiwano heeft een bereidingstijd van 20 minuten.

Het gerecht Coupe Kiwano heeft als bereidingswijze ‘ Schil ... ’

Atomaire zinnen vertegenwoordigen de kleinste eenheden van informatie. Het zijn als het ware ‘informatieatomen’. Discussies over databasestructuur kunnen vaak het beste aan de hand van atomaire zinnen worden gevoerd. Dan weten we namelijk precies waar we het over hebben. Een gebruiker die een database vult met gegevens, vult – vanuit semantisch standpunt gezien – de database eigenlijk met atomaire zinnen. Sterker nog, het zijn niet zomaar zinnen, maar zinnen waarvoor de gebruiker waarheid claimt. Wanneer we een ware zin een feit noemen, kunnen we dus zeggen dat een database is gevuld met feiten . En elke atomaire zin drukt een atomair feit uit. Vanzelfsprekend hebben we het hier niet over waarheid in een filosofische of objectieve betekenis. Ons waarheidsbegrip is pragmatisch , dat wil zeggen gebaseerd op menselijke beslissingen. Normalisatie, standaardisatie en databaseontwerp Er bestaan vele methoden om een relationele database te ontwerpen die voldoet aan eisen van normalisatie en standaardisatie, zoals in dit hoofdstuk besproken. Dat het eindresultaat aan die eisen moet voldoen, wil nog niet zeggen dat een database in de praktijk ook volgens de besproken stappen wordt ontworpen. Dat is ook zeker niet het geval. Dit is geen ontwerpcursus, dus het is hier niet de plaats om op praktijkmethoden in te gaan. Wat we wel willen zeggen, is dat alle goede ontwerpmethoden als uitgangspunt hebben: ‘elk entiteittype zijn eigen tabel’. Die eigen tabel bevat minstens één gestandaardiseerd identificatiekenmerk en eventueel nog extra eigenschappen. De methoden verschillen echter in de manier om vast te stellen welke entiteittypen er zijn en welke kenmerken daar bijhoren. Er zijn extreme verschillen, variërend van een zuiver intuïtieve aanpak tot een gedegen taalkundige benadering via atomaire zinnen.

Opgave 1.5 Het gegeven ‘8’ in de kolom Gerecht.bereidingstijd van figuur 1.13 bevat op zichzelf geen informatiewaarde. Waarom niet? Welk ander gegeven is minimaal nodig om samen met die ‘8’ informatie op te leveren? Door welke atomaire zin is die informatie uit te drukken?

Oefenopgaven De hierna volgende opgaven hebben betrekking op een eenvoudige orderadministratie, van een zekere firma Reijnders. Verderop in dit boek zal deze worden uitgebouwd en gebruikt voor SQL -opdrachten. Beschrijving Een order voor één of meer artikelen omvat de volgende informatie: – een (uniek) ordernummer – klantinformatie, zoals klantnummer (uniek), klantnaam en adres

– de datum – één of meer orderregels , elk met informatie over één besteld artikel: artikelnummer en omschrijving (elk identificerend voor een artikel), prijs per stuk en het bestelde aantal – de subtotalen per orderregel en het ordertotaal. Daarnaast is er nog informatie over de verschuldigde BTW , die we nu voor het gemak buiten beschouwing laten. Zie figuur 1.16 voor een voorbeeld.

Figuur 1.16 Order met orderregels

De belangrijkste informatie op deze order is in de eerste rij van tabel Order in figuur 1.17 weergegeven. Om het simpel te houden, is van de klant alleen het klantnummer en de (achter)naam opgenomen. De tweede rij bevat ordergegevens van een andere klant. De derde rij bevat een andere order van dezelfde klant.

Figuur 1.17 Niet-genormaliseerde ordertabel, met één order per rij

De kolom totaalbedrag bevat automatisch berekende gegevens, aangegeven door de grijze achtergrond. We nemen aan dat deze in de database worden opgeslagen. De kolom ‘orderregels’ is een herhalende groep, dus een gestructureerde kolom die voor elk van de drie rijen een subtabel als waarde heeft. Wanneer de order wordt opgesteld, worden de orderregelbedragen automatisch berekend uit het aantal artikelen en de artikelprijs, en vervolgens opgeslagen in de database. Ook dit is aangegeven met een grijze achtergrond. Herinnering : uitwerkingen van opgaven vindt u in de Boekverkenner.

Opgave 1.6 De tabel van figuur 1.17 bevat gegevens die vanuit het perspectief van orders zijn gestructureerd. a Biedt deze structuur de mogelijkheid om informatie op te nemen over artikelen los van orders? b Voer een omkering uit van de structuur: breng alle gegevens onder in één nieuwe tabel, met een herhalende groep, die is gestructureerd vanuit het perspectief van de artikelen. Laat daarbij de afleidbare totaalbedragen buiten beschouwing. c Biedt deze nieuwe structuur de mogelijkheid om informatie op te nemen over artikelen los van orders? De uitwerking van deze opgave geeft nog een extraatje: de gegevens gestructureerd vanuit het perspectief van de klant.

Opgave 1.7 Volg voor de structuur en de gegevens in figuur 1.17 de procedure zoals die is beschreven voor Reijnders’ Toetjesboek.

a Splits de herhalende groep af. b Welke kolommen bevatten redundante gegevens? Voor welke gegevens is die redundantie ongewenst? c Elimineer de ongewenste redundantie door opnieuw tabellen af te splitsen. Controleer of het eindresultaat voldoet aan ‘elk soort ding zijn eigen tabel’.

Opgave 1.8 De orderregelbedragen en het totaalbedrag van een order worden automatisch berekend. Is het dan wel nodig ze in de database op te slaan? Ze kunnen toch ook door de applicatie worden berekend uit de andere gegevens, op het moment dat ze nodig zijn?

2 Relationele databases: regels Het vorige hoofdstuk ging over goede en minder goede relationele structuren. Een goede structuur heeft als kenmerken: – geen herhalende groepen – geen ongecontroleerde redundantie

– standaardisatie van gegevens wordt afgedwongen waar dat gewenst is – elke ‘soort ding’ in de bijbehorende werkelijkheid heeft zijn eigen tabel. In dit hoofdstuk gaan we het hebben over de regels die voor die structuren gelden: – regels die dingen verbieden of verplichten: beperkingsregels – regels die zorgen dat acties worden ondernomen of juist worden tegengehouden: gedragsregels . Beperkingsregels zijn voorlopig het belangrijkst; in dit hoofdstuk worden verschillende typen behandeld. Van de gedragsregels komt één belangrijk type aan de orde. Voorbeelddatabase In de paragrafen 2.1 en 2.2 gaan we uit van de Toetjesboek-database van h oofdstuk 1 . In de loop van dit hoofdstuk wordt deze op enkele details aangepast. In figuur 2.1 herhalen we de voorbeeldpopulatie.

2.1 Beperkingsregels De tabellen zijn onderling verbonden via verwijzingen op basis van gelijke waarden. De belangrijkste beperkingsregel heeft betrekking op die verwijzingen. Het is de regel die loze verwijzingen (verwijzingen naar niks) verbiedt. Bijvoorbeeld: een maateenheid ‘scheutje’ bij een product, terwijl die niet in de gestandaardiseerde Eenheid-tabel voorkomt. Deze beperkingsregel heet de referentiële-integriteitsregel . Het is een belangrijke regel, omdat verwijzingen als het ware het cement van de database vormen. Zonder deze verwijzingen zou de database uiteenvallen in losse tabellen die niet gecombineerd kunnen worden. Verderop wordt de referentiële-integriteitsregel uitgebreid behandeld. Maar er zijn meer soorten beperkingsregels; daar gaan we het eerst over hebben.

2.1.1 Optionele en verplichte kolommen Elk gerecht moet een naam hebben. Dat is immers dé manier om een gerecht aan te duiden. We zeggen: voor de kolom Gerecht.naam geldt een verplichte-waarderegel . Kortweg, hij is verplicht . Vanzelfsprekend zijn ook Product.naam en Eenheid.naam verplicht. Ook de kolommen Ingredient.gerecht en Ingredient.product zijn verplicht. Laten we immers één van die twee in een rij oningevuld, dan is het onmogelijk betekenis te hechten aan de wel ingevulde gegevens in die rij.

Figuur 2.1 Voorbeeldpopulatie Toetjesboek

Voor alle andere kolommen geldt dat het weglaten van een waarde géén problemen geeft met de interpretatie van de overblijvende gegevens in de rij. Voor die kolommen moet beslist worden of het verplichte kolommen moeten worden of niet. Dat is een ontwerpkeuze. Een niet-verplichte kolom heet optioneel . Een optionele kolom mag lege cellen bevatten. De ‘inhoud’ van een lege cel heet een null . Dat lijkt wat vreemd en dat is het ook: hoe kan ‘niks’ een naam hebben? Eigenlijk is null niet meer dan een indicator, die aangeeft dat de cel leeg is. Nulls zullen ons in dit boek nog een hoop hoofdbrekens kosten. Alleen de kolommen die absoluut moeten worden ingevuld, worden verplicht gemaakt. Zo’n kolom is bereidingswijze. Want wat moeten we met een gerecht zonder bereidingswijze? In de Toetjesboek-database zijn de meeste kolommen verplicht, alleen de volgende zijn optioneel: – Gerecht.energie PP ; deze kolom is optioneel in verband met ingrediënten met onbekende hoeveelheid, maar tevens vanwege het éérst invoeren van ingrediënten en het pas daarna (automatisch) berekenen van energie PP

– Ingredient.hoeveelheid PP : een ingrediënt in een gerecht mag een onbepaalde hoeveelheid per persoon hebben – Product.eenheid: niet elk ingrediënt heeft een vaste maateenheid – Product.energie PE : een product zonder maateenheid (zie vorige punt) kent ook geen energie per maateenheid. De voorbeeldpopulatie illustreert dit: in Glace Terrace zit wel peper, maar een hoeveelheid is niet gegeven. Dat zal dan wel ‘naar smaak’ zijn. En van peper is geen eenheidnaam vermeld en evenmin een energie per eenheid. In een strokenschema geven we een optionele kolom aan met o . De kolommen zonder o zijn dus verplicht, zie figuur 2.2 . De gebruiker moet ervoor zorgen dat die kolommen in elke rij een waarde bevatten. Doet de gebruiker dit niet, dan zal het databasemanagementsysteem (rdbms) via de applicatie (het Toetjesboek-programma) een foutmelding geven.

Figuur 2.2 Strokendiagram voor Toetjesboek (versie 2: optionele kolommen)

2.1.2 Uniciteit Behalve dat de kolom naam in de tabel Gerecht verplicht is, moeten de waarden in die kolom ook uniek zijn: verschillende gerechten moeten verschillende namen hebben. Dit is vanzelfsprekend, maar de noodzaak wordt nog eens onderstreept door het feit dat de gerechtnamen in andere tabellen worden gebruikt om naar steeds één gerecht te verwijzen. Ook voor Product.naam geldt zo’n uniciteitsregel : verschillende producten hebben verschillende namen. Evenzo geldt in Eenheid een uniciteitsregel voor de naamkolom. Ook in de tabel Ingredient geldt een uniciteitsregel, echter niet voor één losse kolom; niet voor gerecht, omdat één gerecht meerdere producten kan bevatten; niet voor product, omdat één ingrediënt in meerdere gerechten kan voorkomen; niet voor hoeveelheid, omdat bij meerdere gerecht-product-combinaties natuurlijk best dezelfde hoeveelheid kan horen. Wel uniek zijn de combinaties (gerecht, product) in Ingredient. Zo’n combinatie bepaalt immers precies één rij in deze tabel, ofwel één ingrediëntregel. Uniciteitsregels over één kolom heten smalle uniciteitsregels . Die over twee of meer kolommen heten brede uniciteitsregels . In een tabelschema geven we uniciteitsregels aan door middel van tweepuntige pijlen, zie figu ur 2.3 .

Het eisen van uniciteit voor één kolom (een smalle pijl) is strenger dan het eisen van uniciteit voor een combinatie van diezelfde kolom en nog één of meer andere kolommen (een brede pijl). Bijvoorbeeld, uit de uniciteitsregel voor Gerecht.naam volgt als afgeleide regel dat ook de waardencombinaties in de kolomcombinatie (naam, energie PP ) verschillend zijn. Dit is echter een zwakkere regel, want het omgekeerde geldt niet.

Figuur 2.3 Strokendiagram voor Toetjesboek (versie 3: uniciteitsregels)

Algemeen wordt als conventie aangehouden dat alleen de meest strenge uniciteitsregels worden benoemd en als uniciteitspijl getekend. In een Toetjesboek-strokendiagram zal daarom wel een uniciteitspijl voor Gerecht.naam worden getekend maar geen brede pijl bij de combinatie (naam, energie PP ), ook al is die combinatie uniek (zie figuur 2.4 ). Tekenen we ergens toch een brede pijl, zoals bij Ingredient, dan zullen we hiermee (vanwege genoemde conventie) in het algemeen aanduiden dat geen uniciteitsregel geldt voor de afzonderlijke kolommen of voor smallere kolomcombinaties.

Figuur 2.4 Uniciteitsregels: alleen minimale unieke combinaties

2.1.3 Illustratieve populaties Een illustratieve populatie bij een bepaalde regel is een populatie die zo goed mogelijk illustreert wat wel en wat niet mag met betrekking tot die regel. Als voorbeeld nemen we een uniciteitsregel over twee kolommen. Een illustratieve populatie hierbij zal twee dingen illustreren: 1 de uniciteit van de waardencombinaties voor die twee kolommen 2 het feit dat voor de afzonderlijke kolommen geen uniciteit geldt. Zie figuur 2.5 voor een minimaal gekozen illustratieve populatie bij de brede uniciteitsregel voor Ingredient.

Figuur 2.5 Minimale illustratieve populatie voor brede uniciteitsregel

Illustratieve populaties spelen vaak een verhelderende ‘illustratieve’ rol bij het vaststellen van de juiste regels. Vooral bij uniciteitsregels.

2.1.4 Identificatie en verwijzing Een kolom of een kolomcombinatie die verplicht is en bovendien uniek, is geschikt om te gebruiken als unieke identificatie van een rij. Immers, in

elke rij is dan de kolomwaarde (of de combinatie) ingevuld en alle waarden (of combinaties) zijn verschillend. Zodoende kunnen we de drie kolommen met een smalle uniciteitspijl in fig uur 2.3 gebruiken om de rijen in respectievelijk Gerecht, Product en Eenheid uniek te identificeren. Die kolommen zijn immers ook verplicht. Evenzo is de kolomcombinatie (gerecht, product) in Ingredient geschikt om de rijen in Ingredient uniek te identificeren. In de voorbeeldpopulatie ziet u hoe een kolomwaarde van Product.naam, bijvoorbeeld ‘ijs’, in een andere tabel (Ingredient) wordt gebruikt om een verwijzing te realiseren juiste rij in Product. De waarden in Ingredient.product zijn als het ware ‘sleuteltjes’ die op een ‘slotje’ passen. De waarden in Product.naam zijn de ‘slotjes’. Deze zijn allemaal verschillend; van een ‘sleuteltje’ kunnen meerdere exemplaren bestaan. Zie figuur 2.6 , waarin dit voor twee ‘slotjes’ (de waarden ‘ijs’ en ‘kiwano’ in Product.naam) is geïllustreerd. In het algemeen geldt: in een relationele database kan een kolom of kolomcombinatie die uniek identificerend is (dus met ‘slotjes’), worden gebruikt om vanuit een andere tabel verwijzingen te realiseren (via ‘sleuteltjes’). Zo’n waarde of waardencombinatie fungeert dan als een soort ‘adres’ van een rij. We spreken van logische adressen van de rijen, ter onderscheiding van de fysieke geheugenadressen .

Figuur 2.6 ‘Sleuteltjes’ (verwijssleutelwaarden) passen op unieke ‘slotjes’ (primaire-sleutelwaarden)

2.1.5 Primaire sleutels en verwijssleutels Een kolom of kolomcombinatie met ‘slotjes’ heet een primaire sleutel . Een correspondende kolom of kolomcombinatie met ‘sleuteltjes’ heet een verwijssleutel . We formuleren dat iets preciezer. Een primaire sleutel van een tabel is een kolom of kolomcombinatie waarvoor geldt: 1 hij is verplicht 2 hij is uniek 3 in het geval van een kolomcombinatie: deze is zo ‘zuinig’ mogelijk gekozen 4 de kolomwaarden (of -waardencombinaties) zijn aangewezen als logische rij-adressen voor verwijzingen. Ter illustratie van de zuinigheidseis (3): de combinatie (naam, bereidingstijd) in Gerecht voldoet aan 1 en 2, maar niet aan 3. Dit geldt ook voor de combinatie (naam, energie PP ), ook al is energie PP niet verplicht. De zuinigheidseis houdt direct verband met het zo smal mogelijk tekenen van uniciteitspijlen (zie figuur 2.4 ). Een verwijssleutel is een kolom of kolomcombinatie waarvan de waarden (of waardencombinaties) naar rijen in een andere tabel verwijzen, via de primaire sleutel daarvan. Een primaire sleutel is – ondanks de naam – het ‘slotje’. Verwijssleutels zijn de ‘sleuteltjes’ die op zo’n slotje moeten passen. Primaire sleutels geven we aan met een ‘p’ bij de uniciteitspijl. Een verwijzing tekenen we in een strokendiagram als een verticale pijl, lopend van de verwijssleutel naar de primaire sleutel, zie figuur 2.7 .

Figuur 2.7 Strokendiagram voor Toetjesboek (versie 4: primaire sleutels)

Opmerkingen – Ook de tabel Ingredient heeft een primaire sleutel, ook al wordt daar niet naar verwezen. Het is echter denkbaar dat de database in de toekomst wordt uitgebreid met een tabel die er wel verwijzingen naar heeft. Dat zal dan met een brede verwijssleutel moeten; het sleuteltje moet immers op het slotje passen. – De primaire sleutel van Ingredient bestaat uit twee afzonderlijke verwijssleutels, één naar Gerecht en één naar Product. Hiermee wordt precies uitgedrukt dat een rij in Ingredient één gerecht koppelt aan één product. – Een brede primaire sleutel mag, vanuit conceptueel standpunt bezien, best een null bevatten, als hij maar niet alleen nulls bevat. Vrijwel elk rdbms eist echter dat primaire-sleutelkolommen geen nulls bevatten (verplichte kolommen).

– Een verwijssleutel mag best optioneel zijn en is in dit opzicht vergelijkbaar met een ‘gewone’ kolomwaarde. Zie echter het vorige punt voor verwijssleutels die deel uitmaken van een brede primaire sleutel.

2.1.6 Kandidaatsleutels en alternatieve sleutels De term ‘primaire sleutel’ suggereert dat er ook zoiets mogelijk is als een ‘niet-primaire sleutel’. Dit is inderdaad juist. Met het gegeven voorbeeld is dat echter niet te illustreren, omdat elke tabel maar één kolom of kolomcombinatie heeft die aan de eisen 1, 2 en 3 voldoet. Bij gebrek aan andere kandidaten worden deze dus vanzelf aangewezen om vanuit andere tabellen naar te verwijzen (eis 4). In paragraaf 2.1.10 wordt de databasestructuur gewijzigd, zodanig dat de tabellen Gerecht en Product beide twee kandidaat-primaire-sleutels (eisen 1, 2 en 3) krijgen. Zo’n kandidaat-primairesleutel heet kortweg kandidaatsleutel . De gekozen kandidaten (één per tabel) worden dan primaire sleutel (eis 4); de afgevallen kandidaten (alleen eisen 1, 2 en 3) heten alternatieve sleutel . Elke tabel heeft minstens één kandidaatsleutel. Immers, de combinatie van alle kolommen is verplicht (er zijn geen lege rijen mogelijk) én uniek (omdat een tabel – als relatie – een verzameling is). Door nu zoveel mogelijk kolommen weg te laten, zodanig dat de eisen 1 en 2 nog steeds gelden, ontstaat een zo zuinig mogelijk gekozen combinatie. Sleutels en herhalende groepen In de meeste boeken over relationele databases worden herhalende groepen zo snel mogelijk ‘in de ban’ gedaan, waarna de relationele theorie, waaronder die over uniciteitsregels en sleutels, volledig wordt ontvouwd voor genormaliseerde structuren, dus zonder herhalende groepen. Echt nodig is dat niet en vaak betekent het zelfs een gemiste kans om iets goed uit te leggen. Wij hebben ons deze beperking dan ook niet opgelegd, en zullen daarvan regelmatig profijt hebben. In dit boek komt u dan ook regelmatig uniciteitsregels en sleutels tegen in tabellen mét een herhalende groep. Naamgeving sleutelkolommen We houden er in dit boek vrij strakke naamgevingsconventies op na. Dat schept eenheid en duidelijkheid en leidt ook tot strakke SQL-code. De kolomnaam van een smalle primaire sleutel luidt meestal ’nr’, ‘code’ of ‘naam’. Een verwijssleutelkolom heeft meestal dezelfde naam als de corresponderende tabel, maar dan met een kleine letter.

2.1.7 Referentiële integriteitsregel Elke waarde in Product.eenheid wordt geacht te verwijzen naar precies één rij in Eenheid, via de primaire sleutel van die tabel (dat is de enige kolom, in dit geval). Dan moet die waarde daar wel voorkomen natuurlijk. Bevat Product.eenheid ergens ‘theelepel’ en komt dit niet voor in Eenheid.naam, dan hebben we een probleem: een verwijzing naar niks. Dan wordt er als het ware gemeten in de niet-bestaande eenheid ‘theelepel’. De ‘referentiële-integriteitsregel’ verbiedt dergelijke loze verwijzingen. Referentiële-integriteitsregel De referentiële-integriteitsregel luidt: elke waarde van een verwijssleutel moet voorkomen als waarde van de bijbehorende primaire sleutel (referentie = verwijzing; integriteit = het voldoen aan beperkingsregels). Informeel zegt de regel: als we ergens een sleuteltje hebben liggen, moet er (aan de andere kant van de verwijspijl) een slotje zijn waar dat sleuteltje op past. Naar een brede primaire sleutel kan alleen worden verwezen met een brede verwijssleutel: als het slotje breed is, moet ook het sleuteltje breed zijn. De rollen die twee tabellen hebben ten opzichte van een verwijzing, worden vaak aangeduid met de termen ouder en kind (Engels: parent en child ). De tabel met de verwijssleutel heet het kind; de tabel waarheen verwezen wordt, heet de ouder, zie figuur 2.8 .

Figuur 2.8 Ouder-kind-combinatie (ten opzichte van verwijzing)

Merk op dat bij een rij van de kindtabel ofwel geen enkele rij ofwel precies één rij van de oudertabel hoort. De mogelijkheid ‘geen enkele rij’ kan zich alleen voordoen wanneer de verwijzing optioneel is, zoals in figuur 2.8 . Bij een verwijzing vanuit een verplichte kolom is er altijd precies één ouderrij bij elke kindrij. Omgekeerd: bij één rij van de oudertabel kunnen nul, één of meer rijen van de kindtabel horen. Dit kan per rij verschillen.

Tekenconventie In dit boek houden we ons aan de volgende tekenconventie : in structuurdiagrammen wordt de ‘ouder’ indien mogelijk hoger dan het ‘kind’ getekend (zie opgave 2.4 voor een uitzonderingssituatie).

Een ouder-kindrelatie wordt ook wel associatie genoemd. We prefereren de term ‘associatie’ boven ‘relatie’ omdat, zoals we hebben gezien, de term ‘relatie’ in de relationele theorie al een andere betekenis heeft, namelijk die van ‘tabel’ (in wiskundige zin). Verwijssleutels vormen het ‘cement’ van de database. Het bewaken van de referentiële-integriteitsregel is daarom een fundamentele taak van het rdbms, die op vele momenten actueel is. Uitgaande van twee tabellen met een ouder-kind-associatie moet het rdbms in de volgende gevallen zorg dragen voor een juiste correspondentie tussen de verwijssleutel (in de kindtabel) en de primaire sleutel (in de oudertabel): Gecontroleerd moet worden of de verwijssleutelwaarde voorkomt als primaire-sleutelwaarde in de oudertabel.

– bij een poging een rij in de kindtabel toe te voegen:

Gecontroleerd moet worden of de nieuwe verwijssleutelwaarde bestaat als primairesleutelwaarde in de oudertabel.

– bij een poging een verwijssleutelwaarde in de kindtabel te wijzigen: – bij een poging een primaire-sleutelwaarde in de oudertabel te wijzigen: – bij een poging tot verwijderen van een ouderrij:

Moeten de corresponderende verwijssleutelwaarden dezelfde wijziging ondergaan, zodat de correspondentie in stand blijft? Moet dit worden toegestaan? En zo ja: wat moet met de kindrijen gebeuren, zodat deze niet als ‘weeskind’ (met ‘naar niets’ verwijzende sleutelwaarden) achterblijven?

Let op het woord ‘poging’: soms zal de actie geen doorgang kunnen vinden, omdat de vereiste correspondentie niet gehandhaafd of tot stand gebracht kan worden. Met name de laatste twee kwesties zijn nadere studie waard, omdat het antwoord niet eenduidig is. In paragraaf 2.2 , die gewijd is aan ‘gedragsregels’, zullen we hier nader op ingaan.

2.1.8 Multipliciteitsregels Bij een verwijzing is het meestal belangrijk vast te leggen hoeveel rijen van de oudertabel bij één rij van de kindtabel kunnen horen. En omgekeerd, hoeveel rijen van de kindtabel bij één rij van de oudertabel kunnen horen. Zo geldt voor de verwijzing van Product naar Eenheid:

– bij één Product-rij horen nul of eenEenheid-rijen – bij één Eenheid-rij horen nul of meer Product-rijen. Hierbij betekent ‘nul of meer’: willekeurig veel. Dit type ‘aantallenregels’ worden multipliciteitsregels genoemd, of ook wel kardinaliteitsregels , naar de wiskundige term ‘kardinaliteit’ voor het aantal elementen van een verzameling. De multipliciteitsregels bij de andere verwijzingen zijn als volgt:

– bij één Ingredient-rij hoort precies één Product-rij – bij één Product-rij horen dus nul of meer Ingredient-rijen en: – bij één Ingredient-rij hoort precies één Gerecht-rij – bij één Gerecht-rij horen nul of meer Ingredient-rijen. Al deze regels worden geïllustreerd in de voorbeeldpopulatie. In figuur 2.9 worden ze grafisch weergegeven, in een zogenaamd multipliciteitendiagram . Merk op dat we de multipliciteiten steeds moeten aflezen ‘aan de andere kant’ van het lijntje, lezend van de ene tabel naar de andere.

Figuur 2.9 Multipliciteitendiagram van Reijnders’ Toetjesboek

Merk op dat we gerechten met nul ingrediënten toestaan. Dit is een ontwerpkeuze waar heel wat over te zeggen is. In de volgende paragraaf gaan we daar nader op in. Een multipliciteitendiagram geeft de structuur van een database overzichtelijk weer, zonder details van de kolommen. Merk op dat elke verwijzing een 1-kant (0-of-1) en een veel-kant (0-of-meer, dan wel 1-ofmeer) heeft. De 1-kant staat aan de kant van de oudertabel; de veel-kant aan de kant van de kindtabel. Dit komt overeen met de pijlnotatie in een strokendiagram: de pijl loopt van veel naar 1. Zie ook figuur 2.10 .

Figuur 2.10 Een verwijzing en multipliciteitsregels

2.1.9 Een ‘kip-ei’-probleem Stel, we gaan in het databaseontwerp uit van de regel dat een gerecht minstens één ingrediënt moet hebben. Zo gek is dat niet, immers wat heb je aan een gerecht zonder ingrediënten? In het multipliciteitendiagram krijgen we dan aan de ‘meer’-kant een één-of-meer regel. In het

strokendiagram moeten we een extra regel moeten toevoegen. Omdat daar geen grafische notatie voor bestaat, doen we dat in tekst. Zie figuur 2.11 .

Figuur 2.11 ‘Kip-ei’-probleem

De 1-of-meer-multipliciteitsregel (‘kraaiepootje met streepje’), in combinatie met een precies-1 regel (dubbel streepje) aan de andere kant, is problematisch. Er is hier een soort ‘kip-ei’-probleem. Want voegen we als eerste een Gerecht-rij toe, dan overtreden we de 1-of-meer-regel. Maar beginnen we met een Ingredient-rij, dan is niet voldaan aan de precies-1regel. En stel dat het ons toch is gelukt: met verwijderen hebben we net zo’n probleem. Want de ‘kip’ (een Gerecht-rij) kunnen we niet verwijderen zolang deze nog een ‘ei’ (Ingredient-rij) bezit, maar hoe beroven we de kip van haar laatste ei? Een beperking van de meest gangbare SQL -dialecten zet dit hele probleem nog in een ander licht: het blijkt daarin namelijk onmogelijk de 1-of-meer-regel te implementeren. Anders gezegd: het blijkt niet zonder meer mogelijk af te dwingen dat een ouderrij minstens één kindrij heeft. Al met al betekent dit dat we in de databasepraktijk altijd 0-of meermultipliciteiten hebben en daarmee het kip-ei-probleem omzeilen. Voor wie per se een één-of-meer-regel wil implementeren, bestaan wel mogelijkheden. Een oplossing is de regel niet door het rdbms maar door de applicatieprogrammatuur te laten bewaken. Zo kunnen we ons voorstellen dat de gebruiker het gerechtenscherm pas kan sluiten wanneer het gerecht een ingrediënt bevat en een passende foutmelding krijgt wanneer hij dat probeert bij een gerecht zonder ingrediënt. In paragraaf 2.2.1 zal overigens blijken dat we het rdbms opdracht kunnen geven om bij het verwijderen van een Gerecht-rij in één klap (als het ware tegelijkertijd) ook alle bijbehorende Ingredient-rijen te verwijderen: een zogenaamde cascading delete .

Opgave 2.1 Ga in figuur 2.7 nog eens na welke ‘sleuteltjeskolommen’ naar welke ‘slotjeskolommen’ verwijzen.

a Welke tabellen hebben geen verwijzing (‘sleuteltje’) naar een andere tabel? b Welke tabel heeft twee verwijzingen naar andere tabellen? c Naar welke tabel wordt vanuit geen enkele andere tabel verwezen?

Opgave 2.2 Welk van de multipliciteitendiagrammen a, b of c in figuur 2.12 is, voor wat betreft de multipliciteiten, equivalent met het strokendiagram in figuur d?

Figuur 2.12 Welk multipliciteitendiagram past bij het strokendiagram?

Opgave 2.3 Dezelfde vraag als in opgave 2.2 , nu met een verplichte kolom Boek.rubriek.

Opgave 2.4 Een grootwinkelbedrijf heeft een aantal filialen. Werknemers kunnen werkzaam zijn bij niet meer dan één filiaal. Elk filiaal heeft onder normale omstandigheden één regiomanager, maar deze functie kan ook vacant zijn. Een regiomanager kan bij één van zijn of haar filialen werkzaam zijn (maar ook op het hoofdkantoor). De bedrijfsdatabase bevat onder meer de tabellen Werknemer en Filiaal. Figuur 2.13 geeft hiervan de structuurdiagrammen, met een kleine voorbeeldpopulatie. Alleen enkele belangrijke kolommen zijn weergegeven. Ter onderscheiding zijn in het multipliciteitendiagram de associaties van een naam voorzien: ‘een werknemer is werkzaam bij geen enkel of bij één filiaal’, en ‘een filiaal heeft maximaal één werknemer als regiomanager’. Vraag: hoe zit het in deze structuur met de kwalificaties ‘ouder’ en ‘kind’?

Figuur 2.13 Tabellen met verwijzingen over en weer

2.1.10 Bijzondere beperkingsregels De beperkingsregels die in de vorige paragrafen zijn behandeld, worden standaard beperkingsregels genoemd. We komen deze in elke relationele database tegen, omdat ze nauw samenhangen met de relationele structuur als zodanig. In een strokendiagram zijn dit soort regels in één oogopslag af te lezen, uit de horizontale en verticale pijlen en door het ontbreken van een ‘ o ’ bij verplichte kolommen. De toetjesdatabase moet nog wel aan meer voorwaarden voldoen, ‘specifieke toetjesregels’, waarvoor geen grafische notatie bestaat. Dit soort regels zullen we soms in natuurlijke taal aan de diagrammen toevoegen.

Voorbeelden van zulke bijzondere beperkingsregels zijn: – in Ingredient mag hoeveelheid PP alleen oningevuld zijn als de energiewaarde van het product ook niet is ingevuld (zoals bij peper) of 0 is – in Ingredient moet hoeveelheid PP oningevuld zijn als van het ingrediënt (in Product) geen eenheid gegeven is. De achtergrond van de eerste regel is dat het Toetjesboek automatisch de energiewaarde (energie PP ) van een gerecht moet berekenen. Daarvoor moet de hoeveelheid van elk ingrediënt bekend zijn, behalve van ingrediënten die niets bijdragen aan die energiewaarde. De tweede regel geldt omdat een hoeveelheid geen betekenis heeft als er geen maateenheid is. Nog enkele voorbeelden van bijzondere beperkingsregels: – een hoeveelheid is altijd groter dan 0 – een energiewaarde is altijd groter dan 0 of gelijk aan 0.

2.1.11 Het bewaken van beperkingsregels De standaard beperkingsregels (verplichte kolommen, uniciteitsregels, primaire sleutels, referentiële integriteit bij verwijssleutels) worden afgedwongen door ze op te nemen in de structuurdefinitie van tabellen. Deze structuurdefinitie wordt in de vorm van opdrachten, geformuleerd in de gegevenstaal SQL , aan het rdbms meegedeeld. Hoe dit gaat, zien we in hoofdstuk 3 ‘Communiceren met een relationele database’. Ook enkele eenvoudige bijzondere beperkingsregels kunnen op deze wijze aan het rdbms kenbaar worden gemaakt, bijvoorbeeld de regels die een minimum- of maximumwaarde voor een kolom aangeven. Ingewikkelder beperkingsregels, en ook allerlei actief gedrag van de database (denk aan het automatisch berekenen van de energie per persoon van een gerecht), moeten worden gerealiseerd door aanvullend programmeerwerk. Zie paragraaf 1.2.4, over triggers .

2.1.12 Kunstmatige sleutels De twee verwijssleutels Ingredient.gerecht en Ingredient.product bevatten vrij lange tekstvelden. Het kost het rdbms relatief veel tijd om daar de corresponderende primaire sleutels in Gerecht respectievelijk Product bij te zoeken. Ook nemen ze nogal wat geheugenruimte in beslag. In plaats van zulke lange tekstvelden worden daarom vaak numerieke codes gekozen als primaire sleutel en verwijssleutel. In dit geval: kunstmatige gerechtnummers en productnummers. Voor de eindgebruiker hebben deze kunstmatige sleutels geen betekenis; in de applicatie moeten ze daarom onzichtbaar blijven. En het is het rdbms zelf dat nieuwe gerechten en producten zo’n kunstmatig nummertje geeft, volautomatisch. Tenminste, wanneer bij de bouw van de database daarvoor de juiste voorzieningen zijn getroffen. Hoe dat gaat, zullen we later zien, in hoofdstuk 12 . Zie figuur 2.14 voor een strokendiagram van Reijnders’ Toetjesboek met kunstmatige sleutels. Beide kolommen met kunstmatige nummers heten ‘id’. De oorspronkelijke primaire sleutels, Gerecht.naam en Product.naam, hebben nu de status van alternatieve sleutel gekregen, zie paragraaf 2.1.4 .

Figuur 2.14 Strokendiagram met kunstmatige sleutels

Figuur 2.15 geeft een voorbeeldpopulatie. De kunstmatige verwijsnummers hoeven niet vanaf 1 te lopen en in de praktijk zullen ze vaak ook niet allemaal opeenvolgend zijn. Dat komt bijvoorbeeld doordat

er ook wel eens gerechten en producten verwijderd worden; hun nummertje wordt dan niet opnieuw gebruikt. Maar het maakt allemaal niets uit: het zijn kunstmatige nummers, die altijd op de achtergrond blijven, onzichtbaar voor de gebruikers van Reijnders’ Toetjesboek. Deze versie van het Toetjesboek is in de Boekverkenner beschikbaar onder de naam Toetjesboek KS (‘met kunstmatige sleutels). Omdat ook in Nederlandse teksten vaak de Engelse benamingen van sleutels gebruikt worden, geven we tot slot van deze paragraaf de Engelse benamingen van de verschillende soorten sleutels en bovendien een veelgebruikt Nederlands synoniem voor ‘verwijssleutel’: primaire sleutel verwijssleutel alternatieve sleutel

= = =

primary key foreign key = alternate key

vreemde sleutel

Figuur 2.15 Toetjesboek-database met kunstmatige sleutels

2.2 Gedragsregels Via SQL of door aanvullend programmeerwerk kan worden gezorgd dat het rdbms acties onderneemt, in antwoord op bepaalde gebeurtenissen (Engels: events ). Zo’n gebeurtenis kan zijn: een verandering van de database-inhoud of een poging daartoe. Ook het aanbreken van een bepaald tijdstip kan een ‘gebeurtenis’ zijn die het startsein is voor een actie van het rdbms. Processen die leiden tot veranderingen in de database, worden gedrag genoemd. In deze paragraaf gaat het om voorgeprogrammeerd gedrag in relatie met dreigende overtredingen van beperkingsregels. We spreken van gedragsregels . De belangrijkste zijn de ‘refererende actieregels’, die verband houden met het handhaven van referentiële integriteit bij het invoegen, verwijderen of wijzigen van een rij.

2.2.1 Refererende actieregels De referentiële integriteitsregel, die eist dat bij elke verwijssleutelwaarde (kind) een ouderrij bestaat, roept in twee gevallen problemen op: – bij het verwijderen van een ouderrij – bij het wijzigen van de primaire-sleutelwaarde in een ouderrij. In het eerste geval kunnen er ‘verweesde’ kinderen achterblijven: rijen in de kindtabel met een loze verwijzing. In het tweede geval heeft een rij in de oudertabel zijn logisch adres (primaire-sleutelwaarde) veranderd, terwijl eventuele kindrijen nog het oude adres (verwijssleutelwaarde) hebben. Om deze problemen bij voorbaat op te lossen, wordt voor elke verwijzing een deleteregel en een updateregel gespecificeerd. Ze drukken uit of er iets mag gebeuren, en zo ja, wat er moet gebeuren bij een poging tot deleten van een rij of een poging tot updaten van een primaire sleutel, beide in de oudertabel. De deleteregels en de updateregels heten samen de refererende actieregels . Deleteregels

Er zijn drie deleteregels. Ze zeggen wat er moet gebeuren bij een poging tot deleten van een ouderrij. Voor elke verwijzing moet één van deze regels worden gespecificeerd.

– Restricted delete : een poging tot deleten van een ouderrij mislukt wanneer er één of meer corresponderende kindrijen bestaan. – Cascading delete : bij een poging tot deleten van een ouderrij zal een poging tot deleten van alle corresponderende kindrijen worden ondernomen. Is er een (andere) regel die dat tegenhoudt, dan gaat het deleten niet door. – Nullifying delete : bij een poging tot deleten van een ouderrij wordt gepoogd verwijzingen in eventuele kindrijen op null te zetten. Is er een (andere) regel die dat tegenhoudt, dan gaat het deleten niet door. De restricted-deleteregel houdt een verbod op ‘oudermoord’ in. Wat wel mag, is van onderaf beginnen: eerst de kinderen verwijderen en dan hun ouder. Van de drie deleteregels komt deze regel het meeste voor. Bij de meeste verwijzingen kunnen we dus niet zomaar een ouderrij verwijderen wanneer er nog één of meer rijen bestaan in de corresponderende kindtabel, zie figuur 2.16 . Zoals al eerder opgemerkt, geldt deze regel automatisch wanneer géén van de andere is gespecificeerd. Hij is dan een direct gevolg van de referentiële-integriteitsregel, die altijd geldt.

Figuur 2.16 Effect van restricted delete (rd)

Wilt u het effect van een restricted delete omzeilen, dan zult u eerst welbewust de kindrijen van een ouderrij moeten verwijderen. Het product ‘aardbeien’ kunt u dus pas verwijderen, wanneer u eerst alle Ingredientrijen met ‘aardbeien’ erin hebt verwijderd. De cascading-deleteregel wordt doorgaans gespecificeerd voor verwijzingen zoals van Ingredient naar Gerecht. Een gebruiker die een Gerecht-rij wil verwijderen, zal ook de corresponderende Ingredient-rijen willen verwijderen, want zonder bijbehorend gerecht hebben deze geen bestaansreden. Zie figuur 2.17 .

Figuur 2.17 Effect van cascading delete (cd)

Wanneer een te verwijderen kindrij zelf ouder is van een kleinkindrij, kan de verwijzing vanuit het kleinkind naar het kind de verwijdering alsnog tegenhouden, bijvoorbeeld wanneer daarvoor een restricted delete geldt! De nullifying-deleteregel komt in de praktijk weinig voor en wordt door veel rdbms’en niet ondersteund. Updateregels Er zijn drie updateregels. Ze zeggen wat er moet gebeuren bij een poging om een primaire-sleutelwaarde in een ouderrij te wijzigen. Voor elke verwijzing moet één van deze regels worden gespecificeerd.

– Restricted update : een poging tot updaten van de primairesleutelwaarde in een ouderrij mislukt wanneer er één of meer corresponderende kindrijen bestaan. – Cascading update : bij een poging tot update van een primairesleutelwaarde in een ouderrij, wordt een poging tot updaten ondernomen van de verwijssleutelwaarden in alle corresponderende kindrijen. Is er een (andere) regel die dat tegenhoudt, dan gaat het updaten niet door.

– Nullifying update : bij een poging tot updaten van een primairesleutelwaarde in een ouderrij, wordt gepoogd verwijzingen daarnaar in corresponderende kindrijen op null te zetten. Is er een (andere) regel die dat tegenhoudt, dan gaat het updaten niet door. Van de updateregels ligt de cascading update het meest voor de hand: een verandering van een primaire sleutelwaarde (bijvoorbeeld om een spelfout te herstellen) zullen we vrijwel altijd willen laten doorwerken in de verwijzingen, om zodoende de correspondentie in stand te houden. Zie fig uur 2.18 . Het is zoiets als het doorgeven van een adreswijziging aan al je relaties; die wijzigen dan dit adres in hun eigen adresboekjes.

Figuur 2.18 Effect van cascading update (cu) Cascading update: ‘zoek-en-vervang’ Een primaire-sleutelwaarde is het ‘single point of definition’ van een rijidentificatie. Alle verwijssleutelwaarden die naar die rij verwijzen, dienen er exact aan gelijk te zijn en te blijven. Dit wordt bereikt met de cascading update, die functioneert als een soort ‘zoek-en-vervang’ voor verwijssleutelwaarden bij verandering van een primaire-sleutelwaarde. Het ligt voor de hand om, gegeven een primaire sleutel, ofwel voor alle verwijzingen ernaar een cascading update te specificeren, ofwel voor geen enkele. Immers een ‘halve zoek-en-vervang’ leidt tot een niet-consistente database.

Aan de nullifying update is in de praktijk weinig behoefte en door veel rdbms’en wordt hij niet ondersteund. In figuur 2.19 is voor elke verwijssleutel één deleteregel (rd of cd) en één updateregel (steeds cu) gespecificeerd. Merk op dat voor de verwijzing

van Ingredient naar Gerecht een cascading delete geldt, maar voor de overige verwijzingen een restricted delete. Dit betekent dat bij het verwijderen van een gerecht door de gebruiker de bijbehorende Ingredient-rijen automatisch meeverwijderd worden. Maar wanneer de gebruiker probeert een product te verwijderen, wordt dit tegengehouden zolang er nog gerechten zijn waarin dit product als ingrediënt voorkomt. Ook een Eenheid-rij kan niet zomaar verwijderd worden; dat kan alleen wanneer geen enkel product meer in die eenheid wordt gemeten.

Figuur 2.19 Strokendiagram met deleteregels en updateregels

De delete- en updateregels worden gespecificeerd voor elke verwijssleutel afzonderlijk. In strokendiagrammen houden we de restricted delete en de cascading update als default aan, omdat die het meeste voorkomen. Voor fi guur 2.19 betekent dit alle regels behalve de cascading delete (cd) mogen worden weggelaten. In SQL moeten we de cascading update wel specificeren, omdat daar de beide restricted varianten default zijn.

2.2.2 Bijzondere gedragsregels De refererende acties moeten bij elke verwijzing worden gespecificeerd, en zijn daarom een type regel dat in elke database voorkomt: het zijn

standaard gedragsregels . Maar er zijn meestal ook gedragsregels die specifiek zijn voor de database. Deze noemen we bijzondere gedragsregels . Een voorbeeld van een bijzondere gedragsregel voor Reijnders’ Toetjesboek is de volgende. ‘De energie per persoon van een gerecht is afleidbaar uit de energie per eenheid van de producten en de hoeveelheden per persoon van de ingrediënten in het gerecht. Berekeningsvoorschrift: alle bij het gerecht horende hoeveelheden per persoon vermenigvuldigen met de bijbehorende energiewaarden per eenheid en deze producten optellen.’ Deze regel moet automatisch in werking treden nadat een nieuw gerecht met alle ingrediëntinformatie is ingevoerd en de gebruiker heeft aangegeven dat de informatie correct is ingevoerd en definitief mag worden vastgelegd. Maar ook moet de regel automatisch in werking treden wanneer er later nog een ingrediënt wordt toegevoegd of wordt verwijderd. En ook wanneer een van de gegevens uit het berekeningsvoorschrift wordt gewijzigd.

2.2.3 Transacties Een opvraging verandert niets aan de inhoud van een database. De volgende elementaire acties doen dat wel: – invoeren van een nieuwe rij – verwijderen van een rij – wijzigen van een celinhoud binnen een rij. Vaak staan deze niet op zichzelf en moeten meerdere acties worden gebundeld om een zinvol resultaat te krijgen. We hebben hier al enkele voorbeelden van gezien. Voorbeeld 2.1 Om een nieuwe Gerecht-rij in te voeren, moeten we ook één of meer Ingredient-rijen invoeren. Wanneer zo’n Ingredient-rij een nieuw product bevat, moet ook een nieuwe Product-rij worden ingevoerd.

Voorbeeld 2.2 Wanneer de energie per eenheid van een product wordt gewijzigd, moet ook (automatisch) de energie per persoon van de gerechten waarin dat product voorkomt, worden gewijzigd. Voorbeeld 2.3 Wanneer we een Gerecht-rij verwijderen, zijn we verplicht om behalve de Gerecht-rij zelf ook de bijbehorende Ingredient-rijen te verwijderen. Anders blijven we immers zitten met Ingredient-rijen die ‘naar niks’ verwijzen, in strijd met de regel voor verwijssleutels. Dit wordt gerealiseerd door de cascading-deleteregel uit paragraaf 2.2.1 . Al deze voorbeelden geven een opeenvolging van databaseacties te zien die van nature bij elkaar horen: doen we het één, dan moet ook het ander gebeuren. Zo’n opeenvolging heet een transactie . Midden in een transactie hoeft niet aan alle regels te zijn voldaan, maar na afloop wel. Tijdens een transactie bestaat veelal een ongewenste situatie. Na afloop is alles in orde en kunnen we aan het rdbms opdracht geven de veranderingen definitief te maken (Engels: commit ). Zie figuur 2.20 .

Figuur 2.20 Transactie: de acties tussen twee commitmomenten

Het committen wordt in de meeste gevallen vanuit de applicatie geregeld, door het versturen van een speciale SQL -opdracht ( commit ) naar het rdbms. Dit is al dan niet het gevolg van een bewuste actie van de gebruiker, bijvoorbeeld het aanklikken van een speciaal knopje: de commit-knop of save-knop. Wordt op dat moment niet aan alle regels voldaan, dan wordt de commit niet uitgevoerd en wordt teruggekeerd naar de toestand aan het begin van de transactie. Alle wijzigingen worden dan teruggedraaid (Engels: rollback ), zie figuur 2.21 . Ook de gebruiker zelf kan de lopende transactie annuleren, bijvoorbeeld door een speciale rollback- of ‘undo’-button aan te klikken. In dit geval wordt het SQL statement rollback naar het rdbms verstuurd.

Figuur 2.21 Rollback na onvoltooide transactie of expliciete rollback-opdracht Korte transacties Transacties moeten zo kort mogelijk worden gemaakt. Een van de redenen daarvoor is de mogelijkheid informatie kwijt te raken wanneer tussentijds het systeem uitvalt door bijvoorbeeld een stroomstoring. In zo’n geval wordt de transactie geannuleerd (rollback) en moeten we opnieuw beginnen. Een andere reden is dat gebruikers nu eenmaal fouten maken en transacties daardoor soms niet kunnen worden voltooid. Ook in die gevallen vindt een rollback plaats. Tot slot komt het bij lange transacties nogal eens voor dat het systeem een foutmelding geeft, maar dat de gebruiker niet meer kan achterhalen op welke actie die foutmelding precies betrekking heeft.

In latere hoofdstukken komen we uitgebreid op transacties terug, onder meer op de problematiek van concurrente transacties : transacties van meerdere gebruikers die elkaar in de tijd overlappen.

Opgave 2.5 Voorspel het effect van elke hierna genoemde actie, uitgaande van de populatie van figuur 2.1 en de structuurdiagrammen van figuren 2.7 en 2.19. Als dat effect alleen een foutmelding is, vertel dan welke databaseregel de oorzaak is van die foutmelding. Beschouw elke actie op zichzelf. a Poging tot toevoegen van een nieuwe Ingredient-rij (‘Coupe Kiwano’, ‘vanille’, null). b Poging tot toevoegen van een nieuwe Ingredient-rij (‘Coupe Kiwano’, ‘suiker’, 12).

c Poging tot toevoegen van een nieuwe Product-rij (‘tjindang’, ‘koffielepel’, 2). d Poging tot toevoegen van een nieuwe Eenheid-rij (‘slagroom’). e Poging tot verwijderen van de Product-rij (‘kiwano’, ‘stuks’, 40). f Poging tot verwijderen van de Product-rij (‘banaan’, ‘stuks’, 40). g poging tot verwijderen van de Gerechtrij (‘Mango Plus Plus’, 131, 8, ‘Snijd ...’). h Poging tot wijzigen van ‘kiwano’ in ‘chiwano’ in de kolom Product.productnaam. i Poging tot wijzigen van de eenheidnaam van aardbeien: ‘gram’ wordt ‘pond’. j Poging tot wijzigen van de bereidingstijd van Coupe Kiwano in null.

2.3 De OpenSchool-database (1) Voor we in de volgende paragraaf dieper ingaan op uniciteitsregels, introduceren we een nieuw voorbeeld: de OpenSchool-database. Deze is ontworpen voor de informatievoorziening van de Open School, een scholingsinstituut voor afstandsonderwijs. Figuur 2.22 toont vier tabellen, met een kleine voorbeeldpopulatie.

Figuur 2.22 Voorbeeldpopulatie van de OpenSchool-database

De tabellen horen bij vier ‘soorten van dingen’ die in verband met de informatievoorziening van belang worden geacht: docenten, studenten, cursussen en inschrijvingen van een student voor een cursus. Van docenten is een unieke code vastgelegd, een acroniem (afgekort acr) en daarnaast hun naam. Van studenten is een uniek nummer (nr) opgenomen en daarnaast hun naam en, bij drie van de vier, een mentor. Een mentor is een docent; deze wordt vastgelegd via een van de docentacroniemen uit tabel Docent. Van cursussen is een unieke code vastgelegd en daarnaast de cursusnaam (uiteraard ook uniek) en, niet verplicht, de examinator. De examinator is weer een docent en wordt dus vastgelegd via een acroniem.

Van een inschrijving is vastgelegd om welke student het gaat en om welke cursus. Dat geeft een combinatie van een studentnummer en een cursuscode. Door zo’n combinatie wordt een inschrijving uniek vastgelegd. Daarnaast is er nog een datumkolom voor de inschrijfdatum. Net als bij het Toetjesboek zijn de tabellen verbonden via verwijzingen (verwijssleutels). Zo fungeert kolom Student.mentor als verwijssleutel van Student naar Docent. Bij drie van de vier Student-rijen hoort precies één corresponderende Docent-rij, met de gegevens van de mentor van de betreffende student. Van student met nummer 4 is geen mentorinformatie opgenomen. Dit kan twee dingen betekenen: student 4 heeft geen mentor of de mentor van student 4 is binnen het systeem niet bekend. Merk op dat we vanuit een inschrijving op twee manieren een docent kunnen vinden: via Student vinden we de mentor van de student bij de inschrijving (als die mentor er is); via Cursus vinden we de examinator van de cursus bij de inschrijving (als die examinator er is). Figuur 2.23 toont een strokendiagram voor de OpenSchool-database.

Figuur 2.23 OpenSchool-database: strokendiagram (onvolledig)(

De tabellen Cursus en Docent hebben naast een primaire sleutel ook een alternatieve sleutel. Tabel Student echter heeft alleen een primaire sleutel. Dit is omdat studentnamen niet uniek zijn. Twee studenten mogen dus met dezelfde naam in het systeem worden geregistreerd. Dit geldt niet voor docenten. Het strokendiagram is onvolledig: er ontbreekt een uniciteitsregel bij Inschrijving. In de volgende paragraaf onderzoeken we welke kandidaten hiervoor zijn.

2.4 Meer over uniciteitsregels Van alle regels waaraan een relationele database moet voldoen, zijn de uniciteitsregels het meest prominent. De structuur van een database (denk aan sleutels) is er direct mee verbonden. Het vaststellen van de juiste uniciteitsregels is daarom essentieel. Hoe bepalen we de juistheid van een voorgestelde uniciteitsregel? In deze paragraaf onderzoeken we welke uniciteitsregels in principe mogelijk zijn en hoe ze kunnen samengaan. Als voorbeeld nemen we tabel Inschrijving van de OpenSchool-database. In fig uur 2.24 ziet u een stukje van het strokendiagram met twee inschrijvingen uit de voorbeeldpopulatie.

Figuur 2.24 Tabel Inschrijving: welke uniciteitsregel(s)?

Het diagram bevat bewust geen uniciteitspijl, omdat we alle mogelijke uniciteitsregels en combinaties daarvan – zelfs de meest onwaarschijnlijke – zullen onderzoeken. De informatie in de twee rijen laat zich aldus verwoorden: “student 1 heeft zich ingeschreven voor cursus DB op 18-mrt-2012” “student 2 heeft zich ingeschreven voor cursus IM op 26-jan-2012” Om erachter te komen welke uniciteitsregels gelden, moeten allerlei vragen worden beantwoord, zoals: zou student 1 zich ook mogen inschrijven voor cursus IM op nog een tweede dag? Of: zou student 1 zich op 18-mrt-2012 voor nog een tweede cursus mogen inschrijven? Of: mag nog een tweede student zich op 18-mrt-2012 inschrijven voor de cursus DB ? Vanuit de praktijk liggen bepaalde antwoorden misschien weinig voor de hand. Hier echter is het ons te doen om een systematisch onderzoek van wat in principe mogelijk is aan eisen en regels.

Daarbij gaat het om mogelijke uniciteitsregels over één kolom (zie 1, 2 en 3 in figuur 2.25 ), over twee kolommen (4, 5 en 6) en over drie kolommen (7).

Figuur 2.25 Uniciteitsregels over drie kolommen: zeven vragen

Deze zeven regels zijn niet onafhankelijk van elkaar. We gaan immers uit van de conventie (zie paragraaf 2.1.2 ) dat wanneer van twee regels de ene regel strenger is dan de andere, we alleen de strengste regel formuleren. Formuleren we bijvoorbeeld regel 4, dan geven we daarmee impliciet aan dat de regels 1 en 2 niet gelden. En formuleren we regel 7, dan geven we daarmee aan dat alle andere regels niet gelden. Immers: wanneer we een pijl versmallen, verbiedt hij méér en wordt de regel strenger. In het algemeen impliceert een brede pijl dat alle smallere over één of meer van de betrokken kolommen niet gelden, zie figuur 2.26 .

Figuur 2.26 Een brede uniciteitsregel impliceert het niet gelden van ‘smallere uniciteitsregels’

Het antwoord op de ‘zeven vragen’ hangt af van de ‘bedrijfsregels’ die voor de Open School gelden, en is niet zonder meer een vanzelfsprekend ‘ja’ of ‘nee’. In figuur 2.27 geven we voor vier van de zeven regels aan wat een ‘ja’ zou inhouden, met steeds een kleine illustratieve voorbeeldpopulatie.

Figuur 2.27 Vier van de zeven uniciteitsregels over drie kolommen

2.4.1 Combinaties van uniciteitsregels In de vorige paragraaf hebben we gezien dat er zeven verschillende uniciteitsregels over drie kolommen mogelijk zijn. Deze regels kunnen gecombineerd voorkomen, voor zover er geen uniciteitspijlen zijn die helemaal over een andere heen liggen, zie figuur 2.26 . Figuur 2.28 geeft voor enkele mogelijke combinaties een illustratieve voorbeeldpopulatie. Er zijn enkele onwaarschijnlijke combinaties bij, zoals b, waarbij elke kolom uniek is (en waarbij de Open School waarschijnlijk snel failliet zal gaan). De populaties illustreren de vrijheid die er nog is binnen de gegeven regels. Zo illustreert de populatie van c dat de combinatie (student, datum) niet uniek is en de kolom cursus evenmin. En de populatie van d illustreert dat elke kolom apart niet uniek is.

Figuur 2.28 Enkele combinaties van uniciteitsregels over drie kolommen Uniciteitsregels in de praktijk In de praktijk is er meestal maar een beperkt aantal kolommen dat onder een uniciteitsregel valt. Bovendien zijn verreweg de meeste uniciteitsregels één, twee of drie kolommen breed. De overige kolommen vertegenwoordigen nietunieke, ‘gewone’ eigenschappen. De in deze paragraaf behandelde uniciteitsregels en hun combinaties geven daarom een aardige indruk van wat we in de praktijk kunnen tegenkomen.

Opgave 2.6 We willen van elke student van de Open School registreren via welke communicatiemiddelen, met bijbehorend adres, de student bereikbaar is.

Communicatiemiddelen zijn bijvoorbeeld: telefoon, e-mail of een internetadres. Figuur 2.29 geeft een tabel Communicatieregel met twee rijen, die elk een ‘communicatiefeit’ bevatten. Deze feiten hebben de volgende verwoordingen: “student 1 heeft telefoon-adres 020-1234567” “student 9 heeft e-mailadres [email protected]

Figuur 2.29 Tabel Communicatieregel

Geef de juiste uniciteitsregel(s) voor de tabel Communicatieregel, met een zo klein mogelijke, maar illustratieve voorbeeldpopulatie.

2.5 Meer over verwijzingsregels In paragraaf 2.1 zijn verwijssleutels behandeld, als een speciaal soort regel: de referentiële-integriteitsregel. Zo’n regel houdt een verbod in op ‘loze verwijzingen’. Ofwel: elke verwijzende waarde hoort naar precies één primaire-sleutelwaarde te verwijzen. In deze paragraaf gaan we dieper op het fenomeen ‘verwijzing’ in. Achtereenvolgens kijken we naar: 1 verwijssleutels van meer dan één kolom breed 2 verwijzingen van een tabel naar zichzelf 3 meerdere verwijzingen tussen dezelfde tabellen 4 verwijzingen waarbij geen sleutels betrokken zijn.

Ter illustratie van 1 t/m 3 gebruiken we een uitgebreidere versie van de Open School-database, die we hiertoe stap voor stap uitbreiden.

2.5.1 Een samengestelde sleutel Een primaire sleutel kan samengesteld zijn, dat wil zeggen: meer dan één kolom omvatten. De kolomwaarden vormen unieke combinaties, die als logische adressen van de rijen dienst doen. Een verwijzing naar een samengestelde primaire sleutel is zelf ook samengesteld en vormt dus een samengestelde verwijssleutel. Voorbeeld 2.4 Tentamens op de Open School Een student die zich bij de Open School heeft ingeschreven voor een cursus verwerft daarmee het recht om tentamens af te leggen. Elk tentamen hoort bij één inschrijving, dat wil zeggen bij één combinatie van een student en een cursus. Een student mag voor een cursus meerdere keren tentamen doen, dus bij één inschrijving kunnen meerdere tentamens horen. Deze worden van elkaar onderscheiden door een volgnummer. Van elk tentamen wordt natuurlijk een tentamendatum vastgelegd. Later zullen we de mogelijkheid toevoegen om behaalde cijfers en vrijstellingen op te nemen. Figuur 2.30 geeft het relevante deel van de nieuwe structuur: een strokendiagram (a) en een voorbeeldpopulatie (b).

Figuur 2.30 Verwijzing over een brede sleutel

Toelichting – In tabel Tentamen is de kolomcombinatie (student, cursus) niet uniek: een student mag immers meer dan één keer tentamen doen voor dezelfde cursus. Evenmin zijn de combinaties (student, volgnr) en (cursus, volgnr) uniek, zoals de voorbeeldpopulatie illustreert. Conclusie: er gelden over deze drie kolommen geen uniciteitsregels van twee kolommen breed. De meest strenge regel is dus de uniciteitsregel over drie kolommen, zoals die in het strokendiagram is aangegeven. Deze combinatie wordt de primaire sleutel. – Naast de primaire sleutel heeft Tentamen nog een alternatieve sleutel: de combinatie student, cursus, datum. Dit houdt o.a. in dat een student op één datum niet vaker dan één keer tentamen mag doen voor dezelfde cursus. De voorbeeldpopulatie illustreert dat ook hier geen strengere regel geldt. Zo is het toegestaan dat een student op één datum meerdere tentamens aflegt.

– De combinatie (student, cursus) in Tentamen wijst naar de bijbehorende rij in tabel Inschrijving. We zien hier een voorbeeld van een samengestelde verwijssleutel, ook wel ‘brede verwijssleutel’ genoemd.

2.5.2 Een recursieve verwijzing In voorbeelden tot nu toe waren bij elke verwijzing twee verschillende tabellen betrokken. Er kunnen echter ook verwijzingen zijn van een tabel naar zichzelf: recursieve verwijzingen . Voorbeeld 2.5 Docenten die docenten vervangen Een voorbeeld ontlenen we weer aan de Open School, waar veel docenten een vaste vervanger hebben. Zie figuur 2.31 .

Figuur 2.31 Recursieve structuur: strokendiagram (a) en multipliciteitendiagram (b)

Toelichting – Tabel Docent is uitgebreid met een extra, optionele kolom waarin de vervanger (als die er is) wordt aangegeven, met diens acroniem. Deze kolom wijst naar de tabel Docent zelf, zoals altijd via de primaire sleutel. Vandaar dat er een verwijzingspijl loopt van Docent.vervanger naar Docent.acr: een recursieve verwijzing. – Ook in het multipliciteitendiagram (figuur b) is Docent met zichzelf verbonden. Zo lezen we: ‘een docent kan nul of één vervanger hebben’.

Omgekeerd geldt dat een docent voor willekeurig veel (nul, één of meer) collega-docenten als vervanger mag optreden. In de voorbeeldpopulatie is dat aantal echter 0 of 1. Ouder- en kindrollen bij een recursieve verwijzing Ook bij een recursieve verwijzing hebben we een oudertabel (waar de verwijzing naartoe gaat) en een kindtabel (waar de verwijzing vandaan gaat). Dat is twee keer dezelfde tabel, echter in verschillende rollen . In voorbeeld 2.5 is de oudertabel de tabel Docent in de rol ‘vervanger’. De kindtabel is de tabel Docent in de rol ‘vervangene’ (degene die vervangen wordt). Wanneer we twee exemplaren van de tabel Docent zouden maken, één in de rol van ouder (naam: Vervanger) en een in de rol van kind (neutrale naam: Docent), dan zou dit het plaatje van figuur 2.32 opleveren. Hieraan ziet u dat een recursieve verwijzing niets bijzonders is. Er bestaat maar één fysieke databasetabel Docent, de beide exemplaren kunt u als virtuele (denkbeeldige) tabellen opvatten. Elk exemplaar is identiek aan Docent, ook al gebruiken we het vanuit één specifiek gezichtspunt (zoals uitgedrukt door de naam).

Figuur 2.32 Recursieve verwijzing als gewone verwijzing tussen virtuele exemplaren van Docent

Een specifieke naam voor een tabelexemplaar wordt een alias of tabelalias genoemd. Een tabelalias wordt zó gekozen dat deze de rol uitdrukt die een specifiek tabelexemplaar vervult in een bepaald probleem. In figuur 2.32 hadden het kindexemplaar ook een alias kunnen geven, bijvoorbeeld Vervangene. Aliassen zullen we veelvuldig gebruiken in SQL .

2.5.3 Een veel-veel-associatie tussen een tabel en zichzelf Een recursieve verwijzing zorgt voor een ouder-kindrelatie tussen een tabel en zichzelf. We kunnen ook zeggen: hij realiseert een één-op-veelassociatie tussen een tabel en zichzelf. Zo zagen we in voorbeeld 2.5 de vervanger-associatie tussen tabel Docent en zichzelf. We bekijken nu een nieuw soort associatie tussen een tabel en zichzelf: een veel-veel-associatie . Omdat verwijzingen altijd één-veel-associaties opleveren, is daar een extra tabel voor nodig. Zie voorbeeld 2.6 . Voorbeeld 2.6 Cursussen die voorkennis zijn voor andere cursussen Een cursus kan andere cursussen als verplichte voorkennis eisen. Een voorbeeld is de cursus Databases ( DB ), die de cursussen Inleiding informatica ( II ) en Discrete wiskunde ( DW ) als voorkennis eist. Omgekeerd kan een cursus zelf verplichte voorkennis zijn voor andere cursussen. Zo is Databases zelf voorkennis voor de cursussen Informatiemodellering ( IM ) en Business intelligence ( BI ). Aldus krijgen we de volgende vier ‘voorkenniseis-feiten’: “cursus “cursus “cursus “cursus

DB eist als voorkennis cursus II ” DB eist als voorkennis cursus DW ” IM eist als voorkennis cursus DB ” SW eist als voorkennis cursus DB ”

Deze feiten kunnen we niet onderbrengen in de tabel Cursus, omdat een cursuscode in elke der beide ‘cursuscodekolommen’ van het feitenrijtje kennelijk meervoudig kan voorkomen. Er is dan ook een aparte tabel nodig; zie de tabel Voorkenniseis in figuur 2.33 , met strokendiagram (figuur a) en voorbeeldpopulatie (figuur b). Merk op dat Cursus en Voorkenniseis op twee manieren een ouder-kindpaar vormen, via elk der twee verwijzingen.

Figuur 2.33 Veel-veel-associatie tussen Cursus en zichzelf via extra tabel

Omdat Voorkenniseis.cursus en Voorkenniseis.voorkennis geen van beide uniek zijn, zien we een brede primaire sleutel. Deleteregels Het leek ons redelijk dat een cursus die voorkennis is voor één of meer andere cursussen niet zomaar verwijderd kan worden. Vandaar een restricted delete voor de verwijzende sleutel bij Voorkenniseis.voorkennis. Echter, bij een (voorgenomen) verwijdering van een cursus X ligt het voor de hand ook alle voorkennisfeiten te willen verwijderen van het type “cursus X heeft als voorkennis cursus …”. Vandaar een cascading delete voor de verwijzende sleutel bij Voorkenniseis.cursus.

2.5.4 Een veel-veel-associatie tussen een tabel en een andere tabel Ook tussen twee verschillende tabellen kan een veel-veel-associatie bestaan. Een mooi voorbeeld is de ‘begeleidingsassociatie’ tussen Cursus en Docent: een cursus kan door meerdere docenten worden begeleid, terwijl omgekeerd een docent meerdere cursussen kan begeleiden. Van beide kanten gezien gaat het om een meervoudig kenmerk. Zo’n ‘meervoud’ (cursussen, docenten) kan noch als Docent-attribuut noch als Cursus-attribuut worden opgeslagen. De oplossing is ook nu een extra tabel, zie tabel Begeleider in figuur 2.34 . Elke Begeleider-rij is een combinatie van een docent en een door die docent begeleide cursus.

Figuur 2.34 Veel-veel-associatie tussen Docent en Cursus via extra tabel

Deleteregels Wanneer een cursus wordt verwijderd, hoeft die ook niet meer begeleid te worden. Vandaar de cascading delete voor de verwijzing van Begeleider naar Cursus. Voor beide verwijzingen naar Docent geldt echter een restricted delete: een docent mag pas verwijderd worden wanneer hij geen taken meer heeft.

2.5.5 Niet-sleutelverwijzingen Sleutels bieden volgens de relationele theorie dé manier om de rijen van een tabel logisch te adresseren. Het logische adres van een rij is de primaire-sleutelwaarde, die men elders kan gebruiken in de vorm van een verwijssleutelwaarde. In de praktijk zijn er ook andere manieren om een rij logisch te adresseren, zoals voorbeeld 2.7 illustreert. Voorbeeld 2.7 Intervalverwijzing Het strokendiagram van figuur 2.35 toont een structuur met een intervalverwijzing .

Figuur 2.35 Intervalverwijzing

Aan Student zijn twee kolommen toegevoegd: postcode en huisnr. Van de overige kolommen is alleen de primaire sleutel getoond. Er is uitgegaan van de Nederlandse postcodesystematiek. De studentplaatsnaam wordt gevonden via de postcode (als deze is geregistreerd), in een Plaats-tabel met de gestandaardiseerde plaatsnamen en het postcode-interval van elke plaats. We nemen hier aan dat elke plaats precies één ‘eigen’ postcodeinterval heeft en één unieke plaatsnaam (te bereiken door aan de naam zo nodig een provinciecode toe te voegen). Ter illustratie geven we in figuur 2.36 een kleine voorbeeldpopulatie.

Figuur 2.36 Voorbeeldpopulatie bij intervalverwijzing postcodes

De postcode van student 4 ligt tussen die van student 1 en die van student 5 in. Wanneer we weten dat student 1 en student 5 beide in Amsterdam

wonen, kunnen we concluderen dat student 4 ook in Amsterdam woont. Verwijzende woonplaatssleutels in de studententabel zouden dus tot redundantie leiden. De gegeven structuur, waarbij de postcodesystematiek in de database zelf wordt opgenomen, is veel efficiënter en vrij van redundantie. Een verwijzing die op een andere manier tot stand komt dan via gelijkheid van sleutelwaarden, noemen we een niet-sleutelverwijzing . Meestal gaat het om een intervalverwijzing. In diagrammen geven we een nietsleutelverwijzing aan met een stippellijn. Merk op dat beginpostcode en eindpostcode in Plaats beide verplicht en uniek zijn, en dus eigenlijk elk een alternatieve sleutel vormen. Het heeft echter niet zoveel zin deze te vermelden, omdat er een veel sterkere regel geldt: de postcode-intervallen zijn disjunct, dat wil zeggen dat twee intervallen niets gemeenschappelijk hebben. Het is deze sterkere regel die maakt dat er werkelijk van een verwijzing sprake kan zijn.

Opgave 2.7 Een cursusgroep van de Open School heeft een ‘alarmlijn’, waarmee studenten elkaar dringende boodschappen doorgeven, zoals het uitvallen van een les. De werking is simpel: één persoon (de contactpersoon van de groep) start de lijn en belt één of meer medestudenten. Die bellen op hun beurt ook één of meer anderen, enzovoort. Elke betrokkene, behalve degene die de lijn start, wordt door één ander gebeld. Het belschema staat vast en is aan iedereen bekend, zie voor een voorbeeld figuur 2.37 .

Figuur 2.37 Belschema alarmlijn

Breng de naamgegevens en de ‘wie belt wie?’-gegevens van figuur 2.37 onder in een (volledig genormaliseerde) tabel Student, met kolommen voor voornaam, voorvoegsel en achternaam. Maak een strokendiagram.

De telefoonnummers zelf mogen buiten beschouwing blijven, omdat deze al zijn opgenomen in de tabel Communicatieregel van opgave 2.6 .

2.6 De OpenSchool-database (2) Tot slot van dit hoofdstuk voegen we een deel van de veranderingen aan de OpenSchool-database samen. Ook voegen we er nog een enkel detail aan toe, zie figuur 2.38 . Toelichting – Het hart van het diagram wordt nog steeds gevormd door de vier tabellen Docent, Student, Cursus en Inschrijving. – Tabel Docent heeft er een kolom ‘vervanger’ bijgekregen, met bijbehorende recursieve verwijzing (zie paragraaf 2.4.2). – Ook behoort een docent tot een vakgroep, wat leidt tot een tabel Vakgroep met unieke code (primaire sleutel) en unieke naam (alternatieve sleutel). – Aan Cursus is een kolom uren toegevoegd (voor de studiebelasting in uren) en een kolom credits (voor de zwaarte van een cursus, uitgedrukt in ECTS -credits). De cursusnaam is uniek gemaakt door van kolom naam een alternatieve sleutel te maken. Ook is er als ‘meervoudig kind’ de tabel Voorkenniseis bijgekomen, met twee verwijzende sleutels (zie paragraaf 2.4.3). – Aan Inschrijving is de kindtabel Tentamen toegevoegd, met een verwijzing over een tweekolomssleutel (zie paragraaf 2.4.1 ). – Docent en Cursus hebben een gemeenschappelijke kindtabel Begeleider gekregen (zie paragraaf 2.4.4).

Figuur 2.38 Strokendiagram OpenSchool-database

Cijfers en vrijstellingen De tabellen Inschrijving en Tentamen bevatten informatie over cijfers en vrijstellingen. Deze kunnen we het beste toelichten aan de hand van een voorbeeldpopulatie; zie figuur 2.39 . Een Tentamen-rij wordt ingevoerd zodra een student zich voor dat tentamen heeft ingeschreven. Er is dan nog geen cijfer bekend, vandaar dat cijfer optioneel is. Elk tentamencijfer dat wordt ingevoerd, leidt tot berekening (of herberekening) van het bijbehorende cijfer in Inschrijving.cijfer, als het maximum van de corresponderende tentamencijfers. U kunt dit in de voorbeeldpopulatie nagaan. Studenten kunnen voor een cursus vrijstelling aanvragen. Zodra daarover een beslissing is genomen, leidt dit tot een J (‘ja’) of N (‘nee’) in kolom Inschrijving.vrijstelling.

Er geldt de volgende bedrijfsregel , die we later als databaseconstraint kunnen implementeren: een student mag zich pas inschrijven voor een tentamen als hij voor de voorkennisvakken een voldoende heeft of vrijstelling. Opmerking : officieel is er een correspondentie tussen de studiebelasting in uren en de waardering van een cursus in ECTS -credits: 1 ECTS = 28 uur. In de tabel zijn de waarden onafhankelijk en gelden zij slechts bij benadering.

Figuur 2.39 Voorbeeldpopulatie OpenSchool-database

Oefenopgaven In deze paragraaf bouwen we de Orderdatabase van hoofdstuk 1 (zie opgaven 1.6 t/m 1.8) verder uit.

Opgave 2.8 Teken een strokendiagram van de Orderdatabase, met de regels die daarvoor – volgens de gegeven beschrijving – gelden: verplichtewaarderegels (aangegeven door een optionaliteitsmarkering van de nietverplichte kolommen), primaire sleutels en eventueel alternatieve sleutels. Geef ook aan welke refererende-actieregels (delete- en updateregels) naar uw mening zouden moeten gelden. In de volgende opgaven wordt de Orderdatabase stap voor stap gewijzigd en uitgebreid. Raadpleeg hierbij zéker de uitwerkingen (in de Boekverkenner), want die geven de juiste uitgangssituatie voor volgende hoofdstukken.

Opgave 2.9 Elke orderregel van de Orderdatabase geven we een volgnummer, dat uniek is per order (zie figuur 2.40 ).

Figuur 2.40 Tabel Orderregel

Welke consequentie heeft dit voor het strokendiagram?

Opgave 2.10 Als onderdeel van een nieuwe marketingstrategie wordt voor klanten de mogelijkheid geopend andere klanten aan te brengen. Hieraan is een bonusregeling gekoppeld (die hier verder buiten beschouwing blijft). Welke wijziging moet de databasestructuur ondergaan om het aanbrengen van klanten te kunnen administreren? Teken die wijziging in het strokendiagram. Teken voor het gewijzigde deel ook een multipliciteitendiagram.

Opgave 2.11 De directie besluit tot introductie van een klachtregistratiesysteem. Klachten zullen worden geregistreerd op het niveau van orderregels: elke klacht zal dus betrekking hebben op één orderregel, maar per orderregel kunnen door de klant meerdere klachten worden ingediend. Elke klacht krijgt een uniek klachtnummer. Tevens wordt geregistreerd of de klacht is afgehandeld. Geef in het strokendiagram aan hoe de databasestructuur moet worden uitgebreid. Opgave 2.12 Voor afname van grotere hoeveelheden van één artikel geldt een systeem van kwantumkorting. Bij afname van 10 t/m 49 stuks bijvoorbeeld geldt een korting van 5%. Het kortingspercentage is onafhankelijk van het artikel. Teken de noodzakelijke uitbreiding van het strokendiagram. Opgave 2.13 Een artikel kan behoren tot een artikelgroep. Artikelgroepen worden uniek geïdentificeerd door een code en tevens door een omschrijving. Verder moeten vanaf nu in plaats van de ‘prijs’ een aparte verkoopprijs en inkoopprijs worden geregistreerd. Tot slot moet van elk artikel de voorraad moet worden bijgehouden. Teken het bij deze aanpassingen horende fragment van het strokendiagram. Opgave 2.14 Teken een strokendiagram waarin alle ingrepen van de voorgaande opgaven zijn verwerkt. Opmerking : het resultaat is een logisch relationeel model. Het implementatiemodel kan hiervan nog gaan afwijken. Een kleine afwijking is voor ons van belang: Order (in hoofd- en/of kleine letters) is een zogenaamd gereserveerd SQL -woord (een woord dat tot de vaste SQL taalschat behoort). In sommige dialecten, waaronder Firebird, is het om die reden niet bruikbaar als tabelnaam of kolomnaam. Voeg daarom aan de tabelnaam Order of kolomnaam order een underscore toe: Order_ respectievelijk order_ . (Het antwoord vindt u als gewoonlijk bij de uitwerkingen in de Boekverkenner. Bijlage 4 geeft een voorbeeldpopulatie.)

3 Communiceren met een relationele database In de hoofdstukken 1 en 2 stond de ‘achterkant’ van een relationeel systeem centraal, dat wil zeggen: het rdbms en de database op de server. In dit hoofdstuk gaat het ook om de ‘voorkant’: applicaties (toepassingsprogramma’s voor programmeur of eindgebruiker) op de client. En meer nog gaat het om de communicatie tussen de applicaties en het rdbms. We zullen kennismaken met SQL , de meest gebruikte relationele taal voor opdrachten vanuit een applicatie aan het rdbms. De eerste applicatie is een programma, waarmee u SQL -statements kunt intypen, laten controleren, versturen en uitvoeren. We noemen zo’n programma een SQL -querytool . Een SQL -querytool hoort tot de

softwaregereedschappen van bouwers van relationele systemen. Ook is het uitstekend geschikt om SQL te leren. De tweede applicatie is een eindgebruikersapplicatie met een GUI (grafische user interface). De gebruiker hoeft hier niet zelf SQL opdrachten in te typen; ze zijn voorgeprogrammeerd in de applicatie, of ze worden door de applicatie dynamisch gegenereerd vanuit gegevens die de gebruiker heeft ingevoerd. Zie figuur 3.1 voor de twee typen client in een netwerk. In ons geval bevinden serverprogramma en beide typen clientsoftware zich op dezelfde computer.

Figuur 3.1 Grafische eindgebruikersapplicatie of SQL-tool: beide ‘spreken’ SQL tegen het rdbms

Voorbeelddatabase De voorbeelddatabase is Reijnders’ Toetjesboek. Bijna alle SQL opdrachten kunnen worden uitgeprobeerd via de ‘Boekverkenner’.

3.1 Uitbreiding van Reijnders’ Toetjesboek In figuur 3.2 en figuur 3.3 herhalen we het strokendiagram respectievelijk de voorbeeldpopulatie van Reijnders’ Toetjesboek, met een kleine toevoeging: Ingredient heeft een extra kolom volgnr gekregen. De kolom Ingredient.volgnr wordt gebruikt om de ingrediënten in de juiste volgorde af te drukken, zodat bijvoorbeeld niet ‘peper’ voorop komt.

Figuur 3.2 Toetjesboek: strokendiagram, uitgebreid met kolom Ingredient.volgnr

Figuur 3.3 Toetjesboek: voorbeeldpopulatie, met ingrediëntvolgnummers

3.2 SQL als ‘universele gegevenstaal’ Een relationeel databasemanagementsysteem ‘verstaat’ SQL . In de loop der tijd heeft SQL de status verworven van ‘universele gegevenstaal’ voor rdbms’en. In deze paragraaf bespreken we enkele algemene aspecten van SQL : wat we ermee kunnen, subtalen, standaard- SQL en SQL -dialecten.

3.2.1 Wat kunnen we met SQL? SQL is een relationele gegevenstaal. Dat wil zeggen dat de taal is

ontworpen om tabellen en tabelstructuren te manipuleren. Idealiter zijn dat tabellen in de zin van relaties, maar we hebben al gezien dat SQL maar ten dele relationeel is. In SQL kunnen we opdrachten formuleren, die we vanuit een clientprogramma naar het rdbms kunnen sturen. SQL is dus een opdrachtentaal en de opdrachten, die we vanaf nu meestal statements zullen noemen, kunnen worden gezien als de zinnen van die taal.

3.2.2 SQL-subtalen SQL bevat de volgende subtalen voor deeltaken:

– DDL ( Data Definition Language ) voor het definiëren van structuren en

het vastleggen van regels – DML ( Data Manipulation Language ) voor het uitvoeren van operaties voor het opvragen of wijzigen van gegevens

– DAL ( Data Authorization Language ) voor de autorisatie, dat wil

zeggen het regelen van de gebruikerstoegang tot de gegevens – DCL ( Data Control Language ) voor beheertaken met betrekking tot de ‘performance’, dat wil zeggen de prestaties van het systeem op het gebied van snelheid en geheugengebruik.

Men komt ook afwijkende indelingen tegen; zo wordt de DAL veelal tot de DDL gerekend. Verder is het zo, dat deze benamingen niet specifiek zijn voor SQL en zelfs niet voor relationele gegevenstalen. Zie tabel 1.1 voor een overzicht van de vier subtalen, de daarmee uitgevoerde taken en specifieke voorbeelden. Tabel 3.1 Subtalen van SQL

DDL ( Data Definition Language ) dient om structuuropdrachten te

formuleren (waarmee databaseobjecten worden gecreëerd of gewijzigd) en ook de regels waaraan de gegevens moeten voldoen. In een relationele database zijn tabellen het belangrijkste type databaseobject. Maar er zijn meer typen, bijvoorbeeld gebruikers. Alle informatie (structuur en regels) over alle typen objecten worden door het rdbms beschreven in een databaseschema , dat zelf ook weer bestaat uit ... tabellen! Voorbeelden zien we verderop in dit hoofdstuk en veel uitgebreider in hoofdstuk 11 ‘Definitie van gegevensstructuren’. DML ( Data Manipulation Language )omvat alle queryopdrachten waarmee

de database kan worden bevraagd en waarmee een tabelinhoud kan worden gewijzigd. Bij het wijzigen van een tabelinhoud kan het gaan om het toevoegen van nieuwe rijen, of het verwijderen of wijzigen van bestaande rijen. Ook hiervan zien we verderop in dit hoofdstuk al enkele voorbeelden. Een uitgebreide behandeling volgt in hoofdstuk 10 ‘Wijzigen van een database-inhoud’.. DAL ( Data Authorization Language ), vaak gerekend tot de DDL , is de

subtaal om toegangsregels vast te leggen. Deze beschrijven wat verschillende gebruikers met de gegevens mogen doen. Via toegangsregels kan een DBA bijvoorbeeld vastleggen dat een gebruiker de gegevens in een bepaalde tabel alleen mag raadplegen, terwijl een andere gebruiker die gegevens ook mag wijzigen. Met voorbeelden wachten we tot hoofdstuk 12 ‘Autorisatie’. DCL ( Data Control Language ) komt in beeld, wanneer we ons (in de rol van DBA ) druk gaan maken om de prestaties van het systeem. Bij

‘prestaties’ moet u denken aan ‘zo snel mogelijk’ en ‘zo min mogelijk geheugen’. Hiervoor kan het nodig zijn de manier waarop de gegevens fysiek zijn opgeslagen of worden benaderd, te veranderen. Voorbeelden komen volop aan bod in hoofdstuk 13 ‘Query-optimalisatie’.

3.2.3 Standaard-SQL en SQL-dialecten SQL is een min of meer levende taal. Succesvolle fabrikanten van rdbms’en

hebben bijgedragen aan de ontwikkeling, óf de ontwikkeling juist afgeremd. Want als een fabrikant veel heeft geïnvesteerd in de eigen variant, ziet die niet graag dat de taal zich heel anders ontwikkelt. Belangrijke fabrikanten zijn: IBM (met onder meer het rdbms DB2 ), Oracle (met Oracle) en Microsoft (met SQL Server). Bij elk rdbms hoort een eigen SQL - dialect , al zijn de verschillen soms marginaal. Verschillende organisaties en bedrijven hebben geprobeerd de ontwikkeling van SQL te sturen door het formuleren van standaarden . De eerste was ANSI (American National Standards Institute); later kwam daar ISO (International Standards Organization) bij. In dit boek maken we gebruik van het rdbms Firebird, een open source databaseproduct. Daardoor hebben we te maken met Firebird- SQL . Dit sluit nauw aan bij de belangrijkste ANSI/ISO -standaarden, zeker voor de basisopdrachten. Wanneer een enkele keer een SQL -opdracht niet in Firebird kan worden uitgevoerd, wordt dat via commentaar in de SQL code aangegeven. De ANSI/ISO-standaarden van SQL De eerste (ANSI-)standaard van SQL dateert van 1986. Het jaar daarop volgde adoptie door ISO en sindsdien zijn ANSI en ISO gezamenlijk opgetrokken in het verder ontwikkelen van de standaard. De standaard werd in het begin bewust mager gehouden en nogal wat zaken werden ‘leverancierafhankelijk’ verklaard. Zelfs primaire sleutels en relationele integriteit (verwijssleutels) werden nog niet beschreven. Men beperkte zich tot wat de belangrijkste dialecten gemeenschappelijk hadden. Zo wilde men het leveranciers gemakkelijker maken zich openlijk aan SQL te conformeren, een slimme manier om de standaard enig gezag te verlenen. Een belangrijke uitbreiding vond plaats in 1992, met SQL2 (ook wel SQL92 genoemd). Deze standaard ging veel verder, maar werd daardoor minder breed gedragen. In zekere zin dreigde het zelf een dialect naast de andere dialecten te worden. Om dit te ondervangen, heeft men verschillende ‘levels of conformance’ bedacht. Latere standaarden zijn: SQL:1999 (ook SQL3 genoemd), SQL:2003, SQL:2006 en SQL:2008, allemaal lijvige boekwerken waarin ANSI/ISO sturing probeerde te geven aan de nieuwste ontwikkelingen. Van die ontwikkelingen noemen we hier triggers (te behandelen in hoofdstuk 16 ‘Triggers en stored procedures’) en het gebruik van XML in combinatie met SQL (valt buiten het bestek van dit boek). Een groot deel van de SQL-standaarden is overigens gewijd aan embedded SQL, een taaluitbreiding bedoeld voor het opnemen van SQL-statements binnen procedurele (3GL) programmeertalen zoals Java, C++ of Pascal. Embedded SQL wordt steeds minder gebruikt en valt buiten het bestek van dit boek. De belangrijkste marktpartijen – waaronder Oracle en IBM – hebben mede hun stempel gedrukt op de ontwikkeling van de ANSI/ISO-standaarden. Er is geen sprake geweest van leverancieronafhankelijk ontwikkelen van een ‘mooie’ taal.

Mede daardoor is er heel wat op SQL aan te merken, wat we in dit boek ook regelmatig zullen doen. We hopen dat de lezer hierdoor leert om vanaf een wat hoger standpunt de theorie achter SQL en de praktijk van het gebruik ervan te bezien. De keuze van een dialect is in wezen niet belangrijk. Wie het echt snapt, stapt moeiteloos over van het ene naar het andere.

3.3 De Boekverkenner en de Interactive Query Utility De Boekverkenner biedt een omgeving waarmee het bestuderen van de cursus en het uitvoeren van SQL -opdrachten zo gemakkelijk en prettig mogelijk wordt gemaakt. De integrale tekst van de cursus is via de Boekverkenner beschikbaar. Een belangrijke voorziening is de mogelijkheid om SQL -opdrachten direct vanuit de tekst uit te voeren. Daartoe werkt de Boekverkenner nauw samen met een SQL -querytool, de Interactive Query Utility ( IQU ). Raadpleeg de Introductie voor de wijze waarop de software moet worden geïnstalleerd.

3.3.1 Boekverkenner Figuur 3.4 toont het hoofdvenster van de Boekverkenner, met een verklaring van de knoppen. Toelichting – De Voorbeeldnavigator brengt u bij allerlei informatie over de voorbeelddatabases: beschrijving, databasediagrammen, voorbeeldpopulatie en scripts. Bij het Toetjesboek kunt u hier de applicatie Reijnders’ Toetjesboek starten. – Navigatieknoppen: de Boekverkenner houdt bij waar u bent geweest; met de navigatieknoppen kunt u langs dat pad heen en terug bewegen. – Bookmarks kunt u plaatsen op elke gewenste boekpagina. Het zijn uw ‘aantekeningen in de marge’. Als u ze een duidelijke naam geeft, vindt u ze weer gemakkelijk terug.

Figuur 3.4 De Boekverkenner

Databasevenster Het knopje met het ‘koektrommeltje’ (symbool voor een database) is van bijzonder belang. Hiermee opent u het databasevenster, met de volgende mogelijkheden (zie figuur 3.5 ): – creëren of verwijderen van een voorbeelddatabase – inloggen op een voorbeelddatabase. In plaats van creëren zeggen we ook installeren . En verwijderen is synoniem met de-installeren . Creëren houdt in: aanmaken; we doen dat via ‘scripts’ van SQL -opdrachten. Later in dit hoofdstuk zullen we dergelijke scripts bekijken. Wie wat al te wilde ingrepen heeft gepleegd, kan gemakkelijk een voorbeelddatabase opnieuw laten creëren. Via het databasemenu kunnen meerdere databases in één keer worden gecreëerd of verwijderd: plaats de vinkjes zoals gewenst en druk op de knop met het vinkje.

Inloggen houdt in: uzelf bekend maken aan het rdbms met gebruikersnaam en wachtwoord en het tot stand brengen van een verbinding met één specifieke database ( databaseconnectie ). SQL opdrachten die hierna worden gegeven, zullen op deze database betrekking hebben. Om op een database in te loggen, hoeft u slechts de naam te markeren en op het bliksemknopje te drukken. Bij elke voorbeelddatabase hoort één speciale gebruiker die ‘eigenaar’ is van de database. De Boekverkenner kent de gebruikersnamen en wachtwoorden, waardoor het inloggen verder automatisch gaat.

Figuur 3.5 Het databasevenster

Later zullen we zien dat creëren, verwijderen of inloggen, zowel automatisch via het databasemenu als via expliciete opdrachten kan plaatsvinden. De Test-database en eigen databases Een van de voorbeelddatabases is Test.fdb. Deze is na installatie leeg en dient om vrij te experimenteren. Via het databasevenster kan ook met eigen voorbeelddatabases worden gewerkt, wanneer de bijbehorende databasebestanden (fdb-bestanden) in de standaard databasemap worden geplaatst. Deze map c:\...\RelSQL\DB is bereikbaar via ‘Relationele databases en SQL‘ in het Windows Startmenu.

Opgave 3.1 a Start de Boekverkenner en probeer de mogelijkheden uit de figuren 3.4 en 3.5 uit. b Raadpleeg de hulpfunctie voor nog meer mogelijkheden. Kijk vooral naar de sneltoetsen, die het werken met de Boekverkenner nog sneller en gemakkelijker maken. De tijd hieraan besteed verdient u snel terug!

3.3.2 Interactive Query Utility Eindgebruikers hoeven geen kennis van SQL te hebben. Ze communiceren doorgaans via een grafische applicatie met het rdbms. Anders ligt dat voor databasebeheerders of ontwikkelaars van informatiesystemen, voor wie grondige kennis van SQL is gewenst. Hoewel ook voor deze professionals steeds meer grafische programma’s beschikbaar komen, blijft het vaak nodig om rechtstreeks SQL -opdrachten in te typen en te versturen naar het rdbms. Ook voor het interpreteren van foutmeldingen is die kennis nodig. Zodoende is kennis van SQL een noodzaak voor iedereen die zich automatiseerder, bouwer of beheerder noemt en die met relationele databases te maken heeft. Daarom is altijd een apart programma beschikbaar om rechtstreeks in SQL te communiceren met het rdbms. Voor zo’n simpele SQL -client zijn diverse benamingen in omloop, zoals SQL -querytool of SQL -omgeving . De Interactive Query Utility (kortweg IQU ) is zo’n SQL -tool. IQU is geïntegreerd in de Boekverkenner en een uitstekend middel om SQL te leren. Van bijzonder belang is de mogelijkheid om in de Boekverkenner een query in de boektekst aan te klikken, waarna deze in het IQU invoervenster wordt geplaatst. Daarbij wordt automatisch ingelogd op de juiste voorbeelddatabase (als dit nog niet was gebeurd). Indien die voorbeelddatabase nog niet is geïnstalleerd, wordt automatisch het databasevenster geopend. Query’s In diverse afkortingen is de letter ‘Q’ voor query opgedoken. Maar wat is een query nu precies? Soms wordt de term vertaald met ‘zoekvraag’. Meer in het algemeen is een query een opdracht aan een databasemanagementsysteem om gegevens te leveren of om de gegevensinhoud van de database aan te passen.

Zie figuur 3.7 voor het IQU -venster met een verklaring van de knoppen. Via de hulpfunctie komt u alles te weten wat u weten wilt.

Figuur 3.7 IQU-venster

Toelichting – Het invoerdeel (bovenste deelvenster) bevat het SQL -statement dat ook in de tekst van figuur 3.4 zichtbaar is. Het zou daarin kunnen zijn aangeklikt, maar het kan ook gewoon zijn ingetypt. – Het uitvoerdeel (onderste deelvenster) bevat, na het uitvoeren van het statement (met bliksemknopje), de opgevraagde gegevens. Waarschuwing Het succesvol uitvoeren van een opdracht is soms afhankelijk van een specifieke beginsituatie. Lukraak uitvoeren van SQL -opdrachten kan tot foutmeldingen of onverwachte resultaten leiden. Tekstversie van IQU Wie IQU de eerste keer opent via de Boekverkenner (met het Q-knopje of via een van de knoppen van het databasescherm) krijgt een mooi, grafisch scherm. Een nadeel hiervan kan zijn dat het niet schaalbaar is. Voor wie liever met een schaalbaar scherm werkt is er ook een tekstversie van IQU beschikbaar. Vanuit

de Boekverkenner schakelt u over door bij het openen (met een van de genoemde knoppen) de ctrl-toets ingedrukt te houden. U houdt de tekstversie tot u weer terugschakelt, ook weer met de ctrl-toets. De tekstversie kunt u ook openen vanuit ‘Relationele databases en SQL’ in het Windows Startmenu.

Opgave 3.2 Ook IQU kent een serie sneltoetsen, die het werken ermee niet alleen gemakkelijker maken maar ook handige, nieuwe mogelijkheden bieden. Raadpleeg de hulpfunctie voor een overzicht en probeer ze uit. Ook hier geldt dat de geïnvesteerde tijd snel wordt terugverdiend. Opmerking : voor de ctrl+shift-sneltoetsen moet u eerst met de muis een tabelnaam of een deelcommando selecteren.

3.4 Een eerste select-statement In deze paragraaf gaan we uit van een al bestaande Toetjesboek-database, waaruit we gegevens zullen opvragen. Via de Boekverkenner kunt u het Toetjesboek moeiteloos installeren. Bij die installatie worden twee scripts (programma’s met SQL -statements) naar het rdbms verstuurd: een voor het aanmaken van de (lege) tabellen (create-script) en een voor het vullen daarvan met de rijen van de voorbeeldpopulatie (insert-script). Installatie voorbeelddatabase De Toetjesboek-database en elke andere voorbeelddatabase is te installeren via het databasevenster van de Boekverkenner, zie figuren 3.4 en 3.5. Bij aanklikken in de cursustekst van een SQL-statement bij een nog nietgeïnstalleerde database wordt dit venster automatisch geopend en wordt er al een vinkje gezet. Via het databasevenster kunt u elke database ook de-installeren (verwijderen) en opnieuw installeren. U kunt daardoor met een gerust hart experimenteren.

Om gegevens op te vragen, dient het DML -commando select . Een select - statement kan heel complex zijn. Maar liefst vijf hoofdstukken, 6 t/m 10, zijn er aan gewijd. In deze paragraaf beperken we ons tot het meest eenvoudige voorbeeld: opvragen van de volledige inhoud van één tabel.

3.4.1 Opvragen van volledige tabelinhoud Het eenvoudigste type select -statement is een select van een volledige tabel: alle gegevens van alle rijen.

Voorbeeld 3.1 Geef alle gegevens van alle gerechten. Oplossing: select * from   Gerecht

Resultaat: NAAM               ENERGIEPP BEREIDINGSTIJD BEREIDINGSWIJZE =============== ============ ============== ================= Coupe Kiwano          430.80             20 Schil ... Glace Terrace         402.50              5 Neem ... Mango Plus Plus       130.50              8 Snijd ...

De tabel Gerecht heet de brontabel van de select -query. Het sterretje staat voor ‘alle kolomwaarden’. Het resultaat van elke select -query is zelf ook weer een tabel, in dit geval de hele brontabel. De energie per persoon wordt in twee decimalen afgedrukt, overeenkomend met de interne manier van opslaan: als getal met twee decimalen. In een eindgebruikersapplicatie kan de weergave worden aangepast: zonder eindnullen. De bereidingswijzen zijn verkort weergegeven; in werkelijkheid worden ze in hun geheel afgedrukt.

Opgave 3.3 In deze opgave onderzoeken we hoe je via de Boekverkenner en de Interactive Query Utility een verbinding maakt met een database (na deze zo nodig eerst te hebben aangemaakt) en een query uitvoert. Ook laten we zien hoe je een connectie verbreekt of een database verwijdert.

a Zoek in de Boekverkenner de query van voorbeeld 3.1 en klik erop. Omdat de Toetjesboek-database nog niet bestaat, opent zich het databasevenster zoals in figuur 3.5 . Geef opdracht deze database te creëren (kijk of het vinkje goed staat, zoals in de figuur). Na creatie hebt u een connectie met de database (u bent ‘ingelogd’), en tevens wordt IQU geopend, met de query in het invoerdeel. b Voer de query uit, via het knopje ‘uitvoeren’ (zie figuur 3.7 ). c Sluit IQU . U bent daarna ‘uitgelogd’: de connectie met de database Toetjesboek is verbroken.

d Log opnieuw in op Toetjesboek, maar nu via het knopje ‘inloggen op database’ van de Boekverkenner. Voer de query van voorbeeld 3.1 nu met de hand in en laat hem uitvoeren via een sneltoets. e Verwijder (de-installeer) de Toetjesboek-database en installeer hem opnieuw.

3.4.2 Opmaak Het statement van paragraaf 3.4.1 is over twee regels opgemaakt, met de select -clausule en de from -clausule elk op een aparte regel. In dit boek hanteren we vrij strikte opmaakconventies, gericht op de menselijke lezer. Voor de SQL -interpreter (het programmaonderdeel van het rdbms dat het statement verwerkt) maakt het niets uit; die is ook tevreden met de hele opdracht op één regel: select * from Gerecht

of over vier regels verspreid: select * from Gerecht

Spaties en ‘returns’ zijn, net als tabs, wittekens (Engels: whitespaces ). Elk witteken is toegestaan als scheidingsteken tussen de ‘woorden’ en tekens van een statement. Als SQL -programmeur bewijst u uzelf en uw collega’s echter een dienst door een strakke opmaak met vaste conventies. Automatiseringsbedrijven zullen hun werknemers daartoe dwingen, via een ‘huisstijl’.

3.4.3 Gereserveerde woorden en identifiers Een SQL -statement is op te vatten als een ‘zin’ met ‘woorden’ en symbolen. De ‘woorden’ zijn er in twee soorten:

1 de woorden die tot de vaste taalschat behoren; deze worden gereserveerde woorden (Engels: reserved words , ook wel: keywords ) genoemd 2 de woorden die door de programmeur zijn gekozen. In het hiervoor gegeven select -statement zijn select en from gereserveerde woorden. De SQL -tool IQU heeft eigen, specifieke opdrachten waarin eveneens gereserveerde woorden voorkomen. Door de programmeur gekozen namen moeten identifiers zijn, dat zijn namen die voldoen aan de volgende beperkingen: 1 Hun maximale lengte is 31 tekens. 2 Ze mogen alleen letters, cijfers en de underscore (_) bevatten. 3 Ze moeten beginnen met een letter. 4 Ze mogen niet overeenkomen met een gereserveerd woord. Veel belangrijker dan deze ‘harde’ regels (die u vanzelf leert) zijn de volgende ‘zachte’ regels: – Kies duidelijke namen, die de betekenis goed weerspiegelen. – Wees consequent bij het kiezen van namen. – Wees terughoudend in het kiezen van afkortingen; als u afkort, doe het dan volgens strakke conventies.

Voor gelijksoortige objecten of eigenschappen hanteren we een vaste naamgeving- en spellingconventie. Zo schrijven we tabelnamen met één hoofdletter en kolomnamen in kleine letters.

3.4.4 Weergave van nulls Een null in de database wordt in een resultaattabel weergegeven als < NULL >, zie voorbeeld 3.2 . Voorbeeld 3.2 Weergave van nulls select * from   Product

geeft resultaattabel: NAAM        EENHEID        ENERGIEPE =========== =========== ============ ijs         liter            1600.00 kiwano      stuks              40.00 slagroom    deciliter         336.00 suiker      gram                4.00 tequila     eetlepel           30.00 aardbeien   gram                0.25 pernod      eetlepel           35.00 peper                    mango       stuks              80.00 zure room   deciliter         195.00 banaan      stuks              40.00

Later komen we ook weergaven van resultaattabellen tegen met lege plekken. Zo’n lege plek duidt op een lege string , dat is een tekst van lengte 0, zonder tekens. In Firebird is een lege string iets anders dan een null. Een lege string is een waarde, terwijl een null een speciale waarde is die eigenlijk een indicator is voor ‘geen waarde’. Het is belangrijk op dit verschil te letten in de resultaattabellen. Er zijn rdbms’en, waaronder Oracle, die dit onderscheid niet maken en een lege string gelijkstellen aan een null. In hoofdstuk 4 zullen hier dieper op ingaan en zien dat het uit conceptueel oogpunt terecht is onderscheid te maken. Vanuit presentatieoogpunt hoeven we ons geen zorgen te maken over het verschil tussen null en de lege string. Het gaat hier immers om een tool voor programmeurs en niet voor eindgebruikers. Voor programmeurs is een ‘mooie’ weergave minder belangrijk en kan het nuttig zijn het onderscheid te zien.

Opgave 3.4 a Voer, als u dit nog niet gedaan hebt, de SQL -query’s van de voorgaande tekst uit. Doe dit door aanklikken in de tekst en/of via intypen in IQU . b Probeer de IQU -opdrachten uit die in dit stadium zin hebben. Doe dit via de knopjes van het IQU -venster ( figuur 3.7 ) en via sneltoetsen. NB: vanaf nu wordt het zelf uitproberen van SQL -query’s niet meer via opdrachten gestuurd. We gaan er tevens van uit dat u – gebruikmakend van de hulpfuncties – de nodige handigheid ontwikkelt in het gebruik van de Boekverkenner en IQU .

3.4.5 Commentaarcode Soms zullen we SQL -code verduidelijken of documenteren via commentaar in gewoon Nederlands. Commentaar is bedoeld voor de menselijke lezer en wordt niet verzonden naar het rdbms. Er zijn twee soorten commentaar: eenregelig of meerregelig. Eenregelig commentaar begint met twee streepjes en wordt vaak gebruikt om een onderdeel van een SQL -statement te verduidelijken. Bijvoorbeeld: select *         -- 2. Geef per rij alle kolomwaarden from   Gerecht   -- 1. Ga uit van tabel Gerecht: alle rijen

In dit boek zullen we commentaar soms ‘misbruiken’ voor didactische doeleinden, zoals in het voorgaande statement. Daar geven de nummers ‘1’ en ‘2’ in het commentaar aan wat de conceptuele verwerkingsvolgorde is, dat wil zeggen: in welke volgorde u de regels als mens moet lezen om de juiste werking te begrijpen. Merk op dat u het moet lezen vanaf de tweede regel: de from -clausule. Daar staat uit welke tabel de gegevens afkomstig zijn. De select -clausule zegt welke gegevens in het eindresultaat komen. In dit voorbeeld zijn dat alle kolomwaarden. Meerregelig commentaar Meerregelig commentaar staat tussen ‘haakjes’: /* en */. Alle tekst daartussen is bedoeld voor de menselijke lezer. Het wordt meestal gebruikt om een SQL -script te documenteren. Zo’n script bevat een reeks SQL -opdrachten voor het uitvoeren van een grotere taak, bijvoorbeeld het creëren van alle tabellen van de Toetjesboek-database. Omdat scripts worden bewaard, is het vaak zinvol er documentatie aan toe te voegen, zoals de auteur en de datum van de laatste wijziging.

Opgave 3.5 Raadpleeg het ‘create-script’ van de Toetjesboek-database via de Voorbeeldverkenner (knop Vb) van de Boekverkenner. Neem globaal kennis van de inhoud.

3.5 Projecties en selecties Door een tabel in te perken tot een deelverzameling van zijn kolommen, krijgen we een nieuwe tabel: een projectie van de oorspronkelijke. Ook kunnen we een tabel inperken tot een deelverzameling van zijn rijen, door het toepassen van een conditie op de rijen. In dit geval spreken we van een selectie .

3.5.1 De projectieoperatie In voorbeeld 3.3 wordt een projectie uitgevoerd op twee kolommen van de brontabel. Voorbeeld 3.3 Projectie Geef de naam en de bereidingstijd van alle gerechten. Oplossing: select naam, bereidingstijd from   Gerecht

Resultaat: NAAM            BEREIDINGSTIJD =============== ============== Coupe Kiwano                20 Glace Terrace                5 Mango Plus Plus              8

De projectie is een relationele operatie : zij werkt op een tabel en het resultaat is ook een tabel. Zoals steeds dienen we hierbij ‘tabel’ te lezen in de zin van ‘relatie’. Opmerking: een projectie ligt vast in de select -clausule. Dit kan verwarring geven met de in paragraaf 3.5.3 te behandelen selectie operatie.

3.5.2 Constante en berekende resultaatkolommen Een resultaattabel mag behalve kolommen van de brontabel ook kolommen met constante of berekende waarden bevatten. Voorbeeld 3.4 geeft een voorbeeld waarbij de resultaattabel naast een kolom van de brontabel ook een kolom met een tekstconstante bevat en een derde kolom waarvan de waarden door berekening zijn verkregen. Voorbeeld 3.4 Constante kolom en berekende kolom Geef van alle gerechten de energie per persoon in kjoule. Toelichting: joule en kjoule (kilojoule = 1000 joule) zijn ISO -energieeenheden, naast de verouderde calorie en kilocalorie (kcal). Eén kcal komt bij benadering overeen met 4,2 kjoule. Oplossing: select naam, 'heeft een energie van', 4.2 * energiePP from   Gerecht

Resultaat: NAAM            CONSTANT                       MULTIPLY =============== ===================== ================= Coupe Kiwano    heeft een energie van          1809.360 Glace Terrace   heeft een energie van          1690.500 Mango Plus Plus heeft een energie van           548.100

Een tekstconstante staat tussen enkele aanhalingstekens. Tekstconstanten mogen spaties en andere wittekens bevatten (tabs en einde-alineatekens). Deze zijn een letterlijk onderdeel van de tekstconstante; het zijn daarin tekens als alle andere. Het toestaan van constante en/of berekende kolommen in een resultaattabel komt neer op een generalisatie van de projectieoperatie.

3.5.3 De selectieoperatie Door een where -clausule toe te voegen is het mogelijk alleen rijen te selecteren die aan een bepaalde voorwaarde voldoen. Deze operatie heet: selectie . Let bij de voorbeelden op de volgorde waarin u de clausules moet lezen.

Voorbeeld 3.5 Geef naam en bereidingstijd van alle gerechten die binnen 10 minuten kunnen worden klaargemaakt. Oplossing: select naam, bereidingstijd     -- 3 from   Gerecht                  -- 1 where  bereidingstijd  5 geeft geen true en geen false . De logische uitkomst van null > 5 is dus niet bepaald en evenmin die van de ontkenning not (null > 5). De meest simpele manier om hiermee om te gaan is de introductie van een derde logische waarde: unknown . Dit geeft een driewaardige logica , met drie logische waarden: true , false en unknown . NB: alléén rijen die true opleveren, doorstaan de selectieconditie. Dus: ‘ where unknown ’ heeft hetzelfde effect als ‘ where false ’. De werking van de operatoren not , and en or in de driewaardige logica is af te lezen uit de volgende waarheidstabellen, die een uitbreiding zijn van die van de tweewaardige logica:

Toelichting – Wanneer we niet weten of een bewering true is of false (een logische unknown ), weten we evenmin of de ontkenning true is of false . Dit verklaart waarom ‘ not unknown’ weer unknown oplevert. – Evenzo: wanneer we van twee beweringen weten dat er één true is, maar van de ander niet weten of zij true is of false , dan kunnen we van

de samenstelling met and noch true noch false als uitkomst afleiden. Dit verklaart waarom ‘ true and unknown ’ als uitkomst unknown oplevert. – Wanneer we echter van een samenstelling met ‘ or ’ weten dat minstens een van de twee beweringen true is, dan weten we genoeg: de uitkomst is true , ook al is de logische waarde van de andere bewering onbekend of onbepaald. Dit verklaart waarom ‘ true or unknown’ een true oplevert. – Ook van de overige samenstellingen met unknown is de logische uitkomst op deze manier te verklaren. Zodoende geeft het volgende statement: select student, cursus, cijfer, vrijstelling from   Inschrijving where  vrijstelling = 'J' or not cijfer 5 (ongeacht de waarde van vrijstelling en of die is ingevuld): STUDENT CURSUS CIJFER VRIJSTELLING ========= ====== ====== ============ 1 II          7 N 1 DB          8 N 2 II      J 2 DW      J 4 II      J

Zijn zowel vrijstelling als cijfer niet ingevuld, dan wordt de conditie: null = 'J' or not null 5 or vrijstelling = 'J')))

In de voorbeeldpopulatie blijken drie inschrijvingen niet aan de voorwaarde te voldoen: STUDENT CURSUS DATUM ========= ====== =========== 1 DB     18-mrt-2012 2 IM     26-jan-2012 4 DB     29-feb-2012

Het volgende voorbeeld illustreert dat niet elk probleem met ‘alle’ tot een geneste not exists leidt. Het laat bovendien zien dat een join binnen een subselect een goed alternatief kan zijn voor nesting van subselects.

Voorbeeld 9.20 Voor welke examinatoren (geef acroniem en naam) geldt dat ze examinator zijn van alle voorkenniscursussen van alle cursussen waarvan ze examinator zijn? De eerste stap is cruciaal: select acr, naam from   Docent where ‘deze’ docent is examinator and not exists ( verzameling cursussen waarvan ‘deze’ docent examinator is en waarbij een voorkenniscursus bestaat waarvan ‘deze’ docent geen examinator is )

We kunnen nu een geneste subselect vormen over Cursus, Voorkenniseis en wederom Cursus (in de rol van voorkenniscursus). De betreffende verzameling kan echter heel goed gevormd worden door een join. Dat is hier beslist eenvoudiger: select acr, naam from   Docent D where  -- ‘deze’ docent is examinator acr in (select examinator from  Cursus) and not exists (-- verzameling cursussen waarvan ‘deze’ docent examinator is en -- waarbij een voorkenniscursus bestaat waarvan ‘deze’ docent -- geen examinator is select * from   Cursus C join Voorkenniseis Ve on C.code = Ve.cursus join Cursus CV on Ve.voorkennis = CV.code where  C.examinator = D.acr and (CV.examinator is null or CV.examinator D.acr))

Resultaat: ACR    NAAM ====== ========= BAC    C.Bachman

Ga na dat dit klopt voor de voorbeeldpopulatie. Voor goed testen is een gevarieerdere populatie nodig.

Opgave 9.13 Welke studenten (geef nummer en naam) zijn voor alle cursussen ingeschreven en hebben deze met een voldoende of een vrijstelling afgesloten?

9.5 De closed world assumption In voorbeeld 9.18 werd gevraagd welke docenten alle cursussen begeleiden. Impliciet was hierbij de aanname dat het ging om ‘alle docenten binnen de tabel Docent van de relationele database OpenSchool’. De vraag is natuurlijk of die database een volledige en correcte afspiegeling is van de relevante wereld. In de relationele databasewereld wordt vrij algemeen aangenomen dat dit zo is: men gaat uit van de closed world assumption . De closed world assumption is niet vanzelfsprekend. Er zijn ook databaseparadigma’s die uitgaan van de open world assumption . Deze stellen dat een database slechts een beperkte feitenverzameling is en dat elders in ‘de wereld’ op elk moment nieuwe, al bestaande feiten van hetzelfde feittype kunnen opduiken. Volgens de open world assumption mag men alleen conclusies trekken op grond van de feiten die men kent, ermee rekening houdend dat men niet alles weet. Zo zal men nooit vragen naar iemands ‘laatste adres’, maar naar iemands ‘laatst bekende adres’. Het verschil tussen de closed world assumption en de open world assumption treedt onder meer op bij beweringen met ‘alle’. Vanuit de closed world assumption kan zo’n bewering zowel true , false als unknown zijn, terwijl hij vanuit de open world assumption doorgaans slechts false of unknown is. Anders gezegd: zo’n bewering kan wel weersproken worden maar niet bevestigd. Ook is er verschil bij ontkenningen. Volgens de closed world assumption mogen we op basis van de database-inhoud concluderen dat de cursus SW (Semantic web) niet wordt begeleid. Volgens de open world assumption is die conclusie niet toegestaan. Semantic web Een van de paradigma’s die uitgaan van de open world assumption is het semantic web . Het semantic web is geheel ingericht op het omgaan met globale (wereldwijd verspreide) feitenverzamelingen. In het semantic web worden niet zo gemakkelijk als in een relationele database conclusies getrokken in termen van ‘alle’ of ‘niet’. Zo moet je geen antwoord verwachten op de vraag welke docenten alle cursussen begeleiden. We weten immers nooit zeker of we geen cursus- of begeleidingsinformatie over het hoofd zien. Vanuit de open world assumption is men ook voorzichtig met ontkenningen. Zo moet je geen antwoord verwachten op de vraag welke docenten niet de cursus DB begeleiden. Men kijkt alleen naar de beschikbare feiten en laat daar alleen ‘positieve’ query’s op los, waarbij ook nieuwe feiten mogen worden afgeleid door redeneerregels.

9.6 De operatoren all en any SQL kent twee operatoren, all en any , die een subselect als operand hebben en die in SQL bepaalde Engels-achtige formuleringen mogelijk

maken. Het gaat daarbij om sommige problemen die in termen van ‘alle’ of ‘minstens één’ zijn geformuleerd. Echt nodig zijn ze niet, meestal is een goed alternatief voorhanden dat gebruikmaakt van een statistische functie. We beperken ons tot één voorbeeld voor elk van beide operatoren. Voorbeeld 9.21 Geef alle herkansingstentamens (student, cursus, volgnr, datum en cijfer) waarvoor het cijfer lager is dan bij alle voorgaande pogingen. Een oplossing met all ziet er aldus uit: select student, cursus, volgnr, datum, cijfer from   Tentamen T where  volgnr >= 2 and cijfer < all ( cijfers van voorgaande pogingen )

De logische expressie cijfer < all (…) is true wanneer cijfer kleiner is dan elke door de subselect opgehaalde waarde. Bevat de subselect een null, dan is de uitkomst unknown . In de andere gevallen is de waarde false . Uitwerking van de subselect geeft: select student, cursus, volgnr, datum, cijfer from   Tentamen T where  volgnr >= 2 and cijfer < all (-- cijfers van voorgaande pogingen select cijfer from   Tentamen Voorgaande where  Voorgaande.student = T.student and Voorgaande.cursus = T.cursus and Voorgaande.volgnr < T.volgnr)

Resultaat: ‘no records returned’. Verder testen met een illustratieve populatie is gewenst! Het kan natuurlijk ook met een statistische functie. Zie opgave 9.14 . Zoals all refereert aan ‘alle’, zo refereert any aan ‘minstens één’.

Voorbeeld 9.22 Geef alle herkansingstentamens (student, cursus, volgnr, datum en cijfer) waarvoor het cijfer lager is dan dat van minstens één voorgaande poging. Een oplossing met any ziet er aldus uit: select student, cursus, volgnr, datum, cijfer from   Tentamen T where  volgnr >= 2 and cijfer < any ( cijfers van voorgaande pogingen )

De logische expressie cijfer < any (…) is true wanneer cijfer kleiner is dan minstens één door de subselect opgehaalde waarde. Is dit niet het geval en bevat de subselect een null, dan is de uitkomst unknown . In de andere gevallen is de waarde false . Uitwerking van de subselect geeft: select student, cursus, volgnr, datum, cijfer from   Tentamen T where  volgnr >= 2 and cijfer < any (-- cijfers van voorgaande pogingen select cijfer from   Tentamen Voorgaande where  Voorgaande.student = T.student and Voorgaande.cursus = T.cursus and Voorgaande.volgnr < T.volgnr)

Resultaat: ‘No records returned’. Ook hier is verder testen met een illustratieve populatie gewenst. Ook bij any is er altijd wel een alternatief voorhanden met een statistische functie. Zie opgave 9.14 . Zowel bij all als any moet u attent zijn op mogelijke null-rijen in het subselect-resultaat. Deze kunnen van grote invloed zijn en soms voor een onverwacht resultaat zorgen. Neem zo nodig een extra conditie op die null-rijen uitfiltert.

Opgave 9.14 Geef bij de voorbeelden 9.21 en 9.22 alternatieve oplossingen met een statistische functie.

9.7 Views We hebben gezien dat we deelproblemen veelal kunnen oplossen met een subselect. Zo’n subselect kan zelfs optreden als brontabel, in de from clausule. Door zo’n subselect als ‘view’ te definiëren, kunnen we de code soms aanzienlijk vereenvoudigen.

9.7.1 Van subselect naar view We lossen het probleem van voorbeeld 9.4 op met een view. Voorbeeld 9.23 (zie ook voorbeeld 9.4 ) Welke cursus heeft (c.q. welke cursussen hebben) de meeste inschrijvingen? Geef van die cursus(sen) de code, de naam en het aantal inschrijvingen. We vonden hiervoor o.a. de volgende oplossing (zie uitwerking opgave 9.3 ): select   C.code, C.naam, count(*) from     Cursus C join Inschrijving I on C.code = I.cursus group by C.code, C.naam having   count(*) = (--maximum aantal inschrijvingen per cursus select   max(aantal_inschrijvingen) from     ( select   count(*) aantal_inschrijvingen from     Inschrijving group by cursus ))

De (grijs gemarkeerde) subselect is niet-gecorreleerd en kan daarom als view worden gedefinieerd. We maken er dan een wat mooiere tabel van, door de identificerende kolom cursus toe te voegen. Ook willen we de cursusnaam erbij, dan is de view ook te gebruiken voor de hoofdselect. De aangepaste subselect, als zelfstandige query, wordt: select   C.code, C.naam, count(*) from     Cursus C join Inschrijving I on C.code = I.cursus group by C.code, C.naam

Merk op dat we nu een join gebruiken, waarvan het navigatiepad start in Cursus. We gaan deze query bewaren als view, waarna we het probleem op een nieuwe en elegante manier oplossen met behulp van de view.

9.7.2 Het create view-statement De select-query als view definiëren gaat met een create view -statement. Voorbeeld 9.23 (vervolg)

Als volgt definiëren we een view voor de select-query van de vorige paragraaf: create view vCursus (code, naam, aantal_inschrijvingen) as select   C.code, C.naam, count(*) from     Cursus C join Inschrijving I on C.code = I.cursus group by C.code, C.naam

Toelichting – De viewnaam (vCursus) is een vrij te kiezen identifier. Voor de herkenbaarheid laten we een viewnaam beginnen met ‘v’. – De kolommenlijst wordt meegegeven na de viewnaam. De kolomnamen mogen afwijken van de kolomnamen in de select-clausule. Merk op dat de derde kolom van de select-expressie (count(*)) als viewkolomnaam aantal_inschrijvingen heeft. Er is ook een alternatieve syntaxis, waarbij de kolommenlijst wordt meegegeven in de select-query zelf (voor de derde kolom als alias): create view vCursus as select   C.code, C.naam, count(*) aantal_inschrijvingen from     Cursus C join Inschrijving I on C.code = I.cursus group by C.code, C.naam

De view is hierna beschikbaar als database-object, tot hij met een drop view -statement wordt verwijderd: drop view vCursus . De view kan worden gebruikt zoals een tabel. Zo hebben we: select * from   vCursus

met als resultaat: CODE   NAAM                  AANTAL_INSCHRIJVINGEN ====== ===================== ===================== DB     Databases                                 2 DW     Discrete wiskunde                         2 II     Inleiding informatica                     4 IM     Informatiemodelleren                      2

Veranderen we hierna de inhoud van Cursus, dan verandert de view mee. Het is immers niet de inhoud van Cursus die in de view wordt bewaard, maar de select -query.

Het probleem van paragraaf 9.6.1 kan met behulp van de view vCursus worden opgelost met de volgende eenvoudige query: select code, naam, aantal_inschrijvingen from   vCursus where  aantal_inschrijvingen = (select max(aantal_inschrijvingen) from   vCursus)

Resultaat: CODE   NAAM                  AANTAL_INSCHRIJVINGEN ====== ===================== ===================== II     Inleiding informatica                     4

In hoofdstuk 12 ‘Autorisatie’ behandelen we nog een andere toepassing van views, als middel om gebruikers databaserechten ‘op maat’ te geven.

Opgave 9.15 Wat is de Top-3 van cursussen met de meeste inschrijvingen? Los dit probleem op met behulp van de view vCursus. Aanwijzing : een cursus hoort tot de Top-3 als het aantal cursussen met meer inschrijvingen kleiner is dan 3. (Bij de gegeven voorbeeldpopulatie bevat de Top-3 vier cursussen!)

9.8 Kiezen uit alternatieven Inmiddels beschikken we over twee belangrijke instrumenten om een queryprobleem aan te pakken: joins en subselects. Soms is het één een alternatief voor het ander, soms ook niet. Soms ook moet een subselect met een join gecombineerd worden. De kracht van een join is dat je er een ‘grote virtuele tabel’ mee creëert, waar je gegevens uit meerdere databasetabellen uit kunt selecteren, al dan niet statistisch van aard. We zullen nu nog één laatste voorbeeld geven, ter illustratie van de verschillende aanpakken. We geven een probleem en lossen dat achtereenvolgens op via alleen een join, met alleen subselects en met een gemengde aanpak. Voorbeeld 9.24 Geef een overzicht van studenten, met studentnummer, naam en aantal behaalde credits. Joinaanpak We geven eerst een aanpak vanuit de joinfilosofie. Daarbij kijken we naar alle betrokken tabellen. Dit zijn: Student, Inschrijving en Cursus. Omdat we een overzicht van studenten willen, start het navigatiepad in Student. Het pad loopt vervolgens omlaag naar Inschrijving (in de richting éénveel!), waar de bijbehorende studentinschrijvingen worden gevonden.

Vanuit Inschrijving navigeren we weer omhoog om bij elke inschrijving (met voldoende cijfer) het aantal credits te vinden. Door deze op te tellen (statistische verdichting) krijgen we het gevraagde aantal credits. Hoewel het navigatiepad en soort ‘verhaal’ vertelt, parallel aan dat van de vraagstelling, kunnen we moeilijk anders dan de SQL -oplossing in één keer noteren: select    S.nr, S.naam, sum(C.credits) aantal_credits from      Student S join Inschrijving I on S.nr = I.student join Cursus C on I.cursus = C.code where     I.cijfer >= 6 -- voldoende group  by S.nr, S.naam

Het resultaat is maar één student, de enige die cursussen met een voldoende heeft afgerond. Natuurlijk willen we ook de andere studenten, met 0 credits. Hiertoe moet de eerste join een left outer join worden, zodat we bij het joinen geen studenten kwijtraken: select    S.nr, S.naam, sum(C.credits) aantal_credits from      Student S left outer join Inschrijving I on S.nr = I.student join Cursus C on I.cursus = C.code where     I.cijfer >= 6 -- voldoende group  by S.nr, S.naam

Dit verandert nog niets; we blijken de andere studenten alsnog kwijt te raken bij de conditie I.cijfer >= 6 . Geen enkele rij van de join voldoet voor die studenten immers aan die conditie. De oplossing voor dit soort situaties hebben we al eens gezien: we moeten de conditie opnemen in de joinconditie zelf. Dan wordt eerst geselecteerd op ‘voldoende’ en wordt pas daarna de ‘left outer’ actief zodat we geen studenten kwijtraken: select    S.nr, S.naam, sum(C.credits) aantal_credits from      Student S left outer join Inschrijving I on S.nr = I.student and I.cijfer >= 6 -- voldoende join Cursus C on I.cursus = C.code group  by S.nr, S.naam

Nog steeds vinden we maar die ene student. Maar dat klopt ook: de ‘andere’ studenten die bij de left outer join behouden blijven, kunnen nooit voldoen aan de tweede joinconditie. Dus ook die tweede join moet een left outer join worden: select    S.nr, S.naam, sum(C.credits) aantal_credits from      Student S left outer join Inschrijving I on S.nr = I.student and I.cijfer >= 6 -- voldoende left outer join Cursus C on I.cursus = C.code group  by S.nr, S.naam

Bijna goed nu: de ‘sum’ van studenten zonder credits moet niet null worden maar 0:

select    S.nr, S.naam, coalesce(sum(C.credits), 0) aantal_credits from      Student S left outer join Inschrijving I on S.nr = I.student and I.cijfer >= 6 -- voldoende left outer join Cursus C on I.cursus = C.code group  by S.nr, S.naam

Resultaat: NR NAAM      AANTAL_CREDITS ========= ====== ================= 1 Berk                 7.0 2 Tack                 0.0 3 Bos                  0.0 4 Eik                  0.0

Een voordeel van de joinaanpak is dat hij tot snelle query’s leidt. De join kan over het algemeen uitstekend worden geoptimaliseerd. Dit is echter alleen van belang bij zeer grote tabellen. Nadelen zijn dat zij lastig zijn te documenteren en al gauw leiden tot technische tekortkomingen, zoals we zagen bij de verschillende pogingen. Aanpak met stapsgewijze verfijning Heel anders verloopt een aanpak met stapsgewijze verfijning. Hierbij mikken we niet op 100% SQL , maar op 100% correcte oplossingen in elk stadium, waarbij niet-formele, exacte formuleringen in natuurlijke taal een belangrijke rol spelen. We beginnen met een hele eenvoudige eerste stap: select nr, naam, aantal_credits from   Student

Dit is half SQL , half Nederlands, en helemaal correct. Om het aantal credits van een student te tellen, moeten we vanuit ‘deze student’ omlaag navigeren naar Inschrijving, daar de corresponderende inschrijvingen selecteren waarvoor een voldoende cijfer is behaald en vervolgens omhoog navigeren naar Cursus om het aantal credits van de betreffende cursus op te halen. Dit leidt tot: select nr, naam, (select sum(credits) from  Cursus where er bestaat een inschrijving voor ‘deze’ cursus door ‘deze’ student, met een voldoende cijfer ) from   Student

De ‘er bestaat’-conditie leidt tot een subselect met exists : select nr, naam, (select sum(credits) from  Cursus C where  -- er bestaat een inschrijving voor ‘deze’ cursus -- door ‘deze’ student, met een voldoende cijfer exists (select * from   Inschrijving where  cursus = C.code and student = S.nr and cijfer >= 6))

from   Student S

De oplossing is correct. We maken hem af met de coalesce -functie voor lege sum -waarden en met een kolomalias voor de derde kolom: select nr, naam, (select coalesce(sum(credits), 0) from  Cursus C where  -- er bestaat een inschrijving voor ‘deze’ cursus -- door ‘deze’ student, met een voldoende cijfer exists (select * from   Inschrijving where  cursus = C.code and student = S.nr and cijfer >= 6)) aantal_credits from   Student S

Een voordeel van een aanpak met stapsgewijze verfijning is dat je eenvoudig kunt beginnen, terwijl je toch mikt op 100% correctheid in elk stadium. Een tweede voordeel is dat de eindoplossing goed documenteerbaar is met commentaren die de stappen aangeven. Een derde voordeel is dat je meestal bent verlost van problemen die te maken hebben met het onderweg kwijtraken van rijen en het al dan niet nemen van een (left) outer join. Een nadeel kan zijn dat de query langzamer is dan met een join. Maar als dit al zo is ( SQL -optimizers worden steeds slimmer) dan geldt dit alleen voor zeer grote databases. Maar de keuze is voor een deel ook een kwestie van smaak en persoonlijke voorkeur. Mengvormen Naast deze ‘uitersten’ is ook een mengvorm mogelijk van stapsgewijze verfijning en de joinaanpak. We kunnen beginnen met stap 1 en dan alsnog overstappen op een join binnen de subselect. Als volgt: select nr, naam, (select sum(C.credits) from   Cursus C join Inschrijving I on C.code = I.cursus where  I.student = S.nr and I.cijfer >= 6) from   Student S

Merk op dat ook hier stap 1 volledig in tact blijft. Ook deze oplossing kunnen we helemaal netjes maken, met een coalesce -functie en een kolomalias voor de subselect.

Oefenopgaven De volgende opgaven hebben betrekking op de Orderdatabase. Geef bij elke vraag een passend select -statement. Diagrammen: zie Boekverkenner. De voorbeeldpopulatie vindt u ook in bijlage 4.

Opgave 9.16 Welke zijn de oudste order(s)? Vermeld ordernummer en datum.

Opgave 9.17 Welke artikelgroep heeft (of welke artikelgroepen hebben) de meeste artikelen? Geef hiervan de omschrijving. Opgave 9.18 Geef de artikelen (vermeld nummer, omschrijving en artikelgroep) die binnen hun eigen artikel-groep de hoogste brutowinstmarge (verkoopprijs minus inkoopprijs) hebben. Opgave 9.19 Geef een artikeloverzicht met van elk artikel het artikelnummer, de naam en de omzet van dat artikel. De omzet is het totaal van orderregelbedragen bij dat artikel. Bij artikelen zonder orderregels moet 0 vermeld worden. Opgave 9.20 Welke klanten (geef klantnummer en naam) hebben geen andere klant of klanten aangebracht? Opgave 9.21 Welke klanten (geef klantnummer en naam) hebben nog nooit wat besteld? Geef minstens twee oplossingen. Opgave 9.22 Over hoeveel artikelen is ooit geklaagd? Geef ten minste drie oplossingen. Opgave 9.23 Geef per artikelgroep (vermeld de omschrijving) het aantal artikelen waarover een of meer klachten zijn geregistreerd. Opgave 9.24 Welke klanten (geef hun klantnummer en naam) die ten minste één order hebben geplaatst, hebben over al hun orders een klacht ingediend? Opgave 9.25 Welke klanten (geef hun klantnummer en naam) hebben een slevel of een wigbek gekocht? Opgave 9.26 Welke klanten (geef hun klantnummer en naam) hebben een slevel én een wigbek gekocht? Opgave 9.27 Geef de Top-5 van artikelen met de hoogste omzet. Maak gebruik van een view. Zie ook opgave 9.19 .

10 Wijzigen van een database-inhoud Het oplossen van opvraagproblemen via select kan de vorm aannemen van een ware sport. Die sport hebben we in de voorgaande hoofdstukken soms op hoog niveau beoefend. Daarmee vergeleken is databaseonderhoud (het zorgen voor de juiste tabelinhouden) vaak maar een saaie aangelegenheid. Toch is dat onderhoud vanzelfsprekend van het grootste belang. U kunt een fantastische query verzinnen, maar wat heeft een echte gebruiker eraan als de gegevens niet kloppen? In dit hoofdstuk gaat het om dat onderhoud: het toevoegen of verwijderen van rijen ( insert , delete ), of het wijzigen van celinhouden van rijen ( update ). Bij de updates komen de liefhebbers van moeilijke query’s gelukkig nog ruimschoots aan hun trekken. Voorbeelddatabase De voorbeelddatabase bij dit hoofdstuk is opnieuw, en nu voor het laatst, OpenSchool. In figuur 10.1 herhalen we het strokendiagram. Raadpleeg de Boekverkenner of bijlage 4 voor de voorbeeldpopulatie.

10.1 Levenswijzen van een database Het leven van een database wordt voornamelijk bepaald door het toevoegen van rijen aan zijn tabellen ( insert ), het verwijderen van rijen ( delete ) of het wijzigen van kolomwaarden ( update ). Soms wordt het leven heel even wat turbulenter door wijzigingen op structuurniveau: toevoegen van extra tabellen ( create table ), verwijderen van tabellen ( drop table ) of structuurverandering van bestaande tabellen ( alter table ). Ook andere databaseobjecten kunnen worden gecreëerd, verwijderd of veranderd, denk aan views.

10.1.1 Typen database Vanuit het perspectief van de ‘levenswijze’ worden verschillende typen databases onderscheiden, horend bij verschillende soorten systemen of verwerking: – OLTP -systemen ( Online Transactional Processing ); de database wordt online gestuurd via programmatuur en gevuld vanuit automaten of bijvoorbeeld internet – transactionele bedrijfssystemen ; hierin worden de dagelijkse veranderingen van de bedrijfswerkelijkheid bijgehouden (nieuwe klanten of bestellingen, adreswijzigingen, enzovoort) – datawarehouses ; hierin worden historiegegevens verzameld uit transactionele systemen (bijvoorbeeld verkoopgegevens) en getransformeerd tot waardevolle informatie. OLTP -systemen en transactionele bedrijfssystemen zijn beide

transactionele systemen . We kunnen dus ook uitgaan van een tweedeling: transactionele systemen tegenover datawarehouses.

Figuur 10.1 Strokendiagram OpenSchool

10.1.2 Online transaction processing Bij online transaction processing ( OLTP) moet u denken aan enorme hoeveelheden gegevens die razendsnel moeten worden verwerkt. Een goed voorbeeld is de verwerking van pincodetransacties. Maar ook ecommercetransacties via het internet horen hiertoe. Het gaat om korte transacties, die volledig door programmatuur worden gestroomlijnd. Elk transactie wordt beschouwd als een bijdrage aan de geschiedenis, waaraan niets meer kan worden veranderd. Er vinden daarom voornamelijk of alleen inserts plaats.

10.1.3 Transactionele bedrijfssystemen Bij transactionele bedrijfssystemen worden door vele gebruikers, elk vanuit de eigen taak, uiteenlopende transacties uitgevoerd: ‘bedrijfsbreed’. Denk aan registratie van gebeurtenissen in een ziekenhuis. De registratie vindt plaats door een menselijke processor, meestal via een grafische eindgebruikersapplicatie. In principe kunnen de transacties zowel inserts, deletes als updates omvatten.

10.1.4 Datawarehouses Datawarehouses zijn grote, historische databases, die regelmatig en batchgewijs (dat wil zeggen: zonder tussenkomst van gebruikers) worden bijgewerkt vanuit andere (meestal transactionele) systemen. De nadruk ligt op het verkrijgen van queryresultaten met een hoog statistisch of analytisch gehalte, ter ondersteuning van beleidsbeslissingen: managementinformatie, analyse van het koopgedrag van klanten, verbetering van het assortiment, identificatie van doelgroepen, het voorspellen van trends, enzovoort. Historie verandert niet, er komt alleen maar bij. Er vinden op een datawarehouse daarom in principe alleen inserts plaats. Voor het raadplegen van datawarehouses bestaan verschillende typen programma’s. Een van die typen heet OLAP , Online Analytical Processing . OLAP -programmatuur levert rapportages waarbij berekende, statistische gegevens worden herleid tot diverse onderliggende detailniveaus. Met een druk op de knop kan men heen en weer tussen grove en meer verfijnde statistieken (‘ drill down ’ en ‘ drill up ’). Historie: deel van de werkelijkheid? Wat betekent registratie van de werkelijkheid, wanneer het gaat om veranderingen? Betekent het dat iemands adres wordt gewijzigd zodra een verhuisbericht is binnengekomen? Bij een postorderbedrijf misschien wel. Maar zeker niet bij het bevolkingsregister; daar wordt een nieuwe regel toegevoegd aan iemands ‘adreshistorie’. Geen update dus, maar een insert. Wanneer de geschiedenis wordt bewaard, komen deletes niet voor en updates alleen om echte fouten te herstellen. De vraag hoe met ‘geschiedenis’ in een informatiesysteem moet worden omgegaan, heet het historieprobleem en is een klassiek discussieonderwerp.

10.2 Transacties In paragraaf 3.8.4 werd een transactie gedefinieerd als een reeks SQL statements die ofwel in hun geheel ofwel in het geheel niet worden uitgevoerd. We zullen wat daarover is gezegd, nu kort herhalen en vervolgens ingaan op verschillende transactiemodellen . Tot slot gaan we in op de transacties van verschillende gebruikers die tegelijkertijd op dezelfde database zijn ingelogd.

10.2.1 Commitmomenten Een commitmoment vindt expliciet plaats door het statement commit . Afhankelijk van het rdbms of van de SQL -omgeving waarmee het rdbms wordt benaderd, kan ook een DDL -statement (zoals create table ) impliciet een commitmoment inhouden. Een commitmoment maakt alle voorafgaande wijzigingen (sinds het vorige commitmoment) definitief. Zolang een transactie nog loopt, kunnen alle wijzigingen die met inserts, deletes of updates zijn aangebracht, nog ongedaan worden gemaakt met het statement rollback . Een transactie kan één commando omvatten, of een langere reeks commando’s die logischerwijs een geheel vormen. Bijvoorbeeld: het invoeren van een order met minimaal een orderregel. Databaseregels hoeven tijdens een transactie niet altijd te gelden. Als ze maar weer gelden na afloop, na het committen van alle wijzigingen. Het kan daarom nodig zijn gedurende een transactie bepaalde regels tijdelijk uit te schakelen. In hoofdstuk 15 ‘Transacties’ zullen we daar voorbeelden van zien. Commitmomenten in Firebird en IQU In Firebird is, conform de SQL-standaard, een DDL-statement geen commitmoment. Een transactie kan dus structuurwijzigingen bevatten. In IQU echter, de SQL-omgeving die met de Boekverkenner samenwerkt, is aan elk DDL-statement een commit verbonden.

10.2.2 Transactiemodellen De mogelijkheden voor het starten en beëindigen van een transactie en de manier waarop dat gebeurt, zijn per rdbms verschillend. We beperken ons tot Firebird, of eigenlijk: tot de combinatie Firebird met de interactive query utility, want via deze laatste zijn de mogelijkheden van Firebird op dit gebied wat uitgebreid.

Het impliciete transactiemodel Tot nu toe werd door Firebird na elk commitmoment automatisch een nieuwe transactie gestart. Dat was in onze oefensituatie wel gemakkelijk: we konden nog eens wat proberen, met rollback was alles weer snel bij het oude. In feite was steeds één van twee mogelijke transactiemodellen actief: het impliciete transactiemodel . Zie figuur 10.2 voor het effect bij dat model van een rollback: terugkeer naar de toestand direct na het laatste commitmoment.

Figuur 10.2 Effect van een rollback bij het impliciete transactiemodel

In hoofdstuk 15 ‘Transacties’ wordt een tweede transactiemodel behandeld: het expliciete transactiemodel .

10.3 Integriteitsregels Aan het toevoegen, verwijderen of wijzigen van rijen zitten nogal wat haken en ogen, die te maken hebben met de diverse soorten constraints. Ook transacties spelen daarbij een rol. We kijken voor elk type constraint welke problemen kunnen optreden en hoe we deze het hoofd kunnen bieden.

10.3.1 De referentiële integriteitsregel De belangrijkste constraint waarmee we te maken hebben, is de referentiële integriteitsregel : de regel die voorschrijft dat een verwijssleutel daadwerkelijk naar een rij in de doeltabel verwijst, via de primaire sleutel daarvan. Een insert van een nieuwe orderregel zal daarom niet lukken als het ordernummer daarin niet verwijst naar een bestaande order in de tabel Order_. Om dezelfde reden kunnen we niet zomaar een delete van een order uitvoeren wanneer de database nog orderregels van die order bevat. En ook bij update ’s moeten we

oppassen, wanneer daar een sleutelwaarde (primaire of verwijssleutel) bij is betrokken. Dan zijn er nog de niet-sleutelverwijzingen , waarvoor vergelijkbare regels gelden als voor sleutelverwijzingen. Als we even aannemen dat de verwijzing van Orderregel naar Kortingsinterval daadwerkelijk wordt afgedwongen, zal een insert van Orderregel mislukken wanneer Orderregel.aantal niet tot een bestaand interval van Kortingsinterval hoort.

10.3.2 Deleteregels De structuur van de OpenSchool-database is zo opgezet dat een deletepoging op een rij van Cursus automatisch een deletepoging van de bijbehorende Begeleider- en Voorkennis-rijen tot gevolg heeft. De gedachte hierachter is kennelijk als volgt: het willen verwijderen van een cursus impliceert het willen verwijderen van alle gerelateerde begeleideren voorkennisinformatie. In het algemeen kan een ‘deletewaterval’ nog meer generaties (ouderkind-kleinkind, …) omvatten. Mocht om wat voor reden dan ook ergens in de cascade een deletepoging mislukken, dan wordt alles teruggedraaid: een cascading delete is een echte transactie. Technisch is het domino-effect van zo’n verwijdering gedefinieerd in het create -script van de database, waarin voor de betreffende verwijssleutels een cascading delete is gespecificeerd (zie hoofdstuk 11 ). Voor de overige verwijzingen geldt een restricted delete. Deze geldt wanneer geen van de beide andere deleteregels (cascading delete en nullifying delete) is gespecificeerd. De gebruiker kan dus bijvoorbeeld niet zomaar een student verwijderen, zolang er nog een inschrijving is van die student. En een inschrijving kan niet worden verwijderd zolang er nog een tentamen is bij die inschrijving. Eerst zal de gebruiker dus ‘van onderaf’ tentamens en inschrijvingen moeten verwijderen, voordat de betreffende student kan worden verwijderd.

10.4 Het insert-statement Insert betekent letterlijk ‘invoegen’. Hierna hebben we het over ‘toevoegen’. Beide termen suggereren dat de nieuwe rij een bepaalde plaats in de tabel krijgt toebedeeld. We weten echter dat een tabel geen rijvolgorde kent, zodat ook een nieuwe rij niet op een speciale plaats terecht komt. Elke tabel is een verzameling rijen en een verzameling kent

geen volgorde. Wanneer we dit willen benadrukken – we herhalen het nog eens – spreken we over een tabel als relatie . Er zijn twee typen insert -statement: bij het basistype wordt één rij aan een tabel toegevoegd. Bij het tweede type kunnen, via een ingebouwd select -statement, meerdere rijen tegelijk worden toegevoegd.

10.4.1 Enkelvoudige inserts De meest eenvoudige insert -statements zijn die waarbij één nieuwe rij wordt toegevoegd. Minstens alle verplichte kolomwaarden moeten daarbij worden opgegeven. In de volgende voorbeelden zijn aandachtspunten: de kolomlijst, defaultwaarden, nulls en date -waarden. Geef tussentijds rollback - statements wanneer u de verschillende alternatieven voor één opdracht daadwerkelijk wilt uitvoeren. Voorbeeld 10.1 Insert met of zonder kolomlijst, met of zonder defaultwaarde Voeg een nieuwe student toe, met studentnummer 5, naam Stam en onbekende mentor. De eerste manier om student 5 in te voeren is met een insert -statement waarvan de values -lijst alle kolomwaarden bevat. De lijstvolgorde moet corresponderen met de kolomvolgorde van het create table -statement: insert into Student values (5, 'Stam', null)

We zien dat er op technisch niveau een kolomvolgorde bestaat. Een alternatief is de kolommenlijst expliciet op te nemen: insert into Student (nr, naam, mentor) values (5, 'Stam', null)

In dit geval mag de optionele kolom worden weggelaten: insert into Student (nr, naam) values (5, 'Stam')

De volgorde is vrij, mits de volgorde van de kolommenlijst klopt met die van de values -lijst. Kolommen met een defaultwaarde mogen worden weggelaten, tenzij we willen afwijken van de default. Voorbeeld 10.2 Behandeling van nulls en date-waarden Schrijf student 5 in voor de cursus II.

Omdat de inschrijvingsdatum default de huidige datum is (zie createscript), kunnen we als volgt student 5 inschrijven voor cursus II: insert into Inschrijving (student, cursus) values (5, 'II')

De inschrijvingsdatum wordt nu de huidige datum en de cijfer- en vrijstelling-kolommen worden null. Willen we ‘huidige datum’ expliciet noemen dan kan dat met de parameterloze functie current_date : insert into Inschrijving (student, cursus, datum) values (5, 'II', current_date)

We kunnen ook expliciet een datum vermelden: insert into Inschrijving (student, cursus, datum) values (5, 'II', ’10-mar-2012’)

De datum wordt hier in characterformaat opgegeven. Omdat dit een van de standaardformaten is, wordt de string door Firebird automatisch gecast naar het interne date -formaat.

Opgave 10.1 a Bestudeer het insert-script van de OpenSchool-database. U vindt dit via de voorbeeldnavigator van de Boekverkenner (knop Vb). b Een nieuwe cursus Business Intelligence ( BI) wordt in het cursusprogramma opgenomen. Aantal uren: 120; credits: 4; vereiste voorkennis: Databases ( DB ) en informatiemodelleren ( IM ). Welke inserts moeten worden uitgevoerd en in welke volgorde? Geef na afloop een rollback .

10.4.2 Meervoudige inserts Met één insert-statement kunnen meer rijen tegelijk worden toegevoegd. We moeten er dan een select -expressie in opnemen. We zullen dit toelichten aan de hand van een zeer eenvoudige data-warehouse van de Open School.

Voorbeeld 10.3 Het management van de Open School wil statistische informatie over cursusinschrijvingen. Hiertoe is een datawarehouse aangemaakt met twee tabellen: DWC ursus en DWC ursusresultaat (zie figuur 10.3 ).

Figuur 10.3 Datawarehouse cursusresultaten

Toelichting – Tabel DWC ursus moet alle cursussen bevatten. Kolom begeleid JN moet de waarde ‘J’ krijgen als de cursus begeleid is en anders ‘N’. Een cursus heet begeleid als tabel Begeleider voor die cursus één of meer begeleiders bevat. – Tabel DWC ursusresultaat bevat informatie over inschrijvingen, gegroepeerd per cursus en per maand (in twee kolommen jaar en maand). Per cursus-maandcombinatie worden vermeld: het totaal aantal inschrijvingen en het aantal inschrijvingen waarbij de cursus is afgerond met een voldoende (cijfer >= 6).

– Tabel DWC ursusresultaat bevat een partiële sleutelafhankelijkheid (welke?) en voldoet dus niet aan de tweede normaalvorm. Omdat een datawarehouse gevoed wordt vanuit één of meer bron-systemen met consistente data en er geen bewerkingen op plaatsvinden, is er geen gevaar voor inconsistentie en is dit geen bezwaar. De datawarehouse wordt gevuld en periodiek bijgewerkt vanuit de OpenSchool-database. We gaan er van uit dat de tabellen DWC ursus en DWC ursusresultaat al bestaan en deel uitmaken van de OpenSchooldatabase zelf (wat inderdaad het geval is). We beperken ons tot het eenmalig vullen van beide tabellen.

We merken op dat hier een wel erg simpele voorstelling wordt gegeven van een datawarehouse. Raadpleeg voor meer informatie de facultatieve hoofdstuk ‘datawarehouses’ op Studienet. Vullen van DWCursus Met de volgende twee insert -statements kunnen alle cursussen worden ingevoegd in DWC ursus. Het eerste statement voegt alle begeleide cursussen in, het tweede alle onbegeleide: insert into DWCursus select code, naam, 'J' from  Cursus C where  -- deze cursus is begeleid exists (select * from   Begeleider where  cursus = C.code); insert into DWCursus select code, naam, 'N' from  Cursus C where  -- deze cursus is niet begeleid not exists (select * from   Begeleider where  cursus = C.code)

Toelichting – Beide insert -statements bevatten een select -expressie waarvan de ‘signatuur’ (aantal kolommen en hun datatypen) overeenkomt met die van DBC ursus. – Alle rijen die door de select -expressie worden opgehaald worden ingevoegd in DWC ursus. Met behulp van de ‘als-dan’-functie iif (zie paragraaf 6.5.4 ) kan het nog een stuk eleganter. De hele klus kunnen we klaren met één insertstatement: insert into DWCursus select code, naam, iif(exists (select * from  Begeleider where  cursus = C.code), 'J', 'N') from  Cursus C

Toelichting : het eerste argument van de iif -aanroep is een subselect met exists . Al naar gelang die true is of false , wordt ‘J’ of ‘N’ geretourneerd. Vullen van DWCursusresultaat

We kunnen nu tabel DWC ursusresultaat vullen. Ook hier zullen we proberen één insert met select te gebruiken. De algemene vorm daarvan luidt als volgt: insert into DWCursusresultaat (cursuscode, cursusnaam, jaar, maand, aantal_inschrijvingen, aantal_afgerond) select   …

Vanwege de statistische waarden, hebben we een select -statement nodig over tabel Inschrijving, gegroepeerd op jaar en maand. Om het aantal afgeronde inschrijvingen te tellen maken we gebruik van de iif functie: insert into DWCursusresultaat (cursuscode, cursusnaam, jaar, maand, aantal_inschrijvingen, aantal_afgerond) select C.code, C.naam, extract(year from I.datum) jaar, extract(month from I.datum) maand, count(*) aantal_inschrijvingen, count(iif(I.cijfer >= 6, 'voldoende', null)) aantal_voldoendes from   Cursus C join Inschrijving I on C.code = I.cursus group by C.code, C.naam, jaar, maand

Toelichting – De select -expressie is een join van Cursus en Inschrijving die verfijnd wordt gegroepeerd op cursus (code en naam), jaar en maand. – Voor jaar en maand worden gebruikgemaakt van de functie extract , die jaar respectievelijk maand ‘uit de datum haalt’ (zie bijlage 1. – Beide extract-expressies hebben we aliasnamen gegeven; deze worden ook gebruikt in de group by -clausule. – count(iif(cijfer >= 6, ‘voldoende’, null)) telt het aantal voldoendes in elke cursus/maandgroep; de string ‘voldoende’ is een in wezen willekeurige niet-nullwaarde.

Opgave 10.2 De tabellen DWC ursus en DWC ursusresultaat bestaan al, als extra tabellen binnen de OpenSchool-database. Vul beide tabellen, gebruikmakend van de meervoudige insert -statements van deze paragraaf, en controleer het resultaat.

10.5 Het delete-statement Om een of meer rijen uit een tabel te verwijderen, is er het delete statement. Een delete is in zekere zin het omgekeerde van een insert . Zolang u maar geen commit of DDL -statement geeft, kunt u de statements van deze paragraaf rustig uitproberen. Anders kan het effect nogal drastisch zijn. Elk statement gaat uit van de gegeven voorbeeldpopulatie; geef daarom na elke delete een rollback . Doet u dit niet, dan hebt u kans rijen te willen verwijderen die al verwijderd zijn.

10.5.1 Eenvoudige deletes Het delete -statement in zijn meest simpele vorm heeft het meest vergaande effect, zoals blijkt uit het volgende voorbeeld. Voorbeeld 10.4 Delete zonder restrictie erwijder alle rijen van Begeleiding. Oplossing: delete from Begeleider

De tabel Begeleider zelf bestaat hierna nog, hij is alleen leeg geworden. Omdat een delete op rijniveau werkt, heeft het geen zin kolommen te vermelden, zelfs niet een * voor ‘alle kolommen’: delete * from Begeleider -- fout!

Om de reikwijdte van het delete -statement in te perken, voegen we een where -clausule met selectieconditie toe: Voorbeeld 10.5 Delete met selectieconditie Verwijder alle begeleidingsinformatie voor cursus IM. Oplossing:

delete from  Begeleider where cursus = ‘IM’

Net als een select wordt een delete rij voor rij afgewerkt. De rij die ‘aan de beurt’ is, noemen we weer de actuele rij . Wanneer de actuele rij voldoet aan de selectieconditie, zal hij worden verwijderd, tenzij er een regel is die dit tegenhoudt. Voorbeeld 10.6 Delete met cascading-delete-effect Verwijder cursus SW (Semantic web). Oplossing: delete from  Cursus where code = ‘SW’

Gevolg: niet alleen cursus SW wordt verwijderd, maar ook de beide rijen van Voorkenniseis van waaruit naar cursus SW wordt verwezen. Dit als gevolg van de cascading delete die geldt voor de verwijzing vanuit Voorkenniseis naar Cursus én omdat er geen inschrijvingen zijn voor cursus SW . Zouden we een analoog statement geven voor de cursus IM , dan zou dit mislukken: voor cursus IM bestaan inschrijvingen en zelfs tentamens en de verwijzingen vanuit Inschrijving en tentamen hebben een restricted delete.

10.5.2 Deletes met subselect De selectieconditie van een delete -statement mag een subselect bevatten. Voorbeeld 10.7 Delete met subselect Verwijder de cursussen zonder inschrijvingen. Eerste oplossing, met niet-gecorreleerde subselect: delete from Cursus where code not in (select cursus from  Inschrijving)

Eventuele bijbehorende rijen in Begeleider en in Voorkenniseis worden meeverwijderd, vanwege de cascading deletes. Tweede oplossing, met gecorreleerde subselect:

delete from Cursus C where not exists (select * from   Inschrijving where  cursus = C.code)

Opgave 10.3 Door welke delete (of delete ’s, in de juiste volgorde) kunnen de insert ’s van opgave 10.1 ongedaan worden gemaakt? Opgave 10.4 Verwijder alle tentamens waarvoor geldt dat er een vervolgtentamen is geweest met een hoger cijfer. Opgave 10.5 Verwijder alle docenten die niet optreden als mentor, noch als examinator, begeleider of vervanger.

10.6 Het update-statement Een update werkt op kolomniveau van een of meer bestaande rijen. Nieuwe waarden kunnen expliciet worden opgegeven of uit andere databasewaarden worden berekend via een ingebouwde select expressie. Zo kunnen update -statements worden geformuleerd van uiteenlopende complexiteit. Wanneer u de opdrachten daadwerkelijk uitvoert, geef dan ook nu tussentijds rollback-statements.

10.6.1 Eenvoudige updates Een update-probleem omvat altijd drie elementen: – Om welke tabel gaat het? – Welke rij(en) moeten een update ondergaan? – Wat is (zijn) de nieuwe kolomwaarde(n)?

Voorbeeld 10.8 Wijzig het aantal uren van de cursus DW in130. Oplossing: update Cursus          -- 1 set    uren = 130      -- 3 where  code = 'DW'     -- 2

De cijfers geven de conceptuele volgorde aan, dat is de volgorde die de SQL -programmeur als uitgangspunt mag nemen bij het opstellen van het statement: 1 de tabel waarin de update moet plaatsvinden 2 de rij waarin de update moet plaatsvinden 3 de update zelf: de kolom, met de nieuwe waarde. Net als bij select ’s (zie paragraaf 8.5.3) zou bij ingewikkelde update ’s het interne verwerkingsproces hiervan kunnen afwijken. De clausule set uren = 130 heet een toekenning . De waarde 130 wordt toegekend aan de cel in de betreffende rij en kolom. De betekenis van het teken = is dus niet: is gelijk aan , maar (samen met set ): maak gelijk aan , zie figuur 10.4 .

Figuur 10.4 Waardetoekenning en gelijkheid

Voorbeeld 10.9 Update van elke rij

Eén credit ( ECTS -punt) is officieel 28 studie-uren. Pas alle credits aan aan het aantal uren. Oplossing: update Cursus set    credits = cast(uren as numeric(5,2)) / 28

Toelichting – De update wordt, door het ontbreken van een where -clausule, uitgevoerd op alle rijen van de tabel. – De nieuwe waarden van credits worden afgerond op één decimaal, conform het datatype van credits (dat is: numeric(3,1) ). De cast functie converteert het datatype van uren van integer naar numeric(5,2) . Zonder conversie hebben we uren / 28 , wat een integer -uitkomst oplevert. Resultaat: CODE   NAAM                       UREN CREDITS ====== ===================== ========= ======= II     Inleiding informatica        80     2.9 DW     Discrete wiskunde           120     4.3 DB     Databases                   120     4.3 IM     Informatiemodelleren        150     5.4 SW     Semantic web                120     4.3

EXAMINATOR ========== BAC DAT COD DAT

Voorbeeld 10.10 Oude waarde gebruiken in toekenning Verhoog het aantal uren van elke cursus met 10%. Oplossing: update Cursus set    uren = uren * 1.1

In de set -clausule komt uren nu twee keer voor: rechts als oude waarde, links als nieuwe waarde: nieuwe ‘uren’ wordt oude ‘uren’ maal 1.1. Voorbeeld 10.11 Waarden van meer dan één kolom wijzigen Wijzig van cursus DW het aantal uren in 140 en het aantal credits in 5. Oplossing: update Cursus

set    uren = 140, credits = 5 where  code = 'DW'

De set-clausule bevat nu twee toekenningen.

10.6.2 updates met subselect Het rechterlid van een toekenning mag ook een subselect bevatten. Ook de where -clausule van een update -statement mag een subselect bevatten. Net als voor subselects binnen een select -statement zijn er twee mogelijkheden: – niet-gecorreleerd , dat wil zeggen: onafhankelijk van de rijen van de te updaten tabel – gecorreleerd , dat wil zeggen: afhankelijk van de rijen van de te updaten tabel. Eerst volgt een voorbeeld met een niet-gecorreleerde subselect. Ter herinnering: geef na elk voorbeeld een rollback , indien u de statements daadwerkelijk uitvoert. Voorbeeld 10.12 Niet-gecorreleerde subselect in toekenning Maak elk aantal uren gelijk aan het huidige, gemiddelde aantal uren. De volgende oplossing (poging) bevat een niet-gecorreleerde subselect. Alle waarden in kolom uren zouden gelijk moeten worden aan de waarde van de subselect; dat is het gemiddelde aantal uren vóór aanvang van het statement: update Cursus set    uren = (select avg(uren) from   Cursus)

Inspecteren we hierna tabel Cursus, dan vinden we echter: CODE   NAAM                       UREN ====== ===================== ========= II     Inleiding informatica       118 DW     Discrete wiskunde           125 DB     Databases                   126 IM     Informatiemodelleren        127 SW     Semantic web                123

Verklaring : de tabel Cursus wordt rij voor rij verwerkt. Voor cursus II wordt nog een correct gemiddelde berekend. Voor cursus DW wordt opnieuw de subselect berekend, waarbij niet de oude, maar de inmiddels gewijzigde urenwaarde van cursus II wordt gebruikt. Daardoor wordt voor DW niet 118, maar 125 als gemiddelde gevonden. Enzovoort. De update doet duidelijk niet wat we zouden willen. Dit gedrag is dialectafhankelijk. Wat we hier beschreven, geldt voor Firebird; bij Oracle wordt de subselect eenmalig berekend en krijgen we wat we hebben willen. Voor een Firebird-oplossing moeten we wachten tot hoofdstuk 15 ‘Triggers’. We kennen daar het resultaat van de subselect toe aan een variabele.

Niet-relationele verwerking van een update Het probleem bij voorbeeld 10.12 zou zich niet voordoen als de update een echte relationele operatie was, werkend op een tabel als een verzameling van zijn rijen. Omdat een verzameling geen rijvolgorde kent, zou het resultaat nooit afhankelijk mogen zijn van de volgorde van verwerking. Bij puur relationele verwerking zou het zo moeten zijn dat, conceptueel gezien, alle rijen tegelijk de update ondergaan.

De vraagstelling in het volgende voorbeeld leidt tot een gecorreleerde subselect in het rechterlid van de toekenning. Voorbeeld 10.13 Update met gecorreleerde subselect: vullen van berekenbare kolom Voor dit voorbeeld brengen we eerst een kleine wijziging aan in de databasestructuur: we breiden tabel Cursus uit met een optionele, afleidbare kolom aantal_inschrijvingen, voor het aantal inschrijvingen per cursus: alter table Cursus add aantal_inschrijvingen integer

Zie figuur 10.5 .

Figuur 10.5 Update van afleidbare kolom: gecontroleerde redundantie

Het idee is dat de nieuwe kolom eenmalig wordt gevuld via een update , en voor nieuwe inschrijvingen automatisch wordt bijgewerkt via triggers (te behandelen in hoofdstuk 15 ). In het vervolg zal dan het aantal inschrijvingen per cursus direct beschikbaar zijn voor select -query’s en niet steeds opnieuw berekend hoeven te worden. Een vorm van gecontroleerde redundantie dus, om de performance te verbeteren. De eenmalige update van Cursus.aantal_inschrijvingen: update Cursus C set    aantal_inschrijvingen = (-- aantal inschrijvingen voor ‘deze’ cursus select count(*) from   Inschrijving where  cursus = C.code)

Rij voor rij wordt tabel Cursus afgewerkt en voor elke rij wordt de waarde in kolom aantal_inschrijvingen gevuld met het aantal corresponderende rijen van tabel Inschrijving.in zowel In opgave 10.6 ziet u een voorbeeld van een update met subselects in zowel de set - als de where -clausule.

Opgave 10.6 Kolom Inschrijving.cijfer is afleidbaar: de waarde is het hoogste corresponderende tentamencijfer. Geef een update-statement waarbij de kolom Inschrijving.cijfer wordt (her)berekend.

10.6.3 Update van primaire sleutel Een primaire-sleutelkolom kan als alle andere een update ondergaan. Toch is hier wel wat meer over op te merken. Update van enkelvoudige primaire sleutel We nemen een update in gedachten van een enkelvoudige primaire sleutel, bijvoorbeeld Student.nr. Een update van alleen die primaire sleutel zou de verwijzingen vanuit Inschrijving (en ook vanuit Tentamen) verbreken. Dat kan nooit de bedoeling zijn, dus zullen corresponderende verwijssleutels dezelfde wijziging moeten ondergaan. En liefst automatisch: een cascading update.

Voorbeeld 10.14 Update met cascading effect Verhoog alle studentnummers met 1000. Met de volgende update: update Student set    nr = nr + 1000

worden niet alleen de studentnummers van Student met 1000 verhoogd, maar ook die van Inschrijving en Tentamen, zodat de ouder-kindcorrespondenties ongewijzigd blijven. Dit geldt dus alleen als voor de verwijssleutel een cascading update is gespecificeerd, zie figuur 10.6 .

Figuur 10.6 Cascading update van primaire sleutel

Voorbeeld 10.15 illustreert dat updates zoals deze niet altijd probleemloos zijn. Voorbeeld 10.15 Volgordeprobleem Verhoog alle studentnummers met 1.

Dit lijkt sprekend op voorbeeld 10.14 , maar de analoge oplossing geeft, voor de gegeven voorbeeldpopulatie, een merkwaardig probleem: update Student set    nr = nr + 1    -- gaat mis!

Als we aannemen dat de update wordt uitgevoerd in de rijvolgorde zoals in figuur 10.6 , dan is eerst de rij met studentnummer 1 aan de beurt. Het nieuwe studentnummer wordt: 2. Maar dat bestaat al. We krijgen een foutmelding die betrekking heeft op de uniciteit van de primaire sleutel. Ook hier doet zich het probleem voor dat de update niet als echte verzamelingenoperatie wordt uitgevoerd, maar als een rij voor rij operatie (zie de grijstekst in paragraaf 10.16.2). In bijvoorbeeld Oracle wordt zo’n update wel goed uitgevoerd. In opgave 10.7 vragen we u te bedenken hoe we het toch voor elkaar kunnen krijgen. Bij een goed databaseontwerp zal het updaten van een primaire sleutel zelden nodig zijn. Maar het kan voorkomen, bijvoorbeeld bij het samenvoegen van verschillende databases.

Opgave 10.7 Verhoog alle studentnummers met 1. Update van samengestelde primaire sleutel Bij de update in voorbeeld 10.15 veranderde er eigenlijk niets aan de betreffende databaseobjecten (de rijen van Student). Hun identificatie wijzigde, maar hun eigenschappen bleven gelijk. Anders is dat bij voorbeel d 10.16 , waarin we via een update van een inschrijving een student omboeken van de ene cursus naar een andere. We waarschuwen maar vooraf: dit is een dubieuze update, die we eigenlijk zouden willen verbieden. We geven eerst het voorbeeld en motiveren daarna de wens om zoiets te verbieden. Voorbeeld 10.16 Update deel van primaire sleutel Boek de inschrijving van student voor Databases ( DB ) om naar de cursus Discrete wiskunde ( DW ). update Inschrijving set    cursus = ‘DW’ where  student = 4 and cursus = ‘DB’

Omdat student 4 voor de cursus DB nog geen tentamen heeft gedaan, verloopt de update in technische zin probleemloos. Toch is er iets raars aan de hand: deze update verandert geen eigenschap van een object (de inschrijving van student 4 voor cursus DB ) maar verandert het wezen van het object zelf. Eigenlijk verdwijnt het oude object en komt er een nieuw object voor in de plaats … Updateverbod

Wanneer we in Inschrijving een cursuscode veranderen ( voorbeeld 10.16 ), is dat een hele andere operatie dan wanneer we in Cursus een cursuscode veranderen (vergelijkbaar met voorbeeld 10.14 ): – Bij verandering van een cursuscode in Cursus gaat het om een administratieve ingreep die niets aan het wezen van een cursus verandert. Alleen de code wordt veranderd, misschien omdat een nieuwe systematiek voor codering wordt ingevoerd. Tegen zo’n verandering is niets in te brengen, mits hij wordt gecombineerd met een cascading update. – Bij verandering van een cursuscode in Inschrijving verandert het wezen van de betreffende inschrijving wel wezenlijk. Sterker nog: eigenlijk is sprake van annulering van de oude inschrijving en komt er een nieuwe inschrijving voor in de plaats. Deze visie gaat algemeen op ten aanzien van een (voorgestelde) update van een deel van een primaire sleutel. In plaats van de update van voorbeeld 10.16 zou het dan ook veel juister zijn om in plaats daarvan de oude inschrijving te verwijderen en een nieuwe inschrijving aan te maken: delete from Inschrijving where  student = 4 and cursus = ‘DB‘; insert into Inschrijving (student, cursus) values (4, ‘DW‘)

In het algemeen is er veel te zeggen voor een updateverbod op meervoudige primaire sleutels (uitgezonderd de updates die het gevolg zijn van een cascading update van een ‘hogere’ primaire sleutel).

10.6.4 Update van verwijssleutel Een update van een verwijssleutel is in wezen een gewone update. Het gaat altijd om één van drie dingen: – toekennen van een nieuwe ouder aan een kind – toekennen van ouder aan een kind zonder ouder

– een kind met ouder tot ‘weeskind’ maken. Hier volgen enkele voorbeelden. Voorbeeld 10.17 Toekennen van ouder aan een kind zonder ouder Geef student 4 mentor ‘ BAC ’ (was eerst null). Oplossing: update Student set    mentor = 'BAC'   -- was eerst null where  nr = 4

Voorbeeld 10.18 Een kind tot weeskind maken Schrap de mentor van student 3 (was eerst ‘ BAC ’). Oplossing: update Student set    mentor = null where  nr = 3

Oefenopgaven De volgende opgaven zijn gebaseerd op Orderdatabase. Voor de diagrammen: zie Boekverkenner. De voorbeeldpopulatie vindt u ook in bijlage 4.

Opgave 10.8 Voer een nieuwe order in: nummer 5900 voor klant 1447. Laat de orderdatum door Firebird invullen, gebruikmakend van de defaultwaarde: de systeemdatum. (Zie create-script). Laat totaalbedrag leeg. Opgave 10.9 Deze opgave gaat over integratie in Orderdatabase van een extra artikeltabel, ArtikelExtra, zie figuur 10.13 . De kolommen corresponderen met de kolommen nr, omschrijving, inkoopprijs en voorraad van Artikel, zie figuur 10.14 . De artikelen zélf komen deels overeen, echter met verschillende artikelnummers, terwijl ook de omschrijvingen en (inkoop)prijzen kunnen verschillen. De tabel Correspondentie geeft aan hoe de artikelnummers met elkaar corresponderen.

Figuur 10.13 Extra artikelen ter integratie in Orderdatabase

Figuur 10.14 Bestaande tabel Artikel in Orderdatabase

a Creëer de tabellen ArtikelExtra en Correspondentie met het volgende script. create table ArtikelExtra (anr     integer, naam    varchar(20), prijs   numeric(7,2), aantal  integer ); create table Correspondentie (nr          integer, anr         integer );

b Vul beide tabellen met de gegevens van figuur 10.13 .

c Werk tabel Artikel van Orderdatabase bij vanuit ArtikelExtra en Correspondentie, gegeven de volgende richtlijnen: – Bij corresponderende rijen wordt Artikel.omschrijving gewijzigd in ArtikelExtra.naam, en wordt Artikel.voorraad verhoogd met ArtikelExtra.aantal. – Artikelen die nog niet bestonden, worden vanuit ArtikelExtra ongewijzigd overgenomen, zonder artikelgroepcode, en met een verkoopprijs die berekend wordt uit de inkoopprijs door een verhoging met 30%. – U mag uit ArtikelExtra en Correspondentie alleen lezen , wat betekent dat alleen select ’s zijn toegestaan. U mag ervan uitgaan dat in ArtikelExtra en Artikel geen gelijke artikelnummers voorkomen.

Opgave 10.10 Maak een script voor het verwijderen van klanten die al sinds 2007 niets meer hebben besteld, met alles wat bij die klanten hoort. Stel een stappenplan op alvorens te gaan programmeren. Opmerking : de voorbeelddatabase bevat geen te-verwijderen-klanten. U kunt uw script testen door zulke klanten toe te voegen (met illustratieve gerelateerde informatie) ofwel door een meer recente datum als grensdatum te nemen.

11 Definitie van gegevensstructuren De hoofdstukken 7 t/m 10 gingen over het bekijken en wijzigen van een bestaande database, met behulp van de DML -subtaal van SQL (data manipulation language). In hoofdstuk 3 hebben we het ook al gehad over het zelf aanmaken van een database en over de mogelijkheden van de data definition language ( DDL , de SQL -subtaal om databaseobjecten binnen een database te creëren, te verwijderen of om hun structuur te veranderen). In dit hoofdstuk gaan we op de DDL dieper in. Bij dit onderwerp doen zich de kracht en de beperkingen van de verschillende SQL -dialecten sterk gelden. Dat geldt in het bijzonder voor

de zogenaamde deltaproblemen , dat zijn de problemen die verband houden met structuurwijzigingen. Databaseobjecten zijn er in vele soorten, en de mogelijkheden zijn dusdanig uitgebreid dat we alleen het belangrijkste behandelen. Raadpleeg via de Boekverkenner de online documentatie voor alle details.

11.1 Voorbeelddatabase: Ruimtereisbureau Ruimtereisbureau Reijnders is gespecialiseerd in reizen buiten de aardse dampkring. Dat kunnen tochtjes zijn rond de aarde met een ruimteveer, korte maanreizen of interplanetaire reizen, met een conventioneel transportmiddel of zelfs met ‘beam up’. Sommige reizen hebben één reisdoel, andere leiden langs verschillende hemellichamen van ons zonnestelsel. Reisbureau Reijnders richt zich op kapitaalkrachtige particulieren die zijn uitgekeken op safari’s, overlevingstochten en poolreizen. Het strokendiagram van figuur 11.1 geeft de databasestructuur voor een ondersteunend informatiesysteem. Dit is een logisch model, waarvoor twee implementatievarianten worden gegeven: een basisvariant Ruimtereisbureau en een variant RuimtereisbureauD die gebruik maakt van ‘domeinen’.

Figuur 11.1 Ruimtereisbureau: strokendiagram

Toelichting – De tabel Hemelobject bevat de Zon en alle planeten en manen van ons zonnestelsel. Hieronder bevinden zich alle mogelijke reisdoelen. Van elk hemelobject, de Zon uitgezonderd, vermeldt de kolom moederobject het hemelobject waar het omheen draait. – De afstand is de gemiddelde afstand in duizenden kilometers van een maan tot ‘zijn’ planeet of van een planeet tot de Zon. De diameter van een hemelobject is in kilometers. – De reisduur is in dagen en de prijs van een ruimtereis is in miljoenen euro. Beide zijn optioneel, maar als de prijs is ingevuld moet ook de reisduur zijn ingevuld. Deze regel wordt verderop in dit hoofdstuk als ‘check-constraint’ geïmplementeerd. – Een hemelobject kan gedurende één reis meermalen worden bezocht, vandaar dat het per reis unieke volgnr deel uitmaakt van de primaire sleutel van tabel Bezoek. Elk bezoek kent een verblijfduur, in dagen. Deze kan 0 zijn, namelijk wanneer het ruimtevaartuig het hemelobject alleen maar passeert of eromheen draait. Positieve waarden duiden op een bezoek-met-landing. – Een bezoek heeft geen bestaansrecht zonder reis. Vandaar de cascading delete voor de verwijzing Bezoek-Reis. Een cascading delete is er ook voor de recursieve verwijzing van Hemelobject (in rol van satelliet) naar Hemelobject (in rol van moederobject). Alle overige deleteregels zijn restricted, terwijl zoals meestal alle updateregels cascading zijn. – De personen die aan één of meer ruimtereizen deelnemen worden geregistreerd als ‘klant’. Als zodanig krijgen ze een uniek klantnummer. Klantnamen zijn omwille van de eenvoud beperkt tot (unieke) voornamen. Elke reis van een klant wordt geregistreerd in tabel Deelname.

– Een planeet is een satelliet van het hemelobject ‘Zon’ en een maan is een satelliet van een planeet. Er is dus verschil tussen ‘Maan’ en ‘maan’: de Maan is een maan van de planeet Aarde, zoals Io een maan is van de planeet Jupiter. Zie figuur 11.2 voor een voorbeeldpopulatie.

Figuur 11.2 Ruimtereisbureau: voorbeeldpopulatie

Natuurlijk zou het gegevensmodel danig verfijnd kunnen worden, om meer reisdetails op te kunnen nemen. Het huidige model is voor ons doel echter rijk genoeg.

11.2 Data definition language De SQL -opdrachten voor het creëren van een database en het definiëren van databaseobjecten vormen samen een subtaal van SQL die de data definition language ( DDL ) wordt genoemd. De meeste DDL -opdrachten beginnen met create , drop of alter , voor achtereenvolgens het creëren van objecten, het verwijderen ervan of het wijzigen van hun structuur. Alleen de opdrachten voor het creëren van gebruikersprivileges of het verwijderen daarvan zijn afwijkend en luiden respectievelijk: grant (‘ken toe’) en revoke (‘herroep’).

11.2.1 Databases en databaseobjecten Databaseobjecten zijn de ‘dingen’ waarmee een database is gevuld. In dit hoofdstuk kijken we naar tabellen , domeinen , views en sequences . Over tabellen is nog heel wat meer te zeggen dan we al hebben gedaan. We doen dat in de paragrafen 4 en 5. Een domein is een soort ‘logisch gegevenstype’ dat aan een of aan meer kolommen, bijvoorbeeld aan een primaire-sleutelkolom en de bijbehorende verwijssleutelkolommen, tegelijk één fysiek gegevenstype doorgeeft. Voorbeelden worden behandeld in paragraaf 11.6 . Met views hebben we al kennisgemaakt in hoofdstuk 9 ‘Subselects en views’. Een view kunnen we opvatten als een virtuele tabel. In feite is het een select -statement dat onder een eigen naam wordt bewaard. Met de daar behandelde views formuleerden we deeloplossingen van min of meer complexe queryproblemen. In paragraaf 11.7 maken we kennis met andere gebruiksmogelijkheden van views. Een sequence is een object dat automatisch een volgnummer kan afgeven, bijvoorbeeld een kunstmatig klantnummer bij een insert -statement voor een nieuwe klant. In paragraaf 11.9 vertellen we er het fijne van. In volgende hoofdstukken worden nog andere typen databaseobjecten behandeld: gebruikers en rollen ( hoofdstuk 12 ‘Autorisatie’), indexen ( ho ofdstuk 13 ‘Query-optimalisatie’) en triggers en stored procedures ( hoofds tuk 15 ‘Triggers en stored procedures’).

11.2.2 Gebruikersinformatie en meta-informatie Informatie over databaseobjecten van alle typen (tabellen, views, domeinen, … ) wordt opgeslagen in … tabellen! Iets anders zouden we ook niet mogen verwachten bij een relationele database. Tabellen en indexen zijn de enige databaseobjecten waarmee fysieke gegevensstructuren corresponderen: tabellen voor de informatie, indexen voor snelle toegang tot die informatie. Tabellen zijn er kennelijk in twee typen: gebruikerstabellen met gebruikersinformatie en administratietabellen met meta-informatie (‘informatie over informatie’) over databaseobjecten. De tabellen met meta-informatie vormen met elkaar de data dictionary (of systeemcatalogus ). Hoofdstuk 16 is daar helemaal aan gewijd. Daar zal dit allemaal concreet worden.

11.2.3 DDL en transacties Het transactiemodel van de SQL -standaard staat toe dat een transactie DDL -statements ( create ..., alter ... of drop ..., grant ..., revoke ...) bevat. Bij rdbms’en waar dat niet voor geldt houdt een DDL -statement een impliciete commit in: er vindt dan een commitmoment plaats, net als bij een expliciet commit -statement. Ook sommige SQL -omgevingen, waaronder IQU , koppelen een commitmoment aan een DDL -statement. In IQU is dus een rollback direct na een DDL -statement zinloos. Zie ook par agraaf 10.2 over transacties.

11.2.4 Structuurwijzigingen: deltaproblematiek Deltaproblematiek is de problematiek van structuurwijzigingen (naar de Griekse letter delta, in de wiskunde vaak gebruikt voor veranderingen). Commando’s voor structuurwijzigingen beginnen alle met alter . Rdbms’en geven echter lang niet altijd de ondersteuning die we zouden mogen verwachten, waardoor soms een ingewikkelde omweg nodig is om een eenvoudig resultaat te bereiken. Zo bestaat er soms geen simpele manier om een kolom uit een tabel te verwijderen, maar moeten we bijvoorbeeld als volgt te werk gaan:

1 een nieuwe (lege) tabel maken zonder die ene kolom ( create table ) 2 de gegevens van de oude tabel overhevelen naar de nieuwe ( insert into ... select ) 3 de oude tabel verwijderen ( drop table ) 4 de nieuwe tabel hernoemen met de naam van de oude ( rename ). In Firebird gaat dat (conform de SQL -standaard) eenvoudig met een alter table drop -statement. Maar daar geven andere wijzigingen weer een hoop gedoe, bijvoorbeeld het wijzigen van een tabelnaam.

11.3 Levenscyclus van een database Een database is de fysieke realisatie van een implementatiemodel, zoals dat kan worden gespecificeerd via create - en alter -statements, al dan niet gebundeld in een script. Een database begint zijn leven ‘leeg’. Dit is echter maar betrekkelijk, want zodra een database is aangemaakt, bevat hij al een data dictionary, die is op te vatten als een verzameling administratietabellen. In deze paragraaf zullen we zien hoe u een nieuwe (Firebird-)database creëert, hoe u er contact mee maakt en hoe u hem kunt verwijderen.

11.3.1 Creëren van een database De manier om een database aan te maken verschilt per dialect. Vanuit IQU maakt u als volgt een Firebird-database aan voor Ruimtereisbureau (de implementatievariant zonder domeinen): create database 'Ruimtereisbureau.fdb' user 'Ruimtereisbureau' password 'pw'

Als gevolg van dit commando wordt op schijf een bestand Ruimtereisbureau.fdb aangemaakt. Dit is direct na het aanmaken niet

leeg, het bevat immers al de catalogustabellen. En deze bevatten al heel wat rijen, met informatie over zichzelf en elkaar. Omdat niet iedereen zomaar een database mag creëren, moet een create database -statement een geldige combinatie van een gebruikersnaam en een wachtwoord bevatten. De gebruiker Ruimtereisbureau moet dus tevoren zijn aangemaakt. Een create database -statement kan worden uitgebreid met opties die van invloed zijn op de fysieke opslagkenmerken. Raadpleeg hiervoor de documentatie. Databases en gebruikers Een Firebird-database omvat bestanden van één gebruiker. Die gebruiker bestaat los van de database en kan eigenaar zijn van meerdere databases. De verhouding tussen databases en gebruikers is vaak anders. Bij Oracle en Ms Sql Server bijvoorbeeld bestaat een gebruiker alleen bínnen een database. Die database kan een onbepaald aantal gebruikers bevatten, elk met hun eigen tabellen en andere objecten. Gebruikers zijn vrij in het kiezen van namen voor hun objecten. Een volledige tabelspecificatie bij bijvoorbeeld Oracle omvat daarom de gebruikersnaam én de tabelnaam, met een punt als scheidingsteken. Databaseopslag en -export De manier waarop de database wordt opgeslagen, verschilt sterk per rdbms. Firebird maakt voor iedere database standaard één bestand aan, met extensie fdb. Alles wat op een database betrekking heeft, dus ook de data dictionary, wordt hierin opgeslagen. Een Firebird-database kan daardoor via het fdbbestand probleemloos worden ‘meegenomen’ (geëxporteerd) naar een andere Firebird-omgeving op een andere computer. In andere omgevingen is export meestal minder eenvoudig. Om bijvoorbeeld in Oracle een database te kunnen exporteren naar een andere omgeving, moet een speciaal ‘dumpbestand’ worden gemaakt.

11.3.2 Een connectie met een database Na het creëren van een database hebt u automatisch een connectie (verbinding) met die database. U bent dan ingelogd en bent een sessie begonnen. Dat betekent dat uw SQL -commando’s vanaf dat moment betrekking hebben op die database. Het eerste commando zal natuurlijk een create table zijn! Voor de puntjes op de i: een sessie correspondeert met de commando’s tussen inloggen en uitloggen die worden gegeven vanuit één clientapplicatie. Nog preciezer: vanuit één instantie van een clientapplicatie. Een gebruiker die tegelijkertijd vanuit twee IQU -vensters werkt, heeft twee sessies open staan. Inloggen op een al eerder aangemaakte database gaat met het connect statement. Stel, u hebt een poosje gewerkt met een andere database, bijvoorbeeld OpenSchool.fdb, dan kunt u een connectie aanbrengen met Ruimtereisbureau.fdb via:

connect 'Ruimtereisbureau.fdb' user 'Ruimtereisbureau' password 'pw'

De vorige sessie wordt dan automatisch beëindigd, wat inhoudt dat de connectie met OpenSchool.fdb wordt gesloten. Als volgt sluit u een connectie zonder een nieuwe aan te brengen: disconnect

Dit uitloggen zou u kunnen gebruiken voor het verrichten van onderhoud aan de database, zoals het maken van een backup of het optimaliseren van indexen. Want tijdens een sessie kunt u er niet van op aan dat het databasebestand volledig is bijgewerkt of vrijgegeven. Meer hierover in pa ragraaf 13.3.11 (‘backup en restore’). Locks in data dictionary Tijdens het bewerken van een database kunnen de betrokken rijen van de data dictionary voor enige tijd zijn ‘gelockt’. Als u dan een structuurwijziging wilt uitvoeren (zoals het verwijderen van een kolom of constraint), kan het voorkomen dat Firebird ‘denkt’ dat bepaalde databaseobjecten (bijvoorbeeld een index) nog in gebruik zijn. Firebird houdt de structuurwijziging op zo’n moment tegen. De foutmelding maakt gewag van een niet uitgevoerde ‘metadata update’ ( metadata zijn de gegevens in de data dictionary). De oplossing is: disconnect , gevolgd door opnieuw inloggen. In één commando kan dat met: reconnect .

11.3.3 Verwijderen van een database Een eenvoudige maar wat lompe manier om een database te verwijderen, is: het bestand van schijf wissen, via het besturingssysteem van de computer. Eleganter is het drop database -statement te gebruiken: drop database

waardoor de connectie met de actieve database wordt gesloten en deze wordt verwijderd. Het effect is in beide gevallen hetzelfde. Logisch, wanneer we bedenken dat een Firebird-database (met al zijn objecten en inclusief zijn data dictionary) volledig vastligt in zijn fdb-bestand. Via de deinstallatieoptie van de Boekverkenner gaat het natuurlijk nog makkelijker.

Opgave 11.1 Creëer als gebruiker Sysdba (met wachtwoord masterkey) een database Testdatabase.fdb. Controleer of het bestand is aangemaakt in de standaard databasemap (te raadplegen via Relationele databases en SQL in het Windows Startmenu). Maak ook een testtabel. Opgave 11.2 Beëindig de sessie en ga na dat het nu niet mogelijk is tabellen te creëren. Welke foutmelding krijgt u?

Opgave 11.3 Wijzig de naam van Testdatabase.fdb in Testbase.fdb. Opgave 11.4 Verwijder Testbase.fdb. Doe dit op twee manieren, na tussentijds opnieuw aanmaken.

11.4 Tabellen Tabellen: daar ging het allemaal om bij relationele databases. En wel om tabellen in de zin van ‘relaties’: een soort abstracte tabelstructuren waarbij de volgorde van rijen en van kolommen geen rol speelt en waarbij alle rijen verschillend zijn, ook al zouden we ze meervoudig noteren. In deze paragraaf zetten we bekende zaken over tabellen- DDL op een rijtje en gaan we hier en daar dieper op de materie in. Het create-script van paragraaf 11.4.1 is onderdeel van het eerste implementatiemodel van het logische Ruimtereisbureau-model. De naam van dit implementatiemodel luidt: Ruimtereisbureau.

11.4.1 Creëren van tabellen Het script RuimtereisbureauCreate.sql bevat alle benodigde create table -statements. We beginnen met een vereenvoudigde versie van de twee statements voor het creëren van de tabellen Transport en Reis. create table Transport (code              varchar(2)     not null, omschrijving      varchar(12)    not null, primary key (code), unique (omschrijving) ); create table Reis (nr                integer        not null, vertrekdatum      date           not null, transport         varchar(2)     not null, duur              integer, prijs             numeric(5,2), primary key (nr), foreign key (transport) references Transport(code) on update cascade );

Toelichting

– Het creëren van de tabellen gaat ‘van boven naar beneden’. We kunnen immers geen tabel met verwijssleutel aanmaken, wanneer de bijbehorende oudertabel nog niet bestaat. – De algehele structuur van een create table -statement is een kommalijst van kolomdefinities en constraints (o.a. primaire sleutels en verwijssleutels). – De primaire sleutel-constraints worden gedefinieerd door de keywords primary key , gevolgd door de kolomnaam tussen haakjes. Bij een brede sleutel wordt dat een kommalijst van kolomnamen. – Met het keyword unique wordt op analoge wijze een alternatieve sleutel (‘unique constraint’) gedefinieerd. – De verwijssleutel-constraints worden gedefinieerd door de keywords foreign key , gevolgd door de kolomnaam, gevolgd door het keyword references , met daarachter de primaire sleutel in de vorm van < tabelnaam >(< kolomnaam >). Bij een brede sleutel krijgen we ook hier een kommalijst van kolomnamen. – Met on update cascade kan een cascading update worden gespecificeerd. Met on update restricted krijgen we een restricted update, maar omdat dat de default is, laten we dat doorgaans weg.

11.4.2 Constraintnamen Bij het wijzigen van structuren (deltaproblemen) zult u wel eens op constraints stuiten die een actie blokkeren. Vaak zal het dan nodig zijn een constraint te verwijderen (en later weer aan te maken) of – als het rdbms dit toestaat – tijdelijk op non-actief te zetten. Hiervoor moet u de constraintnaam kennen. Elke constraint heeft er een, ook een not null constraint. Een constraint krijgt zijn naam via de DDL -code (dus van de programmeur) of van het rdbms (wanneer de programmeur de

naamgeving achterwege laat). In beide gevallen wordt de constraintnaam opgenomen in een van de tabellen van de data dictionary, de tabel Rdb$constraints. De rdbms-namen bestaan echter uit een code en een nietszeggend volgnummer, zodat het een heel gepuzzel kan zijn om uit te zoeken welke naam bij welke constraint hoort. Het is daarom een goede gewoonte constraintnamen te definiëren binnen de DDL -code, met een vaste systematiek zodat u aan de naam ook het type kunt herkennen. Een constraint kunt u zelf een naam geven, door de constraintdefinitie vooraf te laten gaan door het gereserveerde woord constraint met een zelfgekozen identifier als naam. Hier volgt nogmaals de definitie van de tabellen Transport en Reis, nu in de versie van het script RuimtereisbureauCreate.sql. create table Transport (code              varchar(2)     not null, omschrijving      varchar(12)    not null, constraint pk_transport primary key (code), constraint un_transportomschrijving unique (omschrijving) ); create table Reis (nr                integer        not null, vertrekdatum      date           not null, transport         varchar(2)     not null, duur              integer, prijs             numeric(5,2), constraint pk_reis primary key (nr), constraint fk_reis_met_transport foreign key (transport) references Transport(code) on update cascade );

We hebben de constraints herkenbare namen gegeven, waarin ook hun type tot uitdrukking komt. In paragraaf 11.5 gaan we dieper in op kolomdefinities. In paragraaf 11.6 gaan we dieper in op constraints. Naamgeving volgens ‘single point of definition’ Naamgeving is geen creatief proces. Ook bij constraintnamen is het niet de bedoeling elke keer te denken ‘hoe zal ik déze constraint nu weer eens noemen?’ Het principe ‘single point of definition’, dat we kennen voor gegevensopslag, geldt ook hier: op één plaats worden de afspraken vastgelegd over systematiek van naamgeving. Dit is een kwestie van programmeerstijl. Merk op dat de drie onderdelen noodzakelijk zijn voor een strakke naamgeving: de tabelnamen zijn niet altijd voldoende, omdat tussen dezelfde kind- en oudertabel meer dan één verwijzing kan bestaan.

11.4.3 Tabel met recursieve verwijzing De volgende tabel in het script is Hemelobject. Deze bevat een recursieve verwijzing. Hier zitten we met een probleem: de oudertabel bij een recursieve verwijzing is de tabel zelf. Als de oudertabel moet bestaan bij het aanmaken van een verwijzing ernaar, moet de tabel dus al bestaan voor we hem aanmaken… De oplossing is eenvoudig: we maken de tabel eerst aan zonder de recursieve verwijzing en voegen deze daarna toe via een alter table -statement. Hier volgt de code: create table Hemelobject (naam              varchar(10)    not null, moederobject      varchar(10), afstand           numeric(10,3), diameter          integer, constraint pk_hemelobject primary key (naam), ); alter table Hemelobject add constraint fk_satelliet_van_hemelobject foreign key (moederobject) references Hemelobject(naam) on update cascade;

Firebird staat toe een recursieve verwijzing direct op te nemen in het create table -statement. Het voorgaande is echter een nette manier van programmeren die in elk SQL -dialect werkt. Uitvoeren van het volledige script (RuimtereisbureauCreate.sql), na ‘connecten’ aan de database Ruimtereisbureau.fdb, resulteert in zeven lege tabellen.

11.4.4 Verwijderen van tabellen en deltaproblemen Een tabel verwijderen Het commando om een tabel te verwijderen, luidt: drop table . Het volgende statement heeft tot gevolg dat wordt gepoogd de tabel Hemelobject te verwijderen: drop table Hemelobject

Als de poging slaagt, verdwijnt de tabel Bezoek volledig. Het commando drop table ... doet dus heel wat anders dan delete from ..., dat alleen een poging doet de rijen van een tabel te verwijderen. We hebben het steeds over ‘pogen’. Of het echt lukt, hangt af van diverse factoren. Zolang er bijvoorbeeld nog een of meer tabellen bestaan met een actieve verwijzing naar een rij van de te verwijderen tabel, gaat het niet

door. Zo’n tabel kan ook de tabel zelf zijn wanneer deze (zoals Hemelobject) een recursieve verwijzing bevat. Het ligt voor de hand dan eerst al dergelijke (kind)rijen te verwijderen. Technisch gesproken kunnen we ook eerst de verwijzing zelf verwijderen. Het verwijderen van verwijzingen en van andere constraints is een ‘deltaprobleem’ voor tabellen, zie paragraaf 11.4.4 . Deltaproblemen op tabelniveau Een tabel heeft een naam, een of meer kolommen en eventueel nog constraints. In deze paragraaf kijken we naar de volgende deltaproblemen: het toevoegen of verwijderen van een kolom, het toevoegen of verwijderen van een constraint en het wijzigen van de tabelnaam. Het wijzigen van een bestaande kolom zien we als een deltaprobleem op kolomniveau, zie hiervoor paragraaf 11.5.2 . Toevoegen of verwijderen van een kolom In één alter table -statement kunnen meerdere wijzigingen (toevoegen of verwijderen van kolommen) in één kommalijst worden gecombineerd. Voorbeeld 11.1 Alter table voor kolommen Voeg aan Hemelobject verplichte kolommen toe voor de omlooptijd (geteld in aardse dagen) en de gemiddelde oppervlaktetemperatuur (in graden celsius). Verwijder de kolom afstand. Oplossing: alter table Hemelobject add omlooptijd    integer   not null, add temperatuur   integer   not null, drop afstand

De datatypen zijn gekozen omdat we denken zowel voor omlooptijd als temperatuur te kunnen volstaan met gehele getallen. We moeten ons realiseren dat na deze structuurwijziging alle bestaande rijen niet aan de not null -contraints voldoen. Gelukkig worden deze voor bestaande rijen niet direct gecontroleerd. Dat gebeurt echter wel na een update. Het is daarom verstandig direct na dit alter table statement de nieuwe kolommen voor elk hemelobject een waarde te geven. In het script van paragraaf 11.4.3 hebben we nóg een voorbeeld gezien van een alter table … add : bij het toevoegen van een recursieve constraint. Toevoegen of verwijderen van een constraint Het toevoegen of verwijderen van een not null -constraint of default specificatie zien we als het wijzigen van een kolomdefinitie, zie hiervoor pa ragraaf 11.5 . Voor de overige constraints geldt dat het toevoegen of

verwijderen vrijwel net zo gaat als het toevoegen of verwijderen van een kolom. Voorbeeld 11.2 Alter table voor constraint Verwijder de verwijssleutel van Bezoek naar Reis. Oplossing: alter table Bezoek drop constraint fk_bezoek_tijdens_reis

In de volgende subparagraaf ‘Wijzigen van een tabelnaam’ zien we hier een toepassing van, waarbij de constraint even later – in aangepaste vorm – weer wordt aangemaakt. Sommige rdbms’en hebben de mogelijkheid een constraint tijdelijk buiten werking te stellen en later weer te activeren. Dit is vooral bedoeld om ‘reparatiewerkzaamheden’ aan de database te kunnen verrichten, zonder ‘last’ te hebben van referentiële integriteit of andere constraints. Oracle bijvoorbeeld kent de statements disable constraint en enable constraint . Het spreekt vanzelf dat dit vaak handiger is dan een constraint te verwijderen en later weer opnieuw aan te maken. Wijzigen van een tabelnaam Oracle kent het rename -commando om tabellen een nieuwe naam te geven. Bijvoorbeeld: rename Reis to Ruimtereis . Het effect is dat in de hele data dictionary een cascading update plaatsvindt, waarbij de tabelnaam Reis overal wordt vervangen door Ruimtereis. Bij SQL -dialecten die dit commando niet kennen (waaronder Firebird), is een omweg noodzakelijk om hetzelfde resultaat te bereiken. Het grootste probleem is dan het handhaven van referentiële integriteit tijdens die omweg; er zijn immers verwijssleutels naar Reis vanuit Deelname en Hemelobject. De volgende procedure leidt tot het gewenste resultaat.

1 Maak een nieuwe tabel: Ruimtereis. 2 Kopieer de inhoud van Reis naar Ruimtereis. 3 Verwijder alle verwijssleutels naar Reis.

4 Verwijder de oude tabel Reis. 5 Maak nieuwe verwijssleutels, naar Ruimtereis.

Opgave 11.5 We willen in Ruimtereisbureau alle voorkomens van ‘transport’ wijzigen in ‘vervoer’: a Kolom Reis.transport moet vervoer gaan heten. b Tabel Transport moet Vervoer gaan heten. Geef een gedetailleerd stappenplan, ervan uitgaande dat u geen commando’s zult gebruiken om een tabel of een kolom te hernoemen. (Deze geven in de praktijk de nodige problemen.)

Opgave 11.6 Voor de verwijzing van Deelname naar Klant is in de DDL -code een cascading update gespecificeerd. Verhoog nu echter alle klantnummers met 1000, zonder daarvan gebruik te maken.

11.5 Kolomdefinities Een tabeldefinitie omvat definities van kolommen en van constraints. Deze blijken niet altijd gescheiden te zijn. Allereerst zijn er de not null constraints en default -specificaties, die altijd worden gedefinieerd als onderdeel van een kolomdefinitie. We noemen dat: inline , in tegenstelling tot out-of-line constraints die via een aparte logische regel zijn gedefinieerd. Inline definitie van primaire sleutels en verwijssleutels Ook primaire- en verwijssleutelconstraints kunnen inline worden gedefinieerd. Omdat die syntaxis alleen geldt voor sleutels over één kolom, laten we deze buiten beschouwing.

11.5.1 Elementen van een kolomdefinitie Een kolomdefinitie kan deel uitmaken van een create table -statement of van een alter table -statement. De kolomdefinitie in het volgende alter table -statement bevat buiten de kolomnaam en het datatype (beide verplicht) een not null -constraint en een default -specificatie: alter table Reis add aantal_deelnemers smallint default 0 not null

Op elk van de drie elementen na de kolomnaam zullen we nu nader ingaan: het datatype, de not null -specificatie en de defaultspecificatie.

11.5.2 Datatypen Eén van de zaken waarin de databases van fabrikanten onderling sterk verschillen, is de verzameling datatypen waaruit gekozen kan worden. Ondanks de grote verschillen is er inmiddels een basis waarover vrijwel ieder rdbms beschikt. Figuur 11.3 geeft een overzicht van de belangrijkste datatypen die tot de SQL -standaard ( ANSI/ISO ) behoren en die ook door Firebird en de meeste andere rdbms’en worden ondersteund. Raadpleeg de Firebird-documentatie voor het bereik van de datatypen float en double precision en voor het gebruik van blob ’s. Het bereik van de numerieke datatypen en van date kan per dialect verschillen. Rdbms’en hebben vaak eigen namen voor de datatypen. Voor de meeste datatypen accepteren ze ook de naam uit de SQL -standaard. Externe en interne datatypen Het is normaal dat een rdbms slechts een beperkt aantal eigen, interne datatypen kent. Deze interne datatypen zijn direct gerelateerd aan de manier waarop de waarden intern worden opgeslagen en ermee wordt gerekend of gemanipuleerd. Naast de interne typen worden meestal ook andere datatypen ondersteund, bijvoorbeeld van de ANSI/ISO -standaard. Waarden van zo’n extern datatype worden opgeslagen als een waarde van een van de interne typen.

Figuur 11.3 De belangrijkste datatypen van ANSI/ISO-SQL

Zo worden in Firebird numeric ’s opgeslagen als een smallint , integer of double precision , afhankelijk van het totaal aantal cijfers en het aantal cijfers na de decimale punt, zie tabel 11.1 voor enkele voorbeelden. Natuurlijk worden de posities van de decimale punten in de berekeningen correct meegenomen, maar u moet niet gek opkijken als er bij berekening met grote getallen afwijkende uitkomsten optreden. Dat komt dan door benaderingen of afrondingen bij het intern werken met double precision . Tabel 11.1 Numeric’s worden intern verschillend opgeslagen

11.5.3 Not null-constraints De not null is een echte kolomconstraint. Het is een beperkende constraint die een bepaalde toestand afdwingt: de toestand dat elke rij van de betreffende kolom een waarde bevat. In de Firebird data dictionary worden not null -constraints bijna net zo opgeslagen als andere constraints.

11.5.4 Default-specificaties Met een default-specificatie kan een cel van een nieuwe rij een initiële waarde worden gegeven, indien hij niet is gespecificeerd in het insert statement. Bij het voorbeeld aan het begin van deze paragraaf: alter table Reis add aantal_deelnemers smallint default 0 not null

zorgt ‘default 0’ ervoor dat een kolomwaarde van aantal_deelnemers 0 wordt, tenzij anders gespecificeerd. Default-specificaties worden soms wel en soms niet tot de constraints gerekend. Conceptueel gezien zijn het gedragsregels. Ze kunnen immers databasegedrag tot gevolg hebben: het automatisch vullen van een cel in een nieuwe rij met een kolomdefaultwaarde. In technische zin zijn het geen echte constraints; ze kunnen bijvoorbeeld geen naam krijgen en worden in de data dictionary anders opgeslagen.

11.5.5 Afleidbare kolommen De SQL -standaard en ook Firebird ondersteunen afleidbare kolommen .

Als voorbeeld geven definiëren we een aangepaste klantentabel (zonder domeinen) met een afleidbare kolom volledige_naam: create table Klant1 (nr                integer       not null, voornaam          varchar(15)   not null, voorvoegsel       varchar(8), achternaam        varchar(20)   not null, volledige_naam    varchar(45)   generated always as (voornaam || coalesce(' ' || voorvoegsel, '') || ‘ ‘ || achternaam), geboortedatum     date          not null, constraint pk_klant1 primary key (nr) )

Toelichting: De kolom volledige_naam is via een generated always as clausule gedefinieerd als afleidbare kolom. De waarden daarvan worden automatisch berekend als een concatenatie van voornaam, voorvoegsel (indien aanwezig) en achternaam. Voor ingewikkelder afleidingen kan als generated always as -expressie ook een SQL -query worden gekozen. Deze kan gebruik maken van kolomwaarden van de tabel zelf en van andere tabellen. Zo kan bijvoorbeeld de Toetjesboek-kolom Gerecht.energie PP als afleidbare kolom worden gedefinieerd. Er zijn dan geen triggers nodig. Raadpleeg voor meer informatie de documentatie.

11.5.6 Deltaproblemen op kolomniveau Het wijzigen van een bestaande kolomdefinitie kan van alles inhouden: een naamswijziging, veranderen van datatype (bijvoorbeeld van integer naar varchar ), vergroting of verkleining van het datatype (bijvoorbeeld van numeric(10) naar numeric(12) ), of wijzigen van een not null - of default -specificatie. Dit soort operaties is soms erg ingewikkeld, waarbij het ook nog kan uitmaken of de kolom tot een primaire sleutel behoort en of we domeinen gebruiken. De oplossingen zijn veelal afhankelijk zijn van het gebruikte SQL -dialect. Voorbeeld 11.3 Wijzigen kolomnaam Wijzig van tabel Reis de kolomnaam vertrekdatum in lanceerdatum. In sommige SQL -dialecten vraagt dit een omweg: aanmaken van een nieuwe kolom lanceerdatum, kopiëren van de gegevens van de kolom vertrekdatum naar de kolom lanceerdatum en verwijderen van de kolom vertrekdatum. In Firebird bestaat gelukkig een alter table … alter column -commando:

alter table Reis alter column vertrekdatum to lanceerdatum

Voorbeeld 11.4 Wijzigen datatype Verhoog de maximaal toegestane lengte van objectnamen tot twaalf tekens. Oplossing Ruimtereisbureau bevat drie kolommen objectnamen: in Hemelobject (kolommen naam en moederobject) en in Bezoek (kolom object). Op drie plaatsen wordt dus het datatype voor objectnamen vastgelegd. Op drie plaatsen moet het dus ook worden veranderd. Ook hiervoor gebruiken we een variant van het alter table … alter column -commando, bijvoorbeeld: alter table Hemelobject alter column naam type varchar(12) alter column moederobject type varchar(12); alter table Bezoek alter column object type varchar(12)

11.6 Constraints In deze paragraaf zetten we alle behandelde constraints nog eens op een rijtje. Ook gaan we in op een nieuw type, de check -constraint.

11.6.1 Primary key-constraints In deze paragraaf wijden we nog een enkel woord aan primary key constraints. Een smalle kandidaatsleutel mag – als rij-identifcatie – geen nulls bevatten. Bij een brede kandidaatsleutel is daar conceptueel niets op tegen, zolang een rij niet over de hele breedte van de sleutel nulls bevat. Dit geldt dus in het bijzonder voor primaire sleutels. Vrijwel alle rdbms’en zijn echter Codd-relationeel (zie paragraaf 4.1.2 ) en dat houdt een not null -eis in voor élke primaire-sleutelkolom. Per rdbms verschilt het weer of voor een primaire-sleutelkolom een expliciete not

null in het create table -statement dan nog verplicht is. Voor Firebird is dat wel het geval. Kolommen van datatype blob kunnen geen deel uitmaken van een primary key -constraint en evenmin van een foreign key - of unique constraint. Dit is niet zo vreemd: vanwege hun aard (plaatjes, video- of geluidsfragmenten, tekstdocumenten, ...) zijn uniciteits- en gelijkheidscontroles problematisch en niet voor de hand liggend.

11.6.2 Foreign key-constraints Over verwijssleutelconstraints is al veel gezegd. Niet zo vreemd, verwijssleutels vormen immers het ‘cement’ van een relationele database. We gaan nog wat nader in op de refererende-actieregels. Refererende-actieregels drukken uit wat moet gebeuren als de referentiële-integriteitsregel dreigt te worden overtreden, door een poging tot verwijderen van een ouderrij of een poging tot update van een primaire sleutel. In SQL worden ze als onderdeel van een create table -statement gespecificeerd, als on delete - of on update -clausules bij de bijbehorende foreign key -constraint. Firebird ondersteunt de restricted, cascading en nullifying varianten van de deleteregels én van de updateregels. En zelfs nog een vierde variant: de set default -variant. Raadpleeg hiervoor de documentatie. Deleteregels Van de deleteregels is de restricted delete de meeste strenge en dus meest veilige. Deze houdt een delete van een ouderrij tegen wanneer er nog een verwijzing bestaat (via de betreffende verwijssleutel) vanuit een kindrij. De restricted delete is een direct gevolg van de referentiëleintegriteitsregel zelf, wanneer geen andere deleteregel is gespecificeerd. U hoeft deze dus niet expliciet te specificeren. Wilt u dat toch, dan kan dat met on delete restrict . De cascading delete ( on delete cascade ) wordt doorgaans gespecificeerd voor verwijzingen waarbij een kindrij geen betekenis heeft zonder de ouderrij. Denk aan ingrediëntregels, die geen betekenis hebben zonder een bijbehorend gerecht. Bij een poging tot verwijderen van zo’n ouderrij is het dan zeer zeker de bedoeling dat de kindrijen meeverwijderd worden. Updateregels Van de updateregels is de cascading update ( on update cascade ) het meest voor de hand liggend: een poging tot wijziging in een primaire sleutel ‘cascadeert’ naar alle verwijzende waarden. Ook hier wordt dit per verwijzing gespecificeerd. Aan een cascading update zitten de nodige haken en ogen, zie paragraaf 10.6.3 .

11.6.3 Unique-constraints Een primary key -constraint impliceert uniciteit. Maar er zijn meer uniciteitsregels, in het bijzonder die het gevolg zijn van een alternatieve sleutel. Hiervoor is de unique -constraint beschikbaar. Een unique constraint heeft, net als een primaire sleutel of een verwijssleutel, betrekking op een combinatie van (een of meer) kolommen. Een alternatieve sleutel kunt u specificeren door een not null - en een unique -constraint te combineren ( SQL kent geen alternate key constraint). Een voorbeeld zagen we al bij de definitie van tabel Transport, in paragraaf 11.4.1 (zonder constraintnaam) en in paragraaf 11.4.2 (met constraintnaam).

11.6.4 Check-constraints Voor aanvullende voorwaarden is er de check -constraint. We kunnen deze o.a. gebruiken voor waardebeperkingen. Voorbeeld 11.5 Check-constraint op kolommen We bekijken de volledige de DDL -code voor creatie van tabel Hemelobject, uit het script RuimtereisbureauCreate.sql. create table Hemelobject (naam              varchar(10)    not null, moederobject      varchar(10), afstand           numeric(10,3), diameter          integer, constraint pk_hemelobject primary key (naam), constraint ch_afstand check (afstand > 0), constraint ch_diameter check (diameter > 0), constraint ch_nietSatellietVanZichzelf check (moederobject != naam) )

Toelichting – De eerste en tweede check -constraint dwingen af dat een ingevoerde afstand en diameter positief zijn; invoer van 0 of een negatieve waarde geeft een foutmelding.

– De derde check -constraint dwingt af dat een hemelobject geen satelliet is van zichzelf. – Bij alle drie de check -constraints wordt unknown als waarde van de conditie geaccepteerd. Check-condities en de driewaardige logica Een check -conditie is een logische expressie die net als een selectieconditie in een where -clausule drie logische waarden kan hebben: true , false of unknown . In een where -clausule is een true vereist om de test de doorstaan. In een check -conditie is dat anders: conform de SQL standaard wordt ook een unknown geaccepteerd. Dit is de reden dat de code van voorbeeld 11.5 maakt dat niet-ingevulde waarden voor afstand, diameter of moederobject worden geaccepteerd. We hoeven de check conditie dus alleen te baseren op werkelijk ingevulde waarden en geen rekening te houden met het mogelijk optioneel zijn van een kolom. (Dit kan overigens per dialect veschillen.) In een check -conditie mogen alle vergelijkingsoperatoren worden gebruikt, dus ook bijvoorbeeld between ... and en like . Ook kunnen we testen op null, met de operator …is null. Check-constraints met select Een check -conditie mag een select -expressie als operand bevatten, zelfs in combinatie met exists . Wie de mogelijkheden hiervan wil onderzoeken (denk bijvoorbeeld aan het limiteren van het aantal rijen in een tabel tot een maximum), moet zich twee dingen realiseren. – De select -expressie wordt vooraf geëvalueerd, waardoor na afloop niet aan de conditie voldaan hoeft te zijn. – Een check -constraint is alleen actief voorafgaande aan een insert of update van de betreffende tabel. Wijzigingen elders of deletes kunnen van invloed zijn op de waarde van de select -expressie, waardoor de gecontroleerde conditie later toch geschonden kan worden. Met triggers ( hoofdstuk 15 ) krijgen we een krachtiger middel in handen tot regelhandhaving.

Opgave 11.7 Een not null -constraint kan op eenvoudig worden vervangen door een equivalente check -constraint, met een check -clausule van de vorm check (... is not null ). Dit opent de mogelijkheid een gewone not null te vervangen door een meer subtiele variant.

Pas het create table -statement van Reis aan, zodanig dat aan de volgende regel te implementeren: als de prijs is ingevuld, moet ook de reisduur zijn ingevuld. Voeg ook een check -constraint toe die afdwingt dat een reisnummer in het interval 1-2000 ligt.

Opgave 11.8 Bestudeer het script RuimtereisbureauCreate.sql waarin de behandelde check -constraints en nog enkele andere zijn opgenomen. Opgave 11.9 Voeg een check -constraint toe die afdwingt dat een reis niet meer dan vier deelnemers heeft. Aanwijzing : maak gebruik van een select-expressie die voorafgaande aan een insert van een nieuwe deelname controleert of er nog ruimte is, dus of de reis in kwestie minder dan vier deelnemers heeft. Voor het reisnummer van de ‘reis in kwestie’ kunt u de expressie new.reis gebruiken. (Het sleutelwoord new wordt behandeld in hoofdstuk 15 ‘Triggers en stored procedures’.)

11.7 Domeinen In paragraaf 1.3.5 werd het principe van ‘single point of definition’ geïntroduceerd: het principe dat alle informatie op één plek, dus vrij van redundantie, moet worden opgeslagen. Hoewel dit principe lang niet altijd geldt (historische gegevens in een datawarehouse bijvoorbeeld worden vaak in een redundante structuur opgeslagen; ze veranderen toch niet meer), is het in een transactionele omgeving doorgaans een leidend uitgangspunt. In zo’n omgeving moet immers voortdurend (bij inserts, deletes en updates) de integriteit van de database worden bewaakt. Ook de ontwikkelomgeving van een database- en applicatieontwikkelaar is transactioneel. Immers, zolang de ontwikkelaar bezig is, verandert er voortdurend van alles aan de structuren. Die structuren, met name van tabellen, bevatten veelal redundante specificaties. We zullen laten zien hoe domeinen kunnen helpen die redundantie terug te dringen.

11.7.1 Wat zijn domeinen? Reisnummers van ruimtereizen zijn positieve, gehele getallen. Binnen de database zijn ze gespecificeerd door het datatype integer . Er zijn maar liefst drie kolommen met reisnummers, een primaire-sleutelkolom en twee verwijssleutelkolommen. Het datatype geldt voor alle drie en moet dus drievoudig worden opgenomen: een voorbeeld van redundantie op systeemniveau.

Een domein is een databaseobject dat datatypespecificaties (eventueel inclusief een beperkingsregel) bewaart, onder een betekenisvolle naam. Meerdere kolommen kunnen op één domein worden gespecificeerd. Voldoet het datatype (of een beperkingsregel) niet langer, dan hoeft het maar op één plek te worden aangepast: in de domeindefinitie. Een nieuw voorbeeld van ‘single point of definition’. Domeinen worden ook wel logische datatypen genoemd. Ze maken onderdeel uit van de SQL -standaard. Domeinen worden ondersteund door Firebird, maar lang niet door alle andere bekende rdbms’en. Om deze reden hebben we de kolomspecificaties van de meeste voorbeelddatabases onafhankelijk van domeinen gehouden. Niettemin vinden we het domeinconcept zó belangrijk dat we voor het Ruimtereisbureau ook een implementatiemodel met domeinen hebben gemaakt.

11.7.2 Creëren van domeinen Het commando om een domein te creëren, luidt – niet verrassend – create domain . In een tweede implementatiemodel, RuimtereisbureauD, bij het logische Ruimtereisbureau-model hebben we alle kolommen gebaseerd op domeinen, zoals we kunnen zien in het DDL -script RuimtereisbureauDCreate.sql. Hier volgt een deel ervan (bekijk de rest van het script via de Boekverkenner). create domain Transportcode         as create domain Transportomschrijving as create domain Reisnr                as check (value > 0); create domain Objectnaam            as create domain Aantal                as check (value > 0); create domain Geldbedrag            as

varchar(2); varchar(12); integer varchar(10); integer numeric(5,2);

create table Transport (code           Transportcode          not null, omschrijving   Transportomschrijving  not null, constraint pk_transport primary key (code), constraint un_transportomschrijving unique (omschrijving) ); create table Reis (nr             Reisnr         not null, vertrekdatum   Datum          not null, transport      Transportcode  not null, duur           Aantal, prijs          Geldbedrag, constraint pk_reis primary key (nr), constraint fk_reis_met_transport foreign key (transport) references Transport(code) on update cascade );

Toelichting – Een create domain -statement bevat de domeinnaam en een fysiek datatype. – In de create table -statements zijn de datatypen van RuimtereisbureauCreate.sql vervangen door domeinnamen. – Domeinen zijn per definitie algemener dan kolommen. Dat blijkt ook uit de naamgeving: de domeinnaam Aantal is heel algemeen en drukt uit wat de veel specifiekere kolomnamen Reis.duur en Bezoek.verblijfsduur (niet in dit fragment) gemeenschappelijk hebben. – Een primaire-sleutelkolom en de corresponderende verwijssleutelkolommen moeten op hetzelfde domein zijn gebaseerd. – Aan de domeinen Reisnr en Aantal is een check -constraint gekoppeld; in paragraaf 11.7.3 gaan we hier op in. Het is niet verplicht elke kolom op een domein te baseren, maar het is wel aan te bevelen, zelfs wanneer bij een kolom die de enige kolom wordt bij een domein. Argumenten hiervoor zijn: – Domeinen hebben, door hun naam, meer semantiek dan alleen een datatype. – Aan domeinen kunnen we check -constraints koppelen, wat soms een eenvoudige manier is om een regel te implementeren. – Afwisseling van kolomspecificaties met en zonder domeinen geeft rommelige code. In een create domain -statement kunnen ook een not null en een default -specificatie worden opgenomen. Als u dat doet, moet u wel heel zeker weten dat die specificaties echte domeinkenmerken zijn en altijd zullen gelden voor alle kolommen met dat domein.

11.7.3 Check-constraints op domeinen We kunnen de waarden van een domein binden aan een conditie, via een check -constraint. Omdat bij één domein meerdere kolommen kunnen horen, kan in zo’n check -conditie geen kolomnaam worden gebruikt. Voor de kolomwaarde waarop de constraint betrekking heeft, wordt daarom een apart gereserveerd woord gebruikt: value . Zo zien we: create domain Reisnr as integer check (value > 0)

Domeinen vormen het ‘single point of definition’ voor bepaalde kolomkenmerken. Het aan te bevelen om check -constraints indien mogelijk niet op kolomniveau te definiëren, maar op domeinniveau. In de praktijk zal het vaak gaan om waardebeperkingen.

Opgave 11.10 Test de check -constraint op domein Reisnr: probeer een nieuwe reis in te voeren met een niet-positief reisnummer.

11.7.4 Verwijderen van domeinen en deltaproblemen Verwijderen van een domein gaat niet zomaar. Bij een domein waar nog een of meer kolommen op zijn gebaseerd, lukt het niet. Dit doet denken aan het probleem van referentiële integriteit bij verwijssleutels. En het is ook precies zo’n probleem, alleen op het niveau van de systeemtabellen. (Vanuit de systeemtabel met kolomspecificaties wordt verwezen naar de systeemtabel met domeinspecificaties.) Een ‘ongebruikt’ domein echter laat zich eenvoudig verwijderen via een drop domain -statement. Voor veranderingen in een domeindefinitie kent Firebird het commando alter domain . Hiermee is het mogelijk een domeinnaam of een defaultwaarde te wijzigen. Raadpleeg de documentatie voor voorbeelden.

11.8 Views In hoofdstuk 9 ‘Subselects en views’ zijn views geïntroduceerd: relationele expressies in de vorm van een select -statement die onder een eigen naam worden bewaard. Views krijgen pas een waarde op het moment dat

ze worden gebruikt. Ze gedragen zich in veel opzichten als tabellen en worden wel ‘virtuele tabellen’ genoemd. Omdat we in dit hoofdstuk alle typen dataobject de revue laten passeren, zullen we ook de levenscyclus van een view hier met een voorbeeld illustreren.

11.8.1 Creëren van views Een view wordt gecreëerd ‘als een select -query’. Dit is letterlijk terug te vinden in de syntaxis van het volgende create view -statement, waarin een view wordt gecreëerd voor een eenvoudig telprobleem: het aantal deelnemers per ruimtereis. create view vReis1 (nr, aantal_deelnemers) as select   R.nr, count(D.reis) from     Reis R left outer join Deelname D on R.nr = D.reis group by R.nr

Bevragen we deze view met een select *-query: select * from   vReis1

dan is dit het resultaat: NR AANTAL_DEELNEMERS ========= ================= 31                 4 32                 3 33                 3 34                 3 35                 0 36                 0 37                 0

Opmerking : de select -expressie in een viewdefinitie mag een order by -clausule bevatten.

11.8.2 Gebruik van views Views gedragen zich in veel opzichten als gewone tabellen. Net als deze kunnen ze worden gejoind. We gebruiken dat in het volgende voorbeeld, ontleend aan het Ruimtereisbureau, waarin twee views elk de oplossing van een deelprobleem voor hun rekening nemen.

Voorbeeld 11.6 Join van twee views, die elk een deelprobleem oplossen Geef een overzicht van alle ruimtereizen, met reisnummer, vertrekdatum, prijs, aantal deelnemers en aantal bezoeken aan hemelobjecten. Het gevraagde overzicht bevat twee statistische kenmerken. Het eerste, het aantal deelnemers, halen we uit de view vReis1 van de vorige paragraaf. Het tweede, het aantal bezoeken aan hemelobjecten, halen we uit een tweede view: create view vReis2 (nr, aantal_bezoeken) as select     R.nr, count(B.volgnr) from       Reis R left outer join Bezoek B on R.nr = B.reis group by R.nr

We combineren nu de tabel Reis en beide views in een join: select R.nr, R.vertrekdatum, R.prijs, vR1.aantal_deelnemers, vR2.aantal_bezoeken from   Reis R join vReis1 vR1 on R.nr = vR1.nr join vReis2 vR2 on R.nr = vR2.nr

Resultaat: NR VERTREKDATUM        PRIJS AANTAL_DEELNEMERS AANTAL_BEZOEKEN ========= ============ ============ ================= =============== 31 12-jan-2022          2.50                 4               1 32 03-jun-2022         17.50                 3               5 33 12-okt-2022          2.65                 3               1 34 10-jan-2023         75.00                 3               3 35 12-mrt-2023         16.50                 0               3 36 27-jun-2023                         0               3 37 17-jul-2023         60.00                 0               1

Overigens kunt u ook een view maken met beide aantallen (hoe?), zodat u maar één view nodig heeft. En dan kunt u de view ook helemaal weglaten… Het ging er hier alleen maar om te laten zien dat u views kunt joinen als tabellen.

11.8.3 Verwijderen van views en deltaproblemen Verwijderen van view Een view bestaat slechts als querytekst. Bij het verwijderen ervan hoeven we dus niet beducht te zijn voor integriteitsproblemen zoals bij fysiek bestaande databasetabellen. De views vReis1 en vReis2 worden verwijderd met de volgende drop view -statements: drop view vReis1; drop view vReis2

Een view kan echter betrokken zijn bij afhankelijkheden, waardoor hij niet zomaar verwijderd kan worden. Denk aan gebruikers die rechten hebben op een view of aan andere views die een bepaalde view gebruiken. Die afhankelijkheden zijn niets anders dan ouder-kindrelaties in de data dictionary met de view in de rol van ouder. Pas als de kinderen (gebruikersrechten op de view, de andere views die de view gebruiken) zijn verwijderd, zal het lukken de view te verwijderen. Wijzigen van view Voor wijzigen van een view is er het alter view -commando. De syntaxis is gelijk aan die van het create view -commando. Dit is korter dan verwijderen ( drop view ) en opnieuw aanmaken ( create view .). Het grote voordeel is echter dat bestaande afhankelijkheden gewoon gehandhaafd blijven.

11.9 Sequences ‘Dingen’ worden in de werkelijkheid vaak geïdentificeerd door lange namen of complexe, samengestelde kenmerken. Hoewel deze kenmerken in een relationele database als primaire sleutel kunnen worden gebruikt, is het vaak beter hiervoor kunstmatige nummers of codes te nemen.

11.9.1 Kunstmatige sleutels Datatype van kunstmatige sleutels Een primaire-sleutelwaarde kan in een database vele keren voorkomen als verwijssleutelwaarde. Zijn het lange strings, dan kost dat heel wat geheugenruimte. Door over te gaan op korte volgnummers of codes, kan daarop worden bespaard. Nog belangrijker is dat zoekacties op kortere kenmerken (zoals onder meer plaatsvindt bij joinen) veel sneller verlopen dan zoekacties op lange strings. Strings moeten immers teken voor teken worden vergeleken. Dit snelheidsvoordeel is het grootst bij zoeken op numerieke waarden, vandaar dat kunstmatige sleutels bij voorkeur numeriek worden gekozen. Kunstmatige sleutelwaarden: deel van de werkelijkheid? In veel gevallen worden kunstmatige sleutelwaarden zichtbaar afgedrukt in de gebruikersapplicatie of op formulieren en rapporten. Ze zijn dan deel gaan uitmaken van de werkelijkheid. Denk aan een klantnummer dat aan de klant zelf bekend is, of aan een ordernummer dat zichtbaar op een orderformulier wordt afgedrukt. Een vervelender voorbeeld: de gele oorflappen van onze runderen. De automatiseerder heeft in al die gevallen in de werkelijkheid ingegrepen. In andere gevallen blijven de kunstmatige sleutelwaarden onzichtbaar voor gebruikers. Alleen het rdbms gebruikt ze op de achtergrond, met als

enig doel: optimalisatie van geheugengebruik en snelheid. De ontwikkelaar moet zich van dit verschil terdege bewust zijn. In het geval van een ‘zuiver kunstmatige sleutel’ moet de ontwikkelaar ervoor zorgen dat de applicatie een voor de gebruiker betekenisvolle, alternatieve sleutel toont. Sequences Een nieuwe kunstmatige sleutelwaarde is alleen nodig voor een enkelvoudige primaire sleutel en maar in twee gevallen: bij een insert van een nieuwe rij of een update van een primaire sleutel. Vrijwel elk rdbms biedt de faciliteit om numerieke volgnummers automatisch te genereren. Hiervoor kunnen ‘intelligente’ nummergeneratoren worden gecreëerd, sequences genaamd.

11.9.2 Creëren en gebruiken van sequences Een sequence wordt in Firebird gecreëerd met create sequence . We zullen er eerst een creëren voor testdoeleinden. We noemen deze sTest: create sequence sTest

De ‘huidige waarde’, van waaruit het eerst af te geven volgnummer wordt berekend, is initieel gelijk aan 0. Een nieuwe waarde, die 1 hoger is, wordt geretourneerd door de functieaanroep next value for sTest . Hoewel next value for … meestal vanuit een insert -statement wordt aangeroepen – om een nieuwe primaire-sleutelwaarde te retourneren – kan zij worden geactiveerd vanuit elk type SQL -statement. Zo wordt de sequence ook geactiveerd door het volgende select-statement: select next value for sTest from   Rdb$database

(We misbruiken hier de systeemtabel Rdb$database weer als ‘kladbloktabel’.) Resultaat: GEN_ID ========= 1

( GEN_ID refereert nog aan generator , de oude Firebird-naam voor sequence.) Voeren we hetzelfde statement nogmaals uit, dan is de resultaatwaarde 2, enzovoort. Met het commando alter sequence … restart with … kunnen we een sequence ‘resetten’ met een nieuwe startwaarde. Bijvoorbeeld: alter sequence sTest restart with 0

Hiermee wordt sTest gereset op startwaarde 0. Bij set sequence moet worden gezorgd dat de daarna geretourneerde waarden, indien gebruikt voor primaire-sleutelwaarden, nog niet of niet meer in gebruik zijn. Tot nu toe gingen alle gegenereerde waarden verloren. In het volgende voorbeeld wordt een sequence gebruikt waarvoor hij is bedoeld: om automatisch primaire-sleutelwaarden te genereren via een insert statement. Voorbeeld 11.7 Sequence voor klantnummers Als volgt creëren we een sequence, genaamd sKlantnr, voor het automatisch genereren van nieuwe klantvolgnummers voor ons Ruimtereisbureau. Als startwaarde kiezen we 1000, een waarde die hoger is dan het huidige hoogste klantnummer. create sequence sKlantnr; alter sequence sKlantnr restart with 1000

We voegen een nieuwe klant toe: insert into Klant values (next value for sKlantnr, 'Michèle', '14-feb-1989')

De nieuwe klant heeft nu klantnummer 1001 gekregen. Volgende klanten, indien ingevoegd met vergelijkbare commando’s, krijgen volgnummers 1002, 1003, enzovoort. Het is ook mogelijk alle bestaande klantnummers netjes vanaf een beginwaarde te laten lopen, bijvoorbeeld vanaf 1: alter sequence sKlantnr restart with 0; update Klant set    nr = next value for sKlantnr

Dat dit technisch goed gaat, is te danken aan twee dingen. – het feit dat, bij de huidige populatie, alle benodigde klantnummers ‘vrij’ zijn. Zijn ze dat niet, dan moet een trucje zoals in opgave 10.7 worden toegepast. – de optie on update cascade van de verwijssleutel fk_deelname_door_klant, waardoor de bestaande verwijzingen in Deelname automatisch worden bijgewerkt.

Sequences en auto_increment

Sommige databases, zoals MySQL en MsAccess, kunnen een auto_increment koppelen aan een datatype in een create -statement, bijvoorbeeld: create table Test (nr integer auto_increment not null, primary key (nr) ) Deze mix van datatypering en gedrag lijkt handig maar is vaak een bron van problemen. Een apart sequence-object voor een increment is een zuiverder oplossing, die de programmeur volledige controle geeft.

11.9.3 Verwijderen van sequences en deltaproblemen Verwijderen van een sequence gaat met drop sequence . De enige echte deltaproblemen voor sequences zijn het wijzigen van de initiële waarde en van de naam. Wijzigen van de initiële waarde gaat, zoals we al zagen, met alter sequence … restart with … . Voor het wijzigen van de naam zou een alter sequence … rename to … commando welkom zijn. De oplossing is echter: de sequence verwijderen en opnieuw aanmaken.

Opgave 11.11 a Creëer een sequence sReisnr voor reisnummers. Zorg dat het eerst gecreëerde nummer gelijk is aan 101. b Geef de ruimtereizen van de huidige populatie nieuwe reisnummers: 101, 102, 103, ... Controleer of het gelukt is, in alle betrokken tabellen.

Oefenopgaven Opgaven 11.12 en 11.13 zijn gebaseerd op Orderdatabase, opgave 11.14 op OrderdatabaseD. Voor de diagrammen: zie Boekverkenner. De voorbeeldpopulatie vindt u ook in bijlage 4.

Opgave 11.12 We willen, in Orderdatabase, de volgende regel afdwingen: de verkoopprijs van een artikel moet groter zijn dan de inkoopprijs. Implementeer deze regel met een check -constraint. (Misschien niet zo’n verstandige regel: hoe moet het nu met de uitverkoop?)

Opgave 11.13 a Om een constraint te verwijderen, moet u zijn naam kennen. In het script OrderdatabaseCreate.sql hebben de constraints echter geen naam gekregen. Daardoor hebben ze een naam gekregen van het rdbms zelf. Die namen kunt u opzoeken in de data dictionary (het onderwerp van hoofdstuk 16 ), maar u kunt ze veel eenvoudiger achterhalen. Hoe? b Achterhaal de naam van de verwijssleutel van Artikel naar Artikelgroep en verwijder de betreffende foreign key -constraint. c Controleer of het gelukt is en voeg de constraint weer toe.

Opgave 11.14 Ga uit van OrderdatabaseC (de versie met constraintnamen). Ter voorbereiding van een fusie moet een structuurwijziging worden doorgevoerd. Zie figuur 11.4 voor het strokendiagram van de oude situatie, die correspondeert met de installatieversie vanuit de Boekverkenner. image Figuur 11.4 Bestaande structuur OrderdatabaseC (fragment)

De volgende veranderingen moeten worden aangebracht: a Tabel Klant moet Client gaan heten en naam wordt achternaam. b Client krijgt extra kolommen voor voorletters en (optioneel) voorvoegsel. Zie figuur 11.5 voor het strokendiagram van de nieuwe situatie. image Figuur 11.5 OrderdatabaseC na structuurwijziging

Aanwijzingen

– Voorafgaand aan elk drop table - of alter table ... drop -statement moet de daaraan voorafgaande transactie gecommit worden. Geef op die momenten ook een reconnect , om er zeker van te zijn dat alle ‘locks’ op de systeemtabellen zijn vrijgegeven. – Technische problemen zijn bij dit soort structuurwijzigingen meer regel dan uitzondering. Schroom niet de uitwerking te raadplegen. Werkwijze 1 Analyseer knelpunten. 2 Stel een stappenplan op. 3 Voer dit stappenplan uit.

12 Autorisatie Databasegegevens zijn automatisch beveiligd tegen gebruik of misbruik door anderen dan de database-eigenaar. Elke geregistreerde gebruiker kan in Firebird op elke database inloggen, maar komt daarna niet verder. Om iets zinvols te kunnen doen, moet de gebruiker van de databaseeigenaar rechten ontvangen. We spreken van autorisatie . De ene gebruiker (meestal de database-eigenaar) kan de andere gebruiker autoriseren om bepaalde taken te verrichten. Natuurlijk moet de eerste gebruiker zelf daartoe geautoriseerd zijn! In dit hoofdstuk verdiepen we ons in gebruikers en hun mogelijke rechten op de objecten van een database. Daarbij maken we kennis met nieuwe gebruiksmogelijkheden van views. Ook wordt een nieuw type databaseobject behandeld: de rol . Voorbeelddatabase Ruimtereisbureau Reijnders zal ook in dit hoofdstuk weer zijn diensten bewijzen. Zie figuur 12.1 voor het strokendiagram en bijlage 4 voor de voorbeeldpopulatie. We gebruiken de versie zonder domeinen, genaamd ‘Ruimtereisbureau’.

Figuur 12.1 Ruimtereisbureau: strokendiagram

12.1 Gebruikers Een gebruiker (Engels: user ) is eigenlijk niet meer dan een gebruikersnaam ( user name ), waaraan een aantal eigenschappen is opgehangen, waaronder een wachtwoord ( password ). Wanneer meer personen dezelfde gebruikersnaam delen, zijn ze voor de database dezelfde gebruiker.

12.1.1 Een ‘supergebruiker’: de DBA Elke databaseomgeving kent de functie database administrator ( DBA ). Een DBA is kortweg verantwoordelijk voor het blijven draaien van het systeem en het niet verloren gaan van gegevens. Taken van de DBA

Belangrijke taken van de DBA zijn: – gebruikersbeheer: aanmaken of verwijderen van gebruikers – het praktisch realiseren van autorisatie (rechtentoekenning aan gebruikers) – voorkomen dat gegevens blijvend verloren gaan – optimaliseren van de performance en het geheugengebruik – in samenwerking met systeembeheer: een juiste afstemming van software en hardware. Bij ‘gebruikers’ moet hierbij altijd worden gedacht aan de gebruikersnaam-wachtwoord-combinatie waarmee een applicatie zich meldt bij het rdbms. Niet altijd wordt deze communicatie direct geïnitieerd door een gebruiker van vlees en bloed. De DBA als ‘supergebruiker’ De DBA -functie wordt uitgeoefend door één of meer gebruikers met DBA rechten. Door een specifieke gebruikersnaam met wachtwoord, met daaraan gekoppelde privileges, is een DBA in staat te opereren als een soort ‘supergebruiker’. In Firebird is Sysdba zo’n supergebruiker. Deze bestaat direct na het installeren, met wachtwoord ‘masterkey’ (in kleine letters). We hebben het wachtwoord ‘masterkey’ onveranderd gelaten, maar in de praktijk hoort het direct na installatie in een geheim wachtwoord te worden gewijzigd.

12.1.2 Database-user-structuur De manier waarop databases en gebruikers zich tot elkaar verhouden, noemen we de database-user-structuur . In deze paragraaf kijken we eerst naar de database-user-structuur van Firebird en vervolgens naar die van Oracle. Database-user-structuur van Firebird

Firebird houdt gegevens over gebruikers bij in een eigen ‘securitydatabase’. Deze heet Security2.fdb en is te vinden in de Firebird programmamap. Deze is eenvoudig te vinden via Relationele databases en SQL in het Startmenu (map C:\...\ R el SQL \Firebird). Om veiligheidsredenen kan de security-database niet rechtstreeks maar alleen via een andere database worden benaderd. Zo wordt voorkomen dat door kwaadwillenden met brute rekenkracht uit de password hashes (versleutelde wachtwoorden) de wachtwoorden worden achterhaald. Om toch wat inzicht in de security-database te kunnen geven, hebben we een kopie meegeleverd als voorbeelddatabase, die wel kan worden benaderd. De naam van de eigenaar is zoals steeds gelijk gekozen aan de databasenaam en luidt dus Security2. We loggen in als eigenaar: connect 'Security2.fdb' user 'Security2' password 'pw'

De belangrijkste tabel heet Rdb$users. We kijken eens welke gebruikers er in zitten en wat hun wachtwoorden zijn: select rdb$user_name, rdb$passwd from   Rdb$users

Het resultaat is, afgezien van nog extra gebruikers, zoiets als: RDB$USER_NAME RDB$PASSWD ============= ======================================== SYSDBA        NLtwcs9LrxLMOYhG0uGM9i6KS7mf3QAKvFVpmRg= TOETJESBOEK   JobKgcr6vUBhm71uwZwSm9LbhQUrsKgUVQmM7SM=

We zien, naast de namen van de eigenaars van de voorbeelddatabases, ook (in hoofdletters) de naam Sysdba, die van de Firebird-supergebruiker ( DBA ). U ziet dat Firebird gebruikersnamen intern opslaat in hoofdletters. De getoonde ‘wachtwoorden’ zijn natuurlijk niet de echte, wat u ziet is een versleuteling. Met een select * -commando kunt u desgewenst kijken welke gebruikerskenmerken er in de andere kolommen worden opgeslagen. Zoals we hebben gezien, kan een Firebird-gebruiker eigen databases aanmaken. In dit boek is dat steeds tot één beperkt gebleven, maar in principe kunnen het er meer zijn. De gebruiker die een database creëert, heet de eigenaar van de database en van de tabellen en andere objecten daarvan. De database-eigenaar heeft alle rechten op de objecten, ook het recht om anderen op hun beurt rechten te verlenen. Daar zullen we in dit hoofdstuk de nodige voorbeelden van zien. Opmerking : Firebird heeft de eigenaardigheid dat een databasebestand geen autorisatie heeft. Indien je een database kopieert naar een andere omgeving en daar is een security-database met dezelfde gebruikersnaam, dan geldt ineens het daarbij horende wachtwoord. Het is te verwachten dat in een volgende Firebird-versie databaseautorisatie plaats zal vinden. Database-user-structuur van Oracle

Ter illustratie van een hele andere database-user-structuur, zullen we schetsen hoe in Oracle de verhouding tussen gebruikers en databases is geregeld. Daar horen gebruikers tot dezelfde database als de door hen gecreëerde objecten. Terwijl Firebird na installatie maar één supergebruiker heeft (Sysdba), onafhankelijk van enige database, zijn er in Oracle meerdere supergebruikers per database, die tegelijk met de database worden gecreëerd. Het meest bekend is de supergebruiker System, met initieel wachtwoord ‘manager’. Er is wel eens een onderzoek geweest bij grote bedrijven waarbij gekeken werd naar het wachtwoord van de systeemgebruiker van de database, zoals ‘System’ bij Oracle. In een zéér groot aantal gevallen bleek dit het initiële wachtwoord te zijn (zoals ‘manager’ bij System). Geen DBA die de moeite had genomen dit initiële wachtwoord te wijzigen! In Oracle bezitten gebruikers geen enkel recht, tenzij ze (zoals System) als supergebruiker zijn geboren of zo’n recht expliciet hebben verkregen van een gebruiker die het recht heeft dat recht te verlenen! Gelukkig is het zo dat wie het recht heeft eigen tabellen te maken, als eigenaar van die tabellen er alles mee mag doen en ook anderen daar rechten op mag geven.

12.1.3 Gebruikersbeheer Benaderen van de security-database In Firebird heeft geen enkele gebruiker rechtstreekse SQL -toegang tot de security-database. Om veiligheidredenen kan dat alleen indirect, via een andere, gewone database of via een speciale tool. Dat geldt voor gebruiker Sysdba, maar ook voor andere gebruikers die DBA -rechten hebben ontvangen. De benodigde DBA -rechten zijn tweeërlei: DBA -rechten op de securitydatabase zelf én DBA -rechten op die andere database. Door ‘met de rol DBA ’ in te loggen op die tweede database kan de gebruiker dan DBA taken uitvoeren, zoals het aanmaken van nieuwe gebruikers. In deze paragraaf illustreren we gebruikersbeheer aan de hand van gebruiker Sysdba, omdat die al over alle noodzakelijke rechten beschikt. In paragraaf 12.4 ‘Rollen’ laten we zien hoe ook andere gebruikers het recht kunnen krijgen om DBA -taken uit te voeren. Gebruikers aanmaken Gebruikers aanmaken gaat met het commando create user . We laten gebruiker Sysdba nu enkele nieuwe gebruikers aanmaken, die we verderop nodig hebben. Hiertoe laten we Sysdba inloggen op een willekeurige database, waarvoor we Ruimtereisbureau kiezen. Vervolgens maakt Sysdba vijf gebruikers aan voor gelijknamige medewerkers, waarbij we weer kiezen voor het niet-realistische maar simpele wachtwoord ‘pw’: connect 'Ruimtereisbureau.fdb' user 'Sysdba' password 'masterkey'

create create create create create

user user user user user

Tom password 'pw'; Luc password 'pw'; Sofie password 'pw'; Jip password 'pw'; Lisa password 'pw'

Gebruikers wijzigen Via de clausule password hebben de nieuwe gebruikers een simpel wachtwoord gekregen: ‘pw’. Het ligt voor de hand dat een gebruiker het eigen wachtwoord wil kunnen veranderen. Het commando hiervoor luidt, zoals te verwachten: alter user . connect 'Ruimtereisbureau.fdb' user 'Tom' password 'pw'; alter user Tom password 'blwp'

Tom’ wachtwoord is nu veranderd in ‘blwp’. Bij een volgende log-in zal Tom dit nieuwe wachtwoord moeten gebruiken. Ter illustratie laten we hem direct een nieuwe databasesessie starten: connect 'Ruimtereisbureau.fdb' user 'Tom' password 'blwp'

Dit gaat goed, al kan Tom hierna nog niets doen. Hij heeft namelijk geen enkel ‘objectprivilege’, zelfs niet het leesrecht op tabellen. In de volgende paragraaf zien we hoe we gebruikers objectprivileges kunnen geven. Gebruikers verwijderen Wanneer Tom ontslag neemt of krijgt, zal hij ook als gebruiker worden verwijderd. Dit is uiteraard een DBA -verantwoordelijkheid. Ingelogd als Sysdba, kunt u Tom verwijderen met een drop user -commando: connect 'Ruimtereisbureau.fdb' user 'Sysdba' password 'masterkey'; drop user Tom

Opgave 12.1 Maak de vijf users aan uit de voorgaande tekst. Experimenteer met het wijzigen van het eigen wachtwoord door een gebruiker.

12.2 Privileges De technische term voor databaserechten is: privileges . Ze zijn er in twee soorten: systeemprivileges en objectprivileges.

12.2.1 Systeemprivileges Systeemprivileges hebben onder meer betrekking op het mogen starten van databasesessies, op het zelf mogen creëren van databaseobjecten en op DBA -taken. Dit soort privileges is doorgaans dialectspecifiek geregeld. Firebird kent een eenvoudig systeem van systeemprivileges: in paragraaf 12.1 hebben we er al het een en ander over geleerd. Zo hebben we gezien dat elke gebruiker databases mag aanmaken en daar als eigenaar de volledige zeggenschap over heeft. Ook zagen we dat er een oppermachtige supergebruiker Sysdba is, die rechten heeft op alle databases. We zullen nu zien hoe we een gewone gebruiker DBA -rechten kunnen geven. Als voorbeeld nemen we gebruiker Tom, die we DBA -rechten gaan geven, waardoor hij zelf bijvoorbeeld nieuwe gebruikers mag aanmaken. Dit doen ‘we’, in de rol van Sysdba, als volgt: connect 'Ruimtereisbureau.fdb' user 'Sysdba' password 'masterkey'; alter user Tom grant Admin role; grant Rdb$admin to Tom

Toelichting – Door het alter user -commando met de keywords grant Admin role krijgt Tom het recht de security-database te benaderen. Dit recht staat los van de database waarmee Tom inlogt. – Net als Sysdba kan Tom de security-database alleen benaderen via een andere, gewone database. Daarvoor moet hij op een speciale manier op die database inloggen, namelijk ‘met de rol administrator’. Het grant commando kent Tom die rol toe. Een rol is een soort privilege en is gebonden aan één database (Ruimtereisbureau in dit geval). In paragra af 12.4 gaan we uitgebreid op rollen in. Hierna kan Tom ‘met de rol Rdb$admin’ inloggen op een database (we kiezen weer Ruimtereisbureau) en zelf nieuwe gebruikers aanmaken. Bijvoorbeeld: connect 'Ruimtereisbureau.fdb' user 'Tom' password 'pw' role ‘Rdb$admin’; create user Test1 password 'pw'

Opgave 12.2 Test het voorgaande uit:

a Geef, als Sysdba, aan gebruiker Tom DBA -rechten. b Creëer, als gebruiker Tom, een nieuwe gebruiker Test1. c Controleer of het gelukt is door in te loggen als Test1. d Verwijder, als gebruiker Tom, gebruiker Test1. De Firebird-utility gsec Firebird heeft een command line tool genaamd gsec voor gebruikersbeheer, waarmee u als Sysdba de securitydatabase kunt benaderen, voor o.a. het opvragen van een gebruikersoverzicht en het aanmaken of wijzigen van gebruikers. U kunt gsec openen vanaf een command prompt (in een DOSvenster), als volgt: gsec -user sysdba -pass masterkey Een overzicht van gebruikers krijgt u met het commando ‘display’. Voor verdere mogelijkheden: raadpleeg de reference manual op http:// www.firebirdsql.org/en/documentation/ .

12.2.2 Objectprivileges Objectprivileges hebben te maken met databaseobjecten, zoals tabellen en views. Alleen de database-eigenaar (die het create database -commando heeft gegeven) is in eerste instantie gerechtigd iets met deze objecten te doen. De eigenaar kan anderen echter rechten verlenen, bijvoorbeeld om objecten in te zien ( select -privilege) of om ze te veranderen ( insert -, delete - of update -privilege). Er zijn zeven verschillende objectprivileges, zie tabel 12.1 . Tabel 12.1 Objectprivileges

Het references -privilege heeft alleen betrekking op tabellen. De belangrijkste toepassing betreft verwijssleutels: wanneer een tabeleigenaar aan een andere gebruiker het references -privilege op een tabel verleent, heeft deze daarmee het recht verkregen foreign key constraints aan te maken die naar die tabel te verwijzen. Dit privilege is een zwakke vorm van het select -privilege. Eigenlijk zegt het: ‘u mag wel in de doeltabel kijken of de foreign key er in zit, maar verder niet lezen’. Gevolg is dat het select -privilege het references -privilege impliceert. Het references -privilege kan worden ingeperkt tot een of meer kolommen die een sleutel vormen. De select -, insert -, delete -, update - en all -privileges gelden alle voor zowel tabellen als views. Voor insert , delete en update is dat opmerkelijk: een view is toch niet meer dan een select -expressie met een eigen naam? In paragraaf 12.3.2 zal blijken dat het met DML opdrachten op updatable views wel degelijk mogelijk is tabelinhouden te wijzigen. Overigens blijkt dan ook een references -privilege op de onderliggende tabel nodig te zijn. Hoewel alle privileges onafhankelijk van elkaar kunnen worden verleend, blijken we een update - of delete -privilege alleen te kunnen uitoefenen wanneer we ook het corresponderende select -privilege bezitten. Execute-privilege Het execute -privilege heeft betrekking op stored procedures. Net als een gebruiker kan een stored procedure databaseobjecten benaderen. Een gebruiker die een stored procedure aanroept moet daarom wel een speciaal recht daartoe hebben verkregen, tenzij hij zelf de eigenaar is. Rechten op stored procedures valt buiten het bestek van dit boek.

12.2.3 Privileges verlenen Het SQL -commando om andere gebruikers objectprivileges te verlenen, luidt grant . Dit commando en het revoke -commando van de volgende paragraaf zijn in IQU gekoppeld aan een commitmoment: de lopende transactie wordt ermee afgesloten. Met één grant -commando kunnen meerdere rechten worden verleend aan meerdere gebruikers, echter op slechts één object. In de volgende voorbeelden gaan we ervan uit dat u als gebruiker Ruimtereisbureau bent ingelogd op Ruimtereisbureau.fdb en dat er vijf gebruikers Tom, Luc, Sofie, Jip en Lisa zijn aangemaakt, zie opgave 12.1 . Voorbeeld 12.1 Geef Tom leesrecht op Reis, Hemelobject en Bezoek. Oplossing: grant select on Reis to Tom; grant select on Hemelobject to Tom; grant select on Bezoek to Tom

Hier zijn drie commando’s nodig, omdat het om drie verschillende objecten gaat. Voorbeeld 12.2 Geef Luc leesrecht op Deelname en het recht er rijen aan toe te voegen. Oplossing: grant select, insert on Deelname to Luc

De beide privileges op Deelname kunnen worden toegekend met één commando, omdat het om één object gaat. Voorbeeld 12.3 Geef Tom en Sofie het recht te lezen uit de tabel Klant en er rijen aan toe te voegen. Oplossing: grant select, insert on Klant to Tom, Sofie

We zien: meerdere rechten aan meerdere gebruikers. Maar omdat het om één object gaat, kan het via één commando.

Voorbeeld 12.4 Geef Sofie het recht alle kolommen van Klant en de kolommen duur en prijs van Reis te wijzigen. Oplossing: grant update on Klant to Sofie; grant update(duur, prijs) on Reis to Sofie

We zien: met alleen update verleent u het update -recht op alle kolommen. Wilt u het recht beperken tot specifieke kolommen, dan moet u een kolommenlijst tussen haakjes toevoegen. Bij voorbeeld 12.4 kunnen we ons afvragen wat er gebeurt wanneer Sofie van haar updateprivilege op Klant gebruikmaakt om te proberen een primaire-sleutelwaarde (nr) te wijzigen. De verwijssleutel fk_deelname_door_klant van Deelname naar Klant is immers onderhevig aan een cascading update, waardoor de poging gevolgd wordt door een poging ook de klantnummers in Deelname te wijzigen. Sofie heeft echter geen updaterecht op Deelname. Een test wijst uit dat een cascading update alleen rechten op de oudertabel vereist. Sofie kan dus haar gang gaan. Voorbeeld 12.5 Geef Sofie ‘alle’ rechten (zie tabel 12.1 ) op Hemelobject. Oplossing: grant all on Hemelobject to Sofie

Dit commando is equivalent met: grant select, insert, update, delete, references on Hemelobject to Sofie

De genoemde privileges kunnen afzonderlijk worden teruggenomen.

12.2.4 Privileges terugnemen Alleen degene die een privilege heeft verleend, is gerechtigd dat privilege weer terug te nemen. Het commando hiervoor luidt: revoke . De syntaxis komt vrijwel overeen met die van grant , zij het dat to plaats maakt voor from . Voorbeeld 12.6 Ontneem Tom het leesrecht op Reis, Hemelobject en Bezoek. Oplossing:

revoke select on Reis from Tom; revoke select on Hemelobject from Tom; revoke select on Bezoek from Tom

Voorbeeld 12.7 Ontneem Sofie het update - en delete -recht (zie tabel 12.1 ) op Hemelobject, dat zij heeft verkregen via een ‘ all -grant’. Oplossing: revoke update, delete on Hemelobject from Sofie

12.2.5 Privileges verlenen aan alle gebruikers Er is een speciale gebruiker, genaamd public . Deze is in het leven geroepen om ‘iedereen’, alle huidige en bij voorbaat ook alle toekomstige gebruikers, bepaalde privileges te geven. public is eigenlijk een pseudogebruiker: u kunt vanzelfsprekend niet als public inloggen en public zult u niet in de tabel Users (zie paragraaf 12.1.2 ) aantreffen. Wel kunnen aan public privileges worden toegekend. Deze gelden dan voor alle gebruikers en kunnen nimmer van individuele gebruikers worden afgenomen. Voorbeeld 12.8 Geef iedereen leesrecht op tabel Hemelobject. Oplossing: grant select on Hemelobject to public

Nu kan iedereen die op Ruimtereisbureau.fdb is ingelogd, leesopdrachten uitvoeren op Hemelobject.

12.2.6 Privileges verlenen ‘with grant option’ In de praktijk zal het vaak voorkomen dat degene die in technische zin database-eigenaar is, niet degene is die daadwerkelijk binnen de organisatie de toekenning van databaserechten bepaalt. De databaseeigenaar zal dan het recht om privileges te verstrekken, willen uitbesteden aan anderen binnen de organisatie. Hiertoe kan de optie with grant option aan een grant -statement worden toegevoegd. Wie een privilege ‘met grant-optie’ heeft ontvangen, is gerechtigd het privilege aan anderen door te geven, al dan niet ook weer ‘met grantoptie’. Zo kan een netwerk ontstaan van gebruikers die rechten aan elkaar

hebben doorgegeven. Wordt een privilege dat met grant-optie is verstrekt ingetrokken (met revoke ), dan vervallen ook de privileges die via die grant-optie waren verkregen. Zo houdt de database-eigenaar uiteindelijk toch de touwtjes in handen. Privilegeverstrekking ‘met grant-optie’ geeft ook aan dat u gerechtigd bent om de gegevens door een view aan anderen te tonen. Als u een view hebt en u geeft iemand anders select-rechten op de view, dan gaat dit alleen als u een grant-optie hebt op de onderliggende tabel. U geeft immers een impliciete grant op de data. Er kunnen complexe situaties ontstaan wanneer een gebruiker via meerdere wegen hetzelfde privilege heeft ontvangen. Het is verstandig ondoorzichtige situaties te vermijden door juiste afspraken te maken. Voorbeeld 12.9 Gebruiker-eigenaar Ruimtereisbureau geeft Tom het select -privilege op Reis, met grant-optie. Tom geeft het privilege door aan Luc zonder grantoptie en aan Sofie met grant-optie. Sofie geeft het weer door aan Jip en Lisa, zonder grant-optie, zie figuur 12.2 .

Figuur 12.2 Privilegeverstrekking volgens hiërarchische structuur

De structuur is hiërarchisch : elk privilege wordt slechts via één weg verkregen. De commando’s zijn achtereenvolgens:

Ingelogd als gebruiker Ruimtereisbureau: grant select on Reis to Tom with grant option

Inloggen als Tom en vervolgens Luc en Sofie hun rechten geven: connect 'Ruimtereisbureau.fdb' user 'Tom' password 'pw'; grant select on Reis to Luc; grant select on Reis to Sofie with grant option

Inloggen als Sofie en vervolgens Jip en Lisa hun rechten geven: connect 'Ruimtereisbureau.fdb' user 'Sofie' password 'pw'; grant select on Reis to Jip, Lisa

Wanneer gebruiker Ruimtereisbureau nu het aan Tom verstrekte privilege intrekt: connect 'Ruimtereisbureau.fdb' user 'Ruimtereisbureau' password 'pw'; revoke select on Reis from Tom

vervallen ook de rechten van alle anderen. Zou in plaats daarvan Tom het select -privilege terugnemen van Sofie, dan zouden Sofie zelf en Jip en Lisa hun privilege verliezen. Voorbeeld 12.10 Gebruiker-eigenaar Ruimtereisbureau geeft Luc en Sofie het update privilege op de prijs-kolom van Reis, met grant-optie. Luc geeft het privilege door aan Sofie, met grant-optie, hoewel ze het ook al van Ruimtereisbureau heeft gekregen. Sofie geeft het privilege zonder grantoptie door aan Jip en Lisa, zie figuur 12.3 .

Figuur 12.3 Netwerkprivilegestructuur

De structuur is een netwerkstructuur : Sofie heeft haar update -recht met grant-optie via meerdere wegen verkregen. Ruimtereisbureau bedenkt zich nu: hij trekt het door hem aan Sofie verstrekte privilege in. Maar hij vergist zich als hij denkt dat ze het daardoor echt kwijt is. Ze heeft het immers dubbel ontvangen: ook via Luc. Pas als Luc zich ook bedenkt of zelf zijn privilege kwijtraakt, verliest Sofie haar privilege en met haar ook Jip en Lisa. Conclusie uit voorbeeld 12.10 : door een revoke wordt niet altijd een privilege ontnomen. Wat gebeurt, is dat de registratie van het verlenen van het privilege (door de gever) in de data dictionary wordt geschrapt. In tabel 12.2 zijn de privilegeverstrekkingen van de voorbeelden 12.9 en 12.10 samengenomen in één tabel. Deze tabel lijkt veel op de tabel van de data dictionary waarin het geven van privileges wordt bijgehouden, zoals we in hoofdstuk 15 zullen zien. Van elke privilegeverstrekking wordt vastgelegd: de gever ( grantor ), de ontvanger ( grantee ), het privilege zelf, een tabel en eventueel – maar alleen bij een update – nog een kolom. Door een grant -statement komen er een of meer rijen in de dictionarytabel bij, door een revoke worden een of meer rijen uit verwijderd.

Tabel 12.2 Privilegetabel bij voorbeelden 12.9 en 12.10

De volgende opgaven gaan uit van de privileges van tabel 12.2 . U kunt de juiste beginsituatie verkrijgen door de voorbeelden 12.9 en 12.10 uit te voeren. Als alternatief kunt u het script Autorisatie_tabel_12.2.sql draaien, dat u vindt in de standaard scriptmap (te vinden via Relationele databases en SQL in het Windows Startmenu). Dit script gaat ervan uit dat de users bestaan.

Opgave 12.3 Wat deugt er niet aan het volgende commando? grant select on Reis, Deelname, Klant to Sofie

De volgende opgaven zijn gericht tot de database-eigenaar van Ruimtereisbureau. Log dus tevoren in als gebruiker Ruimtereisbureau. Controleer het effect van de verschillende acties.

Opgave 12.4 Ontneem Sofie haar update -recht op de kolom reisduur van Reis. Opgave 12.5 Geef zowel Sofie als public leesrecht op Hemelobject. Neem dit recht dan weer van public af. Heeft Sofie hierna haar privé-leesrecht nog? Opgave 12.6 In deze opgave onderzoeken we het effect van het references -privilege. a Omdat het select -privilege het references -privilege impliceert, moet u – om het onderzoek te laten slagen – vooraf zorgen dat Luc geen select -recht heeft op tabel Reis. Ontneem hem dus dit recht, ingelogd als database-eigenaar (gebruiker Ruimtereisbureau).

b Log in op Ruimtereisbureau als Luc en probeer een tabel Logboekregel te creëren, als volgt: create table Logboekregel (reisnr     integer         not null, datum      date            not null, verslag    varchar(1000), constraint pk_logboekregel primary key (reisnr, datum), constraint fk_reis_bij_logboekregel foreign key (reisnr) references Reis(nr) )

Hoe interpreteert u de foutmelding? c Log in als database-eigenaar (gebruiker Ruimtereisbureau) en geef Luc het references -privilege op Reis. Log opnieuw in als Luc en probeer het nogmaals.

Opgave 12.7 Schrijf een script om Tom, Luc, Sofie, Jip en Lisa al hun rechten op Ruimtereisbureau (uitgezonderd hun public -rechten) af te nemen. Maak dit script onafhankelijk van de precieze rechten die zij bezitten. Voer dit script uit en bewaar het voor later gebruik onder de naam RevokeAll.sql in een map voor eigen scripts.

12.3 Views Views zijn geïntroduceerd als ‘ select -query’s met een eigen naam’. Ze gedragen zich in veel opzichten als tabellen. We zetten de tot nu toe behandelde toepassingen nog eens op een rijtje. Via views kunt u: – sommige beperkingen van SQL of van een specifiek dialect omzeilen, zoals het groeperen van een groeperingsresultaat (een group by van een group by kan niet, maar via een view kunt u toch berei-ken wat u wilt) – select -query’s gemakkelijk hergebruiken

– een complex queryprobleem stapsgewijs oplossen. In deze paragraaf komt daar een belangrijke toepassing bij: – gebruikers functionaliteit en rechten geven op maat, zodat ze precies datgene kunnen zien en doen wat bij hun taak past: views als autorisatiemiddel. Bij functionaliteit op maat past natuurlijk ook het kunnen bijwerken van tabellen. Hoewel een viewdefinitie als basis een select -expressie heeft, zullen we zien dat er zogenaamde updatable views zijn waarmee een tabel kan worden bijgewerkt.

12.3.1 Views op maat Het update -privilege is het enige privilege dat op kolomniveau kan worden gespecificeerd. Dat is omdat update zelf een kolomoperatie is. Ook het select -privilege zou – conceptueel gezien – best op kolomniveau kunnen worden verleend. De behoefte is er zeker: bijvoorbeeld om de ‘zichtbaarheid’ van een tabel voor bepaalde gebruikers te beperken tot niet-gevoelige informatie. Het beperken van select -rechten op een tabel gaat via views. Deze bieden het voordeel dat we er een verfijnde autorisatie mee kunt realiseren: niet alleen kunnen we het zicht beperken tot bepaalde kolommen, maar door een goedgekozen selectieconditie kunnen we ook de rijpopulatie inperken. Door gebruikers rechten te geven op de view maar niet op de onderliggende tabel, blijven de ‘geheime’ gegevens onzichtbaar. Was er in de voorgaande alinea sprake van inperken van functionaliteit, via views kunnen we ook extra functionaliteit aanbieden: variërend van het tonen van gegevens uit een oudertabel tot complexe statistische managementinformatie. We geven verschillende voorbeelden van views en het autoriseren van gebruikers hiervoor. We gaan uit van de beginsituatie dat u als gebruikereigenaar Ruimtereisbureau bent ingelogd op de database Ruimtereisbureau. Voorbeeld 12.11 Geef Luc leesrechten op een view vReisPlan van reizen vanaf het jaar 2021. Via deze view mag hij alle kolommen zien behalve de optionele kolom prijs. Oplossing:

create view vReisPlan as select nr, vertrekdatum, transport, duur from   Reis where  vertrekdatum >= '01-jan-2021'; grant select on vReisPlan to Luc

Opmerkingen – Voor de syntaxis van het create view -statement: zie paragraaf 9.6 . – In paragraaf 12.3.2 krijgt Luc ook andere rechten op deze view, waardoor hij de onderliggende tabel zal kunnen bijwerken. Voorbeeld 12.12 Geef Tom leesrechten op een view voor de Top-3 van ruimtereizen, gerekend naar inkomsten (prijs maal aantal deelnemers). Oplossing: create view vReisMetInkomsten (nr, inkomsten) as select   R.nr, sum(prijs) from     Reis R join Deelname D on R.nr = D.reis group by R.nr; create view vTop3 as select nr, inkomsten from   vReisMetInkomsten RMI where  3 > (-- het aantal reizen met hogere inkomsten dan 'deze' reis select count(*) from   vReisMetInkomsten where  inkomsten > RMI.inkomsten); grant select on vTop3 to Tom

12.3.2 Updatable views De view vTop3 bevat informatie die is ontstaan via statistische ‘verdichting’ van de oorspronkelijke gegevens. Vanuit de statistische gegevens terugrekenen naar de oorspronkelijke gegevens is onmogelijk. Een update van vTop3 is dan ook ondenkbaar, net als een insert of een delete.

Heel anders is dat met de view vReisPlan. Wie die view gebruikt, weet het misschien niet, maar wij weten het wel: hij geeft de hele tabel Reis weer, behalve één optionele kolom. Elke rij van vReisPlan is eenduidig te ‘vertalen’ naar een rij in Reis, met een null voor prijs. Daardoor zijn inserts, deletes of updates op vReisPlan heel goed denkbaar: ze laten zich eenduidig herleiden tot een corresponderende operatie op Reis. Een view heet updatable wanneer het mogelijk is via de view inserts, deletes en updates uit te voeren op een onderliggende tabel. Hiertoe moet elke rij van de view op een eenduidige manier te herleiden zijn tot een rij van de onderliggende tabel. In Firebird moet de view daarvoor aan de volgende voorwaarden voldoen: 1 De view moet één onderliggende tabel of één andere updatable view als brontabel hebben. 2 De view moet (eventueel via een andere updatable view) alle verplichte kolommen van de onderliggende tabel bevatten. 3 In de select -expressie van de view mag niet impliciet (via een statistische functie) of expliciet (via group by ) worden gegroepeerd. Ook pseudogroeperen via distinct is niet toegestaan. Toelichting – Voorwaarde 1 is samen met voorwaarde 2 (meer dan) voldoende om te garanderen dat er een eenduidig verband bestaat tussen de kolommen van de view en die van de onderliggende tabel. – Voorwaarde 2 is nodig om inserts te kunnen uitvoeren. Voor deletes en updates is conceptueel gezien een zwakkere voorwaarde voldoende: dat de viewkolommen een kandidaatsleutel omvatten. Vanzelfsprekend zijn alleen de kolommen te updaten die deel uitmaken van de view. – Voorwaarde 3 is begrijpelijk, omdat door groeperen (in welke vorm dan ook) het één-op-één-verband tussen de rijen van de brontabel en die van de resultaattabel verloren gaat. Verdere opmerkingen

– Voorwaarde 1 verbiedt elke join als brontabel. Dat is conceptueel gezien onnodig streng: het zou geen bezwaar mogen zijn om via een join read only-kolommen uit een oudertabel aan de view toe te voegen. – Ook voorwaarde 3 zou wat kunnen worden afgezwakt: wanneer door groeperen alleen maar een statistische waarde als read-only kolom aan de view wordt toegevoegd, zou dit geen bezwaar mogen zijn. – Het opnemen in de view van read only-kolommen in de vorm van een subselect of een user defined function valt binnen de voorwaarden en is inderdaad toegestaan.

– In voorkomende gevallen kan elke view die zich daar conceptueel gezien voor leent, updatable worden gemaakt door er triggers aan te koppelen voor een insert, delete of update op de onderliggende tabel (zie hoofdstuk 15 ‘Triggers en stored procedures’. Op die manier kan de ontwikkelaar volledig de controle nemen over wat er gebeurt. Voorwaarde is dat elke rij van de view op een eenduidige manier is te herleiden tot een rij van de onderliggende tabel. Een niet-updatable view heet read-only . Bij een read-only view zijn alleen leesrechten (het select -privilege) zinvol. Voorbeeld 12.13 Insert-, delete- en updaterechten op updatable view vReisPlan Luc heeft al select -rechten op vReisPlan. Ingelogd als Ruimtereisbureau geven we hem nu ook het insert -, delete - en update -privilege: grant insert, delete, update on vReisPlan to Luc

Vervolgens loggen we in als Luc en en proberen zijn nieuw verworven rechten uit, door twee nieuwe reizen in te voeren: connect 'Ruimtereisbureau.fdb' user 'Luc' password 'pw' insert into vReisPlan values (38, '20-nov-2021', ‘BU’, 14); insert into vReisPlan values (39, '15-dec-2020', ‘RV’, 28)

Dat gaat allebei goed. Reis 39 heeft echter een vertrekdatum die niet aan de viewconditie voldoet, en is daarom voor Luc direct onzichtbaar geworden, ook al heeft hij hem zelf ingevoerd. Het resultaat van een select * from vReisplan bevestigt dat:

NR VERTREKDATUM TRANSPORT      DUUR ========= ============ ========= ========= 31 12-jan-2022  RV               10 32 03-jun-2022  RV              390 33 12-okt-2022  RV               11 34 10-jan-2023  RV             1380 35 12-mrt-2023  RV              380 36 27-jun-2023  RV             1340 37 17-jul-2023  BU                3 38 20-nov-2021  BU               14

Inderdaad zonder reis 39. Probeert Luc nu: delete from  vReisPlan where nr = 38; delete from  vReisPlan where nr = 39

dan lukt dat wel met reis 38 (‘1 row(s) deleted’), maar niet met reis 39 (‘0 row(s) deleted’). Die reis bestaat als het ware niet voor Luc. Tot slot een bijzondere updatepoging, waarbij een reis die aan de viewconditie voldoet, een nieuwe datum krijgt, waardoor hij daarna níet meer voldoet: update vReisPlan set    vertrekdatum = '31-dec-2020' where  nr = 35

Deze update verloopt vlekkeloos, maar hoewel reis 35 hier wordt gewijzigd via de view, wordt hij via diezelfde view ‘onzichtbaar’, net als eerder reis 39. Deze voorbeelden illustreren dat we nog iets missen bij updatable views: de mogelijkheid om af te dwingen dat elke wijziging van de onderliggende tabel voldoet aan de viewconditie (in de where -clausule). Wat we willen, komt erop neer dat de viewconditie ook ‘de andere kant op’ moet gaan werken: als filter richting database. Die mogelijkheid wordt geboden door het toevoegen van een speciale optie aan de viewdefinitie: with check option .

12.3.3 Views ‘with check option’ Aan een updatable view kan de clausule with check option worden toegevoegd. Deze heeft alleen effect bij inserts en updates, namelijk dat de selectieconditie in de where -clausule gaat fungeren als invoercontrole. Hierdoor wordt verzekerd dat de ingevoerde of gewijzigde rij zelf via de view zichtbaar wordt.

Voorbeeld 12.14 Zorg dat u via de view vReisPlan alleen reizen vanaf het jaar 2021 kunt invoeren. Ook een gewijzigde vertrekdatum mag niet voor 2021 liggen. De oplossing is in te loggen als gebruiker-eigenaar Ruimtereisbureau en de al bestaande view vReisPlan als volgt te wijzigen: drop view vReisplan; create view vReisPlan as select nr, vertrekdatum, transport, duur from   Reis where  vertrekdatum >= '01-jan-2021' with check option

De toevoeging with check option zorgt dat de view twee kanten op filtert: bij uitvoer worden alleen de reizen met vertrekdatum vanaf 2021 getoond, bij invoer (een nieuwe reis of wijziging van een bestaande reis) worden alleen reizen geaccepteerd met vertrekdatum vanaf 2021. Dit voorkomt invoer waarvan het resultaat via de view niet eens zichtbaar is.

De clausule ‘with check option’ en check-constraints De clausule with check option doet denken aan een check -constraint. Het verschil is dat een check -constraint altijd actief is en with check option alleen bij inserts of updates via één speciale view. Een check -constraint is daardoor het aangewezen middel bij algemeen geldende regels, terwijl views with check option bruikbaar zijn bij regels die specifiek zijn voor een groep gebruikers. De eis dat de vertrekdatum van een nieuwe of gewijzigde reis na de systeemdatum moet liggen, is typisch een voorwaarde voor een check constraint. Maar de eis dat bepaalde gebruikers alleen reizen vanaf het jaar 2021 (of alleen korte reizen of alleen goedkope maanreizen, enzovoort) mogen invoeren, leidt tot een view with check option .

Opgave 12.8 a Maak, als gebruiker Ruimtereisbureau, een view vPlaneet voor planeten, met één kolom: planeetnaam. Deze view is updatable. Waarom? b Geef Luc het all -privilege op vPlaneet en het references -privilege op Hemelobject. Opmerking : het references -privilege blijkt Luc nodig te hebben om via vPlaneet rijen in Hemelobject in te voeren of bestaande rijen daarvan te updaten. Direct inzichtelijk is dit niet, maar het heeft te maken met de recursieve verwijzing.

c Wijzig, als Luc, de naam van de planeet Aarde in Tranendal. d Voer, als Luc, een nieuwe planeet Cosimo in via vPlaneet. Ga na dat deze ‘onzichtbaar’ is via een select * from vPlaneet . Waarom is dat? e Hoe zouden we kunnen zorgen dat de planeet Cosimo wel zichtbaar is via de view?

Opgave 12.9 Zijn de volgende views updatable? – een view vDeelnemer met drie kolommen: voor reisnummer, klantnummer en klantnaam – een view vReisFinancieel met reisnummer, reisduur en prijs – een view vReisPlanning met reisnummer, vertrekdatum en transport.

Opgave 12.10 Probeer Tom update -rechten te geven op de statistische view Top3. Hij wil die Top 3 namelijk manipuleren. Krijgt u een foutmelding? Probeer daadwerkelijk een update uit te voeren. Hoe reageert Firebird en wat moet u van dit alles denken? Opgave 12.11 Maak een view vKlant met twee kolommen: naam en geboortedatum. Probeer te voorspellen of u via vKlant updates kunt uitvoeren op Klant. Zullen inserts of deletes lukken?

12.4 Rollen Wanneer gebruikers taken met elkaar delen, is het aannemelijk dat ze ook gelijke databaseprivileges nodig hebben. Ze elk apart hun privileges verstrekken, is dan niet alleen minder handig, maar het geeft ook

redundantie binnen de data dictionary. Beter is de gebruikers in te delen in gebruikersgroepen met gelijke privileges en per groep die privileges toe te kennen. Technisch wordt dit gerealiseerd via ‘rollen’. Een rol is als het ware een bundeltje privileges, maar kan evengoed worden beschouwd als een soort abstracte gebruiker.

12.4.1 Bedrijfsprocessen, gebruikersgroepen en rollen Ruimtereisbureau Reijnders kent drie groepen gebruikers. Deze corresponderen met drie hoofdprocessen van de bedrijfsvoering: planning, verkoop en management. – Het bedrijfsproces ‘planning’ zorgt dat er een verkoopproduct is: ruimtereizen. Pas als de medewerkers van ‘planning’ hun werk goed hebben gedaan, valt er wat te verkopen. – Het bedrijfsproces ‘verkoop’ omvat de daadwerkelijke verkoop van ruimtereizen en de daaruit voortvloeiende klantencontacten. – Het bedrijfsproces ‘management’ omvat sturing van de andere processen, mede aan de hand van goede statistische informatie (managementinformatie). Bij elk van de drie processen hoort enerzijds een gebruikersgroep , anderzijds een pakket databaseprivileges: een rol. In figuur 12.4 is dit concreet uitgewerkt voor Ruimtereisbureau Reijnders. Merk op dat Lisa breed wordt ingezet: zowel bij planning als bij verkoop.

Figuur 12.4 Bedrijfsprocessen, gebruikersgroepen en rollen

Een bedrijfsproces vormt het verbindende element tussen een gebruikersgroep en een rol en geeft aan beide zijn naam. Rolnamen geven we voor de herkenbaarheid de prefix ‘r’. Zo krijgen we naast het bedrijfsproces ‘verkoop’ de gebruikersgroep ‘Verkoop’ en de rol ‘rVerkoop’. Merk op dat per tabel steeds één gebruikersgroep het insert -privilege en het delete -privilege heeft. Twee gebruikersgroepen kunnen zodoende nooit ruzie krijgen over het toevoegen of verwijderen van reizen, deelnames, enzovoort: de verantwoordelijkheden zijn duidelijk gescheiden. Ook de update -privileges zijn, per kolom, toegewezen aan één gebruikersgroep. Zo heeft Management als enige het update -recht op prijs. Een initiële prijs mag door Planning worden vastgesteld. In het algemeen is typerend voor managers dat deze bijna alleen leesrechten hebben, en dan nog voornamelijk op managementinformatie, via views. Het spreekt vanzelf dat de praktijk wel wat ingewikkelder is. In een echt bedrijf vallen de hoofdprocessen uiteen in subprocessen en subsubprocessen en zijn de bevoegdheden gedetailleerder vastgesteld. Zo is het helemaal niet vanzelfsprekend dat een gebruiker met een insert recht ook het corresponderende delete -recht heeft. Privileges: kunt u wat u mag? Een privilege geeft een ‘recht’ om dingen te doen. Maar kunt u altijd doen wat de naam van een privilege lijkt te beloven? Het antwoord is: nee. De

mogelijkheden worden vanzelfsprekend ingeperkt door andere zaken, bijvoorbeeld: – DDL-constraints: een delete -recht is altijd ondergeschikt aan de referentiële-integriteitsregel (ingeval van een restricted delete) – de applicatie: een grafische applicatie is zo ingericht dat u op basis van een gebruikersnaam met wachtwoord toegang krijgt tot één beperkte set vensters. Daarmee moet u het doen, ook al zou u verdergaande databaseprivileges hebben. Tenzij u natuurlijk de database benadert via de ‘achterdeur’ van een SQL-tool.

12.4.2 Privileges, rollen en gebruikers Het creëren van een rol en het toekennen ervan aan gebruikers verloopt in drie stappen, uitgevoerd door de eigenaar van de database: 1 creëren van een ‘lege’ rol met het create role -commando 2 toekennen van privileges aan de rol (met grant ) 3 toekennen van de rol aan gebruikers. Een vierde stap wordt gedaan door de gebruikers zelf aan wie de rol is toegekend:

4 inloggen als gebruiker met rol. Stap 4 is opmerkelijk: in Firebird kunnen we per databasesessie maar over de privileges van één rol tegelijk beschikken. Zo zal Lisa moeten inloggen met de rol rPlanning of met de rol rVerkoop. Maar zo gek is het niet: ze is planner of verkoper, nooit beide tegelijk. In bijvoorbeeld Oracle stapelen de privileges zich op die u vanuit verschillende rollen krijgt toegekend.

Voorbeeld 12.15 Creëer de rol rPlanning, vul deze met de bijbehorende privileges en ken de rol toe aan alle leden van de bijbehorende gebruikersgroep. Log vervolgens in als gebruiker Lisa, in de functie van planner. Oplossing (log vooraf zo nodig in als database-eigenaar: Ruimtereisbureau): -- stap 1 create role rPlanning; -- stap 2 grant all grant all grant all grant all

on on on on

Transport to rPlanning; vReisplan to rPlanning; Hemelobject to rPlanning; Bezoek to rPlanning;

-- stap 3 grant rPlanning to Luc, Lisa; -- stap 4 connect 'Ruimtereisbureau.fdb' user 'Lisa' password 'pw' role 'rPlanning'

Toelichting – Toekennen van privileges aan een rol gaat precies als het toekennen van privileges aan een gebruiker. – Het IQU -commando connect is uitgebreid met het sleutelwoord role gevolgd door de rolnaam (tussen enkele aanhalingstekens). – Lisa kan nu aan de slag als planner. En Luc ook als hij vanaf zíjn werkplek inlogt met de rol rPlanning. – Hoe Luc en Lisa ook inloggen, ze kunnen altijd beschikken over hun persoonlijke privileges, die aan hen los van enige rol zijn toegekend. We creëren de rollen rVerkoop en rManagement in opgave 12.12 .

12.4.3 Vereenvoudiging van privilegebeheer Figuur 12.5 brengt het toekennen van privileges aan rollen en het toekennen van rollen aan gebruikers in beeld. Zou later besloten worden om de privileges behorend bij de rol rVerkoop in te perken of uit te breiden, dan zijn alleen extra grant ’s of revoke ’s nodig met betrekking tot de rol rVerkoop. Alle gebruikers met die rol hebben dan automatisch de nieuwe bevoegdheden horend bij de gewijzigde rol. Het beheer van privileges wordt door het gebruik van rollen vereenvoudigd tot: – het ‘onderhoud’ van de rollen: zorgen dat elke rol steeds de juiste privileges omvat – het toekennen van rollen aan gebruikers of het afnemen van rollen van gebruikers. Een rol: samengesteld privilege én abstracte gebruiker In zoverre we privileges aan een rol kunnen toekennen, lijkt een rol op een gebruiker. Maar een rol lijkt ook op een privilege, omdat we een rol op zijn beurt kunnen toekennen aan een gebruiker. We zouden kunnen zeggen: een rol is een samengesteld privilege en tevens een abstract soort gebruiker. Merk op dat we door de introductie van rollen een ‘single point of definition’ hebben gecreëerd voor het toekennen van privileges aan gebruikers.

image Figuur 12.5 Toekenning van privileges aan rollen en toekennen van rollen aan gebruikers

12.4.4 Wijzigen en verwijderen van rollen Een rol wijzigen betekent het toekennen van extra privileges aan de rol of het terugnemen van eerder toegekende privileges. Het wijzigen gaat dus met grant of revoke . Een rol verwijderen doen we met drop role . Beide zijn voorbehouden aan de eigenaar van de rol, dus aan de eigenaar van de database. Log voor het volgende voorbeeld dus zo nodig in als Ruimtereisbureau.

Voorbeeld 12.16 Geef de rol rPlanning select -rechten op Klant en Deelname. Oplossing : grant select on Klant to rPlanning; grant select on Deelname to rPlanning

Voorbeeld 12.17 Verwijder de rol rPlanning. Oplossing: drop role rPlanning

Opgave 12.12 Creëer opnieuw de rol rPlanning, die in voorbeeld 12.17 is verwijderd. Creëer ook de rollen rVerkoop en rManagement. Ken aan alle rollen de juiste privileges toe en ken de rollen zelf toe aan de juiste gebruikers, zie f iguur 12.4 . Neem alle statements op in een script RuimtereisbureauAutorisatie.sql. Bewaar het script en test het. Opgave 12.13 Log, als gebruiker Lisa, achtereenvolgens in met de rol rPlanning en de rol rVerkoop en probeer de mogelijkheden en beperkingen van beide rollen uit. Let wel op of Lisa nog persoonlijke privileges heeft en neem deze zo nodig van haar af.

Oefenopgaven De volgende opgaven hebben weer betrekking op de voorbeelddatabase OrderdatabaseC (versie met constraintnamen). Voer ze uit als eigenaar/ gebruiker, tenzij het gaat om het aanmaken van nieuwe gebruikers of wanneer een andere gebruiker is aangegeven. Ga bij elke opgave uit van de situatie die door de opgaven daarvóór is gecreëerd. Diagrammen: zie Boekverkenner. De voorbeeldpopulatie vindt u ook in bijlage 4.

Opgave 12.14 a Maak een view vKlacht aan, die de volgende gegevens bevat van nietbehandelde klachten: de gegevens uit Klacht (waarvan ‘behandeld’ dus altijd de waarde ‘N’ heeft), aangevuld met klantnummer, klantnaam, artikelnummer en artikelomschrijving. b Maak een view vKlant aan, met twee kolommen: klantnummer en klantnaam.

Opgave 12.15 a Waarom is vKlant updatable? b Waarom is de view vKlacht niet updatable? c Indien we niettemin de mogelijkheid zouden willen scheppen om via vKlacht de kolom ‘behandeld’ te updaten, hoe zou dat moeten gebeuren? Volsta met een indicatie van de oplossingsrichting.

Opgave 12.16 Leg uit waarom, conceptueel gezien, het onderscheid voor views tussen ‘read only’ en ‘updatable’ verfijnd zou moeten worden tot ‘read only’, ‘insertable’, ‘deletable’ en ‘updatable’. Welke condities zouden per geval moeten gelden? Ga uit van één onderliggende tabel. Opgave 12.17 We onderscheiden onder meer de bedrijfsprocessen ‘artikelbeheer’ en ‘verkoop’. Creëer hierbij de rollen rArtikelbeheer en rVerkoop en ken daar als volgt privileges aan toe. De rol rArtikelbeheer krijgt: – alle’ rechten op Artikel en Artikelgroep – leesrechten op Orderregel en Klacht. De rol rVerkoop krijgt: – ‘alle’ rechten op Order_ en Orderregel – het recht de stored procedure UpdateTotaalbedragOrder (zie zelftoets hoofdstuk 12 , opgave 3) uit te voeren – leesrecht op Artikel, Artikelgroep en Kortingsinterval

– leesrecht op de view vKlant – het recht via vKlant nieuwe klanten in te voeren (hiervoor is onder meer het references-privilege op Klant vereist).

Opgave 12.18 Geef alle (ook toekomstige) gebruikers leesrecht op Artikel. Opgave 12.19 Maak drie gebruikers aan: Pablo, Pepe en Rosa (alle met wachtwoord ‘pw’) en ken hun rollen toe, als volgt: Pablo krijgt de rol rVerkoop, Pepe de rol rArtikelbeheer en Rosa krijgt beide rollen. Geef daarnaast, los van rollen, Rosa alle rechten op Klacht. Zij moet deze rechten op haar beurt aan anderen kunnen doorgeven. Opgave 12.20 Voorspel van elk commando of het enig effect heeft en zo ja welk: a revoke select on Artikel from Pablo b revoke delete on Order_ from rVerkoop Bij eventueel uittesten: herstel de oorspronkelijke situatie. Kan dit met rollback?

Opgave 12.21 Voer de volgende opdrachten uit als Rosa, in de rol rVerkoop. a Voer een nieuwe klant in (via de view vKlant). b Voer een order van deze klant in en laat daarbij de datum en het totaalbedrag leeg. De datum wordt default de systeemdatum. c Voer bij de order twee orderregels in. Laat de regeltotalen leeg of voer ze handmatig in. (in hoofdstuk 15 leert u hoe u de regeltotalen en het ordertotaal kunt laten berekenen door een trigger en een stored procedure).

Opgave 12.22 Welke overeenkomst bestaat er tussen rollen en domeinen?

13 Query-optimalisatie In de voorgaande hoofdstukken hebben we vooral naar de logische opbouw van een query gekeken. In dit hoofdstuk gaan we in op de snelheid waarmee het rdbms een query uitvoert: de performance . Deze is vooral belangrijk binnen programma’s die een query veelvuldig uitvoeren. Voor het stellen van ad-hocvragen via een programma als IQU is de performance van een query uiteraard minder belangrijk. Maar ook bij een ad-hoc-query willen we natuurlijk liever niet uren of zelfs dagen lang op het antwoord wachten.

13.1 Voorbeelddatabase: GrootOrderdatabase We gebruiken een qua omvang uitgebreide versie van de Orderdatabase: GrootOrderdatabase. De structuur komt overeen met die van Orderdatabase ( figuur 13.1 ). De omvang is echter veel groter ( tabel 13.1 ). Tabel 13.1 Recordaantallen in GrootOrderdatabase

Doordat het uitvoeren van query’s op GrootOrderdatabase meer tijd vergt dan bij de andere voorbeelddatabases, worden performanceverschillen merkbaar en meetbaar. Hoe lang de query’s daadwerkelijk duren, hangt in hoge mate af van de technische specificaties van de gebruikte computer. Records en velden We zullen het in dit hoofdstuk vaak hebben over records in plaats van over rijen en soms over velden in plaats van over kolommen of kolomwaarden. Een record is de fysieke realisatie van een rij en een veld is een kolomwaarde binnen zo’n record. Fysieke aspecten spelen bij performancekwesties een grote rol: opslag op de harde schijf (semipermanent), opslag in het interne geheugen (tijdelijk), transport van en naar de harde schijf, transport van server naar client en zoekprocessen.

Figuur 13.1 Strokendiagram GrootOrderdatabase

13.2 De optimizer In hoofdstuk 8 ‘Statistische query’s’ hebben we het conceptuele algoritme behandeld. Een query die volgens het conceptuele algoritme correct is, zou een correct resultaat moeten opleveren. In deze paragraaf gaat het niet om logische correctheid, maar om het bereiken van realistische verwerkingstijden. De optimizer speelt daar een belangrijke rol in, door verwerkingsalgoritmen te genereren die meestal flink afwijken van het conceptuele algoritme. In volgende paragrafen zullen we zien dat we ook als ontwikkelaar verantwoordelijk zijn voor een goede performance en niet alles aan een slimme optimizer kunnen overlaten.

13.2.1 Stappen in het verwerken van een query Wanneer het rdbms een query-statement ontvangt, wordt het door de SQL interpreter verwerkt. De interpreter bestaat uit twee onderdelen: een parser en een optimizer . Eerst kijkt de parser of het statement syntactisch correct is (to parse = ontleden). Zo niet, dan wordt een foutmelding teruggestuurd en is de query afgehandeld. Klopt de syntaxis, dan wordt het statement door de parser ook aan een semantische controle onderworpen: bestaan bijvoorbeeld alle genoemde tabellen en kolommen? Ook dit kan resulteren in een foutmelding. Klopt de syntaxis en is het statement ook semantisch in orde, dan wordt het door de optimizer onder handen genomen. De optimizer is een programma dat bepaalt op welke manier het queryresultaat moet worden berekend om zo snel mogelijk tot een antwoord te komen en op zo min mogelijk geheugen beslag te leggen. Kortom: hoe de beste performance kan worden bereikt. Wordt de query uiteindelijk uitgevoerd, dan kan het daarna nog steeds mis gaan. Bijvoorbeeld doordat een subselect meer dan één waarde oplevert, terwijl er precies één waarde nodig is om verder te kunnen. In dat geval eindigt de verwerking met een ‘run time error’. Het gehele proces is schematisch weergegeven in figuur 13.2 .

Figuur 13.2 Stappen in het verwerken van een query

13.2.2 Performancefactoren Hoe snel het resultaat van een query kan worden berekend, is afhankelijk van een aantal factoren: – de hoeveelheid records die van de harde schijf moeten worden gelezen – de hoeveelheid records die tijdens de berekeningen in het intern geheugen moeten worden gehouden (bij te weinig intern geheugen wordt de harde schijf gebruikt, dit is erg traag)

– de processortijd die nodig is om de records te doorlopen (uitfilteren op grond van een conditie en uitvoeren van berekeningen) – de tijd die nodig is om het resultaat van de server naar de client te sturen. In alle gevallen gaat het erom dat er zo min mogelijk records worden verwerkt. Tevens is van belang op elk moment zo min mogelijk records in het interne geheugen te hebben.

13.2.3 Hypothetische ‘conceptuele’ verwerking Stel, we willen een overzicht met de omschrijvingen van artikelen die geen artikelgroep hebben met het bijbehorende totaalbedrag, in orderregels van de orders van november 2011, aflopend gesorteerd op totaalbedrag. Het gevraagde overzicht is, indien tot zijn essentie teruggebracht, een Artikel-overzicht. De tabel Artikel is dan ook de start van het navigatiepad, zie figuur 13.3 .

Figuur 13.3 Navigatiepad bij voorbeeldquery

Oplossing: select   A.omschrijving, sum(Orl.bedrag) totaal from     Artikel A join Orderregel Orl on A.nr  = Orl.artikel join Order_ O on Orl.order_ = O.nr where    extract(year from O.datum) = 2011 and extract(month from O.datum) = 11 and A.artikelgroep is null group by A.omschrijving order by totaal desc

Om een indruk te krijgen van het aantal records dat wordt verwerkt tijdens het uitvoeren van een query zonder optimalisatie, zullen we deze query op de meest eenvoudige manier aanpakken: volgens het conceptuele algoritme. Zie nogmaals tabel 13.1 voor de relevante aantallen records van GrootOrderdatabase. De eerste stap van het conceptuele algoritme is het maken van de producttabel over alle betrokken tabellen. Hiervoor moet elke tabel van schijf worden gelezen, in totaal 160.665 + 202 + 1.557.457= 1.718.324 records. De volgende stap is het maken van de producttabel, deze bevat 160.665 * 202 * 1.557.457= 5,1 × 10 13 records! Normaal gesproken probeert het rdbms tabellen die zijn opgehaald of als tussenresultaat zijn berekend, in het interne geheugen van de computer te houden. Als we er (heel grof) van uitgaan dat een record uit de artikeltabel 200 bytes in beslag neemt, zou zo’n 1,0×10 16 = 1,0×10 4 terabyte intern geheugen nodig zijn. Zelfs bij veel kleinere tabellen kan de omvang van een producttabel astronomisch worden en de capaciteit van het interne en externe geheugen verre overschrijden. Zelfs als de capaciteit er was, zou het stuk voor stuk doorlopen van al die records, om de juiste er uit te filteren, ‘eindeloos’ duren.

13.2.4 Een verbeterd algoritme Een aantal verbeteringen is eenvoudig aan te brengen. Zo zouden we eerst per tabel een recordselectie kunnen toepassen en van de aldus gefilterde tabellen de producttabel kunnen maken, dit scheelt al aanzienlijk. De datumvoorwaarde geldt alleen voor tabel Order_. Van de 160.665 orders zijn er 1.348 in november 2011 geplaatst, dat is 0,8%. De producttabel wordt dan ‘slechts’ 0,8 × 10 2 terabyte. De artikelgroepvoorwaarde geldt voor tabel Artikel. Er zijn 28 artikelen zonder artikelgroep. Een tweede verbetering ontstaat door van de gefilterde tabellen niet de volledige producttabel te maken, maar ze stap voor stap te joinen, te beginnen met de kleinste al dan niet gefilterde tabel: Artikel. Deze joinen we met Orderregel. De volgende query geeft het aantal records van de tussenresultaattabel: select count(*) aantal from   Artikel A join Orderregel Orl on A.nr  = Orl.artikel where  A.artikelgroep is null

Resultaat: 215.421 records. Vervolgens joinen we het resultaat met de 1.348 records van de gefilterde tabel Order_. Dit resulteert in 1760 records. De aantallen records die (min of meer) gelijktijdig in het geheugen worden bewerkt, zijn hiermee aanvaardbaar geworden. Verdere verbeteringen

Optimizers passen inderdaad dit soort technieken toe, zij het in zeer geavanceerde vorm. Ze zijn een onderdeel van het queryplan dat voor elke query wordt opgesteld voordat hij wordt uitgevoerd. Onderdeel van zo’n queryplan is ook dat gebruik wordt gemaakt van zogenaamde indexen , dit zijn toegangsstructuren die door slimme zoekalgoritmen worden gebruikt om het zoeken in grote tabellen te versnellen. Indexen worden behandeld in paragraaf 13.3 .

13.2.5 Rule-based en cost-based optimizers Er zijn twee typen optimizer. Het eerste type, de rule-based optimizer ( RBO ), gaat uit van een syntactische analyse van het door de gebruiker aangeboden SQL -statement, van informatie over de aanwezige indexen en de grootte van de tabellen en van heuristische regels (kennisregels, gebaseerd op theorie en ervaring). Een RBO komt daarmee tot een executieplan dat het gebruik van bepaalde indexen impliceert, of juist leidt tot ‘dom’ van voor naar achter doorzoeken van een tabel (een full table scan ). Bij joins wordt het fysieke navigatiepad bepaald. Het tweede, recenter type is de cost-based optimizer ( CBO ). Hierbij worden statistische gegevens over de tabelpopulaties gebruikt om een ‘kostenwaarde’ te berekenen van meerdere potentiële executieplannen. In de kosten zijn zowel de processortijd als de lees-/schrijftijd van/naar extern geheugen verdisconteerd. Uiteindelijk wordt het plan met de laagste kostenwaarde uitgevoerd. Optimizers bewaren executieplannen voor als later een identiek statement wordt aangeboden en de populaties van de betrokken tabellen niet wezenlijk zijn veranderd. Een volgende uitvoering van eenzelfde statement kan dan sneller plaatsvinden. Rampquery’s In de praktijk komt het nogal eens voor dat iemand vergeet een joinconditie op te nemen. Dit heeft meestal ernstige gevolgen voor de performance. Soms moet de query zelfs op grove wijze worden afgebroken, omdat een antwoord de eerste dagen niet hoeft te worden verwacht. En dat kan heel vervelend zijn: denk aan de andere gebruikers (dat kunnen er honderden of zelfs duizenden zijn) die het systeem ineens ‘down’ zien gaan. Veel rdbms’en hebben mogelijkheden om dit soort rampquery’s af te breken.

13.3 Indexen Een index is een interne, gesorteerde gegevensstructuur met verwijzingen naar records uit een tabel. Via een index kan het rdbms de records snel in een bepaalde volgorde aflopen. Ook kan het via de index snel een record opzoeken. Omdat vaak wordt gezocht op sleutelwaarden (denk aan joins), worden door Firebird automatisch indexen aangemaakt bij primaire sleutels en bij verwijssleutels. Verder kunnen we als ontwikkelaar met het commando create index opdracht geven een index aan te maken op een kolom of kolomcombinatie. We doen dat wanneer we verwachten dat daardoor bepaalde query’s (bijvoorbeeld met order by ) sneller zullen worden uitgevoerd. Een belangrijke taak van de optimizer is te bepalen of een tabel via een index wordt benaderd of dat hij gewoon vanaf het begin sequentieel wordt doorlopen (via een full table scan ). Alles wat we in deze paragraaf vertellen over de achterliggende gegevensstructuur van een index, is slechts bedoeld om algemeen inzicht bij te brengen. De werkelijke indexstructuren en hun zoekalgoritmen zijn ingewikkelder én slimmer dan de varianten die wij beschrijven.

13.3.1 Indexstructuur We bekijken een voorbeeld van een index, bedoeld voor het sorteren van Order_-records op het veld klant, zie figuur 13.4 . Deze index is een gesorteerde lijst van verschillende klantnummers, een zogenaamde lineaire lijst . Iedere klant uit die lijst verwijst naar één of meer records uit de tabel Order_. Omdat de ordertabel zeer groot is hebben we slechts een klein deel van de records weergegeven. De records van de Order_-tabel staan in de volgorde zoals ze zijn opgeslagen.

Figuur 13.4 Index op klant, als lineaire lijst met verwijzingen

Iedere klant in de lijst verwijst in werkelijkheid naar vele honderden records. In de figuur hebben we ons beperkt tot maximaal drie. Ook tussenliggen records van tabel Order_ hebben we niet weergegeven. Voor gesorteerd afdrukken voldoet de lineaire-lijstindex uitstekend. Maar voor het zoeken naar één speciale klant moet gemiddeld de helft van de lijst worden afgelopen (tot u hem hebt gevonden of tot u er voorbij bent). Indexen worden daarom meestal opgeslagen in een boomvormige structuur; deze leent zich goed om razendsnel te zoeken. In figuur 13.5 ziet u een deel van de index op klant, maar nu gerealiseerd als binaire boom , dat is een boomstructuur waarin op elk niveau maximaal twee nieuwe takken per knooppunt ontspringen. Net als bij de linaire-lijstindex heeft elk lijstelement een verwijzing naar een of meer records die we echter in de figuur niet hebben aangegeven. Door de boom af te lopen volgens de gegeven ordening, te beginnen rechts boven, vindt u de records op volgorde van klantnummer. De volgorde van records met gelijk klantnummer is ook nu willekeurig. De structuur van fig uur 13.5 wordt een binaire zoekboom genoemd. Binair omdat ieder knooppunt maximaal twee kinderen heeft. De boom is zodanig georganiseerd dat alle waarden in de bovenste tak van een knooppunt allemaal kleiner zijn dan die van het knooppunt en dat alle waarden van de

onderste deeltak allemaal groter zijn. In de volgende paragraaf gaan we in op het zoeken via zo’n zoekboom.

Figuur 13.5 Binaire (zoek)boom van klantnummers (fragment)

13.3.2 Binair zoeken Bij zoeken in de binaire zoekboom wordt een gegeven klantnummer (zoekwaarde) vergeleken met het klantnummer in het meest linkse knooppunt van de boom. Is de zoekwaarde groter dan de knooppuntwaarde, dan wordt verder gezocht in de onderliggende boomtak. Is hij kleiner, dan wordt in de bovenste tak verder gezocht. Is hij gelijk, dan kunnen via de verwijzingen (een of meer) direct de bijbehorende records worden opgehaald.

De werking van het zoekalgoritme via een binaire boom kan goed worden geïllustreerd aan de hand van het zoeken van een woord in een woordenboek, wanneer u dat als volgt aanpakt. 1 Neem een woord in het midden van het boek (dit is het beginknooppunt van de boom). 2 Kijk of het gezochte woord: a) vóór dat woord komt, b) ná dat woord komt, c) gelijk is aan dat woord zelf. 3 In geval c bent u klaar. 4 In geval a of b: neem het boekdeel dat het woord bevat en neem een nieuw woord in het midden ervan. 5 Ga terug naar 2. In een boek met 1.000.000 woorden zijn met deze methode maximaal 20 woordvergelijkingen nodig om het juiste woord te vinden (2 20 – 1 = 1.048.575). Deze zoekmethode wordt logaritmisch zoeken genoemd. Er bestaan tal van slimme varianten van. Zelf gaan we al slimmer te werk bij het zoeken in een woordenboek of telefoonboek: door de plaats van openslaan te schatten, in plaats van steeds domweg het midden te nemen. In de praktijk worden bomen gebruikt met meer dan twee takken per knooppunt, in combinatie met schatmethoden om snel de juiste tak te vinden. Hierdoor wordt de boom veel minder diep, al wordt een knooppuntbewerking ingewikkelder. De term logaritmisch zoeken is ontleend aan het maximaal aantal benodigde stappen. Dit is gelijk is aan de (naar boven afgeronde) 2 log van het aantal pagina’s. De

2

log van een getal is de exponent, wanneer je dat getal als 2-macht

schrijft. Bijvoorbeeld:

2

log1024 = 10, want 2

10

= 1024.

13.3.3 Wanneer indexeren? Het maken van een index heeft ook een keerzijde. Het toevoegen en verwijderen van rijen kost meer tijd, omdat niet alleen de tabel, maar ook de index moet worden bijgewerkt. Dit geldt ook voor het updaten van geïndexeerde kolommen. Het creëren van een index moet daarom altijd weloverwogen geschieden. Criteria zijn: – Het creëren van een index op een kolom of kolomcombinatie is vaak verstandig wanneer de kolom(combinatie) vaak wordt gebruikt in zoekcondities, joincondities of order by -clausules. – Voor kolommen die zelden voorkomen in zoekcondities, voor nietsleutelkolommen die veelvuldig een update ondergaan en voor kolommen met een klein aantal mogelijke waarden, heeft het meestal geen zin een index te creëren. In deze gevallen kan een index zelfs vertragend werken. – Bij een kleine tabel is indexeren overbodig. Verder kan het bij transactie-intensieve systemen, waarbij weinig wordt gezocht, maar waarbij de nadruk ligt op het toevoegen van nieuwe records, soms beter zijn indexen achterwege te laten. We zijn wat voorzichtig in onze formuleringen. Het resultaat van het gebruik van een index hangt van vele factoren af en is daardoor soms moeilijk te voorspellen. De optimizer speelt een sleutelrol. Wanneer op een tabel twee indexen zijn gedefinieerd (bij Order_ bijvoorbeeld één op de primaire sleutel en één op datum), kan de optimizer kiezen uit drie mogelijkheden: Order_ benaderen via de primaire-sleutelindex, via de index op datum of geen van beide gebruiken (en dus een full table scan uitvoeren).

13.3.4 Creëren en verwijderen van een index Het creëren van een index gaat met het SQL -commando create index . Verwijderen gaat met drop index . Namen van indexen zijn per database uniek. U kunt een bepaalde naam pas opnieuw gebruiken als u eerst de index met de betreffende naam heeft verwijderd.

Voorbeeld 13.1 Creëer een index voor snel zoeken van orders op klantnummer en voor het afdrukken van ordergegevens, geordend op klantnummer. De gevraagde index is die van paragraaf 13.3.1 . Indexnamen laten we beginnen met een i; deze noemen we daarom iKlant: create index iKlant on Order_(klant)

Er zal nu een index met de naam iKlant worden aangelegd en bijgehouden op kolom Order_.klant. De bedoeling van deze index is het zoeken en gesorteerd afdrukken (klimmend) op het klantveld te versnellen. De standaard sorteervolgorde van een index is: klimmend (‘ascending’). Dit mag expliciet worden vermeld via het sleutelwoord asc . Zo krijgen we de volgende variant: create asc index iKlant on Order_(klant)

Voor gesorteerd afdrukken van later naar vroeger kunt u een aparte, dalende index laten aanmaken. Vervang daarvoor asc door desc . Indexbomen kennen blijkbaar eenrichtingverkeer. Wanneer zowel ‘klimmende’ als ‘dalende’ overzichten gewenst zijn, verdient het aanbeveling twee indexen aan te maken: een klimmende en een dalende. Het is dan geen slecht idee om de volgorde ook in de naam op te nemen, bijvoorbeeld: iKlant_asc en iKlant_desc). Voorbeeld 13.2 Creëer een index op de combinatie van datum en klant van tabel Order_. Oplossing: create index iDatumKlant on Order_(datum, klant)

Aan deze index hebt u alleen iets wanneer u het rdbms regelmatig laat zoeken op een combinatie van datum en klantnummer of voor het afdrukken van lijsten met orderkenmerken, gesorteerd op datum en bij gelijke datum op klantnummer. Soms kan het handig zijn een index te maken op basis van een expressie met daarin kolomnamen. Als we voor tabel Klacht een index willen hebben op de combinatie van order_ en volgnr kunnen we dat doen op de manier van voorbeeld 13.2 . We kunnen echter ook een expressie gebruiken. Als we tabel Orderregel analyseren, kunnen we vaststellen dat het volgnr maximaal een getal van twee cijfers is. We kunnen dan een index maken gebaseerd op de volgende expressie: order_*100 + volgnr. Voor zo’n index gebruiken we een iets andere syntaxis: Voorbeeld 13.3 Creëer voor tabel Klacht een index op de expressie order_*100 + volgnr.

Oplossing: create index iOrder_Volgnr on Klacht computed by (order_* 100 + volgnr)

De expressie moet tussen haakjes staan. Aan deze index hebt u alleen iets wanneer u het rdbms regelmatig laat zoeken op een combinatie van order_ en volgnr.

Indexen en order by Een veel voorkomend misverstand is dat een index invloed zou hebben op een queryresultaat en een order by -clausule op de betreffende kolom of kolomcombinatie overbodig zou maken. Voor alle duidelijkheid: een index heeft géén gevolgen voor enig queryresultaat, althans geen gevolgen waar u tevoren op mag rekenen. Een index wordt door een optimizer soms niet eens gebruikt. Voor gesorteerd afdrukken blijft order by noodzakelijk.

13.3.5 Unieke indexen Omdat een index gesorteerde toegang biedt tot de waarden van een kolom, is het technisch vrij eenvoudig via een index het toevoegen van dubbele waarden te voorkomen. Een index die aldus ook uniciteit bewaakt, wordt een unieke index genoemd. Voorbeeld 13.4 Met het volgende statement wordt een unieke index gecreëerd voor tabel Orderregel op de kolommen order_ en artikel en wordt de uniciteit van deze combinatie afgedwongen: create unique index iOrder_Artikel on Orderregel(order_, artikel)

Een unieke index dwingt – ongevraagd – nóg iets af: dat elk indexveld wordt gevuld met een nietnull. Een kolom van een unieke index is dus uniek én verplicht, ofwel: een kandidaatsleutel (primaire of alternatieve sleutel). In de volgende paragraaf over ‘standaardindexen’ gaan we hier nader op in.

13.3.6 Standaardindexen Vrijwel alle databases maken standaard gebruik van standaardindexen : unieke indexen om primaire en alternatieve sleutels te implementeren. Neem het volgende statement: create table Orderregel (order_   integer not null,

volgnr   integer not null, artikel  integer not null, aantal   integer not null, bedrag   numeric(7,2), constraint pk_orderregel primary key (order_, volgnr), constraint fk_orderregel_bij_artikel foreign key (artikel) references Artikel(nr) on update cascade, constraint fk_orderregel_van_order foreign key (order_) references Order_(nr) on delete cascade on update cascade, constraint un_orderregel unique (order_, artikel) );

Bij het uitvoeren hiervan wordt automatisch een unieke index gecreëerd op de primaire sleutel (de kolomcombinatie order_ en volgnr) en nietunieke indexen op elk van de verwijssleutels (de kolommen artikel en order_). Waarom standaardindexen? Standaardindexen zijn van groot belang bij het optimaliseren van joins. We hebben gezien dat een join wordt uitgevoerd volgens een optimalisatieplan. Het startpunt is de tabel die (na filtering op de selectieconditie) de kleinste is. Van daaruit wordt stap voor stap een join uitgevoerd op de andere betrokken tabellen, langs het ‘joinpad’. Langs dat pad kunnen we zowel van ouder- naar kindtabel gaan als andersom. Bij joinen vanuit een kind met een ouder willen we bij elke kindrij zo snel mogelijk de bijbehorende ouderrij vinden (voor zover de verwijssleutel niet null is). Daarbij werkt een index op de primaire sleutel van de oudertabel versnellend. Bij joinen vanuit een ouder met een kind moeten bij een ouderrij snel de bijbehorende kindrijen worden gevonden. Daarbij zoeken we bij primaire sleutelwaarden de bijbehorende verwijssleutelwaarden; een index daarop versnelt het zoeken aanzienlijk.

13.3.7 Queryplan De keuzes die de optimizer maakt, kunt u zichtbaar maken door eenmalig het commando set plan on te geven. Hierna wordt bij elk queryresultaat een queryplan afgedrukt, waarin staat te lezen welke indexen zijn gebruikt. Bij een subselect-query wordt voor hoofdselect en subselect een apart queryplan opgesteld. Zie paragraaf 13.3.8 voor voorbeelden. Met set plan off maakt u een einde aan het tonen van queryplannen.

13.3.8 Verwerkingsstatistieken Voor het vergelijken van queryprestaties moet u weten hoe het zit met geheugengebruik en hoe lang de verwerking duurt. Gelukkig hoeft u niet

met een stopwatch bij de computer te gaan zitten. Het commando set statistics_display on zorgt dat bij elk queryresultaat statistische gegevens worden verstrekt, waarvan vooral de tijd ons interesseert. Nog gemakkelijker is het ‘statistiekenknopje’ van het IQU -venster (zie figuur 13.6 ), waarmee u de statistiekenweergave aan of uit kunt zetten. Het commando kent vanzelfsprekend ook een off -variant. In de volgende paragraaf gebruiken we de statistiekenweergave om het effect te meten van de index iAantal.

Figuur 13.6 Weergave van statistieken

13.3.9 Een index activeren en inactiveren Na het creëren van een index is deze actief , dat wil zeggen dat de optimizer hem kán gebruiken. Het kan voorkomen dat u een index inactief wilt maken, zonder hem te verwijderen. Bijvoorbeeld wanneer u het effect van een index wilt meten. U voert de query dan één keer uit met actieve index en één keer met inactieve index. Is er geen verschil, dan wordt de

index vermoedelijk niet eens gebruikt. Is er wel verschil, dan gaat het hopelijk om een versnelling door de index (waar u helaas niet altijd zeker van kunt zijn, een optimizer is ook maar een optimizer). Inactiveren en later weer activeren van een index gaat met commando’s alter index … inactive respectievelijk alter index … active . Als we een index weer actief maken, wordt deze opnieuw gegenereerd. Daarom kan het soms even duren voordat een inactieve index weer actief is. Voorbeeld 13.5 Meet het effect van de index iAantal bij een query die alle orderregels telt die een aantal groter dan 50 hebben. set plan on; set statistics_display on

(Het tweede commando al dan niet via het statistiekenknopje) select count(*) from   orderregel where  aantal > 50

Resultaat (op ons systeem): PLAN (ORDERREGEL NATURAL) COUNT ========= 62357 Records returned: 1 Elapsed time: 2.13 sec Reads from memory buffer: 3026567 Writes to memory buffer: 0 Page reads: 6081 Page writes: 0 Delta Server memory: 7164

Toelichting – Het queryplan (in de eerste regel) houdt in dat de tabel Orderregel ‘natural’ wordt benaderd, dat wil zeggen een full table scan. – De performance wordt door IQU gemeten, niet door Firebird. Dit betekent dat eventuele vertraging die de query-uitvoering oploopt door andere processen (zoals printen en activiteiten van andere gebruikers), wordt meegemeten. Het vergelijken van metingen is daardoor des te betrouwbaarder naarmate minder andere processen actief zijn. Hierna maken we de index: create index iAantal on Orderregel(aantal);

en voeren de query nogmaals uit: select count(*) from   Orderregel where  aantal > 50

Resultaat: PLAN (ORDERREGEL INDEX (IAANTAL)) COUNT ========= 62357 Records returned: 1 Elapsed time: 0.31 sec Reads from memory buffer: 149569 Writes to memory buffer: 0 Page reads: 6081 Page writes: 0 Delta Server memory: 528756

Het queryplan vertelt ons nu dat tabel Order_ is benaderd via de index iAantal. Hoewel beide query’s relatief snel zijn (wat mede met de kleine records te maken heeft), is de snelheidswinst groot: ongeveer zes keer zo snel! Ook de ‘memory’-statistieken zijn beduidend gunstiger.

13.3.10 De selectiviteit van een index Wanneer een index is gecreëerd, betekent dit nog niet dat deze altijd wordt gebruikt. De optimizer bepaalt per query of het gebruik van een index zinvol is. Daarbij zijn diverse statistische gegevens over de tabel en de geïndexeerde kolommen van invloed. Een van die statistische gegevens is de zogenaamde selectiviteit van een index. De selectiviteit van een index is hoog wanneer de geïndexeerde kolommen veel verschillende waarden bevatten. Bij lage selectiviteit heeft een index niet zoveel zin. Er zijn dan (gemiddeld) te veel records bij een gegeven indexwaarde. Een index op de naam-kolom van de Klant-tabel heeft een hoge selectiviteit en is dus zinvol, mits de tabel groot genoeg is. Het aanbrengen van een index op de behandeld-kolom van Klacht is onzinnig, tenzij u op één waarde zoekt (bijvoorbeeld ‘N’) en er daarvan maar relatief weinig zijn. Zijn er relatief veel van, dan zou u via een de index snel een record met de gezochte waarde vinden, maar daarna moet u toch nog een groot deel van de tabel doorlopen om de andere records met hetzelfde waarde op te halen. In zo’n geval kunt u net zo goed een full table scan uitvoeren, ofwel: de hele tabel doorlopen en rij voor rij het selectiecriterium toepassen. De selectiviteit van een unieke index is maximaal: alle waarden zijn immers verschillend.

13.3.11 Onderhoud van indexen Door het toevoegen, verwijderen of wijzigen van records kan een indexboom ‘scheef’ groeien. Bijvoorbeeld, in de indexboom van klantnummers, als er aan de ‘hoge kant’ van de boom veel meer recordverwijzingen bijkomen dan aan de ‘lage kant’. Het aantal niveaus in bepaalde takken van de boom kan dan onevenredig groot worden. We illustreren dit met de kleine indexboom van figuur 13.7 (hoewel een index natuurlijk alleen zin heeft bij veel grotere aantallen). De boom is door zijn ‘groeigeschiedenis’ (klantnummers die er aan de hoge kant bijkwamen, andere klantnummers die verdwenen) slecht gebalanceerd geraakt.

Figuur 13.7 Slecht gebalanceerde indexboom

De bolletjes geven het einde van de boom aan. Elk knooppunt wijst naar de records met het betreffende klantnummer. Balanceren van indexbomen

In een erg scheve boom moet voor sommige zoekacties de boom tot grote diepte worden doorlopen. Dit heeft een nadelig effect op de performance. We moeten er daarom voor zorgen dat de boom min of meer gebalanceerd blijft. Een boom is maximaal gebalanceerd wanneer de diepte per tak hooguit 1 verschilt.

Opgave 13.1 Balanceer de zoekboom van figuur 13.7 , zodanig dat het resultaat maximaal gebalanceerd is. Omdat het helemaal niet erg is als een index een beetje uit balans is, is het maar goed dat een index niet automatisch wordt gebalanceerd bij het geringste uit balans raken. Het voortdurend reorganiseren van de indexboom zou onnodig veel tijd kosten. Ieder rdbms staat een zekere mate van onbalans toe. Wordt een bepaalde mate van onbalans overschreden, dan wordt de index gereorganiseerd, meestal op een moment dat er geen of weinig activiteiten zijn. Het kan echter geen kwaad van tijd tot tijd zelf de reorganisatie van indexbomen te forceren. Balanceren van een indexboom doet u door het rdbms de index opnieuw te laten opbouwen. Dat kan op twee manieren: 1 door de index inactief te maken en opnieuw te activeren 2 via een ‘backup en restore’ van de database. Index inactief maken en opnieuw activeren Door een index te activeren na hem eerst inactief te hebben gemaakt, dwingt u het rdbms de indexboom opnieuw op te bouwen. Deze methode werkt alleen voor zelf gecreëerde indexen. We doen dit met het al behandelde commando alter index … inactive respectievelijk alter index … active . ‘Backup en restore’ van de database Het balanceren van een standaardindex is alleen mogelijk door een backup en restore van de database. Dit geeft een aantal verbeteringen in de opslag en in de structuur: – defragmentatie van de databasepages – balancering van alle indexen. Opmerking : ‘backup’ betekent hier iets anders dan bestandskopie.

Databasepages zijn de kleinste fysieke eenheden van lezen en schrijven op de harde schijf. Defragmentatie houdt in dat records van één tabel zoveel mogelijk aaneensluitend worden opgeslagen. Records die in de loop der tijd zijn ingevoegd, kunnen namelijk verspreid op de harde schijf zijn komen te staan. Dit werkt erg vertragend. Bij een databasebackup worden géén indexen opgeslagen. Bij de ‘restore’ (het terugvertalen van de backup naar de database) moeten alle indexen dan ook opnieuw worden aangemaakt, met als prettig neveneffect dat ze hierna zijn gebalanceerd. Dit is met name van belang voor de indexen bij primaire en verwijssleutels (en dat zijn de belangrijkste), die immers niet kunnen worden geïnactiveerd. Een ‘backup en restore’ wordt aangeraden na zeer zware databaseoperaties: ‘mass updates en deletes’.

13.3.12 Tips om lange wachttijden te voorkomen In de volgende opgaven gaan we na wat het effect is van het gebruik van indexen. We kijken daarbij naar het queryplan en de statistieken van een query, zowel zonder als met index. Omdat sommige query’s lang kunnen duren is het verstandig eerst set plan on te gebruiken. Als de query echt uitgevoerd wordt, is het goed om het aantal records van het resultaat te beperken. Vóór het afbeelden van een resultaat met veel records waarschuwt IQU voor het grote aantal records: ‘Returned 1000 records, continue?’ Bij een bevestigend antwoord worden de overige records opgehaald en wordt het gehele resultaat getoond, anders blijft het bij de eerste 1000. Voor de performance wordt het wachten op het antwoord niet meegeteld, het ophalen van de resterende records wel. Ook als het aantal records minder groot is, kan de verwerking van een query lang duren. Daarom is het soms handig het aantal records te beperken. Dit kunt u realiseren door het gebruik van first < n > in een query. Door n te verhogen krijgt u een idee of het zinvol is de query ook zonder first uit te voeren. Een voorbeeld van het gebruik van first : select first 100 * from   Order_

first kan nog worden uitgebreid met skip < n > . Een voorbeeld: select first 5 skip 500 * from   Order_

Nu worden de records met nummers 501 t/m 505 getoond.

Verwijder, voordat u de volgende opgaven maakt, de extra indexen die u in dit hoofdstuk hebt aangemaakt. De indexen uit de voorbeelden zijn: iKlant, iDatumKlant, iOrder_Volgnr, iAantal en iOrder_Artikel.

Opgave 13.2 Maak een tabel waarin u de benodigde tijd noteert voor het bepalen van het resultaat van de volgende query: select count(*) from   Orderregel where  bedrag > n

waarbij n achtereenvolgens de waarden 10, 50, 100, 500, 1000, 5000 en 10000 aanneemt. Herhaal iedere query een aantal keren en neem het gemiddelde van de tijden. Maak daarna een index iBedrag op Orderregel.bedrag en herhaal de uitvoering van de query’s.

Zet eventueel de gegevens in een grafiek. Kunt u een verklaring geven voor het verloop van de tijden, zowel per reeks als voor de verschillen tussen de twee reeksen?

Opgave 13.3 Bij deze opgave gaat het om het vergelijken van twee queryexecutieplannen. Zorg ervoor dat u set plan on gebruikt. Vergelijk de join van Order_ en Klant en die van Orderregel en Klacht met elkaar wat betreft het executieplan. Kunt u het verschil verklaren? Gebruik simpele expressies met select * . Opgave 13.4 We willen de gegevens zien van de laatste 10 orderregels, met de gegevens van de laatste orderregel bovenaan. Stel een query op waarbij geen gebruik wordt gemaakt van een index. Creëer daarna een handige index voor deze situatie. Stel een nieuwe query op en ga door middel van het queryplan na dat de nieuwe index inderdaad gebruikt wordt. Wat is het effect op de verwerkingstijd?

13.4 Performanceverbetering door queryaanpassing De performance van één specifieke query kan vaak worden verbeterd door indexen. Maar ook de keuze van de queryformulering kan van grote invloed zijn. In het voorgaande ging het bij het SQL -programmeren voornamelijk om een goede probleemaanpak. Zo kwamen bepaalde oplossingen veelal

‘vanzelf’ tevoorschijn. In deze paragraaf doen we daar niets vanaf, maar er komt wel wat bij: aanpassing van een queryformulering als dat gunstig is voor de performance. Bij één queryprobleem zijn vaak meerdere oplossingen mogelijk. Denk bijvoorbeeld aan: – de keuze tussen een join en een subselect – de keuze tussen een subselect met ( not ) in en een subselect met ( not ) exists of een left outer join – de keuze tussen een distinct en een group by – het al dan niet gebruiken van functies. De optimizer kan het beste omgaan met gewone joins. Het optimaliseren van outer joins en van subselects is aanzienlijk lastiger. Ook de having is niet favoriet bij optimizers. De condities die worden vermeld bij de having -clausule, worden namelijk pas toegepast op het moment dat de rest van de query al is uitgevoerd. Neem daarom condities altijd op in de where clausule, tenzij het om echte groepscondities gaat, die onvermijdelijk onder having thuishoren. Voorbeeld 13.6 In hoofdstuk 9 ‘Subselects en views’ hebben we gezien dat een subselectquery (al dan niet gecorreleerd) vaak ook als joinquery geformuleerd kan worden. We vergelijken nu de performance van een join met die van een subselectmet- in . We doen dat aan de hand van het volgende probleem: geef per order het ordernummer, de datum en het aantal orderregels. Oplossing met een join: select   O.nr, O.datum, count(Orl.volgnr) aantal from     Order_ O join Orderregel Orl on O.nr = Orl.order_ group by O.nr, O.datum

Deze oplossing leidt tot het volgende queryplan: PLAN SORT (JOIN (O NATURAL, ORL INDEX (FK_ORDERREGEL_VAN_ORDER_))

Op ons systeem was 15.68 s nodig om het resultaat te berekenen. We zien: de join start vanuit Order_ (de kleinste van de twee tabellen). Van daaruit

worden de bijbehorende rijen van Orderregel gezocht. Hiervoor wordt de index fk_orderregel_van_order_ van Orderregel gebruikt. Vanwege de group by wordt het resultaat gesorteerd. We vergelijken deze oplossing met de volgende subselect-oplossing: select nr, datum, (-- aantal orderregels select count(*) from   Orderregel where  order_ = O.nr) aantal from   Order_ O

Het queryplan luidt: PLAN (ORDERREGEL INDEX (FK_ORDERREGEL_VAN_ORDER)) PLAN (O NATURAL)

Ons systeem had hier 6.39 s voor nodig. Merk op dat een standaardindex gebruikt wordt: de naam daarvan is gelijk aan die van de constraint (een foreign key in dit geval). Meestal levert het vervangen van een subselect door een join tijdwinst op, vooral bij een gecorreleerde subselect. Immers, voor ieder record in de buitenste select moeten een aantal records voor de binnenste select doorlopen worden. In dit voorbeeld echter is het tegenovergestelde het geval. De oorzaak ligt in het gebruik van group by , waarvoor het eindresultaat eerst gesorteerd moet worden. Verder blijkt dat bij de subselectoplossing een index gebruikt kan worden waardoor de subselect snel gaat. Voor query’s die veel tijd in beslag nemen, kan het zinvol zijn naar een alternatief te zoeken. Als we een probleem moeten oplossen waarbij een subselect met not in nodig is, kunnen we als alternatief geen join gebruiken. Wel is dan not exists een alternatief en ook een left outer join is dan te overwegen. In o pgave 13.5 gaat u het effect van deze alternatieven na. Waarschuwing Sommige query’s, zoals die met een dubbelgeneste subselect met in , kunnen erg lang duren. Gebruik in uw query’s dus first . Wanneer u dat een keer bent vergeten en het te gek wordt, schakel dan de Firebird Server uit (‘shut down’), via het icoontje op de taakbalk (klikken met rechter muisknop). De Boekverkenner moet vervolgens ook worden gesloten en opnieuw geopend. Optimalisatietips Van oudsher zwerven vele lijsten rond met tips om query’s sneller te maken. Omdat de optimizers steeds beter worden, zijn deze tips veelal verouderd. Het is raadzaam om het daadwerkelijke effect van optimalisaties altijd in de praktijk te testen. Het effect verschilt per database en zelfs per versie van de database.

Opgave 13.5 Ga uit van het probleem: ‘Hoeveel orderregels hebben geen bijbehorende klacht?’ en vergelijk de performance van drie oplossingen: a een subselectquery met not in b een subselectquery met not exists c een left outer join Bekijk ook het queryplan en verklaar het grote verschil tussen de a-query en de b-query.

Opgave 13.6 Functies werken ‘subatomair’, dat wil zeggen dat ze een celinhoud niet als totaliteit manipuleren, maar er een bewerking op toepassen. Dit kost relatief veel tijd. Een query zonder functies verdient vaak de voorkeur boven een alternatief met functies. Probeer dit uit voor het volgende probleem: welke namen van klanten beginnen met Aad?

a Meet eerst de performance van de volgende oplossing zonder functie: -- query q1 select * from   Klant where  naam like ‘Aad%’

b Meet daarna de performance met de oplossing waarin we een functie toepassen. -- query q2 select * from   Klant where  substring(naam from 1 for 3) = 'Aad'

Maak daarna een index iNaam op naam voor tabel Klant en herhaal de twee query’s. Neem in alle gevallen ook het queryplan op.

13.5 Performanceverbetering door aanpassing ontwerp Het ontwerp van een database ligt globaal weliswaar vast, maar toch zijn allerlei varianten mogelijk, zeker als (gecontroleerde) redundantie wordt toegestaan. Qua performance kunnen die varianten zich zeer verschillend gedragen. In deze paragraaf bespreken we een aantal standaardvarianten, waar we bij bijna elk databaseontwerp mee te maken hebben.

13.5.1 De keuze van sleutels Bij de keuze van sleutels hebben we in dit boek maar één keer wat langer stilgestaan: bij het Toetjesboek in hoofdstuk 2 . We hebben daar twee varianten bekeken: zonder en met kunstmatige sleutels. Voor de keuze van sleutels bestaan in het algemeen drie mogelijkheden:

1 volledige namen 2 codes 3 nummers De gemaakte keuze kan van grote invloed zijn op de performance. Het is onmogelijk bij voorbaat te zeggen welke structuur de beste is, omdat het antwoord afhangt van de grootte van de tabellen, van het aantal verschillende sleutelwaarden en van de query. We zullen dit toelichten aan de hand van een grote plaatsnamendatabase, van plaatsen over de hele

wereld. We nemen aan dat een plaats uniek wordt bepaald door de plaatsnaam en het land. Figuur 13.8 geeft drie mogelijkheden voor de identificatie van een land en dus voor de primaire én de verwijzende landsleutels: de volledige landnaam, een begrijpelijke landcode en een betekenisloos landnummer (nr). Om het simpel te houden, hebben we afgezien van een gestandaardiseerde continententabel.

Figuur 13.8 Drie varianten voor de keuze van een sleutel

Bij het vergelijken van de structuren moeten de volgende punten tegen elkaar worden afgewogen. – Joinen van grote tabellen kost veel tijd, ook al zijn er standaardindexen aangemaakt. Vermijden van een join kan dus gunstig zijn. – Het vergelijken van alfanumerieke waarden neemt veel meer tijd in beslag dan het vergelijken van numerieke waarden. Joinen over numerieke sleutels (met numerieke indexen) gaat daardoor sneller dan joinen over alfanumerieke sleutels (met alfanumerieke indexen). – Voor verwijssleutels is de lengte een factor van belang, met het oog op geheugenruimte en transporttijd. Gebruik van codes of nummers kan in dit opzicht gunstig zijn, zeker als de kindtabel veel groter is dan de oudertabel. Volledige namen Zie figuur 13.8 a. Deze structuur heeft als voordeel dat minder vaak hoeft te worden gejoind. Snelheidswinst is er bij query’s die de volledige landnaam vragen, maar niet het continent. Bij query’s die wel het continent bij een plaats vragen, moet echter worden gejoind over een alfanumerieke waarde en dat gaat niet snel. De recordgrootte in bytes van de Plaats-tabel wordt door de lange verwijzingen flink groter. Codes

Zie figuur 13.8 b. Bij deze structuur hoeft niet te worden gejoind als kan worden volstaan met de landcode. Dit voordeel geldt in het algemeen bij gebruik van betekenisvolle en bekende codes. Als wel moet worden gejoind, gaat dit sneller dan bij lange namen, maar langzamer dan bij nummers. De recordgrootte in bytes van de Plaats-tabel blijft klein (een code van vier karakters is even groot als een integer ). Nummers Zie figuur 13.8 c. Een nadeel is dat altijd moet worden gejoind als de landnaam een rol speelt. De landnummers hebben immers geen betekenis voor de gebruiker. Het joinen gaat echter relatief snel, want er wordt gezocht via een numerieke index. De recordgrootte in bytes van de Plaats-tabel blijft ook hier klein. Samengestelde sleutels De primaire sleutel van Plaats ligt over de combinatie van plaatsnaam en land. Als er verder geen verwijzingen zijn naar deze tabel is dat prima maar als dat wel het geval is, hebben we te maken met een trage toegang, zoals we inmiddels al weten op basis van tabel Orderregel van de GrootOrderdatabase. In geval van een samengestelde sleutel kan het dus voordelig zijn over te gaan op een numerieke primaire sleutel. Welke structuur de beste is, is dus per query verschillend. We moeten kijken welke query’s het meest voorkomen. In de praktijk is het verstandig om kleine opzoektabellen (‘lookups’) met weinig waarden te voorzien van een code en grote tabellen van een ‘nr’. Wanneer snelheid echt belangrijk is, probeer dan de primaire sleutels klein te houden, bij voorkeur numeriek en over één kolom. Vermijd in elk geval lange alfanumerieke velden. Ter illustratie zullen we nu een experiment uitvoeren met een gewijzigde versie van de GrootOrderdatabase: GrootOrderdatabase KS . Deze heeft een extra kunstmatige sleutel, in tabel Orderregel. Zie figuur 13.9 en opga ve 13.7 .

Figuur 13.9 GrootOrderdatabaseKS

Opgave 13.7 In opgave 13.5 loste u het probleem ‘Hoeveel orderregels hebben geen bijbehorende klacht?’ op drie manieren op: met een subselectquery met not in , met een subselectquery met not exists en met een left outer join . U vergeleek de performance. Herhaal nu dit experiment met gebruik van GrootOrderdatabase KS . Is er sprake van snelheidswinst?

13.5.2 Redundantie en constraints Het principe van ‘single point of definition’ leidde in deel 2 tot genormaliseerde gegevensstructuren. Daarin had ‘elk soort ding zijn eigen tabel’ en was redundantie goeddeels uitgebannen. Alleen ‘gecontroleerde redundantie’ werd toegestaan, zoals bij de berekende waarde van energie per persoon van het Toetjesboek. Bij het verwerken van transacties kost het bijwerken extra tijd, maar de performance van sommige opvragingen kan er flink door worden verbeterd. In de GrootOrderdatabase heeft tabel

Order_ een berekende kolom totaalbedrag en Orderregel een berekende kolom bedrag. Ook hier is sprake van een compromis: query’s op de tabellen gaan veel sneller als de waarde van de kolom al berekend is. Een wijziging van een van de tabellen zal echter trager verlopen. In het algemeen moeten we goed de verhouding kennen tussen het aantal query’s en het aantal wijzigingen om te weten of het voordelig is een berekende waarde in een kolom op te nemen. Figuur 13.10b geeft een ander voorbeeld van een redundante structuur. Tabel Artikelgroep (zie figuur 13.10 a) bevat slechts drie records. Daarom kunnen we de gegevens van tabel Artikelgroep ook redundant opnemen in tabel Artikel (zie figuur 13.10 b).

Figuur 13.10 Performancewinst door denormalisatie

Er zijn databases waarin dit soort denormalisatie groot voordeel oplevert: historische databases , waarbij opslagruimte geen probleem is en de snelheid van opvragen cruciaal. Feitelijk praten we hier over datawarehouses , die veelal regelmatig worden gevuld vanuit een of meer andere (meestal wél genormaliseerde) brondatabases. Doordat de gegevens er ‘integer’ ingaan en daarna niet meer worden gewijzigd, is redundantie geen probleem. De keuze ‘redundantie wel of niet toestaan’ hangt dus mede af van het soort verwerking dat het meeste voorkomt en van de vraag of het om een historische database gaat. Datzelfde geldt voor het wel of niet opnemen van constraints. In paragraaf 10.1.2 onderscheidden we drie soorten systemen, vanuit het gezichtpunt van de ‘levenswijze’ van een database: OLTP -systemen, transactionele bedrijfssystemen en datawarehouses. We zullen deze nu opnieuw bekijken en zien wat hun ‘levenswijzen’ betekenen voor indexen en constraints. OLTP-systemen Bij OLTP (online transactional processing) ligt de nadruk op verwerkingssnelheid, in het bijzonder bij de opslag van nieuwe records. De programmatuur die de transacties verwerkt, is zo gemaakt dat vele

beperkingsregels bij voorbaat niet worden overtreden. Daarom worden OLTP -databases vaak niet uitgerust met verwijssleutelconstraints en dergelijke. Ook kan dat een reden zijn zo min mogelijk indexen aan te leggen. Transactionele bedrijfssystemen Bij transactionele bedrijfssystemen moet meestal aan heel veel bedrijfsregels worden voldaan. Constraints zijn daarom erg belangrijk. Transacties zijn hier meestal niet al te massaal. De nadruk ligt dan ook veel minder op verwerkingssnelheid van transacties. Ingewikkelde selectquery’s zijn natuurlijk mogelijk en die moeten zoals altijd efficiënt worden verwerkt. Daarom zijn ook indexen belangrijk. Datawarehouses Voor het updaten van datawarehouses kunnen primaire sleutels van belang zijn, om dubbele rijen te voorkomen. Als dat tenminste al niet via datumvelden (historie!) wordt gegarandeerd. Van primaire sleutels kunnen we verder profijt hebben door de automatisch aangelegde index. Dat ook voor verwijssleutels. In het algemeen zijn indexen bij datawarehouses erg belangrijk, omdat de query’s intensieve zoekprocessen met zich meebrengen.

Oefenopgaven De volgende opgaven zijn gebaseerd op GrootOrderdatabase. Voor de diagrammen: zie de Boekverkenner of voorin dit hoofdstuk.

Opgave 13.8 Gegeven is de volgende select -query (die per artikel de artikelgroep en het aantal niet-behandelde klachten ophaalt): --fo GrootOrderdatabase select   A.nr, A.omschrijving artikel, Ag.omschrijving artikelgroep, count(*) from     Artikel A left outer join Artikelgroep Ag on A.artikelgroep = Ag.code join Orderregel Orl on A.nr = Orl.artikel join Klacht Kht on Orl.order_ = Kht.order_ and Orl.volgnr = Kht.volgnr where    behandeld = 'N' group by A.nr, A.omschrijving, Ag.omschrijving

a Ga uit van 3 artikelgroepen, 200 artikelen, 1.500 .000 orderregels en 7500 klachten, waarvan 400 niet-behandelde, en bepaal het maximum aantal rijen dat in het interne geheugen (of in een overloopgeheugen op schijf) aanwezig zou zijn, indien gejoind werd in de volgorde van de joinexpressie, met daarna toepassing van de selectieconditie.

b Wat is de optimale verwerkingsvolgorde ten aanzien van geheugengebruik (uitgedrukt in aantal rijen)? Wat is daarbij het maximum aantal rijen in het interne geheugen?

Opgave 13.9 De query van opgave 13.8 werd door Firebird uitgevoerd volgens het volgende queryplan: PLAN SORT( JOIN (JOIN (JOIN (A NATURAL, AG INDEX (PK_ARTIKELGROEP)), ORL INDEX (FK_ORDERREGEL_BIJ_ARTIKEL) ), KHT INDEX (FK_KLACHT_BIJ_ORDER_REGEL) ) )

Verklaar dit queryplan. Opmerking : we hebben voor de duidelijkheid de opmaak aangepast.

Opgave 13.10 We brengen in de query van opgave 1 een kleine wijziging aan (om het aantal klachten te krijgen over artikel 152): select A.nr, A.omschrijving, Ag.omschrijving, count(*) from   Artikel A join Artikelgroep Ag  on A.artikelgroep = Ag.code join Orderregel   Orl on A.nr = Orl.artikel join Klacht       Kht on Orl.order_ = Kht.order_ and Orl.volgnr = Kht.volgnr where  A.nr = 152 group by A.nr, A.omschrijving, Ag.omschrijving

Voorspel welk queryplan gekozen zal worden en vergelijk uw voorspelling met het door Firebird gekozen queryplan.

Opgave 13.11 Voorspel het queryplan bij: select   * from     Artikel order by verkoopprijs

Als vervolgens een index wordt aangemaakt op verkoopprijs, wat zal dan het queryplan worden?

Opgave 13.12 Artikelgroepen hebben niet-numerieke codes als primaire sleutelwaarden. Is het zinvol deze te vervangen door numerieke waarden?

14 Aanpak van queryproblemen In de vorige hoofdstukken zijn alle ingrediënten aangedragen voor het formuleren van SQL -query’s. We hebben oplossingen gevonden voor queryproblemen, en vaak meer dan één. Vooral select -query’s konden echt lastig zijn, maar ook update -problemen bleken wel eens hoofdbrekens te kosten. In enkele gevallen werd een stappenplan aangegeven, ter illustratie van hoe je complexe problemen kunt aanpakken. Zo’n stappenplan lijkt soms een soort van dwingende logica te bevatten waardoor je achteraf denkt: zo moet het en het kan niet anders. Maar nu vóóraf: bestaat er een methode om queryproblemen op te lossen? Is die achteraf-logica te ontdekken, en zo ja, hoe dan?

Zo héél eenvoudig ligt het in elk geval niet, al was het alleen al vanwege de verschillende oplossingen voor één vraagstuk of deelprobleem. Bijvoorbeeld de keus tussen een join en een subselect. Toch is het goed mogelijk problemen gestructureerd aan te pakken en welbewust keuzen te maken. Daar gaat dit hoofdstuk over, onder het motto: ‘Het oplossen van SQL -problemen is een sport die je kunt leren’. We beperken ons tot select -query’s, maar de aanpak gaat net zo goed op voor complexe delete - of update -problemen. Performance In dit hoofdstuk lossen we elk probleem weer op vanuit een zuiver conceptueel standpunt. Optimalisatie van de performance (minimaliseren van benodigde tijd en geheugen) is belangrijk, maar komt ná het schrijven van goede code.

Voorbeelddatabase De voorbeelddatabase van dit hoofdstuk is Ruimtereisbureau. In figuur 14.1 herhalen we het strokendiagram.

14.1 Warming-up In deze paragraaf bieden we een kleine warming-up aan: SQL -opgaven om een beetje ‘warm te lopen’ voor de wat lastiger problemen verderop. Vooraf geven we wat ruimtereisterminologie. Ruimtereisterminologie In sommige opgaven van dit hoofdstuk is sprake van een ‘passage’, in andere van een ‘landing’. We spreken hierover het volgende af: een passage is een bezoek van 0 dagen en een landing is een bezoek van één of meer dagen.

Figuur 14.1 Ruimtereisbureau: strokendiagram

Opgave 14.1 Gegeven is het volgende vluchtschema, van ruimtereis nummer 38: Het vertrek is op 3 maart 2021; dan wordt, per ruimteveer ( RV ), koers gezet naar Venus. Daar is een verblijf gepland van 3 dagen. Dan wordt teruggevlogen naar de aarde voor een passage, waarna de ruimtereizigers koers zetten naar Mars, voor een verblijf van één week. Daarna wordt teruggekeerd naar de aarde. Verder is gegeven dat de reis in totaal 520 dagen duurt en 3.350.000 euro kost. Er is vooralsnog één inschrijving voor deze reis, door een nieuwe klant, genaamd Linda, geboren op 21 juni 1985. Zij krijgt het eerste vrije klantnummer. Geef, in de vorm van een script, de benodigde insert ’s in de database. Indien u dit script uitprobeert, laat het dan volgen door een rollback.

Opgave 14.2 Welke ruimtereizen (geef reisnummer, vertrekdatum en reisduur) hebben een Mars-landing op het programma? Opgave 14.3 Geef een overzicht van alle hemellichamen. Elk hemellichaam moet daarin verschijnen met in een tweede kolom – indien van toepassing – het hemelobject waarvan het satelliet is, en in een derde kolom – wederom indien van toepassing – het hemelobject waarvan dát hemelobject satelliet is.

Opgave 14.4 Elke planeet heeft een aantal manen. Geef alle voorkomende aantallen, elk aantal één keer. Opgave 14.5 Geef voor elke ruimtereis het reisnummer en het aantal verschillende hemelobjecten dat wordt bezocht. Opgave 14.6 Geef reisnummer en vertrekdatum van de ruimtereis (of ruimtereizen) met het grootste aantal deelnemers. Geef twee oplossingen: a met nesting van statistische functies (niet in Firebird mogelijk): max ( count (...)) b met een view.

Opgave 14.7 Welke reizen (geef het reisnummer) hebben één hemelobject als reisdoel? Opgave 14.8 Welke ruimtereizen (geef reisnummer, vertrekdatum en reisduur) bezoeken ten minste één maan van Mars? Opgave 14.9 Geef de reisnummers van alle reizen met een landing op een planeet. Opgave 14.10 a Geef de reizen (met reisnummer en vertrekdatum) met tenminste één bezoek aan Mars en/of de Maan. b Geef de reizen met één of meer bezoeken aan Mars én de Maan.

14.2 Probleemaanpak door vragen en antwoorden Bij het oplossen van een select -probleem zijn belangrijke vragen:

– Vraag 1 : Waarover wordt informatie gevraagd? – Vraag 2 : Uit welke tabellen is de gevraagde informatie afkomstig? – Vraag 3 : In welke tabellen staan de gegevens die een rol spelen? Het antwoord op vraag 1 leidt tot één tabel. Immers, een goed overzicht is homogeen , in de zin dat het over één type informatieobject gaat. Zo’n type is altijd te herleiden tot één tabel. Overzichten die niet homogeen zijn, noemen we ook wel hybride . Hybride overzichten moeten bij voorkeur worden opgesplitst in homogene deeloverzichten. (Zie ook onze opmerkingen over de full outer join, zie paragraaf 7.8 .) Vraag 2 kan tot extra tabellen leiden. Er kan informatie worden gevraagd die behalve uit de tabel van vraag 1 uit gerelateerde tabellen afkomstig is, via navigatie naar andere tabellen (‘naar boven’ of, in combinatie met statistische verdichting, ‘naar beneden’). Vraag 3 kan behalve de tabellen van vraag 2 nog extra tabellen opleveren: tabellen die betrokken zijn bij conditites waaraan de informatie moet voldoen. De antwoorden op de drie vragen geven houvast bij het navigeren door de database en bij het stellen van extra vragen op de juiste momenten.

14.2.1 De belangrijkste vraag: waarover wordt informatie gevraagd? Waarover, over welke entiteittypen, wordt informatie gevraagd? Dat is de eerste en belangrijkste vraag. Omdat bij een goed gemodelleerde database elk entiteittype zijn eigen tabel heeft, wijst het antwoord naar één tabel. Die tabel is in principe de start van het navigatiepad. We geven vier eenvoudige voorbeelden. Voorbeeld 14.1 Welke klanten hebben een reis gemaakt met een bezoek aan Mars? Geef een overzicht met klantnamen. Hier wordt een klantenoverzicht gevraagd. De tabel Klant is de start van het navigatiepad. Bij stapsgewijs oplossen kunnen we beginnen met: select naam

from  Klant where deze klant heeft een reis gemaakt met een bezoek aan Mars

We kunnen vervolgen met een subselectconditie, hoewel ook is te overwegen in de from -clausule een join op te nemen. Voorbeeld 14.2 Welke klanten hebben een reis gemaakt met een bezoek aan Mars? Geef een overzicht met klantnamen en het betreffende reisnummer. Op het eerste gezicht wordt hier eveneens een klantenoverzicht gevraagd. Bij nadere beschouwing blijkt het echter om deelnamen (of deelnemers) te gaan, combinaties van een klant en een reis. De start van het navigatiepad is dus Deelname. Omdat geen klantnummers maar klantnamen worden gevraagd, moeten we Deelname joinen met Klant: select D.naam from  Deelname D join Klant K on D.klant = K.nr where deze deelnemer neemt deel aan een reis met een bezoek aan Mars

Uit oogpunt van programmeerstijl is het nu belangrijk de volgorden van het navigatiepad aan te houden. Het is een Deelname-overzicht, dus zetten we Deelname voorop. De join is ook niets anders dan een verbrede Deelname-tabel. Voorbeeld 14.3 Geef van alle reizen het reisnummer en het aantal deelnemers. Hier wordt een reisoverzicht gevraagd: het navigatiepad start ‘in principe’in Reis. Al denken we bij ‘aantal deelnemers’ misschien direct aan groeperen, de volgende aanpak is niet zo gek: select nr, aantal_deelnemers from   Reis

In hoofdstuk 9 hebben we gezien dat zoiets inderdaad kan: ook in een select -clausule mag een subselect worden opgenomen. Een raamwerk voor de oplossing met groeperen: select   R.nr, count(…) from     Reis R left outer join Deelname D on … group by R.nr

Over de details moeten we goed nadenken, maar de structuur ligt vast. Voorbeeld 14.4 Geef van alle reizen met tenminste 3 deelnemers het reisnummer en het aantal deelnemers.

Ten opzicht van voorbeeld 14.3 verandert er in principe niet veel. Er komt een having -clausule bij, en de left outer join mogen we vervangen door een inner join: select   R.nr, count(*) from     Reis R join Deelname D on … group by R.nr having   count(*) >= 3

Maar omdat nu alleen reisnummers worden gevraagd, kan het eenvoudiger: select   nr, count(*) from     Deelname group by reis having   count(*) >= 3

Conceptueel gezien start het navigatiepad nog steeds in Reis. Alléén vanwege het feit dat slechts primaire-sleutelwaarden worden gevraagd, kunnen we de oplossing vereenvoudigen. Natuurlijk is eenvoud een belangrijke kwaliteit. Anderzijds is deze oplossing minder mooi vanuit het oogpunt van generaliseerbaarheid. Een kleine variatie in de vraagstelling (weglaten van de conditie) maakt een hele andere oplossing noodzakelijk.

14.2.2 De keuze tussen een subselect en een join Het geheim van een goede oplossingsstrategie is een stapsgewijze aanpak, waarbij steeds de juiste vragen worden gesteld. Het begint bij de fundamentele vraag ‘Waarover wordt informatie gevraagd?’, leidend tot inzicht in de start van het navigatiepad. Maar ook de andere in de inleiding van paragraaf 14.2 genoemde vragen zijn belangrijk: uit welke tabellen is de gevraagde informatie afkomstig? En: in welke tabellen staan de gegevens die een rol spelen? De antwoorden confronteren ons bij herhaling met oplossingsalternatieven waaruit weloverwogen een keuze moet wordt gemaakt, zoals de keuze tussen een subselect en een join. Voorbeeld 14.5 Welke ruimtereizen (geef reisnummer, vertrekdatum en reisduur) hebben een Mars-landing in hun programma? Vraag 1: Antwoord:

Waarover wordt informatie gevraagd? Over reizen; het navigatiepad start dus in Reis.

Vraag 2: Antwoord: Vraag 3: Antwoord:

Uit welke tabellen is de gevraagde informatie afkomstig? Uit alléén Reis. In welke tabellen staan de gegevens die een rol spelen? In Reis en Bezoek.

Vraag 2 stuurt in de richting van een subselectaanpak, terwijl vraag 3 een joinaanpak suggereert. We zullen het probleem via beide aanpakken oplossen, zie ook figuur 14.4 .

Figuur 14.4 Twee vragen: twee aanpakken

Aanpak vanuit vraag 1 en vraag 2 Dit is de subselectaanpak. Het mooie van deze aanpak is dat we in eerste instantie maar naar een stukje van het probleem hoeven te kijken, net voldoende om een begin te kunnen maken: select nr, vertrekdatum, duur from   Reis where  ...

De conditie (verblijf op Mars) heeft betrekking op tabel Bezoek, en wordt dus geformuleerd door een eis te stellen aan de sleutelwaarden die verwijzen vanuit Bezoek naar Reis: select nr, vertrekdatum, duur from   Reis where  nr in ( de verzameling reisnr's van Bezoek die aan de voorwaarde voldoen )

wat resulteert in: select nr, vertrekdatum, duur from   Reis where  nr in (select reis from   Bezoek

where  hemelobject = 'Mars' and verblijfsduur > 0)

Aanpak vanuit vraag 1 en vraag 3 Dit is de joinaanpak. Het idee is: de join van alle betrokken tabellen bevat wellicht, naast de gevraagde gegevens voor het overzicht, ook alle gegevens voor de condities. Op grond hiervan zóuden we als volgt kunnen beginnen: select ... from   Reis R join Bezoek B on R.nr = B.reis where  ...

waarna de aanvullende conditie en de te selecteren gegevens geen probleem meer vormen: select distinct R.nr, vertrekdatum, duur from   Reis R join Bezoek B on R.nr = B.reis where  hemelobject = 'Mars' and verblijfsduur > 0  -- landing

Resultaat: NR VERTREKDATUM      DUUR ========= ============ ========= 32 03-jun-2022        390 34 10-jan-2023       1380 35 12-mrt-2023        380 36 27-jun-2023       1340 37 17-jul-2023          3

Let op de distinct . Deze is nodig omdat de join hier een verbrede Bezoek-tabel is, en bij één ruimtereis in principe meerdere Bezoek-rijen kunnen horen die aan de conditie voldoen (een ruimtereis met eerst 2 dagen Mars, daarna door naar Jupiter, en op de terugweg nog even een paar dagjes Mars). Eigenlijk houdt deze distinct een impliciete groepering in van de rijen van de join op de Reis-kenmerken nr, vertrekdatum en duur. Expliciet groeperen is dan mooier, al lijkt het ingewikkelder: select   R.nr, vertrekdatum, duur from     Reis R join Bezoek B on R.nr = B.reis where    B.hemelobject = 'Mars' and B.verblijfsduur > 0 group by R.nr, R.vertrekdatum, R.duur

Hierbij moet helaas weer ‘verfijnd’ worden gegroepeerd, ook al zijn R.vertrekdatum en R.duur functioneel afhankelijk van R.nr. Zie paragraaf 8.4.2 .

Subselectaanpak versus joinaanpak

De subselectaanpak heeft als voordeel dat we steeds zoveel mogelijk van het probleem voor ons uit kunnen schuiven. Vooral bij complexe problemen kan deze aanpak ons helpen de complexiteit de baas te blijven. In elk stadium wordt het probleem niettemin ‘volledig en exact’ opgelost, al is dat nog voor een deel in natuurlijke taal. Voor de praktijk is belangrijk welke van beide oplossingen de snelste is. Het antwoord is per SQL-interpreter verschillend. Moderne SQL-optimizers zijn dermate ‘slim’ dat er grote kans bestaat dat een query die naar het rdbms wordt gestuurd, intern volledig wordt herschreven. Zo is het heel goed denkbaar dat een subselect intern wordt herschreven als een join. We neigen daarom tot het advies om over het algemeen te kiezen voor de mooiste, meest leesbare oplossing. Het applicatieonderhoud en het foutzoeken worden hierdoor aanzienlijk vergemakkelijkt. Ook het toevoegen van verklarend commentaar kan hieraan bijdragen. Wanneer snelheid echt een probleem is, kunnen alternatieven worden overwogen en performancemetingen worden verricht. ‘Mooi en leesbaar’ impliceert ook dat alle volgorden in de code worden aangepast aan het navigatiepad. Met name bij joinnavigatie, want bij subselectnavigatie gaat dat vanzelf.

14.2.3 Een voorbeeld met groeperen Het volgende voorbeeld bevat een statistische voorwaarde in de vraagstelling: Voorbeeld 14.6 Welke ruimtereizen hebben een totale verblijfsduur op de bezochte hemelobjecten van tenminste 14 dagen? Geef reisnummer, vertrekdatum en reisduur. De drie vragen met hun antwoorden: Vraag 1: Antwoord: Vraag 2: Antwoord: Vraag 3: Antwoord:

Waarover wordt informatie gevraagd? Over reizen; het navigatiepad start dus in Reis. Uit welke tabellen is de gevraagde informatie afkomstig? Uit Reis. In welke tabellen staan de gegevens die een rol spelen? In Reis en Bezoek.

Ook hier geven we twee oplossingen die elk afhangen van de eerst gestelde vraag. Aanpak vanuit vraag 1 en vraag 2 Kijken we alleen naar de start van het navigatiepad (vraag 1) en de gevraagde gegevens (vraag 2) dan leidt dit tot: select nr, vertrekdatum, duur from   Reis

where  ...

Nu stellen we een vervolgvraag, voor de selectieconditie: Vraag: Antwoord:

Om welke reizen gaat het? Die waarvan de totale verblijfsduur op de bezochte hemelobjecten tenminste 14 dagen is.

Deze statistisch getinte voorwaarde wordt nu via een gecorreleerde subselect gerealiseerd: select nr, vertrekdatum, duur from   Reis R where  14 = 14

Hoewel conceptueel gesproken groeperen op reisnr voldoende zou moeten zijn, is subgroepering op beide andere reiskenmerken hier verplicht, zie p aragraaf 10.4.2 .

Voorbeeld 14.7 Welke ruimtereizen hebben een totale verblijfsduur op de bezochte hemelobjecten van tenminste 14 dagen? Geef reisnummer, vertrekdatum, reisduur en de totale verblijfsduur. Bijna dezelfde vraagstelling als van voorbeeld 14.6 dus. Op het eerste gezicht lijkt nu de eerste aanpak af te vallen. Maar we proberen het toch! Aanpak vanuit vraag 1 en vraag 2 We krijgen nu (de eerste aanpak van voorbeeld 14.6 volgend): select R.nr, R.vertrekdatum, R.duur, totale verblijfsduur van deze reis from   Reis R where  14 = 14

14.2.4 Een voorbeeld met een recursieve verwijzing Een recursieve verwijzing leidt al snel tot de noodzaak meerdere exemplaren van dezelfde tabel in de vraagstelling te betrekken, elk met een eigen alias. Voorbeeld 14.8 Welke manen draaien om dezelfde planeet als Triton, afgezien van Triton zelf? De zinsnede ‘afgezien van Triton zelf’ is niet overbodig, Triton draait immers om dezelfde planeet als Triton! De vragen, met hun antwoorden: Vraag 1: Antwoord:

Vraag 2: Antwoord: Vraag 3: Antwoord:

Waarover wordt informatie gevraagd? Over hemelobjecten in een speciale rol: manen die worden gezocht (in tegenstelling tot de maan die is gegeven). Het navigatiepad start dus in een exemplaar van Hemelobject (alias: MaanGezocht). Uit welke tabellen is de gevraagde informatie afkomstig? Uit MaanGezocht. In welke tabellen staan de gegevens die een rol spelen? In MaanGezocht en in een tweede exemplaar van Hemelobject om de gegeven maan (Triton) in op te zoeken (alias: MaanGegeven).

Aanpak vanuit vraag 1 en vraag 2

Deze geeft het volgende begin: select naam from   Hemelobject where  moederobject = naam van hemelobject waar Triton omheen draait and het hemelobject is niet Triton zelf

leidend tot: select naam from   Hemelobject where  moederobject = (-- naam van hemelobject waar Triton omheen draait select moederobject from   Hemelobject where  naam = 'Triton') and naam 'Triton'

Resultaat: OBJECTNAAM ========== Naiad Thalassa Despina Galathea Larissa Proteus Nereïde

Weinig fraai in deze oplossing is het drievoudig voorkomen van ‘Triton’. Beter is ook hier ‘single point of definition’ na te streven: wanneer de vraagstelling verandert (Triton wordt bijvoorbeeld Ganymedes) moet je in de oplossing maar op één plaats de naam hoeven te wijzigen. Helaas lukt dit niet: SQL kent geen variabelen om zo’n naam in op te slaan. Aanpak vanuit vraag 1 en vraag 3 Dit leidt tot de volgende autojoin, met als joinconditie gelijkheid van de verwijzende moederobject-sleutelwaarden: select MaanGezocht.naam from   Hemelobject MaanGezocht join Hemelobject MaanGegeven on MaanGezocht.moederobject = MaanGegeven.moederobject where  MaanGegeven.naam = 'Triton' and MaanGezocht.naam MaanGegeven.naam

Merk op dat we hier wel een single point of definition hebben voor de gegeven maan. Dank zij het gebruik in de laatste regel van MaanGegeven.naam in plaats van 'Triton'.

14.2.5 Join in subselect of geneste subselect? Ook binnen een subselect kun je de keuze hebben tussen een join en een (geneste) subselect, zoals in voorbeeld 14.9 wordt geïllustreerd. Voorbeeld 14.9 Bij welke ruimtereizen (geef reisnummer, reisduur en vertrekdatum) wordt tenminste één maan van Jupiter bezocht of gepasseerd? We geven twee oplossingen, met verschillende vraag-enantwoordsequenties. Oplossing 1 Vraag 1: Antwoord: Vraag 2: Antwoord: Vraag: Antwoord: Vraag: Antwoord:

Waarover wordt informatie gevraagd? Over reizen. Het navigatiepad start dus in Reis. Uit welke tabellen is de gevraagde informatie afkomstig? Uit alléén Reis. Om welke reizen gaat het? Om reizen naar een maan van Jupiter. Welke tabel bevat de reisnummers van die reizen? Bezoek.

Dit geeft: select reisnr, duur, vertrekdatum from   Reis where  nr in (-- reisnummers van reizen die een maan van Jupiter aandoen select reis from   Bezoek where  ... )

We vervolgen met: Vraag: Antwoord:

Om welke Bezoek-rijen gaat het? Bezoek-rijen die verwijzen naar een maan van Jupiter.

Het resultaat is een geneste subselect: select nr, duur, vertrekdatum from   Reis where  nr in (-- reisnummers van reizen die een maan van Jupiter aandoen select reis from   Bezoek where  hemelobject in (-- namen van manen van Jupiter select naam from   Hemelobject

where  moederobject = 'Jupiter'))

Oplossing 2 Deze oplossing gaat uit van dezelfde vraag-en-antwoordsequentie, tot en met het derde antwoord. Daarna wordt een andere weg ingeslagen, leidend tot een join: Vraag 1: Antwoord: Vraag 2: Antwoord: Vraag: Antwoord: Vraag: Antwoord:

Waarover wordt informatie gevraagd? Over reizen. Het navigatiepad start dus in Reis. Uit welke tabellen is de gevraagde informatie afkomstig? Uit alléén Reis. Om welke reizen gaat het? Reizen naar een maan van Jupiter. Welke tabellen zijn bij die conditie betrokken? Bezoek en Hemelobject.

Dit geeft: select nr, duur, vertrekdatum from   Reis where  nr in (-- reisnummers van reizen die een maan van Jupiter aandoen select B.reis from   Bezoek B join Hemelobject H on B.hemelobject = H.naam where  H.moederobject = 'Jupiter')

De problemen in deze paragraaf waren eenvoudig. Bij complexe problemen is het niet meer een kwestie van een simpele keuze tussen de vragen 1 en 2 of de vragen 1en 3 als vertrekpunt. Bij het stapsgewijs oplossen moeten herhaaldelijk dergelijke keuzen worden gemaakt, voor deelproblemen op elk niveau. Het is daarbij echter goed de oplossingsvarianten van simpele problemen in het achterhoofd te houden.

14.3 Stappenplan ’Moeilijke opgaven bestaan niet. Wel vraagt de ene opgave wat meer oplossingsstappen dan de andere’. Dat is de stelling die we graag zouden willen verdedigen. En ook al is het misschien niet helemáál waar: wanneer je elk probleem op de juiste manier gefaseerd oplost, reduceer je het tot een opeenvolging van problemen die stuk voor stuk eenvoudiger zijn dan het hoofdprobleem. Hanteer daarbij het volgende stappenplan: Stap 1: Stap 2: Stap 3:

Ga na of de vraagstelling 100% duidelijk is. Ga na hoe je het probleem handmatig zou oplossen. Overweeg alternatieve aanpakken.

Stap 4:

Transformeer het probleem stapsgewijs (vraag-enantwoord) naar SQL .

Wie te snel naar een eindresultaat wil, raakt veelal verstrikt in de complexiteit van het probleem. Vaak blijkt achteraf dat de vraagstelling niet duidelijk was (Stap 1). Als de vraag wel duidelijk is, blijkt zelfs een beginnend SQL -programmeur meestal wel een handmatige oplossing te kunnen vinden voor een probleem, bij een min of meer illustratieve voorbeeldpopulatie (Stap 2). Hier ligt de sleutel voor een algemene oplossing in de vorm van een SQL -query. De handmatige werkwijze moet expliciet worden gemaakt en je moet je bewust worden van tussenresultaten die een rol spelen. Alvorens door te stomen naar een select -query, is het verstandig stil te staan bij mogelijke alternatieve aanpakken (Stap 3). Daarna vertaal je de gekozen werkwijze stapsgewijs naar steeds completere deeloplossingen (Stap 4), volgens de vraag-enantwoordmethode van de vorige paragraaf. Gebruik natuurlijke taal voor de nog onopgeloste deelproblemen: je moet een complex probleem stap voor stap ‘afpellen’. Voorbeeld 14.10 Het Ruimtereisbureau hanteert de volgende regel: ’Niemand mag aan een interplanetaire reis deelnemen, zonder eerst een Maan-reisje te hebben gemaakt. De achtergrond van deze regel is dat je wel moet weten waar je aan begint. Probleem : welke deelnemers zijn – in strijd met de regels – ingeschreven voor een interplanetaire reis zonder eerst een Maan-reisje te hebben gemaakt? Stap 1: is de vraagstelling 100% duidelijk? Een paar kritische vragen zijn op zijn plaats: 1 Wat wordt bedoeld met een ‘deelnemer’? Een klant? Of is het een klant-in-een-reis, het type object zoals opgenomen in tabel Deelname? En wat willen we precies van zo’n ‘deelnemer’ weten? 2 Wat is een ‘Maan-reisje’ precies? Is een reis die twee bezoeken aflegt aan de Maan maar verder geen andere planeten of manen bezoekt, ook een ‘Maan-reisje’? Een denkbeeldige deskundige laten we als volgt de vraagstelling preciseren: Een ‘deelnemer’ is een klant/reis-combinatie en synoniem met ‘deelname’. We willen weten: reisnummer, vertrekdatum en klantnaam. Een Maanreisje is een reis met in totaal één bezoek, en dat moet een bezoek zijn aan de Maan. Stap 2: hoe zou je het probleem handmatig oplossen?

Je zou de voorbeeldpopulatie als volgt kunnen onderzoeken: per Deelname-rij kijk je of het om een deelname aan een niet-Maan-reisje gaat. Als dat het geval is, kijk je of er een eerdere deelname is van de klantdeelnemer aan een Maanreisje. Stap 3: kan het ook anders, handiger misschien? Zoals helaas in de praktijk vaak gebeurt, slaan we deze belangrijke stap even over. We zullen zien wat de gevolgen zijn. Stap 4: stapsgewijze transformatie naar SQL Er zijn verschillende vraag-en-antwoordsequenties mogelijk, leidend tot verschillende oplossingen. Vraag 3 is in dit stadium nog moeilijk te beantwoorden. Dat hoeft ook niet, want het is niet te verwachten dat we er uitkomen met een grote join van alle betrokken tabellen (of tabelexemplaren). Beter is om bij een dergelijk complex probleem ‘zo klein mogelijk’ te beginnen. De aanpak via de vragen 1 en 2 is daar het meest op toegesneden: Vraag 1: Antwoord: Vraag 2: Antwoord: Vraag: Antwoord:

Waarover wordt informatie gevraagd? Over deelnamen; het navigatiepad start dus in Deelname. Uit welke tabellen is de gevraagde informatie afkomstig? Deelname, Reis en Klant. Om welke deelnamen gaat het? Deelnamen aan een niet-Maan-reisje, waarbij géén Maan-reisje van dezelfde klant bestaat met een eerdere vertrekdatum

Dit geeft als eerste deelstap: select D.reis, R.vertrekdatum, K.naam from   Deelname D join Reis R  on ... join Klant K on ... where dit is een deelname aan een niet-Maan-reisje and er is géén Maan-reisje van dezelfde klant, met een eerdere vertrekdatum

Zonder expliciet verdere vragen en antwoorden te formuleren geven we de tweede deelstap, waarin de in natuurlijke taal geformuleerde deelproblemen zijn vervangen door SQL -code met nieuwe deelproblemen in natuurlijke taal: select D.reis, R.vertrekdatum, K.naam from   Deelname D join Reis R  on ... join Klant K on ... where  -- dit is een deelname aan een niet-Maan-reisje ( de reis bevat een bezoek aan een object dat niet de Maan is or de reis omvat meer dan één bezoek ) and -- er is géén Maan-reisje van dezelfde klant met een eerdere vertrekdatum

not exists (select * from   Deelname D2 join Reis R2 on D2.reis = R2.nr where zelfde klant and reis bezoekt Maan and reis telt één bezoek and eerdere vertrekdatum )

De derde en laatste deelstap bevat alleen SQL -code, maar merk wel op dat alle deelproblemen nog steeds herkenbaar zijn als commentaarcode: select D.reis, R.vertrekdatum, K.naam from   Deelname D join Reis R on D.reis = R.nr join Klant K on D.klant = K.nr where  -- dit is een deelname aan een niet-Maan-reisje -(-- de reis bevat een bezoek aan een object dat niet de Maan is R.nr in (select reis from   Bezoek where  hemelobject 'Maan') or -- de reis omvat meer dan één bezoek R.nr in (select  R1.nr from    Reis R1 join Bezoek B1 on R1.nr = B1.reis group by R1.nr having  count(*) > 1)) and -- er is géén Maan-reisje van dezelfde klant met een eerdere vertrekdatum not exists (select * from   Deelname D2 join Reis R2 on D2.reis = R2.nr where  -- zelfde klant D2.klant = D.klant and -- reis bezoekt Maan D2.reis in (select reis from  Bezoek where  hemelobject = 'Maan') and -- reis telt één bezoek D2.reis in (select  reis from  Bezoek group by reis having  count(*) = 1) and -- eerdere vertrekdatum R2.vertrekdatum < R.vertrekdatum)

Resultaat: No records returned.

Geen overtreders dus in de voorbeeldpopulatie. Alsnog stap 3: kan het ook anders, handiger misschien?

De eerste handmatige oplossing was alleen maar om ons in te werken in het probleem. Ten onrechte hebben we deze direct in SQL vertaald. Nu we het probleem doorzien, is het niet al te lastig om een eenvoudiger aanpak te formuleren: zoek per klant de vroegste reis, en kijk of dat een niet-Maanreisje is. Nogmaals stap 4: transformatie naar SQL Eerste deelstap: select D.reis, R.vertrekdatum, K.naam from   Deelname D join Reis R  on ... join Klant K on ... where dit is de vroegste reis van deze klant and dit is een deelname aan een niet-Maan-reisje

Tweede deelstap: select D.reis, R.vertrekdatum, K.naam from   Deelname D join Reis R  on ... join Klant K on ... where dit is de vroegste reis van deze klant and -- dit is een deelname aan een niet-Maan-reisje ( deze reis bevat een bezoek aan een object dat niet de Maan is or de reis omvat meer dan één bezoek )

Derde en laatste deelstap: select D.reis, R.vertrekdatum, K.naam from   Deelname D join Reis R  on D.reis = R.nr join Klant K on D.klant = K.nr where  -- dit is de vroegste reis van deze klant R.vertrekdatum = (select min(vertrekdatum) from   Reis where  nr in (select reis from   Deelname where  klant = K.nr)) and -- dit is een deelname aan een niet-Maan-reisje -(-- de reis bevat een bezoek aan een object dat niet de Maan is R.nr in (select reis from   Bezoek where  hemelobject 'Maan') or -- de reis omvat meer dan één bezoek R.nr in (select R1.nr from   Reis R1 join Bezoek B1 on R1.nr = B1.reis group by R1.nr having   count(*) > 1))

Heel wat eenvoudiger!

Opgave 14.12 Het Ruimtereisbureau heeft als regel dat je niet mag boeken voor een reis, wanneer je volgens planning nog niet of (vanwege mogelijke vertragingen) korter dan 30 dagen terug zult zijn van een andere reis. Onderzoek of de voorbeeldpopulatie overtreders van deze regel bevat. Ga te werk volgens het stappenplan.

Oefenopgaven Opgave 14.14 Tijdens welke ruimtereis kunnen aan de meeste hemelobjecten die tijdens vroegere reizen zijn bezocht, herinneringen worden opgehaald? Opmerking : Deze opgave vraagt om een precisering, zie ‘Stap 1’ van para graaf 14.3 . Opgave 14.15 Geef een lijst van alle manen die, van alle mede-manen van dezelfde planeet, het verst van die planeet zijn verwijderd. Laat bij elke maan ook de naam van de planeet vermelden. Opgave 14.16 Bij welke reizen (behalve reis 35 zelf) worden tenminste alle hemelobjecten bezocht die tijdens reis 35 óók worden bezocht? Geef van deze reizen reisnummer en vertrekdatum. Opgave 14.17 Voor welke deelnemers (geef klantnummer en naam) aan reis 33 was dit de eerste ruimtereis? Opgave 14.18 Tijdens welke reis (of reizen) wordt het grootste aantal verschillende hemelobjecten bezocht? Vermeld reisnummer en vertrekdatum. Opgave 14.19 Geef van elk hemelobject: de naam (alfabetisch geordend) en het aantal landingen. Opmerking : voor hemelobjecten zonder landing moet een 0 in het overzicht komen. Geef twee oplossingen: a met een left outer join (en een addertje onder het gras!) b met een subselect in de select -clausule.

15 Transacties en concurrency In paragraaf 15.1 pakken we de draad weer op van een verhaal dat we in de hoofdstukken 2, 4 en 11 zijn begonnen, namelijk over het begrip ‘transactie’, een opeenvolging van databaseacties die bij elkaar horen en die allemaal wel of allemaal niet worden uitgevoerd. Deze alles-of-nietsregel, ‘atomiciteit’, is een van de vier eisen van het zogenaamde ACID model. In paragraaf 15.2 beschrijven we deze eisen. We bekijken daarbij ook wat er gebeurt als er ‘concurrency’ in het spel komt: wanneer twee of meer transacties tegelijkertijd worden uitgevoerd kunnen de acties van de één de resultaten van de ander beïnvloeden. In paragraaf 15.3 behandelen we vier klassieke problemen die in dit soort situaties kunnen optreden. Het is duidelijk dat maatregelen nodig zijn om zulke problemen te

voorkomen: er is behoefte aan ‘concurrency control’. In paragraaf 15.4 zien we dat een andere ACID -eis, ‘isolation’, hierbij een grote rol speelt. Voorbeelddatabase De voorbeelddatabase bij de opgaven bij dit hoofdstuk is het Ruimtereisbureau. In figuur 15.1 herhalen we het strokendiagram. Raadpleeg desgewenst bijlage 4 voor de voorbeeldpopulatie.

Figuur 15.1 Ruimtereisbureau: strokendiagram

15.1 Transacties We herhalen kort wat we weten over transacties en introduceren een eenvoudig Rekening-voorbeeld om de theorie uit te leggen. Verder bekijken we twee transactiemodellen en de mogelijkheden die IQU , de Interactive Query Utility, ons biedt om met transacties te experimenteren.

15.1.1 Nogmaals transacties Een transactie is een reeks databasebewerkingen die een logische eenheid vormen en die of helemaal wordt uitgevoerd (commit) of helemaal niet (rollback). Gedurende iedere transactie wordt een transactielog bijgehouden: een verzameling van oude en nieuwe rij-versies. Bij een rollback wordt de ‘inverse’ van deze log afgespeeld. Voorbeeld 15.1 Transactie Een bankdatabase bevat een tabel Rekening, waarvan u het strokendiagram met een kleine populatie ziet in figuur 15.2 .

Figuur 15.2 Het Rekening-voorbeeld

Hiermee kunnen we mooi het nut en zelfs de noodzaak van transacties illustreren; zie de volgende SQL -opdrachten, waarin een bedrag van 100 euro wordt overgemaakt van rekening 1409 naar rekening 1508. update Rekening set    saldo = saldo – 100 where  nr = 1409; update Rekening set    saldo = saldo + 100 where  nr = 1508; commit

Hier is duidelijk dat óf beide statements moeten worden uitgevoerd óf geen van beide. De situatie waarin het eerste statement wel en het tweede niet wordt uitgevoerd is niet acceptabel, omdat er dan 100 euro uit het systeem verdwijnt. Kortom, we moeten op de een of andere manier kunnen aangeven dat de twee statements één geheel vormen. Dat kan door ze samen te voegen in een transactie. Tot nu toe werd steeds na ieder commitmoment impliciet een transactie gestart. In paragraaf 15.1.2 herhalen we dat impliciete transactiemodel

nog eens, en beschrijven we ook een ander transactiemodel, waarin samengestelde transacties pas gestart worden na een expliciete opdracht van de gebruiker. Transaction Control Language (TCL) De commando’s commit en rollback maken deel uit van de SQL-subtaal Transaction Control Language. Tot deze subtaal behoort ook het commando savepoint , waarmee een punt in de transactie kan worden aangegeven tot waar gerollbackt moet worden, bij een rollback. Tot slot bevat deze subtaal het commando set transaction , dat we in paragraaf 15.4 terugzien.

15.1.2 Transactiemodellen We herhalen eerst het impliciete transactiemodel (zie hoofdstuk 10 ). Daarna introduceren we een tweede transactiemodel, het expliciete transactiemodel. Het impliciete transactiemodel Tot nu toe werd steeds bij het begin van een sessie en na elk commitmoment automatisch een nieuwe transactie gestart. Zie figuur 15.3 voor het effect bij dat impliciete transactiemodel van een rollback : terugkeer naar de toestand direct na het laatste commitmoment.

Figuur 15.3 Effect van een rollback bij het impliciete transactiemodel

Het impliciete transactiemodel is het ANSI / ISO standaardtransactiemodel. Het expliciete transactiemodel Bij het tweede model, het expliciete transactiemodel , kan een transactie van meer dan één statement alleen worden gestart via een start transaction -statement. Zo’n expliciet gestarte samengestelde transactie

wordt op de gebruikelijke manier afgesloten met een commitmoment of een rollback, zie figuur 15.4 . Alle statements vóór de start van de samengestelde transactie (het ‘grijze’ gebied van figuur 15.4 ) en alle statements na het afsluiten ervan worden direct gecommit en zijn dus definitief (als ze niet deel uitmaken van een andere expliciet gestarte samengestelde transactie tenminste). We zouden kunnen zeggen: vanaf een start transaction (en tot het einde van de daarmee gestarte transactie) gedraagt het expliciete model zich als het impliciete model.

Figuur 15.4 Effect van een rollback bij het expliciete transactiemodel

Instellen van een transactiemodel Het expliciete model wordt ingesteld met het IQU -commando set start_transaction explicit . Willen we weer terug naar het impliciete model, dan gaat dat met set start_transaction implicit . Voorbeeld 15.2 Bewerkingen op tabel Rekening bij impliciete transactiemodel We gaan uit van het standaard ingestelde transactiemodel: het impliciete model. Verder nemen we aan dat tabel Rekening precies de populatie van figuur 15.2 bevat. We voegen nu twee rijen toe en geven daarna een rollback :

insert into Rekening values (1909, 'Vader', 75); insert into Rekening values (2301, 'Molenaar', 250); rollback

Vragen we hierna de inhoud op: select * from   Rekening

dan is het resultaat: NR NAAM          SALDO ====== ====== ============ 1508 Boer        2004.00 1409 Bakker      2007.00

Dat klopt, volgens het impliciete model was immers automatisch een transactie gestart, zonder dat we daar iets voor hoefden te doen. Voorbeeld 15.3 Bewerkingen op tabel Rekening bij expliciete transactiemodel We schakelen over op het expliciete transactiemodel en zorgen via een commit dat we de begintoestand van figuur 15.4 realiseren: set start_transaction explicit; commit

Nu wordt pas een samengestelde transactie gestart op het moment dat we dat zelf doen. We geven dezelfde commando’s als in voorbeeld 15.2 , met het verschil dat we na het toevoegen van de eerste rij een transactie starten: insert into Rekening values (1909, 'Vader', 75); start transaction; insert into Rekening values (2301, 'Molenaar', 250); rollback

Vragen we hierna de inhoud op: select * from   Rekening

dan is het resultaat: NR NAAM          SALDO ====== ====== ============ 1508 Boer        2004.00 1409 Bakker      2007.00 1909 Vader         75.00

We zien: de rij van de rekening met nummer 1909 was al gecommit, omdat het invoegen ervan niet tijdens een transactie plaatsvond. Alleen met een delete kan deze weer worden verwijderd. Tot slot maken we het impliciete model weer actief:

set start_transaction implicit

Transacties en de interactive query utility In het venster van IQU is een transactie-indicator opgenomen, zie figuur 15.5 . Dat is een soort ‘lampje’ dat aan is wanneer er een transactie loopt: TA = ‘transactie actief’. Brandt het lampje, dan weet u dus dat DML commando’s nog ongedaan kunnen worden gemaakt.

Figuur 15.5 Transactie-indicator en keuzeknop transactiemodel in IQU

Direct na een commitmoment of rollback (of na het openen van IQU ) loopt er nooit een transactie, ongeacht het model. Dan is het lampje dus uit. Kenmerkend voor het impliciete model is dat het na het eerste DML commando ( select , insert , delete of update ) direct weer aangaat. Bij het expliciete model gaat het pas aan na een expliciete start transaction . Direct onder de transactie-indicator bevindt zich een knop waarmee tussen het impliciete en het expliciete transactiemodel kan worden geschakeld. Een alternatief dus voor de set start_transaction commando’s.

Opgave 15.1 Voer enkele experimenten uit analoog aan voorbeeld 15.2 en voorbeeld 15.3 . Let daarbij op de transactie-indicator van IQU . Transacties en select-statements Bij het afbreken van een sessie zonder voorafgaande commit of rollback wordt altijd de vraag gesteld: ‘Transactie is actief, commit uitvoeren voor verbreken?’.Dit is begrijpelijk als het om inserts, deletes of updates gaat. De vraag wordt echter ook gesteld als er niets aan de database is veranderd en er alleen select -statements zijn uitgevoerd. Hoe zit dat, kan een transactie ook bestaan uit alleen select -statements? Het antwoord is ja. Dat is ook niet zo gek: neem bijvoorbeeld een manager die, voor een momentopname van de voorraad, kort na elkaar een aantal select -statements afvuurt op de Artikel-tabel (één voor een volledig overzicht en nog enkele andere voor statistische gegevens). Voor de

verslaggeving is dan belangrijk dat deze gegevens onderling consistent zijn. Ze moeten daarom worden gebundeld in één transactie, afgeschermd voor tussentijdse veranderingen door andere gebruikers. Transacties en DDL-statements Volgens de ANSI / ISO -standaard mag binnen transacties ook DDL -code voorkomen ( create , drop , alter , grant , revoke ). Dit maakt echter het uitvoeren van een rollback nogal gecompliceerd. In de praktijk vormt ieder DDL -statement doorgaans een transactie op zichzelf, die wordt uitgevoerd als er geen andere transacties actief zijn. Daarom laten we in het vervolg DDL verder buiten beschouwing.

15.2 Transactiemanagement In deze paragraaf bekijken we wat er kan en moet gebeuren als we toestaan dat op hetzelfde moment meerdere transacties op dezelfde database worden uitgevoerd. Bovendien roeren we kort aan welke maatregelen genomen moeten worden om ook bij bijvoorbeeld een systeemcrash de database ‘gezond’ te houden. Dit alles wordt samengevat met de term transactiemanagement (of transaction processing ).

15.2.1 Concurrency In het vervolg van dit hoofdstuk hebben we het vooral over concurrency . In het algemeen is er sprake van concurrency – dat letterlijk zoiets betekent als ‘gelijktijdig lopen’ – als verschillende processen tegelijkertijd gebruik willen maken van dezelfde voorziening (resource). Bij databases treedt concurrency op als op een database tegelijkertijd meerdere transacties worden uitgevoerd: concurrente transacties . Dat gebeurt bijvoorbeeld bij online winkels, als twee klanten tegelijkertijd iets bestellen. Als die klanten dan ook nog tegelijkertijd hetzelfde product willen bestellen, kan dat tot problemen leiden, bijvoorbeeld als er nog maar één exemplaar van dat product voorradig is. Het is de taak van het rdbms om die problemen te voorkomen, dat wil zeggen, het rdbms moet zorgen dat ze zo weinig mogelijk last van elkaar hebben en geen inconsistentie kunnen veroorzaken. Multi-user en multi session In bovenstaand voorbeeld is sprake van multi-user -gebruik van de database: meerdere gebruikers (klanten) die, ieder in hun eigen sessie, tegelijkertijd dezelfde database benaderen. Er kan echter ook concurrency

optreden bij één gebruiker, wanneer die meerdere sessies opent: multisession -gebruik. Bijvoorbeeld wanneer gebruiker Toetjesboek een sessie opent vanuit de IQU -client en een andere sessie vanuit de grafische Toetjesboek-client. Problemen en oplossingen De problemen komen voort uit read / write - of write / write -conflicten. Immers, twee transacties die allebei in hetzelfde stuk database willen lezen kunnen elkaar niet in de weg lopen. De problemen ontstaan als (minstens) één van beide transacties wil schrijven (gegevens toevoegen, veranderen of verwijderen). In alle omgevingen (of daar nu databases gebruikt worden of niet) waarin concurrente acties voorkomen bestaat de noodzaak van concurrency control : hoe zorgen we dat alles correct verloopt maar dat we zo weinig mogelijk last (performanceverlies) van de genomen maatregelen hebben? In de volgende paragraaf noemen we een paar manieren, zowel in het algemeen als specifiek voor databases. In paragraaf 15.3 bekijken we vervolgens vier soorten problemen die kunnen optreden bij concurrente transacties, en de manier waarop de SQL -standaard en Firebird daarmee omgaan.

15.2.2 Concurrency control We bespreken kort drie manieren van concurrency control bij databases: locking, multiversion concurrency control en optimistic concurrency control. Locking Een manier van concurrency control is iedere voorziening (tabel, rij, …) een lock (‘slot’) geven. Een transactie mag die voorziening dan pas gebruiken als het lock op die voorziening vrij is en dus aan die transactie toegekend kan worden. Er wordt onderscheid gemaakt tussen gedeelde locks (shared locks of ook wel read-locks) en exclusieve locks (exclusive locks of ook wel write-locks). Gedeelde locks zijn handig voor transacties die alleen lezen: andere transacties mogen dan ook alleen lezen in die voorziening. Exclusieve locks zijn handig voor transacties die schrijven in een voorziening: andere transacties mogen dan niets doen in die voorziening. Een ander punt van aandacht bij lockingmechanismen is de locking granularity : het maakt verschil of je een lock zet op een rij of op een tabel. Hoe groter de voorziening waarop het lock gezet wordt, hoe meer andere transacties ervan merken. Aan de andere kant creëert het bijhouden van vele kleine locks meer werk (‘overhead’) dan het bijhouden van een paar grote locks. Locking introduceert ook een nieuw probleem: ‘deadlocks’. Een deadlock treedt op wanneer transacties op elkaar staan te wachten om verder te

kunnen. Ook hiervoor zijn allerlei oplossingen bedacht, zoals de time-out (een vooraf afgesproken tijd blijven wachten, in de hoop dat degene op wie je wacht dan intussen klaar is) of het willekeurig aanwijzen van een ‘deadlock-loser’. Locking is de meest gebruikte manier van concurrency control in databases. Multiversion concurrency control Een andere vorm van concurrency control in databases is multiversion concurrency control ( MVCC ). Firebird en zijn voorganger Firebird gebruiken hiervoor de term Multigenerational Architecture ( MGA ). Wanneer transactie A een record verandert, wordt er een nieuwe versie van dat record gemaakt en blijft de oude versie bewaard. Een transactie B die hetzelfde record wil lezen, gebruikt een geschikte oudere versie, die dan wel niet de nieuwste versie is, maar die in ieder geval een consistente view op de database geeft. Transactie B hoeft dus geen lock op die data aan te vragen. Als B vervolgens iets in zijn versie wil wijzigen, volgt er een melding dat er intussen een nieuwere versie is. Een versie blijft bewaard zolang er een transactie met die versie bezig is. Dat betekent dat lange transacties ervoor kunnen zorgen dat veel records en vaak ook een heleboel versies van hetzelfde record bewaard moeten blijven tot die transactie gecommit (of gerollbackt) wordt, wat kan leiden tot performanceverlies. Het is daarom verstandig om transacties zo kort mogelijk te houden en om vaak te committen. Dat neemt niet weg dat er in het geval van veel korte transacties nog steeds veel ruimte nodig is om al die versies op te slaan. Aan de andere kant maakt het bestaan van oudere versies rollback en recovery (zie parag raaf 15.2.3 ) een stuk eenvoudiger. Optimistic concurrency control Een rdbms dat optimistic concurrency control als synchronisatiemechanisme gebruikt gaat ervan uit dat het allemaal wel goed zal komen. Iedere transactie kan gewoon zijn gang gaan zonder eerst locks aan te hoeven vragen. Als een transactie A wil committen wordt gecontroleerd of geen andere transactie ‘aan A ’s data gezeten heeft’. Als dat inderdaad zo is gaat de commit door, anders volgt een rollback. Deze techniek heeft vooral voordelen in omgevingen waarin weinig ‘strijd’ om data voorkomt: er hoeven geen locks te worden beheerd, en transacties hoeven niet te wachten tot een lock vrijkomt. In omgevingen waar wel veel conflicten voorkomen kan beter voor een andere techniek gekozen worden.

15.2.3 Recovery Bij recovery (‘herstel’) gaat het om het terugbrengen van de database in een consistente toestand nadat er iets grondig mis is gegaan: een harddiskcrash midden in een transactie bijvoorbeeld. Het rdbms moet

ervoor zorgen dat ook in zo’n geval de transactie óf in zijn geheel wordt uitgevoerd óf helemaal niet. Daarvoor is het noodzakelijk dat op de een of andere manier tijdens een transactie wordt bijgehouden wat de oude en nieuwe waarden zijn van alles wat door de transactie wordt veranderd. Dit gebeurt in een log file , die op de harddisk wordt opgeslagen en bovendien regelmatig wordt gebackupt (de harddisk kan immers crashen…). Er komt heel wat bij kijken om dit allemaal in goede banen te leiden; er bestaan dan ook allerlei protocollen en technieken voor database-recovery. Dit valt echter buiten het bestek van dit hoofdstuk.

15.2.4 ACID Transactiemanagement moet zo gebeuren dat alle transacties voldoen aan vier eisen: atomicity , consistency , isolation en durability . Een database waarin niet gegarandeerd is dat alle uitgevoerde transacties aan deze eisen voldoen kan niet betrouwbaar zijn. De onderdelen van het rdbms die zorgen voor concurrency control en recovery zijn hiervoor verantwoordelijk. Atomicity Atomicity (‘atomiciteit’) zegt dat elke transactie één geheel is. Dat is precies wat we de hele tijd al zeggen: óf de transactie in zijn geheel wordt definitief ( commit ) óf de transactie wordt in zijn geheel teruggedraaid ( rollback ). Consistency Consistency (‘consistentie’) houdt in dat transacties de database altijd weer in een geldige toestand brengen (ervan uitgaand dat de transactie ook in een geldige toestand begon). Bij ‘geldig’ moet u bijvoorbeeld denken aan referentiële integriteit en andere databaseregels. Zoals eerder gezegd zijn er geen eisen aan de consistentie tijdens het verwerken van de transactie: het mag best zo zijn dat de database halverwege een transactie niet in een geldige toestand is, zolang er na het committen of rollbacken van de transactie maar weer een geldige toestand heerst (zie ook voorbeeld 15.1 ). Isolation Isolation (‘isolatie’) houdt in dat concurrente transacties A en B geen invloed hebben op elkaars verwerking. Een transactie A mag geen ‘tussendata’ gebruiken van een gelijktijdig uitgevoerde transactie B , en omgekeerd. Een transactie kan de resultaten van een andere transactie pas zien en gebruiken als de andere transactie gecommit is. In paragraaf 15.3 behandelen we vier standaardproblemen die zich kunnen voordoen bij gebrek aan goede isolatie. Merk op dat het geen kunst is transacties volledig van elkaar te isoleren. Zorg bijvoorbeeld voor dat er altijd maar één transactie actief kan zijn! De kunst is transacties niet meer van elkaar te isoleren dan nodig is: gebruikers moeten zo min mogelijk last van elkaar hebben. In SQL zijn

hiervoor verschillende ‘ isolation levels’ voorgeschreven, die we in paragra af 15.4 behandelen. Durability Durability (‘bestendigheid’) verzekert dat de effecten van een gecommitte transactie op de database niet verloren raken, ook niet bij een software- of hardwarestoring.

15.2.5 Gedistribueerde databases Een gedistribueerde database is een database die letterlijk verspreid is over verschillende locaties. Die locaties kunnen verschillende computers in hetzelfde gebouw zijn, maar ook bijvoorbeeld een paar in Europa en een paar in Amerika. Transactiemanagement in gedistribueerde databases is nog ingewikkelder dan in ‘gewone’ databases, vooral voor wat betreft recovery. Recovery Een van de bijzonderheden van gedistribueerde databases is replicatie : het voorkomen van dezelfde database-inhoud op verschillende plekken. Het hoeft niet om een kopie van de gehele database te gaan, een kopie van een deel (een tabel bijvoorbeeld) kan ook. Dit is een voorbeeld van gecontroleerde redundantie . Een voordeel van het hebben van replica’s is dat het deel van de database dat in Amerika staat een lokale kopie kan gebruiken, in plaats van steeds naar het Europese deel te moeten. Natuurlijk kleeft er ook een groot nadeel aan replicatie: als een van de kopieën wordt gewijzigd moeten alle andere kopieën overeenkomstig worden gewijzigd. Dit staat bekend onder de naam update propagation problem . Een gevolg van het spreiden van delen van een database over verschillende locaties is dat de verwerking van een transactie ook verspreid kan zijn over verschillende locaties. Dat maakt dat er weer een protocol nodig is om te zorgen dat zo’n ‘gedistribueerde transactie’ atomair is: op de een of andere manier moet geregeld worden dat óf alle delen van de transactie committen óf alle delen rollbacken. Een manier om dit voor elkaar te krijgen is de two-phase commit (‘commit in twee fasen’). In de eerste fase vraagt de coördinator – een onderdeel van het rdbms – aan alle delen van de gedistribueerde transactie of ze klaar zijn om te committen. Als alle antwoorden binnen zijn start de coördinator de tweede fase, waarin óf alle delen moeten committen (als iedereen ‘ja’ zei) óf alle delen moeten rollbacken (als iemand ‘nee’ zei). Concurrency Net als bij niet-gedistribueerde databases wordt bij gedistribueerde databases ten behoeve van concurrency control vooral gebruikgemaakt van locking.

15.3 Vier klassieke problemen In deze paragraaf beschrijven we vier problemen die kunnen optreden in een database waarin concurrente transacties mogen voorkomen, maar waarin geen of onvoldoende maatregelen zijn genomen om die concurrency goed te laten verlopen. In paragraaf 15.4 bekijken we mogelijke oplossingen. Bij het introduceren van de vier problemen gaan we steeds uit van een transactie T 1, die concurrent wordt uitgevoerd met een transactie T 2. We willen weten wat voor problemen T 1 kan ondervinden van deze concurrency. Natuurlijk kan T 2 dezelfde soort problemen tegenkomen, maar we bekijken de situatie steeds vanuit transactie T 1.

15.3.1 Lost update T ransactie T 1 update bepaalde gegevens, en nog voordat T 1 die update kan committen wijzigt transactie T 2 diezelfde gegevens. De update van T 1 wordt dus overschreven door de wijziging van T 2, en het is alsof de update van T 1 nooit is gebeurd: het is een lost update . Dit is een

voorbeeld van een write/write-conflict. Voorbeeld 15.4 Lost update Bekijk het volgende scenario, waarin twee transacties T 1 en T 2 concurrent worden uitgevoerd. Beide doen een update van het saldo van rekening 1508. Als er geen concurrency cont r ol geregeld is, kan de volgende situatie zich voordoen (zie tabel 15.1 ): eerst wordt de update van T 1 uitgevoerd, vervolgens die van T 2, en daarna commit eerst T 2 en dan T 1. De tijd loopt van boven naar beneden: een hoger statement wordt eerder uitgevoerd dan een lager statement. Tabel 15.1 De update van T1 gaat verloren

De update die uiteindelijk in de database terechtkomt is die van T 2; die van T 1 gaat dus verloren. Het ‘overschrijvende statement’ in T 2 hoeft geen update te zijn: een delete telt ook (zie de uitwerking van opgave 15.6 a). Een insert kan nooit een update overschrijven, dus een insert in T 2 is hier niet relevant.

Opgave 15.2 Iemand zegt: “Maar het is toch heel normaal dat de update van de medewerker die met T 1 bezig is in de loop der tijd overschreven wordt door een andere update?”. Wat ziet deze persoon over het hoofd? Lost insert en lost delete De term ‘lost update’ suggereert dat het altijd om een update -statement gaat dat overschreven wordt, en dus niet om een insert of delete . Het is echter ook denkbaar dat T1 last heeft van een ‘lost insert’ of een ‘lost delete’. Wees erop attent dat de term ‘lost update’ soms in wat algemenere zin wordt gebruikt.

15.3.2 Dirty read T ransactie T 1 leest gegevens die door transactie T 2 zijn veranderd maar

niet zijn gecommit, en gaat daarmee aan de slag. Dit heet een dirty read , en geeft een probleem als T 2 een rollback uitvoert. Andere namen zijn: uncommitted-dataprobleem , temporary-updateprobleem en uncommitteddependencyprobleem . Dit is een voorbeeld van een read/write-conflict. Voorbeeld 15.5 Dirty read et volgende scenario geeft een voorbeeld van een dirty read:

Tabel 15.2 T1 doet een dirty read

Het maakt hier niet uit of de commit van T 1 plaatsvindt voor of na de rollback van T 2. Het maakt eigenlijk ook niet uit of T 2 uiteindelijk een rollback of een commit doet: het feit dat T 1 gegevens leest die op dat moment ongecommit zijn is ongewenst. Wanneer T 2 een insert doet in plaats van een update treedt hetzelfde probleem op: een dirty read van T 1.

15.3.3 Non-repeatable read T ransactie T 1 leest twee keer dezelfde rij, maar krijgt verschillende resultaten doordat transactie T 2 intussen een update of delete (met

commit ) heeft uitgevoerd. Dit heet een non-repeatable read . Dit probleem staat ook bekend onder de naam inconsistent-dataprobleem , en is weer een voorbeeld van een read/write-conflict. Voorbeeld 15.6 Non-repeatable read Het volgende scenario geeft een voorbeeld van een non-repeatable read: Tabel 15.3 T1 krijgt te maken met een non-repeatable read

De eerste select van T 1 geeft één rij als resultaat, terwijl de tweede select geen rijen zal geven. T 2 heeft immers intussen die ene rij verwijderd, en gecommit. Eenzelfde effect kan bereikt worden met een toepasselijk update statement van T 2, in plaats van een delete (zie opgave 15.7 ). Het verschil tussen een dirty read en een non-repeatable read is dat bij een dirty read T 2 niet gecommit heeft voordat T 1 de tweede ‘read’ doet. Het optreden van een dirty read is, zoals de naam al doet vermoeden, minder netjes dan het optreden van een non-repeatable read. Het non-repeatable-readprobleem komen we in de literatuur ook wel tegen als het inconsistent-analysisprobleem . Een specifieke vorm is het incorrect-summaryprobleem . Een voorbeeld daarvan is het volgende: transactie T 1 is bezig een aantal gegevens te sommeren, terwijl een andere transactie tussendoor een aantal van die gegevens wijzigt (zie ook voorbeeld 15.9 ).

15.3.4 Phantom Het phantom -probleem lijkt op het non-repeatable-readprobleem: transactie T 1 leest een aantal rijen die allemaal voldoen aan een of andere selectieconditie. Vervolgens wijzigt (en commit) transactie T 2 iets, waardoor nu een of meer extra rijen voldoen aan die selectieconditie. Als T 1 dan dezelfde selectie nogmaals doet komen daarin een of meer phantom rows tevoorschijn. Dit is weer een voorbeeld van een read/write-conflict. Het verschil tussen een phantom row (‘spookrij’) en een non-repeatable read is dat het bij een non-repeatable read gaat om een rij die er al was (en die later veranderd of verwijderd blijkt te zijn), terwijl het bij een phantom row gaat om een rij die ineens verschijnt (door toevoegen of veranderen).

Voorbeeld 15.7 Phantom Het volgende scenario geeft een voorbeeld van het phantom-probleem: Tabel 15.4 T1 ziet een phantom row verschijnen

Vanuit het oogpunt van T 1 is de rij die rekening 1909 representeert een phantom row. Phantom rows kunnen ook worden veroorzaakt door een update in plaats van een insert (zie opgave 15.8 ). Net als het non-repeatable-readprobleem komen we ook het phantomprobleem soms tegen in de vorm van een inconsistent analysis (zie ook voo rbeeld 15.9 ). We sluiten deze paragraaf af met een opgave over de vier beschreven problemen.

Opgave 15.3 Hieronder geven we een aantal scenario’s met concurrente transacties in de voorbeelddatabase Ruimtereisbureau. Geef per scenario aan welke transactie welk van de vier problemen ondervindt als er geen concurrency control geregeld is. Tabel 15.5 Scenario a

Tabel 15.6 Scenario b

Tabel 15.7 Scenario c

Tabel 15.8 Scenario d

Tabel 15.9 Scenario e

15.4 Isolation levels In SQL kunnen gebruikers per transactie aangeven welke mate van concurrency control gewenst is. Dat gebeurt door een isolation level (de I van ACID ) te specificeren bij het starten van de transactie. ‘Onder water’ worden die isolation levels over het algemeen geïmplementeerd met een vorm van locking, vandaar dat u in het vervolg de term ‘lock’ nog wel eens zult tegenkomen. In deze paragraaf bespreken we eerst de vier isolation levels van de SQL standaard en hun relatie met de vier problemen van paragraaf 15.3 . Daarna bespreken we hoe Firebird hiermee omgaat. T ot slot volgt een

aantal opgaven, waarin u drie van de vier problemen uit paragraaf 15.3 zelf kunt ervaren en oplossingen kunt uitproberen.

15.4.1 Isolation levels in de SQL-standaard De SQL -standaard noemt drie problemen die zich kunnen voordoen bij onvoldoende concurrency control op transacties: dirty read, nonrepeatable read en phantom. Op basis hiervan schrijft de SQL -standaard vier isolation levels voor, die – als je ze van soepel naar streng bekijkt – steeds een probleem extra moeten uitschakelen. Lost updates mogen bij geen van deze vier levels voorkomen. In tabel 15.10 geven we een overzicht van de relatie tussen de vier problemen die we beschreven hebben en de vier isolation levels: van soepel naar streng zijn dat read uncommitted, read committed, repeatable read en serializable. Tabel 15.10 Standaard SQL: welke problemen bij welke levels? (- /+ onmogelijk/mogelijk in een transactie met dit level)

Isolation levels worden per transactie gespecificeerd, in een set transaction -statement, waarmee de transactie ook meteen wordt gestart: set transaction isolation level

waarbij < level of isolation > een van de volgende vier waarden heeft: serializable , repeatable read , read committed of read uncommitted . De default is serializable . De stadskaartmetafoor Het idee achter isolation levels wordt vaak verduidelijkt aan de hand van de metafoor van een bezoek aan een stad (= de database). Elke bezoeker (= transactie) krijgt zijn eigen kaart van de stad. Of nog beter: een gebruiker krijgt bij elke transactie zijn eigen 3D -kaart van de stad mee, waarin hij kan rondlopen. Verschillende gebruikers ervaren elk hun eigen versie van de stad, maar krijgen soms onverwachte dingen te zien. Het hangt van het isolation level van hun transactie af, in hoeverre de wereld buiten hun eigen transactie tot hun 3D -ervaring van de stad kan doordringen. We schetsen per isolation level wat een gebruiker kan zien gebeuren, buiten de dingen die hij zelf aanricht.

Isolation level serializable Voor een transactie T 1 met isolation level serializable mag geen van de genoemde problemen zich voordoen. Met andere woorden, het rdbms moet totale isolatie van concurrente transacties garanderen: het resultaat van een serializable uitvoering van een transactie T 1 tegelijkertijd met een aantal andere, concurrente transacties is hetzelfde als het resultaat van een of andere seriële uitvoering van al deze transacties. T 1 merkt dus niets van de effecten van concurrente transacties; het is alsof alle transacties één voor één in hun geheel worden uitgevoerd. Het is alleen van tevoren niet te voorspellen in welke volgorde. In termen van de 3D -versie van de stad: het enige wat de gebruiker in zijn 3D -versie ziet veranderen zijn de dingen die hij zelf verandert. Er zijn geen invloeden van buitenaf zichtbaar. Overigens wil het feit dat de vier klassieke problemen niet voor kunnen komen niet zeggen dat er helemaal niets meer mis kan gaan: updateconflicten – zoals wanneer T 1 probeert data te updaten die door een andere transactie is gelockt – zijn bij geen enkel isolation level uitgesloten. In opgave 15.6 zien we daar een voorbeeld van. Isolation level repeatable read Het isolation level repeatable read moet ervoor zorgen dat lost updates, dirty reads en non-repeatable reads niet voorkomen. Phantom rows kunnen echter wel voorkomen. Je zou kunnen zeggen dat, voor een transactie met isolation level repeatable read, gedurende het ‘verblijf’ (transactie) in de stad de 3D kaart buiten de gebruiker om kan worden uitgebreid met voltooide , geheel nieuwe bouwsels. Alles wat al in de 3D -versie aanwezig was blijft dus, tenzij de gebruiker zelf iets wijzigt natuurlijk. Isolation level read committed Het isolation level read committed moet ervoor zorgen dat lost updates en dirty reads niet voorkomen. Non-repeatable reads en phantom rows kunnen echter wel voorkomen. Een transactie T 1 met level read committed ziet dus de gecommitte wijzigingen van concurrente transacties. Dit betekent dat twee keer dezelfde leesactie verschillend resultaat kan opleveren. Eigen updates zijn wel veilig. Je zou kunnen zeggen dat, voor een transactie met isolation level read committed, de 3D -kaart van de gebruiker tijdens het verblijf in de stad voortdurend wordt aangepast aan de actuele situatie voor wat betreft voltooide nieuwbouw, verbouw en sloop. Werk in uitvoering door derden is onzichtbaar. Een voordeel van dit isolation level is dat T 1 steeds de meest recente (gecommitte) gegevens ziet. Een nadeel zou kunnen zijn dat de ‘view’ op de database niet stabiel is. Isolation level read uncommitted Een transactie T 1 met isolation level read uncommitted ziet alle wijzigingen van concurrente transacties, alleen eigen updates worden beschermd tegen overschreven worden.

Je zou kunnen zeggen dat, voor een transactie met isolation level read uncommitted, de 3D -kaart van de gebruiker tijdens het verblijf in de stad voortdurend wordt aangepast aan de actuele situatie. Ook werk in uitvoering van anderen is zichtbaar. Het zal duidelijk zijn dat isolation level read uncommitted qua concurrency control weinig inspanningen vraagt. Het lijkt misschien ook niet erg bruikbaar, maar in bepaalde situaties is het dat toch wel, zoals bij systemen waarbij je weet dat lezers en schrijvers elkaar echt niet in de weg zitten (bijvoorbeeld omdat schrijvers alleen nieuwe waarden toevoegen en lezers altijd iets achterlopen).

15.4.2 Isolation levels in Firebird De SQL -conformance – de mate waarin de implementatie van een specifiek rdbms voldoet aan de specificatie van de SQL -standaard – hoeft niet totaal te zijn. Daardoor kan het gebeuren dat een rdbms niet alle isolation levels van de SQL -standaard implementeert, of dat een rdbms eigen isolation levels introduceert. Zo heeft Firebird de volgende drie isolation levels: snapshot table stability, snapshot en read committed. De default is snapshot. Hieronder beschrijven we de drie isolation levels van Firebird, van soepel naar streng. Read committed Firebirds read committed isolation level doet precies wat het volgens standaard- SQL moet doen: een transactie met dit level ziet veranderingen die door concurrente transacties zijn veroorzaakt alleen vanaf het moment dat ze gecommit zijn. Lost updates en dirty reads kunnen dus niet voorkomen, maar non-repeatable reads en phantoms wel. Snapshot Het standaard isolation level van Firebird is snapshot . Dit is zeer goed op de praktijk toegesneden, al hoort het niet tot de SQL -standaard. Een transactie T 1 waarvoor dit level geldt, merkt eigenlijk niets van andere transacties: resultaten (gecommit of niet) vanuit een andere, concurrente transactie zijn in transactie T 1 onzichtbaar. Ze worden pas zichtbaar voor de uitvoerder van T 1 na het afsluiten van T 1. Lost updates, dirty reads en non-repeatable reads komen niet voor bij transacties met dit isolation level. Wel kan het voorkomen dat een van de eigen opdrachten wordt geblokkeerd, tijdelijk of definitief (update-conflict). In het laatste geval zal een rollback plaatsvinden. Je zou kunnen zeggen dat, voor een transactie met isolation level snapshot, de enige veranderingen die de gebruiker in zijn 3D -versie ziet zijn eigen aanpassingen zijn. Invloed van buitenaf is er niet (of het nu om voltooide projecten of werk in uitvoering gaat). Het is niet toevallig dat een 3D -versie van de stad bij level snapshot precies hetzelfde werkt als bij het theoretische level serializable: in de praktijk komt snapshot dicht bij

dat hoogste isolation level van SQL . Het Firebird/Firebird-level snapshot table stability komt nog iets dichter in de buurt. Door zijn MGA -architectuur is Firebird prima opgewassen tegen het performanceverlies dat dit isolation level bij anders gebouwde rdbms’en kan veroorzaken. Snapshot table stability T ransactie T 1 met isolation level snapshot table stability heeft dezelfde eigenschappen als bij isolation level snapshot, plus iets extra’s: zodra T 1 een tabel nodig heeft zet T 1 een lock op die tabel. Andere transacties

hebben dan alleen read-only toegang tot die tabel. Gebruikers wordt geadviseerd terughoudend te zijn met het gebruiken van dit isolation level, omdat het snel voor conflicten en zelfs deadlocks kan zorgen.

Opgave 15.4 In de Firebird 6/Firebird 2.5-documentatie staat de volgende algemene opmerking over transaction processing: “When simultaneous transactions attempt to update the same data in tables, only the first update succeeds. No other transaction can update or delete that data until the controlling transaction is rolled back or committed.” Wat betekent dit? In tabel 15.11 geven we een overzicht van de relatie tussen de isolation levels van Firebird en de vier klassieke problemen. Tabel 15.11 Firebird: welke problemen bij welke levels? ( - /+ onmogelijk/mogelijk in een transactie met dit level)

Aan het slot van dit hoofdstuk voeren we een paar experimenten uit, waarin we enkele van de genoemde problemen proberen te veroorzaken en verschillende varianten van het set transaction -statement uitproberen. Parallelle IQU-sessies Om gecontroleerd allerlei scenario’s na te spelen maken we gebruik van de mogelijkheid om in IQU meerdere sessies parallel te openen. Deze sessies kunnen van één gebruiker of van verschillende gebruikers zijn. U opent een tweede IQU -venster door te klikken op de titel van het eerste venster. Op dezelfde manier kunt u nog meer IQU -vensters openen. We spelen een scenario na door de ene transactie in het eerste IQU venster en de andere transactie in het tweede IQU -venster uit te voeren,

en wel statement voor statement, in de volgorde die door het scenario wordt aangegeven. Het set transaction-statement in IQU We kunnen met het set transaction -statement meer instellen dan alleen het isolation level van een transactie: set transaction

isolation level

Hierbij zijn de volgende keuzes mogelijk: : : :

read only read write wait no wait snapshot read committed record_version read committed no record_version

De access mode specificeert of het om een transactie gaat die alleen gegevens ophaalt of om een transactie die ook zal schrijven (gegevens toevoegen, verwijderen of veranderen). Omdat de eerste soort minder last veroorzaakt dan de tweede, kan het handig zijn (en wordt het gezien als goed programmeergebruik) om bij het starten van een transactie aan te geven of het om een read only -transactie gaat of om een read write transactie. Met de optie lock conflict behaviour kan het gedrag bij een lock conflict worden gespecificeerd: meteen stoppen met een foutmelding ( no wait ) of afwachten tot de ander een commit of rollback geeft ( wait ). Als level of isolation kunnen we snapshot en read committed instellen, waarbij read committed twee varianten heeft: bij read committed record_version wordt steeds de laatste gecommitte versie van een rij gelezen, zelfs als er recentere niet-gecommitte versies van die rij zijn. Deze optie werkt dus precies zoals je van read committed zou verwachten. Bij read committed no record_version wordt altijd geprobeerd de laatste versie van een rij te lezen. Als die laatste versie niet gecommit is hangt het van de lock conflict behaviour af wat er gebeurt: bij no wait volgt meteen een foutmelding (‘lock conflict on no wait transaction’), bij wait wordt gewacht tot de concurrente transactie commit of rollbackt. In IQU zijn de default opties read write , no wait en snapshot . In Firebird is de default wait in plaats van no wait . Uit praktisch oogpunt hebben wij echter gekozen voor no wait : als een wait -transactie eenmaal begint met wachten, staat de hele Boekverkenner te wachten. De transactieopties die u met set transaction hebt ingesteld, blijven gelden totdat de transactie commit of rollbackt. Pas daarna zijn de

effecten van een nieuw set transaction -statement merkbaar, zelfs als dat nieuwe statement vóór de commit of rollback komt. Experimenten Omdat Firebird alleen hogere isolation levels heeft, kunnen we niet alle vier de klassieke problemen demonstreren. Uit tabel 15.11 blijkt dat we door het instellen van isolation levels maar twee van de vier problemen kunnen veroorzaken: non-repeatable reads en phantoms. Voorbeeld 15.8 Simulatie non-repeatable read We creëren een situatie waarin we twee concurrente transacties kunnen uitvoeren op voorbeelddatabase Ruimtereisbureau. Eerst loggen we via het koektrommeltje in op Ruimtereisbureau; we krijgen dan een IQU -venster (dat we hier in de tekst verder aanduiden met IQU1 ), en worden ingelogd als gebruiker/eigenaar Ruimtereisbureau. Vervolgens openen we een tweede IQU -venster (dat we in de tekst IQU2 zullen noemen), en verbinden daarin met de hand met dezelfde voorbeelddatabase, maar dan als gebruiker Sysdba: connect 'Ruimtereisbureau.fdb' user 'Sysdba' password 'masterkey'

We willen nu een concurrente verwerking van de volgende twee transacties simuleren (het scenario uit opgave 15.3 c): Tabel 15.12 B doet een non-repeatable read

T ransactie B doet hier een non-repeatable read, en die kunnen we alleen zichtbaar maken als transactie B wordt verwerkt met isolation level read

committed. Op dit moment hebben echter zowel IQU1 als IQU2 het default isolation level, snapshot. We moeten dus het isolation level van het IQU venster waarin we transactie B gaan uitvoeren ( IQU2 ) verlagen naar read committed: set transaction read only no wait isolation level read committed record_version --in IQU2

Merk op dat we in beide IQU -vensters met het impliciete transactiemodel werken (want dat is de default). Dat is ook precies de bedoeling, want we willen het commitmoment van transactie B kunnen uitstellen zonder dat steeds expliciet te moeten zeggen. We voeren nu stap voor stap transactie A uit in IQU1 en transactie B in IQU2 , in de volgorde die is aangegeven in tabel 15.12 . De eerste keer dat de select van transactie B wordt uitgevoerd is dit het resultaat: REIS HEMELOBJECT ========= =========== 34 Mars 34 Jupiter 34 Io

Vervolgens voeren we heel transactie A uit in IQU1 , en daarna doen we de select van B nog eens in IQU2 . Het resultaat is dan, zoals verwacht, anders: No records returned.

Vergeet niet om na ieder experiment de oorspronkelijke populatie van het Ruimtereisbureau te herstellen! Dit gaat het gemakkelijkst en veiligst door via het databasemenu de database te verwijderen en weer te creëren. Als u wilt kunt u nu nog controleren dat de read van B wel repeatable is als B als isolation level snapshot instelt: eerst weer de twee IQU -vensters openen zoals aan het begin van dit voorbeeld beschreven, zorgen dat de tweede ook verbinding heeft met de database, en dan bijvoorbeeld: set transaction read only --fn IQU2

De transactieopties in de beide IQU -vensters blijven staan op de waarden die u hebt ingesteld, en gaan dus niet automatisch terug naar de default waarden, ook niet als u het IQU -venster sluit. Alleen het uitzetten van de hele Boekverkenner zorgt ervoor dat de transactieopties weer de default waarden krijgen – en het handmatig zetten van die waarden met een set transaction natuurlijk. Denk er bij dat laatste wel aan eerst de vorige transactie te rollbacken of committen.

Opgave 15.5 Ga uit van de situatie in tabel 15.12 . Voorspel wat er zal gebeuren als B met isolation level read committed record_version werkt en A zijn commit uitstelt tot na de tweede select van B . Controleer uw voorspelling met de Boekverkenner.

Voorbeeld 15.9 Simulatie inconsistent analysis We gaan uit van de voorbeelddatabase Ruimtereisbureau met de oorspronkelijke populatie. We gebruiken de opzet van voorbeeld 15.8 , met gebruiker Ruimtereisbureau in IQU1 en gebruiker Sysdba in IQU2 . We gaan nu proberen een phantom row te laten verschijnen, met de transacties uit tabel 15.13 : Tabel 15.13 Transactie B ziet een phantom row verschijnen

T ransactie B is degene die de phantom row moet zien verschijnen, dus we moeten B ’s isolation level verlagen naar read committed, net als in voorbe

eld 15.8 : set transaction read only isolation level read committed record_version --fn IQU2

De eerste select van B geeft als resultaat: REIS     COUNT ========= ========= 31         4 32         3 33         3 34         3

Na uitvoeren van de delete en insert (met commit ) van transactie A geeft de tweede select van B het volgende resultaat: REIS     COUNT ========= ========= 31         3

32         3 33         4 34         3

T ransactie B heeft dus last van een inconsistent analysis: B zag de eerste

keer vier deelnemers aan reis 31 en ziet er de tweede keer maar drie, en B telde eerst drie deelnemers bij reis 33 en later vier. Deze inconsistent analysis heeft twee onderliggende oorzaken: ten eerste een nonrepeatable read (door de delete verdwijnt er een rij uit Deelname) en ten tweede een phantom row in Deelname (door de insert ).

Opgave 15.6 Stel dat het volgende scenario zich voordoet ( tabel 15.14 ): Tabel 15.14 Experiment met twee updates

a Wat denkt u dat er zal gebeuren? Welke isolation levels moeten we kiezen voor A en B om dit te voorkomen? b Probeer het uit met twee IQU -vensters, en pas eventueel uw antwoord op onderdeel a aan.

Opgave 15.7 Bedenk een scenario waarin transactie A een non-repeatable read doet omdat transactie B tussendoor een update doet (zie ook de opmerking aan het eind van voorbeeld 15.6 ). Gebruik hiervoor alleen tabel Transport van Ruimtereisbureau. Test uw scenario met twee IQU -vensters. Opgave 15.8 Bedenk een scenario waarin transactie A een phantom row ziet verschijnen als gevolg van een update van transactie B (zie ook de

opmerking aan het eind van voorbeeld 15.7 ). Gebruik hiervoor alleen de kolom duur van tabel Reis. Test uw scenario met twee IQU -vensters.

Oefenopgaven De volgende opgaven zijn gebaseerd op Orderdatabase. Zie Boekverkenner of bijlage 4 voor de voorbeeldpopulatie.

Opgave 15.9 We gaan uit van een situatie waarin geen concurrency control geregeld is en waarin het scenario van tabel 15.15 wordt uitgevoerd. Tabel 15.15 Scenario bij opgave 15.9

a Welk van de vier klassieke problemen treedt hier op, en waarom is het niet een van de andere drie klassieke problemen? b Welke transactieopties moeten de transacties minstens hebben om dit probleem te omzeilen?

Opgave 15.10 Bedenk een scenario waarin transactie A een dirty read doet vanwege een insert van transactie B. Gebruik hiervoor alleen tabel Klant. Opgave 15.11 Bedenk een scenario waarin transactie A een non-repeatable read doet omdat transactie B een delete doet. Gebruik hiervoor alleen tabel Klacht.

Opgave 15.12 Bedenk een scenario waarin transactie A een inconsistent analysis doet omdat transactie B een delete doet. Gebruik hiervoor alleen tabel Order_.

16 Triggers en stored procedures In vorige hoofdstukken is al heel wat aandacht besteed aan regelhandhaving via SQL . Het ging daarbij om zowel beperkingsregels als gedragsregels. Opvallend is dat in SQL - DDL de ‘zuivere’ structuurdefinitie nauwelijks is gescheiden van de specificatie van regels. Zo bevat het create table -statement in één kommalijst zowel kolomdefinities als constraints. Echt vanzelfsprekend is deze verstrengeling niet. In dit hoofdstuk behandelen we een nieuwe implementatietechniek voor regels, door middel van de al vaak genoemde ‘triggers’ en ‘stored procedures’. Dit zijn programmaatjes die zijn opgeslagen in de database en die zijn geschreven in een ‘procedurele’ uitbreiding van SQL . We zullen deze gebruiken voor regels die we niet aankunnen met DDL .

Voorbeelddatabase De voorbeelddatabase in dit hoofdstuk is Toetjesboek ZT (een ‘kale’ versie van de Toetjesboek-database, nog zonder triggers en stored procedures). Zie figuur 16.1 voor het strokendiagram. Raadpleeg bijlage 4 voor de voorbeeldpopulatie.

Figuur 16.1 ToetjesboekZT: strokendiagram

16.1 Triggertaal Standaardregels en andere, eenvoudige regels, zoals waardebeperkingen op kolommen, kunnen worden afgedwongen door constraints die onderdeel zijn van DDL -code. Deze dienen primair om beperkingsregels te implementeren. Daarnaast kennen we clausules zoals on update cascade bij de foreign key -constraint, waarmee gedragsregels worden geïmplementeerd . Constraints zijn vastgelegd in de data dictionary en werkzaam op de databaseserver. Het rdbms zorgt dat ze op het juiste moment worden geactiveerd. Voor ingewikkelder beperkingsregels en allerlei typen gedragsregels zijn andere mechanismen beschikbaar. Ze zijn al vaak genoemd: triggers en stored procedures , databaseobjecten die net als constraints in de database worden opgeslagen en aan de serverkant actief zijn. Er is echter

een groot verschil: triggers en stored procedures worden niet in SQL geprogrammeerd, maar in een 3GL -uitbreiding ervan, een triggertaal . 3GL en 4GL Een hogere programmeertaal wordt een 3GL genoemd (taal van de derde generatie) wanneer men als programmeur de processen die tot het gewenste resultaat leiden, volledig kan specificeren. Hiertoe bevat een 3GL procedurele taalstructuren. Naast herhalings- en keuzeopdrachten zijn dat structuren waarbij deeltaken aan deelprogramma’s, procedures genaamd, worden gedelegeerd. Voorbeelden van 3GL’s zijn: COBOL, FORTRAN, Pascal, C, C++, Java en Haskell. In een 4GL hoeft de programmeur alleen het gewenste resultaat te specificeren. SQL wordt vaak een 4GL genoemd, hoewel de term tegenwoordig vooral wordt gebruikt voor complete ontwikkelomgevingen, waarin men onafhankelijk van een speciaal ‘merk’ database (Oracle, Ms SQL Server, MySQL, Firebird, ...) bedrijfsregels en applicaties kan specificeren.

Voorbeelddatabase: een ‘kaal’ toetjesboek Als voorbeelddatabase nemen we het Toetjesboek. We hebben immers nog een belofte in te lossen, namelijk te laten zien hoe de energie per persoon van een gerecht automatisch wordt berekend. Hiertoe beginnen we met een ‘kale’ versie van het Toetjesboek, zonder triggers en stored procedures, genaamd Toetjesboek ZT ( ZT = zonder triggers). Raadpleeg het create-script en het insert-script en ga na dat – de enige databaseobjecten van Toetjesboek de vier tabellen zijn – in Toetjesboek ZT de verwijzing van Ingredient naar Gerecht geen cascading delete heeft. (Deze zullen we zelf maken met een trigger, iets wat bij sommige SQL -dialecten zelfs niet anders kan.) – de waarden van Gerecht.energie PP null zijn (deze laten we immers automatisch berekenen).

16.2 Stored procedures Triggers en stored procedures zijn beide programma’s (meestal klein) die worden geprogrammeerd in een triggertaal. We beginnen met stored procedures, want die zijn het eenvoudigst.

16.2.1 Wat is een stored procedure? Een stored procedure (letterlijk: opgeslagen deelprogramma) is een 3GL programmaatje dat wordt opgeslagen in de database. Het wordt uitgevoerd door een expliciete aanroep vanuit een applicatie, een trigger of een andere stored procedure. Taken die vaak moeten worden uitgevoerd, kunnen we als het ware ‘delegeren’ aan een stored procedure. Dit is weer een vorm van ‘single point of definition’. Een stored procedure leest of wijzigt tabelinhouden en heeft daarom rechten nodig op die tabellen, net als een gebruiker. Het toekennen van rechten is daarom op stored procedures net zo van toepassing als op gebruikers. Anderzijds heeft een gebruiker een execute -recht nodig om een stored procedure te mogen aanroepen. Door een gebruiker rechten te geven op een stored procedure, maar niet rechtstreeks op de tabellen die door die stored procedure worden gewijzigd, kunnen de wijzigingen van allerlei waarborgen worden voorzien die onmogelijk zouden zijn wanneer de gebruiker rechtstreeks de tabellen kon benaderen. In dit hoofdstuk speelt de rechtenkwestie geen rol, daar alle opdrachten worden gegeven door de eigenaar van Toetjesboek ZT , de gebruiker Toetjesboek ZT . Ter illustratie van een stored procedure zullen we in het volgende voorbeeld een begin maken met de automatische energieberekening van gerechten in het Toetjesboek. Een begin, want de stored procedure die het werk doet, zullen we nog zelf moeten aanroepen, met een execute opdracht. In de volgende paragraaf zal de aanroep worden geautomatiseerd door middel van triggers, die reageren op veranderingen in de database. Voorbeeld 16.1 Energieberekening Toetjesboek De stored procedure voor de energieberekening moet de waarde van Gerecht.energie PP berekenen uit de hoeveelheden per persoon van de ingrediënten en de energiewaarden per eenheid van de producten die als ingrediënt worden gebruikt. En dat voor één willekeurig gerecht. De kern van de stored procedure zal dus een update zijn van energie PP in één rij van Gerecht: update Gerecht set    energiePP = (select sum(P.energiePE * I.hoeveelheidPP) from   Ingredient I join Product P on I.product = P.naam where  I.gerecht = naam van ‘het’ gerecht ) where  naam = naam van ‘het’ gerecht

Bij de aanroep van de stored procedure (de execute -opdracht) zal de naam van ‘het’ gerecht meegegeven moeten worden. In de definitie van de

stored procedure zelf is dat een ‘willekeurige waarde’, weergegeven door de formele parameter p_gerechtnaam. -- technisch nog niet correct create procedure pGerecht_update_energiePP(p_gerechtnaam varchar(25)) as begin update Gerecht set    energiePP = (select sum(P.energiePE * I.hoeveelheidPP) from  Ingredient I join Product P on I.product = P.naam where  I.gerecht = :p_gerechtnaam) where  naam = :p_gerechtnaam; end

Toelichting – De naam van de stored procedure is pGerecht_update_energie PP .

– Vetgedrukt daarachter staat tussen haakjes de formele parameter p_gerechtnaam, gevolgd door een datatype. – Een formele parameter is een soort plaatshouder, waarvoor later een echte waarde wordt ingevuld. Hun namen laten we altijd met p_ beginnen, zodat het onderscheid met kolomnamen goed te zien is. – Na create procedure ... as volgt de body van de procedure, met de eigenlijke code. Deze staat tussen de gereserveerde woorden begin en end , te vergelijken met een openings- en een sluithaakje. Vóór end is een puntkomma verplicht. – In het update -statement is voor de gerechtnaam van ‘het’ gerecht de formele parameter ingevuld. Binnen een SQL -statement moet een formele parameter worden voorafgegaan door een dubbele punt; anders zou de parameter als kolomnaam worden opgevat (met een foutmelding als gevolg).

Opgave 16.1 Voer het voorgaande create procedure -statement uit. Sluit de twee venstertjes waarin om een parameterwaarde gevraagd (het maakt niet uit hoe). Let daarna op de foutmelding. Toelichting : de ‘unexpected end of command’ wordt veroorzaakt door de puntkomma waarmee het update -statement wordt afgesloten. De interpreter ‘denkt’ dat dit ook het einde is van de proceduredefinitie. Voor dit technische probleem biedt het hierna te behandelen set term commando een oplossing. Het set term-commando Bij opgave 16.1 bleek dat de puntkomma waarmee het update -statement wordt afgesloten, tevens wordt gezien als het einde van het create procedure -statement. De oplossing is dat we als terminator (afsluitteken) voor stored procedures tijdelijk een ander teken definiëren. We kiezen hiervoor een ^. Een puntkomma wordt dan gezien als terminator voor statements binnen de stored procedure. De definitie wordt nu: set term ^; create procedure pGerecht_update_energiePP(p_gerechtnaam varchar(25)) as begin update Gerecht set    energiePP = (select sum(P.energiePE * I.hoeveelheidPP) from   Ingredient I join Product P on I.product = P.naam where  I.gerecht = :p_gerechtnaam) where  naam = :p_gerechtnaam; end^ set term ;

Toelichting – De opdracht set term ^ (zelf nog afgesloten met de oude terminator, de puntkomma) definieert ^ als nieuwe terminator, die de proceduredefinitie scheidt van vervolgopdrachten. – De opdracht set term ; (met een spatie voor de puntkomma!) herstelt de puntkomma in zijn gewone rol van algemene terminator. Indien nog andere statements volgen, moet dit statement worden afgesloten met de nog geldende ^-terminator: set term ;^ . Deze vorm maakt extra duidelijk dat de puntkomma in set term ; geen terminator is, maar de aanduiding voor de puntkomma als nieuwe terminator.

Opgave 16.2 Voer nogmaals het create procedure -statement uit, nu inclusief de set term -statements.

16.2.2 Uitvoeren van een stored procedure Een stored procedure wordt uitgevoerd door middel van een execute opdracht. Dat kan vanuit een applicatie, vanuit een andere stored procedure of vanuit een trigger. Wij zullen de stored procedure pGerecht_update_energie PP uitvoeren vanuit IQU . Als eigenaar van Toetjesboek ZT hebben we automatisch het vereiste execute -recht: execute procedure pGerecht_update_energiePP('Coupe Kiwano')

Door deze aanroep wordt de programmacode van pGerecht_update_energie PP uitgevoerd, met ‘Coupe Kiwano’ gesubstitueerd voor :p_gerechtnaam. De volgende code wordt dus uitgevoerd: update Gerecht set    energiePP = (select sum(P.energiePE * I.hoeveelheidPP) from   Ingredient I join Product P on I.product = P.naam where  I.gerecht = 'Coupe Kiwano') where  naam = 'Coupe Kiwano'

Een expressie (‘Coupe Kiwano’ in dit geval) waarmee een procedure wordt aangeroepen, heet een actuele parameter . Deze wordt gesubstitueerd voor de corresponderende formele parameter in de proceduredefinitie. In de volgende paragraaf laten we de procedure pGerecht_update_energie PP uitvoeren vanuit een vijftal triggers, horend bij evenzovele situaties waarin de energiewaarde automatisch moet worden herberekend. Formele parameter en variabele In de definitie van procedure pGerecht_update_energiePP ( voorbeeld 16.1 ) zagen we het gebruik van een formele parameter. Zo’n formele parameter is iets anders dan een variabele. Een formele parameter is slechts een invulplaats, zonder verdere semantiek. Een variabele daarentegen correspondeert met een stukje geheugen: het is een ‘geheugenplaats’. Bij aanroep van een stored procedure (met een execute -statement) wordt op de plaats van de formele parameter ‘iets’ ingevuld dat een waarde heeft. Dat ‘iets’ kan een constante zijn of een variabele of een samengestelde expressie.

Opgave 16.3 Voer de stored procedure uit voor alle drie de gerechten.

16.3 Triggers Een trigger is net als een stored procedure een programmaatje geschreven in de 3GL -triggertaal van het rdbms, dat wordt opgeslagen in de data dictionary van de database. Anders dan een stored procedure wordt een trigger niet expliciet aangeroepen, maar automatisch geactiveerd bij bepaalde events (gebeurtenissen): – invoegen van een rij of een poging daartoe – verwijderen van een rij of een poging daartoe – wijzigen van een kolomwaarde of een poging daartoe. De term ‘trigger’ Het woord ‘trigger’ is geassocieerd met de ‘trekker’ van een geweer, die het geweer doet afgaan. Dit is echter misleidend: een trigger is juist het proces dat ‘afgaat’ (het geweer), en de inserts of insert-pogingen, deletes of deletepogingen en updates of updatepogingen zijn de ‘trekkers’ (events) die uitvoering van het triggerprogrammaatje tot gevolg hebben.

16.3.1 Before-triggers en after-triggers Triggers kunnen we in twee categorieën verdelen: – before-triggers : deze worden geactiveerd vóórdat een insert, delete of ujpdate wordt uitgevoerd; ze kunnen deze eventueel tegenhouden. – after-triggers : deze worden geactiveerd nadat een insert, delete of update heeft plaatsgevonden en voeren aanvullende acties uit. Van beide typen zullen we voorbeelden zien. Eerst echter moeten we wat dieper ingaan op de events (gebeurtenissen) waardoor een trigger kan worden geactiveerd.

16.3.2 Events Een insert -opdracht is een verzoek aan het rdbms om een of meer rijen in te voegen. Vanuit het rdbms gezien vormt zo’n invoegverzoek, voor elke rij apart, een event (gebeurtenis). Meer specifiek: een before-insert-event . Vanuit de gebruiker gezien (dus vanuit de applicatie) is zo’n verzoek in eerste instantie niet meer dan een poging een nieuwe rij in te voeren. Voor het rdbms is het zelfs dát niet: voordat het rdbms eventueel de rij poogt in te voeren, kijkt het eerst of er een before-insert-trigger is gedefinieerd voor de betreffende tabel. Zo ja, dan wordt deze uitgevoerd. Een voorbeeld is een before-insert-trigger voor een orderregel die voor die orderregel het juiste volgnummer berekent. Na die berekening zal het rdbms pogen het insert -statement uit te voeren. Of dat echt lukt, is nog maar de vraag: het kan altijd nog mis gaan, bijvoorbeeld door een overtreding van de referentiële-integriteitsregel. Een before-insert-trigger kan ook een controle bevatten, waarna – als deze negatief uitvalt – de insert -opdracht wordt geannuleerd en een foutmelding naar de applicatie wordt gestuurd. Laten we nu aannemen dat een rij met succes is ingevoerd. Er heeft dan een nieuwe event plaatsgevonden, een after-insert-event . Het rdbms raadpleegt de data dictionary om te kijken of er een after-insert-trigger is gedefinieerd voor de tabel. Zo ja, dan wordt deze uitgevoerd. Het invoegen van de rij is dan al een feit en kan alleen ongedaan worden gemaakt door een mislukking bij het invoeren van een andere rij tengevolge van hetzelfde insert -statement, of door een rollback van de transactie waar het statement onderdeel van is. Zo’n after-insert-trigger zal in het algemeen extra gedrag genereren, bijvoorbeeld het schrijven van een rij in een ‘logboektabel’, waarin wordt bijgehouden welke gebruikers wanneer welke veranderingen in de database hebben aangebracht. Het zal nu duidelijk zijn dat er minstens zes eventtypen zijn waaraan een trigger kan worden gekoppeld: – before-insert-event – after-insert-event – before-delete-event – after-delete-event

– before-update-event – after-update-event. Daarnaast zijn er in principe nog eventtypen, gekoppeld aan commits of rollbacks, of aan pogingen daartoe. Deze blijven hier buiten beschouwing. Opmerking : een insert -, delete - of update -statement kan betrekking hebben op meerdere rijen. In zo’n geval genereert de statement voor elke rij apart de bijbehorende before- en after-events. Voor elke rij apart worden dus eventuele triggers uitgevoerd. Vanzelfsprekend wordt het geheel als één transactie uitgevoerd. Gaat er ergens iets mis, dan wordt alles teruggedraaid. Voorbeeld 16.2 De kolom Gerecht.energie PP moet in vier gevallen automatisch worden bijgewerkt: 1 na het toevoegen van een ingrediënt 2 na het verwijderen van een ingrediënt 3 na het wijzigen van een hoeveelheid per persoon 4 na het wijzigen van een energie per eenheid Voor elke situatie is er een aparte trigger, die reageert op de betreffende event: 1 een after-insert-trigger op Ingredient: tIngredient_ai 2 een after-delete-trigger op Ingredient: tIngredient_ad

3 een after-update-trigger op Ingredient: tIngredient_au 4 een after-update-trigger op Product: tProduct_au Let op de systematiek in de naamgeving: elke triggernaam begint met een t, gevolgd door de tabelnaam, een underscore en een aanduiding voor het type. Hoewel de triggers van voorbeeld 16.2 op verschillende situaties reageren, doen ze vrijwel hetzelfde: herberekenen van energie PP . Voor die herberekening hebben we een stored procedure: pGerecht_update_energie PP , die door elke trigger zal worden aangeroepen. Toch zullen er in de triggercode kleine verschillen optreden: de update-triggers bijvoorbeeld moeten controleren of het echt wel nodig is de herberekening uit te voeren. Misschien immers had de update betrekking op een ‘onschuldige’ kolom! Zo zal trigger t Ingredient_au eerst kijken of de kolomwaarde van hoeveelheid PP is veranderd, door de oude en de nieuwe waarde met elkaar te vergelijken. Hierdoor wordt duidelijk dat een update-trigger in principe moet kunnen beschikken over de oude én de nieuwe kolomwaarden. Die oude en nieuwe waarden zijn beschikbaar via zogenaamde contextvariabelen , waarover meer in de volgende paragraaf.

16.3.3 Contextvariabelen old en new De after-update-trigger tIngredient_au reageert op een update van Ingredient en zal moeten controleren of hoeveelheid PP is veranderd. De oude en de nieuwe waarde zijn beschikbaar via de contextvariabelen old en new . Deze staan voor de oude en de nieuwe waarde van de rij waarvoor het update-event heeft plaatsgevonden. De kolomwaarden van old en new zijn beschikbaar via een puntnotatie: zo zijn old.hoeveelheidPP en new.hoeveelheidPP de oude waarde (vóór update) en de nieuwe waarde (na update) van hoeveelh eidPP . Het vergelijken hiervan geschiedt via een voorwaardelijke programmaconstructie met if…then… : … if (old.hoeveelheidPP new.hoeveelheidPP) then execute procedure pUpdateGerechtEnergie(new.gerecht) …

Dit fragment zullen we in de volgende paragraaf terugzien als onderdeel van de code voor tIngredient_au. De contextvariabele old speelt ook een rol bij deletes, ter aanduiding van de rij die is verwijderd of waarop de poging tot verwijderen betrekking heeft. Bij deletes bestaat geen contextvariabele new . Bij inserts hebben we het omgekeerde: daarbij beschikken we over de contextvariabele new voor de rij die is ingevoegd of waarop een invoegpoging betrekking heeft. Bij een insert bestaat geen contextvariabele old . Zie tabel 16.1 voor een overzicht. Tabel 16.1 old- en new-contextvariabelen

16.3.4 Voorbeelden van after-triggers We weten nu genoeg om de code te kunnen begrijpen van de triggers voor de automatische energieberekening van de Toetjesboek-gerechten. Drie ervan geven we als voorbeeld, een vierde (tIngredient_ad) als opgave. Ze zijn alle van het after-type. Voorbeeld 16.3 De code voor de eerste trigger van voorbeeld 16.2 , tIngredient_ai, die wordt geactiveerd na het invoegen van een Ingredient-rij, luidt als volgt: set term ^; create trigger tIngredient_ai for Ingredient after insert as begin execute procedure pGerecht_update_energiePP(new.gerecht); end^ set term ;

Toelichting – Net als bij stored procedures moet voorafgaand aan de definitie van een trigger de puntkomma als scheidings- en afsluitteken tijdelijk

worden vervangen door een ander teken (^). Na de definitie wordt de puntkomma in zijn oude rol hersteld. – De naamgeving weerspiegelt voor de menselijke lezer het type trigger en bevordert een logische volgorde in alfabetische overzichten. Het rdbms ontleent zijn informatie omtrent het type niet aan de triggernaam, maar aan de for -clausule. – Het enige wat de trigger doet, is het aanroepen van de stored procedure voor de herberekening. – new .gerecht is de waarde van kolom gerecht van de zojuist ingevoegde Ingredient-rij.

Opgave 16.4 Creëer de trigger tIngredient_ai door het create trigger -statement uit te voeren en controleer het effect door een Ingredient-rij toe te voegen. Opmerking : in Toetjesboek ZT zijn alle energie PP -waarden aanvankelijk null. Opgave 16.5 Schrijf de create trigger -code voor de tweede trigger van voorbeeld 16.2 , tIngredient_ad, die reageert op het verwijderen van een ingrediënt. Voer de opdracht uit en controleer het effect, bijvoorbeeld door de in opga ve 16.8 ingevoegde Ingredient-rij weer te verwijderen. Voorbeeld 16.4 De derde trigger van voorbeeld 16.2 , tIngredient_au, reageert op een update van een ingrediënt. De stored procedure wordt alleen aangeroepen indien hoeveelheid PP blijkt te zijn gewijzigd: set term ^; create trigger tIngredient_au for Ingredient after update as begin if (old.hoeveelheidPP new.hoeveelheidPP) then execute procedure pGerecht_update_energiePP(new.gerecht); end^ set term ;

Toelichting – De if -conditie staat verplicht tussen haakjes.

– De taak (na then ) wordt alleen uitgevoerd wanneer de conditie true oplevert. Wellicht merkt u op dat energie PP ook herberekend moet worden bij een update van Ingredient.gerecht of Ingredient.product. Deze updates zijn echter nogal onnatuurlijk, omdat daarmee niet een eigenschap van een ingrediënt wordt veranderd, maar een ingrediënt wordt vervangen door een ander ingrediënt. Conceptueel gezien hebben we hier niet te maken met een update, maar met een delete gevolgd door een insert. Het is dan ook beter een SQL -update voor dit soort gevallen onmogelijk te maken. De middelen daarvoor zullen we verderop aanreiken. Zie ook de discussie in paragraaf 10.6.3 over updates van een deel van een primaire sleutel. Voorwaardelijke programmastructuur Voorbeeld 16.4 geeft nog aanleiding tot de volgende opmerkingen bij de voorwaardelijke programmastructuur (keuzestructuur) met if . Als de te verrichten taak (na then ) meerdere opdrachten omvat, dan moeten deze tussen begin en end staan: if ( conditie ) then begin …; …; end;

Ook hier vormen begin en end een soort openings- en sluithaakje. Het geheel, van begin tot en met end , heet een samengestelde opdracht (Engels: compound statement ). Indien er ook bij een uitkomst false of unknown van de conditie iets moet gebeuren, is hiervoor de if ... then ... else ...-constructie beschikbaar: if ( conditie ) then …; else …;

Opmerkingen – Na zowel then als else mag een enkelvoudig of een samengesteld statement volgen. – Aan zowel end als else gaat een puntkomma vooraf; dit is het afsluitteken voor de voorafgaande statement. In Firebird- 3GL is de

puntkomma geen scheidingsteken tussen statements ( separator ) maar afsluitteken ná een statement ( terminator ).

Opgave 16.6 Voer de code van tIngredient_au uit en controleer het effect. Voorbeeld 16.5 De vierde trigger van voorbeeld 16.2 is tProduct_au. Deze reageert op een update van een Product-rij. Dit wordt de ingewikkeldste; we zullen de code stap voor stap ontwikkelen. Alleen wanneer de kolomwaarde energie PE blijkt te zijn gewijzigd, hoeft er wat te gebeuren. De structuur van de code (de technische kwestie met set term even buiten beschouwing gelaten) wordt dus als volgt: create trigger tProduct_au for Product after update as begin if (old.energiePE new.energiePE) then pas energiePP aan in alle Gerecht-rijen waarin ‘dit’ product voorkomt ; end^

We hoeven nu ‘alleen nog maar’ de natuurlijke-taalopdracht te vertalen in triggercode. De natuurlijke-taalopdracht bevat echter een nieuw element: het herhaald uitvoeren van iets. In principe moeten immers meerdere Gerecht-rijen worden aangepast. De triggertaal beschikt hiervoor over een zogenaamde lusconstructie met for…do… : create trigger tProduct_au for Product after update as begin if (old.energiePE new.energiePE) then for alle (namen van) gerechten die ‘dit’ product als ingrediënt hebben do voer voor die (namen van) gerechten de procedure p Gerecht_update_energiePP uit ; end^

Kennelijk moeten alle namen van gerechten die ‘dit’ product als ingrediënt hebben, uit de database worden opgehaald. Hiervoor breiden we de code uit met een SQL -statement op de tabel Ingredient: create trigger tProduct_au for Product after update as begin if (old.energiePE new.energiePE) then for select gerecht from   Ingredient where  product = new.naam … do  execute procedure pGerecht_update_energiePP(…); end^

We zijn er nu bijna: we moeten nu de gerechtnamen die de select opdracht oplevert, nog één voor één overdragen aan de procedure. Hiervoor is een variabele nodig, een soort opbergplaats in het geheugen

waarin een waarde kan worden bewaard. We zullen daarom een variabele creëren, die we de naam v_gerechtnaam geven. Hoewel, technisch gezien, elke identifier als variabelenaam voldoet, is het vanuit oogpunt van programmeerstijl prettig als we direct kunnen zien dat het om een variabele gaat (vandaar het voorvoegsel v_) en wat zijn functie is (vandaar de rest van de naam). Het creëren van een variabele geschiedt in de triggercode, vóór de openings- begin , met declare variable , gevolgd door de variabelenaam en zijn datatype: declare variable v_gerechtnaam varchar(25);

Om de gerechtnamen die de select -expressie oplevert, één voor één aan de variabele v_gerechtnaam toe te kennen, wordt de select -expressie uitgebreid met een into -clausule. De volledige triggercode wordt nu: set term ^; create trigger tProduct_au for Product after update as declare variable v_gerechtnaam varchar(25); begin if (old.energiePE new.energiePE) then for select gerecht from   Ingredient where  product = new.naam into  :v_gerechtnaam do execute procedure pGerecht_update_energiePP( v_gerechtnaam ); end^ set term ;

Toelichting : elke gerechtnaam die de select -opdracht oplevert, wordt (op zijn beurt) aan de variabele v_gerechtnaam toegekend, waarna voor díe waarde van de variabele de do -opdracht wordt uitgevoerd (dus de procedure wordt uitgevoerd).

Opgave 16.7 Voer de code van tProduct_au uit en controleer het effect. Opmerking : wanneer u nu de hele populatie verwijdert en opnieuw invoert (bijvoorbeeld door het script Toetjesboek ZT Insert.sql uit te voeren), worden alle energie PP -waarden correct berekend en ingevuld. Triggers: houd ze eenvoudig Al met al komt er nogal wat kijken bij het bewaken van één regel (het berekenen of herberekenen van energie PP ). En wie nu denkt dat het allemaal waterdicht is, heeft het mis. Bijvoorbeeld: worden in één update van Gerecht tegelijkertijd de kolommen naam en energie PP veranderd, dan ontstaat een gecompliceerde situatie. Er is dan verschil tussen new .naam en old .naam en de vraag is welke van de twee we in de afterupdate-trigger moeten gebruiken. Het blijkt dat de trigger in beide gevallen niet goed werkt! Het beste is om dergelijke onnatuurlijke ingrepen onmogelijk te maken. Ook dat kan weer met triggers, maar het kan ook in de code van de

applicatie. Algemeen geldt overigens dat regels niet alleen via triggers kunnen worden afgedwongen, maar ook via applicatiecode. Of zelfs via een aparte module, rule engine genaamd. In de cursus Model-driven development komen al die manieren uitgebreid aan de orde. Voor zover we regels in de database afdwingen, met triggers dus, is het vaak beter meer triggers te maken die allemaal eenvoudig werk doen, dan dat we met minder triggers in gecompliceerde situaties verzeild raken. In elk geval moet worden vermeden dat de code voor twee verschillende regels in één trigger wordt gecombineerd. Immers, als een regel niet meer geldt, moet je de bijbehorende triggers zonder bezwaar kunnen weggooien, zonder enig gevolg voor andere regels.

16.3.5 Voorbeelden van before-triggers Een after-trigger wordt geactiveerd nadat een gebeurtenis (insert, delete, update) heeft plaatsgevonden. Een before-trigger doet dat voordat het zover is, dus bij een poging tot insert, delete of update. Het is daarbij maar helemaal de vraag of die poging gaat lukken. In deze paragraaf zien we twee voorbeelden (waarvan één via een opgave) waarbij zo’n poging lukt, maar via een before-trigger wordt voorafgegaan door extra gedrag. In de volgende paragraaf zullen we zien hoe beforetriggers kunnen worden gebruikt om bepaalde beperkingsregels af te dwingen en waarbij een poging tot insert enzovoort wordt tegengehouden wanneer niet aan de regel is voldaan. Voorbeeld 16.6 Before-insert-trigger voor berekening Ingredient.volgnr In dit voorbeeld laten we zien hoe een before-insert-trigger een volgnummer berekent voor een nieuwe Ingredient-rij. Dat volgnummer hoeft dan niet aan het insert -statement te worden meegegeven. De triggercode stellen we in eerste instantie op als volgt: create trigger tIngredient_bi for Ingredient before insert as begin bereken over de al bestaande Ingredient-rijen bij hetzelfde gerecht max(volgnr)+1 en ken dit toe aan new.volgnr ; if (new.volgnr is null) then new.volgnr = 1; end

We gebruiken hier weer dat voor de in te voegen rij een contextvariabele new beschikbaar is. Volgens goed programmeergebruik hebben we het lastigste van alles in natuurlijke taal geformuleerd (anders gezegd: als deelprobleem voor ons uitgeschoven). Wanneer het om de eerste

Ingredient-rij gaat bij de opgegeven gerechtnaam, zal max(volgnr) + 1 een null retourneren. Hierin voorziet het if -statement; new.volgnr krijgt dan de waarde 1. De waarde max(volgnr) + 1 zullen we berekenen met een select statement, waarna we deze waarde via een into -clausule toekennen aan new.volgnr (zie ook voorbeeld 16.5 .) De volledige code wordt nu: set term ^; create trigger tIngredient_bi for Ingredient before insert as begin select max(volgnr) + 1 from   Ingredient where  gerecht = new.gerecht into   new.volgnr; if (new.volgnr is null) then new.volgnr = 1; end^ set term ;

Opgave 16.8 Controleer tIngredient_bi door een nieuw ingrediënt (zonder volgnummer!) in te voeren voor een van de bestaande gerechten. Voer ook een nieuw gerecht in en daarbij één ingrediënt (zonder volgnummer) en kijk of het volgnummer 1 wordt. Opgave 16.9 In sommige dialecten is het onmogelijk refererende acties te implementeren via een create table -statement. Het moet dan met triggers. Schrijf zo’n trigger voor een cascading delete bij de verwijzing van Ingredient naar Gerecht. Opmerking : Toetjesboek ZT is gecreëerd zonder cascading deletes of cascading updates. Ook voor de cascading updates zouden vergelijkbare triggers kunnen worden geschreven.

16.3.6 Before-trigger met exception Deze paragraaf is gewijd aan de mogelijkheid een trigger (en eventueel een stored procedure) een uitzonderings- of foutsituatie te laten signaleren en afhandelen. Dit gaat gepaard met het ‘opgooien’ van een exception . Zo’n exception (letterlijk: uitzondering) is niet veel meer dan een foutmelding, met daaraan gekoppeld een mechanisme om de normale verwerking onder bepaalde voorwaarden af te breken (waarbij de foutmelding wordt getoond) en eventueel alternatieve actie te ondernemen.

Voorbeeld 16.7 Verbod rechtstreeks wijzigen energiePP In paragraaf 16.3.4 hebben we vier after-triggers gezien voor herberekening van Gerecht.energie PP . Er is echter een vijfde situatie waarbij herberekening nodig is: wanneer een gebruiker energie PP rechtstreeks wijzigt. Ook hiervoor kunnen we een after-trigger schrijven, die energie PP herberekent en die dus de oude waarde terugzet. Beter echter is eerder in actie te komen, voordat het kwaad is geschied: via een before-update-trigger met een exception. Eerst creëren we de exception: create exception eUpdatepogingEnergiePP 'De energiewaarde van een gerecht wordt automatisch berekend en kan niet worden gewijzigd.'

Toelichting – Als foutmelding van een exception formuleren we de regel die dreigt te worden overtreden. – Als exceptionnaam kiezen we een ‘e’ met daarachter een bondige formulering van de foutsituatie waarbij de trigger in actie moet komen. De triggercode luidt als volgt. set term ^; create trigger tGerecht_bu for Gerecht before update as begin if (new.energiePP old.energiePP) then exception eUpdatepogingEnergiePP; end^ set term ;

Toelichting – Een update-trigger beschikt over de contextvariabelen new en old . – De trigger test of de nieuwe waarde van energie PP verschilt van de oude waarde. Zo ja, dan wordt de exception ‘opgegooid’, wat inhoudt dat een uitzonderingstoestand wordt uitgeroepen, waarbij de update niet doorgaat en de gebruiker de foutmelding krijgt te zien. Voeren we deze code uit en wagen we daarna de volgende updatepoging: update Gerecht

set    energiePP = 600 where  naam = 'Coupe Kiwano'

dan is het resultaat de volgende foutmelding: exception 1 EUPDATEPOGINGENERGIEPP De energiewaarde van een gerecht wordt automatisch berekend en kan niet worden gewijzigd. At trigger 'TGERECHT_BU' line: 5, col: 7.

Dit gebeurt alleen wanneer energie PP al een waarde had. Bij waarde null treedt geen foutsituatie op en wordt het update -statement gewoon uitgevoerd.

Opgave 16.10 Voer de code van voorbeeld 16.7 uit: creëren exception, creëren trigger en pogen energie PP rechtstreeks te wijzigen. Geen goede oplossing In opgave 16.10 vroegen we u de before-update-trigger op een beperkte manier te testen: hij deugt namelijk niet. Omdat de stored procedure pUpdateGerechtEnergie PP een update van Gerecht bevat, zal de trigger ook afgaan bij elke aanroep van die procedure. En dat is bij elke event die tot herberekening van energie PP aanleiding geeft. Het effect is dat we bijvoorbeeld geen ingrediënten meer kunnen toevoegen of weghalen. Het is wel op te lossen, maar voor dit hoofdstuk voert dat wat ver. Wanneer u het script ToetjesboekCreate.sql raadpleegt, zult u ook zien dat daar gekozen is voor een vijfde after-trigger, die achteraf de waarde van energie PP herstelt, mocht de gebruiker deze rechtstreeks hebben gewijzigd. Opgooien en vangen van een exception Bij het optreden van een exception spreekt men doorgaans over het opgooien van een exception (Engels: to throw an exception). Dit heeft te maken met de mogelijkheid een opgegooide exception te vangen (Engels: to catch) door alternatieve code te laten uitvoeren. Hiertoe kan de body van een trigger of stored procedure aan het eind een of meer when -clausules bevatten, met na het gereserveerde woord when een exceptionnaam en de alternatieve code. De syntax hiervoor luidt: when exception < exceptionnaam > do < statement >.

Het tweede voorbeeld ontlenen we aan de OpenSchool-database. Omdat het wat ingewikkelder is, passen we een stapsgewijze aanpak toe. Voorbeeld 16.8 Voorkennisregel OpenSchool

In de OpenSchool-database geldt de regel dat een student pas mag worden ingeschreven voor een cursus wanneer hij voor de voorkenniscursussen een voldoende heeft of een vrijstelling. De regel gaat over ‘inschrijven voor een cursus’ en heeft dus betrekking op inserts op tabel Inschrijving. Zo’n insert moet worden tegengehouden wanneer niet aan de voorkenniseis is voldaan. We formuleren eerst een exception: create exception eNiet_aan_voorkennis_voldaan 'Niet voor alle voorkenniscursussen een voldoende of vrijstelling'

De trigger die de regel moet bewaken, zal een before-insert-trigger moeten worden op tabel Inschrijving. De globale opzet wordt als volgt: create trigger tInschrijving_bi for Inschrijving before insert as begin if ( voorkennisregel overtreden ) then exception eNiet_aan_voorkennis_voldaan; end

Als volgende stap vervangen we de conditie door een expressie met exists : create trigger tInschrijving_bi for Inschrijving before insert as begin if (exists ( voorkenniscursussen bij de cursus van ‘deze’ (nieuwe) inschrijving waarvoor de student van ‘deze’ (nieuwe) inschrijving geen voldoende heeft en ook geen vrijstelling )) then exception eNiet_aan_voorkennis_voldaan; end

Merk op dat het probleem dat we aan het oplossen zijn, inmiddels heel erg is gaan lijken op het probleem van voorbeeld 9.18 . Voorgaande exists expressie komt letterlijk in voorbeeld 9.18 voor. In deze nieuwe context verwijst ‘de cursus van deze inschrijving’ naar de cursus van de nieuwe inschrijving, dat is naar new.cursus. De student bij die inschrijving is: new.student. Dit geeft: create trigger tInschrijving_bi for Inschrijving before insert as begin if (exists ( voorkenniscursussen van new.cursus waarvoor new.student geen voldoende heeft en ook geen vrijstelling )) then exception eNiet_aan_voorkennis_voldaan; end

Analoog aan voorbeeld 9.18 vervangen we nu de tekst in natuurlijke taal door een subselect. Samen met de set term -commando’s geeft dit de volgende triggercode: set term ^;

create trigger tInschrijving_bi for Inschrijving before insert as begin if (exists (-- voorkenniscursussen van new.cursus waarvoor new.student geen voldoende heeft -- en ook geen vrijstelling select * from   Cursus Voorkenniscursus where  code in (-- codes van voorkenniscursussen van new.cursus select voorkennis from   Voorkenniseis where  cursus = new.cursus) and code not in (-- cursussen van new.student met een voldoende of een vrijstelling select cursus from   Inschrijving where  student = new.student and (cijfer > 5 or vrijstelling = 'J')))) then exception eNiet_aan_voorkennis_voldaan; end^ set term ;

Opgave 16.11 Creëer de exception en de trigger van voorbeeld 16.8 en voer een test uit. In voorbeeld 16.8 hebben we de voorkennisregel bewaakt via één eventtype (before-insert) op één tabel (Inschrijving). Maar wat als bijvoorbeeld de voorkenniseisen veranderen, waardoor een bestaande inschrijving ineens niet meer aan de nieuwe eisen voldoet? In ons voorbeeld hebben we daar geen last van: de regel spreekt uitdrukkelijk over een conditie die moet gelden bij inschrijving, niet over veranderingen achteraf. Toch moeten we in het algemeen erop attent zijn dat een regel overtreden kan worden door events in meer dan één tabel en ook bij meerdere eventtypen (bijvoorbeeld zowel before-insert als before-update). We zien daarvan een voorbeeld in opgave 16.12 .

Opgave 16.12 In de OpenSchool-database willen we de volgende regel implementeren: de examinator van een cursus mag geen begeleider zijn van die cursus. a De voorbeeldpopulatie voldoet niet aan deze regel. Maak daarom vooraf tabel Begeleider even leeg. Dat maakt ook straks het testen wat gemakkelijker. b Geef voor de betrokken tabellen (Cursus en Begeleider) aan of een insert, een update dan wel een delete aanleiding kan zijn tot een overtreding. Opmerking : laat updates van Begeleider buiten beschouwing; zie de

discussie in paragraaf 10.6.3 over het wijzigen van een deel van een primaire sleutel. c Implementeer de regel door middel van triggers en één exception. Kies voor de exception een neutrale naam en foutmelding, die alleen gerelateerd is aan de regel en niet aan het type trigger.

16.3.7 Multi-event-triggers We hebben al gezien dat voor implementatie van één regel soms meerdere triggers nodig zijn, één voor elk type event waarbij de regel bewaakt moet worden. Voor zover die triggers op dezelfde tabel betrekking hebben en van hetzelfde algemene type (‘before’ of ‘after’) zijn, kunnen ze vaak worden samengevoegd tot één trigger die reageert op meer eventtypen: een multi-event-trigger . Zo kun je multi-event-triggers schrijven die reageren op de multi-event ‘insert or delete’ of op ‘insert or delete or update’. Het is niet nodig dat de code van de oorspronkelijke triggers gelijk is. Door middel van de boolean contextvariabelen inserting , deleting en updating kan onderscheid worden gemaakt naar de werkelijk optredende event, zodat verschillende code kan worden uitgevoerd in geval van een insert, een delete of een update. Voorbeeld 16.9 Triggers bij meer dan één eventtype Voor de automatische berekening van Gerecht.energie PP hebben we vijf triggers geschreven, waaronder drie after-triggers op tabel Ingredient. Het fragment van ToetjesboekCreate.sql waarin deze drie triggers worden gedefinieerd, luidt aldus: create trigger tIngredient_ai for Ingredient after insert as begin execute procedure pGerecht_update_energiePP(new.gerecht); end^ create trigger tIngredient_ad for Ingredient after delete as begin execute procedure pGerecht_update_energiePP(old.gerecht); end^ create trigger tIngredient_au for Ingredient after update as begin if (old.hoeveelheidPP new.hoeveelheidPP) then execute procedure pGerecht_update_energiePP(new.gerecht); end^

De triggers tIngredient_ai en tIngredient_au bevatten dezelfde procedureaanroep, alleen de voorwaarden waaronder die aanroep wordt uitgevoerd, zijn verschillend. Bij tIngredient_ai is dat de impliciete voorwaarde dat de event een insert is. Bij tIngredient_au zijn er twee voorwaarden: de impliciete voorwaarde dat de event een update is en de expliciete voorwaarde dat old.hoeveelheid PP niet gelijk is aan new.hoeveelheid PP . We kunnen beide triggers vervangen door één trigger van het multi-eventtype ‘insert or update’, als volgt: create trigger tIngredient_a iu for Ingredient after insert or update as begin if ( inserting or updating and old.hoeveelheidPP new.hoeveelheidPP) then execute procedure pGerecht_update_energiePP(new.gerecht); end^

Toelichting – De triggernaam hebben we aangepast aan het samengestelde eventtype: aiu staat voor ‘after insert or update’. – De boolean contextvariabele inserting krijgt zijn waarde door de context waarin de trigger wordt aangeroepen: bij een insert wordt inserting true , bij een update (de andere mogelijkheid bij de multi-event ‘insert or update’) wordt hij false . Analoog voor de contextvariabele updating . Door gebruik van een if … else -constructie kunnen we de trigger ook geschikt maken om te reageren op een delete-event. Dit geeft één trigger van het multi-eventtype ‘insert or delete or update’, als volgt: create trigger tIngredient_aidu for Ingredient after insert or delete or update as begin if (inserting or updating and old.hoeveelheidPP new.hoeveelheidPP) then execute procedure pGerecht_update_energiePP(new.gerecht); else if (deleting) then execute procedure pGerecht_update_energiePP(old.gerecht); end^

Toelichting – Onder ‘else’ valt ook de mogelijkheid van een update op een andere kolom dan hoeveelheid PP ; daarom volgt nog de clausule if (deleting) then .

De nieuwe trigger vervangt de oorspronkelijke drie after-triggers. Door gebruik van een conditionele structuur en contextvariabelen kunnen triggers op dezelfde tabel en van hetzelfde type (before of after) altijd in één multi-event-trigger worden samengevoegd. Het geheel aan triggercode kan hierdoor een stuk overzichtelijker worden. Een tweede voorbeeld van multi-event-triggers treft u aan bij opgave 16.13 . Computed column We hebben in deze paragraaf vrij veel moeite gedaan (vijf triggers) om één vrij eenvoudige regel te implementeren: de berekening van afleidbare kolomwaarden. Kan dat nu niet eenvoudiger? Sinds enige tijd is het antwoord: ja, met een computed column (berekenbare of afleidbare kolom, zie ook paragra af 11.5.5 ). In de DDL-code van Gerecht hadden we energiePP als volgt als computed column kunnen opnemen: create table Gerecht (… energiePP generated always as ((select sum(P.energiePE * I.hoeveelheidPP) from   Ingredient I join Product P on I.product = P.naam where  I.gerecht = Gerecht.naam)), … ); U kunt dit eenvoudig uitproberen, bijvoorbeeld door met een alter table drop -statement de oude kolom energiePP weg te gooien en met een alter table add -statement een nieuwe aan te maken als computed column. Verwijder wel eerst de energiePP-triggers of ga uit van een ‘schoon’ ToetjesboekZT.

16.4 Meer over triggers en stored procedures De mogelijkheden van triggers en stored procedures zijn veel uitgebreider dan we in dit boek kunnen behandelen. We stippen nog een aantal aspecten aan..

16.4.1 Deltaproblematiek Stored procedures, triggers en ook exceptions laten zich via drop statements eenvoudig verwijderen. Bijvoorbeeld: drop procedure

Het is wel zaak de juiste volgorde in acht te nemen: triggers kunnen immers een stored procedure aanroepen en stored procedures ook elkaar, terwijl beide op hún beurt een exception kunnen aanroepen. Het is zaak van onderaf te beginnen: bij een databaseobject dat zelf niet door andere objecten wordt aangeroepen. Ook kunnen de definities worden gewijzigd, via corresponderende alter statements. Het alter trigger -commando kent ook een variant om een trigger tijdelijk buiten werking te stellen: alter trigger ... inactive . Met alter trigger ... active wordt de trigger weer actief. Zoals ook bij andere objecten zijn er naast de alter -commando’s ook varianten met create or alter , die een object veranderen als het al bestaat en anders creëren. Daarnaast zijn er de recreate -commando’s. Het voert te ver om deze varianten hier te bespreken maar wie voor het ‘echte werk’ gaat, kan er veel plezier van hebben. Raadpleeg voor de precieze werking de documentatie.

16.4.2 Executable procedures en select-procedures De stored procedures van dit hoofdstuk waren alle zogenaamde executable procedures . Een procedure kan echter ook zodanig worden geschreven dat hij een waarde retourneert, net als een functie. Die waarde hoeft echter niet enkelvoudig te zijn en kan ook een lijst zijn of een tabel, die tijdens uitvoering van de stored procedure rij voor rij wordt opgebouwd. Indien het alleen om zo’n geretourneerde waarde gaat, spreekt men van een select-procedure . Gebruikers kunnen daar rechten op krijgen, zelfs insert-, delete- en updaterechten. Dit werkt als volgt: DML -statements ( insert -, delete - en update -opdrachten) worden afgevangen door triggers, waarvan de code vervolgens bepaalt wat er zal gebeuren.

16.4.3 Stored procedures en views Stored procedures kunnen zich aan de gebruiker voordoen als een tabel (zie paragraaf 16.4.2 ). Net als views zijn het in dat geval ‘virtuele tabellen’. Ook DML -statements op views kunnen worden afgevangen door

triggers, die vervolgens een gepaste actie zullen uitvoeren op de onderliggende tabel. Daarnaast, zo zagen we in hoofdstuk 12 , zijn soms ook zonder tussenkomst van triggers DML -acties op views mogelijk (updatable views).

16.4.4 Executievolgorde van triggers Het is mogelijk meerdere triggers van hetzelfde type en bij dezelfde tabel te definiëren. Allereerst moeten we dan naamsconflicten vermijden, bijvoorbeeld door de namen te voorzien van een verduidelijkend achtervoegsel. Verder moeten we erop bedacht zijn dat het effect wel eens kan afhangen van de executievolgorde (de volgorde waarin de triggers worden uitgevoerd). Die volgorde is: alfabetisch, tenzij in de ‘kop’ van de definitie een volgnummer is gespecificeerd met een position -clausule. Zie de Firebird-documentatie (Language reference) voor de details en nog heel veel meer informatie over stored procedures, triggers en exceptions. Applicatieconstraints DDL-constraints en triggers worden opgeslagen in de database en geactiveerd door het rdbms. Ze zijn actief aan de serverkant. In een moderne applicatie met grafische gebruikersinterface zijn vrijwel altijd ook regels actief aan de clientkant. Deze zijn vastgelegd in de applicatiecode (veelal horend bij een specifiek venster) en reageren op specifieke gebruikersacties. Ze kunnen een beperkend effect hebben (bijvoorbeeld alleen hoofdletters toestaan) of een gedragseffect (bijvoorbeeld een automatische berekening uitvoeren). Gedragseffecten kunnen zich beperken tot het applicatievenster zelf of een commando richting rdbms inhouden. Dit soort regels worden applicatieconstraints genoemd. Het programmeren van applicatievensters, dus ook van applicatieconstraints, is geen databaseonderwerp. Bij ontwerp en implementatie van een database kan men zich echter niet aan de vraag onttrekken welke regels aan de serverkant moeten worden bewaakt en welke regels aan de clientkant. Deze load balancing vormt vaak zelfs een belangrijk en soms lastig ontwerpprobleem.

Oefenopgaven De volgende opgaven hebben betrekking op de voorbeelddatabase OrderdatabaseC (de versie met constraintnamen, zonder domeinen). Ze hebben betrekking op een klein aantal van de vele mogelijke of in de praktijk zelfs noodzakelijke regels (beperkingsregels dan wel gedragsregels). Voor de diagrammen: zie de Boekverkenner. De voorbeeldpopulatie vindt u ook in bijlage 4.

Opgave 16.13 We willen een regel implementeren die het onmogelijk maakt om aan de verzameling orderregels van een order nog iets te veranderen na de orderdatum. a Creëer een exception, met als foutmelding ‘Datum verstreken; niet mogelijk nog wijzigingen aan te brengen’. b Creëer één multi-event-trigger die, samen met de exception uit onderdeel a, de regel afdwingt. (U kunt hierbij de strategie hanteren om eerst single-event-triggers te schrijven en uit te testen.) Maak gebruik van de systeemvariabele current_date voor de huidige datum. c Via welke omweg kan de regel toch nog worden overtreden? Hoe kan die omweg worden afgesneden?

Opgave 16.14 a Schrijf en test een stored procedure pUpdateTotaalbedragOrder voor het berekenen van het totaalbedrag van een order waarvan het ordernummer aan de procedure wordt meegegeven. b Indien we ervan uitgaan dat het totaalbedrag van een order op elk moment moet overeenstemmen met de orderregelbedragen, bij welke events moet het totaalbedrag dan worden herberekend?

Opgave 16.15 a Creëer een trigger die de artikelvoorraad bijwerkt na het invoeren van een nieuwe orderregel (zonder controle op toereikende voorraad). b Vervang de trigger (met een create or alter trigger-commando) door een andere die (na een controle vooraf en in combinatie met een exception) de voorraad alleen bijwerkt indien deze toereikend is, en die anders het invoeren van de orderregel tegenhoudt en een foutmelding geeft.

Opgave 16.16 Een bekende triggertoepassing is het automatisch bijhouden van een ‘log van wijzigingen’. Dit is een tabel (een soort logboek) waarin wordt bijgehouden welke gebruikers veranderingen in de database hebben uitgevoerd of pogen uit te voeren, met de aard van de verandering en het tijdstip. De opdracht luidt: realiseer dit voor OrderdatabaseC. Creëer vooraf de logboektabel: create table LogOrderdatabase (gebruiker     varchar(20)   not null, datumTijd     timestamp     not null, tabel       varchar(15)   not null, actie         char(1)       not null )

Toelichting – De kolom datumTijd is voor de systeemdatum/tijd waarop de actie heeft plaatsgevonden. – De actie is een insert, delete of update, in LogOrderdatabaseC op te nemen als ‘I’, ‘D’ en ‘U’. Aanwijzingen – De naam van de ingelogde gebruiker is beschikbaar als waarde van de systeemvariabele user. – Om de hoeveelheid werk binnen de perken te houden, kan worden volstaan met het laten loggen van daadwerkelijk aangebrachte veranderingen op tabel Artikel.

17 De data dictionary DDL -statements zijn boodschappen aan het rdbms, dat de informatie

hierin opslaat in eigen ‘administratietabellen’: metatabellen . Deze vormen samen de data dictionary ofwel systeemcatalogus . De data dictionary bevat structuurinformatie over de database. DDL -statements ( create , alter , drop , grant en revoke ) geven aanleiding tot het toevoegen, wijzigen of verwijderen van rijen van de metatabellen. Metatabellen zijn gewone tabellen. Hun structuur wordt dan ook beschreven in de data dictionary zelf, op dezelfde wijze als de structuur van de gebruikerstabellen. Dit geeft aanleiding tot boeiende experimenten, waarbij we naar hartelust gaan ‘knoeien’ in de data dictionary. Maar wees niet beducht: het ergste wat kan gebeuren, is dat

uw systeem vastloopt en een database opnieuw moet worden geïnstalleerd. Voorbeelddatabase In dit hoofdstuk hebben we een simpele, maar toch ‘rijke’ database nodig. We nemen hiervoor een vereenvoudigde versie van het Ruimtereisbureau: RuimtereisSimpel, een versie met domeinen. Zie figuur 17.1 voor het strokendiagram. Populaties van RuimtereisSimpel spelen in dit hoofdstuk geen rol. Des te meer de structuur, in paragraaf 17.1 kijken we naar de DDL -code.

Figuur 17.1 Strokendiagram RuimtereisSimpel

17.1 Metastructuren Databasediagrammen en DDL -code zijn structuurbeschrijvingen: ze beschrijven de structuur van tabellen, met de regels die ervoor gelden. Databasediagrammen doen dat (deels) grafisch, in DDL -code gebeurt dat in tekst. Deze paragraaf is gewijd aan de structuur van dergelijke structuren: metastructuren . Het vastleggen van zo’n metastructuur gebeurt ook weer met gegevens: metagegevens .

17.1.1 Het objectniveau We gaan uit van structuur en regels zoals die zijn opgesteld voor een eenvoudige versie van het Ruimtereisbureau, met databasenaam RuimtereisSimpel.fdb, zie figuur 17.1 . De structuur en de regels zoals die in dit soort figuren (en in eventuele aanvullende tekst) zijn vastgelegd, noemen we het objectniveau . Opmerking : het woorddeel ‘object’ in deze context staat los van ‘object’ in eerder gebruikte betekenissen. Objectstructuren en -regels in een fysieke database worden gerealiseerd via DDL -code. De code voor RuimtereisSimpel is als volgt. create create create create

domain domain domain domain

Reisnr     as Objectnaam as Geldbedrag as Volgnr     as

integer; varchar(10); numeric(5,2); integer;

create table Reis (nr                Reisnr        not null, vertrekdatum      date          not null, prijs             Geldbedrag, constraint pk_reis primary key (nr) ); create table Hemelobject (naam              Objectnaam    not null, moederobject      Objectnaam, constraint pk_hemelobject primary key (naam) ); alter table Hemelobject add constraint fk_satelliet_van_hemelobject foreign key (moederobject) references Hemelobject (naam) on update cascade; create table Bezoek (reis              Reisnr        not null, volgnr            Volgnr        not null, hemelobject       Objectnaam    not null, constraint pk_bezoek primary key (reis, volgnr), constraint fk_bezoek_tijdens_reis foreign key (reis) references Reis (nr) on delete cascade on update cascade, constraint fk_bezoek_aan_hemelobject foreign key (hemelobject) references Hemelobject (naam) on update cascade );

Opmerkingen

– Kolom Reis.vertrekdatum is als enige niet op een domein gebaseerd. Dit is met opzet gedaan, om het verschil te laten zien in de data dictionary.

– Er zijn ook omwegen mogelijk, via een sequentie van create table -, alter table - en/of drop table -statements. Voor de data dictionary telt het resultaat.

17.1.2 Het metaniveau De objectstructuren van RuimtereisSimpel verschillen volledig van de objectstructuren van bijvoorbeeld de databases Toetjesboek of OpenSchool. Maar de structuur van al die structuren komt overeen. We noemen die gemeenschappelijke structuur: de metastructuur . De rest van dit hoofdstuk is aan deze metastructuur gewijd. In een relationele omgeving ligt het voor de hand ook metastructuren relationeel vast te leggen. Het komt er dan op aan de informatie van figuu r 17.1 te ‘vangen’ in relationele tabellen. We noemen die tabellen: metatabellen . Zo is er een metatabel met gegevens over alle tabellen van de database, inclusief de metatabellen zelf. Alle metatabellen bij elkaar vormen de data dictionary . Een andere veelgebruikte term is systeemcatalogus (met systeemtabellen ). Het ontwerp van een data dictionary ligt – afgezien van het feit dat het altijd relationeel is – niet voor 100% vast: het hangt o.a. af van de database-user-structuur die een rdbms-fabrikant kiest. Toch komen deze ontwerpen in de kern allemaal overeen. En dat is niet verwonderlijk: ze zijn immers bedoeld om informatie over dezelfde ‘soort dingen’ op te slaan: gebruikers, tabellen, kolommen, constraints, enzovoort.

17.2 Meta-informatie over tabellen De eerder genoemde dictionarytabel met gegevens over alle tabellen van de database, inclusief de metatabellen zelf – wordt de eerste tabel van de data dictionary. Firebird noemt deze tabel Rdb$relations, zie figuur 17.2 . Deze naam refereert aan ‘relations’, de meer wiskundige aanduiding voor tabellen. In figuur 17.2 is ook de metatabel voor gebruikers Rdb$users opgenomen, uit de systeemdatabase Security2.fdb (zie paragraaf 12.1.2 ).

Tabel- en kolomnamen in hoofdletters In Firebird begint de naam van elke metatabel met RDB$. Dat geldt ook voor hun kolommen. Verder worden intern tabel- en kolomnamen opgeslagen in hoofdletters. Wanneer we rechtstreeks lezen of schrijven in de data dictionary, moeten we die namen opgeven in hoofdletters. Om die reden schrijven we in dit hoofdstuk de namen van metatabellen en hun kolommen in alle figuren met hoofdletters. In code en tekst houden we onze huisstijl aan.

Figuur 17.2 Multipliciteitendiagram voor de metatabellen voor tabellen en gebruikers

Figuur 17.3 geeft een vereenvoudigd populatiediagram. Rdb$relations heeft in werkelijkheid veel meer kolommen, evenals Rdb$users (met onder meer een kolom met het – versleutelde – wachtwoord).

Figuur 17.3 Metatabellen voor tabellen en gebruikers (eenvoudige versie)

Figuur 17.3 bevat uniciteitspijlen: in de metatabel Rdb$users moet de gebruikersnaam uniek zijn, evenals de tabelnaam in de tabellentabel Rdb$relations. Deze worden afgedwongen via unieke indexen, zoals we in opgave 17.2 zullen zien. Voor de metatabellen zelf worden geen constraints gedefinieerd: – Expliciete primary key -constraints zijn niet echt nodig, omdat de unieke indexen de benodigde functionaliteit leveren: de combinatie ‘not null’ en uniciteit. – Expliciete foreign key -constraints zijn ook niet nodig, omdat wijzigingen in de data dictionary (in reactie op DDL -commando’s) worden aangebracht onder programmabesturing; de programmatuur bewaakt de referentiële integriteit. In figuur 17.3 hebben we daarom wel uniciteitspijlen, maar geen primaire sleutels (‘p’-tjes) getekend. De verwijzingspijlen duiden als altijd referentiële integriteit aan, al wordt deze dus niet via foreign key constraints afgedwongen.

Merk op dat Rdb$relations behalve rijen voor gebruikerstabellen ook een rij voor zichzelf bevat. Hieruit mag terecht geconcludeerd worden dat een metatabel niet tot een aparte database behoort. Gebruikerstabellen en metatabellen van ons versimpelde ruimtereisbureau vormen samen een en dezelfde database: RuimtereisSimpel.fdb. Alleen de tabellen die in dit hoofdstuk aan bod komen zijn weergegeven in figuur 17.3 . Merk op dat alle tabellen eigendom zijn van gebruiker RuimtereisSimpel. Logisch, want die heeft ze gecreeerd. Metatabel voor database zelf Een Firebird-database ‘kent’ zijn tabellen door onder meer de metatabel Rdb$relations. Maar kent een database ook zichzelf? We mogen dit wel verwachten. Er zou dus een metatabel moeten zijn voor ‘het specifieke soort ding database’. Met precies één rij, voor zichzelf! En inderdaad, die tabel bestaat: Rdb$database. De tabelnaam (een enkelvoud, terwijl Firebird verder altijd meervouden gebruikt) geeft al aan dat deze tabel altijd één rij bevat. In opgave 17.5 inspecteren we de inhoud. Doordat deze tabel één rij bevat, is hij geschikt als ‘kladbloktabel’, zodat een speciale kladbloktabel (zoals Dummy in Oracle) niet nodig is. Gevaarlijk spel? Door rechtstreeks in de data dictionary te ‘schrijven’ (met insert -, delete - of update -statements), kunt u dingen doen die via normale DDL -code zijn uitgesloten. Bijvoorbeeld: een tabel aanmaken zonder kolommen. Of: een tabel verwijderen, maar zijn kolommen laten staan. Zie hiervoor de volgende opgaven. Het is dus een kleine moeite om ongebruikelijke structuren te creëren of om een bestaande database grondig om zeep te helpen. Door gericht te experimenten, kunt u van dit ‘gevaarlijke spel’ een hoop leren. We raden u dan ook aan om er lustig op los te experimenteren. Als de zaak vastloopt, is het meestal voldoende het slachtoffer te verwijderen en opnieuw te installeren. In het ergste geval loopt uw systeem vast en moet u opnieuw opstarten. Door directe ingrepen in de data dictionary kunt u ‘vreemde toestanden’ veroorzaken. De volgende opgaven illustreren dit in de data dictionary van RuimtereisSimpel.fdb. Zolang u werkt onder het impliciete transactiemodel (zie paragraaf 10.2.2 ) en geen DDL-commando geeft, kunt u de experimenten in de volgende opgaven via rollback weer ongedaan maken.

Opgave 17.1 Probeer de volgende (veilige) acties uit en let op de foutmeldingen: a de hele tabel Rdb$relations verwijderen via drop table b alle rijen verwijderen van Rdb$relations via delete

c in Rdb$relations alleen de rij van Rdb$relations zelf verwijderen.

Opgave 17.2 Maak een tabel zonder kolommen: a Voeg een rij toe aan Rdb$relations voor een tabel Test, zonder een gebruiker op te geven. Opmerking : rdb$owner_name blijkt als defaultwaarde de naam van de huidige gebruiker te krijgen. b Controleer via een select op Rdb$relations of het gelukt is. Als dat zo is, bevat de data dictionary nu een tabel zonder kolommen. U hebt er niets aan, want u kunt geen geldig insert -commando op die tabel geven. c Herhaal het insert -commando uit onderdeel a en stel vast dat een unieke index op Rdb$relations uitvoering verhindert.

Opgave 17.3 Bij tabelcreatie via create table Test (...) wordt de tabelnaam geconverteerd naar hoofdletters ( TEST ) voordat hij in de data dictionary wordt opgeslagen. In de vorige opgave hebt u via een directe insert de naam waarschijnlijk als ‘Test’ ingevoerd. Voer nogmaals hetzelfde insert -commando uit, maar nu met ‘ TEST ’. Constateer dat dit wordt geaccepteerd! Conclusie : intern is ‘Test’ iets anders dan ‘ TEST ’. Door zelf in de data dictionary te schrijven, passeert u de omzetting in hoofdletters die in een normale situatie op DDL -code wordt uitgevoerd. Schroef hierna alle wijzigingen terug met rollback . Mocht er toch iets onherstelbaars zijn gebeurd, installeer RuimtereisSimpel.fdb dan opnieuw.

Opgave 17.4 Inspecteer de inhoud van de metatabel Rdb$database. a Waartoe dient de kolom rdb$relation_id? b En de kolom rdb$character_set_name?

c Waarom is er geen kolom voor de bestandsnaam?

17.3 Meta-informatie over kolommen en domeinen In deze paragraaf bestuderen we twee nieuwe metatabellen, die betrekking hebben op kolommen en domeinen: Rdb$relation_fields en Rdb$fields. Rdb$relation_fields bevat een aantal lokale kolomkenmerken , zoals de kolomnaam en een volgnummer voor de kolompositie. Met ‘lokaal’ wordt bedoeld dat deze kenmerken tabelgebonden zijn en niet gedeeld kunnen worden met andere kolommen via een domeindefinitie. In Rdb$fields zijn de niet-lokale kolomkenmerken ondergebracht. Dit zijn – per definitie – de kenmerken die ook domeinen bezitten. Het is daardoor niet zo verbazend dat deze tabel ook de domeindefinities bevat. Sterker nog: voor elke kolom zonder DDL -domeindefinitie maakt Firebird een eigen privédomein in Rdb$fields. Zo bezien bevat Rdb$fields alleen domeinen.

17.3.1 Lokale kolomkenmerken Figuur 17.4 geeft het multipliciteitendiagram voor Rdb$relations en Rdb$relation_fields. Elke kolom behoort tot één tabel en een tabel heeft een of meer kolommen. In de voorgaande opgaven is duidelijk geworden dat het niet zo moeilijk is een toestand te creëren met een tabel zonder kolom of een kolom zonder tabel. Als u alles doet via brave DDL statements zoals create table , is dat onmogelijk en kunt u ervan op aan dat aan de multipliciteiten van figuur 17.4 is voldaan.

Figuur 17.4 Tabellen en lokale kolomkenmerken: multipliciteiten

In figuur 17.5 ziet u vier kolommen van Rdb$relation_fields en een deel van de rijenpopulatie. Toelichting – Een kolom wordt uniek bepaald door de tabel en de kolomnaam, vandaar de uniciteitsregel over de eerste en tweede kolom. – Een derde kolom geeft de standaardpositie van de kolom, zoals die bijvoorbeeld wordt gebruikt bij een select * from … – De vierde kolom bevat een systeemvlag : 1 = kolom van metatabel, 0 = kolom van gebruikerstabel. Ook Rdb$relations heeft zo’n systeemvlag, die we eveneens hebben opgenomen. Systeemvlaggen zijn prettig wanneer we query’s willen uitvoeren op alleen metatabellen of metakolommen (beginnend met Rdb$ of rdb$) of juist alleen op gebruikerstabellen en -kolommen. Opmerking : een logische waarde, zoals ja/nee of waar/onwaar, wordt vaak een vlag genoemd.

Figuur 17.5 Tabellen en lokale kolomkenmerken: populatie

17.3.2 Niet-lokale kolomkenmerken en domeinen Een verwijssleutelkolom en de corresponderende primaire-sleutelkolom delen kenmerken zoals datatype en lengte. In paragraaf 11.7 is uiteengezet dat een domeindefinitie ervoor kan zorgen dat deze kenmerken op één plaats en vrij van redundantie worden vastgelegd. De kenmerken die een kolom kan ontlenen aan een domein, heten niet-lokale kenmerken . De tabel Rdb$fields bevat alle domeinen die via DDL -code zijn gedefinieerd. Daarnaast maakt Firebird voor elke kolom zónder ‘ DDL domein’ in Rdb$fields een eigen domein aan voor de niet-lokale kolomkenmerken. Zodoende hoort bij elke kolom precies één domein in Rdb$fields, zie figuur 17.6 .

De populatie in figuur 17.7 illustreert dit. In het create-script van RuimtereisSimpel zijn twee domeinen gedefinieerd: Reisnr en Objectnaam. Op Reisnr zijn twee kolommen gebaseerd, op Objectnaam drie. Beide domeinnamen zijn terug te vinden in de identificerende kolom rdb$field_name van Rdb$fields. De andere kolommen van de gebruikerstabellen hebben een eigen domein gekregen. De naam hiervan is door het systeem gegenereerd, met het formaat Rdb$ nn (waarin nn een volgnummer is). Vanuit Rdb$relation_fields wordt via de domeinnamen naar Rdb$fields verwezen.

Figuur 17.6 Domeinen en niet-lokale kolomkenmerken: multipliciteiten

Merk op dat ook de metakolommen op domeinen zijn gebaseerd. Zichtbaar in figuur 17.7 : acht metakolommen gebaseerd op vijf domeinen. In de kolom rdb$field_type wordt, via codes, het datatype van een domein vastgelegd. Merk op dat dit het interne datatype is (zie paragraaf 11.5 ). Bijvoorbeeld: waarden van het domein Reisnr, gespecificeerd als numeric (4) , worden opgeslagen als een integer (code 8).

Figuur 17.7 Domeinen en niet-lokale kolomkenmerken: populatie

Opgave 17.5 Stel, we hadden Reis.vertrekdatum gebaseerd op een domein Datum, met datatype date . Welke verschillen zouden er zijn in Rdb$relation_fields en Rdb$fields ten opzichte van figuur 17.7 ? Opgave 17.6 Geef een select -statement voor een overzicht met tabelnaam, kolomnaam en kolompositie van alle gebruikerskolommen. Opgave 17.7 Creëer een tabel zonder DDL te gebruiken, door insert -statements in de data dictionary: – tabelnaam: Test – kolommen: kolom1 (datatype date ) en kolom2 (datatype char (1) )

– geen constraints. Kijk, als het gelukt is, of u een rij aan Test kunt toevoegen. Aanwijzingen – Gebruik een zo kort mogelijke values-lijst door alleen de metakolommen uit de voorgaande figuren op te nemen. De overige kolommen krijgen dan nulls. – Schrijf, zoals Firebird dat zelf ook doet, alle namen in hoofdletters. – Vergeet de insert ’s in de data dictionary niet te committen, vóór u een insert op Test doet.

17.4 Overige dictionarytabellen In deze paragraaf noemen we kort nog een paar andere interessante metatabellen. In bijlage 2 vindt u een uitgebreidere beschrijving. In de Firebird 6.0 Language Reference (in te zien via de Boekverkenner) kunt u alle dictionarytabellen opzoeken. Metatabellen voor indexen Informatie over eventuele indexen die voor een database zijn aangemaakt wordt opgeslagen in de metatabellen Rdb$indices en Rdb$index_segments. De tabel Rdb$indices bevat de enkelvoudige kenmerken van indexen, waaronder de door Firebird gegenereerde naam van de index, de tabel waar de index betrekking op heeft en een vlag die aangeeft of de index uniciteit afdwingt. In de tabel Rdb$index_segments wordt de veel-veel-associatie tussen indexen en kolommen vastgelegd: een index is op een of meer kolommen van die tabel gedefinieerd; omgekeerd kan een kolom tot meerdere indexen behoren. Metatabellen voor constraints De eigenschappen van constraints worden bewaard in de metatabel Rdb$relation_constraints. Een constraint hoort bij een tabel en Rdb$relation_constraints bevat dan ook een verwijzing naar Rdb$relations.

Bij primary key -, unique - en foreign key -constraints wordt door Firebird standaard een index aangemaakt op de betreffende kolom(men). Metatabellen voor autorisatie De metatabel voor gebruikers, Rdb$users, kennen we al. Deze maakt deel uit van de security database Security2.dbf. Er is natuurlijk ook een metatabel voor rollen: Rdb$roles, waarin de naam van de rol en de eigenaar van de rol (de gebruiker die de rol gecreëerd heeft) worden bewaard. De metatabel Rdb$user_privileges bevat alle informatie over de toegekende privileges, voortvloeiend uit gegeven grant -commando’s: – privileges toegekend aan gebruikers – privileges toegekend aan rollen – rollen toegekend aan gebruikers. Overige metatabellen Er zijn nog vele andere metatabellen, bijvoorbeeld tabellen met metainformatie over door de programmeur gedefinieerde functies (user defined functions, udf’s) of over triggers. Ook is er een metatabel over sequences, waarmee automatisch volgnummers worden gegenereerd. De volgende opgaven geven een paar voorbeelden van wat we via de dictionarytabellen zoal te weten kunnen komen.

Opgave 17.8 Geef een select -commando dat alle recursieve foreign key -constraints geeft. Aanwijzing : gebruik metatabel Rdb$ref_constraints (zie bijlage 2). Opgave 17.9 Achterhaal het laatste, automatisch gegenereerde nummer dat is gebruikt voor een indexnaam. Aanwijzing : raadpleeg eerst tabel Rdb$generators (zie bijlage 2) om de naam van de sequence te achterhalen die de indexvolgnummers bijhoudt. Vraag vervolgens aan die sequence wat zijn laatst uitgedeelde nummer is (zie paragraaf 11.9 ).

17.5 Metaviews De data dictionary van Firebird heeft een erg inzichtelijke tabelstructuur. Als eigenaar-gebruiker hebt u bovendien ruime mogelijkheden om de

inhoud te manipuleren. Toch kan er reden zijn om de metatabellen niet rechtstreeks te benaderen, maar via views.

17.5.1 Waarom metaviews? In veel rdbms’en heeft een gewone gebruiker niet de mogelijkheid om metatabellen rechtstreeks te wijzigen, zeker niet wanneer de database (zoals bij Oracle) door vele gebruikers wordt gedeeld. Toegang tot de metatabellen wordt dan voornamelijk verkregen via views: metaviews of systeemviews genaamd. Veel daarvan bevatten een selectieconditie die het select -resultaat beperkt tot objecten waarvan de gebruiker zelf eigenaar is, of waarop deze rechten heeft ontvangen. Een voorbeeld van zo’n metaview is de view Users, die is gebaseerd op de metatabel Rdb$users (zie het create-script van Security2.fdb). In Firebird is elke database eigendom van één gebruiker. Views die filteren per gebruiker, zijn daardoor minder noodzakelijk. Toch kunnen er redenen zijn om metaviews aan te maken:

– Gemak: een select * from ... voor een view (met precies de juiste kolommen) is sneller gegeven dan een ingewikkelde query met een hoop rdb$-kolomnamen. – Een gebruiker die geen eigenaar is, maar wel al privileges heeft op gebruikerstabellen, heeft soms behoefte aan structuurinformatie. We kunnen die geven via views, op een veilige en gebruikersvriendelijke manier. – De ANSI/ISO -normalisatiecommissie kan onmogelijk normen opstellen voor de metatabellen. De structuur daarvan is te zeer verbonden met fabrikantspecifieke techniek. Wel wordt ernaar gestreefd dat fabrikanten zich conformeren aan een standaard voor meta views . In de SQL2 -standaard is een aantal metaviews opgenomen, waarvan er in de Firebird-documentatie vier zijn overgenomen. Een ervan behandelen we, als voorbeeld, in de volgende paragraaf.

17.5.2 ANSI / ISO-metaviews Als voorbeeld van een ANSI/ISO -metaview geven we in tabel 17.1 de specificatie van de Constraints_column_usage-view, die alle kolomconstraint-combinaties geeft voor primary key -, unique - of foreign key -constraints. Tabel 17.1 De Constraints_column_usage-metaview

De code waarmee u deze view in Firebird kunt creëren, is als volgt: create view Constraints_column_usage (table_name, column_name, constraint_name) as select rdb$relation_name, rdb$field_name, rdb$constraint_name from   Rdb$relation_constraints R_C, Rdb$index_segments I_S where  R_C.rdb$index_name = I_S.rdb$index_name

Hierna krijgt u via een simpele query: select * from   Constraints_column_usage

als resultaat: TABLE_NAME  COLUMN_NAME  CONSTRAINT_NAME =========== ============ ============================ REIS        NR           PK_REIS HEMELOBJECT NAAM         PK_HEMELOBJECT HEMELOBJECT MOEDEROBJECT FK_SATELLIET_VAN_HEMELOBJECT BEZOEK      VOLGNR       PK_BEZOEK BEZOEK      REIS         PK_BEZOEK BEZOEK      REIS         FK_BEZOEK_TIJDENS_REIS BEZOEK      HEMELOBJECT  FK_BEZOEK_AAN_HEMELOBJECT

Opgave 17.10 Maak een metaview vRelations bij tabel Rdb$relations, die deze beperkt tot de in dit hoofdstuk genoemde kolommen. Neem als kolomnamen de oorspronkelijke kolomnamen, maar zonder de prefix rdb$ .

17.6 Van tabel ‘Tabel’ naar volledige database We beëindigen dit hoofdstuk met een mooi aspect van relationele databases: het zelfbeschrijvende karakter.

17.6.1 Zelfbeschrijvende deelsystemen De data dictionary is zelfbeschrijvend , in die zin dat zijn eigen tabellen, kolommen, indexen, enzovoort er op dezelfde wijze in worden vastgelegd als de gebruikerstabellen met hun kolommen, enzovoort. De kern van de data dictionary wordt gevormd door de tabel Rdb$relations. Volgens onze eigen naamgeving zou deze tabel ‘Tabel’ heten. We kunnen de hele database in gedachten opbouwen beginnend met alleen deze tabel, met informatie over één ‘soort ding’, namelijk tabellen. In eerste instantie heeft die ene tabel maar één kenmerk, de tabelnaam. Dus heeft Rdb$relations voorlopig maar één kolom, voor de tabelnaam. In de data dictionary heet deze rdb$relation_name. Aangezien Rdb$relations in dit stadium van opbouw van de database de enige tabel is, is er ook maar één rij, zie figuur 17.8 .

Figuur 17.8 De tabel ‘Tabel’

Nu hebben we echter naast ‘tabel’ ook ‘kolom’ als structuurelement. Willen we ook de kolom rdb$relation_name in de database zelf opnemen, dan hebben we ook een metatabel voor kolommen nodig: een tabel ‘Kolom’. Deze kennen we al uit de data dictionary onder de naam Rdb$relation_fields. Dit leidt tot de kleine zelfbeschrijvende database van f iguur 17.9 .

Figuur 17.9 Zelfbeschrijvende database met tabellen ‘Tabel’ en ‘Kolom’

Ga na dat inhoud en structuur precies in overeenstemming zijn: het plaatje toont twee tabellen en drie kolommen, en inderdaad heeft de tabel ‘Tabel’ twee rijen en heeft de tabel ‘Kolom’ drie rijen. Aan deze kern kunnen nu gebruikerstabellen worden toegevoegd. Daardoor wordt het aantal tabellen niet alleen groter, maar ook het aantal rijen in Rdb$relations en Rdb$relation_fields. Het toevoegen van nieuwe structuurelementen, zoals sleutels, leidt tot nieuwe metatabellen. Al met al kan een relationele database worden gezien als een uitbreiding van zijn eigen data dictionary. Verder beschrijft hij volledig zijn eigen structuur: we spreken van een zelfbeschrijvende database .

17.6.2 Een meta-metaniveau? Dictionarytabellen horen tot een metaniveau . Ze bevatten immers informatie óver tabellen, onder meer over zichzelf. Stel dat iemand zegt: “Hier heb ik een leuke gebruikersdatabase. De ‘soorten van dingen’ waarover hij informatie bevat, zijn: gebruikers, tabellen, kolommen, indexen, constraints, enzovoort.” Die soorten van dingen verschillen dan niet wezenlijk van hemelobjecten en ruimtereizen! Men zou er dus een gebruikersdatabase van kunnen maken. In het voorgaande hebben we gezien dat dit niet meer hoeft: de Ruimtereisbureau-database bevat informatie over soorten van dingen zoals hemelobjecten, maar ook over gebruikers, tabellen, kolommen, enzovoort. Aangezien het metaniveau dus ook zichzelf beschrijft, is er geen apart meta-metaniveau.

Oefenopgaven Opgave 17.11 a Het IQU -commando describe toont een opsomming van de namen van niet-metatabellen. Met welk select -statement op de data dictionary is describe equivalent? b Het IQU -commando describe toont kolominformatie over de genoemde tabel. Geef ook hiervoor een equivalent select statement. Voldoende is de kolomnamen te tonen.

Opgave 17.12 a Ga na dat de uitvoer van het volgende select -statement een ‘dropscript’ is voor alle gebruikerstabellen: select 'drop table ' || trim(rdb$relation_name) || ';' from   Rdb$relations where  rdb$system_flag = 0 and rdb$view_blr is null

Toelichting : de kolom rdb$view_blr is een van de kolommen in Rdb$relations die null is voor tabellen en not null voor views. Opmerking : het drop -script zal, vanwege referentiële integriteit, pas werken als eerst de verwijssleutels zijn verwijderd.

b Pas het script zodanig aan dat het een drop -script wordt voor alle gebruikersviews.

Opgave 17.13 Veel andere rdbms’en (waaronder Oracle) hebben een database-userstructuur waarbij gebruikers onderdeel zijn van dezelfde database als de door hen gedefinieerde databaseobjecten. Verschillende gebruikers mogen objecten, zoals tabellen, definiëren met dezelfde naam. In situaties waarin verwarring mogelijk is, wordt de naam van het object voorafgegaan door de gebruikersnaam met een punt. Wanneer bijvoorbeeld twee gebruikers, Sofie en Lisa, beiden een tabel Test hebben gedefinieerd, heten die tabellen voluit: Sofie.Test en Lisa.Test. Ontwerp voor dergelijke rdbms’en een stukje van de data dictionary: een strokendiagram met uniciteitsregels, dat betrekking heeft op gebruikers,

tabellen en kolommen. Kies zelf namen voor de metatabellen en hun kolommen. Zorg ook voor een kleine, zelfbeschrijvende voorbeeldpopulatie, met daarin gegevens van minstens twee gewone gebruikers en van door hen gecreëerde tabellen. Ga uit van één ‘supergebruiker’ die eigenaar is van de metatabellen.

Opgave 17.14 Uitgangspunten van deze opgave: – een gebruikersmodel waarbij een tabel volledig wordt geïdentificeerd door de tabelnaam – een data dictionary met onder meer de tabellen van het strokendiagram van figuur 17.10 – namen van tabellen en kolommen en van andere databaseobjecten worden intern in hoofdletters opgeslagen.

Figuur 17.10 Strokendiagram

Toelichting op tabel Trigger: – de triggernaam is de identificerende naam van de trigger – de tabelnaam is de naam van de tabel waarvoor de trigger is gecreëerd

– het eventtijdstip is B of A (voor ‘before’ of ‘after’) – het eventtype is I, D of U (voor ‘insert’, ‘delete’ of ‘update’) – de body is de triggercode vanaf begin tot en met end . Verder zijn gegeven: – één gebruikerstabel, genaamd Test, met één kolom genaamd testkolom – één trigger, waarvan hier de (onvolledige) code volgt: create trigger tTest_bi for Test before insert as begin … end

Opdracht : vul het strokendiagram van de figuur aan met een populatie, alleen rekening houdend met de tabellen Tabel, Kolom, Trigger en Test en met de trigger tTest_bi .

Bijlage 1

Firebird: functies en contextvariabelen

De belangrijkste Firebird-functies vallen in de volgende groepen uiteen: – stringfuncties – rekenkundige functies – wiskundige functies – datumfuncties – conditionele functies – typeconversiefunctie – null -vervangfunctie – datumconversiefuncties ( IQU , user defined). Naast de functies zijn er de parameterloze contextvariabelen , die hun waarde krijgen van het rdbms. Hieronder volgt een (niet volledig) overzicht. Raadpleeg de Firebirddocumentatie (Firebird-2.5-LangRef-Update.pdf) voor details over de functieargumenten (met komma’s en/of keywords), de datatypen van de geretourneerde waarden en voorbeelden.

Op de plaats van … kunnen optionele argumenten en eventuele keywords staan.

Stringfuncties

Rekenkundige functies

Wiskundige functies

Datumfuncties

Alle tijdseenheden zijn numeriek: year (1-9999), month (1-12), week (1-53), day (1-31), weekday (0-6, 0 = Sunday), yearday (0-365, 0 = 1 January), hour (0-23), minute (0-59), second (0.0000-59.9999), millisecond (0.0-999.9).

Conditionele functies

Typeconversiefunctie Null-vervangfunctie

Contextvariabelen

Stringconstante ‘now’ Naast current_date , current_time en current_timestamp is er nog de stringconstante 'now' . Indien deze gecast wordt naar een date , time of timestamp , geeft deze de huidige datum, tijd of datum/tijd. Bijvoorbeeld: cast('now' as timestamp) . Anders dan current_date geeft deze in een routine (trigger of procedure) altijd de exacte servertijd op het moment van aanroep. Dit maakt 'now' geschikt voor tijdmetingen van onderdelen van een routineaanroep. Voor andere toepassingen zijn current_date , enzovoort te prefereren.

Datumconversiefuncties - user defined in IQU

Datumuitvoer: conversie van date, time of timestamp naar string Voor datum/tijduitvoer is conversie nodig van de interne date -, time - of timestamp -waarde naar een stringtype. In veel gevallen gebeurt dit automatisch, soms is expliciete typecasting noodzakelijk. Voorbeeld Automatische conversie select current_timestamp from   Rdb$database

Resultaat (op 6 maart 2012, ’s avonds tegen achten): ==================== 06-mrt-2012 19:54:10

De waarde van current_timestamp is door IQU geconverteerd naar een string in een Nederlandstalig formaat (vandaar ‘mrt’ en geen ‘mar’), onder afronding op hele seconden.

Voor een andere volgorde of naamgeving is expliciete typecasting nodig. Dat kan met to_char ; zie de volgende tabel voor de codes die zijn toegestaan in de formaatstring. Het voorbeeld betreft de datum-met-tijd 9 januari 2015, 08:07:06 uur .

Een formaatstring mag een willekeurige combinatie van codes of andere tekens bevatten. Tekens die geen deel uitmaken van een code, worden letterlijk afgedrukt. Voorbeeld Conversie met to_char select 'Het is nu ' || to_char(current_timestamp, ‘dd-mmmm-yyyy hh:nn’) from   Rdb$database

Resultaat (op 26 januari 2012, even na 17 uur 03): CONCATENATION ================================ Het is nu 26-januari-2012 17:03

Datuminvoer: conversie van string naar date, time of timestamp De enige externe representatie van date -, time - en timestamp waarden is als string. Dat geldt dus ook indien ze onderdeel zijn van een SQL -statement. Er zal dus altijd conversie plaatsvinden naar de interne waarde. Dit kan expliciet of automatisch. Voorbeeld Automatische conversie Een voorbeeld van automatische conversie: insert into Klant values (130, 'Bill', '15-jun-1987')

Als derde element in de waardenlijst wordt een date -waarde verwacht. Intern is deze formaatloos, maar ‘noodgedwongen’ moet deze als string worden ingevoerd. Om dubbelzinnigheid te voorkomen (denk aan 05-12-2008: is dat 5 december of 2 mei?), zijn we gebonden aan een van de (Amerikaanse) standaard datum/tijdformaten . Firebird herkent onder meer:

Wat niet wordt gespecificeerd, krijgt de waarde 0. Met andere scheidingssymbolen (/ of spatie) gaat het ook goed. In dit boek en de scripts gebruiken we meestal de Europese weergave. Voorbeeld Expliciete conversie De automatische conversie van het vorige voorbeeld expliciet gemaakt: insert into Klant values (130, 'Bill', to_date('15-jun-1987'))

De string '15-jun-1987' wordt expliciet geconverteerd naar een date waarde, met de conversiefunctie to_date . Omdat verwerking verloopt via een udf, wordt hier een Nederlandse maandafkorting verwacht. Voorbeeld Verplichte expliciete conversie In dit voorbeeld is, vanwege de datumberekening, expliciete conversie verplicht: select to_date('31-dec-1999') + 10000 from   Rdb$database

Resultaat: de tienduizendste dag van dit millennium.

Bijlage 2

Firebird: data dictionary

Deze bijlage bevat een overzicht van de belangrijkste (meta-)tabellen van de data dictionary van Firebird, met een deel van hun kolommen. Raadpleeg voor meer informatie de Firebird-documentatie. Zie ook hoofds tuk 16 ‘De data dictionary’. We gebruiken dezelfde voorbeelddatabase als in hoofdstuk 16 : RuimtereisSimpel. In figuur 1 geven we nogmaals het strokendiagram. Voor de data dictionary is de voorbeeldpopulatie niet relevant. De voorbeeldpopulatie van de data dictionary wordt gevormd door de metagegevens van RuimtereisSimpel!

Figuur 1 Structuur van RuimtereisSimpel

De database Metagegevens over de database zelf worden opgeslagen in tabel Rdb$database, zie figuur 2.

Figuur 2 Metatabel met gegevens over de database

Tabel Rdb$database bevat maar één rij: die over ‘de’ database. Over de inhoud is niet veel boeiends te zeggen. Interessanter is wat er niet in staat: de databasenaam. Deze wordt uitsluitend (single point of definition!) op het niveau van het besturingssysteem vastgelegd, als de fdb-naam.

Tabellen Metagegevens over tabellen worden opgeslagen in tabel Rdb$relations, zie figuur 3. Voor een toelichting: zie paragraaf 17.2 .

Figuur 3 Metatabellen voor tabellen en gebruikers

Kolommen Metagegevens over kolommen worden opgeslagen in tabel Rdb$relation_fields, zie figuur 4. Voor een toelichting: zie paragraaf 17.3.1 .

Figuur 4 Metatabellen voor tabellen en lokale kolomkenmerken

Domeinen Metagegevens over domeinen (met niet-lokale kolomkenmerken) worden opgeslagen in tabel Rdb$fields, zie figuur 5. Voor een toelichting: zie para graaf 17.3.2 .

Figuur 5 Metatabellen voor domeinen

Indexen Enkelvoudige kenmerken van indexen worden opgeslagen in de metatabel Rdb$indices, zie figuur 6.

Figuur 6 Metatabellen over indexen en hun kolommen

De tabel Rdb$index_segments bevat een verwijzing naar een index en – indirect – een verwijzing naar een kolom. De kolomnaam staat in Rdb$index_segments zelf, maar die is niet voldoende: ook de tabelnaam is nodig. Deze wordt via de indexnaam gevonden in Rdb$indices. Deze indirecte verwijzing is in figuur 5 aangegeven met stippellijnen. Als een index meerdere kolommen (‘segments’) heeft, is de volgorde van belang. Deze volgorde wordt bepaald door de kolom rdb$field_position.

Constraints De eigenschappen van constraints worden opgeslagen in metatabel Rdb$relation_constraints, zie figuur 7.

Figuur 7 Metatabellen over constraints

Een constraint hoort bij een tabel (een not null -constraint meer specifiek bij een kolom). Rdb$relation_constraints bevat dan ook een verwijzing naar Rdb$relations. Bij primary key -, unique -en foreign key -constraints wordt door Firebird standaard een index aangemaakt. Die index wijst naar dezelfde tabel als de constraint. Voor die typen constraints bestaan daardoor twee navigatiemogelijkheden vanuit Rdb$relation_constraints naar Rdb$relations: rechtstreeks en via Rdb$indices. Via beide wegen komen we bij dezelfde tabel uit. Informatie over verwijssleutels vinden we allereerst in de constraintstabel Rdb$relation_constraints. Daarnaast is er nog een tabel met specifieke informatie over verwijssleutelconstraints: Rdb$ref_constraints. Tussen beide tabellen bestaat een subtyperelatie ‘is een’, gebaseerd op gelijkheid van de rdb$constraintname-kolomwaarden. Opmerking : hoe het zit met not null -constraints laten we over aan de lezer.

Autorisatie Om illustratieve metagegevens te krijgen hebben we, voor database RuimtereisSimpel en als eigenaar daarvan, de volgende commando’s gegeven: grant select on Reis to Lisa with grant option; grant update(prijs) on Reis to Lisa; create role rPlanning; grant all on Hemelobject to rPlanning;

grant rPlanning to Lisa, Luc

Gebruikers en rollen De eigenschappen van rollen worden opgeslagen in metatabel Rdb$roles, zie figuur 8.

Figuur 8 Metatabel rollen

Een rol is gecreëerd door één gebruiker, die de eigenaar is van de rol. In dit geval hebben we één rol rPlanning, gecreëerd door RuimtereisSimpel. Privileges De eigenschappen van privileges worden opgeslagen in metatabel Rdb$user-privileges, zie figuur 9. De metatabel Rdb$user_privileges bevat alle informatie over de toegekende privileges, voortvloeiend uit gegeven grant -commando’s: – privileges toegekend aan gebruikers – privileges toegekend aan rollen – rollen toegekend aan gebruikers. Elke rij van Rdb$user_privileges bevat één elementaire privilegeverstrekking, die betrekking heeft op een gever (rdb$grantor: de ‘grantor’) en een ontvanger (rdb$user : de ‘grantee’). De waarde in kolom rdb$user wijst óf naar tabel Rdb$users óf naar tabel Rdb$roles. Omdat de kolom rdb$user zowel users als rollen kan bevatten, zijn de corresponderende lijnen gestippeld en vertakt. Opmerking : de data dictionary kent wel afhankelijkheden tussen kolommen, maar niet in de vorm van verwijssleutels.

Figuur 9 Metatabel privileges

– De kolom rdb$privilege geeft aan welk privilege wordt toegekend: – een S, I of D staat voor een select -, insert - of delete -privilege op een tabel of view – een U staat voor een update-privilege op een tabel, view, kolom van een tabel of kolom van een updatable view – een R staat voor een references -privilege op een tabel – een M staat voor een rol. Een toegekende rol wordt gecodeerd als ‘M-privilege’. Je zou kunnen zeggen dat de coderegel grant rPlanning to Lisa, Luc wordt verwerkt

alsof er stond: grant M on rPlanning to Lisa, Luc . Een rol wordt hier dus als een object gezien, waarop een privilege wordt verstrekt. Vandaar dat rPlanning in de kolom rdb$relation_name verschijnt en dat is weer de reden waarom de corresponderende verwijzingslijnen gestippeld zijn en vertakt. De waarde in kolom rdb$relation_name wijst óf naar tabel Rdb$roles óf naar tabel Rdb$relations. In dat laatste geval kan in rdb$field_name een specifieke kolom van de bedoelde tabel worden genoemd. Het all -privilege wordt niet als zodanig opgeslagen. In plaats daarvan worden in Rdb$user_privileges de onderliggende privileges opgeslagen. Een privilegeregel in Rdb$user_privileges kan als ‘grantee’ public hebben, die zelf geen gewone gebruiker is. RuimtereisSimpel is eigenaar van de database en is daarom gemachtigd als ‘grantor’ alle rechten op alle objecten te verstrekken, om te beginnen aan zichzelf. Van zijn rechten op Reis, Hemelobject en Bezoek zijn alleen die op Reis volledig weergegeven.

Sequences De tabel Rdb$generators bevat één rij voor elke automatische nummersequence, zie figuur 10. De naam herinnert nog aan de oude benaming van sequences: generators . Het aardige van deze tabel is dat we er aan kunnen zien dat Firebird ook zelf de nodige volgnummersequences maakt. Welke dat zijn, hebben we gedeeltelijk al kunnen zien in dit hoofdstuk: er zijn allerlei systeemobjecten met namen waarin een automatisch gegenereerd nummer voorkomt. Bijvoorbeeld de automatisch gegenereerde indexen. Welke sequences door het systeem zijn gegenereerd en welke via DDL code ( create sequence ), is te zien aan de kolom rdb$system_flag.

Figuur 10 Metatabel voor sequences

Bijlage 3

Firebird: databasetools

Command line tools Firebird wordt standaard geleverd met een aantal command line tools. Zie tabel 1. Tabel 1 Firebird-tools

U zult in de praktijk zelden van deze tools gebruik maken omdat er andere, grafische tools beschikbaar zijn die een min of meer gelijkwaardige functionaliteit bezitten en veel gebruiksvriendelijker zijn.

Grafische tools Er zijn verschillende grafische tools verkrijgbaar om een Firebird-database te benaderen en te beheren. In de cursus hebt u IQU gebruikt, geïntegeerd in de Boekverkenner. IQU is ook los te gebruiken en heeft dan vensters van variabele grootte. (Dit is ook in de Boekverkenner te realiseren door IQU te openen met de control-toets ingedrukt.) Vergelijkbare tools zijn FlameRobin en IBE xpert, beide specifiek bedoeld voor Firebird en Firebird. FlameRobin is open source; IBE xpert is commercieel, maar kent een gratis versie met beperkte functionaliteit. Er zijn ook commerciële GUI -tools voor Firebird beschikbaar, waarvoor betaald moet worden: Firebird Maestro, EMS SQL M anager for Firebird/ Firebird en DBM anager Pro Enterprise for Firebird. Met al deze tools kunnen via een grafische user interface databases worden beheerd (tabellen, indexen, enzovoort wijzigen, backup en restore uitvoeren) en worden bevraagd. Ook kunnen eenvoudig scripts worden gegenereerd waarmee de structuur en de inhoud van een database gemakkelijk is te kopiëren naar een andere database.

Figuur 1 toont enkele schermen van IBE xpert. Links ziet u de Databaseverkenner, die een connectie weergeeft naar de GrootOrderdatabase. Het mapje met indexen is opengeklapt. Rechts ziet u een master-detail-detailscherm van tabellen Klant, Order_ en Orderregel van de GrootOrderdatabase.

Figuur 1 Databaseverkenner en master-detail-detailscherm in IBExpert

Firebird is, hoewel gratis, een volwassen database die ook zeer grote aantallen tabellen en records aankan. Tabel 2 toont een aantal karakteristieken. Tabel 2 Firebird karakteristieken

Bijlage 4

Voorbeelddatabases

Deze bijlage bevat de populatiediagrammen van de voorbeelddatabases. Strokendiagrammen vindt u in de Boekverkenner.

OpenSchool

Orderdatabase (ook: Orderdatabase C , Orderdatabase D en GrootOrderdatabase; voor GrootOrderdatabase KS : zie Boekverkenner of hoofdstuk 13 )

Ruimtereisbureau (ook: Ruimtereisbureau D en RuimtereisSimpel; RuimtereisSimpel: alleen tabellen Reis, Hemelobject en Bezoek)

Toetjesboek (ook: Toetjesboek ZT )

Register 1NV, 135 2NV, 135 , 139 3GL, 444 3NV, 141 4GL, 444 4NV, 151 5NV, 151 Access mode, 436 ACID, 425 Actie, 46 Actieregel. Zie Gedragsregel Actuele parameter, 447 Afleidbare kolom, 329 Alarmlijn, 66 Alfanumeriek, 162 Algoritme het conceptuele ~, 241 Alias kolom~, 159 tabel~, 63 , 94 all, 282 all-privilege, 348 alter index ... (in)active, 381 alter sequence, 339 alter trigger, 463 alter user, 346 Alternate key. Zie Alternatieve sleutel Alternatieve sleutel, 38 , 44 ~specificeren, 332 ANSI/ISO ~-metaviews, 477 ~-SQL-standaards, 76 any, 282 Applicatie, 4 Applicatie-constraint, 464 Applicatielogica, 42 Argument, 90 , 167 asc, 179 ASCII, 163 , 174 Associatie, 39 één-veel-~, 63 ouder/kind-~, 38 , 43 veel-veel-~, 63 , 64 Atomair ~ feit, 27 ~e informatie, 27 ~e zin, 27 Atomicity, 425 Autojoin, 210 inner ~, 211 outer ~, 214 Autorisatie, 343

avg, 223 Backup, 384 Balanceren van index, 383 , 384 Bandbreedte, 7 BCNV, 145 Bedrijfslogica, 6 Bedrijfsproces, 362 begin, 446 Beperkingsregel, 23 , 31 bewaken van ~, 44 bijzondere ~, 43 standaard ~, 43 between ... and, 175 , 176 Bijzondere beperkingsregel, 43 Bijzondere gedragsregel, 51 Binaire operator, 164 blob, 331 Boekverkenner, 77 Boomstructuur, 353 Boyce/Codd normaalvorm, 145 Brontabel, 82 , 158 Cartesisch product, 192 Cascading delete, 47 , 295 Cascading update, 49 , 51 , 307 case, 170 cast, 164 , 169 Casting, 164 Cel, 11 Character, 162 Characterset, 163 Check option, 360 check-constraint, 332 , 333 , 361 ~ op domein, 335 ~ op kolom, 332 Client, 4 Client/server, 4 coalesce, 123 Codd, 5 , 124 , 125 ~-relationaliteit, 126 , 331 Collation order, 163 , 174 Commentaarcode, 85 commit, 97 , 293 Commit, 52 Commitmoment, 293 Concatenatie, 159 , 165 Conceptuele algoritme, 241 , 372 Concurrency, 423 Concurrency control, 423 multiversion ~, 424 optimistic ~, 424 Conjunctie, 128 connect, 320 Connectie, 78 , 320 Consistency, 425 Constante, 158 Constraint, 23 , 104 , 330 ~ in data dictionary, 476 ~ toevoegen of verwijderen, 325

~naam, 322 applicatie-~, 464 check-~, 332 , 361 foreign key-~, 104 , 331 inline ~, 326 not null-~, 104 out-of-line ~, 326 primary key-~, 104 , 331 unique-~, 332 Contextvariabele, 450 ~ deleting, 460 ~ inserting, 460 ~ new, 450 ~ old, 450 ~ updating, 460 Cost-based optimizer, 373 count, 221 create database, 319 create exception, 456 create index, 377 create procedure, 446 create role, 363 create sequence, 339 create table, 102 , 322 create trigger, 451 create user, 346 create view, 284 , 336 DAL. Zie Data Authorization Language Data Authorization Language, 75 Data Control Language, 75 Data definition language, 75 , 317 Data Definition Language, 101 Data dictionary, 318 , 469 constraint in ~, 476 database in ~, 471 domein in ~, 472 gebruiker in ~, 476 index in ~, 476 kolom in ~, 472 privilege in ~, 476 rol in ~, 476 tabel in ~, 472 zelfbeschrijvende ~, 478 Data Manipulation Language, 75 Database, 319 ~ creëren, 319 ~ en gebruikers, 320 ~ in data dictionary, 471 ~ verwijderen, 321 ~connectie, 78 , 320 ~-export, 320 ~object, 101 , 318 ~ontwerp, 27 ~opslag, 320 ~page, 384 ~rechten. Zie Privilege ~structuur, 23 relationele ~, 4

zelfbeschrijvende ~, 479 Database administrator, 344 Database-user-structuur ~ van Firebird, 344 ~van Oracle, 345 Datatype, 161 , 327 extern ~, 327 intern ~, 327 logisch ~, 334 Datawarehouse, 291 , 392 , 393 Datumconversiefunctie, 163 DBA, 344 DCL. Zie Data Control Language DDL. Zie Data definition language, Zie Data Definition Language, Zie Data Definition Language De Morgan, 129 Deadlock, 424 default, 325 , 329 ~-specificatie in create domain-statement, 335 Defragmentatie, 384 delete, 97 , 108 , 299 ~ met subselect, 301 ~ met where, 300 ~ zonder restrictie, 300 delete-privilege, 348 Deleteregel, 47 , 295 , 331 deleting, 460 Deltaproblematiek, 319 , 463 Deltaproblemen ~ bij sequences, 341 ~ bij views, 338 ~ op domeinniveau, 336 ~ op kolomniveau, 330 ~ op tabelniveau, 324 Denormaliseren, 139 , 191 , 231 Derde normaalvorm, 141 Determinant, 137 Dirty read, 428 disable constraint, 326 disconnect, 320 Disjunctie, 128 distinct, 160 , 259 groeperen in plaats van ~, 259 DML. Zie Data Manipulation Language Domein, 333 , 473 ~ creëren, 334 ~ in data dictionary, 472 ~ verwijderen, 336 ~ wijzigen, 336 Doorsnede, 185 Driewaardige logica, 129 Drill down, 293 Drill up, 293 drop constraint, 325 drop database, 321

drop exception, 463 drop index, 377 drop procedure, 463 drop role, 365 drop sequence, 341 drop table, 324 drop trigger, 463 drop user, 347 Durability, 426 Eerste normaalvorm, 135 Embedded SQL, 77 enable constraint, 326 end, 446 Entiteit, 16 Entiteittype, 16 Event, 46 , 448 except, 182 , 185 Exception, 456 , 458 ~ creëren, 456 ~ verwijderen, 463 execute, 445 execute-privilege, 348 exists, 266 Export, 320 Extensie, 24 Extern datatype, 327 false, 127 Fat client, 6 Fat server, 6 Firebird, 76 ~ Server Manager, 384 ~ Server uitschakelen, 387 first , 385 foreign key, 104 Foreign key. Zie Verwijssleutel Formele parameter, 445 , 447 full outer join, 216 Full table scan, 373 , 374 , 382 Functie, 90 , 167 argument van ~, 167 cast-~, 169 geretourneerde waarde van ~, 168 statistische ~, 220 typecast-~, 169 Functionaliteit, 105 zoek~, 107 Functionele afhankelijkheid, 137 triviale en niet-triviale ~, 145 Gebruiker, 344 ~ aanmaken, 346 ~ in data dictionary, 476 ~ verwijderen, 347 ~ wijzigen, 346 Gebruikersbeheer, 344 , 346 Gebruikersgroep, 362 Gebruikersnaam, 344 Gecorreleerde subselect, 253 , 264 ~ met exists, 266

~ met not exists, 270 Gedetermineerde, 137 Gedistribueerde database, 426 Gedrag, 46 Gedragsregel, 31 , 46 bijzondere ~, 51 standaard ~, 51 Gegeven, 3 , 13 , 25 afleidbaar ~, 11 , 23 enkelvoudig ~, 9 gestandaardiseerd ~, 21 inconsistent ~, 13 redundant ~, 13 samengesteld ~, 10 Gegevensbank. Zie Database Gegevenstaal, 4 , 15 Generator, 339 ~ in systeemcatalogus, 499 Genest groeperen, 240 Geneste subselect, 274 Gereserveerd woord, 83 grant, 348 , 349 Grantee, 354 Grantor, 354 Graphic user interface. Zie GUI Groeperen, 219 ~ als denormaliseren, 231 ~ en standaardisatie, 246 ~ in plaats van distinct, 259 ~ op berekende expressie, 230 ~ op optionele kolom, 230 genest ~, 240 onverwacht verfijnd ~, 236 verfijnd ~, 227 GrootOrderdatabase, 369 GrootOrderdatabaseKS, 390 group by, 226 , 236 GUI, 105 Harde semantiek, 151 having, 228 ~ of where, 229 Herhalende groep, 14 elimineren van ~, 17 omkering van ~, 15 Hiërarchische structuur, 353 Historieprobleem, 293 Identificatie, 35 , 50 Identifier, 83 if ... then ..., 452 iif, 171 , 185 Illustratieve populatie, 35 , 58 in........, 178 Inconsistent-analysisprobleem, 429 , 430 Inconsistent-dataprobleem, 428 Inconsistentie, 13 Incorrect-summaryprobleem, 429 Index, 374 , 376 ~ (in)activeren, 381 , 384

~ creëren, 377 ~ en order by, 378 ~ verwijderen, 377 ~in data dictionary, 476 ~structuur, 374 balanceren van ~, 383 , 384 binaire-boom~, 375 lineaire-lijst~, 374 onderhoud van ~, 383 selectiviteit van ~, 382 standaard~, 379 unieke ~, 378 Informatie, 3 , 13 , 25 ~systeem, 3 atomaire ~, 27 Ingres, 6 Inline constraint, 326 Inloggen, 78 , 320 inner join, 197 Inner join, 190 ~ via productoperator, 191 insert, 94 , 108 , 295 enkelvoudige ~, 295 meervoudige ~, 297 inserting, 460 insert-privilege, 348 Installeren, 78 Integriteit, 38 Integriteitsregel referentiële ~, 31 , 38 Interactive Query Utility, 80 Intern datatype, 327 Interne functie, 90 Interpreter, 83 intersect, 182 , 185 Intersectie, 185 Intervalverwijzing, 65 IQU, 80 is not null, 178 is null, 178 ISO, 76 Isolation, 425 Isolation level, 432 ~ read committed, 433 , 434 ~ read uncommitted, 433 ~ repeatable read, 433 ~ snapshot, 434 ~ snapshot table stability, 434 ~s in de SQL-standaard, 432 ~s in Firebird, 434 ~serializable, 433 Join, 91 , 189 ~ over brede sleutel, 201 ~ over niet-sleutelverwijzing, 214 ~conditie, 192 ~navigatie, 256 ~operatoren, 197 between-~, 215

full outer ~, 215 inner ~, 190 left outer ~, 195 outer ~, 195 , 199 right outer ~, 195 , 215 samengestelde ~, 203 statistische ~, 234 Joinen als denormaliseren, 191 Joinuniciteitsregel, 148 , 149 Kandidaatsleutel, 37 Kardinaliteitsregel, 40 Keuzelijst, 111 Keyword, 83 Kindtabel, 38 , 62 Kolom, 11 ~ in data dictionary, 472 ~ toevoegen of verwijderen, 325 ~definitie, 327 afleidbare ~, 329 lokale ~kenmerken, 472 niet-lokale ~kenmerken, 472 optionele ~, 31 samengestelde ~, 11 verplichte ~, 31 , 37 , 39 Kolomalias, 159 Kunstmatige sleutel, 44 , 338 left outer join, 196 , 199 Left outer join, 195 ~ via union, 196 Lege string, 121 Lijststructuur, 103 like, 175 Lock, 424 , 432 exclusieve ~, 424 gedeelde ~, 424 read-~, 424 write-~, 424 Lock conflict behaviour, 436 Locking granularity, 424 Logaritmisch zoeken, 376 Logica, 6 bedrijfs~, 6 driewaardige ~, 129 tweewaardige ~, 127 Logisch adres, 35 Logische algebra, 127 Logische equivalentie, 129 Logische expressie, 176 Lookup, 111 Lost update, 427 Master-detailvenster, 106 , 110 max, 224 Meta ~data, 321 ~-metaniveau, 479 ~niveau, 469 , 479 ~structuur, 467 ~tabel, 469

~view, 477 MGA, 434 , Zie Multi-Generational Architecture min, 224 Minimax-problemen, 240 Mulit-event trigger, 460 Multigenerational Architecture, 424 Multipliciteitendiagram, 40 Multipliciteitsregel, 40 Multi-user, 423 Multiversion concurrency control, 424 MVCC. Zie Multiversion Concurrency Control Naamgeving ~van kolom, 11 Navigatie ~ over brede sleutel, 261 join~, 256 subselect~, 256 Navigatiepad, 203 Navigeren, 203 Negatie, 128 Netwerkstructuur, 354 new, 450 Niet-gecorreleerde subselect, 251 , 264 Niet-sleutelverwijzing, 65 Non-repeatable read, 428 Normaalvorm, 135 Boyce Codd ~, 145 derde ~, 141 eerste ~, 135 tweede ~, 135 , 139 vierde ~, 151 vijfde ~, 151 Normalisatie, 21 , 27 , 135 not in, 271 not null, 325 , 329 ~ in create domain-statement, 335 ~ via check-constraint, 333 null, 118 , 127 ~-constante, 118 Null, 32 , 84 , 178 ~ in primaire sleutel, 37 interpretatie van ~’s, 124 Nullifying delete, 47 Nullifying update, 49 Objectniveau, 468 Objectprivilege, 348 OLAP. Zie Online Analytical Processing old, 450 OLTP. Zie Online Transactional Processing Onbepaald, 176 Onderhoud, 108 Onderhoudsvenster, 108 Onderliggende query, 106 Online Analytical Processing, 293 Online Transactional Processing, 291 , 392 OpenSchool, 54 , 67 Operand, 89 , 164 Operator, 89 , 164

alfanumerieke ~, 165 binaire ~, 164 datum~, 166 gemengde ~, 167 numerieke ~, 165 prioriteit van ~, 165 , 176 relationele ~, 182 string~, 165 ternaire ~, 175 unaire ~, 164 vergelijkings~, 174 verzamelingen~, 182 Optimistic concurrency control, 424 Optimizer, 205 , 370 , 373 , 377 , 386 cost-based ~, 373 keuzes van ~ tonen, 380 rule-based ~, 373 Optionele kolom, 31 Oracle, 5 Ordening, 88 , 174 ~ op expressies, 181 syntaxvarianten, 181 order by, 179 Orderdatabase, 70 OrderdatabaseC, 112 , 342 OrderdatabaseD, 112 Ouder/kind-associatie, 38 , 43 Oudertabel, 38 , 62 Outer join, 195 ~ via left outer join, 199 ~ via union, 196 Out-of-line constraint, 326 Parameter actuele ~, 447 formele ~, 445 , 447 Parser, 370 Partiële sleutelafhankelijkheid, 140 Password. Zie Wachtwoord Performance, 75 , 369 , 371 , 397 Performanceverbetering ~ door aanpassing databaseontwerp, 389 ~ door gecontroleerde redundantie, 391 ~ door indexen, 374 ~ door queryaanpassing, 386 Persoonsnamen, 125 Phantom, 429 Populatie, 23 ~diagram, 23 illustratieve ~, 35 , 58 Pragmatisch waarheidbegrip, 132 Primaire sleutel, 36 , 38 ~ en not null, 331 brede ~, 38 samengestelde ~, 60 Primary key. Zie Primaire sleutel Prioriteit, 128 Prioriteit van operatoren, 165 , 176 Privilege, 347

~ in data dictionary, 476 ~ in systeemcatalogus, 497 ~ terugnemen, 351 ~ toekennen aan rol, 363 ~ verlenen, 349 ~ verlenen aan alle gebruikers, 352 ~ verlenen with grant option, 352 all-~, 348 delete-~, 348 execute-~, 348 insert-~, 348 object~, 348 references-~, 348 select-~, 348 systeem~, 347 update-~, 348 Probleemaanpak, 397 stappenplan bij ~, 410 Procedure executable ~, 463 select-~, 463 Projectie, 85 , 158 public, 352 Puntnotatie, 19 Q’inspector, 105 QUEL, 6 Query, 80 ~plan, 373 , 380 ~tool, 73 aantal records in ~ beperken, 385 onderliggende ~, 106 statistische gegevens van ~, 380 Querytool, 79 Rdbms, 4 Read-only view, 359 reconnect, 321 Record, 369 Recovery, 425 Recursieve verwijzing, 62 , 63 Redundantie, 12 elimineren van ~, 18 gecontroleerde ~, 13 references, 104 references-privilege, 348 Referentie, 38 Referentiële integriteitsregel, 31 , 38 , 47 , 294 Refererende-actieregel, 47 , 331 Relatie, 4 , 23 Relationeel databasemanagement systeem. Zie Rdbms Relationele database, 4 , 23 Relationele model, 9 Relationele operator, 182 Restore, 384 Restricted delete, 47 , 51 , 295 Restricted update, 49 Resultaatkolom berekende ~, 159 constante ~, 158

Return value, 168 revoke, 351 right outer join, 215 Right outer join, 195 Rij, 11 Rol, 361 , 362 ~ in data dictionary, 476 ~ toekennen aan gebruiker, 363 ~ verwijderen, 365 ~ wijzigen, 365 lege ~ creëren, 363 privileges toekennen aan ~, 363 rollback, 97 , 293 Rollback, 53 Ruimtereisbureau, 315 , 505 RuimtereisbureauD, 334 , 505 RuimtereisSimpel, 467 , 505 Rule-based optimizer, 373 Samengestelde join, 203 Samengestelde sleutel, 60 Scheve query, 225 , 241 Script, 95 , 96 Security2.fdb. Zie security-database Security-database, 344 select, 82 , 158 Selectie, 173 Selectiviteit van index, 382 select-privilege, 348 Semantiek, 25 , 370 SEQUEL, 5 Sequence, 338 ~ creëren, 339 ~ resetten, 339 ~ verwijderen, 341 server, 4 Sessie, 320 set plan on / off, 380 set start_transaction, 420 set statistics display on / off, 380 set term, 446 set transaction, 432 ~ in IQU, 435 Single point of definition, 22 , 50 , 334 , 408 , 409 , 444 Single-user, 423 Sleutel, 35 alternatieve ~, 38 , 44 brede ~, 62 kandidaat~, 37 keuze van ~s, 389 kunstmatige ~, 44 primaire ~, 36 , 38 samengestelde ~, 60 verwijs~, 36 , 38 SQL, 4 , 5 , 73 ~-client, 4 ~-server, 4 SQL-interpreter, 370 Standaard beperkingsregel, 43

Standaard gedragsregel, 51 Standaardindex, 379 Standaardisatie, 21 , 27 start transaction, 419 Statement, 75 Statistieken ~ over één groep, 220 ~ over meerdere groepen, 225 Statistische functie, 220 Statistische query, 219 Stored procedure, 444 ~ en views, 463 ~ uitvoeren, 447 ~ verwijderen, 463 aanroep van ~, 445 String, 162 Strokendiagram, 23 Structuur, 23 Subselect ~ als oplossing van deelprobleem, 249 ~ in from-clausule, 253 ~ in having-clausule, 254 ~ in select-clausule, 252 ~ in where-clausule, 250 ~ met all, 282 ~ met any, 282 ~ met exists, 267 , 269 ~ met in, 267 , 269 ~ met not exists, 270 ~navigatie, 256 ~navigatie over brede sleutel, 261 brede ~, 261 gecorreleerde ~, 253 , 264 gecorreleerde ~ met exists, 266 gecorreleerde ~ met not exists, 270 geneste ~, 274 niet-gecorreleerde ~, 251 , 264 Subtabel, 10 sum, 223 Synchronisatie, 107 Syntaxis, 25 , 370 Sysdba, 344 Systeem, 3 Systeemcatalogus. Zie Data dictionary Systeemprivilege, 347 Systeemvlag, 473 Tabel, 4 , 23 , 321 ~ in data dictionary, 472 ~ met recursieve verwijzing, 323 ~ verwijderen, 324 ~alias, 63 , 94 ~naam wijzigen, 326 creëren van ~, 322 meta~, 469 virtuele ~, 63 , 336 Tekenrij, 162 , 165 Tekstconstante, 95 Temporary-updateprobleem, 428

Terminator, 446 Ternaire operator, 175 Test-voorbeelddatabase, 79 Thin client, 6 Thin server, 6 to_char, 169 , 488 to_date, 169 Toegangsregel, 76 Toekenning, 303 Toepassingsprogramma, 4 Toetjesboek, 7 ToetjesboekKS, 45 ToetjesboekZT, 443 , 444 Transactie, 52 , 97 , 293 , 418 ~-indicator, 421 concurrente ~s, 423 DDL-statement in ~, 318 , 422 samengestelde~, 419 select-statement in ~, 422 Transactiemanagement, 423 Transactiemodel het expliciete~, 419 het impliciete ~, 294 , 419 Transactioneel bedrijfssysteem, 291 , 392 Transactioneel systeem, 291 Transitieve sleutelafhankelijkheid, 142 Trigger, 14 , 447 , 454 ~ verwijderen, 463 executievolgorde van ~s, 464 multi-event ~, 460 Triggertaal, 444 true, 127 Tupel, 24 Tweede normaalvorm, 135 , 139 Tweewaardige logica, 127 Typecastfuncties, 164 Typecasting, 164 Udf. Zie User defined function Unaire operator, 164 Uncommitted-dataprobleem, 428 Uncommitted-dependencyprobleem, 428 Uniciteitsconstraint, 332 Uniciteitsregel, 33 , 56 brede ~, 34 , 57 join~, 148 smalle ~, 34 Unieke identificatie, 35 Unieke index, 378 union, 182 , 183 Union en join, 185 unique-constraint, 332 unknown, 129 Updatable view, 358 update, 99 , 108 , 302 ~ met subselect, 304 , 305 ~ van primaire sleutel, 307 ~ van verwijssleutel, 309 Update conflict, 433

update-privilege, 348 Updateregel, 49 , 331 Updateverbod, 309 updating, 460 User. Zie Gebruiker User defined function, 90 , 167 , 172 Valideren, 6 Variabele, 447 , 453 Veld, 369 Venster master-detail~, 106 master-detail-~, 110 onderhouds~, 108 Verbrede tabel, 91 Verfijnd groeperen, 227 onverwacht ~, 236 Verfijnde ordening, 180 Vergelijkingsoperator, 174 Verplichte kolom, 31 , 37 , 39 Verplichte-waarderegel, 31 Verwijssleutel, 36 , 38 brede ~, 37 , 38 , 62 optionele ~, 37 samengestelde ~, 62 Verwijzing, 19 , 22 , 35 , 37 , 38 , 40 , 47 , 60 ~over een brede sleutel, 61 loze ~, 31 niet-sleutel~, 65 , 66 optionele ~, 39 recursieve ~, 62 , 63 samengestelde ~, 60 Verwijzingsregel, 60 Verwoording, 25 Verzameling, 23 Verzamelingenoperator, 182 Vierde normaalvorm, 151 View, 283 , 336 ~ als autorisatiemiddel, 356 ~ als deeloplossing van probleem, 337 ~ creëren, 336 ~ verwijderen, 338 ~ wijzigen, 338 ~ with check option, 360 meta~, 477 read-only ~, 359 updatable ~, 358 Vijfde normaalvorm, 151 Virtuele tabel, 63 , 336 Voorbeeldnavigator, 77 Vreemde sleutel. Zie Verwijssleutel Waardetoekenning, 99 Wachtwoord, 344 where, 87 Whitespace. Zie Witteken with check option, 360 with grant option, 352 Witteken, 83

Zachte semantiek, 151