Relationele databases [2, 1 ed.]
 9789492231499, 9789492231802 [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

Blok 4

Wijzigen en beheren

Inhoud leereenheid 8

Wijzigen van een database-inhoud 9

Introductie Leerkern 1

2 3

4

5

6

10

Levenswijzen van een database 10 1.1 Typen database 10 1.2 Online transaction proccessing 11 1.3 Transactionele bedrijfssystemen 11 1.4 Datawarehouses 11 Transacties 12 Integriteitsregels 13 3.1 De referentiële-integriteitsregel 13 3.2 Deleteregels 13 Het insert-statement 14 4.1 Enkelvoudige inserts 14 4.2 Meervoudige inserts 15 Het delete-statement 18 5.1 Eenvoudige deletes 18 5.2 Deletes met subselect 19 Het update-statement 19 6.1 Eenvoudige updates 20 6.2 Updates met subselect 21 6.3 Update van primaire sleutel 23 6.4 Update van verwijssleutel 26

Samenvatting Zelftoets

26

27

Terugkoppeling 1 2

30

Uitwerking van de opgaven 30 Uitwerking van de zelftoets 31

8

Leereenheid 8

Wijzigen van een database-inhoud

INTRODUCTIE

Het oplossen van opvraagproblemen via select kan de vorm aannemen van een ware sport. Die sport hebben we in de voorgaande leereenheden 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 deze leereenheid 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. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – kunt toelichten welke samenhang bestaat tussen transacties en verschillende typen integriteitsregels – op een gestructureerde manier problemen kunt oplossen waarbij een database-inhoud gewijzigd moet worden, met behulp van de SQL-statements insert, update en delete. De studielast van deze leereenheid bedraagt 5 uur. Studeeraanwijzing

Net als bij eerdere leereenheden gaan we er weer vanuit dat u in ieder geval alle query’s uit deze leereenheid uitprobeert, maar liefst nog wat meer experimenteert. Geef tussendoor steeds een rollback-statement om de wijzigingen terug te draaien (en te voorkomen dat alles door elkaar gaat lopen).

Voorbeelddatabase

De voorbeelddatabase bij deze leereenheid is OpenSchool. In figuur 8.1 herhalen we het strokendiagram. Raadpleeg de bijlage achter in dit deel voor de voorbeeldpopulatie.

FIGUUR 8.1

Strokendiagram OpenSchool

LEERKERN 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. Wijzigingen op structuurniveau zijn het onderwerp van de volgende leereenheid. 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 transaction 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. 1.2

Online transaction processing OLTP

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 e-commercetransacties via het internet horen hiertoe. Het gaat om korte transacties, die volledig door programmatuur worden gestroomlijnd. Elke transactie wordt beschouwd als een bijdrage aan de geschiedenis, waaraan niets meer kan worden veranderd. Er vinden daarom voornamelijk of alleen inserts plaats. 1.3

Transactioneel bedrijfssysteem

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. 1.4

Datawarehouse

ONLINE TRANSACTION PROCESSING

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.

OLAP

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’). Datawarehouses vallen buiten het bestek van deze cursus.

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. 2

Transacties

Een transactie is een reeks SQL-statements die ofwel in hun geheel ofwel in het geheel niet worden uitgevoerd, zie ook paragraaf 1.6 van leereenheid 1. Een transactie kan één statement omvatten, of een langere reeks statements 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, dus na het committen van alle wijzigingen. Het kan daarom nodig zijn gedurende een transactie bepaalde regels tijdelijk uit te schakelen. In deze leereenheid ligt de nadruk niet op de inhoud en noodzaak van transacties, maar maken we wel gebruik van de mogelijkheid om wijzigingen te committen of eigenlijk vooral te rollbacken. Commitmoment

commit

rollback

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, zie leereenheid 9) impliciet een commitmoment inhouden. Een commitmoment maakt alle voorafgaande wijzigingen (sinds het vorige commitmoment) definitief. Zolang een transactie nog loopt, dus zolang er nog geen commitmoment is geweest, kunnen alle wijzigingen die met inserts, deletes of updates zijn aangebracht, nog ongedaan worden gemaakt met het statement rollback. Overigens is het definitieve karakter van een voltooide transactie ook maar relatief: het is bijvoorbeeld mogelijk om ‘definitief’ toegevoegde nieuwe rijen weer te verwijderen met een delete (in een volgende transactie). Commitmomenten in Firebird In Firebird is, conform de SQL-standaard, een DDL-statement geen commitmoment. Een transactie kan dus structuurwijzigingen bevatten. Het is wel mogelijk om in te stellen dat DDL-statements wél commitmomenten zijn voor DDL-statements. Dat wil zeggen dat een uitgevoerd DDL-statement dan meteen gecommit wordt, maar eventuele voorafgaande nog niet eerder gecommitte inserts niet.

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. We kijken voor elk type constraint welke problemen kunnen optreden en hoe we deze het hoofd kunnen bieden. 3.1

Referentiëleintegriteitsregel

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. 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. Cascading delete: transactie

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.

Zie leereenheid 9

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. Voor de overige verwijzingen geldt een restricted delete. 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.

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. Twee typen insert

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. 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, null’s en date-waarden. Geef tussen de verschillende alternatieven voor één opdracht steeds rollbackstatements. VOORBEELD 8.1 (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 8.2 (behandeling van null’s en datewaarden)

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', '2016-06-03'); OPGAVE 8.1

Een nieuwe cursus Business Intelligence (BI) wordt in het cursusprogramma opgenomen, met een studiebelasting van 150 uur en zwaarte 5 ECTS. Vereiste voorkennis: Databases (DB) en informatiemodelleren (IM). Welke inserts moeten worden uitgevoerd en in welke volgorde? Geef na afloop een rollback. 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 eenvoudig datawarehouse van de Open School. VOORBEELD 8.3

Het management van de Open School wil statistische informatie over cursusinschrijvingen. Hiertoe is een datawarehouse aangemaakt met twee tabellen: DWCursus en DWCursusresultaat (zie figuur 8.2).

FIGUUR 8.2

Datawarehouse cursusresultaten

Toelichting  Tabel DWCursus moet alle cursussen bevatten. Kolom begeleidJN moet de waarde ‘J’ krijgen als de cursus begeleid is en anders ‘N’. Een cursus is begeleid als tabel Begeleider voor die cursus één of meer begeleiders bevat.  Tabel DWCursusresultaat 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 DWCursusresultaat bevat een partiële sleutelafhankelijkheid (welke?) en voldoet dus niet aan de tweede normaalvorm. Omdat een datawarehouse gevoed wordt vanuit één of meer bronsystemen met consistente data en er geen bewerkingen op plaatsvinden, is er geen gevaar voor inconsistentie en is dit geen bezwaar. Het datawarehouse wordt gevuld en periodiek bijgewerkt vanuit de OpenSchool-database. We gaan er van uit dat de tabellen DWCursus en DWCursusresultaat al bestaan en deel uitmaken van de OpenSchooldatabase zelf (wat inderdaad het geval is). We beperken ons tot het eenmalig vullen van beide tabellen. Vullen van DWCursus Met de volgende twee insert-statements kunnen alle cursussen worden ingevoegd in DWCursus. 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 DWCursus.  Alle rijen die door de select-expressie worden opgehaald worden ingevoegd in DWCursus.  Merk op dat de select-expressie een soort subselect is, maar dan zonder de haakjes eromheen.

Met behulp van de ‘als-dan’-functie iif (zie leereenheid 4, paragraaf 5.4) kan het nog een stuk eleganter. De hele klus kunnen we klaren met één insert-statement: 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 DWCursusresultaat 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 wordt gebruikgemaakt van de functie extract, die jaar respectievelijk maand ‘uit de datum haalt’ (zie paragraaf 5.2 van leereenheid 4).  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.

5

Het delete-statement

Om een of meer rijen uit een tabel te verwijderen, is er het deletestatement. Een delete is in zekere zin het omgekeerde van een insert.

delete-statement

Zolang u maar geen commit-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. 5.1

EENVOUDIGE DELETES

Het delete-statement in zijn meest simpele vorm heeft het meest vergaande effect, zoals blijkt uit het volgende voorbeeld. VOORBEELD 8.4 (delete zonder restrictie)

Verwijder 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 8.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 8.6 (delete met cascading-deleteeffect)

Verwijder cursus SW (Semantic web). Oplossing: delete from Cursus where code = 'SW';

Gevolg: niet alleen cursus SW wordt verwijderd, maar ook de rij van Voorkenniseis vanwaaruit naar cursus SW wordt verwezen.

Dit als gevolg van de cascading delete die geldt voor de verwijzing vanuit Voorkenniseis naar Cursus. Het verwijderen van de cursus SW slaagt overigens alleen maar omdat cursus SW geen begeleider heeft é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. 5.2

DELETES MET SUBSELECT

De selectieconditie van een delete-statement mag een subselect bevatten. VOORBEELD 8.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, áls dit alles tenminste niet stuit op een probleem vanwege een restricted delete. Tweede oplossing, met gecorreleerde subselect: delete from Cursus C where not exists (select * from Inschrijving where cursus = C.code); OPGAVE 8.2

Door welke delete (of delete’s, in de juiste volgorde) kunnen de insert’s van opgave 8.1 ongedaan worden gemaakt? OPGAVE 8.3

Verwijder alle tentamens waarvoor geldt dat er een vervolgtentamen is geweest met een hoger cijfer. OPGAVE 8.4

Verwijder alle docenten die niet optreden als mentor, noch als examinator, begeleider of vervanger. 6

update-statement

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 selectexpressie. Zo kunnen update-statements worden geformuleerd van uiteenlopende complexiteit.

6.1

EENVOUDIGE UPDATES

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

Wijzig het aantal uren van de cursus DW in 130. Oplossing: update Cursus set uren = 130 where code = 'DW';

-- 1 -- 3 -- 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(en) waarin de update(s) moet(en) plaatsvinden 3 de update zelf: de kolom(men), met de nieuwe waarde(n).

Toekenning =

Net als bij select’s (zie paragraaf 6 van leereenheid 6) 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 8.3.

FIGUUR 8.3 VOORBEELD 8.9 (update van elke rij)

Waardetoekenning en gelijkheid

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 ====== II DW DB IM SW

VOORBEELD 8.10 (oude waarde gebruiken in toekenning)

NAAM UREN CREDITS EXAMINATOR ===================== ========= ======= ========== Inleiding informatica 80 2.9 BAC Discrete wiskunde 120 4.3 DAT Databases 120 4.3 COD Informatiemodelleren 150 5.4 DAT Semantic web 120 4.3

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 8.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. 6.2

Subselects in toekenning of selectieconditie

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.

Niet-gecorreleerd Gecorreleerd

Eerst volgt een voorbeeld met een niet-gecorreleerde subselect. Ter herinnering: geef na elk voorbeeld een rollback. VOORBEELD 8.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 ====== II DW DB IM SW

NAAM UREN ===================== ========= Inleiding informatica 118 Discrete wiskunde 125 Databases 126 Informatiemodelleren 127 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 leereenheid 12 ‘Triggers en stored procedures’. We kennen daar het resultaat van de subselect toe aan een variabele. Niet-relationele verwerking van een update Het probleem bij voorbeeld 8.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 8.13 (update met gecorreleerde subselect: vullen van berekenbare kolom)

Voor dit voorbeeld brengen we eerst een kleine wijziging aan in de databasestructuur: vooruitlopend op de volgende leereenheid breiden we de 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 8.4.

FIGUUR 8.4

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 leereenheid 12). 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 opgave 8.5 ziet u een voorbeeld van een update met subselects in zowel de set- als de where-clausule. OPGAVE 8.5

Kolom Inschrijving.cijfer is afleidbaar: de waarde is het hoogste corresponderende tentamencijfer. Geef een update-statement waarbij de kolom Inschrijving.cijfer wordt (her)berekend. 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.

Cascading update van primaire sleutel

VOORBEELD 8.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 8.5.

FIGUUR 8.5

Cascading update van primaire sleutel

Voorbeeld 8.15 illustreert dat updates zoals deze niet altijd probleemloos kunnen worden uitgevoerd. VOORBEELD 8.15 (volgordeprobleem: strijdig met relationele theorie)

Verhoog alle studentnummers met 1. Dit lijkt sprekend op voorbeeld 8.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 8.5, 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 in Firebird de update geen echte verzamelingenoperatie is, maar rij voor rij wordt uitgevoerd (zie de grijstekst in paragraaf 6.2). In bijvoorbeeld Oracle wordt zo’n update wel goed uitgevoerd. In opgave 8.6 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 8.6

Verhoog alle studentnummers met 1. Update van samengestelde primaire sleutel Bij de update in voorbeeld 8.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 voorbeeld 8.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 8.16 (update deel van primaire sleutel)

Boek de inschrijving van student 4 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 8.16), is dat een heel andere operatie dan wanneer we in Cursus een cursuscode veranderen (vergelijkbaar met voorbeeld 8.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 8.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). Cursus Modeldriven development

In de vervolgcursus Model-driven development zult u zien dat door zo’n verbod het handhaven van allerlei bedrijfsregels een stuk eenvoudiger wordt.

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 8.17 (toekennen van ouder aan een kind zonder ouder)

Geef student 4 mentor ‘BAC’ (was eerst null). Oplossing: update Student set mentor = 'BAC' where nr = 4;

VOORBEELD 8.18 (een kind tot weeskind maken)

-- was eerst null

Schrap de mentor van student 3 (was eerst ‘BAC’). Oplossing: update Student set mentor = null where nr = 3;

SAMENVATTING Paragraaf 1

Op grond van het type transacties dat op de database wordt uitgevoerd, onderscheiden we: – systemen voor online transaction processing (OLTP): voor het snel, online verwerken van een grote hoeveelheid nieuwe gegevens – transactionele bedrijfssystemen: voor het bijhouden van de dagelijkse bedrijfswerkelijkheid – datawarehouses: grote historiedatabases met gegevens uit verschillende bronnen, voor het opvragen van managementinformatie en het uitvoeren van analyses. De term OLTP wordt vaak breder gebruikt en omvat dan mede de genoemde transactionele bedrijfssystemen.

Paragraaf 2

Wijzigingen worden definitief door een commitmoment. Dit valt samen met het uitvoeren van het commit-statement, of – bij sommige rdbms’en of SQL-omgevingen  bij een commit als neveneffect van een DDL-statement.

Paragraaf 3

Bij het wijzigen van een database-inhoud moet het handhaven van database-integriteit gewaarborgd zijn. Soms is het daarbij wenselijk een beperkingsregel tijdens een transactie tijdelijk uit te schakelen. Een belangrijk mechanisme om referentiële integriteit te handhaven, wordt gevormd door de delete- en updateregels.

Paragrafen 4, 5 en 6

Het insert-statement, voor het invoeren van nieuwe rijen, kent twee varianten: een met een values-lijst voor het invoeren van één rij en een met een select-clausule voor het invoeren van een verzameling rijen. In de variant met de values-lijst is het specificeren van een kolommenlijst verplicht, tenzij de values-lijst, in lengte en volgorde, correspondeert met de kolomdefinitie van het create table-statement. Het delete-statement en het update-statement, voor respectievelijk het verwijderen en wijzigen van rijen, kennen – optioneel – een whereclausule met selectieconditie. Deze mag een subselect bevatten, al dan niet gecorreleerd. Het update-statement bevat daarnaast een verplichte set-clausule met een expressie waarin de nieuwe celwaarden worden gespecificeerd. Ook deze expressie mag een al dan niet gecorreleerde subselect bevatten. Bijzondere aandacht verdienen updates van sleutelwaarden. ZELFTOETS

De volgende opgaven zijn gebaseerd op Orderdatabase. Het strokendiagram herhalen we hier, de voorbeeldpopulatie vindt u in de bijlage achterin.

FIGUUR 8.6

Strokendiagram Orderdatabase

1

Voer een nieuwe order in: nummer 5900 voor klant 1447. De orderdatum hoeft u niet zelf in te vullen: volgens het create-script wordt daarvoor als defaultwaarde de systeemdatum ingevuld. Laat totaalbedrag leeg.

2

Deze opgave gaat over integratie in Orderdatabase van een extra artikeltabel, ArtikelExtra, zie figuur 8.7. De kolommen corresponderen met de kolommen nr, omschrijving, inkoopprijs en voorraad van Artikel, zie figuur 8.8. 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 8.7

Extra artikelen ter integratie in Orderdatabase

FIGUUR 8.8

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 8.7. 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. 3

Maak een script voor het verwijderen van klanten die al sinds november 2012 niets meer hebben besteld, met alles wat bij die klanten hoort. Stel een stappenplan op alvorens te gaan programmeren.

TERUGKOPPELING 1

8.1

Uitwerking van de opgaven

Invoeren van de cursus: insert into Cursus (code, naam, uren, credits) values ('BI', 'Business Intelligence', 150, 5);

De examinator wordt null. Dat kan ook expliciet gebeuren, zoals in OpenSchoolInsert.sql. Invoeren van voorkenniseisen: insert into Voorkenniseis values ('BI', 'DB'); insert into Voorkenniseis values ('BI', 'IM');

8.2

Eén statement is voldoende: delete from Cursus where code = 'BI';

Vanwege de cascading delete worden de bijbehorende rijen van Voorkenniseis meeverwijderd. 8.3

Een oplossing is een delete met een gecorreleerde subselect delete from Tentamen where exists (select from where

T * Tentamen cursus = T.cursus and student = T.student and volgnr > T.volgnr and cijfer > T.cijfer);

De subselect met exists kan vervangen worden door een subselect met in. 8.4

SQL-statement:

delete from Docent where acr not in (select mentor from Student where mentor is not null) --cf. uitwerking opg 7.7 and acr not in (select examinator from Cursus where examinator is not null) and acr not in (select docent from Begeleider where docent is not null) and acr not in (select vervanger from Docent where vervanger is not null);

De subselects kunnen uiteraard worden vervangen door gecorreleerde varianten met not exists.

8.5

Het gevraagde update-statement bevat een gecorreleerde subselect: update Inschrijving I set cijfer = (select max(cijfer) from Tentamen where student = I.student and cursus = I.cursus) where cursus in (select cursus from Tentamen);

De subselect met in kan uiteraard vervangen worden door een (gecorreleerde) subselect met exists. Ga na dat de laatste where-clausule ook achterwege mag blijven. Het is echter wel netjes om de berekening alleen uit te voeren voor cursussen waarvoor een tentamen bestaat. 8.6

We gebruiken het volgende trucje: verhoog alle nummers eerst met een ‘veilige’ waarde en verlaag ze daarna met die veilige waarde minus 1. Bijvoorbeeld: update set update set

Student nr = nr + 1000; Student nr = nr - 999;

Enigszins onverwacht blijkt het volgende ook te werken: update Student set nr = nr +1 order by nr desc; 2

1

Uitwerking van de zelftoets

Voldoende is de kolommen nr en klant te vullen: insert into Order_ (nr, klant) values (5900, 1447);

Het create-script bevat de volgende definitie van de tabel Order_: create table Order_ (nr integer not null, klant integer not null, datum date default current_date totaalbedrag numeric(7,2), primary key (nr), foreign key (klant) references Klant(nr) on update cascade );

not null,

Hieraan zien we dat de kolomwaarde van datum ‘vanzelf’ de systeemdatum wordt. Totaalbedrag is optioneel, dus als we daarvoor geen waarde opgeven blijft dat leeg.

2

b Insert-script: insert insert insert insert insert insert

into into into into into into

ArtikelExtra ArtikelExtra ArtikelExtra ArtikelExtra ArtikelExtra ArtikelExtra

values values values values values values

(501, (502, (503, (504, (505, (506,

'keggebek', 16.50, 'handboom', 85.00, 'radiobutton', 6.50, 'zwalik', 24.00, 'slewel', 14.75, 'wormgat', 99.00,

120); 75); 28); 185); 10); 7);

insert into Correspondentie values (449, 501); insert into Correspondentie values (180, 504); insert into Correspondentie values (351, 505); commit;

c

Het gaat in twee stappen.

Stap 1: aanpassen van omschrijvingen en voorraden in Artikel aan die van corresponderende rijen in ArtikelExtra Stap 2: invoeren in Artikel van de artikelen in ArtikelExtra die niet in Artikel (en dus niet in Correspondentie) voorkomen. Stap 1 update Artikel A set omschrijving = (select AE.naam from ArtikelExtra AE join Correspondentie C on AE.anr = C.anr where C.nr = A.nr), voorraad = voorraad + (select AE.aantal from ArtikelExtra AE join Correspondentie C on AE.anr = C.anr where C.nr = A.nr) where nr in (select nr from Correspondentie);

Stap 2 insert into Artikel (nr, omschrijving, verkoopprijs, inkoopprijs, voorraad) select anr, naam, round(prijs * 1.30, 2), prijs, aantal from ArtikelExtra where anr not in (select anr from Correspondentie);

3

Het stappenplan: Stap 1: Verwijder alle klachten van de klanten die sinds november 2012 niets meer hebben besteld. Stap 2: Verwijder alle orders met orderregels (cascading delete!) van de klanten die sinds november 2012 niets meer hebben besteld. Stap 3: Maak een aanbrengernummer null, indien dit het klantnummer is van een klant zonder order. Stap 4: Verwijder alle klanten zonder order.

Het achterwege laten van stap 3 zou problemen geven bij stap 4. Een klant kan immers niet worden verwijderd indien er nog een verwijzing naar bestaat. Stap 1 delete from Klacht where order_ in (select O.nr from Order_ O where -- de klant bij ‘deze’ order heeft geen order van 2007 of -- later not exists (select * from Order_ where klant = O.klant and datum >= '2012-11-01'));

Merk op dat deze wat ingewikkeld aandoende route via Order_ noodzakelijk is om na te gaan of de betreffende klant behalve de order waar de klacht over gaat nog (en die van vóór november 2012 kan zijn) nog andere orders heeft, die (wel) van november 2012 of later zijn. Stap 2 delete from Order_ O where -- de klant bij deze order heeft geen order van nov 2012 of later not exists (select * from Order_ where klant = O.klant and datum >= '2012-11-01');

Nu is de weg vrij om ook die klanten zelf te verwijderen. Omdat Klant echter een recursieve verwijzing bevat, moeten vooraf de verwijzingen naar die klanten null gemaakt worden. Stap 3 update Klant set aanbrenger = null where -- aanbrenger is het klantnummer van een klant zonder order aanbrenger not in (select klant from Order_);

Stap 4 delete from Klant where -- bij ‘deze’ klant hoort geen enkele order nr not in (select klant from Order_);

Opmerking: om goed te kunnen testen, is het van belang dat de populatie illustratief is (en dat is hij nu niet).

Inhoud leereenheid 9

Definitie van gegevensstructuren 35

Introductie Leerkern 1 2

3

4

5

6

7

8

9

36

Voorbeelddatabase: Ruimtereisbureau 36 Data definition language 38 2.1 Databases en databaseobjecten 38 2.2 Gebruikersinformatie en meta-informatie 38 2.3 DDL en transacties 39 2.4 Structuurwijzigingen: deltaproblematiek 39 Levenscyclus van een database 39 3.1 Creëren van een database 40 3.2 Een connectie met een database 41 3.3 Verwijderen van een database 41 Tabellen 42 4.1 Creëren van tabellen 42 4.2 Constraintnamen 43 4.3 Tabel met recursieve verwijzing 44 4.4 Verwijderen van tabellen en deltaproblemen 44 Kolomdefinities 46 5.1 Elementen van een kolomdefinitie 47 5.2 Datatypen 47 5.3 Not null-constraints 48 5.4 Default-specificaties 48 5.5 Afleidbare kolommen 49 5.6 Deltaproblemen op kolomniveau 49 Constraints 50 6.1 Primary key-constraints 50 6.2 Foreign key-constraints 50 6.3 Unique-constraints 51 Check-constraints 51 6.4 Domeinen 53 7.1 Wat zijn domeinen? 53 7.2 Creëren van domeinen 54 7.3 Check-constraints op domeinen 55 7.4 Verwijderen van domeinen en deltaproblemen 55 Views 56 8.1 Creëren van views 56 8.2 Gebruik van views 56 8.3 Verwijderen van views en deltaproblemen 57 Sequences 58 9.1 Kunstmatige sleutels 58 9.2 Creëren en gebruiken van sequences 59 9.3 Verwijderen van sequences en deltaproblemen 60

Samenvatting Zelftoets

61

62

Terugkoppeling 1 2

64

Uitwerking van de opgaven 64 Uitwerking van de zelftoets 67

34

Leereenheid 9

Definitie van gegevensstructuren

INTRODUCTIE

De leereenheden 4 t/m 8 gingen over het bekijken en wijzigen van een bestaande database, met behulp van de DML-subtaal van SQL (Data Manipulation Language). In deze leereenheid behandelen we de data definition language (DDL), de SQL-subtaal om de database zelf en de databaseobjecten daarbinnen te creëren, te verwijderen of om hun structuur te veranderen. 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 problemen die verband houden met structuurwijzigingen. Databaseobjecten zijn er in vele soorten, en de mogelijkheden zijn dusdanig uitgebreid dat we alleen het belangrijkste behandelen. Meer details vindt u in de (Firebird-)documentatie. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – een korte uitleg kunt geven bij de behandelde typen databaseobject – weet welke typen opdrachten behoren tot SQL-DDL – kunt uitleggen wat wordt bedoeld met een database installeren, inloggen, uitloggen, databaseconnectie en sessie – de DDL beheerst voor het creëren of verwijderen van tabellen, views, domeinen en sequences – de DDL beheerst voor het uitvoeren van structuurwijzigingen bij tabellen – zogenaamde ‘deltaproblemen’ kunt oplossen, ook indien SQL (ongeacht welk dialect) geen directe taalondersteuning biedt – kunstmatige sleutelwaarden automatisch kunt laten genereren door middel van een sequence. Deze leereenheid heeft een studielast van 7 uur. Studeeraanwijzing

Zoals bij alle voorgaande SQL-leereenheden gaan we ervan uit dat u op zijn minst alle gegeven code uitprobeert, maar liefst ook verder experimenteert met de aangeboden stof. Kijk in de handleiding bij de SQL-omgeving op de cursussite over hoe om te gaan met commits in deze leereenheid.

LEERKERN 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 9.1 geeft de databasestructuur voor een ondersteunend informatiesysteem. Dit is een logisch model, waarvoor twee implementatievarianten worden gegeven: een basisvariant Ruimtereisbureau, en in paragraaf 7 een variant RuimtereisbureauD die gebruikmaakt van ‘domeinen’.

FIGUUR 9.1

Planeet en maan Let op verschil ‘Maan’ en ‘maan’.

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.  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.  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 deze leereenheid 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 verblijfsduur, 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. 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 Deelnemer. Zie figuur 9.2 voor een voorbeeldpopulatie.

FIGUUR 9.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.

2

Data definition language DDL

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’). Deze laatste twee laten we in deze cursus buiten beschouwing. 2.1

DATABASES EN DATABASEOBJECTEN

Databaseobjecten zijn de ‘dingen’ waarmee een database is gevuld. In deze leereenheid kijken we naar tabellen, domeinen, views en sequences. Tabel

Over tabellen is nog heel wat meer te zeggen dan we al hebben gedaan. We doen dat in de paragrafen 4 t/m 6.

Domein

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 7.

View

Met views hebben we al kennisgemaakt in leereenheid 7. Een view kunnen we opvatten als een virtuele tabel. In feite is het een selectstatement dat onder een eigen naam wordt bewaard. Met de daar behandelde views formuleerden we deeloplossingen van min of meer complexe queryproblemen. In paragraaf 8 maken we kennis met andere gebruiksmogelijkheden van views.

Sequence

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 9 vertellen we er het fijne van. In volgende leereenheden worden nog andere typen databaseobjecten behandeld: in leereenheid 12 triggers en stored procedures, en in leereenheid 10 indexen. 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.

Gebruikersinformatie Meta-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). Leereenheid 13 is daar helemaal aan gewijd. Daar zal dit allemaal concreet worden.

Data dictionary Systeemcatalogus

2.3

Commitmoment Zie de handleiding op de cursussite

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 koppelen een commitmoment aan een DDL-statement. Zie de handleiding van de SQL-omgeving voor tips over hoe u daar in deze leereenheid mee om kunt gaan. 2.4

Deltaproblematiek

DDL EN TRANSACTIES

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: een nieuwe (lege) tabel maken zonder die ene kolom (create table) 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). 1 2

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. 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.

Zie de handleiding op de cursussite

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. De handleiding van de SQL-omgeving op de cursussite beschrijft waar u de genoemde SQL-statements precies moet geven. 3.1

CREËREN VAN EEN DATABASE

De manier om een database aan te maken verschilt per dialect. In Firebird gaat het creëren van een database voor Ruimtereisbureau (de implementatievariant zonder domeinen) als volgt: create database 'Ruimtereisbureau.fdb' user 'Ruimtereisbureau' password 'pw';

create databasestatement

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.

User ‘Sysdba’ heeft password ‘masterkey’

Omdat niet iedereen zomaar een database mag creëren, moet een create database-statement een geldige combinatie van een gebruikersnaam en een wachtwoord bevatten. Wij laten in deze cursus het beheren van gebruikers en hun privileges buiten beschouwing, en we hebben dus maar één gebruiker: de database administrator, met gebruikersnaam ‘Sysdba’ en wachtwoord ‘masterkey’. Een create database-statement kan worden uitgebreid met opties die van invloed zijn op de fysieke opslagkenmerken. Raadpleeg hiervoor de documentatie. Databases en gebruikers

Relatie databases en gebruikers is rdbms-afhankelijk

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 Export Firebirddatabase = ‘meenemen’ .fdbbestand

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.

3.2

Connectie Inloggen Sessie

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 vensters werkt, heeft twee sessies open staan. Inloggen op een al eerder aangemaakte database gaat met het connectstatement. 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 'Sysdba' password 'masterkey';

Connect

Na uw antwoord op de vraag of u de lopende transactie wilt committen of niet wordt de vorige sessie dan automatisch beëindigd, wat inhoudt dat de connectie met OpenSchool.fdb wordt gesloten. Daarna wordt u ingelogd op Ruimtereisbureau.fdb. 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

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. OPGAVE 9.1

Creëer als gebruiker Sysdba (met wachtwoord masterkey) een database Testdatabase.fdb. Controleer of het bestand ook daadwerkelijk is gemaakt. Maak ook een testtabel. OPGAVE 9.2

Beëindig de sessie en ga na dat het nu niet mogelijk is tabellen te creëren. OPGAVE 9.3

Wijzig de naam van Testdatabase.fdb in Testbase.fdb. OPGAVE 9.4

Verwijder Testbase.fdb. Doe dit op twee manieren, na tussentijds opnieuw aanmaken.

4

Implementatiemodel Ruimtereisbureau

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 4.1 is onderdeel van het eerste implementatiemodel van het logische Ruimtereisbureau-model. De naam van dit implementatiemodel luidt: Ruimtereisbureau. 4.1

CREËREN VAN TABELLEN

Het script RuimtereisbureauCreate.sql bevat alle benodigde create tablestatements. We beginnen met een vereenvoudigde versie van de twee statements voor het creëren van de tabellen Transport en Reis.

create table

script RuimtereisbureauCreate.sql (vereenvoudigd fragment)

create table Transport (code varchar(2) omschrijving varchar(12) primary key (code), unique (omschrijving) );

not null, not null,

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-sleutelconstraints 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 verwijssleutelconstraints worden gedefinieerd door de keywords foreign key, gevolgd door de kolomnaam tussen haakjes, gevolgd door het keyword references, met daarachter de primaire sleutel in de vorm van (). Bij een brede sleutel krijgen we ook hier een kommalijst van kolomnamen.  Met on update cascade specificeren we een cascading update. Met on delete restricted kunnen we ook nog een restricted delete specificeren, maar omdat dat de default is, laten we dat doorgaans weg.

4.2

Constraintnaam

Elke constraint heeft een naam

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 nullconstraint. 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$relation_constraints. De rdbms-namen bestaan echter uit een code en een nietszeggend volgnummer (zoals RDB$PRIMARY23), 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.

script RuimtereisbureauCreate.sql (fragment met constraintnamen)

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 5 gaan we dieper in op kolomdefinities. In paragraaf 6 gaan we dieper in op constraints.

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. 4.4

drop table

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 Hemelobject 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.

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 5.6. Toevoegen of verwijderen van een kolom In één alter table-statement kunnen meerdere wijzigingen (toevoegen of verwijderen van kolommen) in één kommalijst worden gecombineerd.

alter table

VOORBEELD 9.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 add temperatuur integer drop afstand;

not null, not null,

De datatypen zijn gekozen omdat we denken zowel voor omlooptijd als temperatuur te kunnen volstaan met gehele getallen. Na deze structuurwijziging hebben alle bestaande rijen in de twee nieuwe kolommen automatisch een 0 gekregen, wat blijkbaar de default-waarde is voor later toegevoegde not null-kolommen van type integer. We moeten de juiste waarden nog invoeren middels een update-statement. In het script van paragraaf 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 defaultspecificatie zien we als het wijzigen van een kolomdefinitie, zie hiervoor paragraaf 5. Voor de overige constraints geldt dat het toevoegen of verwijderen vrijwel net zo gaat als het toevoegen of verwijderen van een kolom. VOORBEELD 9.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 Deelnemer en Bezoek. De volgende procedure leidt tot het gewenste resultaat. Omweg bij gebrek aan renamecommando

1 2 3 4 5

Maak een nieuwe tabel: Ruimtereis. Kopieer de inhoud van Reis naar Ruimtereis. Verwijder alle verwijssleutels naar Reis. Verwijder de oude tabel Reis. Maak nieuwe verwijssleutels, naar Ruimtereis.

OPGAVE 9.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.) Aanwijzing: Raadpleeg de handleiding om te weten wanneer u moet committen. OPGAVE 9.6

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

Inline en out-of-line

Kolomdefinities

Een tabeldefinitie omvat definities van kolommen en van constraints. Deze blijken niet altijd gescheiden te zijn. Allereerst zijn er de not nullconstraints 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.

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 gaan we nader in: het datatype, de not null-specificatie en de defaultspecificatie. 5.2

SQL-standaard datatypen

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 9.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.

FIGUUR 9.3

De belangrijkste datatypen van ANSI/ISO-SQL

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 SQLstandaard.

Beperkt aantal interne datatypen per rdbms

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. 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 figuur 9.4 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.

FIGUUR 9.4 5.3

not null

NOT NULL-CONSTRAINTS

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

default-specificatie

numeric’s worden intern verschillend opgeslagen

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 insertstatement. 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. Overigens is deze default-specificatie in dit geval overbodig: zie voorbeeld 9.1. 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. 5.5

Afleidbare kolom

AFLEIDBARE KOLOMMEN

De SQL-standaard en ook Firebird ondersteunen afleidbare kolommen. Als voorbeeld 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.energiePP als afleidbare kolom worden gedefinieerd. Er zijn dan geen triggers nodig; zie leereenheid 12, aan het eind van paragraaf 3. 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 van het gebruikte SQL-dialect.

VOORBEELD 9.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; 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. 6.1

PRIMARY KEY-CONSTRAINTS

In deze paragraaf wijden we nog een enkel woord aan primary keyconstraints. Primaire sleutel en not null

Een smalle kandidaatsleutel mag  als rij-identifcatie  geen null’s bevatten. Bij een brede kandidaatsleutel is daar conceptueel niets op tegen, zolang een rij niet over de hele breedte van de sleutel null’s bevat. Dit geldt dus in het bijzonder voor primaire sleutels. Vrijwel alle rdbms’en zijn echter Codd-relationeel (zie paragraaf 2 van leereenheid 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 laatste niet het geval.

Geen sleutels met een blob

Kolommen van datatype blob kunnen geen deel uitmaken van een primary key-constraint en evenmin van een foreign key- of uniqueconstraint. Dit is niet zo vreemd: vanwege hun aard (plaatjes, videoof geluidsfragmenten, tekstdocumenten, ...) zijn uniciteits- en gelijkheidscontroles problematisch en niet voor de hand liggend. 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. Refererendeactieregel

Refererende-actieregels drukken uit wat moet gebeuren als de referentiëleintegriteitsregel 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 (de nullifying variant hebben we niet behandeld). 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ële-integriteitsregel 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 6.3 van leereenheid 8. 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 uniqueconstraint 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 4.1 (zonder constraintnaam) en in paragraaf 4.2 (met constraintnaam). 6.4

CHECK-CONSTRAINTS

Voor aanvullende voorwaarden is er de check-constraint. We kunnen deze o.a. gebruiken voor waardebeperkingen. VOORBEELD 9.4 (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 1 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. 2 De derde check-constraint dwingt af dat een hemelobject geen satelliet is van zichzelf. 3 Bij alle drie de check-constraints wordt unknown als waarde van de conditie geaccepteerd.

Let op

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 9.4 ervoor zorgt dat niet-ingevulde waarden voor afstand, diameter of moederobject worden geaccepteerd. We hoeven de checkconditie 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 (zie ook de hierna volgende opgaven). Wie de mogelijkheden hiervan verder 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 (leereenheid 12) krijgen we een krachtiger middel in handen tot regelhandhaving.

OPGAVE 9.7

Een not null-constraint kan 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 (zie paragraaf 4.2) aan om 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 9.8

Bestudeer het script RuimtereisbureauCreate.sql waarin de behandelde check-constraints en nog enkele andere zijn opgenomen. OPGAVE 9.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 deelnemer 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 leereenheid 12 ‘Triggers en stored procedures’.) 7

Domeinen

In Inleiding informatica hebben we 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 bewaakt worden. Ook de ontwikkelomgeving van een database- en applicatieontwikkelaar is een transactionele omgeving. Want zolang de ontwikkelaar bezig is, verandert er voortdurend van alles aan de structuren. In deze paragraaf zullen we laten zien dat die structuren, met name de tabelstructuren, veelal redundante specificaties bevatten en zullen we zien hoe domeinen kunnen helpen die redundantie terug te dringen. 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. Domein

Logisch datatype

Domein: ‘single point of definition’ voor kolomkenmerken

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.

7.2

create domain

Implementatiemodel RuimtereisbureauD

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 SQL-omgeving). create domain Transportcode create domain Transportomschrijving create domain Reisnr check (value > 0); create domain Objectnaam create domain Aantal check (value > 0); create domain Geldbedrag

as varchar(2); as varchar(12); as integer as varchar(10); as integer as 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 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 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 defaultspecificatie 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. 7.3

value

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 9.10

Test de check-constraint op domein Reisnr: probeer een nieuwe reis in te voeren met een niet-positief reisnummer (in RuimtereisbureauD). 7.4 Verwijderen van domeinen en referentiële integriteit

drop domain

alter domain

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.

8

View

Views

In leereenheid 7 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 deze leereenheid alle typen dataobject de revue laten passeren, zullen we ook de levenscyclus van een view hier met een voorbeeld illustreren. 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

create view vReis1 (nr, aantal_deelnemers) as select R.nr, count(D.reis) from Reis R left outer join Deelnemer 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

Views en order by

Opmerking: desgewenst mag de select-expressie in een viewdefinitie een order by-clausule bevatten. 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 9.5 (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 ============ 31 32 33 34 35 36 37

VERTREKDATUM PRIJS AANTAL_DEELNEMERS AANTAL_BEZOEKEN ============ ============ ================= =============== 2022-01-12 2.50 4 1 2022-06-03 17.50 3 5 2022-10-12 2.65 3 1 2023-01-10 75.00 3 3 2023-03-12 16.50 0 3 2023-06-27

0 3 2023-07-17 60.00 0 1

Overigens kunt u ook een view maken met beide aantallen (hoe?), zodat u maar één view nodig hebt. 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. OPGAVE 9.11

Geef de query waarop de laatste alinea van voorbeeld 9.5 doelt. 8.3

drop view

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. 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. 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.

Sequence

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. 9.2

create sequence

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;

next value for …

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 zo’n aanroep in elk type SQL-statement voorkomen. 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. alter sequence

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 insertstatement.

VOORBEELD 9.6 (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, 'Michelle', '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 8.6 worden toegepast.  de optie on update cascade van de verwijssleutel fk_deelname_door_klant, waardoor de bestaande verwijzingen in Deelnemer automatisch worden bijgewerkt. auto_increment

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. 9.3

drop sequence

alter sequence

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 9.12

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. SAMENVATTING Paragraaf 2

De DDL-subtaal van SQL bevat commando’s voor het definiëren ( create) of verwijderen (drop) van databaseobjecten en het wijzigen van hun structuur (alter). Databaseobjecten zijn er van velerlei typen. Naast tabellen zijn er domeinen (een soort logische gegevenstypen), views (opgeslagen selectstatements met een naam), sequences (objecten die een volgnummer kunnen genereren), triggers (3GL-programmaatjes die automatisch actief worden bij bepaalde wijzigingen van de database-inhoud of pogingen tot wijziging), stored procedures (eveneens 3GL-programmaatjes die worden uitgevoerd bij een expliciete aanroep), indexen (toegangsstructuren ter verhoging van de performance), gebruikers (bepaald door gebruikersnaam en wachtwoord) en rollen (‘bundeltjes’ privileges). Tabellen zijn er in twee typen: tabellen met gebruikersinformatie en tabellen met informatie over databaseobjecten (meta-informatie). De laatste vormen samen de data dictionary of systeemcatalogus. Een DDL-statement wordt in het algemeen niet meteen automatisch gecommit. In bepaalde dialecten of omgevingen, of met bepaalde instellingen, kan dit wel het geval zijn. Structuurwijzigingen worden, wisselend per SQL-dialect, soms slecht ondersteund. Voor sommige ‘deltaproblemen’ kan daarom een complexe omweg noodzakelijk zijn.

Paragraaf 3

Een database wordt gecreëerd met een create database-statement en verwijderd met drop database. Een sessie is de periode dat één gebruiker vanuit één instantie van een clientapplicatie een connectie (verbinding) met een database onderhoudt. Een sessie beginnen (inloggen) gaat in Firebird met het commando connect (of automatisch na het creëren van de database).

Paragraaf 4

Een tabel wordt gecreëerd met een create table-statement. Zo’n statement bevat kolomdefinities en constraintdefinities. Constraints (onder meer voor primaire of verwijssleutels) kunnen een naam krijgen door het gereserveerde woord constraint gevolgd door de constraintnaam. Het commando om een tabel te verwijderen, luidt: drop table. Of een poging slaagt, hangt onder meer af van referentiële integriteit. Structuurwijziging van een tabel (inclusief naamwijziging of wijziging van constraints) kan soms worden bewerkstelligd met het commando alter table.

Paragraaf 5

Een kolomdefinitie (onderdeel van een create table-statement) bevat verplicht een kolomnaam en een datatype, en optioneel een not nullen/of default-specificatie. Het datatype hierin is een extern datatype, te onderscheiden van de door het rdbms gebruikte interne datatypen.

Paragraaf 6

Er zijn de volgende typen constraints: primary key-, foreign key-, uniqueen check-constraints. Een unique-constraint legt, samen met een not null, een alternatieve sleutel vast. Met een check-constraint kan een conditie worden vastgelegd die wordt gecontroleerd bij een insert of een update.

Paragraaf 7

Het begrip domein komt tegemoet aan de behoefte aan een ‘single point of definition’ voor kenmerken die bepaalde kolommen a priori gemeenschappelijk hebben (in het bijzonder het datatype, zoals bij een primaire sleutel en de corresponderende verwijssleutels).

Paragraaf 8

Een view is een select-statement dat onder een eigen naam wordt bewaard. Views mogen in select-statements overal worden gebruikt waar een databasetabel is toegestaan. Views kunnen worden gebruikt ter oplossing van deelproblemen van een complex probleem (als zodanig ook om syntaxbeperkingen van SQL te omzeilen). Een belangrijke toepassing is het verstrekken van rechten ‘op maat’ aan gebruikers.

Paragraaf 9

Met een sequence kunnen automatisch volgnummers worden gegenereerd. De sequence ‘onthoudt’ waar hij gebleven was. In de praktijk wordt voor elke kunstmatige sleutel een sequence gedefinieerd. Deze wordt aangeroepen vanuit een insert-statement (soms ook bij een update van een primaire sleutel).

ZELFTOETS

Opgaven 1 en 2 zijn gebaseerd op Orderdatabase, opgave 3 op OrderdatabaseC. We herhalen hier het strokendiagram. De voorbeeldpopulatie vindt u achterin.

FIGUUR 9.5

Strokendiagram Orderdatabase

1

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?)

2

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 leereenheid 17), 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.

3

Ga uit van OrderdatabaseC (de versie met constraintnamen). Ter voorbereiding van een fusie moet een structuurwijziging worden doorgevoerd. Zie figuur 9.6 voor het strokendiagram van de oude situatie, die correspondeert met de installatieversie vanuit de SQLomgeving.

FIGUUR 9.6

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 9.7 voor het strokendiagram van de nieuwe situatie.

FIGUUR 9.7

Raadpleeg tijdig de uitwerking!

OrderdatabaseC na structuurwijziging

Aanwijzingen – Voorafgaand aan elk drop table- of alter table ... drop-statement moet de daaraan voorafgaande transactie gecommit worden. – 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.

TERUGKOPPELING 1

9.1

Uitwerking van de opgaven

Creëren testdatabase (in de handleiding bij de SQL-omgeving op de cursussite staat waar u deze code precies moet intikken): create database 'Testdatabase.fdb' user 'Sysdba' password 'masterkey';

Een testtabel creëren kan bijvoorbeeld zo: create table Testtabel( testkolom integer not null );

9.2

Zie de handleiding bij de SQL-omgeving op de cursussite.

9.3

Na het beëindigen van de huidige sessie (zie vorige opgave) past u de naam aan via het besturingssysteem van de computer; zie de handleiding op de cursussite.

9.4

Verwijderen van Testbase.fdb (mits dit de actieve database is): drop database;

Alternatief: verwijderen via het besturingssysteem. 9.5

Het volgende stappenplan volgt min of meer de onderdelen a en b van de opgave: a Kolom Reis.transport moet vervoer gaan heten: 1 Verwijder foreign key fk_reis_met_transport 2 Maak nieuwe kolom Reis.vervoer 3 Kopieer inhoud van Reis.transport naar Reis.vervoer 4 Verwijder kolom Reis.transport b Tabel Transport moet Vervoer gaan heten: 1 Maak nieuwe tabel Vervoer 2 Hevel inhoud van Transport over naar Vervoer 3 Verwijder tabel Transport c Breng de in stap 1a verwijderde foreign key opnieuw aan (aangepast). Een script hierbij: --a alter table Reis drop constraint fk_reis_met_transport; alter table Reis add vervoer varchar(2) not null; update Reis set vervoer = transport; alter table Reis drop transport; commit; --zie de handleiding

--b create table Vervoer (code varchar(2) not omschrijving varchar(12) not constraint pk_vervoer primary key constraint un_vervoeromschrijving ); insert into Vervoer select * from Transport; commit; --zie de handleiding drop table Transport;

null, null, (code), unique (omschrijving)

--c alter table Reis add constraint fk_reis_met_vervoer foreign key (vervoer) references Vervoer(code);

Opmerking: het hernoemen van kolommen hebben we vermeden, hoewel dat in Firebird in principe mogelijk is. Het stuit echter snel op problemen met ‘afhankelijkheden’ in de data dictionary. 9.6

De oplossing is: verwijder de verwijzende constraint, hoog in Deelnemer en in Klant ‘ongestoord’ de nummers op en maak de constraint weer aan: alter table Deelnemer drop constraint fk_deelname_door_klant;

Bij een foutmelding: voer eerst een reconnect uit. update Klant set nr = nr + 1000; update Deelnemer set klant = klant + 1000; commit; alter table Deelnemer add constraint fk_deelname_door_klant foreign key (klant) references Klant(nr) on update cascade;

Eenvoudiger zou zijn de constraint tijdelijk buiten werking te stellen (disable) en later weer actief te maken (enable). Dit wordt echter niet ondersteund. 9.7

Het aangepaste create table-statement van Reis luidt aldus: 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, constraint ch_nr check (nr between 1 and 2000), constraint ch_duur check (not(prijs is not null and duur is null)) );

Toelichting – De check-constraint ch_nr controleert of het reisnummer, indien ingevuld, in het aangegeven interval ligt. – De tweede check-constraint luidt, vrij vertaald: check (not verboden toestand). Een rij verkeert in de verboden toestand wanneer zijn prijs bekend is, maar zijn reisduur niet. Ga na dat het ook wat korter kan: check (prijs is null or duur is not null). 9.8

Geen uitwerking.

9.9

We voegen de check-constraint toe via het volgende alter tablecommando: alter table Deelnemer add constraint ch_maximaal_aantal_deelnemers check (4 > (select count(*) from Deelnemer where reis = new.reis));

Toelichting: Deze oplossing maakt gebruik van de contextvariabele new, waarmee de nieuwe rij (na update of insert) wordt aangeduid die de oorzaak is dat de check wordt uitgevoerd. De contextvariabele new en zijn tegenhanger old worden behandeld in leereenheid 12. Proberen we hierna aan reis 31 een vijfde deelnemer toe te voegen: insert into Deelnemer values (31, 125);

dan resulteert dit in een foutmelding: Operation violates CHECK constraint CH_MAXIMAAL_AANTAL_DEELNEMERS on view or table DEELNEMER -At trigger 'CHECK_23'.

Het kan ook zonder new: alter table Deelnemer add constraint ch_maximaal_aantal_deelnemers check (4 > (select count(*) from Deelnemer D where D.reis = Deelnemer.reis));

Toelichting: Deelnemer.reis is gecorreleerd aan ‘Deelnemer’ in de eerste regel en is het reisnummer van de ‘reis in kwestie’. Voor de brontabel van de select-expressie moeten we nu, ten onderscheid, een alias gebruiken (D). 9.10

Geen uitwerking.

9.11

De bedoelde query is: select nr, vertrekdatum, prijs, (select count(*) from Deelnemer where reis = nr), (select count(*) from Bezoek where reis = nr) from Reis;

9.12

a

SQL-statements:

create sequence sReisnr; alter sequence sReisnr restart with 100;

b Uitgaande van de oude tabelnaam Reis: update Reis set nr = next value for sReisnr;

(Controleren in Reis, Bezoek en Deelnemer.) 2

1

Uitwerking van de zelftoets

SQL-statement:

alter table Artikel add constraint ch_verkoopprijs check (verkoopprijs > inkoopprijs);

2

a Een eenvoudige manier om een constraintnaam te achterhalen, is bewuste overtreding van de constraint. De foutmelding bevat de constraintnaam. Er is een nóg eenvoudiger manier; kijk daarvoor in de handleiding van de SQL-omgeving op de cursussite. b We zoeken de naam op door een foutmelding te forceren, bijvoorbeeld door een update waarbij we proberen een artikel een nietbestaande artikelgroep te geven. Vervolgens verwijderen we die constraint: alter table Artikel drop constraint INTEG_21; Het kan zijn dat bij u de constraint een ander volgnummer heeft. De hoofdletters ‘INTEG’ mogen ook kleine letters zijn; deze worden, net als kleine letters in bijvoorbeeld tabel- en kolomnamen, intern in hoofdletters omgezet.

c De actie uit onderdeel a moet nu wel lukken! Na controle (en een rollback) voegt u de verwijssleutel weer toe: alter table Artikel add foreign key (artikelgroep) references Artikelgroep(code);

3

Knelpunten Omdat er geen rename-commando is, is een nieuwe tabel Client onvermijdelijk. Voor het toevoegen van voorletters en eventueel een voorvoegsel aan de oude klantnamen zijn er meerdere mogelijkheden. Bijvoorbeeld: 1 Klant laten zoals die is en, na overzetten van de populatie, de updates op voorletters en voorvoegsel uitvoeren in Client 2 de correcte en volledige namen van de oude klanten eerst invoeren in Klant en dan pas de populatie overzetten naar Client. Oplossing 1 stuit op het probleem van de verplichte kolom Client.voorletters: u kunt de populatie alléén overzetten als u ofwel deze kolom vult met tijdelijke waarden, ofwel deze kolom eerst optioneel maakt en achteraf, via een structuurwijziging, alsnog verplicht maakt. Bij oplossing 2 is een structuurwijzing onvermijdelijk, maar vermijden we het probleem van de verplichte voorletters. In het hierna volgende stappenplan is voor oplossing 1 gekozen. Een knelpunt is nog, zoals vaak, de recursieve verwijzing. U kunt immers geen klant toevoegen met een verwijzing naar een andere, nog niet toegevoegde klant. Het eenvoudigst is de aanbrenger-kolom van Client eerst te vullen en pas daarna de recursieve verwijzing toe te voegen. Stappenplan 1 Creëer Client, zonder de recursieve aanbrengerverwijzing. 2 Kopieer de populatie van Klant naar Client; vul daarbij de verplichte kolom Client.voorletters met een lege string. 3 Breng in Client de recursieve aanbrengerverwijzing aan. 4 Maak nieuwe kolom Order_.client. 5 Vul Order_.client vanuit Order_.klant en commit. 6 Leg een verwijssleutel van Order_.client naar Client.nr. 7 Verwijder de oude verwijssleutel van Order_ naar Klant. 8 Verwijder kolom Order_.klant. 9 Verwijder de recursieve verwijzing in Klant en daarna Klant zelf. Uitvoering stappenplan Een werkend script, genaamd OrderdatabaseCConversie.sql, vindt u in de SQL-omgeving. Merk op dat er voor Firebird een eenvoudiger stappenplan bestaat, dat gebruikmaakt van de mogelijkheid kolomnamen aan te passen. We kunnen de stappen 4 t/m 8 vervangen door drie stappen: – verwijder de verwijssleutel van Order_ naar Klant – hernoem de kolom Order_.klant naar Order_.client – leg een verwijssleutel van Order_.client naar Client.nr

Blok 5

Verdieping

Inhoud leereenheid 10

Query-optimalisatie 71

Introductie Leerkern 1 2

3

4 5

72

Voorbeelddatabase: GrootOrderdatabase 72 De optimizer 73 2.1 Stappen in het verwerken van een query 73 2.2 Performancefactoren 74 2.3 Hypothetische ‘conceptuele’ verwerking 74 2.4 Een verbeterd algoritme 75 2.5 Rule-based en cost-based optimizers 76 Indexen 77 3.1 Indexstructuur 77 3.2 Binair zoeken 79 3.3 Wanneer indexeren? 80 3.4 Creëren en verwijderen van een index 81 3.5 Unieke indexen 82 3.6 Standaardindexen 82 3.7 Queryplan 83 3.8 Verwerkingsstatistieken 83 3.9 Een index activeren en inactiveren 84 3.10 De selectiviteit van een index 85 3.11 Onderhoud van indexen 86 3.12 Tips om lange wachttijden te voorkomen 88 Performanceverbetering door query-aanpassing 89 Performanceverbetering door aanpassing databaseontwerp 5.1 De keuze van sleutels 91 5.2 Redundantie en constraints 93

Samenvatting Zelftoets

95

96

Terugkoppeling 1 2

98

Uitwerking van de opgaven 98 Uitwerking van de zelftoets 103

70

91

Leereenheid 10

Query-optimalisatie

INTRODUCTIE

In de voorgaande leereenheden hebben we vooral naar de logische opbouw van een query gekeken. In deze leereenheid 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-hoc-vragen via een SQL-omgeving zoals we in deze cursus gebruiken 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. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – oog hebt voor het belang, in de praktijk van grote databases, van query-optimalisatie – inzicht hebt in de rol van de optimizer – weet welke factoren van invloed kunnen zijn op de performance van een query – kunt uitleggen wat een index is en hoe en wanneer die gebruikt kan worden – weet hoe u in SQL een index creëert of verwijdert – queryplannen en query’s aan elkaar kunt relateren – inzicht hebt in het belang van performancemetingen en in mogelijkheden tot performanceverbetering, zoals het balanceren van indexen, het aanpassen van een query en het herzien van het databaseontwerp. Deze leereenheid heeft een studielast van 6 uur. Studeeraanwijzing

Ook in deze leereenheid is het van belang dat u dingen uitprobeert. Het gaat er daarbij om dat u een idee krijgt van het nut van de optimizer, en hoe deze te werk zou kunnen gaan. We verwachten niet dat u de optimizer volledig doorgrondt, of dat u bij iedere query kunt voorspellen welk queryplan de optimizer zal kiezen. Let erop dat u in de juiste voorbeelddatabase werkt: u moet nu GrootOrderdatabase hebben in plaats van de gewone Orderdatabase (zie paragraaf 1).

LEERKERN 1

Voorbeelddatabase: GrootOrderdatabase

We gebruiken een qua omvang uitgebreide versie van de Orderdatabase: GrootOrderdatabase. De structuur komt overeen met die van Orderdatabase (figuur 10.1).

FIGUUR 10.1

Strokendiagram GrootOrderdatabase

De omvang van GrootOrderdatabase is echter veel groter dan die van de Orderdatabase die we kennen uit eerdere leereenheden (tabel 10.1). TABEL 10.1 tabel Klant Order_ Artikelgroep Artikel

Recordaantallen in GrootOrderdatabase aantal records 32.002 160.665 3 202

tabel Kortingsinterval Orderregel Klacht

aantal records 4 1.557.457 7.784

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.

Record Veld

Records en velden We zullen het in deze leereenheid 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. 2

De optimizer

In leereenheid 6 ‘Statistische informatie’ 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. 2.1

Interpreter Parser

Optimizer

Performance

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 10.2.

FIGUUR 10.2 2.2

Stappen in het verwerken van een query

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. 2.3

HYPOTHETISCHE ‘CONCEPTUELE’ VERWERKING

Stel, we willen een overzicht met de omschrijvingen van artikelen die geen artikelgroep hebben met het bijbehorende totaalbedrag van de 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 10.3.

FIGUUR 10.3

Navigatiepad bij voorbeeldquery

Oplossing: select from

A.omschrijving, sum(Orl.bedrag) totaal 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; Hypothetische verwerking volgens het conceptuele algoritme

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 10.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  1013 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1016 = 1,0104 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. 2.4 Verbetering 1: eerst recordselectie per tabel

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  102 terabyte. De artikelgroepvoorwaarde geldt voor tabel Artikel. Er zijn 28 artikelen zonder artikelgroep.

Verbetering 2: stap voor stap joinen

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 de Orderregel-tabel. 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_. Het resultaat bedraagt 1760 records. De aantallen records die (min of meer) gelijktijdig in het geheugen worden bewerkt, zijn hiermee aanvaardbaar geworden. Queryplan

Verdere verbeteringen: onderdeel van queryplan

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 3. 2.5

Rule-based optimizer

Full table scan

Cost-based optimizer

RULE-BASED EN COST-BASED OPTIMIZERS

Rule-based optimizer 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 – en zoals we hierboven zagen kan dat afwijken van het door ons bedachte logische navigatiepad. Cost-based optimizer 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. Raadpleeg voor informatie over de Firebird optimizer de Optimizer guide in de documentatie.

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. 3

Index

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. Daarmee bedoelen we twee dingen. Ten eerste: het gaat erom dat u begrijpt wat het nut van een index kan zijn, en wanneer het handig kan zijn om bij een kolom(combinatie) een index aan te maken. Het is voor ons niet van belang of dat een lineaire-lijstindex is of een binaire-boomindex; we gebruiken die twee voorbeelden alleen om het nut van een index te kunnen uitleggen. Ten tweede: de werkelijke indexstructuren en hun zoekalgoritmen zijn ingewikkelder én slimmer dan de varianten die wij beschrijven. 3.1

Lineaire-lijstindex

INDEXSTRUCTUUR

We bekijken een voorbeeld van een index, bedoeld voor het sorteren van Order_-records op het veld klant, zie figuur 10.4. Deze index is een gesorteerde lijst van verschillende klantnummers, een zogenaamde lineaire lijst. Ieder klantnummer 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 10.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 tussenliggende records van tabel Order_ hebben we niet weergegeven.

Binaire-boomindex

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 voorbij het punt bent waar hij had moeten staan). Indexen worden daarom meestal opgeslagen in een boomvormige structuur; deze leent zich goed om razendsnel te zoeken. In figuur 10.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 lineaire-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 rechtsboven, vindt u de records op volgorde van klantnummer. De volgorde van records met gelijk klantnummer is ook nu willekeurig. De structuur van figuur 10.5 wordt een binaire zoekboom genoemd. 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 10.5 3.2

Binaire (zoek)boom van klantnummers (fragment)

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).

Logaritmisch zoeken

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) 2log van het aantal pagina’s. De 2log van een getal is de exponent, wanneer je dat getal als 2-macht schrijft. Bijvoorbeeld: 2log1024 = 10, want 210 = 1024. 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 voor wel/niet creëren van index

Optimizer kiest

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).

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.

create index drop index

VOORBEELD 10.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 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 10.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 10.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 10.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);

create index computed by (expression)

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. 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.

Unieke index VOORBEELD 10.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); 3.6

Standaardindex

STANDAARDINDEXEN

Vrijwel alle databases maken gebruik van standaardindexen. 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_). Als de constraint waarbij een standaardindex wordt aangemaakt bij creatie een naam heeft gekregen, krijgt die index dezelfde naam. Heeft de constraint geen naam gekregen, dan krijgt de bijbehorende index een door het rdbms gegenereerde naam, zoals RDB$FOREIGN23. 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 vaak 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 op die verwijssleutel versnelt het zoeken aanzienlijk. 3.7

set plan on / off

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

set stats on / off

QUERYPLAN

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 stats on zorgt dat bij elk queryresultaat statistische gegevens worden verstrekt, waarvan vooral de tijd ons interesseert. 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.

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). alter index ... (in)active

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 10.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 stats on; select count(*) from orderregel where aantal > 50;

Resultaat (op ons systeem): PLAN (ORDERREGEL NATURAL) COUNT ============ 62357 Current memory = 9089304 Delta memory = 104240 Max memory = 9160088 Elapsed time= 1.12 sec Cpu = 0.00 sec Buffers = 2048 Reads = 25206 Writes = 0 Fetches = 3165578

NATURAL

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 de SQL-omgeving gemeten, niet door Firebird. Dit betekent dat eventuele vertraging die de query-uitvoering oploopt door andere processen (zoals printen en activiteiten van andere gebruikers) meetelt in de meting. 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 Current memory = 9852216 Delta memory = 629072 Max memory = 59526872 Elapsed time= 0.14 sec Cpu = 0.00 sec Buffers = 2048 Reads = 23121 Writes = 0 Fetches = 124832

Het queryplan vertelt ons nu dat tabel Orderregel 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! Eerlijkheidshalve moeten we wel vermelden dat het aanmaken van de index ook tijd kostte: bij ons zelfs 4.28 sec. Dit heeft dus alleen zin als de index vaak gebruikt wordt. 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. Selectiviteit van index

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 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. 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 10.6 (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 10.6

Slecht gebalanceerde indexboom

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

Gebalanceerde index

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. Het rdbms moet 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 10.1

Balanceer de zoekboom van figuur 10.6, 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. Twee manieren van balanceren

Balanceren door inactiveren en activeren

Backup Restore

Databasepage Defragmentatie

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’ betekent hier iets anders dan bestandskopie

‘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. 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’.

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 planonly on te gebruiken. Als de query echt uitgevoerd wordt, is het goed om het aantal records van het resultaat te beperken. 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 rows in een query. Door n te verhogen krijgt u een idee of het zinvol is de query ook zonder rows uit te voeren.

Rows

Een voorbeeld van het gebruik van rows: select * from Order_ rows 100; rows kan nog worden uitgebreid met to . Een voorbeeld: select * from Order_ rows 501 to 505;

Nu worden de records met nummers 501 t/m 505 getoond. Verwijder, voordat u de volgende opgaven maakt, de extra indexen die u in deze leereenheid hebt aangemaakt. De indexen uit de voorbeelden zijn: iKlant, iDatumKlant, iOrder_Volgnr, iAantal en iOrder_Artikel. OPGAVE 10.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. n=

10

50

100

500

1000

5000

10000

zonder index met index

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 10.3

Bij deze opgave gaat het om het vergelijken van verschillende queryplannen. Kunt u eventuele verschillen en overeenkomsten verklaren? Gebruik simpele expressies met select *. Zorg ervoor dat u set plan on of set planonly on gebruikt. Vergelijk de volgende (inner) joins: a Artikel en Artikelgroep b Orderregel en Klacht c Order_ en Klant. OPGAVE 10.4

We willen de gegevens zien van de laatste 10 orderregels, met de gegevens van de laatste orderregel van de laatste order 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? 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 10.6

In leereenheid 7 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 subselect-met-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 from

O.nr, O.datum, count(Orl.volgnr) aantal 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 (ORL NATURAL, O INDEX (PK_ORDER)))

Op ons systeem was 15.68 s nodig om het resultaat te berekenen. We zien: de join start vanuit Orderregel (in dit geval dus niet de kleinste van de twee tabellen). Van daaruit worden de bijbehorende rijen van Order_ gezocht. Hiervoor wordt de index op de primaire sleutel van Order_ 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 10.33 s voor nodig. 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 subselect-oplossing 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 in het algemeen geen join gebruiken. Wel is dan not exists een alternatief en ook een left outer join is dan te overwegen. In opgave 10.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 rows. Zie verder de handleiding bij de SQL-omgeving op de cursussite.

Nut / onnut van optimalisatietips

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 10.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. 5

Performanceverbetering door aanpassing databaseontwerp

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. 5.1 Drie mogelijkheden voor keuze sleutels

DE KEUZE VAN SLEUTELS

Bij de keuze van sleutels hebben we maar één keer wat langer stilgestaan: bij het Toetjesboek in Inleiding Informatica. We hebben daar twee varianten bekeken: zonder en met kunstmatige sleutels. Voor de keuze van sleutels bestaan in het algemeen drie mogelijkheden: 1 2 3

volledige namen codes 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 10.7 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 10.7

Drie varianten voor de keuze van een sleutel

Afweging

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 10.7a. 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 10.7b. 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 10.7c. 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 voeren we nu een experiment uit met een gewijzigde versie van de GrootOrderdatabase: GrootOrderdatabaseKS. Deze heeft een extra kunstmatige sleutel, in tabel Orderregel. Zie figuur 10.8 en opgave 10.6.

FIGUUR 10.8

GrootOrderdatabaseKS

OPGAVE 10.6

In opgave 10.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 GrootOrderdatabaseKS. Is er sprake van snelheidswinst? 5.2 Performanceverbetering door gecontroleerde redundantie

REDUNDANTIE EN CONSTRAINTS

Het principe van ‘single point of definition’ leidde in Inleiding informatica 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 10.9b geeft een ander voorbeeld van een redundante structuur. Tabel Artikelgroep (zie figuur 10.9a) bevat slechts drie records. Daarom kunnen we de gegevens van tabel Artikelgroep ook redundant opnemen in tabel Artikel (zie figuur 10.9b).

FIGUUR 10.9 Denormalisatie in historische databases

Datawarehouse

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 1 van leereenheid 8 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.

Online transactional processing

Transactioneel bedrijfssysteem

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 select-query’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.

Datawarehouse

SAMENVATTING Paragraaf 1

In de context van meer technische onderwerpen, zoals performance, worden rijen vaak aangeduid met de wat meer fysiek georiënteerde term records en kolommen of kolomwaarden met velden.

Paragraaf 2

Na controle door de parser op syntactische correctheid, wordt een query onder handen genomen door de optimizer, die probeert een verwerkingsalgoritme te genereren met optimale performance (snelheid) en geheugengebruik. Dit verwerkingsalgoritme is gebaseerd op een queryplan. Het queryplan bepaalt bijvoorbeeld hoe een eventuele join wordt afgewikkeld.

Paragraaf 3

Indexen (interne gegevensstructuren voor snelle toegang tot records) vormen een van de middelen voor de optimizer om de performance te verbeteren. Bij snelle toegang moet worden gedacht aan zoekprocessen of benadering van records in een bepaalde sorteervolgorde. Op primaire en verwijssleutels wordt automatisch een standaardindex aangemaakt. Door de programmeur (of database administrator) kunnen indexen op andere kolommen (of kolomcombinaties) worden aangemaakt. In principe bepaalt de optimizer of een index wordt gebruikt als onderdeel van het queryplan. Wordt geen index gebruikt, dan wordt de tabel in kwestie rij voor rij doorlopen (een full table scan). De lineaire-lijstindex en de (meestal snellere) binaire-boomindex (bomen met twee takken per knooppunt) zijn voorbeelden van gegevensstructuren waarmee een index intern kan worden gerealiseerd. In de praktijk worden vaak bomen gebruikt met meer dan twee takken per knooppunt, die niet al te diep zijn. Omdat het bijhouden van een index tijd kost en optimizers soms verkeerde keuzes maken, is het nut van een index niet bij voorbaat zeker. Metingen moeten uitwijzen of er echt sprake is van winst.

Een unieke index heeft als neveneffect een uniciteitscontrole. Een van de (statistische) gegevens op grond waarvan de optimizer bepaalt of een index wordt gebruikt, is de selectiviteit van de index. Deze is hoog wanneer de index relatief veel verschillende waarden bevat. Een index met lage selectiviteit is doorgaans weinig zinvol. Voor goede prestaties moet een indexboom (min of meer) gebalanceerd zijn, wat wil zeggen dat de takken ongeveer even diep zijn. Bij scheefgegroeide bomen kan onderhoud noodzakelijk zijn, bijvoorbeeld door de index inactief te maken en opnieuw te activeren. Paragraaf 4

Bij slechte performance van een query kan het nuttig zijn op zoek te gaan naar een equivalente query die het beter doet. Lijstjes van wat goed is en wat niet, zijn vaak dialect- of zelfs versiespecifiek.

Paragraaf 5

Aanpassing van het (technisch) databaseontwerp kan soms de performance van query’s ten goede komen. Bijvoorbeeld de bewuste introductie van (bewaakte) redundantie. Tot de mogelijkheden behoren: het opslaan van statistische gegevens en denormaliseren van de tabelstructuur. Ook de keuze van sleutels (hun datatype en wel of geen kunstmatige sleutels introduceren) kan van grote invloed zijn.

ZELFTOETS De volgende opgaven zijn gebaseerd op GrootOrderdatabase, zie figuur 10.1.

1

Gegeven is de volgende select-query (die per artikel de artikelgroep en het aantal niet-behandelde klachten ophaalt): 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?

2

De query van opgave 1 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. 3

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, from Artikel A join Artikelgroep Ag join Orderregel Orl join Klacht Kht

Ag.omschrijving, count(*) on A.artikelgroep = Ag.code on A.nr = Orl.artikel 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. 4

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

TERUGKOPPELING 1

10.1

Uitwerking van de opgaven

Er zijn meerdere goede oplossingen. Dit is er een (zie figuur 10.10).

FIGUUR 10.10

10.2

Mogelijke oplossing indexboom

Figuur 10.11 geeft de resultaten op ons systeem grafisch weer.

FIGUUR 10.11

Tabel met grafiek

Zonder index wordt de gehele tabel Orderregel doorlopen, wordt van ieder record nagegaan of bedrag groter is dan n en zo ja dan wordt het aantal met 1 opgehoogd. Het doorlopen van de tabel en het testen of bedrag aan de gestelde voorwaarde voldoet is onafhankelijk van de waarde van n. Omdat er bij grote n minder records voldoen, zal het bijwerken van het aantal wat sneller gaan. Dit verklaart het langzaam afnemen van de benodigde tijd, zoals te zien in de gegevens van reeks 1. Bij reeks 2 worden dankzij de index snel de records gevonden die voldoen aan de voorwaarde. Maar daarna moeten de gevonden records sequentieel worden doorlopen om het aantal te bepalen. Dit kost iets meer tijd dan het doorlopen van de tabel zonder index omdat bij iedere waarde van de index het bijbehorende record gezocht moet worden. Bij bedrag > 10 kost dit iets meer tijd dan in het geval zonder index: dit punt van de grafiek van reeks 2 ligt boven dat van reeks 1. Voor hogere waarden van n is de versnelling door het gebruik van de index iBedrag zeer goed merkbaar. 10.3

a

We krijgen :

select * from Artikel A join Artikelgroep G on A.artikelgroep = G.code;

Het plan hiervoor luidt : PLAN JOIN (G NATURAL, A INDEX (FK_ARTIKEL_BIJ_ARTIKELGROEP))

Figuur 10.12 toont de betrokken tabellen. Ter herinnering: Artikelgroep heeft 3 records, Artikel heeft er 202.

FIGUUR 10.12

Blijkbaar heeft de optimizer er hier voor gekozen om te starten met de kleinste tabel, waardoor het een join van ouder naar kind wordt. De oudertabel wordt middels een full table scan afgelopen (G NATURAL), en de bijbehorende kindrijen (artikelen) worden gezocht met behulp van de standaardindex op de verwijssleutel van Artikel naar Artikelgroep.

b De volgende query luidt: select from

* Orderregel Orl join Klacht Kt on Orl.order_ = Kt.order_ and Orl.volgnr = Kt.volgnr ;

met als queryplan : PLAN JOIN (KT NATURAL, ORL INDEX (PK_ORDERREGEL))

Figuur 10.13 toont de betrokken tabellen, met hun aantallen records.

FIGUUR 10.13

Ook hier was de optimale strategie blijkbaar starten met de kleinste tabel. Omdat dat in dit geval een join van kind naar ouder oplevert, wordt de standaardindex op de primaire sleutel van Orderregel gebruikt om de Orderregel-informatie bij een Klacht op te zoeken. c

De laatste query wordt:

select * from Order_ O join Klant K on O.klant = K.nr;

Het queryplan hierbij is: PLAN JOIN (O NATURAL, K INDEX (PK_KLANT))

En het relevante stukje strokendiagram, met de aantallen records:

FIGUUR 10.14

Kennelijk heeft de optimizer in dit geval bepaald dat het gunstiger is om in Order_ te beginnen en die tabel met een full table scan te doorlopen, ondanks de grootte van de tabel.

10.4

De query: select from order by rows

* Orderregel order_ desc, volgnr desc 10;

Op ons systeem was de benodigde tijd 2.40 s. Deze ‘lange’ tijd wordt veroorzaakt doordat het gehele bestand eerst gesorteerd moet worden. Het gaat sneller als we een index maken op basis van de sorteervolgorde: create desc index iNr2 on Orderregel(order_, volgnr);

Voor de sortering wordt index iNr2 gebruikt, wat resulteert in een zeer snelle uitvoering van de query: 0.00 s. 10.5

We krijgen achtereenvolgens: --a select count(*) from Orderregel Orl where Orl.order_|| '/' || Orl.volgnr not in (select K.order_|| '/' || K.volgnr from Klacht K);

Het queryplan hierbij is: PLAN (KLACHT NATURAL) PLAN (KLACHT NATURAL) PLAN (ORL NATURAL)

Deze query duurt veel te lang; we moeten de uitvoering afbreken (zie de handleiding van de SQL-omgeving). Redenen waarom het zo lang duurt:  er kan geen index gebruikt worden, omdat we zoeken op het resultaat van een functie (concatenatie)  voor iedere rij van Orderregel én voor iedere rij van Klacht moet die concatenatie uitgevoerd worden. --b select count(*) from Orderregel Orl where not exists (select * from Klacht where order_ = Orl.order_ and volgnr = Orl.volgnr );

Hierbij is het queryplan: PLAN (KLACHT INDEX (FK_KLACHT_BIJ_ORDER_REGEL)) PLAN (ORL NATURAL)

Op ons systeem duurde deze query 4.24 s.

Het alternatief met een left outer join: --c select count(*) from Orderregel Orl left outer join Klacht K on Orl.order_ = K.order_ and Orl.volgnr = K.volgnr where behandeld is null;

Het queryplan is: PLAN JOIN (ORL NATURAL, K INDEX (FK_KLACHT_BIJ_ORDER_REGEL))

Deze query nam 4.06 s in beslag. 10.6

We krijgen nu: select from where

count(*) Orderregel nr not in (select orderregel from Klacht);

De originele query duurde uren, deze versie 11.23 s. In het queryplan zien we de reden: PLAN (KLACHT NATURAL) PLAN (KLACHT INDEX (FK_KLACHT_BIJ_ORDER_REGEL, FK_KLACHT_BIJ_ORDER_REGEL)) PLAN (ORDERREGEL NATURAL)

De tweede query wordt: select from where

count(*) Orderregel Orl not exists (select * from Klacht where orderregel = Orl.nr);

Benodigde tijd 4.89 s, weinig verschil met originele query. Het queryplan is hetzelfde als dat van de originele query. De laatste query met de left outer join wordt nu: select count(*) from Orderregel Orl left outer join Klacht K on Orl.nr = K.orderregel where behandeld is null;

Ook hier wordt geen tijdwinst geboekt ten opzichte van de originele query: 5.21 s. En ook hier is het queryplan gelijk aan dat van de originele query.

2

1

Uitwerking van de zelftoets

a Aantallen rijen: – start met Artikel: 200 – left outer join met Artikelgroep: 200 – join met orderregel: 1.500.000 – join met Klacht: 7500 – pas selectieconditie toe: 400 Het maximum aantal rijen is dus 1.500.000. b De volgende volgorde vergt aanzienlijk minder geheugen: – start met Klacht: 7500 – pas selectieconditie toe: 400 – join met Orderregel: 400 – join met Artikel: 400 – join met Artikelgroep: 400 Het maximum aantal rijen is nu 7500. In werkelijkheid kunnen deze aantallen wel wat verschillen, daar de rijen vanaf schijf per ‘page’ (een fysieke lees-/schrijfeenheid) worden gelezen. Bovendien hebben we alleen naar de aantallen rijen gekeken, terwijl de grootte van een rij (in bytes) ook een rol speelt.

2

Voor de duidelijkheid herhalen we het 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) ) )

Volgens dit queryplan worden er drie joins gemaakt:  van Artikel met Artikelgroep  het resultaat wordt gejoind met Orderregel  dit resultaat wordt weer gejoind met Klacht Het resultaat van de joins wordt gesorteerd (SORT) vanwege de group by: NR ========= 2 3 4 5 6 7 8 9 … 201 202

ARTIKEL ============ Bakwiel Bandklem Belbak Belbek Berkel Blaffel Bradel Bradelbak … Zwinzwalik Zwipstaart

ARTIKELGROEP COUNT ============ ========= niznomoeren 2 niznomoeren 3

4

2

1 niznomoeren 2 voegringen 1 voegringen 5 … … voegringen 2 voegringen 2

3

De conditie op de Artikel-tabel resulteert in slechts één rij. Dit maakt het voordelig daar het fysieke joinnavigatiepad te starten. Dit leidt tot het volgende fysieke pad en queryplan. Artikel (en daarin zoeken van artikel 152 via de primaire-sleutelindex)  Artikelgroep (gebruikmakend van de index op de primaire sleutel)  Orderregel (gebruikmakend van de index op de verwijssleutel)

 Klacht (gebruikmakend van de index op de verwijssleutel). Blijkbaar ‘vindt’ de optimizer de winst die met ons plan behaald kan worden niet significant, of ‘weet’ de optimizer een nog sneller plan, want dit is het door Firebird gekozen queryplan: PLAN SORT (JOIN (KHT ORL A AG

4

NATURAL, INDEX (UN_ORDERREGEL), INDEX (PK_ARTIKEL), INDEX (PK_ARTIKELGROEP))))

De tabel Artikelgroep is klein en zal dat altijd blijven. Zoeken en joinen met deze tabel zal daarom altijd snel gaan. Het argument dat zoeken en joinen bij numerieke sleutels doorgaans sneller gaat dan bij alfanumerieke, is daarom voor Artikelgroep niet van toepassing. De keuze voor codes is nu voordelig, mits deze betekenisvol zijn. Immers: bij betekenisvolle codes (bijvoorbeeld ‘NM’) zal het minder vaak nodig zijn om ook de artikelgroepomschrijving (‘niznomoeren’) op te halen (wat weer een join vraagt).

Inhoud leereenheid 11

Transacties en concurrency Introductie Leerkern 1 2

3

4

107 108

Transacties 108 Transactiemanagement 110 2.1 Concurrency 110 2.2 Concurrency control 111 2.3 Recovery 113 2.4 ACID 113 Gedistribueerde databases 114 2.5 Vier klassieke problemen 115 3.1 Lost update 115 3.2 Dirty read 116 3.3 Non-repeatable read 116 3.4 Phantom 117 Isolation levels 120 4.1 Isolation levels in de SQL-standaard 4.2 Isolation levels in Firebird 122

Samenvatting Zelftoets

128

129

Terugkoppeling 1 2

131

Uitwerking van de opgaven 131 Uitwerking van de zelftoets 134

106

120

Leereenheid 11

Transacties en concurrency

INTRODUCTIE

In paragraaf 1 van deze leereenheid pakken we de draad weer op van een verhaal dat we in eerdere leereenheden 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-niets-regel, atomiciteit, is een van de vier eisen van het zogenaamde ACID-model. In paragraaf 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 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 4 zien we dat een andere ACID-eis, isolation, hierbij een grote rol speelt. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u  weet wanneer en hoe een transactie wordt gestart, wanneer en op welke manieren een transactie kan worden beëindigd en wat dit voor gevolgen heeft – kunt uitleggen wat concurrency en concurrency control inhouden – de vier ACID-eigenschappen kunt beschrijven – vier klassieke problemen kunt noemen en herkennen die zich kunnen voordoen bij onvoldoende concurrency control tijdens het afhandelen van transacties, en deze kunt illustreren met voorbeelden – weet welke isolation levels de SQL-standaard voorschrijft, en hun relatie met de vier problemen kunt aangeven – kunt aangeven welke isolation levels in een gegeven situatie de beste keuze zijn en waarom. Deze leereenheid heeft een studielast van 6 uur. Studeeraanwijzing

De eerste drie paragrafen van deze leereenheid zijn theoretisch van aard. Pas in paragraaf 4 kunt u gaan experimenteren, en zoals gewoonlijk bevelen we dat van harte aan.

Voorbeelddatabase

De voorbeelddatabase bij de opgaven bij deze leereenheid is het Ruimtereisbureau. In figuur 11.1 herhalen we het strokendiagram. Kijk desgewenst achterin voor de voorbeeldpopulatie.

FIGUUR 11.1

Ruimtereisbureau: strokendiagram

LEERKERN 1

Transacties

We herhalen kort wat we weten over transacties en introduceren een eenvoudig Rekening-voorbeeld om de theorie uit te leggen. Transactie

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.

Transactielog

VOORBEELD 11.1 (transactie)

Een bankdatabase bevat een tabel Rekening, waarvan u het strokendiagram met een kleine populatie ziet in figuur 11.2.

FIGUUR 11.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. Dat samenvoegen in een transactie gaat makkelijker dan u misschien denkt: in Firebird wordt, in overeenstemming met de SQL-standaard, bij het begin van een sessie en na ieder commitmoment automatisch (impliciet) een transactie gestart. Zie figuur 11.3 voor het effect bij dat impliciete transactiemodel van een rollback: terugkeer naar de toestand direct na het laatste commitmoment.

FIGUUR 11.3 VOORBEELD 11.2 (bewerkingen op tabel Rekening bij impliciete transactiemodel)

Effect van een rollback

We nemen aan dat tabel Rekening precies de populatie van figuur 11.2 bevat en dat die gecommit is. 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 ====== 1508 1409

NAAM SALDO ====== ============ Boer 2004.00 Bakker 2007.00

Dat klopt, volgens het impliciete model was immers automatisch een transactie gestart, zonder dat we daar iets voor hoefden te doen.

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 4 terugzien. OPGAVE 11.1

Voer op het Ruimtereisbureau enkele experimenten uit analoog aan voorbeeld 11.2. Transacties en select-statements Bij het afbreken van een sessie zonder voorafgaande commit of rollback wordt soms de vraag gesteld of de lopende transactie wel of niet gecommit moet worden (zie de handleiding bij de SQL-omgeving). 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 van deze leereenheid DDL verder buiten beschouwing. 2

Transactiemanagement Transaction processing

Concurrency

Concurrente transacties

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). 2.1

CONCURRENCY

In het vervolg van deze leereenheid 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 de transacties van die twee klanten zo weinig mogelijk last van elkaar hebben en geen inconsistentie kunnen veroorzaken.

Multi-user

Multi-session

Read/write-conflict Write/writeconflict

Concurrency control

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: multi-session-gebruik. Bijvoorbeeld wanneer gebruiker Sysdba een sessie opent vanuit een eenvoudige SQL-client en een andere sessie vanuit de grafische applicatie die voor eindgebruikers om de database heen is gebouwd. 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 3 bekijken we vervolgens vier soorten problemen die kunnen optreden bij concurrente transacties, en de manier waarop de SQL-standaard en Firebird daarmee omgaan. 2.2

CONCURRENCY CONTROL

We bespreken kort drie manieren van concurrency control bij databases: locking, multiversion concurrency control en optimistic concurrency control.

Lock

Gedeelde lock Exclusieve lock

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.

Locking granularity

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.

Deadlock

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 Multi-generational architecture

Multiversion concurrency control Een andere vorm van concurrency control in databases is multiversion concurrency control (MVCC). Firebird en zijn voorganger InterBase gebruiken hiervoor de term multi-generational 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 paragraaf 2.3) een stuk eenvoudiger.

Optimistic concurrency control

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.

2.3

Recovery

Log file

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 databaserecovery. Dit valt echter buiten het bestek van deze leereenheid. 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 onbetrouwbaar zijn. De onderdelen van het rdbms die zorgen voor concurrency control en recovery zijn hiervoor verantwoordelijk.

Atomicity

Consistency

Isolation

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 11.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 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 uitdaging 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 paragraaf 4 behandelen.

Durability

Durability Durability (‘bestendigheid’) verzekert dat de effecten van een gecommitte transactie op de database niet verloren raken, ook niet bij een softwareof hardwarestoring. 2.5

Gedistribueerde database

Replicatie

Gecontroleerde redundantie

Update propagation problem

Two-phase commit

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. Replicatie 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.

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 4 bekijken we mogelijke oplossingen. Uitgangspunt

Bij het introduceren van de vier problemen gaan we steeds uit van een transactie T1, die concurrent wordt uitgevoerd met een transactie T2. We willen weten wat voor problemen T1 kan ondervinden van deze concurrency. Natuurlijk kan T2 dezelfde soort problemen tegenkomen, maar we bekijken de situatie steeds vanuit transactie T1. 3.1

LOST UPDATE

Transactie T1 update bepaalde gegevens, en nog voordat T1 die update kan committen wijzigt transactie T2 diezelfde gegevens. De update van T1 wordt dus overschreven door de wijziging van T2, en het is alsof de update van T1 nooit is gebeurd: het is een lost update. Dit is een voorbeeld van een write/write-conflict.

Lost update

VOORBEELD 11.3 (lost update)

Bekijk het scenario in tabel 11.1, waarin twee transacties T1 en T2 concurrent worden uitgevoerd. De tijd loopt van boven naar beneden: een hoger statement wordt eerder uitgevoerd dan een lager statement. TABEL 11.1 transactie T1

De update van T1 gaat verloren transactie T2

update Rekening set saldo = 0 where nr = 1508; update Rekening set saldo = 100 where nr = 1508; select * from Rekening where nr = 1508;

Beide transacties doen een update van het saldo van rekening 1508. Als er geen concurrency control geregeld is, kan de volgende situatie zich voordoen: eerst wordt de update van T1 uitgevoerd, en vervolgens die van T2. T1 denkt dan verder te kunnen werken in de wetenschap dat het saldo op 0 is gezet, maar dat klopt niet doordat de update van T2 er tussendoor is gekomen. De update van T1 gaat dus verloren. Het ‘overschrijvende statement’ in T2 hoeft geen update te zijn: een delete telt ook (zie de uitwerking van opgave 11.6a). Een insert kan nooit een update overschrijven, dus een insert in T2 is hier niet relevant.

OPGAVE 11.2

Iemand zegt: “Maar het is toch heel normaal dat de update van de medewerker die met T1 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. 3.2

DIRTY READ

Transactie T1 leest gegevens die door transactie T2 zijn veranderd maar niet zijn gecommit, en gaat daarmee aan de slag. Dit heet een dirty read, en geeft een probleem als T2 een rollback uitvoert. Andere namen zijn: uncommitted-dataprobleem, temporary-updateprobleem en uncommitteddependencyprobleem. Dit is een voorbeeld van een read/write-conflict.

Dirty read Uncommitted data Temporary update Uncommitted dependency VOORBEELD 11.4 (dirty read)

Het volgende scenario geeft een voorbeeld van een dirty read: TABEL 11.2

T1 doet een dirty read

transactie T1

transactie T2 update Rekening set saldo = saldo + 100 where nr = 1409;

select saldo from Rekening where nr = 1409; commit; rollback;

Het maakt hier niet uit of de commit van T1 plaatsvindt voor of na de rollback van T2. Het maakt eigenlijk ook niet uit of T2 uiteindelijk een rollback of een commit doet: het feit dat T1 gegevens leest die op dat moment ongecommit zijn is ongewenst. Wanneer T2 een insert doet in plaats van een update treedt hetzelfde probleem op: een dirty read van T1 (als T1 tenminste een select doet die ook de door T2 toegevoegde rij oplevert). 3.3

Non-repeatable read Inconsistent data

NON-REPEATABLE READ

Transactie T1 leest twee keer dezelfde rij, maar krijgt verschillende resultaten doordat transactie T2 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 11.5 (non-repeatable read)

Het volgende scenario geeft een voorbeeld van een non-repeatable read: TABEL 11.3

T1 krijgt te maken met een non-repeatable read

transactie T1

transactie T2

select saldo from Rekening where nr = 1409; delete from Rekening where nr = 1409; commit; select saldo from Rekening where nr = 1409; commit;

De eerste select van T1 geeft één rij als resultaat, terwijl de tweede select geen rijen zal geven. T2 heeft immers intussen die ene rij verwijderd, en gecommit. Eenzelfde effect kan bereikt worden met een toepasselijk updatestatement van T2, in plaats van een delete (zie opgave 11.7). Het verschil tussen een dirty read en een non-repeatable read is dat bij een dirty read T2 niet gecommit heeft voordat T1 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. Inconsistent analysis Incorrect summary

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 T1 is bezig een aantal gegevens te sommeren, terwijl een andere transactie tussendoor een aantal van die gegevens wijzigt (zie ook voorbeeld 11.8). 3.4

Phantom

PHANTOM

Het phantom-probleem lijkt op het non-repeatable-readprobleem: transactie T1 leest een aantal rijen die allemaal voldoen aan een of andere selectieconditie. Vervolgens wijzigt (en commit) transactie T2 iets, waardoor nu een of meer extra rijen voldoen aan die selectieconditie. Als T1 dan dezelfde selectie nogmaals doet komen daarin een of meer phantom rows te voorschijn. Dit is weer een voorbeeld van een read/writeconflict. 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).

Het volgende scenario geeft een voorbeeld van het phantom-probleem:

VOORBEELD 11.6 (phantom)

TABEL 11.4

T1 ziet een phantom row verschijnen

transactie T1

transactie T2

select nr from Rekening where saldo 100;

insert into Order_ values (5803, 1234, '13-jan-2012', null); select nr, totaalbedrag from Order_ where totaalbedrag > 100;

a Welk van de vier klassieke problemen treedt hier op? Geef ook aan waarom het niet een van de andere drie klassieke problemen is. b Welke transactieopties moeten de transacties minstens hebben om dit probleem te omzeilen? 2

Bedenk een scenario waarin transactie A een dirty read doet vanwege een insert van transactie B. Gebruik hiervoor alleen tabel Klant.

3

Bedenk een scenario waarin transactie A een non-repeatable read doet omdat transactie B een delete doet. Gebruik hiervoor alleen tabel Klacht.

4

Bedenk een scenario waarin transactie A een inconsistent analysis doet omdat transactie B een delete doet. Gebruik hiervoor alleen tabel Order_.

TERUGKOPPELING 1

Uitwerking van de opgaven

11.1

Geen uitwerking.

11.2

Het is inderdaad heel gewoon dat gegevens die de ene gebruiker heeft ingevoerd, op een gegeven moment worden overschreven door iemand anders. Het probleem zit hem in het feit dat in het voorbeeld de eerste medewerker in de veronderstelling is dat hij met een transactie bezig is: een aantal bij elkaar horende databaseacties, die óf allemaal wel óf allemaal niet worden uitgevoerd. Anders gezegd: hij gaat ervan uit dat zijn acties één geheel vormen, waar niets tussen kan komen zolang hij daarmee bezig is. (In ACID-termen: een transactie moet atomair en geïsoleerd zijn.) Hij mag van het rdbms verwachten dat dat ervoor zorgt dat zijn transactie op die manier wordt uitgevoerd. Als er een lost update optreedt, heeft het rdbms dat duidelijk niet goed gedaan. Opmerking: dat laatste is niet noodzakelijk een fout van het rdbms; in paragraaf 4 zien we hoe een gebruiker per transactie kan aangeven hoe strikt het rdbms erop moet letten dat andere transacties hem niet in de wielen rijden.

11.3

a Transactie A doet hier een dirty read: onder de gegevens die uit tabel Reis worden gelezen bevindt zich een ongecommitte rij die door transactie B is toegevoegd. b Dit is een voorbeeld van een lost update van transactie B. Het feit dat slechts een deel van B’s update verloren gaat doet er niet toe. Opmerking: u kunt dit scenario later simuleren; u zult zien dat u dan een ‘lock conflict’ krijgt, net als in de uitwerking van opgave 11.6. c Transactie B leest tweemaal dezelfde rij in tabel Bezoek, maar zal de tweede keer een ander resultaat krijgen omdat transactie A intussen die rij heeft verwijderd: een standaardvoorbeeld van een non-repeatable read door B. In voorbeeld 11.7 simuleren we dit scenario. d De update van transactie A gaat verloren door de delete van transactie B; een klassiek geval van een lost update dus. Opmerking: Als u wilt kunt u deze opgave later uitproberen en constateren dat er inderdaad een ‘lock conflict’ optreedt. e Dit is een strikvraag: de delete van B gaat niet door vanwege de restricted delete op de verwijzing van Reis naar Transport. In termen van concurrency gaat er dus niets mis, want B krijgt een foutmelding en de delete wordt niet uitgevoerd.

11.4

Dit betekent dat lost updates niet voor kunnen komen, welk isolation level je ook kiest.

11.5

Met isolation level read committed zie je alleen gecommitte wijzigingen van andere transacties. Zolang transactie A niet gecommit heeft, zou transactie B dus geen wijziging moeten zien. Simulatie van dit scenario bevestigt dit.

11.6

a Dit is een voorbeeld van een update-conflict (zie ook de opmerking bij de bespreking van het isolation level serializable uit de SQLstandaard). Vanwege de eis dat lost updates nooit – bij geen enkel isolation level – mogen voorkomen heeft Firebird zichzelf de volgende beperking opgelegd (zie ook opgave 11.4): “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”. Het maakt dan ook niet uit welk isolation level we kiezen: in alle gevallen krijgen we een update-conflict, en een foutmelding, omdat transactie B probeert de data te wijzigen die transactie A net gewijzigd heeft. Omdat transactie A nog niet gecommit heeft, heeft A namelijk nog een write-lock op die data. Opmerking: met data wordt in dit geval de rij bedoeld die door transactie A gewijzigd is. Het gaat dus niet om een lock op een hele tabel. U kunt dit nagaan door straks bij onderdeel b als alternatief voor transactie B de opdracht update Bezoek set verblijfsduur = 5 where reis = 32 and volgnr = 1;

te nemen: die update wordt gewoon uitgevoerd. Opmerking: Het veroorzaken van een update-conflict is de enige manier die we hebben om een lost-update-scenario te simuleren. Lost updates mogen immers bij geen enkel isolation level voorkomen, en we kunnen een transactie niet starten met ‘no isolation level’. b Op het moment dat transactie B probeert zijn update uit te voeren in sessie2 wordt de volgende foutmelding gegeven: lock conflict on no wait transaction -deadlock -update conflicts with concurrent update -concurrent transaction number is 120

De derde regel geeft aan wat er aan de hand is: de update van transactie B conflicteert met de concurrente update van transactie A. Het rdbms weet dat A een lock heeft op de data die B wil updaten en meldt dus een ‘lock conflict’. Opmerking: op de tweede regel van de foutmelding staat de term ‘deadlock’. Dit is een beetje verwarrend, omdat het hier niet gaat om een ‘totale’ deadlock, waarin alle transacties op elkaar staan te wachten en er

dus helemaal niets meer kan gebeuren. Het gaat hier om twee transacties, waarvan de tweede een halt wordt toegeroepen omdat die probeert gegevens te updaten waarop de eerste al een update heeft uitgevoerd; de tweede wordt dus afgebroken, maar de eerste kan gewoon verdergaan. Opmerking: als u de code van transactie B verandert in delete from Bezoek where reis = 31 and volgnr = 1;

krijgt u hetzelfde resultaat: een lock conflict. 11.7

Een scenario dat een non-repeatable read voor transactie A veroorzaakt is het volgende: TABEL 11.16 transactie A

Transactie A doet een non-repeatable read transactie B

select * from Transport; update Transport set omschrijving = 'upbeamen' where code = 'BU'; commit; select * from Transport;

Vergeet de commit van transactie B niet! We gaan weer uit van een sessie1 voor transactie A en een sessie2 voor transactie B. Transactie A moet natuurlijk isolation level read committed hebben om de verandering van B te kunnen zien: set transaction read only no wait isolation level read committed record_version; --in sessie1 Transactie B

kan gewoon de default transactieopties gebruiken. De eerste select van transactie A geeft dan als resultaat: CODE ====== RV BU

OMSCHRIJVING ============ ruimteveer beam up

De tweede select geeft zoals verwacht: CODE ====== RV BU

OMSCHRIJVING ============ ruimteveer upbeamen

11.8

Een scenario dat een phantom row voor transactie A veroorzaakt is: TABEL 11.17

Transactie A ziet een phantom row verschijnen

transactie A

transactie B

select duur from Reis where duur > 500; update Reis set duur = duur * 10; commit; select duur from Reis where duur > 500;

Vergeet de commit van transactie B niet! We gaan weer uit van een sessie1 voor transactie A en een sessie2 voor transactie B. Transactie A moet natuurlijk isolation level read committed hebben om de verandering van B te kunnen zien: set transaction read only no wait isolation level read committed record_version; --in sessie1

De eerste select van transactie A geeft dan als resultaat: DUUR ========= 1380 1340

De tweede select geeft zoals verwacht iets anders: DUUR ========= 3900 13800 3800 13400

2

1

Uitwerking van de zelftoets

a Transactie B doet bij zijn tweede select een dirty read, omdat transactie A zijn insert nog niet gecommit heeft. Het gaat duidelijk niet om een lost update (van transactie A), want er wordt geen update-statement uitgevoerd (het is ook geen lost insert, want de insert van A is het enige statement in het gehele scenario dat iets verandert in de database). Het is geen non-repeatable read van transactie B, omdat A nog niet gecommit heeft. (En als A wel gecommit zou hebben voor B’s tweede select was het ook geen non-repeatable read, omdat alle rijen die B tijdens zijn eerste select las tijdens de tweede select nog aanwezig en onveranderd zijn). Het is geen phantom, omdat A nog niet gecommit heeft.

b We willen dat transactie B niet de kans loopt een dirty read te doen, dus we moeten ervoor zorgen dat B alleen gecommitte gegevens van andere transacties kan lezen: set transaction read only no wait isolation level read committed record_version; -- voor B

Voor transactie A maakt het niet uit; als het erom gaat een zo laag mogelijk isolation level te kiezen kunnen we voor A het volgende nemen: set transaction read write no wait isolation level read committed record_version; -- voor A

2

Een scenario waarin transactie A een dirty read doet vanwege een insert van transactie B is het volgende: TABEL 11.18

Transactie A doet een dirty read

transactie A

transactie B insert into Klant values (1460, 'Wilson', 1452);

select naam from Klant where aanbrenger = 1452;

Opmerking: natuurlijk kan A die dirty read alleen daadwerkelijk doen bij geen of een laag genoeg isolation level; in Firebird kunnen we dit dus niet simuleren. 3

Een scenario waarin transactie A een non-repeatable read doet vanwege een delete van transactie B is het volgende: TABEL 11.19 transactie A

Transactie A doet een non-repeatable read transactie B

select * from Klacht; delete from Klacht where nr = 2; commit; select * from Klacht;

Opmerking: Natuurlijk kan A die non-repeatable read alleen doen bij geen of een laag genoeg isolation level; in Firebird kunnen we dit scenario simuleren met isolation level read committed record_version voor transactie A.

4

Een scenario waarin transactie A een inconsistent analysis doet vanwege een delete van transactie B is het volgende: TABEL 11.20

Transactie A doet een inconsistent analysis

transactie A

transactie B

select

klant, count(nr) from Order_ group by klant; delete from Order_ where klant = 1234; commit; select

klant, count(nr) from Order_ group by klant;

Opmerking: natuurlijk kan A die inconsistent analysis alleen doen bij geen of een voldoende laag isolation level; in Firebird kunnen we dit scenario simuleren met isolation level read committed record_version voor transactie A.

Inhoud leereenheid 12

Triggers en stored procedures Introductie Leerkern 1 2

3

4

139 140

Triggertaal 140 Stored procedures 141 2.1 Wat is een stored procedure? 141 2.2 Uitvoeren van een stored procedure 143 Triggers 144 3.1 Before-triggers en after-triggers 144 Events 145 3.2 Contextvariabelen old en new 146 3.3 Voorbeelden van after-triggers 147 3.4 Voorbeelden van before-triggers 151 3.5 Before-trigger met exception 153 3.6 Multi-event-triggers 156 3.7 Meer over triggers en stored procedures 159 4.1 Deltaproblematiek 159 Executable procedures en select-procedures 4.2 Stored procedures en views 159 4.3 Executievolgorde van triggers 160 4.4

Samenvatting Zelftoets

160

161

Terugkoppeling 1 2

164

Uitwerking van de opgaven 164 Uitwerking van de zelftoets 166

138

159

Leereenheid 12

Triggers en stored procedures

INTRODUCTIE

In vorige leereenheden 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 deze leereenheid 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. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – inzicht hebt in het gebruik van triggers, stored procedures en exceptions – eenvoudige regels kunt implementeren in de triggertaal van Firebird. Deze leereenheid heeft een studielast van 6 uur. Studeeraanwijzing

Zoals steeds bevelen we het uitproberen en testen van alle code van harte aan. Raadpleeg de handleiding bij de SQL-omgeving voor tips over het omgaan met (auto)commit en rollback in deze leereenheid.

Voorbeelddatabase

De voorbeelddatabase in deze leereenheid is Toetjesboek ZT (een ‘kale’ versie van de Toetjesboek-database, zonder triggers en stored procedures). Zie figuur 12.1 voor het strokendiagram. Raadpleeg de bijlage achter in dit deel voor de voorbeeldpopulatie.

FIGUUR 12.1

ToetjesboekZT: strokendiagram

LEERKERN 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.

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 ToetjesboekZT (ZT = zonder triggers). Zie de handleiding bij de SQLomgeving op de cursussite

Raadpleeg het create-script en het insert-script en ga na dat  de enige databaseobjecten van ToetjesboekZT de vier tabellen zijn  in ToetjesboekZT de verwijzing van Ingredient naar Gerecht geen cascading delete heeft. (Deze zullen we in opgave 12.9 zelf maken met een trigger, iets wat bij sommige SQL-dialecten zelfs niet anders kan.)  de waarden van Gerecht.energiePP null zijn (deze laten we immers automatisch berekenen).

Stored procedure

2

Stored procedures

2.1

WAT IS EEN STORED PROCEDURE?

Een stored procedure (letterlijk: opgeslagen deelprogramma) is een 3GLprogrammaatje 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 deze leereenheid speelt de rechtenkwestie geen rol, daar alle opdrachten worden gegeven door Sysdba. 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 executeopdracht. In de volgende paragraaf zal de aanroep worden geautomatiseerd door middel van triggers, die reageren op veranderingen in de database. VOORBEELD 12.1 (energieberekening Toetjesboek)

De stored procedure voor de energieberekening moet de waarde van Gerecht.energiePP 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.

Formele parameter

-- 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

Notatie

:p_gerechtnaam

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 12.1

Voer het voorgaande create procedure-statement uit, en let 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 termcommando een oplossing.

Terminator

Het set term-commando Bij opgave 12.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 ;^ herstelt de puntkomma in zijn gewone rol van algemene terminator. Let op de spatie voor de puntkomma! We sluiten af 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 12.2

Voer nogmaals het create procedure-statement uit, nu inclusief de set term-statements. 2.2

UITVOEREN VAN EEN STORED PROCEDURE

Een stored procedure wordt uitgevoerd door middel van een executeopdracht. Dat kan vanuit een applicatie, vanuit een andere stored procedure of vanuit een trigger. Wij zullen de stored procedure pGerecht_update_energiePP uitvoeren vanuit onze SQL-omgeving. Als Sysdba hebben we automatisch het vereiste execute-recht: execute procedure pGerecht_update_energiePP('Coupe Kiwano');

Door deze aanroep wordt de programmacode van pGerecht_update_energiePP 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';

Actuele parameter

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_energiePP 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 12.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 12.3

Voer de stored procedure uit voor alle drie de gerechten. 3

Trigger

Event

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. 3.1

Before-trigger After-trigger

BEFORE-TRIGGERS EN AFTER-TRIGGERS

Triggers kunnen we in twee categorieën verdelen:  before-triggers: deze worden geactiveerd vóórdat een insert, delete of update 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.

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.

Event

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 ten gevolge 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. Belangrijk

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 12.2

De kolom Gerecht.energiePP 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 12.2 op verschillende situaties reageren, doen ze vrijwel hetzelfde: herberekenen van energie PP. Voor die herberekening hebben we een stored procedure: pGerecht_update_energiePP, 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 tIngredient_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. 3.3

Contextvariabele

old new

if…then…

CONTEXTVARIABELEN OLD EN NEW

De after-update-trigger tIngredient_au reageert op een update van Ingredient en zal moeten controleren of hoeveelheidPP 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 hoeveelheidPP. 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 figuur 12.2 voor een overzicht.

FIGUUR 12.2 3.4

old- en new-contextvariabelen

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 12.3

De code voor de eerste trigger van voorbeeld 12.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. Deze puur technische kwestie wordt geregeld door beide set term-opdrachten.  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 12.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 ToetjesboekZT zijn alle energiePP-waarden aanvankelijk null. OPGAVE 12.5

Schrijf de create trigger-code voor de tweede trigger van voorbeeld 12.2, tIngredient_ad, die reageert op het verwijderen van een ingrediënt. Voer de opdracht uit en controleer het effect, bijvoorbeeld door de in opgave 12.4 ingevoegde Ingredient-rij weer te verwijderen. VOORBEELD 12.4

De derde trigger van voorbeeld 12.2, tIngredient_au, reageert op een update van een ingrediënt. De stored procedure wordt alleen aangeroepen indien hoeveelheidPP 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 energiePP 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 6.3 van leereenheid 8 over updates van een deel van een primaire sleutel. Voorwaardelijke programmastructuur Voorbeeld 12.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).

Samengestelde opdracht

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 12.6

Voer de code van tIngredient_au uit en controleer het effect. VOORBEELD 12.5

De vierde trigger van voorbeeld 12.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 energiePE 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^

Herhaling for…do…

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 pGerecht_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 selectopdracht 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).

Variabele

Variabele declareren

declare variable

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).  Beide voorkomens van v_gerechtnaam tussen begin en end mogen worden voorafgegaan door een dubbelepunt; dit is echter niet verplicht.

OPGAVE 12.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 ToetjesboekZTInsert.sql uit te voeren), worden alle energiePP-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 energiePP). En wie nu denkt dat het allemaal waterdicht is, heeft het mis. Bijvoorbeeld: worden in één update van Gerecht tegelijkertijd de kolommen naam en energiePP 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 van voorbeeld 12.5 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. 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 before-triggers 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 12.6 (before-inserttrigger 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 uit geschoven). 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. Zie ook voorbeeld 12.5.

De waarde max(volgnr) + 1 zullen we berekenen met een selectstatement, waarna we deze waarde via een into-clausule toekennen aan new.volgnr. 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 12.8

Controleer tIngredient_bi door een nieuw ingrediënt (zonder volgnummer, dat wil zeggen met volgnr null) 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 12.9

In sommige dialecten (zoals Oracle) is het onmogelijk refererende acties, zoals een cascading update, te implementeren via een create tablestatement. Het moet dan met triggers. Schrijf zo’n trigger voor een cascading delete bij de verwijzing van Ingredient naar Gerecht. Opmerking: ToetjesboekZT is gecreëerd zonder cascading deletes. Ook voor de cascading updates zouden vergelijkbare triggers kunnen worden geschreven.

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.

Exception

VOORBEELD 12.7 (verbod rechtstreeks wijzigen energiePP)

In paragraaf 3.4 hebben we vier after-triggers gezien voor herberekening van Gerecht.energiePP. Er is echter een vijfde situatie waarbij herberekening nodig is: wanneer een gebruiker energiePP rechtstreeks wijzigt. Ook hiervoor kunnen we een after-trigger schrijven, die energiePP herberekent en die dus de oude waarde terugzet. Beter echter is eerder in actie te komen, voordat het kwaad is geschied: via een before-updatetrigger 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 energiePP 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 2 -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 energiePP al een waarde had. Bij waarde null treedt geen foutsituatie op en wordt het update-statement gewoon uitgevoerd. OPGAVE 12.10

Voer de code van voorbeeld 12.7 uit: creëren exception, creëren trigger en pogen energiePP rechtstreeks te wijzigen. Geen goede oplossing In opgave 12.10 vroegen we u de before-update-trigger op een beperkte manier te testen: hij deugt namelijk niet. Omdat de stored procedure pUpdateGerechtEnergiePP 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 energiePP aanleiding geeft. Het effect is dat we bijvoorbeeld geen ingrediënten meer kunnen toevoegen of weghalen. Het is wel op te lossen, maar voor deze leereenheid voert dat wat te 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 energiePP herstelt, mocht de gebruiker deze rechtstreeks hebben gewijzigd. Opgooien en vangen van een exception 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 do .

Het tweede voorbeeld ontlenen we aan de OpenSchool-database. Omdat het wat ingewikkelder is, passen we een stapsgewijze aanpak toe. VOORBEELD 12.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 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 inschrijving is ingeschreven maar geen vrijstelling heeft en ook geen voldoende)) 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 7.18. Voorgaande exists-expressie komt letterlijk in voorbeeld 7.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 bij new.cursus waarvoor new.student is ingeschreven maar geen vrijstelling heeft en ook geen voldoende)) then exception eNiet_aan_voorkennis_voldaan; end

Analoog aan voorbeeld 7.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 vrijstelling of voldoende heeft select * from Voorkenniseis V where cursus = new.cursus and voorkennis not in (--cursussen waarvoor --new.student een vrijstelling --of voldoende heeft select cursus from Inschrijving where student = new.student and (vrijstelling = 'J' or cijfer > 5)))) then exception eNiet_aan_voorkennis_voldaan; end^ set term ;^

OPGAVE 12.11

Creëer de exception en de trigger van voorbeeld 12.8 en voer een test uit. In voorbeeld 12.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 beforeupdate). We zien daarvan een voorbeeld in opgave 12.12. OPGAVE 12.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 6.3 van leereenheid 8 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. Test uw implementatie. 3.7

Multi-eventtrigger

Contextvariabele

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-eventtriggers 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 12.9 (triggers bij meer dan één eventtype)

Voor de automatische berekening van Gerecht.energiePP hebben we vijf triggers geschreven, waaronder drie after-triggers op tabel Ingredient:

(fragment ToetjesboekCreate .sql)

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.hoeveelheidPP niet gelijk is aan new.hoeveelheidPP. We kunnen beide triggers vervangen door één trigger van het multi-eventtype ‘insert or update’, als volgt: create trigger tIngredient_aiu 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 hoeveelheidPP; 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 1 van de zelftoets. Computed column Zie ook leereenheid 9, paragraaf 5.5

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). 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 dropstatement de oude kolom energiePP weg te gooien en met een alter table addstatement een nieuwe aan te maken als computed column. Verwijder wel eerst de energiePP-triggers of ga uit van een ‘schoon’ ToetjesboekZT.

4

Meer over triggers en stored procedures

4.1

DELTAPROBLEMATIEK

Stored procedures, triggers en ook exceptions laten zich via dropstatements 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 alterstatements. 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. 4.2

EXECUTABLE PROCEDURES EN SELECT-PROCEDURES

De stored procedures van deze leereenheid 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. 4.3

STORED PROCEDURES EN VIEWS

Stored procedures kunnen zich aan de gebruiker voordoen als een tabel (zie paragraaf 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 zijn soms ook zonder tussenkomst van triggers DMLacties op views mogelijk (de zogenaamde updatable views).

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

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.

Load balancing

SAMENVATTING Paragrafen 1 t/m 3

Stored procedures en triggers zijn programmaatjes, geschreven in een procedurele (3GL) triggertaal. Ze zijn opgeslagen in de database en actief aan de serverkant. Triggertalen zijn te beschouwen als dialectspecifieke, procedurele uitbreidingen van SQL. Door de typische procedurele elementen (keuze- en herhalingsstructuren) is het mogelijk er verfijnde controles en acties mee te implementeren. Een trigger wordt aangeroepen bij het optreden van een bepaalde gebeurtenis (event). Een before-insert-trigger bijvoorbeeld wordt actief bij een poging tot invoegen van een rij in een specifieke tabel. Een afterinsert-trigger wordt actief na het daadwerkelijk invoegen. Analoge triggers kunnen worden gecreëerd voor delete- en update-events. Via de contextvariabele new zijn in de code van een update- of insert-trigger de nieuwe kolomwaarden beschikbaar. Analoog zijn via old de oude kolomwaarden beschikbaar in de code van een update- of delete-trigger. Een stored procedure moet, via een execute-statement, expliciet worden aangeroepen, vanuit een trigger, een andere stored procedure of een applicatie. Vaak wordt in een stored procedure de eigenlijke actie geprogrammeerd, terwijl triggers alleen maar zorgen dat de stored procedure op het juiste moment wordt aangeroepen.

De verwerking van een trigger of stored procedure kan worden afgebroken door het ‘opgooien’ van een exception (een databaseobject met een naam en een geassocieerde foutmelding). Bij een trigger van het before-type wordt hiermee tevens de beoogde actie (insert, delete of update) afgebroken. Eventueel kan de verwerking worden voortgezet door het uitvoeren van alternatieve code (vangen van de exception) in een when-clausule. Via dit mechanisme (exception handling) kunnen regels worden geïmplementeerd die niet via een constraint kunnen worden afgedwongen. Paragraaf 4

De executable procedures in deze leereenheid ‘doen alleen maar wat’ en retourneren geen waarde (zoals een functie). Men kan ook selectprocedures creëren die wel een waarde teruggeven. Die waarde kan zelfs een tabel zijn. Dit opent vele mogelijkheden voor procedurele rijgewijze verwerking van tabellen. Applicatieconstraints zijn regels die actief zijn aan de clientkant. Ze horen tot het domein van applicatieontwikkeling. Load balancing (de keuze welke regels aan de server- en welke aan de clientkant moeten worden geïmplementeerd) is echter ook een databaseprobleem. ZELFTOETS

Let op

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 (beperkende regels dan wel gedragsregels). Om te kunnen testen moet u het creëren van de triggers committen. We herhalen hier het strokendiagram van de Orderdatabase. De voorbeeldpopulatie vindt u achterin.

FIGUUR 12.3

Strokendiagram Orderdatabase

1

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?

2

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?

3

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 van onderdeel a 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.

4

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’ of ‘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.

5

Schrijf een trigger die een ´voorwaardelijke cascading delete´ implementeert voor de verwijzing van Klacht naar Orderregel: een poging tot verwijderen van een orderregel slaagt als alle klachten bij die orderregel zijn afgehandeld. Geef een toepasselijke melding als deze ‘voorwaardelijke cascading delete’ niet doorgaat.

TERUGKOPPELING 1

12.1 t/m 12.4 12.5

Uitwerking van de opgaven

Geen uitwerking. Code voor de tweede trigger: set term ^; create trigger tIngredient_ad for Ingredient after delete as begin execute procedure pGerecht_update_energiePP(old.gerecht); end^ set term ;^

12.6 t/m 12.8 12.9

Geen uitwerking. De benodigde trigger is een before-delete-trigger: set term ^; create trigger tGerecht_bd_cascading_delete for Gerecht before delete as begin delete from Ingredient where gerecht = old.naam; end^ set term ;^

De triggernaam is samengesteld volgens de gebruikelijke conventie, met een extra, verduidelijkend achtervoegsel. 12.10 en 12.11 12.12

Geen uitwerking. b De regel kan niet worden overtreden bij invoer van een nieuwe cursus (insert op Cursus), maar wel bij een update, indien de examinator daarbij wordt gewijzigd. Bij een delete op Cursus (gevolgd door een cascading delete van bijbehorende Begeleider-regels) is geen overtreding mogelijk. Ook invoer van een nieuwe begeleider (insert op Begeleider) kan tot overtreding leiden, maar een delete niet. c Op basis van het voorgaande zullen we twee triggers schrijven:  trigger 1: een before-update-trigger op Cursus  trigger 2: een before-insert-trigger op Begeleider Beide laten we een exception aanroepen, met de volgende code: create exception eExaminatorIsBegeleider 'Wanneer een docent examinator is van een cursus mag hij geen begeleider zijn van die cursus.';

De triggers ontwikkelen we in stappen. De eerste krijgt de volgende opzet: Trigger 1, stap 1

create trigger tCursus_bu for Cursus before update as begin if (de examinator is veranderd and de nieuwe examinator is begeleider van deze cursus) then exception eExaminatorIsBegeleider; end

Dit geeft: Trigger 1, stap 2

set term ^; create trigger tCursus_bu for Cursus before update as begin if (new.examinator old.examinator and new.examinator in (select docent from Begeleider where cursus = new.code)) then exception eExaminatorIsBegeleider; end^ set term ;^

De tweede trigger krijgt de volgende opzet: Trigger 2, stap 1

create trigger tBegeleider_bi for Begeleider before insert as begin if (de nieuwe begeleider is examinator van de betreffende cursus) then exception eExaminatorIsBegeleider; end^

Dit leidt tot: Trigger 2, stap 2

set term ^; create trigger tBegeleider_bi for Begeleider before insert as begin if (new.docent = (select examinator from Cursus where code = new.cursus)) then exception eExaminatorIsBegeleider; end^ set term ;^

2

1

a

Uitwerking van de zelftoets SQL-statement:

create exception eOrderwijziging 'Datum verstreken; niet mogelijk nog wijzigingen aan te brengen';

b We geven eerst een oplossing met drie single-event-triggers: – een trigger die voorkomt dat aan een oude order nog een orderregel wordt toegevoegd – een trigger die voorkomt dat uit een oude order een orderregel wordt verwijderd – een trigger die voorkomt dat van een oude order een orderregel een update ondergaat. De drie triggers in één script: set term ^; create trigger tOrderregel_bi for Orderregel before insert as begin if (current_date (-- datum van order bij in te voegen orderregel select datum from Order_ where nr = new.order_)) then exception eOrderwijziging; end^ create trigger tOrderregel_bd for Orderregel before delete as begin if (current_date (-- datum van order bij te verwijderen orderregel select datum from Order_ where nr = old.order_)) then exception eOrderwijziging; end^ create trigger tOrderregel_bu for Orderregel before update as begin if (current_date (-- datum van order bij aan te passen orderregel select datum from Order_ where nr = old.order_)) then exception eOrderwijziging; end^ set term ;^

Merk op dat de code van de triggers tOrderregel_bd en tOrderregel_bu identiek is. We kunnen deze samenvoegen tot één multi-event-trigger van het type ‘delete or update’: set term ^; create trigger tOrderregel_bdu for Orderregel before delete or update as begin if (current_date (select datum from Order_ where nr = old.order_)) then exception eOrderwijziging; end^ set term ;^

Er zijn nog tal van varianten. We geven er één, waarbij we ons beperken tot trigger tOrderregel_bi, waarin de datum in een variabele wordt opgeslagen: set term ^; create trigger tOrderregel_bi for Orderregel before insert as declare variable v_orderdatum date; begin select datum from Order_ where nr = new.order_ into v_orderdatum; if (v_orderdatum current_date) then exception eOrderwijziging; end^ set term ;^

Door gebruik te maken van boolean systeemvariabelen inserting, deleting en updating kunnen we zelfs alle (before-)triggers op Orderregel onderbrengen in één trigger tOrderregel_bidu, van het type ‘insert or delete or update’, als volgt (in een versie met variabele). set term ^; create trigger tOrderregel_bidu for Orderregel before insert or delete or update as declare variable v_orderdatum date; begin if (inserting) then select datum from Order_ where nr = new.order_ into v_orderdatum; if (deleting or updating) then select datum from Order_ where nr = old.order_ into v_orderdatum; if (v_orderdatum current_date) then exception eOrderwijziging; end^ set term ;^

c Het moet ook onmogelijk zijn om de orderdatum te veranderen. Want anders is er de volgende sluiproute: orderdatum veranderen in de huidige datum, orderregelverzameling aanpassen, orderdatum weer terugzetten.

2

a

Code voor procedure pUpdateTotaalbedragOrder:

set term ^; create procedure pUpdateTotaalbedragOrder(p_ordernr integer) as begin update Order_ set totaalbedrag = (select sum(bedrag) from Orderregel where order_ = :p_ordernr) where nr = :p_ordernr; end^ set term ;^

Testen: execute procedure pUpdateTotaalbedragOrder(…)

Vul op de plaats van … een ordernummer in. b Het ordertotaalbedrag moet worden herberekend bij de volgende events: – after insert op Orderregel – after delete op Orderregel – after update op Orderregel Opmerkingen  Daarnaast is het denkbaar dat een ordertotaalbedrag ‘zomaar’ wordt gewijzigd. In plaats van dit toe te staan en ‘after update’ te corrigeren, is het beter dit te voorkomen, met een before-update-trigger en een exception.  We nemen aan dat het wijzigen van Orderregel.order niet kan voorkomen. 3

a Er wordt hier een after-insert-trigger voor Orderregel gevraagd. Om naamsconflicten met eventuele eerder gemaakte after-update-triggers voor Orderregel te vermijden, voorzien we de triggernaam van een extra achtervoegsel _voorraad: set term ^; create trigger tOrderregel_ai_voorraad for Orderregel after insert as begin update Artikel set voorraad = voorraad - new.aantal where nr = new.artikel; end^ set term ;^

b Er wordt nu een before insert-trigger gevraagd, die de after inserttrigger van onderdeel a vervangt. We verwijderen dus eerst de trigger van onderdeel a: drop trigger tOrderregel_ai_voorraad;

Daarna creëren we een toepasselijke exception en een nieuwe trigger. Definitie van exception: create exception eVoorraad_negatief 'Een voorraad mag niet negatief zijn';

De definitie van de nieuwe trigger bouwen we op in stappen. Stap 1

create trigger tOrderregel_bi_voorraad for Orderregel before insert as begin bepaal de voorraad van het artikel van de nieuwe orderregel; if (voorraad niet-toereikend) then exception eVoorraad_negatief; werk voorraad bij; end^

Stap 2 De voorraad moet kennelijk na het bepalen ervan worden onthouden. Daar is een variabele voor nodig. Verder is het mooier de stap ‘werk voorraad bij’ onder te brengen in een else-clausule van het if-statement: create trigger tOrderregel_bi_voorraad for Orderregel before insert as declare variable v_voorraad integer; begin bepaal de voorraad van het artikel van de nieuwe orderregel en ken deze toe aan v_voorraad; if (v_voorraad < bestelde aantal) then exception eVoorraad_negatief; else werk voorraad bij; end^

Stap 3 (eindoplossing)

set term ^; create trigger tOrderregel_bi_voorraad for Orderregel before insert as declare variable v_voorraad integer; begin -- bepaal voorraad en ken deze toe aan v_voorraad select voorraad from Artikel where nr = new.artikel into v_voorraad; if (v_voorraad < new.aantal) then exception eVoorraad_negatief; else -- werk voorraad bij update Artikel set voorraad = voorraad - new.aantal where nr = new.artikel; end^ set term ;^

4

Wanneer we alleen daadwerkelijk aangebrachte veranderingen op tabel Artikel loggen, moeten we drie after-triggers schrijven op Artikel: een after insert, een after delete en een after update. Hier volgen de drie triggerdefinities in één script: set term ^; create trigger tArtikel_ai for Artikel after insert as begin insert into LogOrderdatabase values (user, current_timestamp, 'Artikel', 'I'); end^ create trigger tArtikel_ad for Artikel after delete as begin insert into LogOrderdatabase values (user, current_timestamp, 'Artikel', 'D'); end^ create trigger tArtikel_au for Artikel after update as begin insert into LogOrderdatabase values (user, current_timestamp, 'Artikel', 'U'); end^ set term ;^

Desgewenst kunnen we deze tot één multi-event-trigger combineren, als volgt. set term ^; create trigger tArtikel_aidu for Artikel after insert or delete or update as begin if (inserting) then insert into LogOrderdatabase values (user, current_timestamp, 'Artikel', 'I'); else if (deleting) then insert into LogOrderdatabase values (user, current_timestamp, 'Artikel', 'D'); else -- updating insert into LogOrderdatabase values (user, current_timestamp, 'Artikel', 'U'); end^ set term ;^

In de laatste else-clausule hadden we in plaats van het commentaar ‘-updating’ ook ‘if (updating) then‘ kunnen schrijven. Maar zo’n extra if is niet nodig. Immers, de laatste else-clausule wordt alleen maar uitgevoerd wanneer inserting en deleting beide false zijn, en dan is updating vanzelf true.

5

Definitie van de exception: create exception eNogOnbehandeldeKlachten 'Deze orderregel heeft nog onbehandelde klachten en mag dus niet verwijderd worden';

Als er nog onbehandelde klachten zijn bij de bewuste orderregel, dan gaat de beoogde cascading delete niet door. We kunnen dus gebruikmaken van een if met exists: set term ^; create trigger tOrderregel_cd_bd for Orderregel before delete as begin if (exists (select * from Klacht where order_ = old.order_ and volgnr = old.volgnr and behandeld = 'N')) then exception eNogOnbehandeldeKlachten; else delete from Klacht where order_ = old.order_ and volgnr = old.volgnr; end^ set term ;^

Inhoud leereenheid 13

De data dictionary Introductie Leerkern 1

2 3

4 5

6

173 174

Metastructuren 174 1.1 Het objectniveau 174 1.2 Het metaniveau 175 Meta-informatie over tabellen 175 Meta-informatie over kolommen en domeinen 179 3.1 Lokale kolomkenmerken 179 3.2 Niet-lokale kolomkenmerken en domeinen 180 Overige dictionarytabellen 183 Metaviews 185 5.1 Waarom metaviews? 186 5.2 ANSI/ISO-metaviews 186 Van tabel ‘Tabel’ naar volledige database 187 6.1 Zelfbeschrijvende deelsystemen 187 62 Een meta-metaniveau? 188

Samenvatting Zelftoets

188

190

Terugkoppeling 1 2

192

Uitwerking van de opgaven 192 Uitwerking van de zelftoets 194

172

Leereenheid 13

De data dictionary

INTRODUCTIE DDL-statements

zijn boodschappen aan het rdbms, dat de informatie uit die boodschappen 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 hartenlust 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. LEERDOELEN

Na het bestuderen van deze leereenheid wordt verwacht dat u – weet wat metagegevens zijn en kunt uitleggen waarom dit in zekere zin ‘gewone’ gegevens zijn – kunt uitleggen wat een data dictionary is – inziet dat elk DDL-statement aanleiding geeft tot het invoegen, verwijderen en/of wijzigen van een of meer rijen in de datadictionarytabellen – inzicht hebt in de structuur van de Firebird data dictionary – kunt uitleggen dat een relationele database een uitbreiding is van zijn data dictionary – kunt uitleggen waarom een relationele data dictionary zelfbeschrijvend is en het meta-metaniveau samenvalt met het metaniveau. Deze leereenheid heeft een studielast van 5 uur. Studeeraanwijzing

Ook in deze laatste leereenheid is het belangrijk dat u code uitprobeert.

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 13.1 voor het strokendiagram. Populaties van RuimtereisSimpel spelen in dit hoofdstuk geen rol. Des te meer de structuur, in paragraaf 1 kijken we naar de DDL-code.

FIGUUR 13.1

Strokendiagram RuimtereisSimpel

LEERKERN 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 structuren van dergelijke structuren: metastructuren. Het vastleggen van zo’n metastructuur gebeurt ook weer met gegevens: metagegevens.

Metastructuren Metagegevens

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 13.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.

Objectniveau

Objectstructuren en -regels in een fysieke database worden gerealiseerd via DDL-code. De code voor RuimtereisSimpel is als volgt. Create-script RuimtereisSimpel

create create create create

domain domain domain domain

Reisnr Objectnaam Geldbedrag Volgnr

as as as as

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

create table Reis (nr Reisnr not null, not null, vertrekdatum date 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. 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 deze leereenheid 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 figuur 13.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).

Metastructuur

Metatabel Data dictionary Systeemcatalogus

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. 2

Metatabellen voor tabellen en voor gebruikers Rdb$relations

Rdb$users

Meta-informatie over tabellen

De net 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 13.2. Deze naam refereert aan ‘relations’, de meer wiskundige aanduiding voor tabellen. In figuur 13.2 is ook de metatabel voor gebruikers Rdb$users opgenomen, uit de systeemdatabase Security2.fdb.

Tabel- en kolomnamen in hoofdletters In Firebird begint de naam van elke metatabel met RDB$. Dat geldt ook voor hun kolommen. Verder worden intern alle tabel- en kolomnamen opgeslagen in hoofdletters. Wanneer we rechtstreeks lezen of schrijven in de data dictionary, moeten we die namen in hoofdletters opgeven. Om die reden schrijven we in deze leereenheid de namen van metatabellen en hun kolommen in alle figuren met hoofdletters. In code en ook in de tekst houden we echter onze huisstijl aan.

FIGUUR 13.2

Multipliciteitendiagram voor de metatabellen voor tabellen en gebruikers

Figuur 13.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). Verder hebben we een tot nu toe onbekende gebruiker Leo in het diagram gezet, om te laten zien dat Sysdba in het algemeen niet de enige gebruiker is. Iedere Firebird-gebruiker kan eigen databases aanmaken. De gebruiker die een database creëert heet de eigenaar van de database en van de tabellen en andere objecten erin, vandaar de kolomnaam rdb$owner_name. De database-eigenaar heeft alle rechten op de objecten, ook het recht om anderen op hun beurt rechten te verlenen. Behalve dat we het in deze leereenheid af en toe tegenkomen in populatiediagrammen, houden we ons daar in deze cursus niet mee bezig.

FIGUUR 13.3

Metatabellen voor tabellen en gebruikers (eenvoudige versie)

Uniciteit en referentiële integriteit voor metatabellen zelf niet afgedwongen via sleutelconstraints.

Figuur 13.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 13.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 13.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.

Gebruikerstabellen en metatabellen: één database

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 deze leereenheid aan bod komen zijn weergegeven in figuur 13.3. Merk op dat alle tabellen eigendom zijn van gebruiker RuimtereisSimpel. Logisch, want die heeft ze gecreëerd.

Metatabel voor database zelf Rdb$database

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 13.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-, deleteof 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. Zoals gewoonlijk kunt u alle DML-commando’s in de volgende opgaven via rollback weer ongedaan maken. Voor eventuele DDL-commando’s hangt dat af van de instellingen van de SQL-omgeving; zie de handleiding. Metatabellen kunnen erg breed (en dus moeilijk of niet leesbaar) zijn; raadpleeg ook hiervoor de handleiding. OPGAVE 13.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 13.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 13.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 insertcommando 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 13.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?

3

Metatabellen voor kolommen en domeinen

Meta-informatie over kolommen en domeinen

In deze paragraaf bestuderen we twee nieuwe metatabellen: Rdb$relation_fields en Rdb$fields. Rdb$relation_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.

Rdb$fields

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. 3.1

LOKALE KOLOMKENMERKEN

Figuur 13.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 13.4 is voldaan.

FIGUUR 13.4

Metatabel voor lokale kolomkenmerken

Tabellen en lokale kolomkenmerken: multipliciteiten

In figuur 13.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 …

Systeemvlag

Een logische waarde (ja/nee, waar/onwaar) wordt vaak een ‘vlag’ genoemd.

 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.

FIGUUR 13.5 3.2

Metatabel voor domeinen (nietlokale kolomkenmerken)

Tabellen en lokale kolomkenmerken: populatie

NIET-LOKALE KOLOMKENMERKEN EN DOMEINEN

Een verwijssleutelkolom en de corresponderende primaire-sleutelkolom delen kenmerken zoals datatype en lengte. In paragraaf 7 van leereenheid 9 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 13.6.

De populatie in figuur 13.7 illustreert dit. In het create-script van RuimtereisSimpel zijn vier domeinen gedefinieerd: Reisnr, Objectnaam, Geldbedrag en Volgnr. Op Reisnr zijn twee kolommen gebaseerd, op Objectnaam drie, en op Geldbedrag en Volgnr ieder één. Alle vier de 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 13.6

Domeinen en niet-lokale kolomkenmerken: multipliciteiten

Merk op dat ook de metakolommen op domeinen zijn gebaseerd. Zichtbaar in figuur 13.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 5 van leereenheid 9). Bijvoorbeeld: waarden van het domein Geldbedrag, gespecificeerd als numeric(5,2), worden opgeslagen als een integer (code 8).

FIGUUR 13.7

Domeinen en niet-lokale kolomkenmerken: populatie

OPGAVE 13.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 13.7? OPGAVE 13.6

Geef een select-statement voor een overzicht met tabelnaam, kolomnaam en kolompositie van alle gebruikerskolommen. OPGAVE 13.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 null’s.  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.

4

Overige dictionarytabellen

In deze paragraaf noemen we kort een paar andere interessante metatabellen. In de InterBase 6.0 Language Reference (zie de cursussite) kunt u alle metatabellen opzoeken. (De data dictionary van Firebird is grotendeels gelijk aan die van InterBase.)

Metatabel voor indexen Rdb$indices Rdb$index_segments

Indexen Informatie over eventuele indexen die voor een database zijn aangemaakt wordt opgeslagen in de metatabellen Rdb$indices en Rdb$index_segments, zie figuur 13.8. De tabel Rdb$indices bevat de enkelvoudige kenmerken van indexen, waaronder de 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.

FIGUUR 13.8

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 13.8 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.

Metatabel voor constraints

Rdb$relation_constraints

Constraints De eigenschappen van constraints worden opgeslagen in metatabel Rdb$relation_constraints, zie figuur 13.9.

FIGUUR 13.9

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$constraint_name-kolomwaarden. Opmerking: hoe het zit met not null-constraints laten we over aan de lezer. Metatabellen voor gebruikers en rollen

Rdb$users Rdb$roles

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.

Metatabel voor privileges

Rdb$user_privileges

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 voor sequences, waarmee automatisch volgnummers worden gegenereerd. De tabel Rdb$generators bevat één rij voor elke automatische nummersequence, zie figuur 13.10. De naam herinnert nog aan de oude benaming van sequences: generators. Het aardige van deze tabel is dat we eraan kunnen zien dat Firebird ook zelf de nodige volgnummersequences maakt. Welke dat zijn, hebben we gedeeltelijk al kunnen zien in deze leereenheid: 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 13.10

Metatabel voor sequences

De volgende opgaven geven een paar voorbeelden van wat we via de dictionarytabellen zoal te weten kunnen komen. OPGAVE 13.8

Geef een select-commando dat alle recursieve foreign key-constraints geeft. OPGAVE 13.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. 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.

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).

Metaview

Nut van metaviews in Firebird

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 metaviews. In de SQL2-standaard is een aantal metaviews opgenomen, waarvan er in de Firebirddocumentatie vier zijn overgenomen. Een ervan behandelen we, als voorbeeld, in de volgende paragraaf. 5.2

ANSI/ISO-METAVIEWS

Als voorbeeld van een ANSI/ISO-metaview geven we in tabel 13.1 de specificatie van de Constraints_column_usage-view, die alle kolomconstraint-combinaties geeft voor primary key-, unique- of foreign keyconstraints. TABEL 13.1

De Constraints_column_usage-metaview

kolomnaam

datatype

lengte

beschrijving

table_name

char

31

column_name constraint_name

char char

31 31

tabel waarvoor de constraint is gedefinieerd kolom genoemd in constraintdefinitie unieke constraintnaam

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 =========== REIS HEMELOBJECT HEMELOBJECT BEZOEK BEZOEK BEZOEK BEZOEK

COLUMN_NAME ============ NR NAAM MOEDEROBJECT VOLGNR REIS REIS HEMELOBJECT

CONSTRAINT_NAME ============================ PK_REIS PK_HEMELOBJECT FK_SATELLIET_VAN_HEMELOBJECT PK_BEZOEK PK_BEZOEK FK_BEZOEK_TIJDENS_REIS FK_BEZOEK_AAN_HEMELOBJECT

OPGAVE 13.10

Maak een metaview vRelations bij tabel Rdb$relations, die deze beperkt tot de in deze leereenheid genoemde kolommen. Neem als kolomnamen de oorspronkelijke kolomnamen, maar zonder de prefix rdb$. 6

Van tabel ‘Tabel’ naar volledige database

We beëindigen deze leereenheid met een mooi aspect van relationele databases: het zelfbeschrijvende karakter. 6.1

Data dictionary is zelfbeschrijvend

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 13.11.

FIGUUR 13.11

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 figuur 13.12.

FIGUUR 13.12

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.

Zelfbeschrijvende database

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. Geen apart metametaniveau

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.

SAMENVATTING Paragraaf 1

Gegevens om gegevensstructuren vast te leggen heten metagegevens. Deze zijn zelf gestructureerd volgens een metastructuur. De metastructuur van relationele schema’s beschrijft wat deze schema’s gemeenschappelijk hebben. Die schema’s zijn op hun beurt de structuurbeschrijving van een concrete relationele database. Tegenover het metaniveau van de structuurbeschrijving staat het objectniveau van concrete relationele schema’s.

De metastructuur van relationele schema’s is zelf ook relationeel. Metagegevens worden dus, net als gegevens van het objectniveau, vastgelegd in relaties (tabellen). Deze vormen samen de data dictionary of systeemcatalogus. Zo is er een metatabel met gegevens over alle tabellen van de database, inclusief de metatabellen zelf. De structuur van de data dictionary is – afgezien van het feit dat deze altijd relationeel is – dialectspecifiek. Paragrafen 2-4

Om te illustreren hoe een data dictionary kan zijn opgezet, is in deze paragrafen een deel van de data dictionary van Firebird behandeld, aan de hand van een eenvoudige gebruikersdatabase. Hieruit komt naar voren: – Elke database heeft een eigen data dictionary. – Elke data dictionary heeft dezelfde tabellen. – De populatie van de data dictionary wordt deels bepaald door structuur van de gebruikersdatabase, voor een ander deel door de data dictionary zelf; dit laatste deel van de populatie is dus in elke data dictionary hetzelfde. – Het effect van een DDL-statement is in principe equivalent met een verzameling DML-statements uitgevoerd op de data dictionary. Beperkings- of gedragsregels, werkzaam op een data dictionary (bijvoorbeeld via triggers), en autorisatiebeperkingen zullen de mogelijkheden in de praktijk beperken of zelfs tot nul reduceren.

Paragraaf 5

Metaviews of systeemviews zijn views op de data dictionary. Deze kunnen dienen om gebruikers op een veilige manier bepaalde rechten op de data dictionary te geven, bijvoorbeeld om objecten waarvan zij eigenaar zijn, in te zien. Een ander doel kan zijn: het bieden van een gestandaardiseerde, dus dialectonafhankelijke manier om gegevens te lezen uit de data dictionary. ANSI/ISO heeft een aantal van dergelijke metaviews gespecificeerd.

Paragraaf 6

Elke gebruikersdatabase (database van het objectniveau) is op te vatten als een uitbreiding van zijn data dictionary. Aangezien de data dictionary niet alleen de structuur van de gebruikersdatabase beschrijft, maar ook zijn eigen structuur, bestaat er geen apart meta-metaniveau: mochten we daar al van willen spreken, dan valt dit samen met het metaniveau. Dit correspondeert met het feit dat de data dictionary zelf ook een ‘gewone’ database is en als zodanig tot het objectniveau behoort: ‘metaniveau’ en ‘objectniveau’ zijn relatieve begrippen.

ZELFTOETS

De voorbeelddatabase voor deze zelftoets is RuimtereisSimpel. Zie voor het strokendiagram figuur 13.1. 1

a Geef een DML-statement om de namen van alle niet-metatabellen op te vragen. b Geef een DML-statement om de kolomnamen van de tabel Bezoek te tonen.

2

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 worden verwijderd. b Pas het script zodanig aan dat het een drop-script wordt voor alle gebruikersviews. 3

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.

4

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 volgende strokendiagram (zie figuur 13.13)  namen van tabellen en kolommen en van andere databaseobjecten worden intern in hoofdletters opgeslagen.

FIGUUR 13.13

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.

TERUGKOPPELING 1

Uitwerking van de opgaven

13.1

Geen uitwerking.

13.2

SQL-statement:

insert into Rdb$relations (rdb$relation_name) values ('Test');

We willen straks de veranderingen ongedaan maken door middel van een rollback, maar mocht u nu toch een commit willen proberen, dan is dit het resultaat: NB

unsuccessful metadata update -TABLE Test -Can't have relation with only computed fields or constraints.

We kunnen dus niet écht een tabel zonder kolommen toevoegen. 13.3

Geen uitwerking.

13.4

Inspectie van Rdb$database: select * from Rdb$database;

geeft als resultaat: RDB$DESCRIPTION RDB$RELATION_ID RDB$SECURITY_CLASS RDB$CHARACTER_SET_NAME =============== =============== ================== ======================

131 NONE

a In de kolom rdb$relation_id wordt het eerstvolgende vrije ‘id’ voor gebruikerstabellen (en -views) bewaard. Dit blijkt uit inspectie van de tabel Rdb$relations die ook zo’n id-kolom blijkt te hebben. b De kolom rdb$character_set_name bevat de naam van een character set (zie paragraaf 3.2 van leereenheid 4), indien Firebird een andere character set gebruikt dan die van het besturingssysteem. c Er is geen kolom voor de databasenaam omdat het ‘single point of definition’ hiervan de fdb-bestandsnaam is. Deze is vastgelegd op het niveau van het besturingssysteem. 13.5

Waar nu de kunstmatige domeinnaam Rdb$1 staat (in elke tabel één keer), zou Datum staan. Verder zou het volgnummer in eventuele andere kunstmatige domeinnamen anders luiden. Het rdb$field_type bij Datum zou 35 zijn.

13.6

SQL-statement:

select rdb$relation_name, rdb$field_name, rdb$field_position from Rdb$relation_fields where rdb$system_flag = 0;

13.7

In één script: insert into Rdb$relations (rdb$relation_name, rdb$owner_name, rdb$system_flag) values ('TEST', 'RUIMTEREISSIMPEL', 0); insert into Rdb$fields (rdb$field_name, rdb$field_type, rdb$field_length, rdb$system_flag) values ('RDB$101', 35, 8, 0); insert into Rdb$fields (rdb$field_name, rdb$field_type, rdb$field_length, rdb$system_flag) values ('RDB$102', 14, 1, 0); insert into Rdb$relation_fields (rdb$relation_name, rdb$field_name, rdb$field_source, rdb$field_position, rdb$system_flag) values ('TEST', 'KOLOM1', 'RDB$101', 0, 0); insert into Rdb$relation_fields (rdb$relation_name, rdb$field_name, rdb$field_source, rdb$field_position, rdb$system_flag) values ('TEST', 'KOLOM2', 'RDB$102', 1, 0); commit;

Vervolgens: insert into Test values (current_date, 'a');

Met een select * from Test;

controleren we vervolgens of het gelukt is, met als resultaat: KOLOM1 KOLOM2 ========================= ====== 2016-07-08 00:00:00.0000 a

13.8

SQL-statement

voor alle recursieve foreign key-constraints:

select Refc.rdb$constraint_name from Rdb$Ref_constraints Refc join Rdb$relation_constraints Relc1 on Refc.rdb$constraint_name = Relc1.rdb$constraint_name join Rdb$relation_constraints Relc2 on Refc.rdb$const_name_uq = Relc2.rdb$constraint_name where Relc1.rdb$relation_name = Relc2.rdb$relation_name;

13.9

Eerst raadplegen we de tabel Rdb$generators om de naam van de sequence te achterhalen die de indexvolgnummers bijhoudt. Dit blijkt Rdb$index_name te zijn. Vervolgens vragen we aan de sequence Rdb$index_name wat zijn eerstvolgende uit te delen nummer is: select next value for rdb$index_name from Rdb$database;

Toelichting: als brontabel kozen we Rdb$database, omdat deze maar één rij heeft en in elke data dictionary zit. Het antwoord is: GEN_ID ===================== 1

Dat wil zeggen dat er nog geen nummers aan indexen waren uitgedeeld. Een nadeel van deze oplossing is overigens dat hij een neveneffect heeft: het gezochte nummer wordt opgehoogd, en de eerste index die een automatisch nummer moet krijgen zal nu nummer 2 krijgen. 13.10

SQL-statement:

create view vRelations (relation_name, owner_name, system_flag) as select rdb$relation_name, rdb$owner_name, rdb$system_flag from Rdb$relations; 2

1

Uitwerking van de zelftoets

a Voor DML kunnen we kiezen uit select, insert, update en delete. Met een select-statement rechtstreeks op de data dictionary krijgen we het gevraagde overzicht: select rdb$relation_name from Rdb$relations where rdb$system_flag = 0;

b Uitgaande van RuimtereisSimpel toont het volgende select-statement de kolomnamen van tabel Reis: select rdb$field_name from Rdb$relation_fields where rdb$relation_name = 'REIS';

2

a

Geen uitwerking.

b drop table wordt drop view. Verder kan de eerste selectieconditie worden weggelaten, omdat de data dictionary zelf geen gebruikersviews bevat. Omkeren van de tweede conditie geeft het verlangde resultaat: select 'drop view ' || trim(rdb$relation_name) || ';' from Rdb$relations where rdb$view_blr is not null;

3

Een oplossing geeft het strokendiagram van figuur 13.14, met de drie gevraagde ‘kern-metatabellen’. Toelichting – De tabel Gebruiker bevat naast de ‘supergebruiker’ Systeem twee gewone gebruikers: Sofie en Lisa. Sofie is eigenaar van één tabel, genaamd Test (met twee kolommen). Lisa is eigenaar van één tabel, die ook Test heet (met één kolom), en van een tweede tabel, die Klant heet (met twee kolommen). – De tabel Tabel bevat de drie metatabellen en de drie gebruikerstabellen. – De tabel Kolom bevat de zeven kolommen van de data dictionary en de vijf kolommen van de gebruikerstabellen. – We zijn uitgegaan van de aanname dat namen van databaseobjecten in de data dictionary in hoofdletters worden opgeslagen.

FIGUUR 13.14

Strokendiagram metatabellen

4

Zie figuur 13.15.

FIGUUR 13.15

Populatiediagram

Bijlage 1

Firebird  functies en contextvariabelen

Firebird-functies

De belangrijkste Firebird-functies vallen in de volgende groepen uiteen:  stringfuncties  rekenkundige functies  wiskundige functies  datumfuncties  conditionele functies  typeconversiefunctie  null-vervangfunctie

Contextvariabelen

Naast de functies zijn er de parameterloze contextvariabelen, die hun waarde krijgen van het rdbms. Documentatie: via internet of cursussite

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 char_length(str)

lengte van str

lower(str)

str in kleine letters

upper(str)

str in hoofdletters

trim(… str ...)

verwijdert spaties of gegeven string aan begin en/of einde van str

rpad(str, doellengte …) lpad(str, doellengte …)

vult str rechts (rpad) of links (lpad) aan met spaties (of andere gegeven string) tot doellengte

right(str, lengte) left(str, lengte)

rechterdeel (linkerdeel) van str; aantal tekens gegeven door lengte

substring(str from startpos) substring(str from startpos for length)

substring van str vanaf startpos (met for: dan ook aantal tekens)

position(substr in str) position(substr, str …)

eerste positie van substr in str (in kommasyntax kan nog startpositie worden opgegeven)

replace (str, zoek, vervang)

vervangt zoek door vervang in str

hash(str)

hash-waarde voor str

Rekenkundige functies abs(getal)

absolute waarde

round(getal) round(getal, schaal)

afronding (grootteorde kan optioneel als schaal worden meegegeven)

trunc(getal) trunc(getal, schaal)

afkappen (= afronding richting 0; grootte-orde kan optioneel worden meegegeven)

floor(getal)

grootste gehele getal kleiner dan of gelijk aan getal

ceiling(getal)

kleinste gehele getal groter dan of gelijk aan getal

mod(a, b)

a modulo b (rest bij gehele deling; niet-integer argumenten worden eerst afgerond)

power(x, y)

machtsverheffen: x tot de macht y

maxvalue(expr …) minvalue(expr …)

maximum- en minimumwaarde van een expressielijst

Wiskundige functies sqrt(getal)

vierkantswortel van getal

sin(hoek) cos(hoek) tan(hoek) cot(hoek)

sinus, cosinus, tangens en cotangens van hoek (in radialen)

asin(getal) acos(getal) atan(getal)

arcsinus, arccosinus en arctangens van getal; geretourneerde waarde is een hoek (in radialen)

sinh(hoek) cosh(hoek) tanh(hoek)

sinus, cosinus en tangens hyperbolicus van hoek (in radialen)

pi()

pi ( = 3,1415…)

exp(getal)

e tot de macht getal

ln(x)

natuurlijke (e-)logaritme van x

log10(x)

10-logaritme van x

log(x, y)

x-logaritme van y

rand()

randomgetal tussen 0 en 1

Datumfuncties extract(element from datumtijd)

retourneert een element (van year tot millisecond) uit een date-, time- of timestamp-waarde.

dateadd(…)

vermeerdert (eventueel met negatieve waarde) een datum/tijd met een aantal tijdseenheden (van type year tot millisecond)

datediff(eenheid from moment1 to moment2) datediff(eenheid, moment1, moment2)

verschil van twee data of tijdstippen, gemeten in eenheid (van year tot millisecond)

Conditionele functies iif(conditie, result1, result2)

als conditie true dan result1, anders result2; shorthand voor: case when conditie then result1 else result2 end

nullif(expr1, expr2)

retourneert expr1 als expr1 ongelijk is aan expr2; indien gelijk, dan null (gebruikt om uitzonderingen  lees: expr2  uit te filteren)

Typeconversiefunctie cast(expr as doeltype)

converteert expr naar doeltype

Null-vervangfunctie coalesce(expr, vervangexpr1, …)

retourneert expr of, als deze null is, de eerste van de vervangexpressies die niet null is

Contextvariabelen current_user

huidige gebruiker

current_role

rol waarmee de huidige gebruiker is ingelogd

current_date

huidige serverdatum

current_time

huidige servertijd

current_timestamp

huidige serverdatum en -tijd

old

de oude rij bij een delete- of update-trigger (in triggercode)

new

de nieuwe rij bij een insert- of update-trigger (in triggercode)

inserting

boolean die true is bij een insert en anders false (in triggercode)

deleting

boolean die true is bij een delete en anders false (in triggercode)

updating

Now

boolean die true is bij een update en anders false (in triggercode)

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.

Bijlage 2

Voorbeelddatabases

Deze bijlage bevat de populatiediagrammen van de in dit cursusdeel gebruikte voorbeelddatabases. Strokendiagrammen vindt u in de betreffende leereenheden. OpenSchool

Orderdatabase (ook: OrderdatabaseC en OrderdatabaseD)

Ruimtereisbureau (ook: RuimtereisbureauD en RuimtereisSimpel; RuimtereisSimpel: alleen tabellen Reis, Hemelobject en Bezoek)

Toetjesboek (ToetjesboekKS: zie SQL-omgeving)

'' -*/ /* || =

I: 52 I: 107 I: 107 I: 107 I: 51 II: 20

1NV 1-tier 2NV 2-tier 3GL 3NV 3-tier 4GL 4NV 5NV

I: 68 I: 34 I: 68, 73 I: 34 II: 140 I: 76 I: 35 II: 140 I: 85 I: 85

Aanroep van stored procedure II: 142 Access mode II: 124 ACID II: 113 Activeren van een index II: 84 Actuele parameter II: 143 Actuele rij I: 217; II: 18 Afgeleide kolom I: 20; II: 49 Afleidbare kolom II: 49 After-trigger II: 144 Alfanumeriek I: 110 Alfanumerieke operator I: 114 Alfanumerieke waarde I: 110 Algoritme I: 197 conceptuele ~ I: 197; II: 73, 75 Alias I: 105, 148 kolom~ I: 105 tabel~ I: 148 all I: 244 alter domain II: 55 alter index ... (in)active II: 84 alter sequence II: 59, 60 alter table II: 45 alter trigger II: 159 ~ ... (in)active II: 159 Alternatieve sleutel I: 20, 54; II: 51 ~specificeren II: 51 Alternatieven voor intersect en except I: 133 and I: 56, 58 ANSI/ISO I: 28; II: 186 ~-metaviews II: 186 ~-standaards van SQL I: 28 any I: 244, 245 Applicatieconstraint II: 160 Applicatieontwikkelaar I: 23 Applicatieserver I: 36 Architectuur I: 33

Argument I: 116 as I: 105 asc I: 127 ASCII I: 111, 122 ~-ordening voor strings Atomicity II: 113 auto_increment II: 60 Autojoin I: 160 inner ~ I: 161 outer ~ I: 164 avg I: 181

I: 122

Backup II: 87 Balanceren door inactiveren en activeren II: 87 Balanceren van index II: 86, 87 Batchverwerking I: 31 BCNV I: 79 Bedrijfsregel I: 35 Before-trigger II: 144 Begeleiding I: 11 begin II: 142 Beperkingsregel I: 19, 25 Beperkt aantal interne datatypen per rdbms II: 48 Betere structuur door join-operator I: 152 between ... and I: 123, 124 Between-join I: 166 Beveiliging I: 21 Binaire operator I: 113 Binaire-boomindex II: 78 blob II: 50 Boyce-Codd-normaalvorm I: 79 Brede primaire sleutel I: 20 Brede subselect I: 224 Brede verwijssleutel I: 20 Brontabel I: 103 Cartesisch product I: 146 Cascading delete I: 21; II: 13 Cascading update II: 23 ~ van primaire sleutel II: 23 case I: 118 cast I: 113, 117 Cast van null I: 118 Casting I: 113 char I: 110 Character I: 110 Character set I: 111 check-constraint II: 51, 52 ~ op domein II: 55 ~ op kolom II: 51 Client-server I: 33 Client-tier I: 34

Closed world assumption I: 243 coalesce I: 51,132 Codd I: 53, 54 ~-relationaliteit I: 54 ~-relationeel II: 50 Collation order I: 111, 122 Commentaar I: 27, 107 Commentaarcode I: 107; II: 12 commit II: 108 Commitmoment II: 12, 39 Computed column II: 158 Concatenatie I: 51, 104, 114 Concatenatie-operator || I: 51 Concatenatietruc I: 225 Concateneren I: 51 Conceptuele algoritme I: 197; II: 73, 75 Conceptuele verwerkingsvolgorde I: 107, 148 Concurrency II: 110 Concurrency control I: 22; II: 111, 112 multiversion ~ II: 112 optimistic ~ II: 112 Concurrente transacties II: 110 Conjunctie I: 56 Connect II: 41 connect II: 41 Connectie II: 41 Consistency II: 113 Constante I: 104 Constante null I: 49 Constraint I: 25; II: 50, 160, 184 applicatie-~ II: 160 check-~ II: 51 ~ in data dictionary II: 184 ~ toevoegen of verwijderen II: 45 ~naam II: 43 foreign key-~ II: 50 inline ~ II: 46 out-of-line ~ II: 46 primary key-~ II: 50 unique-~ II: 51 Contextvariabele II: 146, 156, 197 ~ deleting II: 156 ~ inserting II: 156 ~ new II: 146 ~ old II: 146 ~ updating II: 156 Converteren I: 113 Cost-based optimizer II: 76 count I: 179 create database II: 40 ~-statement II: 40 create domain II: 54 create exception II: 153

create index II: 81 ~ computed by (expression) create procedure II: 142 create sequence II: 59 create table II: 42 create trigger II: 147 create view I: 246; II: 56 Cursussite I: 11

II: 82

DAL Zie Data authorization language Dalend ordenen I: 127 Data authorization language I: 24, 27 Data control language I: 24, 28 Data definition language I: 24; II: 38 ~-statement in transactie II: 39, 110 Data dictionary II: 39, 175 constraint in ~ II: 184 database in ~ II: 177 domein in ~ II: 179 gebruiker in ~ II: 184 index in ~ II: 183 kolom in ~ II: 179 privilege in ~ II: 185 rol in ~ II: 184 tabel in ~ II: 179 zelfbeschrijvende ~ II: 187 Data manipulation language I: 24, 26 Database I: 17; II: 40, 87, 177, 188 ~ administrator I: 23 ~ creëren II: 40 ~ en gebruikers II: 40 ~ in data dictionary II: 177 ~ verwijderen II: 41 ~connectie II: 41 ~-export II: 40 ~object II: 38 ~opslag II: 40 ~page II: 87 zelfbeschrijvende ~ II: 188 Databasemanagementsysteem I: 21 relationeel ~ I: 21 Databaseontwerper I: 23 Databasepage II: 87 Databaseschema I: 24 Databasesysteem I: 21 architectuur van ~ I: 33 Databasetaal I: 24 Datadictionary II: 39 Datatype I: 109; II: 47 extern ~ II: 48 intern ~ II: 48 logisch ~ II: 53

Datatypen van SQL2 I: 110 Datawarehouse I: 31; II: 11, 94, 95 date I: 110 Datuminvoer I: 111 Datumoperatoren I: 114 DBA Zie Database administrator Dbms Zie Databasemanagementsysteem DCL I: 24 DDL Zie Data definition language De Morgan I: 57 Deadlock II: 112, 132 default II: 45, 48, 55 ~-specificatie II: 48 ~-specificatie in create domainstatement II: 55 Defragmentatie II: 87 delete II: 18 ~ met subselect II: 19 ~ met where II: 18 ~ zonder restrictie II: 18 Deleteregel II: 13, 51 delete-statement II: 18 deleting II: 156 Deltaproblematiek II: 39, 159 Deltaproblemen II: 35 ~ bij sequences II: 60 ~ bij views II: 57 ~ op domeinniveau II: 55 ~ op kolomniveau II: 49 ~ op tabelniveau II: 45 Denormaliseren I: 72, 145, 146, 186 Derde normaalvorm I: 76 Determinant I: 71 Dialect I: 28 Dirty read II: 116 disable constraint II: 46 Disjunctie I: 56 distinct I: 108, 181, 222 groeperen in plaats van ~ I: 223 DML Zie Data manipulation language Domein II: 38, 53, 180 ~ creëren II: 54 ~ in data dictionary II: 179 ~ verwijderen II: 55 ~ wijzigen II: 55 Driewaardige logica I: 58 Drill down I: 31; II: 11 Drill up I: 31; II: 11 drop ~ constraint II: 45 ~ database II: 41 ~ domain II: 55

~ exception II: 159 ~ index II: 81 ~ procedure II: 159 ~ sequence II: 60 ~ table II: 44 ~ trigger II: 159 ~ view I: 247; II: 57 Durability II: 114 Dynamische regel I: 25 Eén-veel-associatie I: 20 Eerste normaalvorm I: 68 Eigenaar II: 176 Eindgebruiker I: 23 Embedded SQL I: 29 enable constraint II: 46 end II: 142 Event II: 144, 145 except I: 129, 133 Exception II: 153, 154 ~ creëren II: 153 ~ verwijderen II: 159 Exclusieve lock II: 111 execute II: 141, 142 Executievolgorde van triggers exists I: 230 Export II: 40 Extensie I: 109 Extern datatype II: 48

II: 160

false I: 56 Firebird I: 28; II: 87 ~ Server Manager II: 87 ~-functies II: 197 first II: 88 Formaatstring I: 123 Formele parameter II: 142, 144 full outer join I: 167 Full table scan II: 76, 77, 84, 86 Functie I: 115, 179 argument van ~ I: 116 cast-~ I: 117 geretourneerde waarde van ~ I: 116 resultaatwaarde van ~ en argumenten I: 115 statistische ~ I: 179 typecast-~ I: 117 Functionele afhankelijkheid I: 70, 71 triviale en niet-triviale ~ I: 79 Fysieke gegevensonafhankelijkheid I: 22, 32 Fysieke navigatiepad II: 76

Gebalanceerde index II: 86 Gebruiker II: 184 ~ in data dictionary II: 184 Gebruikersinformatie II: 39 Gecentraliseerd clientserversysteem I: 34 Gecontroleerde redundantie II: 114 Gecorreleerd II: 21 Gecorreleerde subselect I: 217, 227, 228 ~ met exists I: 230 ~ met not exists I: 233 Gedeelde lock II: 111 Gedetermineerde I: 71 Gedistribueerde database II: 114 Gedragsregel I: 25 Geen apart meta-metaniveau II: 188 Gegevensonafhankelijkheid I: 22, 32 fysieke ~ I: 22, 32 logische ~ I: 32 Gemengde operatoren I: 115 Generator II: 59, II: 185 ~ in systeemcatalogus II: 185 Genest groeperen I: 195 Geneste subselect I: 237 Gereserveerd woord I: 106 Geretourneerde waarde I: 116 Geschatte studielast I: 9 Grijsteksten I: 11 Groeperen I: 178, 223 genest ~ I: 195 ~ als denormaliseren I: 186 ~ en standaardisatie I: 201 ~ in plaats van distinct I: 223 ~ op berekende expressie I: 189 ~ op optionele kolom I: 190 onverwacht verfijnd ~ I: 192 verfijnd ~ I: 185 GrootOrderdatabase II: 72 GrootOrderdatabaseKS II: 93 group by I: 183, 192 ~-clausule I: 183 having I: 188 ~ of where I: 188 Herhaling II: 149 Historieprobleem II: 12 Hogere normaalvormen I: 79 Hypothetische verwerking volgens het conceptuele algoritme II: 75 Identieke operatie I: 103 Identificatie I: 18 Identifier I: 106

if ... then ... II: 146, 148 if ... then ... else ... II: 148 iif I: 119 Illustratieve populatie I: 73 Impliciete ~ transactiemodel II: 109 ~ typecasting I: 118 in........ I: 126 Inactiveren II: 84 Inconsistent analysis II: 117 Inconsistent-analysisprobleem II: 117, 118 Inconsistent data II: 116 Inconsistent-dataprobleem II: 116 Inconsistentie I: 72 Incorrect summary II: 117 ~-probleem II: 117 Index II: 76, 77, 80, 183 balanceren van ~ II: 86, 87 binaire-boom~ II: 78 ~ (in)activeren II: 84, 87 ~ creëren II: 81 ~ en order by II: 82 ~ in data dictionary II: 183 ~ verwijderen II: 81 ~structuur II: 77 lineaire-lijst~ II: 77 onderhoud van ~ II: 86 selectiviteit van ~ II: 85 standaard~ II: 82 unieke ~ II: 82 Initieel afleidbaar I: 39 Inline II: 46 ~ constraint II: 46 Inloggen II: 41 Inner autojoin I: 160 Inner join I: 144, 145, 146, 152 ~ via productoperator I: 146 insert II: 14 enkelvoudige ~ II: 14 meervoudige ~ II: 15 inserting II: 156 Integriteitscontrole I: 22 Intern datatype II: 48 Interne functie I: 115 Interpretatie van null I: 52 Interpreter I: 106; II: 73 intersect I: 129 Introductie I: 10 is not null I: 48, 126 is null I: 48, 126 ISO I: 28 ‘Is-element-van’-operator I: 126 Isolation II: 113

Isolation level II: 120 ~ read committed II: 121, 122 ~ read uncommitted II: 122 ~ repeatable read II: 121 ~ serializable II: 121 ~ snapshot II: 122 ~ snapshot table stability II: 123 ~s in de SQL-standaard II: 120 ~s in Firebird II: 122 Join I: 143, 165, 191, 220 between-~ I: 166 full outer ~ I: 167 inner ~ I: 144 ~ met statistische gegevens I: 191 ~ over brede sleutel I: 165 ~ over niet-sleutelverwijzing I: 166 ~conditie I: 147 ~navigatie I: 115, 220 ~operatoren I: 151 ~-uniciteitsregel I: 82, 83 left outer ~ I: 150 outer ~ I: 150, 153 right outer ~ I: 150, 167 samengestelde ~ I: 155 Joinen I: 145, 157 ~ als denormaliseren I: 145 ~ vanwege selectieconditie I: 157 Joinquery’s I: 191 statistische ~ I: 191 Kandidaatsleutel I: 54 Kernbegrippen I: 10 Keyword I: 106 Kladblokberekening I: 48 Klimmend ordenen I: 127 Kolom II: 45, 47, 49, 179 afleidbare ~ II: 49 ~alias I: 105 ~definitie II: 47 ~ in data dictionary II: 179 ~ toevoegen of verwijderen II: 45 lokale ~kenmerken II: 179 niet-lokale ~kenmerken II: 179 Kritisch voorbeeld BCNV I: 80 Kunstmatige sleutel II: 58, 93 Language driver I: 112 Leerdoelen I: 10 Leereenheid I: 10 Leerkern I: 10 Left outer join I: 150, 153 ~ via union I: 150

Lege string I: 46, 49, 50, 52 Level of isolation II: 124 like I: 123 Lineaire-lijstindex II: 77 Load balancing II: 160 Lock II: 111, 120 ~ conflict II: 131 ~ conflict behaviour II: 124 exclusieve ~ II: 111 gedeelde ~ II: 111 read-~ II: 111 write-~ II: 111 Locking granularity II: 112 Log file II: 113 Logaritmisch zoeken II: 80 Logica I: 56, 58 driewaardige ~ I: 58 tweewaardige ~ I: 56 Logisch datatype II: 53 Logische algebra I: 56 Logische equivalentie I: 57 Logische expressie I: 124 Logische gegevensonafhankelijkheid I: 32 Logische navigatiepad II: 76 Logische operator I: 56, 124 Logische waarde I: 124 Logische waarde ‘onbepaald’ I: 58 Lost update II: 115 max I: 182 Meervoudige (samengestelde) joins I: 155 Meta ~gegevens II: 174 ~-informatie II: 39 ~niveau II: 175, 188 ~structuur II: 174, 175 ~tabel II: 175 ~view II: 185, 186 Meta-metaniveau II: 188 Metatabel II: 175 ~ voor constraints II: 184 ~ voor database zelf II: 177 ~ voor domeinen (niet-lokale kolomkenmerken) II: 180 ~ voor indexen II: 183 ~ voor lokale kolomkenmerken II: 179 ~ voor privileges II: 185 ~ voor gebruikers en rollen II: 184 ~ voor kolommen en domeinen II: 179 ~ voor tabellen en gebruikers II: 175 MGA II: 112, 123

Middle tier I: 34, 35 Middleware I: 35 min I: 182 Minimaxprobleem I: 195 Multi-event-trigger II: 156 Multi-generational architecture II: 112 Multi-session II: 111 Multi-user II: 111 Multiversion concurrency control II: 112 Muziekdatabase I: 99 MVCC II: 112 Naamgeving sleutelkolommen I: 69 Naamgevingsconventie I: 69, 75, 106 Navigatie I: 155, 224 join~ I: 220 ~ over brede sleutel I: 224 ~pad I: 154, 155 ~ richting ouder-kind (outer join) I: 193 subselect~ I: 220 Negatie I: 56 new II: 146 next value for … II: 59 Niet-gecorreleerd II: 21 Niet-gecorreleerde subselect I: 215, 228 Niet-sleutelkolom I: 74 Non-repeatable read II: 116 Normaalvorm I: 68 Boyce-Codd-~ I: 79 derde ~ I: 76 eerste ~ I: 68 tweede ~ I: 68, 73 vierde ~ I: 85 vijfde ~ I: 85 Normalisatie I: 67 not I: 56, 58 not in I: 234 not null II: 45, 48 ~ in create domain-statement II: 55 ~ via check-constraint II: 52 Now II: 199 Null I: 25, 47, 126 ~s vooraf elimineren I: 191 ~-waarde I: 48 null I: 47, 56 interpretatie van ~ I: 52, 53 ~-constante I: 47 ~-vervangfunctie I: 51 numeric I: 110 Numerieke operatoren I: 113

Objectniveau II: 174 OLAP Zie Online analytical processing old II: 146 OLTP Zie Online transaction(al) processing ‘Omkeren’ van vraagstelling I: 233 Onbepaald I: 124 Online analytical processing I: 31; II: 11 Online transaction(al) processing I: 30, 31; II: 10, 11, 94 Onverwacht verfijnd groeperen I: 192 Opdracht I: 25 Opdrachtafhandeling I: 21 Open world assumption I: 243 Operand I: 113, 135 Operator I: 113, 135, 152 alfanumerieke ~ I: 114 binaire ~ I: 113 gemengde ~ I: 115 numerieke ~ I: 113 ~ en operand I: 113 ~ except I: 133 ~ intersect I: 133 ~ left outer join I: 153 ~ like voor zoeken op deelstring I: 123 ~ union I: 130 prioriteit van ~ I: 114, 124 relationele ~ I: 129 string~ I: 114 ternaire ~ I: 123 unaire ~ I: 113 vergelijkings~ I: 121 verzamelingen~ I: 129 Opgave I: 10 Opgooien en vangen van een exception II: 154 Opmaakconventie I: 106 Optimistic concurrency control II: 112 Optimizer I: 156; II: 73, 76, 77, 80, 89 cost-based ~ II: 76 keuzes van ~ tonen II: 83 rule-based ~ II: 76 Optionele kolom I: 20 or I: 56, 58 Ordening I: 122, 129 ~ op expressies I: 129 ~ via kolomvolgnummer I: 129 order by I: 127; II: 82 Ouder en kind I: 19 Outer autojoin I: 164 Outer join I: 149, 150 ~ via left outer join I: 153 ~ via right outer join I: 167 ~ via union I: 150, 151 Out-of-line constraint II: 46

Parameter II: 142, 143, 144 actuele ~ II: 143 formele ~ II: 142, 144 Parser II: 73 Partiële sleutelafhankelijkheid I: 74 Per verwijzing kiezen voor inner of outer join I: 158 Performance I: 24; II: 71, 73, 74 Performanceverbetering II: 77, 89, 91, 93 ~ door aanpassing databaseontwerp II: 91 ~ door gecontroleerde redundantie II: 93 ~ door indexen II: 77 ~ door queryaanpassing II: 89 Persoonsnamen I: 53 Phantom II: 117 ~row II: 117 Pragmatisch waarheidbegrip I: 60 Primaire sleutel I: 20, 54; II: 50 ~ en not null II: 50 Prioriteit I: 56, 114 ~ van operatoren I: 114, 124 Privilege II: 185 ~ in data dictionary II: 185 Procedure II: 159 executable ~ II: 159 select-~ II: 159 Producttabel I: 146 Projectie I: 103 ~ opererend op join I: 148 Query I: 26; II: 76, 83, 88 aantal records in ~ beperken II: 88 ~plan II: 76, 83 statistische gegevens van ~ II: 83 Rdb$database I: 48 Rdbms Zie Relationeel databasemanagementsysteem Read committed II: 121, 122 Read-lock II: 111 Read uncommitted II: 122 Read/write-conflict II: 111 Record II: 73 Recovery II: 113 Recursieve verwijzing I: 20 Redundantie I: 72 gecontroleerde ~ II: 114 Referentiële integriteit I: 19 Referentiële-integriteitsregel II: 13 Refererende-actieregel I: 21; II: 50

Regels van De Morgan I: 57 Relatie I: 17 Relationeel I: 21 Relationeel databasemanagementsysteem Relationele database I: 17 Relationele operator I: 129 Repeatable read II: 121 Replicatie II: 114 Restore II: 87 Restricted delete I: 21; II: 13 Restricted update Cascading update I: 21 Resultaatkolom I: 104 berekende ~ I: 104 constante ~ I: 104 Resultaattabel I: 103 Retrieval I: 26 Return value I: 116 Right outer join I: 150, 167 Rol II: 184 ~ in data dictionary II: 184 rollback II: 12, 108 round-functie I: 116 Rows II: 88 Ruimtereisbureau II: 36 RuimtereisSimpel II: 173 Rule-based optimizer II: 76

I: 21

Samengestelde join I: 155 ~conditie bij joinen over brede sleutel I: 165 Samengestelde opdracht II: 149 Samenvatting I: 10 Scheve groepsconditie I: 189 Scheve query I: 182, 183, 196 Schriftelijk tentamen I: 12 Script I: 107 select * I: 46, 103 select-statement in transactie II: 110 Selecteren op not null I: 126 Selecteren op nulls I: 126 Selectie I: 120 Selectieoperator having I: 188 Selectie opererend op join I: 148 Selectiviteit van index II: 85 Semantic web I: 243 Semantiek II: 73 Sequence II: 38, 58, 59, 60 ~ creëren II: 59 ~ resetten II: 59 ~ verwijderen II: 60 Serializable II: 121

Server I: 33 Server-tier I: 34 Sessie II: 41 set plan on / off II: 83 set statistics display on / off II: 83 set stats on / off II: 83 set term II: 142 set transaction II: 120 ~ in IQU II: 124 Single point of definition II: 53, 141 Single-user II: 111 Sleutel II: 91 keuze van ~ II: 91 Snapshot II: 122 ~ table stability II: 123 Spellingconventie I: 106 SQL I: 24 ~-conformance II: 122 ~-interpreter II: 73 ~-standaard I: 28 ~-standaard datatypen II: 47 Standaardindex II: 82 Starttabel I: 154, 155 Statische regel I: 25 Statistieken I: 179, 183 ~ over één groep I: 179 ~ over meerdere groepen I: 183 ~ over meerdere groepen van rijen I: 183 ~ van tabel als één groep van rijen I: 179 Statistische ~ functie I: 178, 179 ~ query I: 177 ~ verdichting I: 178, 187, 191 Stored procedure II: 141 aanroep van ~ II: 142 ~ en views II: 159 ~ uitvoeren II: 143 ~ verwijderen II: 159 String I: 110, 114 Stringoperator I: 114 Strokendiagram Populatiediagram I: 18 Structuurwijziging II: 39 Studeeraanwijzingen I: 11 Subselect I: 214 brede ~ I: 224 gecorreleerde ~ I: 217, 227 gecorreleerde ~ met exists I: 230 gecorreleerde ~ met not exists I: 233 geneste ~ I: 237 niet-gecorreleerde ~ I: 215, 228

~ als oplossing van deelprobleem I: 214 ~ in from-clausule I: 217 ~ in having-clausule I: 218 ~ in select-clausule I: 216 ~ in toekenning of selectieconditie II: 21 ~ in where-clausule I: 215 ~ met all I: 244 ~ met any I: 245 ~ met exists I: 230, 232 ~ met in I: 230, 232 ~ met not exists I: 233 ~navigatie I: 155, 220 ~navigatie over brede sleutel I: 224 sum I: 181 Syntaxis II: 73 syntaxvarianten I: 129 Systeemcatalogus Zie Data dictionary Systeemontwikkelaar I: 23 Systeemvlag II: 180 Tabel I: 17, 148; II: 38, 42, 175, 179 creëren van ~ II: 42 meta~ II: 175 ~alias I: 148 ~ in data dictionary II: 179 ~ met recursieve verwijzing II: 44 ~naam wijzigen II: 46 ~ verwijderen II: 44 virtuele ~ II: 56 Tekenconventie I: 69 Tekenrij I: 110, 114 Tekstconstante I: 104 Temporary update II: 116 ~probleem II: 116 Terminator II: 142 Ternaire operator I: 123 Terugkoppeling I: 10 Testen op null I: 49 Tier I: 34 time I: 110 timestamp I: 110 Toegangsregels I: 27 Toekenning II: 20 ToetjesboekZT II: 140 Transactie I: 29; II: 12, 39, 107, 108 concurrente ~s II: 110 DDL-statement in ~ II: 39, 110 select-statement in ~ II: 110 Transactielog II: 108

Transactiemanagement II: 110 Transactiemodel II: 109 impliciete ~ II: 109 Transaction processing II: 110 Transactioneel bedrijfssysteem II: 10, 11, 94 Transactioneel systeem I: 30; II: 11 Transitieve sleutelafhankelijkheid I: 76 Trigger II: 144, 151 executievolgorde van ~s II: 160 multi-event ~ II: 156 ~taal II: 140 ~ verwijderen II: 159 Triviale functionele afhankelijkheid I: 79 true I: 56 Tupel I: 109 Tweede normaalvorm I: 68, 73, 74 Tweewaardige logica I: 56 Two-phase commit II: 114 Typecastfunctie I: 117 Typecasting I: 113 Udf Zie User defined function Unaire operator I: 113 Uncommitted II: 116 ~ data II: 116 ~ dependency II: 116 ~-dataprobleem II: 116 ~-dependencyprobleem II: 116 Uniciteitsconstraint II: 51 Uniciteitsregel I: 20, 82 Join-~ I: 82, 83 Uniek I: 18 Unieke index II: 82 union I: 129, 130, 143 unique-constraint II: 51 unknown I: 49, 56, 58 update II: 19 ~ met subselect II: 21, 22 ~-statement II: 19 ~ van primaire sleutel II: 23 ~ van verwijssleutel II: 26 Update ~conflict II: 121, 132 ~ propagation problem II: 114 ~regel II: 51 ~verbod II: 25 Updating II: 156 User defined function I: 115, 120

value II: 55 varchar I: 110 Variabele II: 144, 150 ~ declareren II: 150 Veel-veel-associatie I: 20 Veld II: 73 Verfijnd groeperen I: 185, 193 onverwacht ~ I: 192 Verfijnd ordenen I: 128 Vergelijkingsoperator I: 121 Verplichte kolom I: 20 Verwerkingsvolgorde I: 197 Verwijzing I: 18 Verzamelingsoperator I: 129 Vierde normaalvorm I: 85 View I: 245; II: 38, 56, 185 meta~ II: 185, 186 ~ als deeloplossing van probleem II: 56 ~ creëren II: 56 ~ verwijderen II: 57 ~ wijzigen II: 58 Vijfde normaalvorm I: 85 Virtuele tabel I: 162; II: 56 Voorbeeldpopulatie I: 73 Waarheidstabel I: 56 Webapplicatieserver I: 36 Webserver I: 36 Whitespace I: 107 Witteken I: 107 Write-lock II: 111 Write/write-conflict II: 111 Zachte en harde semantiek I: 84 Zelfbeschrijvende ~ database II: 188 ~ data dictionary II: 187 Zelftoets I: 10