Skriftlig opgave til eksamen for faget»databaser«designtanker i database-nære systemer Martin Ancher Holm Juni 2010 1
Intro Denne skriftlige opgave indeholder kort de daglige tanker jeg har omkring design af systemer, der er database-nære. Dette er suppleret med en kort beskrivelse af teorien bag disse valg. Struktur i en database En database har nogle helt elementære funktioner. Man strukturerer dataene i rækker. Der er en garanti for at man ikke lægger en halv række ind. Det, man beskriver som en række, kommer ind som en hel række. Der er garanti på at hver række i en database har en unik nøgle: Primær-nøglen. De fleste databaser har også løst behovet for at der er konsistent mellem dataene i forskellige tabeller. Således kan man laves integritetsregler på tværs af tabellerne. Man beskriver at nøgler i disse rækker, skal findes i en anden tabel. Rettere sagt: Man laver laver fremmednøgler. Indholdet af dette felt skal eksistere i en anden tabel, for ellers hænger det ikke sammen. Et klassisk eksempel: Man kan fx ikke lave en konto før man har oprettet en kunde. Altså skal kunden eksistere i kunde-tabellen, før man kan lave en konto i konto-tabellen, der tilhører kunden. Kontoen refererer til kunden med en fremmed-nøgle. Dette er elementær database-lære, og dette hæver databasen fra blot at være en flad fil. Afhængigheder mellem tabellerne, er dog ikke noget krav, for at der er tale om en database. Fx har SQLite ikke nogle af disse features. Fokus for denne database er, at den skal være simpel og nem at implementere i software, uden behov for at køre som en seperat database-server. Altså hvis databasen ikke kan disse ting, er det fordi fokus er et andet sted. Fx blot hurtigt at gemme data i et let tilgængeligt filformat. DBMS I dag har man som regel ikke blot en database, men et Database Management System (DBMS). Denne applikation indeholder en del sub-systemer, som sørger for konsistens, adgangskontrol og samtidighedsstyring. DBMS'en er den eneste der kommunikerer med databasen, og stiller værktøjer til rådighed for andre applikationer, til at tilgå databasen. Dette sker oftest ved sproget Data Definition Language (DDL) til at definere databaser og tabeller, samt Structured Query Language (SQL) til at oprette, slette og manipulere dataene i databasen. ACID For at man skal kunne stole på dataene i en database, skal den understøtte transaktioner. Understøttelse af transaktioner indeholder fire ting. Dette kalder man ACID, der er forbogstaverne i de fire ting. Atomicity Consistency Isolation Durability Enten udføres alt, eller ingenting. Dataene skal være konsistente. Dataene skal hænge sammen. Isolation. Transaktioner der udføres samtidigt, kan ikke se hinandens ændringer. Først når der er udført commit, kan ændringerne ses af andre. Holdbarhed. Når dataene er gemt, kan man stole på at de er gemt. En fejl fra en anden transaktion, må ikke forhindre, at dataene er blevet gemt. 2
Commit-styring Det er godt, at man er garanteret at en række er gemt korrekt, så dataene ikke indeholder fejl. Der kan dog være behov for, at gemme en hel del rækker på én gang, evt. fordelt over flere forskellige tabeller. Fx kan oplysninger om en kunde være normaliseret ud i flere tabeller. En tabel med adresse-linjer og en anden tabel med telefonnumre. Der er et behov for at enten bliver kunden oprettet i alle tre tabeller, eller også bliver kunden ikke oprettet. Selv om rækkerne bliver lagt korrekt i tabellen, er der behov for at enten kommer alle rækkerne i de respektive tabeller, eller ingen. Alt andet vil give en "halv" kunde. Alle disse rækker hører sammen. Dette løses ved transaktionsstyring med commit og rollback. Hvordan bliver dette benyttet? Det afhænger af behovet i applikationen. Mange applikationer, benytter blot et implicit commit efter hver SQL-kald. Hvis man har en større og kompleks applikation, bliver behov for at styre commit og rollback større. Fx hos Bankdata, hvor jeg arbejder, er oprettelse af en kunde ret komplekst. Der skal oprettes data i en del tabeller, og på tværs af systemer. Dette er ikke blot et enkelt SQLkald, men kald til adskillige sub-systemer, der hver har sine SQL-kald. Når alt går vel, afsluttes med commit før funktionen afsluttes, og alle rækker bliver indsat i tabellerne. Hvis der sker en uventet fejl i denne process, udføres rollback, og funktionen afsluttes. Hvis der ikke bliver lavet rollback af alle SQL-kald udført indenfor funktionen, vil der ligge inkonsistente data i tabellerne. Så vil det ende med fx en adresse, der ikke har en kunde. Så commit/rollback skaber igen konsistens, men blot på et højere abstraktionsniveau end tidligere. Commit-scope Hvornår starter commit-scope? Commit-scope'et er mellem første SQL-kald og indtil der foretages commit eller rollback. Commit-scope'et varer indtil en af tre ting indtræffer. 1. Forbindelsen bliver lukket, og der sker implicit commit. 2. Der udføres manuel commit i applikationen. 3. Der udføres rollback i applikationen, og ændringerne til databasen bliver annuleret. Faren ved nr. 2, hvor der udføres ekstraordinært commit, skal man være varsom med. Fx skal man sørge for at lave dette på øverste niveau i sin applikation. Eksempel Når jeg kalder et subsystem til at generere en faktura til min kunde. Hvis subsystemet laver commit, bliver fakturaen liggende i systemet selvom der sker en uventet hændelse efterfølende i det overliggende system, og jeg foretager en rollback. Designvalg omkring commit Lad os tage et andet eksempel, hvor commit har indflydelse på hvordan vi kan designe en applikation. Lad os tænke på en webapplikation, med applikationsserver og database. Man kan overveje disse to løsninger eller en mellemting. 1. Man logger på og laver en session. I sin session laver man en forbindelse til databasen. Der kan forespørges og opdateres. Der er forbindelse til databasen, når der er behov for læse, oprette, slette eller ændre data. Man gemmer alt hvad man kan på session (i RAM på applikationsserveren), og har kun 3
forbindelse til databasen, hvis der sker ændringer til dataene. Hvis brugeren er igang med en større oprettelse, der kræver adskillige skærmdialoger, gemmes alle data på session. Når alle data er tilstede, lægges de i databasen. Hvis brugeren bliver koblet af systemet i forløbet, skal brugeren starte forfra, når han får forbindelse igen. 2. Hver gang man får vist et skærmbillede, så bliver alt håndteret. Mellem hver skærmbillede er man færdig. Man skal ikke huske på nogle data indtil brugeren kommer igen. Det bliver en "stateless" applikation. Man sørger for at databasen er designet sådan at, den kan indeholde alle "mellem-states". Man kan jo bestemme, når man designer sin database, hvad er valid state. Hvis brugeren er i gang med en større oprettelse, der kræver adskillige skærmdialoger, gemmes dataene i databasen med det samme, selvom, dataene ikke er komplette endnu. Brugeren bliver hele tiden koblet af systemet, da hver interaktion med brugeren afsluttes med at brugeren får vist næste skræmdialog. Når brugeren kobler på systemet ved at udfylde næste skærmdialog, véd systemet præcist hvor brugeren var i forløbet. Der er nogle ting som kan have indflydelse på valget. Hvis man har mange brugere, og deres data er på session, som i løsning 1, kan der hurtigt blive behov for store mængder RAM. Hvis der er behov for at skalere applikationen over flere servere, vil det simplificere opgaven med løsning 2, da brugeren kan kontakte en vilkårlig af applikationsserverne. Brugeren har jo intet gemt på session fra forrige forbindelse. (Det kræver dog stadig, at der kun er én database.) Oftest vil man lave en løsning, der blander løsning 1 og 2. SQL i programmer Når man bruger SQL i sine programmer, er der to muligheder, der hver har sine fordele. 1. Statisk embedded SQL 2. Dynamisk SQL Statisk embedded SQL Her benyttes en pre-kompiler før kompilering af programmet. SQL-statements i programmet begynder med med "EXEC SQL" og slutter med "" eller "END-EXEC" alt efter programmeringssprog. Udveksling af data mellem program og DBMS'en foregår mellem to kommunikationsarealer. Det ene er et fast kommunikationsareal kaldet SQLCA (SQL Communication Area), der indeholder statuskoder og fejlmeddelelser fra DBMS'en. Det andet areal definerer man selv med variable. Variablerne kan benyttes is SQL'en, enten som output (SELECT-delen) eller input (WHERE-delen). Man mapper mellem variablerne og databasen ved at SELECT-statementet bliver udvidet med INTO, hvor listen af de udtrukne felterne bliver suppleret med en liste af variabler. Dette fungerer fint hvis har lavet SQL-statement, der kun returnerer én række fra databasen. 4
EXEC SQL SELECT FIRSTNAME INTO :firstname, :lastname WHERE EMPNO = 34 Hvis man har en SQL, der returnerer flere rækker, benytter man cursors. Her hentes en rækker ind af gangen, og så bladrer man mellem rækkerne, for at hente data ud. SQL'en laves i et cursor statement. EXEC SQL DECLARE CURSOR CURSOR_1 FOR SELECT FIRSTNAME WHERE EMPNO BETWEEN 1 AND 30 Efterfølgende åbnes cursor'en med OPEN og rækkerne fetch'es ind en efter en med FETCH INTO. Når der ikke er flere rækker, indeholder "sqlcode" i kommunikationsarealet SQLCA værdien for NOT_FOUND. Nu lukkes cursoren med CLOSE. EXEC SQL OPEN CURSOR_1... EXEC SQL FETCH CURSOR_1 INTO :firstname, :lastname... EXEC SQL CLOSE CURSOR_1 En fordel ved statisk embedded SQL er sikkerheden, at SQL'en ikke ændre sig, da den er blevet kompileret ind i programmet. En anden fordel er, at det er muligt at lave udtræk på SQL, og køre "EXPLAIN PLAN" på SQL'en, og se executionplanen og fange "performance-syndere" inden rettelserne kommer i produktion. Dynamisk SQL Her benyttes et API til DBMS'en. Der er ikke behov for pre-kompiler, og al kommunikation foregår med almindelig funktionskald eller objekt-manipulation afhængig af programmeringssprog. Her er benyttet JDBC i Java. 5
ResultSet result = db_statement.executequery(" \ SELECT FIRSTNAME \ \ FORM EMPLOYEE \ WHERE EMPNO BETWEEN 1 AND 30 \ ") while(result.next()) { String firstname = result.getstring("firstname") String lastname = result.getstring("lastname") } Fordelen ved dynamisk SQL er at man kan sammensætte SQL på runtime-tidspunkt. Altså kan brugerens valg i applikationen have indflydelse på SQL'ens kompleksitet. Indeks Dataene i en database ligger i rækkefølge, så de er hurtige at finde frem igen. Tabellen har som oftest mindst et indeks. Dette er den rækkefølge som dataene ligger i, på harddisken. Hvis det indeks, der bestemmer rækkenfølgen af data på harddisken er primær-nøglen på tabellen, kaldes indekset for primært indeks. Hvis det indeks, der bestemmer rækkenfølgen af data på harddisken er en anden nøgle, kaldes indekset for clustered indeks. Derudover kan man lave sekundære indeks på andre kolonner i tabellen. Oftest tilføjer man et sekundært indeks, hvis dataene ofte hentes fra tabellen uden brug af primært eller clustered indeks, og derfor foretager en table-scan på tabellen (alle rækker bliver læst). Ved at tilføje et sekundært indeks, kan data hurtigt findes, og i nogle tilfælde hvor alle data er i indekset, kan man nøjes med at læse i indekset. Hvornår vil man benytte primært indeks? Vores eksempel ovenfor med en EMPLOYEEtabel ville være et godt bud. Specielt hvis informationer om den ansatte oftest læses via EMPNO. I en EMPLOYEE-tabel ville det muligvis også give mening at lave et sekundær indeks på PHONE eller LASTNAME, hvis telefonnummer eller efternavn ofte bliver benyttet til at finde den ansatte. Hvornår vil det give mening at anvende et clustered indeks fremfor et primært indeks? Hvis man fx har en logbog, der ikke har timestamp for logningen som primær-nøgle. Her kan det give mening at lave et clustered indeks på timestamp, så dataene hurtigt kan indsættes i tabellen i stigende rækkefølge. Performance Lad os antage følgende indeks: Primært indeks: [EMPNO] Sekundært indeks: [PHONE] Når der nu skal laves SQL mod ovenstående tabel vil jeg få en god performance hvis min WHERE-clause indeholder felter jeg har indeks på. 6
SELECT FIRSTNAME WHERE EMPNO = 33 Denne SQL vil gå direkte til det primære indeks og finde EMPNO 33 og en pointer til stedet i tabellen hvor FIRSTNAME og LASTNAME ligger. SELECT FIRSTNAME WHERE EMAIL = 'boss@firm.dk' Denne SQL vil lave en table-scan, og læse hele tabellen igennem, da EMAIL ikke findes i noget indeks. SELECT LASTNAME WHERE PHONE = 99886655 Denne SQL performer rigtigt godt. Der findes et sekundært indeks, der starter med PHONE. Derudover indeholder indekset også LASTNAME, så efternavnet bliver også læst her, og der er slet ikke behov for pointeren til den faktiske tabel, for at finde flere informationer. Kilder Thomas Connolly, Carolyn Begg: Database Systems - A Practical Approach to Design, Implementation, and Management. (Inklusiv Appendix F og I). http://www.javacoffeebreak.com/articles/jdbc/ 7