]>
Denna bok är framställd för användare av anpassade medier enligt 17§ Upphovsrättslagen. Olaga spridning eller överföring beivras.
Talboken har 689 sidor och rubriker på fyra nivåer.
Boken är inläst för Myndigheten för tillgängliga medier, MTM år 2023.
Inläsare är Patrik Nyman vid Gramma Korrektur.
Denna talbok innehåller även elektronisk text som är tillgänglig via läsprogram i dator.
Thomas Padron-McCarthy är teknologie licentiat och arbetar som lärare, bland annat i databasteknik, på Örebro universitet. Han har också arbetat som konsult, lett företagsutbildningar, och undervisat på Linköpings universitet och på Mittuniversitetet.
Tore Risch är professor emeritus i databasteknik vid Uppsala universitet. Han har också varit professor i tekniska databaser vid Linköpings universitet, och varit verksam i USA, bland annat på Hewlett-Packard Laboratories i Palo Alto och på Stanford University.
Databasteknik används nästan överallt i dag där data behöver lagras och bearbetas. Allt från webbplatser till mobilappar lagrar sina data i databaser. Databaser har därför blivit ett av de områden inom datatekniken som man har störst nytta av att kunna, vare sig man är användare, datatekniker eller programmerare.
Boken Databasteknik börjar med grunderna om databaser, som scheman, datamodellering och användningsområden för databaser. Vi går vidare bland annat med frågespråk, transaktionshantering, säkerhet, prestanda och hur databashanteraren arbetar internt.
Den vanligaste och populäraste typen av databaser är relationsdatabaser, och tyngdpunkten ligger därför på dem, bland annat med frågespråket SQL, och vi beskriver system som MySQL, PostgreSQL och Microsoft SQL Server. Men vi vänder oss till alla som arbetar med databaser, även till exempel objektorienterade databaser och NoSQL-databaser.
select
117
in
och not in
130
exists
och not exists
131
any, some
och all
133
order by
134
commit
och rollback
144
where
162
group by
och having
166
Count
kan räkna antingen rader eller värden
174
Select
-frågor
196
Insert
199
Delete
200
Update
200
Create table
201
Create view
204
Alter table
204
Create index
205
Drop
206
grant
och revoke
209
Var och en som håller på med datorer, vare sig det är som vanlig användare eller som tekniker eller programmerare, brukar ofta komma i kontakt med databaser av olika slag. Den som har ett mer tekniskt arbete inom dataområdet ser kanske dagligen termer som relationsdatabas, SQL, datamodell och primärnyckel.
Därför finns det ett stort behov av utbildning och litteratur inom databasområdet. Det finns redan åtminstone ett tiotal tjocka grundböcker om databaser, till exempel Fundamentals of Database Systems av Elmasri och Navathe. Men alla dessa böcker är på engelska, och de är på tusen sidor eller mer. Det finns också böcker som är kortare och på svenska, men då täcker de ett ganska begränsat område, till exempel genom att koncentrera sig på en enda databashanterare snarare än på vilka tekniker som finns tillgängliga och på användningen av databashanterare i allmänhet.
Den här boken är tänkt att fylla behovet av en kort, 1 svensk bok som täcker hela området databasteknik – även om allt förstås inte kan tas upp, och allt det som tas upp inte kan gås igenom på djupet. Boken ska ge förståelse för grunderna inom databasområdet, den ska lära läsaren att använda databashanterare och modellera data, och den ska också ge den förståelse för databashanterarens inre arbete, till exempel lagringsstrukturer och frågeoptimering, som behövs för en avancerad användare eller databasadministratör. Den ska vara så fullständig att den kan användas som kurslitteratur i en 5-veckorskurs (7,5 högskolepoäng) för civilingenjörsstuderande i datateknik, och den ska också passa som kursmaterial i databaskurser anpassade för företag, och för självstudier. Boken kan också 2 användas i en större kurs om databaser eller i en fortsättningskurs, men då behöver man förmodligen komplettera boken med ytterligare material, till exempel några översiktsartiklar om mer specialiserade ämnen inom databasområdet.
Den här boken har en webbplats som finns på adressen www.databasteknik.se/boken
Där finns kompletterande material som kodexempel, svar till övningar och eventuella rättelser.
Delar av boken bygger på en webbkurs om databaser. Webbkursen finns på adressen www.databasteknik.se/webbkursen
Kapitel 17, Objektorienterade och objektrelationella databaser, tar inte upp grunderna om objektorientering, till exempel vad som menas med arv och polymorfism. Det finns emellertid en introduktion till objektorientering på bokens webbplats.
Kapitel 23, Fysiska lagringsstrukturer i databaser, handlar om hur olika fysiska datastrukturer används internt i en databashanterare. Det tar inte upp grunder om lagringsstrukturer, som till exempel vad som menas med en hashtabell. Men även där finns det en introduktion på bokens webbplats.
Kapitel 26, Frågebearbetning, handlar en del om komplexitet (vilket ungefär betyder hur lång tid de tar att göra olika saker). Där underlättar 3 det om man tidigare studerat datastrukturer, algoritmer och komplexitetsmått.
Boken består av ett antal kapitel, där varje kapitel behandlar ett ämne inom databasområdet. De flesta kapitlen avslutas med en repetition i form av en ordlista med de viktigaste begreppen från kapitlet, och en litteraturlista med tips om vidare läsning. Till en del kapitel finns också några övningsuppgifter.
Kursiverad stil används för nya termer, som när vi berättar att det finns databashanterare som är objektrelationella. Kursiverad stil används också för betoning, som när vi berättar att ett B-träd inte är samma sak som ett binärt träd.
Fetstil används för exempel i löpande text, när de behöver markeras så de kan skiljas från resten av texten, som när vi talar om att kolumnen Lön i tabellen Anställd innehåller 30 stycken förekomster av värdet 30. Fetstil används också för uppslagsorden i listorna med viktiga begrepp som finns i slutet av varje kapitel.
Skrivmaskinstypsnitt
används för kodexempel, som när vi skriver SQL-frågor:
select Lön
from Kunder
where Nummer = 17
Boken gavs ursprungligen ut hösten 2005, och vi vill tacka alla de personer som bidrog med uppmuntran eller med synpunkter på innehållet eller utformingen, både när det gäller den ursprungliga webbkursen och boken. Särskilt tack till Eva L. Ragnemalm, Björn Ericsson och Johan Malmborg.
Inför den nya tryckning som gjordes 2007 rättade vi en del av de fel som fanns med i första tryckningen. Bland dem som upptäckt och påpekat dessa fel, och även fel på den tillhörande webbplatsen, vill vi tacka bland andra Thomas Adolfsson, Örebro, Fredrik Bökman, Gävle, David Hall, Norrköping, och Bo Peterson, Malmö. Att det finns fel kvar beror förstås inte på dem, utan på oss själva.
Inför den andra upplagan 2018 har vi gått igenom och uppdaterat texten, och ersatt vissa delar av innehållet. Bland annat har det långa kapitlet om Microsoft Access utgått, och vi har i stället ett kapitel om Microsoft SQL Server och ett om PostgreSQL. En beskrivning av Microsoft Access finns i stället på bokens webbplats.
I det här kapitlet går vi igenom databasteknikens grunder, och förklarar vad som menas med många av de viktigaste termerna inom området. Det är termer som databas, databashanterare, datamodell och schema. Kapitlet tar också upp fördelarna med databasteknik, jämfört med de alternativ som finns, och även nackdelarna. Vi nämner användningsområden för databasteknik, och beskriver mycket kort hur en databashanterare är uppbyggd.
Det här kapitlet ger grundläggande kunskaper om databaser. De kunskaperna är nyttiga och användbara i sig, men behövs också för att förstå resten av boken. Vi går till exempel igenom vad som egentligen menas med databas och databashanterare, vad det är för skillnad på schema och data, och vad som menas med en datamodell. Vi tar upp de fördelar (och nackdelar) som det innebär att använda databasteknik, vad man kan använda databastekniken till, och även grunderna för hur en databashanterare fungerar.
Data är uppgifter av olika slag. Ibland skiljer man data från information, som är data som man gett en tolkning. Alltså är 23 ett exempel på data, medan det är information att det är 23 grader varmt ute. Ibland talar man också om kunskap, som i "kunskapsbaserade system". Kunskap är anvisningar om beteende, exempelvis regler för hur datorn ska dra slutsatser.
Med ordet databas brukar man mena
De som håller på med databasteknik brukar också mena att en databas ska
En databashanterare är ett program som har till uppgift att lagra och hantera databaser. Andra namn på samma sak är databashanteringssystem (förkortat DBHS) och databasmotor. På engelska heter det database management system och förkortas DBMS.
9Det finns hundratals olika databashanterare. Några av de mest kända är Db2 från IBM, MariaDB, Microsoft Access, Microsoft SQL Server, Mimer, MongoDB, MySQL, ObjectStore, Oracle, PostgreSQL och SQLite. Mimer och MySQL är utvecklade i Sverige. Oracle, Microsoft SQL Server och Db2 kallas ibland för "de tre stora", både för att de tillsammans har en stor andel av marknaden och för att de är stora och komplicerade system med många funktioner.
Det kan vara svårt att avgöra vilken databashanterare som är populärast, och det beror på vad och hur man mäter, men enligt webbplatsen DB-Engines var när detta skrivs (december 2017) Oracle populärast, tätt följd av MySQL och därefter Microsoft SQL Server. 3 Därefter är steget långt till fjärdeplatsen, PostgreSQL.
Efter PostgreSQL kommer MongoDB, som är ett exempel på en ny typ av databassystem som man brukar kalla NoSQL-databaser. Till största delen är dessa databashanterare nya implementeringar av kända databastekniker. I boken kommer vi att nämna på några ställen hur NoSQL-databaser skiljer sig från relationsdatabaser, och hur de tillämpar olika databastekniker. Se även kapitel 28.
Det företag som levererar en databashanterare brukar kallas vendor på engelska, dvs "säljare", även om det gäller en databashanterare som är gratis att ladda ner och använda. När en databashanterare har egna, leverantörsspecifika funktioner, kallas de på engelska för vendor-specific.
Termerna inom databasområdet används ibland slarvigt eller med lite olika betydelser. Exempelvis används ordet "databas" ofta för att beteckna det som vi här kallar för "databashanterare". Ordet "databassystem" används ibland för att beteckna det som vi kallar "databashanterare", och ibland för kombinationen av det vi kallar "databas" och "databashanterare". Hur orden "data" och "information" används ska vi bara inte tala om!
För det mesta förstår man av sammanhanget vad som menas, men att termer används i olika betydelser av olika personer eller i olika sammanhang kan ibland leda till kommunikationsproblem och missförstånd. Även i resten av boken kommer vi därför att varna 10 för den här sortens språkförbistring genom att påpeka när en och samma term brukar användas i flera olika betydelser, eller när flera olika termer kan användas som namn på samma sak.
Om man ska förstå fördelarna med att använda databasteknik, dvs att låta en databashanterare hantera ens data, måste man jämföra med alternativet. Ett alternativ är kalkylprogram, som Excel, men särskilt när det gäller stora datamängder och flera olika användare är de ganska begränsade. Vi gör en jämförelse med kalkylprogram i avsnitt 1.7. Annars kan alternativet vara att ha en eller flera vanliga filer med data. Sen skriver man ett program i något vanligt programmeringsspråk som C eller Java, och låter det programmet hantera datafilerna.
struct kund {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xint nummer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xchar namn[50 + 1]; char adress[50 + 1];
};
Det finns ganska många fördelar med att i stället använda en databashanterare. De viktigaste fördelarna är att det är
Många databashanterare erbjuder ett textbaserat gränssnitt. Starta databashanteraren och skriv:
create table Kunder
(Nummer integer,
Namn char(50),
Adress char(50));
Klart! Nu finns det en tabell som ser ut så här (när man stoppat in lite data):
Kunder | ||
---|---|---|
Nummer | Namn | Adress |
7 | Hjalmar | Vägen 7 |
113 | Hulda | Stora torget 18 |
4 | Lotta | Vägen 7 |
Uppgifterna i tabellen kallar vi förstås för data. Tabellens utseende, dvs vilka kolumner som finns, vad de heter och vad de får innehålla, kallas för schema. Schemat bestämmer vilka data som kan lagras i databasen.
En grundregel för databaser är att schemat sällan ändras. Om man konstruerar ett schema som man räknar med att ändra i hela tiden, så har man förmodligen gjort fel.
I schemat ovan använder vi oss av tabeller och kolumner. (En riktig databas innehåller för det mesta mer än en enda tabell.) Vi kunde i stället ha använt något annat, till exempel objekt och klasser. Hur schemat kan se ut bestäms av databasens datamodell. Man kan säga att datamodellen är ett "schema för schemat". Datamodellen bestämmer 12 alltså hur schemat får se ut, och schemat bestämmer vilka data som får lagras i databasen. (Den här datamodellen, där man lagrar sina data i tabeller, kallas relationsmodellen.)
Nu ska vi använda vår databas också. Antag att vi vill ha reda på namnet och adressen för kunden med kundnummer 17. Då skriver vi bara:
select Namn, Adress
from Kunder
where Nummer = 17;
(Eller kanske inte, egentligen. På riktigt är det förstås viktigt att ha ett lättanvänt användargränssnitt mot databasen, till exempel ett grafiskt gränssnitt med rutor man fyller i och knappar man klickar på. Man kan normalt inte kräva av vanliga användare att de ska formulera SQL-frågor för att kunna arbeta med kundregistret. Men vi har i alla fall redan skapat ett fungerande kundregister, även om det inte är så användarvänligt.)
13Den andra fördelen i listan är att databasteknik är ett kraftfullt verktyg. Att ett system är kraftfullt betyder att komplicerade saker går att göra på ett enkelt sätt.
Antag att vi vill ha reda på alla kunder som har namn som börjar med S, och få dem utskrivna i bokstavsordning efter adressen. Då skriver vi bara så här:
select *
from Kunder
where Namn like 'S%'
order by Adress;
Vill vi veta hur många personer som heter Anders som bor på varje adress? Enkelt:
select Adress, count(*)
from Kunder
where Namn = 'Anders'
group by Adress;
14
Att ett system är flexibelt betyder att det är lätt att ändra.
Kommer vi plötsligt på att vi vill göra en helt ny sorts sökning i databasen? Enkelt! Skriv bara in sökningen som en fråga:
select Namn
from Kunder
where Adress = 'Vägen 8'
and Namn like 'S%';
Kommer vi plötsligt på att vi vill ha med telefonnummer också i kundregistret? Enkelt! Skriv:
alter table Kunder add Telefon char(10);
Nu har vi utökat tabellen Kunder med en ny kolumn, som heter Telefon!
När kundregistret använts ett tag kan det hända att tabellen innehåller så många kunder att det tar lång tid att söka efter en kund med ett visst namn. Vad kan man göra åt det?
create index Namnindex on Kunder(Namn);
Nu har vi ändrat databasens interna lagringsstrukturer, så att det går fortare att söka efter ett visst namn. Tabellen ser fortfarande likadan ut, för oss som användare, men nästa gång vi söker på ett namn kommer frågan att gå mycket fortare att köra. Databashanteraren utnyttjar automatiskt den nya lagringsstrukturen.
15Om vi hade velat göra de här logiska och fysiska ändringarna i en lösning med vanliga filer, som hanteras av ett program skrivet i C eller i Java, hade det krävt mycket mer arbete. Att införa nya, mer effektiva datastrukturer brukar kräva omfattande ändringar i programmet, inte bara för att hantera dessa nya datastrukturer utan också för att utnyttja dem i sökningarna. Om vi ändrar formatet på datafilen, till exempel för att lägga till ett nytt fält i posterna, måste vi kanske också skriva ett särskilt konverteringsprogram som går igenom och ändrar våra existerande data.
Det finns flera saker som är mycket besvärliga att få att fungera om man skriver ett program själv, men som finns inbyggda från början i de flesta databashanterare. Här räknar vi upp några:
Vad händer om flera personer samtidigt håller på och ändrar i kundregistret? Då är det lätt hänt att en person skriver över ändringar som en annan person gjort. Databashanteraren ser till att det inte blir några skadliga krockar.
Vad händer om strömmen plötsligt går, så att datorn "tappar minnet"? Om ens program läser in datafilen i primärminnet, och skriver tillbaka filen när man jobbat klart, blir man kanske av med alla de ändringar man gjort. Och ännu värre: om man precis höll på att spara när strömmen gick, kanske en del av de data som därefter finns på disken är nya, och en del är gamla, och man vet inte vilka som är vilka. I värsta fall måste man kasta bort hela datafilen!
16Med en databashanterare slipper man sådana problem. Den ser till att inga data någonsin försvinner, hur olyckligt ett strömavbrott än skulle komma.
Olika användare kan ha helt olika behov, även om de arbetar med samma data. En databashanterare erbjuder ofta flera olika gränssnitt, dvs sätt för användaren att kommunicera med databasen. Det brukar alltid finnas ett textgränssnitt där man kan skriva kommandon av den typ vi sett i exemplen, och ett eller flera gränssnitt som andra program kan använda för att kommunicera med databasen, men det kan också finnas till exempel olika typer av grafiska gränssnitt.
Man brukar också kunna definiera vyer. En vy gör det möjligt för olika användare att se databasen på olika sätt. Till exempel kan man i många databashanterare skriva så här:
create view Enkelkunder
as select Nummer, Namn
from Kunder;
Enkelkunder | |
---|---|
Nummer | Namn |
7 | Hjalmar |
113 | Hulda |
4 | Lotta |
Det går att definiera mer komplicerade vyer. Till exempel kanske en annan användare bara är intresserad av antalet kunder, och bryr sig inte om vilka de är? En vy underlättar:
create view AntalKunder
as select count(*) as Antal
from Kunder;
AntalKunder |
---|
Antal |
3 |
De flesta databashanterare har mekanismer för att ge olika användare olika rättigheter i databasen, och för att skydda data mot obehörig åtkomst. Till exempel kan man ge en användare rätt att ändra i vissa delar av databasen och att söka i andra delar, medan hon inte alls får se resten av databasen.
Databasteknik passar inte för alla tillämpningar. Exempelvis brukar all den där enkelheten och flexibiliteten som vi pratade om ovan göra att en databashanterare kräver mer resurser än ett specialskrivet program. Det går alltså åt mer minne och diskutrymme. Kanske går det också långsammare att köra, i synnerhet för enkla sökningar. 7
Å andra sidan kan en lösning med databashanterare i stället gå mycket fortare än ett specialskrivet program, särskilt för komplicerade sökningar som kombinerar data. Orsaken är att databashanteraren innehåller avancerade datastrukturer och algoritmer, som man sällan hinner bygga in i det specialskrivna programmet.
Ovan har vi jämfört databasteknik, dvs att använda en databashanterare för att lagra sina data, med att skriva ett program i ett vanligt programmeringsspråk och lagra data på en eller flera vanliga filer. Ett annat alternativ är att använda ett kalkylprogram, vanligen Microsoft Excel. (Excel är alltså inte en databashanterare, ifall någon trodde det!) Kalkylprogram som Excel har redan mycket färdig funktionalitet, och väljer man att lagra sina data i form av en 18 Excel-fil går det mycket fortare och lättare att komma i gång än om man skriver ett program. Det kan också vara mycket snabbare och enklare än med en databashanterare!
Men det finns många nackdelar med ett kalkylprogram jämfört med en databashanterare:
Trots detta används Excel ibland för tillämpningar där en databashanterare varit bättre, till exempel för att arbeta med stora datamängder, och därför har Microsoft lagt till en del nya finesser i Excel för att det ska fungera bättre. Men det får nog ses som en nödlösning, och även om det är lättare att komma i gång med ett Excel-ark än att sätta upp en riktig databas, blir det svårare sen.
Allt. Låt inte lura er av att databasböcker bara brukar ha kundregister och studentdatabaser som exempel. Databaser används också i cad-system ("Computer-Aided Design"), CASE-system ("ComputerAided Software Engineering"), telefonväxlar, styr- och reglertillämpningar, 19 och mycket annat. Databasteknik är också mycket använd på webben, för att lagra innehållet i webbplatser.
Här är några exempel där man kommer i kontakt med databaser utan att man alltid tänker på det:
Vilken datamodell man använder bestämmer hur schemat får se ut. Schemat bestämmer sen i sin tur vilka data som kan lagras i databasen.
Man kan dela in datamodellerna i tre klasser:
• Konceptuella 8 eller begreppsmässiga datamodeller. Om man ska skapa en databas som beskriver en del av verkligheten, till exempel ett företag, brukar man börja med att göra en beskrivning av hur den delen av verkligheten ser ut och fungerar. Till det kan man använda en konceptuell datamodell, som man använder för att skapa ett konceptuellt schema. Det konceptuella schemat behöver egentligen inte ha någonting med datorer att göra, utan är bara en beskrivning av verkligheten som lika gärna skulle kunna användas av någon som bara vill analysera hur företaget fungerar.
Ett exempel på en konceptuell datamodell är den så kallade Entity-Relationship-modellen, ofta förkortad ER-modellen. 20 När man använder ER-modellen ritar man upp de saker ("entities") som man vill lagra i databasen, och de förhållanden eller samband ("relationships") som finns mellan dem.
• Implementationsmodeller. Det här är de datamodeller som används av databashanterare. Om man vill skapa en databas måste den beskrivning man gjort med hjälp av en konceptuell datamodell först översättas till något som går att stoppa in i en dator, nämligen en beskrivning enligt en implementationsmodell.
Den vanligaste implementationsmodellen är relationsmodellen, som går ut på att man lagrar data i tabeller. Dessutom har det dykt upp olika typer av objektorienterade modeller. På 1960- och 1970-talet var den hierarkiska datamodellen och nätverksmodellen 9 vanliga. Dessa gamla datamodeller har återkommit i ny tappning på 2010-talet i NoSQL-databaser.
Det kan vara praktiskt att betrakta sin databas på tre olika nivåer. Det är hela tiden samma data, men man använder tre olika scheman för att beskriva dem:
Tre-schema-arkitekturen kallas ibland "tre-nivå-arkitekturen". Alla de tre nivåerna hanteras av databashanteraren, som alltså håller reda på tre olika scheman för samma databas. (Men det är inte alla databashanterare som fungerar så.)
De tre nivåerna i tre-schema-arkitekturen motsvaras inte av de tre klasserna av datamodeller. På den lägsta nivån kan man använda en fysisk datamodell för att uttrycka det fysiska schemat, och på den logiska nivån använder man en implementationsmodell för att uttrycka det logiska schemat, men på den översta nivån i tre-schemaarkitekturen, vy-nivån, uttrycks det externa schemat normalt med samma implementationsmodell som det logiska schemat. Den tredje klassen av datamodeller, konceptuella datamodeller, används inte i databashanterare. 11
Ibland skiljer man på olika typer av användare, dvs personer som arbetar med databasen:
Om jag samlar mina kakrecept i en databas i min hemdator, är det antagligen jag själv som spelar alla dessa roller. I stora system, som en stor biljettbokningsdatabas, kan det finnas hundratals eller tusentals personer som arbetar med databasen.
En databashanterare är oftast ett stort och komplicerat program, eller ett helt system av program. Förutom själva "kärnan" i databashanteraren, som hanterar den lagrade databasen, finns det ofta flera olika användargränssnitt, dvs program (eller delprogram eller system av program) som användaren kan använda för att söka eller ändra i databasen. Det brukar finnas ett frågespråk, i regel SQL, men också olika grafiska verktyg. Olika gränssnitt passar för olika användare och för olika användningsområden.
Det är också vanligt att en databashanterare innehåller verktyg för att bygga tillämpningsprogram. Ett annat ord för samma sak är 23 applikationsprogram. Ett tillämpningsprogram är ett program som är avsett för ett specifikt ändamål, till exempel för att låta SJ:s resenärer 12 boka tågresor. I det här sammanhanget tänker vi oss att alla data lagras i en databas som hanteras av databashanteraren, och tillämpningsprogrammet kommunicerar med databashanteraren, men tillämpningsprogrammet är enklare att använda för resenären. I det programmet kan man kanske klicka på knappar eller på en karta för att välja resmål. Det är lättare än att kommunicera med databashanteraren direkt, i värsta fall genom att skriva SQL-kommandon. Med verktygen för att bygga tillämpningsprogram kan man (mer eller mindre enkelt) sätta ihop ett sådant program, ibland till och med utan att man behöver skriva någon programkod.
Tillämpningsprogrammerare är ännu en typ av databasanvändare. Tillämpningsprogrammeraren tillverkar tillämpningsprogram, antingen med hjälp av särskilda verktyg eller genom att skriva program i ett vanligt programmeringsspråk med inlagda anrop till databashanteraren. Ett annat ord för samma sak är applikationsprogrammerare.
En normal databashanterare lagrar alla data på sekundärminne, dvs SSD eller mekanisk hårddisk, och hämtar bara in de data som den för tillfället behöver till primärminnet. Men det finns också databashanterare som har alla data i primärminnet hela tiden. (Även databashanterare med data på sekundärminne utnyttjar primärminnet så mycket den kan, eftersom primärminnet är mycket snabbare än sekundärminnet. Att spara en extra kopia av en del data i primärminnet för att kunna arbeta snabbare med det kallas för caching.)
När man installerat en databashanterare på sin dator, kan den ofta hantera flera olika databaser samtidigt. Varje databas består egentligen av två samlingar data: dels databasens innehåll, dels schemat. Schemat kallas också datakatalog eller meta-data, vilket betyder "data om data".
Om man ritar upp en databashanterare, kan det se ut som på bilden nedan. Överst har vi alla användarna, som arbetar med databasen. De kommunicerar med databasen med hjälp av olika program och verktyg. Databasen hanteras (förstås) av databashanteraren. Allra längst "in" i systemet, längst ner i figuren, finns själva databasen. 24 Notera att den är uppdelad i två delar: databasens "riktiga" data och dess meta-data.
Förenklat kan man säga så här:
Ovanstående gäller under förutsättning att man har "vanlig användning" av "vanliga databaser" med "vanliga data". Med "vanliga data" menar vi korta texter och numeriska tal, som i de flesta exemplen i den här boken, inte till exempel filmer. Om en rad (eller motsvarigheten till en rad) i databasen innehåller en långfilm i biokvalitet, blir det miljoner gånger mer data än om raderna innehåller personnummer, namn och adress. Med "vanlig användning" menar vi ganska få samtidiga användare som gör ganska enkla sökningar. Google eller Facebook, med mer än en miljard användare, har förstås stor belastning på sina databaser, oavsett hur deras data och sökningar ser ut. Ställer man mycket komplicerade frågor, som en Sudoku-lösare,, 13 kan de ta lång tid att utföra i databasen. Med "vanliga databaser" menar vi en disk- eller SSD-baserad relationsdatabas som hanteras av en enda serverdator, eller några få serverdatorer. Är man Google eller Facebook, med sina miljarder användare, måste man använda andra typer av databaser, som distribuerade databaser med en hierarkisk datamodell.
Vi skrev i avsnitt 1.2 att en databas modellerar en del av världen, till exempel ett företag och dess verksamhet. Det är ungefär som när man bygger en modell av en båt: den beskriver hur båten ser ut. Men en modell är sällan en helt exakt avbildning av verkligheten.
26Ibland är det databasens innehåll som är verkligheten. Om det står i bankens databas att jag har 175 kronor på mitt bankkonto, så kanske det är den noteringen som är bankkontot. Det finns ingen hög med riktiga pengar i ett kassavalv som man kan gå och räkna.
Men ofta är databasen bara en avbildning av verkligheten, och man bör komma ihåg att den sällan är perfekt. Alla uppgifter är inte med, mätvärden kanske inte är exakta, och det finns nästan alltid en eftersläpning, när ändringar i verkligheten inte hunnit införas i databasen. Eftersläpningen kan vara allt från bråkdelar av en sekund (mätvärden från en sensor som ska hinna registreras och sparas) till flera år (en nybyggd väg som ska in i gps-kartorna).
Data (engelska: data). Data är uppgifter av olika slag. Ofta används termen "data" om databasens innehåll, som är de data som råkar finnas i databasen vid ett visst tillfälle, till skillnad från databasens schema, som är en beskrivning av vilka data som kan finnas i databasen.
Information (engelska: information). Ibland skiljer man på data och information, där information är data som man gett en tolkning.
Kunskap (engelska: knowledge). Kunskap betyder ungefär "information som man tillgodogjort sig", men i datasammanhang talar man ibland också om kunskap i betydelsen anvisningar om beteende, exempelvis regler för hur datorn ska dra slutsatser. En term som var populär på 1980-talet var kunskapsbaserade system. Ett annat namn på samma sak var expertsystem.
Databas (engelska: database). En samling data som hör ihop på något sätt. För det mesta brukar man anta att den lagras på en dator, att den hanteras av en databashanterare och att den har ett schema. Ibland används ordet "databas" även för att beteckna det som vi här kallar för databashanterare.
Databashanterare, databashanteringssystem, DBHS (engelska: database management system, DBMS). Ett program eller ett system av program som kan hantera en eller flera databaser. 27 De flesta databashanterare är generella, och kan hantera olika sorters databaser. Exempel på databashanterare är Oracle och Microsoft Access.
Schema, databasschema (engelska: schema, database schema). En beskrivning av vilka data som kan finnas i en databas, oberoende av vilka data (innehållet) som råkar finnas i databasen just nu. I relationsmodellen, där man beskriver världen med hjälp av tabeller, består schemat huvudsakligen av vilka tabeller som finns i databasen och vilka kolumner de har, men inte vilka värden som råkar finnas i tabellerna just nu.
Datamodell (engelska: data model). Ett sätt att beskriva världen, som man kan använda till exempel när man bygger upp en databas. Exempel: relationsmodellen, där man beskriver världen med hjälp av tabeller. Utanför den akademiska världen används ordet "datamodell" ofta för att beteckna det som vi här kallar för schema.
SQL. Av Structured Query Language. Ett frågespråk som används i de flesta databashanterare. SQL hette från början SEQUEL, och uttalas fortfarande så av en del.
Fråga (engelska: query). En sökning i databasen. Den är normalt uttryckt i ett särskilt frågespråk som SQL. Ibland kallar man alla operationer, alltså även till exempel att lägga in data i databasen, för "frågor".
Frågespråk (engelska: query language). Ett språk som man använder för att göra sökningar i databasen, eller "ställa frågor till databashanteraren". Synonymer: datamanipuleringsspråk, DML.
Logiskt dataoberoende (engelska: logical data independence). Möjligheten att ändra på den logiska strukturen hos en samling data, utan att man också måste ändra på de program som arbetar med dessa data. Exempel på den logiska strukturen i en databas är vilka kolumner som finns i en tabell.
Fysiskt dataoberoende (engelska: physical data independence). Möjligheten att ändra på den fysiska strukturen, dvs det interna lagringssättet, hos en samling data, utan att man också måste ändra på de program som arbetar med dessa data.
Tre-schema-arkitekturen (engelska: the three-schema architecture). Kallas även tre-nivå-arkitekturen (engelska: the threelevel architecture). Samma databas beskrivs på tre olika nivåer, med tre olika scheman: ett externt schema överst (närmast användaren), 28 ett logiskt schema i mitten, och ett fysiskt schema underst (längst in i datorn). Genom att man kan ändra i ett av dessa scheman utan att schemana ovanför påverkas, får man bättre dataoberoende, vilket innebär att det blir lättare att anpassa systemet för nya krav.
Gränssnitt (engelska: interface). En skiljelinje mellan två delar i ett system, till exempel mellan ett tillämpningsprogram och en databashanterare, eller mellan ett system och dess omgivning, till exempel mellan en databashanterare och dess användare. Till gränssnittet hör både var gränsen dragits och hur kommunikationen över gränsen sker. All interaktionen mellan de två delsystemen sker genom gränssnittet. Gränssnittet kan till exempel bestå av ett antal funktioner eller subrutiner som går att anropa, eller – i ett objektorienterat system – av en samling klassdefinitioner. Gränssnittet mellan ett tekniskt system, som ett datorsystem, och en användare kallas användargränssnitt (engelska: user interface). Användargränssnittet till en bil består av ratt, växelspak och övriga reglage, och också av hastighetsmätaren och kontrollamporna. (Man kan argumentera för att en del andra saker, till exempel ljudet från motorn, ingår i användargränssnittet.) Användargränssnittet till en vanlig dator består av tangentbordet, musen, skärmen, högtalare och mikrofon. Användargränssnittet till en databashanterare består av ett eller flera program, till exempel ett program där man kan skriva in SQL-frågor och få resultatet utskrivet på skärmen.
Vy (engelska: view). En vy är ett sätt att se en databas. Olika användare kan betrakta samma databas genom olika vyer. I SQL är en vy en SQL-fråga som fått ett eget namn. När man tittar på en vy i SQL ser den ut som en tabell, men innehållet beräknas på nytt (genom att SQL-frågan körs) varje gång man tittar på den. 14 Vyn är alltså något som definierar information som härleds från databasens innehåll med hjälp av en SQL-fråga.
Konceptuell datamodell eller begreppsmässig datamodell (på engelska: conceptual data model). En datamodell där man beskriver verkligheten i termer av de saker som finns i den verkligheten. Den säger inget om hur de ska lagras i en databas. Exempel: ER-modellen.
Implementationsmodell eller implementeringsmodell (engelska: implementation data model). En datamodell som man kan 29 använda i en verklig databashanterare, och alltså skapa eller realisera ("implementera") sin databas med. Den vanligaste implementationsmodellen numera är relationsmodellen.
Relationsmodellen (engelska: the relational model). En datamodell där man beskriver verkligheten genom att lagra data i tabeller.
DBA, databasadministratör (engelska: DBA, database administrator). En person eller en grupp av personer som är ansvariga för driften av ett databassystem.
Tillämpningsprogram eller applikationsprogram (engelska: application program). Ett program som är avsett för ett specifikt ändamål, till exempel för att låta SJ:s resenärer boka tågresor. I databassammanhang tänker vi oss att alla data lagras i en databas som hanteras av databashanteraren, och tillämpningsprogrammet kommunicerar med databashanteraren. Tillämpningsprogrammet är enklare att använda för resenären än om hon skulle kommunicera med databashanteraren direkt.
Tillämpningsprogrammerare eller applikationsprogrammerare (engelska: application programmer). En person som skriver tillämpningsprogram.
Datakatalog (engelska: data dictionary, data catalog). Databashanterarens lagrade meta-data om en databas.
Meta-data (engelska: meta-data). Data om data. Används av databashanterare för att den ska veta vad det är för data som den hanterar. Meta-data består av databasens schema (till exempel vilka tabeller som finns), men också av information om vilka data som just nu finns i databasen (till exempel hur många rader som varje tabell innehåller).
Det finns många databasböcker på grundnivå, och de brukar alltid ha ett eller två kapitel i början som ger en inledning till området. Följande är några av de böcker som ofta används i grundläggande databaskurser på högskolor och universitet runt om i världen. (Se kapitel 33, Litteratur och resurser, på sidan 671 för fullständiga uppgifter om böckerna.) De angivna kapitlen i böckerna motsvarar det här kapitlet.
302 Ordet konsistent är en försvenskning av engelskans consistent. En del anser att det ordet egentligen inte finns på svenska med den betydelsen, och att man därför bör undvika att använda det.
7 Göran Hasse har berättat att han på kurser brukar visa ett exempel med Sveriges 10 miljoner invånare på en textfil respektive i en databas. En enkel sökning i textfilen med Unix-sökkommandot grep brukar gå flera gånger snabbare än samma sökning i databasen! Det är först när man slår samman data från flera tabeller som databashanteraren blir snabbare.
8 Det svenska ordet konceptuell betyder begreppsmässig, precis som det engelska ordet conceptual, men det svenska ordet koncept betyder utkast eller kladd. Det engelska ordet concept ska därför inte översättas med koncept, utan med begrepp.
9 Nätverksmodellen har inget med datornätverk att göra, utan går ut på att man lagrar data i poster som innehåller länkar till varandra. Länkarna bildar en sorts nätverk.
11 Det finns enstaka databashanterare som arbetar direkt med en konceptuell datamodell, till exempel ER-diagram. Det förekommer ibland också felaktiga påståenden om att någon databashanterare arbetar med ER-diagram när det egentligen inte alls handlar om ER-diagram utan om att rita upp tabeller – som ju hör till implementationsmodellen – på ett sätt som har vissa likheter med ER-diagram.
Om man ska skapa en databas som beskriver en del av verkligheten, till exempel ett företag, brukar man börja med att göra en beskrivning av hur den delen av verkligheten ser ut och fungerar. Denna beskrivning på hög nivå kallas en konceptuell eller begreppsmässig beskrivning. I stället för "beskrivning" säger man ofta schema.
Det konceptuella schemat behöver egentligen inte ha någonting med datorer att göra, utan är bara en beskrivning av verkligheten som lika gärna skulle kunna användas till exempel av någon som vill analysera hur företaget fungerar. Om man vill skapa en databas, måste det konceptuella schemat översättas till ett schema som går att mata in i en databashanterare. Om man använder en relationsdatabashanterare, består det schemat av en eller flera tabeller.
32En vanlig konceptuell datamodell är den så kallade ER-modellen, och det är den vi ska använda i det här kapitlet. "ER" står för "EntityRelationship", dvs ungefär "saker" och "samband". En beskrivning som man skapar med hjälp av ER-modellen ritas upp som en bild med olika figurer, och den kallas ER-schema eller ER-diagram.
I dag använder man för det mesta relationsmodellen när man arbetar med databaser. När man ska skapa en databas skulle man därför kunna tycka att det vore enklast att direkt bestämma tabellernas utseende, i stället för att gå omvägen med att först göra ett konceptuellt schema.
Ett schema med tabeller kräver dock en del speciallösningar som visserligen är enkla när man kan dem, men som ändå gör att man inte kan koncentrera sig helt på den del av världen som man vill avbilda i databasen, och hur den fungerar. I stället måste man tänka på "tabelldetaljer", som till exempel att man ibland behöver införa extra hjälptabeller. Tabeller kan också fort bli oöverskådliga, med långa rader av kolumner (!) och med mer eller mindre kryptiska kopplingar mellan de olika tabellerna.
ER-diagram är därför lättare att jobba med, i synnerhet för den som är ovan vid tabeller. Det brukar leda till en bättre design i slutänden 33 om man börjar med att rita ER-diagram, och sedan översätter till tabeller, i stället för att försöka konstruera tabeller direkt.
Det är viktigt att databasens schema blir rätt och bra. En felaktigt eller dåligt uppbyggd databas kan ge stora och långvariga svårigheter. När man väl konstruerat en databas och börjat fylla den med data, kan den finnas kvar i årtionden, kanske långt efter att de nuvarande applikationsprogrammen slutat användas och ersatts av nya, som arbetar med samma databas. Även om det i teorin går att ändra databasens schema medan den är i drift, kan det i praktiken vara mycket svårt. Då sitter man där, med årtionden av problem. Därför är det mycket viktigt att göra ett korrekt och bra schema för databasen.
Rita upp de typer av saker som ska finnas i databasen. De kallas entitetstyper, och ritas som fyrkantiga lådor:
Rita sen upp de samband som finns mellan de olika typerna av saker. De kallas sambandstyper, och ritas som diamanter mellan de fyrkantiga lådorna:
Det här ER-diagrammet betyder alltså att det finns personer, det finns hus, och personerna bor i husen. (ER-diagrammet säger dock inget om hur många personer och hus det är, eller vilka, och vilka personer som bor i vilka hus. Allt det där är data, och ER-diagrammet är ett schema.)
34"Sambandstyp" heter "relationship type" på engelska, och "samband" heter "relationship", men undvik att kalla dem för "relation" eller "relationstyp" på svenska. I databassammanhang är en "relation" en tabell i relationsmodellen.
När man bestämmer vilka entiteter och samband som ska lagras i databasen, måste man förstås utgå från vad databasen ska användas till. Man gör en avbildning av världen, men det är bara precis de delar som behöver vara med i databasen som ska vara med i databasen.
Man kan också rita ut mer exakt vad det är för sorts sambandstyp:
"N" betyder att det kan bo flera personer i varje hus, och "1" betyder att varje person bara kan bo i ett hus. Det kallas ett många-till-ettsamband.
"Många" betyder i det här sammanhanget noll, ett eller flera. Det kan finnas hus som det inte bor någon i, det kan finnas hus som det bara bor en person i, och det kan finnas hus som det bor en miljon personer i.
Sambandstyper kan vara av tre olika slag, så kallade kardinalitetsförhållanden:
• Ett-till-ett-samband 1 eller 1:1-samband. En sambandstyp där en sak av något slag kan höra ihop med en sak av ett annat slag, och varje sak av det andra slaget kan höra ihop med en sak av det första slaget. Exempel: En person kan vid ett och samma tillfälle bara köra en bil, och varje bil kan bara köras av en person. 2 35
• Ett-till-många-samband eller 1:N-samband. En sambandstyp där en sak av något slag kan höra ihop med flera saker av ett annat slag, men varje sak av det andra slaget kan bara höra ihop med en sak av det första slaget. Exempel: En person kan äga flera bilar, men varje bil kan bara ägas av en person. Om man vänder på det och börjar med bilarna blir det i stället ett många-till-ett-samband (N:1-samband).
• Många-till-många-samband eller N:M-samband. En sambandstyp där en sak av något slag kan höra ihop med flera saker av ett annat slag, och varje sak av det andra slaget kan höra ihop med flera saker av det första slaget. Exempel: En person kan äga flera hus, och varje hus kan ägas gemensamt av flera personer.
ER-diagrammet är ett schema, och beskriver vilka data som kan lagras. Det är inte en avbildning av data.
Om Person alltså är en entitetstyp, brukar man kalla de enskilda personerna för entitetsinstanser. (Ja, egentligen är det ju inte personerna, utan deras representation i databasen.) Man kan också kalla personerna för entiteter, men det kan vara förvirrande eftersom den benämningen ibland används även för entitetstyperna. På samma sätt talar man om sambandsinstanser.
Så här skulle det kunna se ut om man ritade upp instanserna i en databas med ER-schemat ovan:
37Det går bra att ha flera olika sambandstyper som binder ihop samma entitetstyper. Till exempel kan man tänka sig att personer både kan bo i husen och äga dem:
Hus kan ägas gemensamt av flera personer, och varje person kan vara med och äga flera hus. Varje person kan bara bo i ett hus, men flera personer kan bo i samma hus.
Det kallas fullständigt deltagande: alla personer som finns med i databasen måste delta i ett boendesamband. Motsatsen, dvs att det kan finnas personer i databasen som inte bor någonstans, brukar kallas partiellt deltagande. En sambandstyp kan ha fullständigt deltagande på ena sidan, på båda sidorna, eller inte på någon sida.
Ett annat exempel: Genom att ändra lite i figuren, säger vi i stället att en person kan bo i flera olika hus, men alla personer behöver inte bo någonstans, och i varje hus bor det bara en person. Det finns inga hus som det inte bor någon person i.
Om sakerna eller sambanden har egenskaper, kallar man dem attribut, och ritar dem som ovaler:
39Att Nummer på Person är understruket betyder att personernas nummer är unika, dvs två personer kan inte ha samma personnummer. Personnumret är vad som brukar kallas för en nyckel. Även husens adresser är en nyckel.
Även sambandstyper kan ha egenskaper:
Inflyttningsår anger vilket år en person flyttade in i det hus där hon bor. I det här fallet skulle man också kunna sätta attributet Inflyttningsår på personen, eftersom varje person bor i exakt ett hus, och alltså har exakt ett inflyttningsår, men det är nog naturligare att låta inflyttningsåret höra till Bor i-sambandet.
Det finns också fall när det är nödvändigt att ha attributet på sambandstypen:
Nybörjare på ER-diagram gör ibland följande fel. Om de får uppgiften att rita ett ER-diagram för sin skola, börjar de med att rita en ruta:
Det här är förstås galet, eftersom det här ER-diagrammet säger att det finns en entitetstyp som heter "skola". Alltså kommer databasen att innehålla ett antal skolor (entitetsinstanser av entitetstypen "skola"). "Skola" hade passat som rubrik till hela ER-diagrammet, men inte som en entitetstyp.
Ibland kan ett attribut vara sammansatt av flera delar, som hör ihop men som man även vill behandla var för sig. Då kan man rita det som ett sammansatt attribut:
Attributen ovan är "enkla" i betydelsen att varje enskild entitet ("entitetsinstans") har högst ett värde på attributet. Varje person har ett namn, ett personnummer och ett telefonnummer. Ibland räcker inte det. Till exempel kanske man vill kunna lagra flera telefonnummer som hör till en person. Då kan man rita det som ett flervärt attribut (även kallat multipelt attribut) genom att använda en dubbelellips runt attributnamnet:
En del attribut vill man kanske inte lagra i databasen, utan man kan räkna ut dem utifrån andra data som redan finns i databasen. Sådana härledda attribut markeras med en streckad oval:
I det här fallet ska man alltså inte lagra antal boende för varje hus, utan man räknar ut det på nytt varje gång man behöver värdet, 42 helt enkelt genom att räkna hur många instanser det finns av "bor i"-sambandet som hör ihop med just det huset.
Ibland vill man ha med saker som det är svårt att prata om som självständiga saker, utan de hör alltid ihop med en annan sak. Ta till exempel rummen i en lägenhet: köket, vardagsrummet, badrummet, med flera. Om vi bara ska lagra data om en enda lägenhet i databasen, räcker det att säga "köket", för då finns det bara ett kök att välja på. Men om vi har många lägenheter, till exempel i en databas för ett bostadsföretag, räcker det inte med "köket". Man vet inte vilket kök man menar, utan man måste alltid precisera sig med till exempel "köket i min lägenhet" eller "köket i lägenhet nummer 86".
Bostadsföretagets olika rum har alltså ingen egen nyckel. En nyckel i databassammanhang är något som man kan använda för att identifiera en viss sak, till exempel personnummer för personer. Personnumret 631211-1658 identifierar en viss person, men rumsnamnet köket räcker inte för att veta vilket kök man menar.
Om man ändå vill lagra rum i databasen, så kan man förstås hitta på en särskild nyckel för varje rum. Köket i min lägenhet kan till exempel heta kök 17, och köket i din lägenhet kan heta kök 42. Badrummet i min lägenhet kanske heter badrum 203, och ditt badrum heter badrum 65. Eller så ger man bara alla rum var sitt nummer: mitt kök är rum nummer 828, och mitt badrum är rum nummer 703.
Det kan vara lite opraktiskt, och det vore nog naturligare att fortfarande säga köket och badrummet, men dessutom tala om vilken lägenhet som det hör till. Då kan man använda en svag entitetstyp:
43Det här ER-diagrammet säger att det finns lägenheter, som var och en har ett eget nummer. Lägenheterna innehåller rum, men rummen har bara namn, som inte är unika för hela databasen. Däremot är de unika inom en viss lägenhet: om man vet ett rumsnamn, och numret på den lägenhet som rummet hör till, räcker det för att unikt identifiera rummet i hela databasen.
Vi har ritat Rum med en dubbel fyrkant, för att ange att det är en svag entitetstyp. Rumsnamnet Namn kallas partiell nyckel, och ritas understruket med ett (hmmm...) streckat streck. Sambandstypen Innehåller, som används för att identifiera vilken lägenhet ett rum tillhör, kallas identifierande sambandstyp, och ritas som en dubbel diamantbox. Entitetstypen Lägenhet kallas identifierande entitetstyp. Naturligtvis kan vi inte ha några lösa rum, som inte tillhör någon lägenhet, så vi ritar ut ett fullständigt deltagande med hjälp av ett dubbelstreck mellan Rum och Innehåller. Sambandstypen blir 1:N, eftersom varje rum hör till en lägenhet, och en lägenhet kan innehålla flera rum.
Ibland säger man att den identifierande entitetstypen äger den svaga: en lägenhet "äger" de rum som den innehåller. Om man tar bort en viss lägenhet i databasen, vill man också ta bort rummen som den innehåller.
Ibland kan man välja mellan att använda sig av en sambandstyp eller en entitetstyp. Att använda en entitetstyp i stället för en sambandstyp betyder att man betraktar kopplingen som en egen sak, och inte bara som en koppling mellan två andra saker. Det kallas ibland objektifiering.
44Till exempel kan vi göra om Bor i-sambandet till en egen entitetstyp, som vi kallar Boende:
Nu ser vi inte längre ett boende som en sambandstyp som knyter ihop en person och ett hus, utan nu är ett boende en egen sak. Varje boende-entitet hör ihop med en person (den som bor) och med ett hus (huset hon bor i).
Det finns två anledningar till att man ibland vill objektifiera en sambandstyp:
En sambandstyp som Bor i-sambandet i de tidigare exemplen är en koppling mellan en person och ett hus. Man kan bara ha en sådan koppling mellan en viss person och ett visst hus. Antingen bor man i huset, eller också inte. Det gäller alla sambandstyper. Det går inte att ha flera instanser av samma sambandstyp mellan samma entitetsinstanser.
Antag att vi ska rita ett ER-diagram som modellerar att personer har sett filmer:
N:M-sambandet betyder att en person kan ha sett flera filmer, och att en film kan ha setts av flera personer. Men vi kan inte ange att en person sett samma film flera gånger. (Jo, vi skulle kunna lägga på ett attribut Antal på sambandstypen, eller ett flervärt attribut Datum som anger alla datum som personen sett filmen. Men som det ser ut nu så har personen antingen sett filmen, eller också har hon inte sett den.)
Bor i-sambandet mellan personer och hus ovan kallas ett tvåvägssamband, eftersom det binder ihop två entitetstyper. Man kan också ha sambandstyper som binder ihop fler entitetstyper, till exempel tre.
Kanske vill vi än en gång hålla reda på vilka personer som har sett vilka filmer, och vi vill dessutom veta vilka biografer de sett filmerna på. Jag har kanske sett Spindelmannen på Rigoletto och på Filmstaden 3, så jag kan uttala mig om hur upplevelsen är av att se just den filmen där. Däremot kan jag inte säga något om hur bra Spindelmannen passar ihop med ljudsystemet på Röda kvarn, för jag har inte sett den filmen på den biografen.
46Det här ER-diagrammet visar att personer har sett filmer på biografer. Ett Sett-samband ("en instans av sambandstypen Sett") binder alltså ihop en person, en film och en biograf.
Ett trevägssamband går normalt inte att byta ut mot tre tvåvägssamband. Jämför med det här ER-diagrammet:
I en databas med det här schemat kan vi lagra att jag har sett Spindelmannen, att jag har besökt Rigoletto, och att Spindelmannen visats på Rigoletto, men inte att jag såg Spindelmannen just på Rigoletto. Jag kanske såg någon annan film när jag var på den biografen.
Det kan vara lite lurigt att rita ut kardinalitetsförhållandena i ett flervägssamband. Det blir lättare om man objektifierar Sett-sambandet genom att förvandla det till entitetstypen Tittning:
47Här ser vi alltså inte filmseende som en sambandstyp som knyter ihop en person, en film och en biograf, utan nu är filmseendet en egen sak. Varje filmtittningsentitet hör ihop med en person (vem), en film (vad) och en biograf (var).
ER-diagrammet med den nya entitetstypen Tittning säger inte riktigt samma sak som trevägssambandet, för nu kan vi lagra samma kombination av person, film och biograf flera gånger. Dessutom vill vi kanske hitta på en nyckel till den nya entitetstypen. I stället kan man skapa en svag entitetstyp med tre identifierande samband. Då säger man samma sak som med trevägssambandet: 3
Erfarenhet från objektorienterad 4 datamodellering visar att det ofta är praktiskt att låta en klass ärva egenskaper från en annan. Därför har man konstruerat en utvidgad ER-modell, den så kallade EER-modellen. "EER" står för "Enhanced 5 Entity-Relationship".
Det här säger att databasen innehåller personer. En del av de personerna är (förutom att de är personer) lärare, en del är studenter, och en del är astronauter.
En student har alla egenskaper som en person har, till exempel att hon har ett personnummer och bor i ett hus, för en student är ju en person. Man säger att entitetstypen Student ärver från entitetstypen Person. Sen kan studenter ha ytterligare egenskaper, förutom personegenskaperna, till exempel att de kan läsa kurser.
Vi kan kalla Student för underentitetstyp och Person för överentitetstyp. I objektorienterade sammanhang kallar man underentitetstypen för subklass eller härledd klass, och överentitetstypen för superklass eller basklass. Eftersom den objektorienterade terminologin är så vanlig i andra sammanhang, skriver vi i fortsättningen oftast "klasser".
Strecket med "gaffelsymbolen" anger att Student är en subklass (dvsunderentitetstyp) till Person, och att Student alltså ärver alla 49 egenskaper som Person har. Man kan också se det som en sorts delmängdssymbol: mängden studenter är en delmängd av mängden personer.
Låt oss gå tillbaka till vårt tidigare exempel med personer som bor i hus:
Vi antar nu att en del av personerna är studenter. Precis som alla andra personer har en student ett namn och ett personnummer, och hon bor i ett hus. Men en student har dessutom ett medelbetyg, och hon läser kurser:
Om vi vill ange samma sak utan arv, skulle vi bli tvungna att rita något i den här stilen:
50Notera att entitetstypen Student har samma attribut och deltar i samma sambandstyper som Person, plus egna attribut och sambandstyper. De gemensamma attributen och sambandstyperna skulle vi bli tvungna att rita två gånger. Dessutom har vi tappat bort informationen om att alla studenter är personer.
Vi går tillbaka till det första exemplet på EER-diagram. Diagrammet visar, som vi kanske minns, att det finns personer, och en person kan vara lärare, student eller astronaut:
Men det finns några frågor som det här EER-diagrammet inte ger svar på:
Den första frågan handlar om fullständig eller partiell specialisering – eller, med objektorienterad terminologi, ifall klassen Person är en abstrakt klass eller inte. Den andra frågan handlar om ifall subklasserna är disjunkta eller om de är överlappande.
Det här går att rita upp i EER-diagrammet. Här har vi lagt till en del information:
d:et står för disjunkt, och betyder att subklasserna är disjunkta: en person kan vara antingen student, lärare eller astronaut, men hon kan inte vara flera saker på en gång. Dubbelstrecket mellan entitetstypen Person och cirkeln med d:et betyder fullständig specialisering: varje person måste vara student, lärare eller astronaut, och det får inte finnas personer som inte är någondera. Vi känner igen notationen med dubbelstreck från hur man ritar fullständigt deltagande i sambandstyper.
52Motsatsen till d:et (för disjunkta subklasser) är o, som står för overlapping, vilket är engelska för överlappande. Här är en annan variant på samma EER-diagram:
Det här diagrammet visar, precis som förut, att personer kan vara studenter, lärare och astronauter. Men det visar också att det kan finnas personer som inte tillhör någon av de tre subklasserna, utan som bara är personer. Det visar dessutom att det kan finnas personer som tillhör mer än en subklass, och till exempel är både lärare och studenter samtidigt.
Om man ritat ett ER-diagram, och vill lagra sin databas i en databashanterare, måste man normalt översätta ER-diagrammet till en datamodell som databashanteraren förstår. Det kan vara till exempel en objektorienterad databashanterare, men det allra vanligaste är att man vill arbeta med en relationsdatabashanterare. ER-diagrammet ska alltså översättas till tabeller. Läs mer i kapitel 6, Översättning från ER-modellen till relationsmodellen.
I avsnitt 2.2 skrev vi att ER-diagram ger en fördel över relationsmodellens tabeller genom att vara både enklare och överskådligare. Därför är det för det mesta bättre att börja med att rita ett ER-diagram, som man sen översätter till tabeller, än att konstruera tabeller direkt. Men om nu ER-diagram har en fördel över tabeller, varför då alls blanda in relationsdatabaser? Det finns inget som säger att en databashanterare måste använda sig av relationsmodellen, så varför inte använda ER-diagram även i databashanteraren?
Det finns objektorienterade och objektrelationella databaser som använder en datamodell som är ganska lik ER-modellen. Det finns också åtminstone någon enstaka databashanterare där man kan mata in ER-diagram direkt. Men de allra flesta databashanterare kräver att man översätter sitt ER-diagram till något annat format. För det mesta använder man en relationsdatabashanterare, vilket betyder att data lagras i form av tabeller.
Det finns också en del val som man kan göra när man översätter från ER-diagrammet till hur saker ska lagras internt i databashanteraren, som det kanske är bättre att låta en människa göra, än att databashanteraren väljer en standardlösning. En standardlösning skulle kunna leda till att en del databaser blir onödigt stora, eller att en del sökningar går onödigt långsamt.
Här är några olika sätt att rita samma 1:N-samband mellan personer och hus:
54Det första alternativet är vårt vanliga skrivsätt, och visar att varje person bor i exakt ett hus, medan det i varje hus kan bo noll, en eller flera personer. I det andra anger man hur många sambandsinstanser varje entitetsinstans kan delta i: en person kan delta i exakt ett Bor i-samband, och ett hus kan delta i noll till hur många som helst. (Det blir alltså lite bakvänt jämfört med det första skrivsättet.) Pilen i det tredje alternativet liksom pekar ut det enda hus som en person bor i. Det fjärde alternativet är ganska vanligt i Sverige och brukar kallas för en "infologisk modell". "Gaffeln" ska illustrera att ett hus hör ihop med flera personer.
Det sista exemplet är ritat med UML, som egentligen inte är ett sätt att rita ER-diagram, utan ett objektorienterat modelleringsspråk. UML är ganska likt ER-modellen, men i UML kan man också bland annat modellera beteende, inte bara data. UML är ett betydligt större och mer komplext modelleringsspråk än ER-modellen.
Entity-Relationship-modellen, ER-modellen eller entitets-sambands-modellen (engelska: the Entity-Relationship model, the ER model, the E/R model). En konceptuell datamodell där man beskriver verkligheten genom att ange de typer av entiteter (engelska: entities), alltså ungefär "saker", som finns i den verkligheten, och vilka typer av samband (engelska: relationships) som finns mellan dessa. Beskrivningen ritas oftast upp grafiskt i form av ett ER-diagram.
ER-schema (engelska: ER schema). En beskrivning av verkligheten gjord enligt ER-modellen. Som vanlig utgör schemat en beskrivning av vilka data som kan finnas i databasen, oberoende av vilka data som råkar finnas där just nu.
ER-diagram (engelska: ER diagram). Ett ER-schema uppritat som ett diagram.
Entitetstyp (engelska: entity type). I ER-modellen beskriver man världen med hjälp av två grundläggande byggblock: typer av saker, eller entiteter, och typer av samband mellan dessa saker.
Sambandstyp (engelska: relationship type). I ER-modellen beskriver man världen med hjälp av två grundläggande byggblock: typer av saker, eller entiteter, och typer av samband mellan dessa saker. Undvik att översätta "relationship" med "relation" eller "relationstyp", för en relation är en tabell i relationsmodellen, och det är något helt annat.
Instans eller förekomst (engelska: instance). En viss sak som tillhör en entitetstyp eller sambandstyp. Till exempel kan personen Kalle vara en instans av entitetstypen Person.
Kardinalitetsförhållande (engelska: cardinality ratio). På vilket sätt som saker kan kopplas ihop av en sambandstyp: ett-till-ettsamband, ett-till-många-samband och många-till-många-samband.
Fullständigt deltagande (engelska: total participation). Kravet att varje förekomst av en entitetstyp måste delta i en viss sambandstyp.
Attribut (engelska: attribute). Betyder "egenskap", och används om egenskaperna hos entiteterna i ER-modellen.
Svag entitetstyp (engelska: weak entity type). En entitetstyp i ER-modellen där instanserna av den entitetstypen inte har en egen nyckel, och därför inte går att unikt identifiera utan hjälp av en annan entitetstyp.
56Den utvidgade ER-modellen, EER-modellen (engelska: the Enhanced Entity-Relationship Model, the EER model). En variant av ER-modellen där man lagt till arv, på samma sätt som i objektorienterad datamodellering. Beskrivningen ritas oftast upp som ett EER-diagram.
Fullständig specialisering (engelska: total specialization). Kravet att varje instans av basklassen måste tillhöra (minst) en av subklasserna.
Partiell specialisering (engelska: partial specialization). Egenskapen att varje instans av basklassen inte nödvändigtvis måste tillhöra någon av subklasserna.
Disjunkta subklasser (engelska: disjoint subclasses). Egenskapen att varje förekomst av en entitetstyp bara kan tillhöra (högst) en av den entitetstypens subklasser.
Överlappande subklasser (engelska: overlapping subclasses). Egenskapen att varje förekomst av en entitetstyp kan tillhöra mer än en av den entitetstypens subklasser.
UML. Av Unified Modeling Language. Ett grafiskt modelleringsspråk som påminner om ER-modellen, men som är mer omfattande och mer standardiserat. Läs mer på http://www.uml.org/ . 6
Varje jul ska jultomten distribuera flera miljarder julklappar till flera hundra miljoner barn. För att allt ska bli rätt måste jultomten och hans nissar hålla reda på stora mängder information. De måste till exempel hålla reda på vilka barn finns och var de bor, om de varit snälla eller elaka, och vilka presenter de önskat sig. Tidigare har det gått bra med papper och pärmar, men förra året blev mängden papper så stor att hela arkivavdelningen sjönk genom isen vid 57 Nordpolen, där tomten ju bor. Därför har jultomten bestämt sig för att datorisera julklappsadministrationen med hjälp av en databas.
Jultomten berättar att han vill ha med följande saker i databasen:
Rita ett ER-diagram för databasen. (Använd grunderna. Det behövs varken arv eller svaga entitetstyper.)
Jultomten är nöjd med det ER-diagram du ritade i övning 1, och nu har han kommit på ytterligare några saker som behöver vara med i databasen. Rita därför ett nytt diagram genom att utöka det gamla ER-diagrammet. Den här gången behöver du förmodligen använda arv, och därför ska du göra ett EER-diagram. Följande ska vara med:
Det är inte säkert att allt som tomten vill ha med går att representera i EER-diagrammet. Notera i så fall dessa separat, så att de kraven inte glöms bort.
De angivna kapitlen i böckerna motsvarar det här kapitlet.
1 På svenska ska sammansatta ord inte skrivas isär. Alltså heter det exempelvis "ett-till-ett-samband", och inte "ett-till-ett samband" eller "ett till ett samband". Det heter "ER-diagram" och inte "ER diagram". Det heter "databas" och inte "data bas". Alla sammansatta ord skrivs ihop. Varför göra fel när det är så lätt att göra rätt?
I kapitlet om ER-modellering gick vi igenom hur man ritar ett ER-schema som beskriver en del av världen, så att man sedan kan översätta ER-schemat exempelvis till ett relationsschema som kan användas i en relationsdatabashanterare. I det här kapitlet ska vi inte lära oss rita några nya figurer i ER-schemat, utan vi ska titta på hur man bäst modellerar några olika företeelser från verkligheten. Det vi tar upp är
Samma bit av verkligheten brukar kunna modelleras på flera olika sätt, bland annat med flera olika ER-scheman. För att databasen ska bli användbar krävs det att man väljer en modellering som är bra, i stället för en dålig.
Man kan dela in hierarkierna i två olika typer:
En heterogen hierarki, som innehåller flera olika typer av saker, ritar man i ER-modellen genom att flera olika entitetstyper kopplas ihop med hjälp av 1:N-sambandstyper. Dubbelstrecken betyder som vanligt fullständigt deltagande, dvs i det här fallet att varje person i databasen måste tillhöra en avdelning, och att varje avdelning måste tillhöra ett lokalkontor:
61Vi ritar upp hur instanserna kan se ut i en databas med det ER-schemat:
Personer är chefer för varandra. Sambandet är av typen 1:N, så en person kan vara chef för flera andra personer, men varje person kan bara ha en enda chef.
För att förtydliga att det är så man menar, och inte tvärtom (att en person kan ha flera chefer, men bara kan vara chef för en enda person), kan man sätta ut rollnamn i ER-diagrammet: Chef-sambandet kan binda ihop en person med flera underställda, men bara med en chef. Rollnamn är ofta bra just för "en-entitetstyps-sambandstyper":
Eftersom en person kan vara chef för flera andra, som i sin tur är chefer för ytterligare andra, så kan ett sånt här ER-diagram alltså användas för att beskriva en hierarki. Vi ritar upp hur instanserna kan se ut i en databas med det ER-schemat. Pilarna anger vilka som är chefer för vilka:
63Notera att en del personer inte har någon chef alls, och att en del personer är chefer för sig själva. Dessutom finns det mer än en chefshierarki. Vi hade kanske tänkt oss att alla personerna skulle ingå i en enda hierarki, med överbossen överst, men det finns inget i ER-diagrammet som säger att det måste vara så. Om man vill ha en enda hierarki, måste man hålla reda på det på annat sätt.
Något som är generellt gäller flera olika saker, och något som är speciellt eller specifikt gäller i färre, eller kanske bara ett enda, fall. Till exempel är det ett generellt uttalande att säga att frukter är nyttigt, medan det är mer specifikt att säga att apelsiner är nyttiga. (Tänk på att en general är en officer som för befäl över en väldig massa soldater, medan en specialist är en person som kan väldigt mycket inom ett enda område.)
Generalisering handlar om att göra om något så att det gäller fler saker, medan specialisering handlar om att göra om något så att det gäller färre saker. Om någon säger att apelsiner och bananer är goda, kan man generalisera det uttalandet till att handla om alla frukter: alla frukter är goda. Man har generaliserat lite mindre om man bara säger att alla importerade frukter är goda, och man har generaliserat ännu mer om man säger att all mat är god.
64Saker som är generella kan ofta användas inom ett stort område, och de är kanske tillräckligt allomfattande för att de inte ska behöva ändras varje gång någon förutsättning ändras. Å andra sidan är något som är mer specifikt ofta mer användbart: Om någon berättar att hon tycker om apelsiner är det lättare för mig att handla åt henne än om hon säger att hon tycker om frukt.
Vi ska nu titta på två olika typer av generalisering som kan användas i datamodellering:
Antag att vi har några olika sorters mellanmål:
Detta ER-schema säger att databasen innehåller ett antal frukter, ett antal grönsaker och ett antal sorters godis. Än så länge har vi förstås inte sagt något om vilka frukter, grönsaker och godissorter som ska finnas, för det hör till databasens data och inte dess schema. Men som ett exempel skulle vi kunna ha frukterna apelsin, banan och citron.
Eftersom frukter, grönsaker och godis alla är mellanmål, har de kanske en del saker gemensamt som vi vill representera i databasen. När vi ritar ut attributen, kan det visa sig att de tre entitetstyperna har likartade attribut:
65Det kan också vara så att de olika entitetstyperna deltar i likartade sambandstyper. Antag till exempel att vi vill modellera att olika personer tycker om olika sorters mellanmål. Då måste vi rita ett Tycker om-samband för varje mellanmålstyp:
Det blir förstås jobbigt att rita ut alla dessa attribut och sambandstyper, särskilt om vi har många fler än bara tre olika sorters mellanmål. Schemat blir onödigt stort och oöverskådligt. Eftersom samma information upprepas på flera ställen, till exempel att Tycker om-sambandet är ett många-till-många-samband, blir det också omständligt att ändra i schemat.
66I ovanstående EER-diagram har vi angett att varje mellanmål måste tillhöra exakt en av grupperna frukter, grönsaker och godis. Om den detaljen inte är viktig i sammanhanget, kan man använda en förenklad notation:
Införandet av en superklass kan göra schemat mindre, överskådligare och lättare att ändra. Men även om den sortens generalisering alltså gör schemat lättare att ändra, måste schemat fortfarande ändras. Om det tillkommer en ny grupp av mellanmål, till exempel energidrycker, måste vi rita om schemat och lägga till en ny subklass. Om EER-schemat använts som grund för översättning till ett relationsschema, och om databasen redan satts i drift, måste vi förmodligen logga in som databasadministratör och skapa en ny tabell. Om det finns applikationsprogram eller formulär som används vid arbetet med databasen, måste de kanske göras om för att ta hänsyn till den nya mellanmålsgruppen.
En vanlig användare med rätt rättigheter kan lägga in ett nytt mellanmål, till exempel frukten dadel, i databasen, men ska man lägga 68 in en ny mellanmålsgrupp måste databasen designas om. Schemat måste ändras.
Information som ändras ofta vill man kunna ändra utan att behöva ändra i databasens schema. Den bör därför inte ligga i schemat, utan i databasens data. Det är ju inte meningen att schemat i en databas ska ändras särskilt ofta. Om vi tror att det är vanligt att nya mellanmålsgrupper dyker upp, och om vi vill att vanliga användare ska kunna lägga in dessa nya grupper, ska vi alltså flytta informationen om grupperna från schemat till data.
Som ett första försök ändrar vi vårt ursprungliga ER-diagram, som såg ut så här:
Attributet Grupp anger vilken grupp av mellanmål som ett visst mellanmål tillhör, till exempel att apelsiner är frukt. Nu kan vem som helst lägga in det nya mellanmålet Red Bull, och ange att det tillhör den nya gruppen energidrycker. Vilka mellanmålsgrupper som finns lagras inte separat i databasen, utan det är bara ett vanligt attribut. Man kan förstås ta reda på vilka mellanmålsgrupper som finns i databasen vid ett visst tillfälle, genom att helt enkelt titta på attributet Grupp på de inlagda mellanmålen och se vilka olika värden det har, men det finns ingen färdig lista eller tabell över vilka mellanmålsgrupper man kan välja på.
Det är kanske olämpligt att inte alls styra mellanmålsgrupperna. Det finns ingen kontroll på inmatningen av grupper, och det går inte att se vilka grupper som är tillåtna eller vad de egentligen kallas. Heter det till exempel "energidryck" eller "sportdryck"? En bättre lösning kan vara att även lagra mellanmålsgrupperna i databasen, och det gör vi förstås genom att skapa en entitetstyp som heter Mellanmålsgrupp:
69Om man nu vill lägga in energidrycken Red Bull, så får man börja med att lägga in en ny mellanmålsgrupp som heter Energidrycker. Sen kan man lägga in Red Bull, och välja att den tillhör Energidrycker. Allt är vanlig inmatning av data i databasen, och det krävs inga ändringar i schemat.
Ett problem: Attributet Tillverkare, som var specifikt för entitetstypen Godis, finns inte med i den mer generella entitetstypen Mellanmål. Vi kan alltså inte längre lagra data om vem som tillverkat en viss godissort. Det är ett vanligt problem när man generaliserar schemat för en databas: schemat blir flexiblare i den betydelsen att man kan lägga in fler nya saker, men samtidigt minskar möjligheterna till specialanpassade lösningar, eftersom allt ska hanteras med samma entitetstyper (och, när man sen översätter till ett relationsschema, samma tabeller). Vilket man väljer blir förstås en avvägning mellan å ena sidan flexibiliteten i ett generellt schema, och å andra sidan möjligheterna till "skräddarsydda" lösningar i det specialiserade schemat.
Skillnaderna mellan de olika mellanmålslösningarna kan framgå tydligare om man studerar användargränssnittet till ett applikationsprogram som arbetar med databasen. I lösningen där de olika mellanmålsgrupperna ingick som en del av schemat kan man låta användaren välja i en förutbestämd lista vilken mellanmålsgrupp ett nytt mellanmål tillhör:
70Det här är den lösning som rekommenderas om man vill ha en databas som är både lättanvänd och flexibel! Det är lite krångligare för den som konstruerar databasen, men bättre för användarna.
Motsatsen till generalisering är specialisering. När det gäller datamodellering med ER-modellen betyder specialisering antingen att man inför nya subklasser till en entitetstyp i ett EER-diagram eller att man flyttar information från databasens data till dess schema. Om vi utgår från det sista ER-diagrammet ovan, det med entitetstypen Mellanmålsgrupp, kanske vi bestämmer oss för att vi behöver lagra specialiserad information om de olika mellanmålen, beroende på vilken grupp de tillhör, och att det därför inte räcker med den generella entitetstypen Mellanmål. Då kan vi gå tillbaka till EER-schemat som har en subklass för varje mellanmålsgrupp. I det schemat kan man lägga in extra attribut för varje mellanmålsgrupp.
Nu ska vi se ett exempel på hur datamodelleringen kan bli väldigt besvärlig om man väljer fel generalitetsnivå. Vi tänker oss en kommun som har ett antal nämnder och andra myndigheter, till exempel 72 miljönämnden och räddningstjänsten. Dessa nämnder och myndigheter genomför inspektioner av olika slag. Till exempel gör miljönämnden miljöinspektioner, och räddningstjänsten gör brandsyner. Varje inspektion sker på en viss plats, och dessa platser kan utgöras till exempel av rektorsområden och vårdcentraler. Vi ritar ett ER-diagram över de olika typerna av inspektioner, och de olika typerna av platser:
Som synes blir ER-diagrammet ohanterligt redan med tre nämnder och tre platser som kan inspekteras. (Dessutom är modelleringen inte riktigt rätt, för det ser ut som om en och samma inspektion kan ske på flera olika ställen.) Vi måste förenkla schemat, och det gör vi genom att införa de mer generella entitetstyperna Inspektion och Plats, och koppla ihop dem med en generell Var? -sambandstyp. Nu finns det inspektioner av olika slag, som kan ske på platser av olika slag:
73Nu är schemat enklare, mer överskådligt och lättare att ändra. Men man måste fortfarande ändra i schemat om det tillkommer en ny typ av inspektion eller en ny typ av plats. Kommunen måste kanske ringa efter en datakonsult, som kommer och gör ändringarna. Om vi vill att det ska gå lätt att lägga till nya inspektionstyper och nya platstyper, kan vi generalisera genom att flytta informationen om vilka typer som finns från databasens schema till dess data:
Generalisering (engelska: generalization). Att göra om något, till exempel ett uttalande eller ett databasschema, så att det gäller för fler saker eller fall. Samtidigt blir det ofta mindre detaljerat, och kanske också mindre användbart.
Specialisering (engelska: specialization). Att göra om något, till exempel ett uttalande eller ett databasschema, så att det gäller för 74 färre saker eller fall. Samtidigt blir det ofta mer detaljerat, och kanske också mer användbart.
Företaget Karlssons maskin och kompani tillverkar och säljer en mängd olika produkter. Särskilt deras självrengörande kattlåda har blivit en stor succé.
Nu behöver de en databas för att hålla reda på sina produkter, och tillverkningen av dem.
Det som ska lagras i databasen är följande:
Det finns en hel mängd olika kategorier, och det tillkommer hela tiden nya som man inte tänkt på tidigare. Varje detalj ska tillhöra en viss kategori.
75Hur bör man modellera detta? Visa vilka tillägg och förändringar som bör göras i schemat, och motivera varför den valda lösningen är lämplig.
Även med den nya uppdelningen i kategorier blir det svårt att hålla reda på alla detaljer. Mängden av kategorier blir svåröverskådlig. Därför vill man i sin tur organisera kategorierna i kategorier, som sen i sin tur kan organiseras i kategorier, och så vidare. Till exempel kanske detaljen 20-millimeters träskruv tillhör kategorin träskruv, som i sin tur tillhör kategorin skruv, som sen i sin tur tillhör kategorin metalldetaljer.
Hur bör man modellera detta? Visa vilka tillägg och förändringar som bör göras i schemat, och motivera varför den valda lösningen är lämplig.
Vi börjar med att beskriva den traditionella, databascentrerade, designprocessen. Man utgår från vilka data som ska hanteras, och börjar med att skapa databasen. De applikationsprogram som ska användas för att arbeta med databasen kommer i andra hand, men påverkar förstås databasens utseende.
Ett annat sätt är att börja med programmet, och (mer eller mindre automatiskt) översätta programmets data till ett databasschema. Det brukar kallas ORM, eller Object-Relational Mapping, och beskrivs kort i avsnitt 4.2 nedan.
Designen av en databas består i att utveckla scheman, dvs beskrivningar av vilka data som kan lagras i databasen. Vi kan dela upp arbetet i fyra viktiga steg:
Ovanstående steg beskriver designen av databasens data. Men ett databassystem består inte bara av data, utan det ingår ofta också ett eller flera tillämpningsprogram, även kallade applikationer eller applikationsprogram. Tillämpningsprogrammen är program som arbetar med databasen för att till exempel hjälpa användarna att göra olika sökningar. Dessa program innehåller en eller flera transaktioner, vilket här helt enkelt betyder en följd av operationer som ska göras med databasen. Till exempel kan transaktionen gå ut på att en användare matar in namn och adress på en ny kund, varefter tillämpningsprogrammet först kontrollerar att den kunden inte redan finns i databasen, och sen lägger in den i databasen. Det finns en växelverkan mellan design av data och design av tillämpningsprogram: vilka operationer som ska utföras på databasen påverkar förstås vad man behöver lagra, och vad som finns lagrat påverkar hur man ska arbeta med det.
Det finns även andra steg som det kan vara bra att ha med när man tänker på hur en databas skapas:
Tänk på att den typ av "vattenfallsmodell" som visas i figuren är en förenklad bild. I verkligheten handlar det om iterationer snarare än faser. Till exempel arbetar man med den fysiska designen, för att få 80 systemet tillräckligt snabbt, och inser att det är omöjligt med den konceptuella design man valt. Då måste man gå tillbaka och ändra.
När man utvecklar program i dag arbetar man ofta objektorienterat, med objekt, klasser, arv och så vidare, och man använder objektorienterade programmeringsspråk som C# och Java. Ofta vill man också ha persistenta data, dvs data som finns kvar även om man avslutar programmet och stänger av datorn, och då är det förstås praktiskt att lagra sina data i en databas, med hjälp av en databashanterare. Det finns objektorienterade databaser, men den vanligaste typen av databaser är inte objektorienterade, utan relationsdatabaser med tabeller. Då uppstår problemet att data måste översättas mellan programmets objektorienterade datamodell och databasens relationsdatamodell.
En enkel klass med attribut (även kallade medlemsvariabler) kan översättas med en tabell, där varje attribut i klassen blir en kolumn i tabellen, och varje objekt motsvaras av en rad i tabellen, men mer komplicerade data, till exempel med kopplingar mellan objekt, är förstås mer komplicerade att översätta. Se också kapitel 6, Översättning från ER-modellen till relationsmodellen.
ORM, eller Object-Relational Mapping ("objekt-relations-översättning"), betyder översättning mellan objektorienterade data och data i en relationsdatabas. Termen ORM handlar egentligen om vilken metod som helst som används för att översätta mellan objektorienterade data och data i en relationsdatabas, men den brukar användas för att beskriva automatisk översättning. Denna automatiska översättning kan göras på olika sätt, till exempel genom att ett program läser programkoden för de klasser man skrivit i ett objektorienterat programmeringsspråk, och automatiskt skapar både ett relationsschema och programkod för att göra översättningen. Den skapade programkoden kan spara data från det objektorienterade programmet i relationsdatabasen, och hämta data från relationsdatabasen till det objektorienterade programmet.
81Verktyg som dessa kan göra många saker automatiskt, men de är ingen ersättning för kunskaper om databaser. För att använda ORM på ett effektivt sätt i verkligheten (eller alls!) måste man ändå kunna SQL och relationsdatabaser. Med ORM kan det till och med vara ännu viktigare att kunna konstruera ett bra databasschema. Om ORM-verktyget genererar programkod utifrån en beskrivning av vilka data man vill ha i databasen, och man därefter arbetar vidare med den programkoden, kan även en liten ändring av den ursprungliga beskrivningen av databasen göra att koden måste genereras på nytt.
Det här en bok om databaser, och den här typen av verktyg ligger utanför bokens ämne. Därför går vi inte inte in på djupet när det gäller ORM-ramverk.
Designprocessen brukar beskrivas ganska kort i de flesta grundläggande databasböcker. För en mer utförlig beskrivning, till exempel om hur man genomför intervjuer för att luska ut vilka krav databasen behöver uppfylla, hänvisas exempelvis till Database Design for Mere Mortals av Michael J. Hernandez.
Grundböcker om databaser tar inte alltid upp ORM-verktyg, eller tar upp dem mycket kort. För den som behöver en djupare diskussion om ORM-verktyg, och en introduktion till hur man använder dem i praktiken, hänvisas i stället till specialiserade böcker och böcker om programmering.
82Relationsmodellen är en av flera olika datamodeller, dvs sätt att organisera sina data, som används i databaser. Det är den vanligaste modellen i dag. Den går, enkelt uttryckt, ut på att man lagrar data i tabeller.
Relationsmodellen är den helt dominerande datamodellen i dagens databashanterare. Om du någon gång kommer i kontakt med databaser, vilket är ganska troligt, är chansen därför stor att det är just relationsdatabaser det rör sig om.
Det gäller även när man har ett program som ska lagra sina data i en databas. Även om man för det mesta programmerar objektorienterat, brukar man lagra data i en relationsdatabas. I så fall måste man skriva programkod som översätter från programmets objekt och klasser till relationsdatabasens tabeller. Det finns också verktyg och ramverk som gör översättningen automatiskt, som Hibernate för Java och Microsofts Entity Framework, men erfarenheten visar att även om dessa verktyg kan göra många saker automatiskt, måste programmeraren ändå ha kunskaper om relationsdatabaser och SQL.
Relationsmodellen går ut på att data lagras i relationer. En relation är samma sak som en tabell, 1 med rader och namngivna kolumner, även om den sortens tabeller som används här har en del speciella egenskaper.
I praktiska sammanhang, som när man skriver SQL-frågor, talar man oftast om just rader och kolumner. Man kan också kalla en rad för post (engelska record) och en kolumn för fält (engelska field). I mer teoretiska sammanhang brukar man kalla raderna för tupler, och kolumnerna kallas attribut.
En databashanterare som använder sig av relationsmodellen för att lagra data kallas relationsdatabashanterare eller relationsdatabashanteringssystem. På engelska kallas det RDBMS eller Relational Database Management System.
Titta på den här tabellen, som heter Medlemmar, och som innehåller data om medlemmarna i en klubb:
Medlemmar | ||
---|---|---|
Medlemsnummer | Namn | Telefonnummer |
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta1 | 74590 |
1 | Olle | 260088 |
Varje tupel (rad) innehåller data om en medlem i klubben. Varje attribut (kolumn) anger en viss egenskap som medlemmarna kan ha.
Tuplerna (raderna) i en relation utgör en mängd. Det betyder att tuplerna (raderna) inte har någon speciell ordning, utan de kan skrivas i vilken ordning som helst. Det kan inte heller förekomma några dubbletter, dvs flera tupler med samma värde på alla attributen (kolumnerna).
Ibland definierar man en eller flera nycklar. En nyckel är ett attribut, eller en kombination av attribut, vars värden är unika. Det betyder att det inte kan finnas flera tupler med samma värde på 85 alla attribut som ingår i nyckeln. Nyckeln kan därför användas för att ange eller "peka ut" en viss tupel (rad), så att man inte blandar ihop den med de andra tuplerna. I tabellen Medlemmar ovan är attributet Medlemsnummer en nyckel, om vi antar att medlemsnummer är unika. Attributen Namn och Telefonnummer kan vi inte använda som nycklar, eftersom vi vet att namn på personer inte brukar vara unika, och eftersom flera medlemmar kan bo på samma ställe och ha samma telefonnummer.
Relationen innehåller bara enkla och atomära värden. Att värdena är enkla betyder att man inte kan ha mer än ett enda värde per ruta. Om en person, i relationen Medlemmar ovan, har två olika telefonnummer, kan vi alltså inte skriva in båda telefonnumren på samma rad. Vi måste använda två olika rader för att lagra dem. Att värdena är atomära betyder att vi (normalt) bara arbetar med hela telefonnumret, och inte tittar på delar av det. En relation som bara kan innehålla enkla och atomära värden sägs uppfylla den så kallade första normalformen. 2
Det finns också något som kallas null-värden, eller bara null. Det är egentligen inte ett värde, utan bristen på värde. Rutan är tom. Om en person inte har någon telefon, eller om vi inte vet telefonnumret, kan vi skriva null i det attributet. Null är inte samma sak som noll. Om till exempel en temperaturangivelse satts till null betyder det att det inte finns någon temperatur, och det är ju inte samma sak som en temperatur på noll grader.
Därför talar vi om relationens schema (även kallat intension) och relationens innehåll (även kallat instans eller extension). I schemat ingår bland annat vilka attribut som relationen har, deras domäner (dvs vilka värden de kan innehålla), och vilka nycklar som finns.
Vad ska tabellerna heta? Man kan göra på olika sätt, men de flesta brukar skriva tabellnamn i plural, dvs Medlemmar i stället för Medlem. Man tänker sig att man beskriver samlingen av medlemmar, inte datatypen "medlem". I ER-diagram brukar man däremot använda singular som namn på entitetstyperna, för där är det typen Medlem man menar.
I den här boken har vi av pedagogiska skäl gett svenska namn till tabeller och annat, men på riktigt kanske man bör använda engelska namn, som Members. Dels är världen internationell, och nästa person som ska arbeta med databasschemat kanske inte kan svenska. Dels lär erfarenheten oss att även om det ska fungera med svenska tecken (ÅÄÖ) i olika namn på datorer, så är det tyvärr inte alltid det verkligen gör det. Plötsligt får man något konstigt fel som efter flera timmars letande visar sig bero på att man hade ett svenskt tecken i ett filnamn. Samma sak gäller mellanslag och andra skiljetecken. Därför kan det vara en god idé att undvika andra tecken än bokstäverna A-Z, siffror och kanske understreck (_), åtminstone i namn som slutanvändarna inte ser. Namn som visas för slutanvändare ska vara så begripliga som möjligt, och bör vara på slutanvändarens språk.
Slutligen, ska tabellen heta Members eller members? De flesta databashanterare skiljer inte på stora och små bokstäver, och där spelar det inte så stor roll, men en del (som MySQL, åtminstone på vissa plattformar) gör det. Här i boken har vi för det mesta valt att använda versal begynnelsebokstav (Members), mest för att namnen ska synas lite tydligare i löpande text.
Här ovan sa vi att en nyckel är ett attribut, eller en kombination av attribut, vars värden garanterat är unika. Det är inte riktigt sant. Med den definitionen på nyckel skulle till exempel attributkombinationen Medlemsnummer + Namn vara en nyckel. Om Medlemsnummer är unikt, så blir det ju inte mindre unikt om man stoppar dit Namn också! Men det verkar ju ganska dumt. Vi har också slarvat lite med terminologin, och det ska vi åtgärda nu.
Vi börjar med att kalla ett attribut, eller en kombination av attribut, vars värden garanterat är unika, för en supernyckel. En supernyckel 87 kan innehålla "onödigt" många attribut. I relationen Medlemmar finns fyra supernycklar:
En kandidatnyckel är en minimal supernyckel, dvs en supernyckel där man inte kan ta bort några attribut om den fortfarande ska vara garanterat unik. I relationen Medlemmar finns bara en kandidatnyckel, nämligen attributet Medlemsnummer. (Men en kandidatnyckel kan vara sammansatt av flera attribut, om alla behövs för att den ska bli unik.)
Det finns alltid minst en supernyckel och minst en kandidatnyckel i varje relation. Samtliga attribut tillsammans utgör alltid en supernyckel, för vi har ju sagt tidigare att det inte kan finnas två rader med samma värde på alla attribut. Alltså är kombinationen av alla attributen garanterat unik, och därför en supernyckel. (Att bevisa att det finns minst en kandidatnyckel lämnas som övning till den intresserade läsaren. 3 ) Kandidatnycklarna heter kandidatnycklar eftersom det är bland dessa kandidater som vi väljer en primärnyckel. Vi väljer alltid en primärnyckel i varje relation, och det är primärnyckeln som oftast används för att identifiera tupler i tabellen.
Låt oss utvidga vår exempeldatabas, som än så länge bara innehåller relationen Medlemmar, med ytterligare två relationer. Först skapar vi relationen Sektioner, som innehåller data om olika sektioner i klubben (som nu visar sig vara en sportklubb):
Sektioner | ||
---|---|---|
Sektionskod | Namn | Ledare |
A | Bowling | 4 |
C | Konstsim | 2 |
B | Kickboxing | 4 |
Attributet Ledare är ett referensattribut, även kallat främmande nyckel (på engelska foreign key), till relationen Medlemmar. Ett referensattribut refererar alltid till primärnyckeln 4 i en annan (eller samma!) relation. Vi ser till exempel att ledaren för bowlingsektionen är medlem nummer 4. Alltså går vi till relationen Medlemmar, letar rätt på medlem nummer 4, och ser att det är Lotta.
Om det står att medlem nummer 4 leder en sektion, så måste det också finnas en medlem nummer 4 i medlemstabellen. Detta villkor kallas referensintegritet.
Sedan relationen Deltar, som anger vilka medlemmar som deltar i vilka sektioner:
Deltar | |
---|---|
Medlemmar | Sektioner |
1 | A |
1 | B |
1 | C |
2 | C |
3 | A |
Det här är ett vanligt sätt att rita upp tabellerna och kopplingarna mellan dem:
Ett fel som nybörjare på databasdesign ibland gör är att skapa en design som kräver att nya tabeller skapas hela tiden när databasen är i drift. Till exempel kanske man vill bygga ett diskussionsforum där användare och de texter de skriver ska lagras i en databas, och så gör man en lösning där varje användare får en egen tabell för sina texter.
En grundregel vid design av databaser är att man ska skilja på schema och data. Schemat för en relationsdatabas är vilka tabeller som finns, och vilka kolumner de har. Data är de rader och värden man stoppar in i tabellerna. Schemat gör man en gång, och ändrar bara om det sen visar sig att man gjort fel eller att förutsättningarna ändras. 90 Att ändra i schemat varje gång man lägger till en ny användare, eller vad det nu är, är fel.
Ibland motiverar man uppdelningen i flera tabeller med att det blir för långsamt med alla data i en enda tabell. Det kan stämma, om man har miljarder rader och stränga tidskrav, men det existerar knappast någon relationsdatabashanterare som inte klarar att söka i tabeller med åtminstone många miljoner rader utan att det tar någon märkbar tid alls. 5
Dessutom är alla relationsdatabashanterare byggda med tanke på att de ska klara att arbeta snabbt med få tabeller som innehåller många rader. En lösning med många olika tabeller (som en per användare) kan mycket väl bli mycket, mycket långsammare än en som har en enda stor tabell.
Det här är ett misstag som man nog bara gör om man börjar med att skapa tabeller. Börja i stället med att rita ett schema över databasen med hjälp av en konceptuell datamodell, som ER-modellen, och översätt sen det konceptuella schemat till ett relationsschema.
Som vi nämnt ovan är det tabellerna i en relationsdatabas som kallas relationer, och inte någon sorts kopplingar mellan dem. Men varför kallas tabellerna för relationer?
Relationsmodellen bygger på matematikens relationer, som är en generalisering av funktioner. En funktion i matematiken är en avbildning från en mängd av värden (kallad definitionsmängden) till en annan mängd av värden (kallad värdemängden). Varje element i definitionsmängden motsvaras av ett enda element i värdemängden, men ett element i värdemängden kan motsvaras av flera olika element i definitionsmängden:
91I stället för att rita upp relationen med pilar kan man skriva den som en tabell:
X | Y |
---|---|
1 | 1 |
-3 | 9 |
3 | 9 |
3 | 4 |
2 | 1 |
2 | 4 |
Den kartesiska produkten av de två mängderna är alla de kombinationer som går att göra, vilket i det här exemplet, med fyra element i definitionsmängden och fyra i värdemängden, skulle bli 16 stycken. Relationen är en delmängd av den kartesiska produkten.
92Och det är precis samma sak som en relation i databasbetydelsen: en delmängd av alla de rader som man skulle kunna skapa genom att göra alla tänkbara kombinationer av värden i de olika kolumnerna. (Däremot kan vi ha fler än två kolumner, och genom att definiera nycklar begränsar vi vilka värdekombinationer som är tillåtna.)
Relationsmodellen (engelska: the relational model). En datamodell där man beskriver verkligheten genom att lagra data i tabeller.
Relationsdatabas (engelska: relational database). En databas organiserad enligt relationsmodellen, dvs med alla data lagrade i tabeller.
Relation (engelska: relation). En tabell av den typ som används i relationsmodellen. Kan också helt enkelt kallas för tabell (engelska: table).
Tupel (engelska: tuple). En rad i en tabell i relationsmodellen. Kan också helt enkelt kallas för rad (engelska: row).
Attribut (engelska: attribute). Betyder "egenskap", och används både om kolumnerna i tabellerna i relationsmodellen, och om egenskaperna hos entiteterna i ER-modellen. Kan också helt enkelt kallas för kolumn (engelska: column).
Domän (engelska: domain). Ett attributs domän är den mängd av möjliga värden som man kan välja ett attributs värden från. Ungefär samma sak som datatyp, men inte riktigt. Exempelvis kan temperaturer, angivna i Celsius, representeras med datatypen reella tal, men det går inte att ha lägre temperaturer än -273,15. Datatypen är alltså reella tal, men domänen är rella tal större än eller lika med -273,15.
Kandidatnyckel (engelska: candidate key). Något som unikt identifierar en viss sak, till exempel personnumret på en person. I en relationsdatabas menar man en kolumn, eller en kombination av flera kolumner, 6 som alltid har ett unikt värde för varje rad i tabellen. (Man får dock inte ta med några onödiga kolumner.)
93Supernyckel (engelska: superkey). En kolumn, eller en kombination av flera kolumner, som alltid har ett unikt värde för varje rad i tabellen. Till skillnad från en kandidatnyckel får man ta med onödiga kolumner.
Primärnyckel (engelska: primary key). Man väljer en av kandidatnycklarna att användas som primärnyckel, dvs den nyckel som används i första hand.
Alternativnyckel (engelska: alternate key) eller sekundärnyckel (engelska: secondary key). En kandidatnyckel som inte valts som primärnyckel.
Referensattribut (engelska: reference attribute). Ett attribut (dvs en kolumn) i en tabell som refererar till (dvs "pekar ut rader i") en annan (eller ibland samma) tabell. Det är inga "pekare" av samma typ som man har i många programmeringsspråk, utan referensen består i att det står ett värde, och sen ska det stå samma värde på en rad i den refererade tabellen. Kallas även främmande nyckel (engelska: foreign key).
Referensintegritet (engelska: referential integrity). Om två tabeller är hopkopplade med referensattribut, ska det värde som refereras till alltid existera: Om det står i tabellen Anställd att en viss anställd jobbar på avdelning nummer 17, ska det också finnas en avdelning med nummer 17 i tabellen Avdelning.
1 Det är en vanlig missuppfattning att "relationerna" i relationsdatabaser skulle vara de kopplingar som finns mellan tabellerna. Det är fel. Det är själva tabellerna som kallas relationer. Inte heller har de något att göra med ER-modellens "relationships".
2 En normalform är en regel som ställer vissa villkor på en relation. Regeln kräver att relationen är gjord på ett visst sätt, och orsaken är att man vill undvika vissa typer av "dum design". Det finns många fler normalformer än den första.
3 Svar: En kandidatnyckel är en minimal supernyckel, dvs en supernyckel där man inte kan ta bort några attribut om den fortfarande ska vara garanterat unik. Det finns alltid minst en supernyckel i varje relation. Välj vilken som helst av supernycklarna. Är den minimal? I så fall är den en kandidatnyckel. Är den inte minimal? Ta i så fall bort alla onödiga attribut, så att det inte går att ta bort fler om den fortfarande ska vara garanterat unik. De attribut som är kvar är en kandidatnyckel.
UNIQUE.
6 Om en kandidatnyckel är sammansatt av flera kolumner, kallas de ingående kolumnerna inte var för sig för kandidatnycklar. De är bara kolumner som ingår i kandidatnyckeln. Samma sak gäller för supernycklar, primärnycklar och alternativnycklar.
Du bör ha läst kapitel 2 om ER-modellering först, och helst också kapitel 5 om relationsmodellen.
Vi börjar med en kort sammanställning av vad vi ska göra. I de följande styckena går vi sen igenom dessa steg lite mer i detalj, med exempel. Det är tio steg för ER-modellen, plus ett elfte steg för att hantera den utvidgade ER-modellens arvshierarkier.
Det finns också alternativa lösningar för en del av punkterna ovan.
Jättelätt! Varje vanlig entitetstyp blir en tabell, med samma namn som entitetstypen. Vanliga attribut blir kolumner i tabellen. Exempel:
Översättning, med några inlagda exempel på hur data kan komma att se ut:
Personer | ||
---|---|---|
Nummer | Namn | Telefon |
17 | Hjalmar | 174590 |
4711 | Hulda | 019-94639 |
42 | Hjalmar | 070-7347013 |
Man brukar ge tabeller namn i plural, som Personer, till skillnad från entitetstypens singular, Person. (Se även diskussionen om tabellnamn i avsnitt 5.4.)
Man kan kalla den här sortens tabell för entitetstabell. Entitetstypens nyckel blir primärnyckel i tabellen. Om det finns flera kandidatnycklar, väljer man en av dem som primärnyckel. Välj i så fall hellre en numerisk nyckel än ett strängfält, för tänk på att andra tabeller kanske ska ha referensattribut till primärnyckeln i den här tabellen.
Egentligen ska det alltid finnas en nyckel angiven för varje vanlig entitetstyp i ER-diagrammet, men om vi glömt nyckeln (till exempel för ett samband som objektifierats) måste vi hitta på en nu. Ett enkelt löpnummer i form av ett heltal brukar fungera bra.
Det kan också vara praktiskt att skapa en enkel heltalsnyckel i de fall där det visserligen finns en nyckel, men där den nyckeln är en textsträng eller en sammansatt nyckel. Det är ofta både enklare och effektivare att låta andra tabeller referera till exempelvis land nummer 17 än till Demokratiska folkrepubliken Korea. Den här nya nyckeln ska bara användas internt i databasen, och kallas surrogatnyckel.
Det kan till och med vara ett gott råd att alltid skapa en surrogatnyckel i varje tabell, och gärna då en som automatiskt räknas upp när nya rader läggs in. Det kan förenkla arbetet med databasen, och det skyddar också mot att företaget plötsligt får för sig att ändra på den egenskap man valt att använda som nyckel. 1
98I stället för ett enkelt heltal som automatgenererad surrogatnyckel, kan det vara praktiskt att använda UUID:er. UUID står för "Universally Unique Identifier", och är 128-bitarstal som genereras automatiskt, och som (ganska säkert) är unika i hela världen. Det finns ingen hundraprocentig garanti, men sannolikheten att två sådana UUID:er blir lika är (för de allra flesta tillämpningar) försumbar. Om man använder UUID som nyckel i en tabell, är den alltså inte bara unik i just den tabellen, utan i alla tabeller i alla databaser, överallt. Man använder också termen GUID, "Globally Unique Identifier". En UUID som nyckel tar förstås lite mer plats i databasen än vanliga små heltal, men kan ge stora fördelar när man söker i eller slår samman flera databaser, eftersom raderna fortfarande har unika värden på nyckeln.
Också jättelätt! Varje 1:N-sambandstyp blir ett referensattribut i "många"-entitetstypens tabell.
Personer | ||
---|---|---|
Nummer | Namn | Telefon |
17 | Hjalmar | 174590 |
4711 | Hulda | 019-94639 |
42 | Hjalmar | 070-7347013 |
Bilar | ||
---|---|---|
Nummer | Modell | Ägare |
RFN540 | Renault Scénic | 42 |
SQL123 | Volvo 245 | 42 |
DBA456 | Mercedes A-klass | 4711 |
WCA912 | BMW Z5 | null |
BMW-bilen har ingen ägare, så det står null i kolumnen Ägare. (Den bara står där och väntar på att någon ska hitta den!) ER-diagrammet säger inget om att alla bilar måste ha en ägare. Om Bil hade varit markerat som fullständigt deltagande, alltså med ett dubbelstreck till Äger-sambandet, så kan vi lägga på ett villkor 2 not null när vi skapar tabellen.
Det finns alltså tre olika sätt att göra översättningen:
De två entitetstyperna blir förstås två tabeller. Sen ska vi översätta Har-sambandstypen, och vi börjar med ett första, rättframt, försök:
100Personer | |||
---|---|---|---|
Nummer | Namn | Telefon | Näsa |
17 | Hjalmar | 174590 | 1 |
4711 | Hulda | 019-94639 | 2 |
42 | Hjalmar | 070-7347013 | null |
Näsor | |
---|---|
Nummer | Längd |
1 | 5 cm |
2 | 14 cm |
Sambandstypen Har blir attributet Näsa i Personer-tabellen. Som vi ser har den ene Hjalmar (person 17) en ganska normal näsa, medan Hulda har en ovanligt lång näsa. Den andre Hjalmar (person nummer 42) har ingen näsa alls, den stackarn. 3
Men förmodligen (gissar vi) finns det fler personer i databasen som inte har någon näsa, än det finns lösa näsor utan personer. Därför är det kanske bättre att placera referensattributet i den andra tabellen. Dels slipper vi en del null-värden, 4 och dels inbillar vi oss att det känns naturligare att näsan refererar till den person den hör till, än tvärtom:
Personer | ||
---|---|---|
Nummer | Namn | Telefon |
17 | Hjalmar | 174590 |
4711 | Hulda | 019-94639 |
42 | Hjalmar | 070-7347013 |
Näsor | ||
---|---|---|
Nummer | Längd | SitterPå |
1 | 5 cm | 17 |
2 | 14 cm | 4711 |
Man vill helst undvika null-värden. Om den ena entitetstypen har fullständigt deltagande, dvs är ansluten med ett dubbelstreck till sambandstypen, bör man placera referensattributet på den sidan. I annat fall bör man välja den entitetstyp som har störst ("mest fullständigt") deltagande, i det här fallet alltså Näsa.
Ett tredje sätt är att slå ihop tabellerna. I så fall bör (åtminstone nästan) alla personer ha näsor, annars blir det en massa nullvärden. Dessutom måste alla näsor ha personer, för man kan inte ha null-värden i nyckeln.
Personer | ||||
---|---|---|---|---|
Nummer | Namn | Telefon | Näsnummer | Näslängd |
17 | Hjalmar | 174590 | 1 | 5 cm |
4711 | Hulda | 019-94639 | 2 | 14 cm |
42 | Hjalmar | 070-7347013 | null | null |
Varje N:M-sambandstyp blir en egen tabell.
Personer | ||
---|---|---|
Nummer | Namn | Telefon |
17 | Hjalmar | 174590 |
4711 | Hulda | 019-94639 |
42 | Hjalmar | 070-7347013 |
Hus | |
---|---|
Nummer | Färg |
1 | Rött |
2 | Rött |
3 | Rött |
4 | Hemsk |
Äger | |
---|---|
Person | Hus |
17 | 1 |
17 | 2 |
17 | 3 |
4711 | 3 |
Tabellen Äger har en primärnyckel som består av både Person och Hus. Dessutom är Person referensattribut till tabellen Personer, och Hus är referensattribut till tabellen Hus.
Person 42 äger inget hus, och hus 4 har ingen ägare. Hus 3 ägs gemensamt av två personer.
102Tekniken med en mellantabell kan användas också när man översätter 1:1-samband och 1:N-samband. Det kan vara bra till exempel om deltagandet är väldigt lågt på båda sidor om sambandstypen, så att det skulle blir många null-värden med de vanliga lösningarna. För 1:1- och 1:N-samband blir primärnyckeln i mellantabellen inte sammansatt av primärnycklarna från båda de hopkopplade tabellerna.
Trevägssamband, och samband av ännu högre grad, är det enklast att göra en mellantabell av. För ett många-till-många-till-mångasamband så måste man ha mellantabellen, men ibland, till exempel för 1:1:1-samband, kan man klara sig utan en extra tabell. (Precis när man kan och inte kan klara sig utan tabell lämnas som övning åt läsaren.)
Personer | ||
---|---|---|
Nummer | Namn | Telefon |
17 | Hjalmar | 174590 |
4711 | Hulda | 019-94639 |
42 | Hjalmar | 070-7347013 |
Sett | ||
---|---|---|
Person | Film | Biograf |
17 | 1 | 1 |
17 | 1 | 2 |
17 | 2 | 1 |
42 | 2 | 2 |
Biografer | |
---|---|
Nummer | Namn |
1 | Rigoletto |
2 | Filmstaden |
3 | Röda kvarn |
Filmer | ||
---|---|---|
Nummer | Namn | Kategori |
1 | Spindelmannen | Superhjältar |
2 | Titanic | Romantik |
3 | Exit wounds | Åtgärdsfilm |
Attribut på sambandstyper blir kolumner. För 1:1- och 1:N-sambandstyper i samma tabell som den med referensattributet, och för N:M-sambandstyper i den särskilda sambandstabellen.
Samma exempel på 1:N-samband som förut, men med ett attribut på sambandet:
Personer | ||
---|---|---|
Nummer | Namn | Telefon |
17 | Hjalmar | 174590 |
4711 | Hulda | 019-94639 |
42 | Hjalmar | 070-7347013 |
Bilar | |||
---|---|---|---|
Nummer | Modell | Ägare | Inköpsår |
RFN540 | Renault Scénic | 42 | 2000 |
SQL123 | Volvo 245 | 42 | 1981 |
DBA456 | Mercedes A-klass | 4711 | 2002 |
WCA912 | BMW Z5 | null | null |
Samma exempel på N:M-samband som förut, men med ett attribut på sambandet:
Vi översätter precis som vi gjorde förut, men lägger till kolumnen Andel i tabellen Äger:
Personer | ||
---|---|---|
Nummer | Namn | Telefon |
17 | Hjalmar | 174590 |
4711 | Hulda | 019-94639 |
42 | Hjalmar | 070-7347013 |
Hus | |
---|---|
Nummer | Färg |
1 | Rött |
2 | Rött |
3 | Rött |
4 | Hemsk |
Äger | ||
---|---|---|
Person | Hus | Andel |
17 | 1 | 100% |
17 | 2 | 100% |
17 | 3 | 30% |
4711 | 3 | 70% |
Varje svag entitetstyp blir en tabell. Primärnyckeln består av den svaga entitetstypens partiella nyckel, kombinerad med den identifierande entitetstypens primärnyckel.
Lägenheter | |
---|---|
Nummer | Hyra |
1 | 4500 |
2 | 2500 |
Rum | ||
---|---|---|
Lägenhet | Namn | Golvyta |
1 | kök | 14 |
1 | badrum | 5 |
1 | sovrum | 20 |
1 | vardagsrum | 20 |
2 | kök | 8 |
2 | badrum | 3 |
2 | sovrum | 12 |
Tabellen Rum har en primärnyckel som består av både Lägenhet och Namn. I tabellen Rum är Lägenhet referensattribut till Nummer i tabellen Lägenheter, och anger vilken lägenhet rummet finns i.
Sammansatta attribut behandlas som om de bara bestod av delarna.
106Personer | |||
---|---|---|---|
Nummer | Namn | Riktnummer | Lokalnummer |
17 | Hjalmar | null | 174590 |
4711 | Hulda | 019 | 94639 |
42 | Hjalmar | 070 | 7347013 |
Varje flervärt attribut blir en egen tabell, som vi kan kalla attributtabell. Primärnyckeln består av entitetstypens primärnyckel, kombinerad med det flervärda attributet.
Personer | |
---|---|
Nummer | Namn |
17 | Hjalmar |
4711 | Hulda |
42 | Hjalmar |
Telefon | |
---|---|
Person | Telefon |
17 | 174590 |
17 | 260088 |
42 | 070-7347013 |
42 | 174590 |
4711 | 019-94639 |
Eftersom attributtabellens nyckel bildas av entitetstypens primärnyckel och attributet, kan samma telefonnummer inte förekomma flera gånger för en och samma person. Om samma värde ska kunna förekomma flera gånger i ett och samma flervärda attribut för en och samma entitetsinstans, måste man använda en annan lösning.
Härledda attribut ska inte lagras i databasen. De kommer ofta att finnas med i relationsdatabasen i form av vyer, men tas inte med i själva tabelldesignen.
Behövs det verkligen ett exempel på hur man inte tar med ett attribut? Men okej då:
Personer | ||
---|---|---|
Nummer | Namn | BorI |
17 | Hjalmar | 1 |
4711 | Hulda | 1 |
42 | Hjalmar | 3 |
Hus |
---|
Nummer |
1 |
2 |
3 |
Notera att vi snikade in ett exempel på ett N:1-samband här. Förut har vi bara sett 1:N-samband, men ett N:1-samband hanteras precis likadant, fast förstås spegelvänt. Dessutom ser vi att man kan ha en tabell som bara består av en enda kolumn. Det är inget konstigt med det.
Arv i EER-modellen kan översättas till tabeller på flera olika sätt, men det som brukar vara bäst är att varje subklass får en egen tabell, 108 som har samma primärnyckel som superklassen, plus subklassens extra kolumner. En entitetsinstans har en rad i alla tabellerna för de entitetstyper som den instansen tillhör.
Ett exempel med personer, som (förutom att de är personer) också kan vara studenter och lärare:
Varje entitetstyp blir en tabell:
Personer | |
---|---|
Nummer | Namn |
17 | Hjalmar |
4711 | Hulda |
42 | Hjalmar |
4712 | Bengt |
Studenter | |
---|---|
Nummer | Medelbetyg |
17 | 3.9 |
4711 | 3.2 |
Lärare |
---|
Nummer |
4711 |
4712 |
Den förste Hjalmar (person 17) är student. Nykomlingen Bengt är lärare. Hulda är både student och lärare. Den andre Hjalmar (person 42) är varken lärare eller student, utan bara en person.
För att få fram all information om en viss entitetsinstans, till exempel Hulda, måste man alltså titta i flera olika tabeller. Det kan ge prestandaproblem, och det är nackdelen med det här sättet att översätta arv.
Vi använde den förenklade notationen för arv i EER-diagram, där det inte är specificerat ifall mängderna av studenter och lärare kan överlappa, och ifall alla personer måste tillhöra minst en av de mängderna. Om vi studerar tabellerna vi skapade ser vi att det går bra att lägga in personer som inte tillhör någon av underentitetstyperna, och det går också bra att lägga in personer som samtidigt är både studenter och lärare. Underentitetstyperna är alltså överlappande, och vi har inte fullständig specialisering. Vill man kräva disjunkta underentitetstyper eller fullständig specialisering, får man lösa det genom att ange integritetsvillkor i SQL.
Sambandstyper ärvs också. Anta att personer äger bilar, och lärare ger kurser:
110När vi översätter till tabeller blir sambandet Äger ett referensattribut från tabellen Bilar till tabellen Personer. Alla personer kan äga bilar, och alla personer är med i den tabellen. Sambandet Ger blir ett referensattribut från tabellen Kurser till tabellen Lärare. Det är bara lärare som kan ge kurser, och det är bara lärare som är med i den tabellen.
Personer | |
---|---|
Nummer | Namn |
17 | Hjalmar |
4711 | Hulda |
42 | Hjalmar |
4712 | Bengt |
Studenter | |
---|---|
Nummer | Medelbetyg |
17 | 3.9 |
4711 | 3.2 |
Lärare |
---|
Nummer |
4711 |
4712 |
Bilar | ||
---|---|---|
Nummer | Märke | Ägare |
RFN540 | Renault | 17 |
JHR109 | Citroen | null |
PXT158 | Citroen | 42 |
Kurser | ||
---|---|---|
Kod | Namn | Lärare |
A | Databaser | 4711 |
B | Databaser 2 | 4711 |
D | Historia A | 4712 |
Det finns svar till övningarna på bokens webbplats, men försök lösa uppgifterna själv innan du tittar i facit. Om du inte ritat, eller har kvar, de ER-diagram som övningarna baseras på, finns även de på bokens webbplats.
1 En av författarna valde en gång att använda e-postadress som nyckel i en tabell med studenter, för e-postadresser är ju unika och ändras aldrig. Det fungerade bra, tills universitetets centrala administration i ett plötsligt anfall av verksamhetslust fick för sig att ändra en del studenters e-postadresser.
3 Eller så vet vi bara inte vilken näsa som är Hjalmars. Det kan också vara så att Hjalmar är en fisk, och fiskar har inga näsor.
4 Man säger ofta "null-värden", men tänk på att null egentligen inte är ett värde, utan det betyder att värdet saknas. Rutan i tabellen är tom.
Det här kapitlet är en handledning till grunderna om hur man använder frågespråket SQL. Ett frågespråk är ett språk som man använder för att ställa frågor till en databashanterare, dvs göra sökningar i en databas. Mer avancerad användning av SQL, till exempel operationen yttre join, mer komplicerat arbete med aggregatfunktioner och rekursiva frågor, tas upp i nästa kapitel. Det kan vara bra att läsa avsnittet om relationsmodellen innan man läser det här kapitlet.
SQL är det helt dominerande frågespråket i dag. Om du någon gång ska arbeta med en databashanterare på ett mer avancerat sätt än att bara fylla i formulär, är chansen därför stor att du kommer att använda SQL. SQL används både för att kommunicera med databasen från ett tillämpningsprogram, och för att mata in så kallade ad hoc-frågor, dvs sökningar i databasen som man kommer på, formulerar och kör direkt.
Med början 1986 har man arbetat på att standardisera SQL. Den första standarden som blev mer allmänt accepterad antogs 1992, och kallades SQL-92 eller SQL2. En kraftigt utökad standard antogs 1999, och kallades SQL:1999 eller SQL3. Även om det kommit flera senare standarder, är både SQL-92 och SQL:1999 fortfarande en sorts riktmärken, och man talar ibland fortfarande om hur väl olika databashanterare uppfyller SQL-92.
Inte minst beror det på att det finns en officiell valideringstest för att uppfylla SQL-92-standarden 1 men inte för senare SQL-versioner. Mimer tillhandahåller dock en allmänt accepterad validerare även för SQL:1999. 2
Den SQL-standard som gäller när detta skrivs fastställdes 2016 och kallas ISO/IEC 9075:2016, eller SQL:2016. Den är uppdelad i nio olika delar, och som exempel är det fullständiga namnet på den första delen ISO/IEC 9075-1:2016 Information technology – Database languages – SQL – Part 1: Framework (SQL/Framework). Bara denna inledande del, på 78 sidor, kostar över 1 500 kronor att köpa. (Standardiseringsarbetet finansieras delvis genom försäljning av standarddokumenten.) Sammanlagt består SQL:2016 av flera tusen sidor.
Tyvärr följer de flesta av de existerande databashanterarna inte SQL-standarden särskilt exakt, utan de har sina egna dialekter. Grunderna, till exempel de jämförelsevis enkla sökningar som tas upp i det här kapitlet, är för det mesta lika och i överensstämmelse med vad standarden säger, men en så enkel sak som att ange hur många rader från svaret som ska tas med görs fortfarande på helt olika sätt i olika databashanterare. När det gäller mer avancerade saker, som triggers och lagrade procedurer, kan skillnaderna mellan olika SQL-dialekter vara stora. Det är nämligen inte så att man har hittat på en standard, och sen har ett antal tillverkare byggt databashanterare som följer den standarden. I stället har de stora databashanterarna, 115 till exempel Oracle och Db2, funnits mycket längre än standarden, och SQL-standarden får nog närmast ses som en kompromiss mellan de olika stora tillverkarna av databashanterare, där var och en helst ville få just sin dialekt och just sina finesser upphöjda till standard. Det gör att det kan vara mycket arbete att byta från en databashanterare till en annan.
Vi antar att du nu sitter framför en dator, att det finns en databashanterare i gång, med en databas, och att du kan ge SQL-kommandon till databashanteraren. Hur man gör för att åstadkomma denna situation, och hur man gör för att skriva in och köra SQL-kommandona, beror på vilken typ av dator och vilken databashanterare det handlar om. Därför tar vi inte upp det här. 3
Vi antar också att du arbetar med en databas som beskriver en liten idrottsklubb. Databasen består av tre tabeller, som kommer att beskrivas nedan. 4
Ge följande kommando till databashanteraren:
select * from Medlemmar;
Det här kommandot, även kallat en SQL-fråga eller select
-fråga, betyder ungefär "välj ut alla kolumnerna ur tabellen Medlemmar". Semikolonet på slutet ingår egentligen inte i kommandot, men behövs ibland för att markera slutet på frågan eller för att skilja olika SQL-kommandon åt.
116
När du kör frågan kommer du som svar att få en tabell som liknar den här:
Mnr | Namn | Telefon |
---|---|---|
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta | 174590 |
1 | Olle | 260088 |
select
-frågor ger en tabell som svar. Även om svaret (som vi kommer att se exempel på senare) bara består av ett enda värde, får man det i form av en tabell. Frågorna utgår från en eller flera tabeller (i det här fallet tabellen Medlemmar), och sammanställer eller väljer ut information som alltså resulterar i en ny tabell.
Exakt hur man gör för att "köra frågan" beror på vilken databashanterare man använder. På vissa system finns kanske ett särskilt fönster där man kan skriva in och redigera frågan, varefter man kör den genom att klicka med musen på en knapp som det står Kör på. I andra system skriver man in frågan, avslutar den med ett semikolon (;), och sen körs frågan när man trycker på returtangenten. Det är också vanligt med SQL-frågor inbakade i ett program som skrivits i ett vanligt programmeringsspråk som C eller Java, men det tar vi upp i ett senare kapitel.
Det här är alltså innehållet i tabellen Medlemmar i databasen. Den tabellen innehåller data om klubbens medlemmar. Varje rad beskriver en medlem. Som vi ser har tabellen tre kolumner, nämligen Mnr (medlemsnummer), Namn (medlemmens namn) och Telefon (medlemmens telefonnummer).
Notera att raderna i svaret inte kommer i någon särskild ordning.
117Alla värden i samma kolumn har samma typ, till exempel heltal eller textsträng med en viss längd, och mängden av alla värden som kan finnas i kolumnen brukar vi kalla domän. Det motsvarar ungefär begreppet datatyp i programmeringsspråk.
Med hjälp av SELECT kan vi mer detaljerat beskriva vad vi vill ha för svar. Om vi till exempel bara vill ha reda på namnen på klubbens medlemmar, alltså kolumnen Namn i tabellen Medlemmar, skriver vi:
select Namn from Medlemmar;
Namn |
---|
Stina |
Sam |
Lotta |
Olle |
select Namn, Telefon from Medlemmar;
Namn | Telefon |
---|---|
Stina | 282677 |
Sam | 260088 |
Lotta | 174590 |
Olle | 260088 |
"select * from Medlemmar",
skulle också kunna uttryckas så här:
118
select Mnr, Namn, Telefon
from Medlemmar;
Vi har nu sett hur man kan använda select för att välja ut dem av tabellens kolumner som man är intresserad av. Men vi kan också tala om vilka rader vi vill ha med i svaret. Om vi vill ha information bara för den medlem som heter Lotta, skriver vi:
select * from Medlemmar
where Namn = 'Lotta';
Mnr | Namn | Telefon |
---|---|---|
4 | Lotta | 174590 |
Med ordet where kan vi alltså ange ett sökvillkor som talar om vilka rader vi är intresserade av.
Vi skulle kunna omformulera frågan så här:
select * from Medlemmar
where Medlemmar.Namn = 'Lotta';
Mnr | Namn | Telefon |
---|---|---|
4 | Lotta | 174590 |
Vi kan också göra strängmatchning med jokertecken (eller wildcards som det heter på engelska):
select Namn, Telefon from Medlemmar
where Namn like 'S%';
119
Namn | Telefon |
---|---|
Stina | 282677 |
Sam | 260088 |
Om man använder ordet like i jämförelsen, innebär det strängmatchning med jokertecken. Understreck ("_") betyder ett tecken, vilket som helst, medan procenttecken ("%") betyder en följd av noll, ett eller flera av vilka tecken som helst. 5 Om man i stället använder likhetstecken ("=") som vanligt, får man en vanlig, exakt jämförelse i stället för mönstermatchning. Följande fråga söker efter medlemmar med det säkert ovanliga namnet S%:
select Namn, Telefon from Medlemmar
where Namn = 'S%';
Namn | Telefon |
select * from Medlemmar
where Namn like 'S%' and Mnr <= 2
or Telefon = '174590';
Mnr | Namn | Telefon |
---|---|---|
2 | Stina | 282677 |
4 | Lotta | 174590 |
Således har and högre prioritet (är klibbigare) än or. Uttrycken kan grupperas med parenteser:
select * from Medlemmar
where Namn like 'S%' and (Mnr <= 2
or Telefon = '174590');
Mnr | Namn | Telefon |
---|---|---|
2 | Stina | 282677 |
SQL använder enkla citationstecken, som i 'Lotta', för att avgränsa strängar, inte dubbla som i "Lotta". I en del databashanterare fungerar det med dubbla citationstecken också, men dubbla citationstecken ska egentligen användas för kolumn- och tabellnamn, om de namnen är reserverade ord eller innehåller otillåtna tecken:
select "select", "Glö glö glö"
from "Åiåaäeö";
Normalt skiljer SQL inte på stora och små bokstäver i tabell- och kolumnnamn, men när man sätter dem inom citationstecken är stora och små bokstäver olika. Många databashanterare översätter internt alla namn till stora bokstäver, och då måste man skriva med stora bokstäver innanför citationstecknen.
En del SQL-dialekter skiljer sig från standarden. Exempelvis använder MySQL bakåtvända enkla citationstecken, som i 'Åiåaäeö
', och Microsoft SQL Server använder hakklamrar, som i [Åiåaäeö]
.
Om man skriver fel i sina SQL-frågor, får man oftast ett felmeddelande. Felmeddelanden innehåller ofta nyttig information om vad som var fel, så läs dem. 6 Olika databashanterare har olika felmeddelanden, och hur felmeddelandena skrivs ut beror också på vilket verktyg man använder för att ställa SQL-frågorna, men här ger vi ett par exempel. Den här (felaktiga) SQL-frågan:
select * from Medlemmar
where Medlemsnamn = 'Lotta';
ger till exempel det här felmeddelandet 7 i databashanteraren MySQL:
ERROR 1054 (42S22): Unknown column 'Medlemsnamn'
in 'where clause'
och det här felmeddelandet i databashanteraren Mimer:
121
2: where Medlemsnamn = 'Lotta'
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x^
Mimer SQL error -12202 in function PREPARE Medlemsnamn is not a column of an inserted table, updated table or any table identified in a FROM clause
select * from Sektioner;
Skod | Namn | Ledare |
---|---|---|
A | Bowling | 4 |
C | Konstsim | 2 |
B | Kickboxing | 4 |
Varje rad i tabellen Sektioner innehåller data om en av klubbens sektioner. Tabellen har tre kolumner, nämligen Skod (sektionskod), Namn (sektionens namn) och Ledare (vem som är ledare för sektionen).
Kolumnen Ledare anger medlemsnumret på den medlem som är ledare för sektionen. Vi ser till exempel att medlem nummer 4 leder bowlingsektionen, och vi kan sen gå till medlemstabellen för att ta reda på vem medlem nummer 4 är. Som vi kanske kommer ihåg såg medlemstabellen ut så här:
Mnr | Namn | Telefon |
---|---|---|
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta | 174590 |
1 | Olle | 260088 |
Vi ser alltså att det är Lotta som leder bowlingsektionen.
122Tabellerna i exemplet är så små att vi direkt ser att ledaren för bowlingsektionen är Lotta. Men om det är stora tabeller vill man förstås använda SQL-frågor för att få fram den informationen. Vi ska nu försöka oss på att göra detta.
Vi börjar med det mest uppenbara (men inte särskilt bra) sättet. Först tar vi fram numret på bowlingsektionens ledare:
select Ledare from Sektioner
where Namn = 'Bowling';
Ledare |
---|
4 |
Och sen söker vi helt enkelt i medlemstabellen efter vem nummer4 är:
select Namn from Medlemmar
where Mnr = 4;
Namn |
---|
Lotta |
Det är dock onödigt att ställa två separata frågor. I andra fall kan det dessutom bli väldigt arbetsamt, till exempel om man har stora mellanresultat som måste kopieras till nästa fråga.
I den andra frågan,"select Namn from Medlemmar where Mnr = 4",
var ju 4:an resultatet av den första frågan, "select Ledare from Sektioner where Namn = 'Bowling'".
Därför stoppar vi in denna första fråga i den andra frågan, i stället för 4:an!
select Namn from Medlemmar
where Mnr = (select Ledare from Sektioner where Namn = 'Bowling');
Namn |
---|
Lotta |
Notera att vi behövde skriva parenteser runt den andra, inre frågan. Den kallas underfråga, inre fråga, sub-fråga eller sub-select.
Egentligen borde vi inte använda likhetstecken ("=") vid jämförelsen mellan Mnr och resultatet av den inre frågan, utan nyckelordet in. Det skulle kunna finnas flera sektioner som heter "Bowling", och därigenom flera ledare, och om den inre frågan ger flera rader som svar fungerar inte jämförelse med likhetstecken. Men mer om detta senare.
Nu ska vi göra samma sak utan någon inre fråga i where-villkoret. Prova först att skriva båda tabellerna Sektioner och Medlemmar efter from i select-frågan:
select * from Sektioner, Medlemmar;
Skod | Namn | Ledare | Mnr | Namn | Telefon |
---|---|---|---|---|---|
A | Bowling | 4 | 2 | Stina | 282677 |
C | Konstsim | 2 | 2 | Stina | 282677 |
B | Kickboxing | 4 | 2 | Stina | 282677 |
A | Bowling | 4 | 3 | Sam | 260088 |
C | Konstsim | 2 | 3 | Sam | 260088 |
B | Kickboxing | 4 | 3 | Sam | 260088 |
A | Bowling | 4 | 4 | Lotta | 174590 |
C | Konstsim | 2 | 4 | Lotta | 174590 |
B | Kickboxing | 4 | 4 | Lotta | 174590 |
A | Bowling | 4 | 1 | Olle | 260088 |
C | Konstsim | 2 | 1 | Olle | 260088 |
B | Kickboxing | 4 | 1 | Olle | 260088 |
Resultatet blir den så kallade kartesiska produkten av de två tabellerna, dvs alla kombinationer av rader. Det var ju inte det vi ville ha, så vi ändrar frågan så att den i stället väljer ut bara de rader där ledarnumret Ledare är samma som medlemsnumret Mnr:
select * from Sektioner, Medlemmar
where Ledare = Mnr;
124
Skod | Namn | Ledare | Mnr | Namn | Telefon |
---|---|---|---|---|---|
C | Konstsim | 2 | 2 | Stina | 282677 |
A | Bowling | 4 | 4 | Lotta | 174590 |
B | Kickboxing | 4 | 4 | Lotta | 174590 |
Vi har plötsligt fått en fin liten tabell där varje rad innehåller information om en sektion och dess ledare. Om vi vill, kan vi se det som att vi slagit ihop varje rad i tabellen Sektioner med den rad som den hör ihop med i tabellen Medlemmar. Lottas rad kom med två gånger, men det är inget konstigt med det, för hon är ledare för två sektioner.
select * from Sektioner, Medlemmar
where Ledare = Mnr
and Sektioner.Namn = 'Bowling';
Skod | Namn | Ledare | Mnr | Namn | Telefon |
---|---|---|---|---|---|
A | Bowling | 4 | 4 | Lotta | 174590 |
Och så gör vi en sista ändring, för att bara få med namnet på ledaren:
select Medlemmar.Namn from Sektioner, Medlemmar
where Ledare = Mnr
and Sektioner.Namn = 'Bowling';
Namn |
---|
Lotta |
from
raden
8
och hopkopplade i where
-villkoret, eller med en inre fråga
125
som vi såg i förra avsnittet. Man talar om flata respektive nästlade frågor.
Internt i databashanteraren kommer båda frågorna att hanteras på samma sätt, så det finns inga prestandaskäl till att välja det ena eller andra sättet. 9 En komplicerad sökning med många inre frågor kan bli svårhanterlig. Välj det som är lättast att formulera och lättast att förstå, vilket oftast är flata frågor.
Det här är grunden för hur man skriver enkla SQL-frågor:
select A, B, C
from T1, T2, T3
where VILLKOR
En SQL-fråga kan inte köras steg för steg som ett C- eller Javaprogram, utan den måste först översättas av databashanteraren till en så kallad exekveringsplan. Men om man känner sig mer hemma med steg-för-steg-språk, skulle man kunna skriva om SQL-frågan ovan som ett litet program med nästlade loopar, dvs loopar inuti varandra:
for each t1 in T1 do:
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfor each t2 in T2 do:
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfor each t3 in T3 do:
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif VILLKOR then:
print A, B, C
126
I ovanstående pseudokod 10 betyder for each t1 in T1 do att vi loopar igenom alla raderna i tabellen T1, och använder t1 som loopvariabel. if-satsen längst in kommer alltså att köras en gång för varje tänkbar kombination av raderna i de tre tabellerna. Om tabellerna innehåller tusen rader var, kommer if-satsen alltså att köras en miljard gånger. (Databashanteraren kommer dock för det mesta att verkligen köra frågan på ett helt annat och snabbare sätt, men svaret blir detsamma. Ordningen på raderna i resultatet kan visserligen bli en annan, men det gör inget, för ordningen spelar ju ingen roll: resultatet av en fråga anses vara detsamma oberoende av radernas ordning.)
Nu ska vi prova på ännu en fråga som kräver att man kombinerar flera tabeller. Först tittar vi på den nya tabellen Deltar, som innehåller data om vilka medlemmar som deltar i de olika sektionernas verksamhet:
select * from Deltar;
Medlem | Sektion |
---|---|
1 | A |
1 | B |
1 | C |
2 | C |
3 | A |
Av den första raden i tabellen ser vi att medlem nummer 1 deltar i sektionen med sektionskoden A. På nästa rad står det att (samma) medlem nummer 1 även deltar i sektionen med sektionskoden B. Och så vidare. Den som kommer ihåg datamodellering med ER-modellen känner igen detta som ett många-till-många-samband mellan medlemmar och sektioner: en medlem kan delta i flera sektioner, och en sektion kan ha flera deltagande medlemmar.
127Först gör vi på det dumma sättet, och börjar med att ta reda på Olles medlemsnummer:
select Mnr
from Medlemmar
where Namn = 'Olle';
Mnr |
---|
1 |
Sen tittar vi i tabellen Deltar för att få reda på vilka av sektionerna som Olle deltar i.
select Sektion from Deltar
where Medlem = 1;
Sektion |
---|
A |
B |
C |
Ok, så vad är 'A', 'B' och 'C' för nåt?
select Namn from Sektioner
where Skod = 'A'
or Skod = 'B'
or Skod = 'C';
Namn |
---|
Bowling |
Konstsim |
Kickboxing |
in:
select Namn from Sektioner
where Skod in ('A', 'B', 'C');
Namn |
---|
Bowling |
Konstsim |
Kickboxing |
('A', 'B', 'C')":
128
select Namn from Sektioner
where Skod = (select Sektion
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Deltar
where Medlem = 1);
Vi får ett felmeddelande. Så här ser det ut i databashanteraren Mimer:
Mimer SQL error -10107 in function DYNAMIC FETCH
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xThe result of a subquery or select into is more
than one row
Vi måste använda "in"
i stället för "=". Den vanliga lika-med-jämförelsen med "=" kan nämligen bara jämföra med ett värde, och här är det ju tre (eftersom Olle deltar i tre sektioner). Använd alltid "in"
i stället för "=" för att jämföra med resultatet från en inre fråga!
select Namn from Sektioner
where Skod in (select Sektion
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Deltar
where Medlem = 1);
Namn |
---|
Bowling |
Konstsim |
Kickboxing |
"select Mnr from Medlemmar where Namn = 'Olle'
", i stället för 1:an:
select Namn from Sektioner
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere Skod in (select Sektion
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Deltar
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere Medlem in (select Mnr
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Medlemmar
where Namn = 'Olle'));
Namn |
---|
Bowling |
Konstsim |
Kickboxing |
Vi kan också skriva samma sak som en flat fråga, dvs utan någon inre fråga. Ett första försök:
129
select Namn from Sektioner, Deltar, Medlemmar
where Skod = Sektion
and Medlem = Mnr
and Namn = 'Olle';
Vi får ett felmeddelande 11 som kan se ut så här:
1: select Namn from Sektioner, Deltar, Medlemmar
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x^
Mimer SQL error -12204 in function PREPARE
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xColumn reference Namn ambiguous
4: and Namn = 'Olle'
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x^
Mimer SQL error -12204 in function PREPARE
Column reference Namn ambiguous
"Ambiguous" betyder "tvetydig". Och mycket riktigt finns det två olika kolumner som heter Namn, en i tabellen Medlemmar och en i tabellen Sektioner. Därför måste man ange vilken av de båda Namn-kolumnerna det är man menar. Vi skriver om frågan så det blir rätt:
select Sektioner.Namn
from Sektioner, Deltar, Medlemmar
where Skod = Sektion
and Medlem = Mnr
and Medlemmar.Namn = 'Olle';
Namn |
---|
Bowling |
Kickboxing |
Konstsim |
Notera att where-villkoret som vanligt dels kopplar ihop de olika tabellerna, dels gör ett urval av vilka rader i resultatet vi är intresserade av. Det behövs två villkor för att koppla ihop tre tabeller, och ytterligare ett villkor för att bara behålla Olle-raderna i svaret.
130Vilka deltar i någon (det vill säga minst en) sektion? Jo, det är förstås de medlemmar vars medlemsnummer förekommer i Deltartabellens Medlem-kolumn:
select * from Medlemmar
where Mnr in (select Medlem from Deltar);
Mnr | Namn | Telefon |
---|---|---|
2 | Stina | 282677 |
3 | Sam | 260088 |
1 | Olle | 260088 |
select
-sats i where
-villkoret:
select distinct Medlemmar.Mnr, Medlemmar.Namn,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xMedlemmar.Telefon
from Medlemmar, Deltar
where Medlemmar.Mnr = Deltar.Medlem;
Nyckelordet distinct
tar bort dubbletter, och behövs eftersom medlemmar som deltar i flera sektioner annars kommer med flera gånger i svaret. I den ursprungliga teoretiska relationsmodellen byggde relationerna på mängder av tupler, och därför fick man aldrig dubbletter i svaret, men i modernare formuleringar av relationsmodellen, och i SQL, arbetar man inte med vanliga mängder utan med multimängder (även kallade påsar, efter engelskans "bag"), som kan innehålla dubbletter. Resultatet av en select
-sats är således en mängd om man specificerar distinct
och en påse om man inte gör det.
Om vi vill veta vilka som inte deltar i någon sektion, ändrar vi bara in
till not in:
select * from Medlemmar
where Mnr not in (select Medlem from Deltar);
Mnr | Namn | Telefon |
---|---|---|
4 | Lotta | 174590 |
in (select
..." kunde skrivas om med en vanlig likhet. Men "... not in (select
..." är svårare att skriva om. Frågan ovan kan inte skrivas om så här:
select distinct Medlemmar.Mnr, Medlemmar.Namn, Medlemmar.Telefon
131
from Medlemmar, Deltar
where Medlemmar.Mnr <> Deltar.Medlem;
Den frågan tar inte fram vilka som inte sportar, utan den tar fram alla medlemmar som inte är helt ensamma om att sporta! (Tänk så här: Varje medlem som går att para ihop med en rad i Deltartabellen som inte handlar om henne själv.)
in
finns det en hel rad med nyckelord som kan användas i samband med en underfråga i SQL: exists, any, some
och all.
exists
används för att avgöra om underfrågan ger några rader som svar. Till exempel kan man göra samma sak som i förra avsnittet, nämligen ta reda på vilka medlemmar som deltar i någon (det vill säga minst en) sektion. Det är ju samma sak som att fråga för vilka medlemmar det existerar rader i Deltar-tabellen:
select * from Medlemmar
where exists (select * from Deltar
where Deltar.Medlem = Medlemmar.Mnr);
Mnr | Namn | Telefon |
---|---|---|
1 | Olle | 260088 |
2 | Stina | 282677 |
3 | Sam | 260088 |
Lägg märke till hur den yttre frågan kan "användas" i den inre frågan! Om den inre frågan stod för sig själv,
select * from Deltar
where Deltar.Medlem = Medlemmar.Mnr;
skulle den ge ett felmeddelande. Tabellen Medlemmar nämns inte i from
-listan, och kan därför inte användas i frågan. I stället kan man se det som att den yttre frågan körs, och sen körs den inre frågan en gång för varje rad i den yttre frågan.
12
Då har man också tillgång till kolumnen Medlemmar.Mnr, från den yttre frågan, och kan använda den i where
-villkoret i den inre frågan.
132
select * from Medlemmar
where exists (select * from Deltar
where Medlem = Mnr);
select *
from Medlemmar, Deltar
where Deltar.Medlem = Medlemmar.Mnr;
skulle man kunna tro att man får samma svar, men (förutom att det blir fler kolumner i svaret) kommer alla medlemmar som deltar i fler än en sektion nu att komma med flera gånger i svaret. Det är ett exempel på hur SQL använder påsar i stället för mängder, som vi nämnde på sidan 130.
Mednot exists
kan man få fram de rader i den yttre frågan som inte ger några rader i den inre, till exempel för att ta reda på vilka medlemmar som inte sportar alls:
select * from Medlemmar
where not exists (select * from Deltar
where Medlem = Mnr);
Mnr | Namn | Telefon |
---|---|---|
4 | Lotta | 174590 |
Vi vill (återigen) varna för att frågan inte kan skrivas om med en olikhet:
select *
from Medlemmar, Deltar
where Medlem <> Mnr;
133
any, some
och all.
Om vi vill veta vilka medlemmar som har ett medlemsnummer som är större än i alla fall någon av de andra medlemmarnas nummer, kan vi använda any (eller some,
som betyder samma sak):
select * from Medlemmar
where Mnr > any (select Mnr
from Medlemmar);
Mnr | Namn | Telefon |
---|---|---|
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta | 174590 |
Vi använde tabellen Medlemmar i både den inre och den yttre frågan, men det är inget konstigt med det. Som vanligt när man har en inre fråga kan man se det som att den yttre frågan körs, och sen körs den inre frågan en gång för varje rad som ska kollas i den yttre frågan. Man gör alltså, kan man säga, flera olika sökningar i samma tabell, men det finns ingen risk att de skulle blandas ihop på något sätt.
Finns det någon som har ett medlemsnummer som är större än alla medlemsnummer?
select * from Medlemmar
where Mnr > all (select Mnr
from Medlemmar);
Mnr | Namn | Telefon |
---|
Här skulle man kanske förväntat sig att Lotta, med medlemsnummer 4, skulle ha kommit med i svaret, för hon har ju ett medlemsnummer som är större än alla andras. Men tänk på att den inre frågan tar fram alla medlemsnummer, inklusive Lottas eget. Därför kommer den yttre frågan inte att ge några rader alls i svaret, för inte ens Lotta har ju ett nummer som är högre än hennes eget nummer.
134order by
, som placeras allra sist i SQL-frågan:
select Namn, Telefon
from Medlemmar
where Mnr > 2
order by Namn;
Namn | Telefon |
---|---|
Lotta | 174590 |
Sam | 260088 |
asc
(för ascending eller stigande, som är default-värdet) respektive desc
(för descending eller fallande). Exempel:
select Medlemmar.Namn, Sektioner.Namn
from Medlemmar, Deltar, Sektioner
where Mnr = Medlem
and Sektion = Skod
order by Medlem.Namn asc, Sektioner.Namn desc;
Namn | Telefon |
---|---|
Olle | Konstsim |
Olle | Kickboxing |
Olle | Bowling |
Sam | Bowling |
Stina | Konstsim |
select
-sats med order by
returnerar en sekvens, vilket är samma sak som en påse där ordningen mellan raderna har betydelse. Eftersom databashanteraren internt arbetar med påsar och mängder, är order by
inte tillåtet inuti frågor, utan bara för det yttersta select-uttrycket i en fråga.
order by
att göra är urval av rader i resultatet. Man vill ofta ha bara en del av resultatet från en
135
fråga, till exempel för att presentera en skärmsida åt gången för användaren, eller för att få fram en tio-i-topp-lista
13
med de snällaste barnen i jultomtens databas. Det kräver förstås att man också gjort en sortering med order by,
för annars kan man ju inte vara säker på vilka rader som blir exempelvis de tio första.
Med moderna databashanterare kan man specificera hur många rader man vill ha i resultatet, för att informera databashanteraren om att man bara är intresserad av de snällaste barnen. Systemet behöver då inte först hämta, sedan sortera och slutligen slänga bort alla utom de snällaste barnen. Denna typ av frågor är speciellt användbar för datalager och beslutsstöd, vilket kommer att behandlas i kapitel 18.
Ursprungligen föreslogs syntaxenstop after
, men den har inte slagit igenom. SQL-standarden innehåller en ganska krånglig lösning med en funktion som heter row_number(),
men många databashanterare har helt andra (och bättre) lösningar.
I MySQL och PostgreSQL använder man nyckelordet limit:
14
select Namn, Lön
from Anställda
where Ålder > 50
order by Lön desc
limit 10;
I MySQL kan man dessutom skriva limit 5,10
för att få rad nummer 6-15, och i PostgreSQL ger limit 10 offset 5
samma resultat.
Som ett annat exempel visar vi hur man kan skriva i Db2:
select Namn, Lön
from Anställda
where Ålder > 50
order by Lön desc
fetch first 10 rows only;
136
select top 10 Namn, Lön
from Anställda
where Ålder > 50
order by Lön desc;
SQL-frågor kan ge svar med väldigt många rader. När man skriver in SQL-frågor och kör dem direkt i ett textgränssnitt brukar databashanteraren dela upp resultatet i lagom många rader för att visa dem på ett läsbart sätt, men när SQL-frågorna ingår i ett program måste man som programmerare oftast tänka på att bara hämta ett lämpligt antal rader åt gången.
I den föregående SQL-frågan tog vi fram alla de medlemmar som hade ett medlemsnummer som var större än alla medlemsnummer. Det blev ju ingen, för inte ens den medlem som har det högsta medlemsnumret har ett nummer som är högre än sitt eget. Men om vi vill ha fram den som har ett högre medlemsnummer än alla andra?
Vi kommer ihåg att den inre frågan (konceptuellt sett) körs en gång för varje rad som ska kollas i den yttre frågan, och att man då (i den inre frågan) kan använda den rad i den yttre frågan som kollas. Vi skulle alltså kunna lägga till ett where-villkor i den inre frågan som tar med alla medlemmar utom just den som vi just nu tittar på i den yttre frågan. Ett första försök:
select * from Medlemmar
where Mnr > all (select Mnr
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Medlemmar
where Mnr <> Mnr);
Det där blir förstås alldeles fel, för hur ska databashanteraren kunna veta att vi menar två olika medlemsnummer när vi skriver "Mnr <> Mnr
"? När databashanteraren kör den inre frågan kommer den att leta efter tabellnamn och kolumnnamn, som Mnr, i de tabeller som räknas upp på den inre frågans som from
-lista, och det är först när den inre frågan nämner en tabell eller kolumn som inte finns med i from
-listan som den "hoppar ut" eller "höjer blicken" ett steg, och tittar på den yttre frågan.
137
select * from Medlemmar as gammal
where Mnr > all (select Mnr
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Medlemmar as ung
where ung.Mnr <> gammal.Mnr);
Mnr | Namn | Telefon |
---|---|---|
4 | Lotta | 174590 |
as
kan utelämnas om man vill, och vissa databashanterare tillåter inte att man har med det.
En kort utviktning: Att fråga vem som har ett medlemsnummer som är högre än alla andras är ju samma sak som att fråga vem som har det högsta medlemsnumret. Vi kommer inte att lära oss om så kallade aggregatfunktioner, som max
, förrän i ett senare avsnitt, men frågan om vem som har det högsta medlemsnumret uttrycks lättast så här:
select * from Medlemmar
where Mnr = (select max(Mnr)
from Medlemmar);
insert
, ta bort rader med delete
, och ändra på raders innehåll med update
.
select * from Medlemmar;
Mnr | Namn | Telefon |
---|---|---|
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta | 174590 |
1 | Olle | 260088 |
insert into Medlemmar values (7, 'Isaac', '281000');
select * from Medlemmar;
138
Mnr | Namn | Telefon |
---|---|---|
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta | 174590 |
1 | Olle | 260088 |
7 | Isaac | 281000 |
insert into Medlemmar values (8, 'Nelson');
Mimer SQL error -12233 in function PREPARE
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xThe number of insert values is not the same as the
number of object columns
insert into Medlemmar (Mnr, Namn) values (8, 'Nelson');
select * from Medlemmar;
Mnr | Namn | Telefon |
---|---|---|
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta | 174590 |
1 | Olle | 260088 |
7 | Isaac | 281000 |
8 | Nelson | null |
Nelsons telefon fick värdet null. Nelson har alltså inte något telefonnummer lagrat i tabellen, utan rutan är tom. Ett null-värde kan betyda olika saker: att Nelson inte har någon telefon, att vi inte vet numret, eller kanske att telefonnummer inte är tillämpliga för medlemmar som är döda engelska amiraler.
Som ett alternativ till att utelämna kolumnen iinsert
-satsen kan man skriva ut ordet null
:
insert into Medlemmar (Mnr, Namn, Telefon)
values (18, 'Nelson', null);
Vi kan söka efter de medlemmar som inte har något telefonnummer:
139
select * from Medlemmar where Telefon is null;
Mnr | Namn | Telefon |
---|---|---|
8 | Nelson | null |
Telefon is null
", och inte "Telefon = null
". Null är inte samma sak som talet noll eller en tom sträng, utan det är ett eget värde som just betyder att det inte finns något värde i den rutan i tabellen.
Kan vi lägga in en ny medlem med samma nummer som en existerande medlem? Det beror på. Om vi angett att kolumnen Mnr är en nyckel, kommer databashanteraren automatiskt att kontrollera att inga dubbletter läggs in. Exempel:
insert into Medlemmar
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvalues (4, 'Bengt', '222000');
Mimer SQL error -10101 in function EXECUTE
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xPRIMARY KEY constraint violated, attempt to
insert duplicate key in table Medlemmar
Det fanns redan en medlem med medlemsnummer 4, nämligen Lotta. Vi kan föregripa avsnittet om hur man skapar tabeller med SQL genom att visa tabelldefinitionen för tabellen Medlemmar:
create table Medlemmar
(Mnr integer not null,
Namn varchar(6),
Telefon varchar(10),
primary key (Mnr));
insert into Medlemmar
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvalues (17, 'Nelson', '281000');
select * from Medlemmar;
140
Mnr | Namn | Telefon |
---|---|---|
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta | 174590 |
1 | Olle | 260088 |
7 | Isaac | 281000 |
8 | Nelson | null |
17 | Nelson | 281000 |
Man kan även ta raderna i resultatet av en SQL-fråga och lägga in i en tabell. Ett exempel:
insert into Medlemmar (Mnr, Namn, Telefon)
select Nummer + 10, Namn, null
from Prospects
where Inbetalt >= 100;
Vi kan ta bort rader med kommandot delete
:
delete from Medlemmar where Namn = 'Isaac';
1 row deleted
Vi kontrollerar att medlemmen Isaac har försvunnit:
select * from Medlemmar;
Mnr | Namn | Telefon |
---|---|---|
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta | 174590 |
1 | Olle | 260088 |
7 | Isaac | 281000 |
8 | Nelson | null |
17 | Nelson | 281000 |
delete
tar helt enkelt bort alla rader som matchar where
-villkoret. Se upp med "delete from Medlemmar
", utan where
-villkor, som tömmer hela tabellen!
Till sist ska vi också se att vi kan ändra rader med kommandot update:
141
update Medlemmar
set Telefon = '260088'
where Namn = 'Lotta';
1 row updated
Mnr | Namn | Telefon |
---|---|---|
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta | 174590 |
1 | Olle | 260088 |
7 | Isaac | 281000 |
8 | Nelson | null |
17 | Nelson | 281000 |
update
för att ändra ett medlemsnummer:
update Medlemmar
set Mnr = 4
where Namn = 'Sam';
Mimer SQL error -10101 in function EXECUTE
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xPRIMARY KEY constraint violated, attempt to
insert duplicate key in table Medlemmar
Det är inget fel att ändra på värdet i en kolumn som är deklarerad som nyckel, men i det här fallet fanns det ju redan en medlem med medlemsnummer 4. Precis som vid insättning av nya rader kontrollerar databashanteraren att nyckelvillkoret, alltså att varje rad ska ha ett unikt värde i kolumnen Mnr, upprätthålls.
Man kan ändra flera kolumner på en gång:
update Medlemmar
set Mnr = 100, Telefon = '118118'
where Namn = 'Sam';
Det går också bra att ändra på flera rader på en gång med update
, och att basera de nya värdena på de gamla. Om vi tillfälligtvis antar att vi har lagrat anställda i tabellen Anställda,
15
och vill höja lönen (kolumnen Lön) för alla som arbetar på dataavdelningen med fem procent, kan vi använda det här kommandot:
142
update Anställda
set Lön = Lön * 1.05
where Avdelning = 'Data';
En vy i SQL är en tabell som inte lagras i databasen, utan vars innehåll räknas ut på nytt varje gång man tittar på den. (Det kan fungera annorlunda internt i databashanteraren, men det ser alltid ut som om vyns innehåll räknas ut på nytt.) Man kan också se en vy som en fråga, som man gett ett namn och sparat undan, så att man sen kan köra den på nytt och titta på dess resultat. Eftersom alla SQL-frågor ger en tabell som resultat, ser vyn ut som om den var en vanlig tabell.
En vydefinition består alltså av en enda SQL-fråga, som definierar vyns innehåll.
Nu ska vi prova på vyer. Vi börjar med en vanlig SQL-fråga, som visar sektionsnamn tillsammans med namnet på ledaren för den sektionen:
select Sektioner.Namn, Medlemmar.Namn
from Sektioner, Medlemmar
where Ledare = Mnr;
Namn | Namn |
---|---|
Konstsim | Stina |
Bowling | Lotta |
Kickboxing | Lotta |
Vi försöker göra en vy av den frågan, med hjälp av kommandot
create view:
create view Ledarskap
as select Sektioner.Namn, Medlemmar.Namn
from Sektioner, Medlemmar
where Ledare = Mnr;
Mimer SQL error -12252 in function EXECUTE
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xCREATE VIEW statement must include a
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xcolumn list because the SELECT clause
contains duplicated column name Namn
143
Det misslyckades. I svaret på en fråga, som bara ska skrivas ut, kan man tillåta två kolumner med samma namn, men inte i en vy eller tabell. Precis som det står i felmeddelandet måste vi ge namn till kolumnerna i vyn, till exempel så här:
create view Ledarskap (Sektionsnamn, Ledarnamn)
as select Sektioner.Namn, Medlemmar.Namn
from Sektioner, Medlemmar
where Ledare = Mnr;
Alternativt kan man använda as
för att ge kolumnerna nya namn:
create view Ledarskap
as select Sektioner.Namn as Sektionsnamn,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xMedlemmar.Namn as Ledarnamn
from Sektioner, Medlemmar
where Ledare = Mnr;
select * from Ledarskap;
Sektionsnamn | Ledarnamn |
---|---|
Konstsim | Stina |
Bowling | Lotta |
Kickboxing | Lotta |
select Ledarnamn
from Ledarskap
where Sektionsnamn = 'Bowling';
Eftersom en vy i SQL är en lagrad SQL-fråga, kan vyer vara lika komplicerade som godtyckliga SQL-frågor. 16
En skillnad mellan vyer och vanliga tabeller är att vyer vanligen inte kan uppdateras. En del databashanterare tillåter att man gör ändringar i innehållet i en vy, och dessa ändringar slår då igenom till de tabeller som vyn baseras på, men det finns många ändringar som databashanteraren inte kan lyckas med. Antag till exempel att vi ger följande kommando:
144
update Ledarskap
set Ledarnamn = 'Lotta'
where Sektionsnamn = 'Konstsim';
Den ändringen skulle kunna genomföras genom att vi byter ledare för konstsimsektionen från Stina till Lotta, med en ändring av kolumnen Ledare i tabellen Sektioner. Men den skulle också kunna göras genom att vi byter namn på medlemmen Stina till Lotta, och alltså ändrar i kolumnen Namn i tabellen Medlemmar. Databashanteraren kan inte veta vilken av dessa ändringar vi vill göra och ger därför ett felmeddelande.
I regel kan vyer som kombinerar tabeller inte uppdateras. En allmän regel är att vyer över en enda tabell som inkluderar dess primärnyckel kan uppdateras, som till exempel:
create view Medlemsnamn
as select Mnr, Namn from Medlemmar;
commit
och rollback
.
Ofta är default-inställningen att varje SQL-kommando räknas som en egen transaktion, så kallad auto-commit, och då måste man explicit slå på transaktionshanteringen för att kunna gruppera ihop flera SQL-satser till en transaktion. Man brukar använda kommandot start transaction
för att påbörja en transaktion som sträcker sig över flera satser. I en del system kallas det i stället begin transaction.
Om man använder SQL inifrån ett program, till exempel med ODBC eller JDBC, finns det särskilda anrop för att slå på transaktionshanteringen. Ibland kallas det att "stänga av autocommit".
Vi startar en transaktion, och studerar än en gång innehållet i tabellen Sektioner:
start transaction;
select * from Sektioner;
145
Skod | Namn | Ledare |
A | Bowling | 4 |
C | Konstsim | 2 |
B | Kickboxing | 4 |
Lägg till en ny sektion i tabellen:
insert into Sektioner values ('D', 'Brännboll', 3);
select * from Sektioner;
Skod | Namn | Ledare |
A | Bowling | 4 |
C | Konstsim | 2 |
B | Kickboxing | 4 |
D | Brännboll | 3 |
commit:
commit;
I och med att vi gjort commit, är ändringarna "sparade" i databasen. De kommer inte att försvinna om datorn kraschar eller om strömmen går, och i ett fleranvändarsystem blir de nu synliga för andra användare.
Nu kan vi göra fler ändringar, i en ny transaktion:
start transaction;
delete from Sektioner where Namn = 'Brännboll';
insert into Sektioner
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvalues ('E', 'BASE-hoppning', 3);
update Sektioner set Namn = 'Maraton'
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere Namn = 'Bowling';
select * from Sektioner;
Skod | Namn | Ledare |
A | Maraton | 4 |
C | Konstsim | 2 |
B | Kickboxing | 4 |
E | BASE-hoppning | 3 |
rollback
. Och plötsligt är allting som förut:
rollback;
select * from Sektioner;
Skod | Namn | Ledare |
A | Bowling | 4 |
C | Konstsim | 2 |
B | Kickboxing | 4 |
D | Brännboll | 3 |
Databasens schema kan ändras. Både tabeller och vyer kan ändras så att de får fler, eller färre, kolumner än de hade från början. När man skriver SQL-kommandon "ad hoc", dvs som man skriver in direkt, för hand, och som ska köras en enda gång, kan man strunta i det. Men SQL-kommandon som ska finnas kvar länge och köras många gånger, till exempel om de ingår i ett program eller ett skript, kan ge problem om databasens schema ändras.
select * from Medlemmar;
Tabellen Medlemmar kanske har de tre kolumnerna Mnr, Namn och Telefon. Om frågan ingår i ett program, skriver vi kanske programkod för att hantera tre värden från varje rad i resultatet. Men så ändrar någon tabellen (med SQL-kommandot alter table
) och lägger till en fjärde kolumn, Adress. Nu ger frågan plötsligt fyra värden per rad, och om programmet inte kan hantera det, kanske det slutar fungera. Skriv hellre ut de kolumner som faktiskt ska hämtas:
select Mnr, Namn, Telefon from Medlemmar;
147
Om tabellens schema ändras kommer programmet fortfarande att fungera. Det kan inte hantera den nya kolumnen med adresser, men programmet klarar i alla fall att jobba vidare med de tre gamla kolumnerna.
Ett liknande problem kan uppstå med frågor som den här (från sidan 124):
select Medlemmar.Namn
from Sektioner, Medlemmar
where Ledare = Mnr
and Sektioner.Namn = 'Bowling';
Kolumnen Namn fanns, som vi kanske kommer ihåg, i båda tabellerna, och därför måste vi ange vilket namn vi menar, som med Medlemmar.Namn
. Kolumnerna Ledare och Mnr fanns bara i en av tabellerna, så där behövdes det inte. Men vad händer om någon ändrar någon av tabellernas schema och exempelvis lägger till en kolumn Ledare även i tabellen Medlemmar? Då finns det plötsligt två kolumner med samma namn att välja mellan, och frågan slutar fungera. Därför kan det vara bra att alltid ange tabellnamnen i SQL-kommandon som ska användas länge:
select Medlemmar.Namn
from Sektioner, Medlemmar
where Sektioner.Ledare = Medlemmar.Mnr
and Sektioner.Namn = 'Bowling';
create table
. Det kan se ut så här:
create table Sektioner
(Skod char(1) not null,
Namn varchar(14),
Ledare integer,
primary key (Skod),
unique (Namn),
148
foreign key (Ledare) references Medlemmar (Mnr));
Här skapade vi en tabell som heter Sektioner. Mellan parenteserna räknade vi upp tabellens kolumner, och vilka datatyper de har. Char(1)
betyder ett textfält som innehåller ett enda tecken.
Varchar(14)
betyder ett textfält som kan innehålla från noll upp till fjorton tecken. Integer
betyder heltal. Det finns fler datatyper i SQL, till exempel date
som betyder datum. Det kan variera lite mellan olika databashanterare vilka datatyper som finns och vad de heter.
Not null
, som står efter kolumnen Skod, anger förstås att den kolumnen inte får innehålla några null-värden.
För att undvika null-värden kan man ange ett defaultvärde för en kolumn, till exempel så här:
Namn varchar(14) default 'Felix',
Om man lägger in en ny rad i tabellen (med insert
) och inte sätter den kolumnens värde, sätter databashanteraren automatiskt in defaultvärdet i stället. Eftersom null-värden orsakar en del problem när man ställer frågor
19
är det ofta bra att använda defaultvärden, om de understöds av databashanteraren.
Primary key (Skod)
anger vad som är primärnyckel i tabellen, nämligen kolumnen Skod. En primärnyckel kan vara sammansatt av flera kolumner, och då räknar man upp dem med komma emellan. Förutom att primärnyckeln inte får ha samma värde på två olika rader, får den inte innehålla några null-värden. En del databashanterare (men inte alla) kräver därför att man skriver not null på de kolumner som ingår i primärnyckeln.
Foreign key (Ledare) references Medlemmar (Mnr)
anger att Medlem är ett referensattribut, eller främmande nyckel som det också kallas, som refererar till kolumnen Mnr i tabellen Medlemmar. Ett referensattribut ska alltid referera till en nyckel.
149
direkt i definitionen av den kolumnen, som kolumnvillkor i stället för som tabellvillkor. Följande definition av tabellen Medlemmar är ekvivalent med den föregående:
create table Sektioner
(Skod char(1) not null primary key,
Namn varchar(14) unique,
Ledare integer references Medlemmar (Mnr));
Om man har fler villkor som man vill att databashanteraren ska hjälpa till att hålla reda på, kan man skriva dit dem med nyckelordet check. Här är ett exempel på en tabell med flera olika villkor:
create table Blaj77
(Nummer integer,
Florp integer check (Florp in (1, 17, 4711)),
Fnyyb integer check (Fnyyb > 17 and Fnyyb < 123
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xor Fnyyb = -1),
primary key(Nummer),
check (Florp is null or Fnyyb > 100));
alter table
för att ändra en tabells definition, till exempel för att lägga till ett referensattribut. Man kan också lägga in referensattributen direkt i create table
-kommandona, men om man har cirkulära referenser måste minst en av dem läggas till i efterhand.
create table Medlemmar
(Mnr integer not null,
Namn varchar(6),
Telefon varchar(10),
primary key (Mnr));
create table Sektioner
(Skod char(1) not null,
150
Namn varchar(14),
Ledare integer,
primary key (Skod));
create table Deltar
(Medlem integer not null,
Sektion char(1) not null,
primary key (Medlem, Sektion));
alter table Sektioner
add foreign key (Ledare) references Medlemmar (Mnr);
alter table Deltar
add foreign key (Medlem) references Medlemmar (Mnr);
alter table Deltar
add foreign key (Sektion) references Sektioner (Skod);
Vår rekommendation är att skapa ett skript, dvs en textfil med alla kommandon som behövs för att skapa databasens schema, och använda det när man skapar databasen. Se till exempel kommandona i föregående avsnitt. Om databasen ingår i ett projekt med versionshantering, ska det skriptet versionshanteras på samma sätt som all annan programkod.
Man kan också arbeta medcreate table
och alter table
direkt i ett SQL-gränssnitt, eller klicka sig fram i ett grafiskt gränssnitt, tills databasen är färdig. Detta kan dock vara både omständligt och långsamt, och när man är klar kan det vara besvärligt att få fram databasens schema ur databashanteraren i en form som går att använda som dokumentation, för att skapa om databasen, eller för att skapa en ny instans av samma databas.
insert
-kommandot kan användas för att lägga till rader i en tabell. För att underlätta för den som vill provköra exemplen visar vi här hur vi ursprungligen la in data i tabellerna i idrottsdatabasen:
151
insert into Medlemmar (Mnr, Namn, Telefon)
values (2, 'Stina', '282677');
insert into Medlemmar (Mnr, Namn, Telefon)
values (3, 'Sam', '260088');
insert into Medlemmar (Mnr, Namn, Telefon)
values (4, 'Lotta', '174590');
insert into Medlemmar (Mnr, Namn, Telefon)
values (1, 'Olle', '260088');
insert into Sektioner (Skod, Namn, Ledare)
values ('A', 'Bowling', 4);
insert into Sektioner (Skod, Namn, Ledare)
values ('C', 'Konstsim', 2);
insert into Sektioner (Skod, Namn, Ledare)
values ('B', 'Kickboxing', 4);
insert into Deltar (Medlem, Sektion)
values (1, 'A');
insert into Deltar (Medlem, Sektion)
values (1, 'B');
insert into Deltar (Medlem, Sektion)
values (1, 'C');
insert into Deltar (Medlem, Sektion)
values (2, 'C');
insert into Deltar (Medlem, Sektion)
values (3, 'A');
Vi har tidigare sagt att ett schema beskriver vilka data databasen kan innehålla, till skillnad från de data som råkar finnas i databasen vid ett visst tillfälle. Termen schema används också specifikt i SQL för att beskriva en samling tabeller och andra objekt. En databas kan innehålla flera olika scheman. Tabellerna i ett schema har egentligen namn på formen schemanamn. tabellnamn, men varje användare har ett default-schema, och i det schemat räcker det att skriva tabellnamn för att referera till en tabell. I det här kapitlet har vi hela tiden hållit oss inom ett och samma schema, och därför har vi inte behövt bry oss om det.
152 En databashanterare som följer SQL-standarden ska ha ett särskilt schema som heterinformation_schema
, och som innehåller ett antal tabeller och vyer med metadata om databasen. Till exempel finns vyn information_schema.tables
, som innehåller en rad för varje tabell i databasen. Följande SQL-fråga listar namnen på alla tabellerna:
select table_name
from information_schema.tables;
primary key
, eller (mindre vanligt) med unique
.
Tänk på att det inte räcker med att kolumnen man refererar till ingår i en nyckel. Om nyckeln i den andra tabellen är sammansatt, kan ett referensattribut bara referera till hela den nyckeln.
Ett annat vanligt fel är att försöka skriva om en SQL-fråga mednot in
eller not exists
till en flat SQL-fråga med en olikhet. Ta till exempel en fråga som den här, som talar om vilka medlemmar i klubben som inte sportar alls:
select * from Medlemmar
where not exists (select * from Deltar
where Medlem = Mnr);
Mnr | Namn | Telefon |
4 | Lotta1 | 74590 |
Den kan inte skrivas om så här:
select *
from Medlemmar, Deltar
where Medlem <> Mnr;
Den frågan ger inte de medlemmar som inte deltar i någon sektion. I stället ger den alla kombinationer av medlemmar och deltagande utom just de intressanta som parar ihop en medlem med den medlemmens deltagande:
153Mnr | Namn | Telefon | Medlem | Sektion |
---|---|---|---|---|
1 | Olle | 260088 | 2 | C |
1 | Olle | 260088 | 3 | A |
2 | Stina | 282677 | 1 | A |
2 | Stina | 282677 | 1 | B |
2 | Stina | 282677 | 1 | C |
2 | Stina | 282677 | 3 | A |
3 | Sam | 260088 | 1 | A |
3 | Sam | 260088 | 1 | B |
3 | Sam | 260088 | 1 | C |
3 | Sam | 260088 | 2 | C |
4 | Lotta | 174590 | 1 | A |
4 | Lotta | 174590 | 1 | B |
4 | Lotta | 174590 | 1 | C |
4 | Lotta | 174590 | 2 | C |
4 | Lotta | 174590 | 3 | A |
En SQL-fråga är inte som ett program i Java eller C, som går att köra direkt, steg för steg. I stället ska man se SQL-frågan som en regel som specificerar vad man vill ha fram, och databashanteraren måste sen alltid översätta den till de operationer som ska göras på databasen för att beräkna svaret, den så kallade exekveringsplanen. Det går oftast att göra många olika exekveringsplaner för samma fråga. Frågan kan ta mycket olika lång tid att köra, beroende på vilken exekveringsplan man väljer. Det kan röra sig om en skillnad mellan sekunder och år. Därför optimerar databashanteraren SQL-frågorna. Frågeoptimering innebär att databashanteraren räknar ut det snabbaste sättet (eller åtminstone ett någorlunda snabbt sätt) att beräkna svaret, med hänsyn tagen till databasens schema, lagringsstruktur och innehåll.
Frågeoptimeraren är helt enkelt den del av databashanteraren som väljer ut den snabbaste exekveringsplanen (eller i alla fall en snabb). Detta skiljer sig från optimeringssteget i en kompilator för ett vanligt programmeringsspråk som Java eller C, som bara kan göra jämförelsevis enkla omstuvningar av operationerna för att göra programmet snabbare.
154Därför behöver man inte bry sig om exakt hur man formulerar SQL-frågorna, bara man skriver dem på ett sätt som ger rätt svar. Databashanteraren kommer ändå att köra dem så snabbt det går.
Emellertid är optimerarna förstås inte ofelbara, och det kan (tvärtemot vad vi påstod i förra stycket) ibland spela roll hur man formulerar frågan. Vill man att det ska gå fort, bör man skriva flata frågor. I regel är flata frågor bättre än nästlade frågor både eftersom de är lättare för människor att läsa, och därför att frågeoptimeraren utgår från flata frågor. En sofistikerad frågeoptimerare kan inte alltid automatiskt transformera en nästlad fråga till en flat. Men vill man veta säkert måste man mäta, med just det schemat på just den versionen av just den databashanteraren.
Olika databashanterare är olika bra på att optimera olika typer av SQL-frågor, speciellt komplicerade och konstiga frågor. När en databashanterartillverkare som Microsoft eller Oracle ska mäta prestanda hos sin databashanterare, väljer de förstås gärna en fråga som just deras optimerare råkar lyckas ovanligt bra med, och som konkurrenterna lyckas ovanligt dåligt med. Därför kan man få se en mätning där databashanteraren Oracle är en miljon gånger snabbare än databashanteraren SQL Server, och en annan mätning där SQL Server är en miljon gånger snabbare än Oracle. Det säger inte så mycket om hur snabba SQL Server och Oracle är, utan mer om att både Microsoft och Oracle har lagt ned tid på att hitta på konstiga SQL-frågor.
För att på ett (någorlunda) rättvist sätt jämföra olika databashanterare för olika sorters realistiska tillämpningar finns det därför ett antal standardiserade benchmarks. 21 Ett benchmark, eller provbänk på svenska, är en mätning av till exempel snabbhet med syftet att jämföra med andra liknande system. Dessa databasprovbänkar är utvecklade av TPC, the Transaction Processing Performance Council, som är en oberoende organisation där de ledande databasteknologiföretagen är medlemmar.
Man kan dock inte alltid lita ens på standardiserade provbänkar. Det är inte ett okänt fenomen att databashanterartillverkare lägger in kod i optimeraren för att specialhantera just de frågor som ingår i proven. Man kan jämföra med dieselbilsskandalen från 2015. 22 Det ska dock sägas att man i TPC lagt ner en hel del arbete på att få sina 155 provbänkar rättvisa: till exempel automatgenereras databasen, och frågorna är definierade av provbänken.
För den som är intresserad av benchmarks, och hur TPC utvecklades, rekommenderas boken J. Gray (red.): Database and Transaction Processing Performance Handbook, Morgan Kaufmann, 1993, ISBN 1-55860-292-5. 23
Webbsöktjänsten Gurgel används för att hitta webbsidor som innehåller sökord. Till exempel kan man mata in flogiston och struts, varefter Gurgel svarar med en lista med adresserna till alla webbsidor som innehåller både ordet flogiston och ordet struts.
Söktjänsten Gurgel kan förstås inte läsa igenom hela webben för varje fråga den kör, utan i stället bygger den upp en databas som beskriver vilka ord som finns på vilka webbsidor. Den databasen lagras lokalt hos Gurgel, och när någon söker efter ord (till exempel flogiston och struts) översätter Gurgel det till en SQL-fråga, som körs mot den databasen.
Här är ett ER-diagram som visar hur Gurgels databas ser ut:
Även om såväl sökorden (attributet Ord hos entitetstypen Sökord) som webbsidornas adresser (attributet Adress hos entitetstypen Webbsida) är unika, och alltså skulle kunna användas som nycklar, har 156 man valt att ge båda entitetstyperna en primärnyckel (attributen Id) som är ett enkelt heltal.
create table
-kommandon för de tabellerna.
De flesta grundläggande databasböcker innehåller en genomgång av SQL:
Man kan hitta flera olika handledningar på webben, till exempel SQL-boken PostgreSQL: Introduction and Concepts av Bruce Momjians. Se avsnitt 33.3 på sidan 675 för detaljer om dessa.
Det finns också böcker som behandlar mer avancerad SQL, och som förutsätter att man redan har grundkunskaper. Några bra exempel:
157Se avsnitt 33.2 på sidan 673 för detaljer om böckerna.
1585 I många andra datasammanhang använder man frågetecken ("?") för att matcha mot ett godtyckligt tecken och stjärna ("*") för en följd av tecken, men SQL har alltså sin egen variant.
9 Så skulle det i alla fall vara i en ideal värld. Det är inte sant för alla databashanterare. Det är inte säkert att komplicerade nästlade frågor blir lika effektiva att utföra som flata frågor. Databashanteraren har lättare att optimera flata frågor, och måste därför först överföra nästlade frågor till flata frågor, innan de kan optimeras. Sådan 'avnästling' är inte alltid implementerad.
10 Pseudokod är programkod som inte är skriven i ett riktigt programmeringsspråk, utan i en blandning mellan vanlig svenska och programspråkskonstruktioner. Den används för att beskriva för människor hur man gör saker.
11 Det är inte säkert att man får något felmeddelande. Databashanteraren Mimer ger som synes ett felmeddelande, men vi har råkat ut för en databashanterare som körde den här frågan utan att protestera, men gav fel svar. Den valde nämligen att tolka båda förekomsterna av Namn som Medlemmar.Namn, och varnade inte för någon tvetydighet.
Kapitel 7 tar upp grunderna om SQL. I det här kapitlet ska vi titta på lite mer avancerad användning av SQL, främst med aggregatfunktioner, explicit join och yttre join. Lagrade procedurer och triggers, som också kan räknas till mer avancerad användning av SQL, tas upp i egna kapitel.
Vi utgår från två enkla tabeller, en som heter Arbetare och en som heter Kontor:
Arbetare | |||
Anr | Anamn | Lön | Placering |
1 | Bob | 1 000 | 10 |
2 | Liz | 2 000 | 10 |
3 | Sam | 1 000 | 30 |
Kontor | |
Knr | Knamn |
10 | Gnesta |
20 | Moskva |
30 | Pyongyang |
För att underlätta för den som vill provköra 1 visar vi här SQL-kommandona för att skapa tabellerna och fylla dem med exempeldata: 2
create table Kontor
(Knr integer,
Knamn varchar(10),
primary key (Knr));
insert into Kontor (Knr, Knamn)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvalues (10, 'Gnesta');
insert into Kontor (Knr, Knamn)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvalues (20, 'Moskva');
insert into Kontor (Knr, Knamn)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvalues (30, 'Pyongyang');
create table Arbetare
(Anr integer,
Anamn varchar(3),
Lön integer,
Placering integer,
foreign key (Placering) references Kontor (Knr),
primary key (Anr));
insert into Arbetare (Anr, Anamn, Lön, Placering)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvalues (1, 'Bob', 1000, 10);
insert into Arbetare (Anr, Anamn, Lön, Placering)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvalues (2, 'Liz', 2000, 10);
insert into Arbetare (Anr, Anamn, Lön, Placering)
values (3, 'Sam', 1000, 30);
161
Vi kan koppla ihop de två tabellerna så man tydligt ser vem som jobbar var:
select *
from Arbetare, Kontor
where Placering = Knr;
Anr | Anamn | Lön | Placering | Knr | Knamn |
1 | Bob | 1000 | 10 | 10 | Gnesta |
2 | Liz | 2 000 | 10 | 10 | Gnesta |
3 | Sam | 1000 | 30 | 30 | Pyongyang |
avg
, som räknar ut medelvärdet, count
, som räknar antalet värden i en kolumn,
3
max
, som returnerar det största värdet i en kolumn, min
, som returnerar det minsta, och sum
, som summerar alla värdena. Dessutom finns every
, som ger ett sant värde om alla värden den arbetar med är sanna, och any
med sin synonym some
, som ger ett sant värde om något av de värden den arbetar med är sant.
Vi provar max
-funktionen:
select max(Anr) from Arbetare;
Vad kolumnen kommer att heta är olika i olika databashanterare. Det kan bli max(Anr) som i exemplet, men det kan också bli något annat, eller inget alls. Det kan vara bra att ge den ett nytt namn:
select max(Anr) as Maxnumret from Arbetare;
162
Ofta vill man inte bara beräkna en aggregatfunktion, utan man vill använda det beräknade värdet för att få fram något annat. Att få fram det högsta anställningsnumret var lätt, men vem är det som har det numret? Vi försöker först med den här frågan:
select *
from Arbetare
where Anr = max(Anr);
Det gick inte, och databashanteraren ger ett felmeddelande som till exempel kan se ut så här:
Mimer SQL error -12213 in function PREPARE
Set function not specified in a SELECT clause or
HAVING clause
Felmeddelandet talar om mängdfunktioner (engelska: set function). Det är den term som används i standarddokumenten, men alla andra säger aggregatfunktioner.
I SQL kan man inte ha aggregatfunktioner direkt iwhere
-villkoret. Vi måste baka in aggregatfunktionen i en underfråga:
select *
from Arbetare
where Anr = (select max(Anr) from Arbetare);
Anr | Anamn | Lön | Placering |
3 | Sam | 1000 | 30 |
avg
av 2, 4 och null är alltså 3, inte 2.
select sum(Lön), avg(Lön)
from Arbetare;
163
sum(Lön) | avg(Lön) |
4 000 | 1 333 |
Notera att man alltså kan beräkna flera olika aggregatfunktioner i samma fråga.
Man kan kombinera aggregatfunktioner med ettwhere
-villkor, vilket gör att aggregatfunktionen bara beräknas för de rader som uppfyller where
-villkoret. Man kan alltså se det som att först körs frågan med where-villkoret, och sen beräknas aggregatfunktionen, på de rader som kom med. Till exempel kan vi ta fram Bob och Sam, och sen beräkna genomsnittet av deras löner:
select avg(Lön)
from Arbetare
where Anamn = 'Bob' or Anamn = 'Sam';
Frågan som körs "först", före aggregatfunktionen, kan vara en mer komplicerad fråga, med flera tabeller. Till exempel kan vi ta fram genomsnittslönen för alla som arbetar på Gnesta-kontoret:
select avg(Lön)
from Arbetare, Kontor
where Placering = Knr
and Knamn = 'Gnesta';
Som vanligt kan man koppla ihop tabeller antingen genom att räkna upp dem i from
-listan och koppla ihop dem i where
-villkoret, eller genom att använda en underfråga i where
-villkoret. Det här är alltså ett alternativt sätt att skriva samma fråga:
select avg(Lön)
from Arbetare
where Placering = (select Knr from Kontor
where Knamn = 'Gnesta');
Vi nämnde ovan att SQL-standarden avänder termen "mängdfunktioner" för det som brukar kallas aggregatfunktioner. Men när man talar om mängder och vad man kan göra med dem, brukar det handla om mängdoperationer, som arbetar med mängder och ger mängder som svar.
En mängd är en samling element, utan några dubbletter. Ett element kan alltså inte vara med i samma mängd två gånger, utan antingen är det med i mängden eller också inte. En person kan till exempel vara svensk, dvs vara med i mängden svenskar, men hon kan inte vara svensk två gånger.
Följande figurer visar unionen, snittet respektive differensen av X och Y:
union.
intersect.
except.
Operationen union finns i många SQL-dialekter, men snitt och differens kan saknas i en del.
165Man kan inte utföra en mängdoperation direkt på två tabeller, utan man gör det på resultatet av två SQL-frågor. Exempel:
select Anr, Anamn
from Arbetare
where Placering = 10
union
select Anr, Anamn
from Arbetare
where Placering = 30;
Anr | Anamn |
1 | Bob |
2 | Liz |
3 | Sam |
union
är ekvivalent med följande fråga med or
:
select Anr, Anamn
from Arbetare
where Placering = 10 or Placering = 30;
Även om en lagrad tabell, med sina nyckelvillkor, inte kan innehålla dubbletter (alltså rader med samma värden), ger SQL-operationer resultat som kan innehålla dubbletter. SQL-operationerna returnerar alltså egentligen inte vanliga mängder, utan multimängder, även kallade påsar. De engelska termerna är multiset och bag.
Resultatet av enselect
-sats är alltså en påse, om man inte specificerar distinct
för att ta bort dubbletterna.
Mängdoperationerna finns också i varianter för påsar. Genom att lägga till nyckelordet all
får man operationer som inte tar bort
166
dubbletter, till exempel union all
för pås-union. SQL-operatorn or gör en union all.
Notera att union all
är betydligt effektivare än en vanlig mängdunion
eftersom dubbletter i resultatet inte behöver tas bort.
För att förenkla frågorna i de efterföljande exemplen kommer vi ihåg att Gnesta-kontoret har nummer 10. Beräkna alltså genomsnittslönen bara för dem som jobbar på kontoret i Gnesta (nummer 10):
select avg(Lön)
from Arbetare
where Placering = 10;
Vi kan ta båda kontoren, i Gnesta (nummer 10) och Pyongyang (nummer 30) med hjälp av SQL-operationen union
:
select avg(Lön)
from Arbetare
where Placering = 10
union
select avg(Lön)
from Arbetare
where Placering = 30;
Vi nämnde tidigare att mängdoperationen union och den logiska operationen eller är relaterade. Men SQL-frågan ovan, med union
, är inte samma sak som den här, med or
:
select avg(Lön)
from Arbetare
where Placering = 10 or Placering = 30;
167
Skillnaden är att i det här fallet körs frågan med where
-villkoret först, och först därefter beräknas genomsnittet av alla rader som uppfyllde where
-villkoret. Det är en annan sak än att först beräkna två olika genomsnitt, och sen ta med båda resultaten i svaret.
Det går inte heller att byta ut or
mot and
:
select avg(Lön)
from Arbetare
where Placering = 10 and Placering = 30;
Den frågan söker fram de anställda vars Placering samtidigt är både 10 och 30. Några sådana finns förstås inte. Resultatet av aggregatfunktionen blir därför null, eftersom medelvärdet av noll tal är odefinierat.
Om vi vill ha genomsnittslönen för vart och ett av kontoren kan vi alltså användaunion
för att slå ihop resultaten av flera separata frågor, som vi gjorde ovan med kontor nummer 10 och 30. Det blir mycket enklare om vi i stället använder SQL-konstruktionen group by
:
select avg(Lön)
from Arbetare
group by Placering;
Men vilka kontor gäller dessa genomsnitt, och i vilken ordning kommer de? Om vi tar med den kolumn som vi använde för grupperingen även i svaret, så blir det mycket tydligare:
select Placering, avg(Lön)
from Arbetare
group by Placering;
Placering | avg(Lön) |
10 | 1 500 |
30 | 1 000 |
where
-villkor, som i så fall görs först, före aggregatfunktionen:
select Placering, avg(Lön)
from Arbetare
where Anamn = 'Bob' or Anamn = 'Sam'
group by Placering;
Placering | avg(Lön) |
10 | 1 000 |
30 | 1 000 |
where
-villkoret:
select *
from Arbetare
where Anamn = 'Bob' or Anamn = 'Sam';
Anr | Anamn | Lön | Placering |
1 | Bob | 1 000 | 10 |
3 | Sam | 1 000 | 30 |
och sen beräknas aggregatfunktionen på resultatet från den frågan.
Genom att använda nyckelordethaving
kan man ta med bara vissa av grupperna i svaret:
select Placering, avg(Lön)
from Arbetare
group by Placering;
Placering | avg(Lön) |
10 | 1500 |
30 | 1 000 |
select Placering, avg(Lön)
from Arbetare
group by Placering
having avg(Lön) > 1000;
Placering | avg(Lön) |
10 | 1500 |
where, group by
och having
i samma fråga:
select Placering, avg(Lön)
from Arbetare
169
where Anamn = 'Bob' or Anamn = 'Sam'
group by Placering
having avg(Lön) > 1000;
Resultatet blev en tom tabell, för ingen av grupperna hade mer än 1 000 i genomsnittslön:
select Placering, avg(Lön)
from Arbetare
where Anamn = 'Bob' or Anamn = 'Sam'
group by Placering;
Placering | avg(Lön) |
10 | 1000 |
30 | 1 000 |
where
-villkoret som "väljer ut rader", sen aggregatfunktionerna (eventuellt uppdelade i grupper med group by
), sist
having
-villkoret som "väljer ut grupper"
Vi kan ha fler än en tabell i den där frågan som körs först:
select *
from Arbetare, Kontor
where Placering = Knr;
Anr | Anamn | Lön | Placering | Knr | Knamn |
1 | Bob | 1000 | 10 | 10 | Gnesta |
2 | Liz | 2000 | 10 | 10 | Gnesta |
3 | Sam | 1000 | 30 | 30 | Pyongyang |
select Knamn, avg(Lön)
from Arbetare, Kontor
where Placering = Knr
group by Knamn;
Knamn | avg(Lön) |
Gnesta | 1500 |
Pyongyang | 1000 |
select * from ... where
..." först, och sen kör man aggregatfunktionerna.
Antag att vi vill räkna ut lönesumman för varje kontor:
select Knamn, sum(Lön)
from Arbetare, Kontor
where Placering = Knr
group by Knamn;
Knamn | sum(Lön) |
Gnesta | 3 000 |
Pyongyang | 1 000 |
Problem: Moskva försvann. (Ingen jobbar i Moskva, men vi har ett kontor där.) Det kan vara ännu värre om man vill ha en lista över hur många arbetare som finns på varje kontor:
select Knamn, count(*)
from Arbetare, Kontor
where Placering = Knr
group by Knamn;
Knamn | count(*) |
Gnesta | 2 |
Pyongyang | 1 |
Hur ska man få med Moskva? För att så småningom kunna göra det, måste vi först studera explicita joinar.
Vi har tidigare sett hur man kan koppla ihop de två tabellerna, så man ser vem som jobbar var:
select *
from Arbetare, Kontor
where Placering = Knr;
Anr | Anamn | Lön | Placering | Knr | Knamn |
1 | Bob | 1000 | 10 | 10 | Gnesta |
2 | Liz | 2000 | 10 | 10 | Gnesta |
3 | Sam | 1000 | 30 | 30 | Pyongyang |
Om vi för ett ögonblick ska prata relationsalgebra i stället för SQL, så har vi kopplat ihop de två tabellerna med en join-operation, under 171 villkoret Placering = Knr. Om man skriver joinen av de två tabellerna med ett relationsalgebrauttryck, ser det ut så här:
Arbetare P lacering=Knr Kontor
En join av den här typen brukar kallas "vanlig" join eller inre join. Det finns nämligen också något som kallas yttre join, som vi kommer att ta upp senare i det här kapitlet.
Så här skulle man kunna rita upp det:
SQL har utökats med relationsalgebraoperatorn join som ett alternativt sätt att formulera frågor som kombinerar tabeller. Det här kan man skriva med en explicit join i from-listan:
select *
from (Arbetare join Kontor on Placering = Knr);
select *
from Arbetare, Kontor
where Placering = Knr and Knamn = 'Gnesta';
Anr | Anamn | Lön | Placering | Knr | Knamn |
1 | Bob | 1000 | 10 | 10 | Gnesta |
2 | Liz | 2000 | 10 | 10 | Gnesta |
Alternativt, med en explicit join:
172
select *
from (Arbetare join Kontor on Placering = Knr)
where Knamn = 'Gnesta';
Anr | Anamn | Lön | Placering | Knr | Knamn |
1 | Bob | 1000 | 10 | 10 | Gnesta |
2 | Liz | 2000 | 10 | 10 | Gnesta |
Man kan se det som att först skapas "tabellerna i from-listan", genom att explicita joinar beräknas, och sen körs frågan. 4
Notera att när vi joinar två tabeller, vare sig implicit,
select *
from Arbetare, Kontor
where Placering = Knr;
select *
from (Arbetare join Kontor on Placering = Knr);
så försvinner de rader som inte går att koppla ihop med en annan rad: kontoret i Moskva försvinner.
I en vanlig join försvinner alla rader som inte går att koppla ihop med en eller flera rader i den andra tabellen. I en yttre join behåller man dem. Eftersom det inte finns någon rad att koppla ihop dem med, fyller man på med null-värden.
select *
from (Arbetare right outer join Kontor
on Placering = Knr);
Anr | Anamn | Lön | Placering | Knr | Knamn |
1 | Bob | 1000 | 10 | 10 | Gnesta |
2 | Liz | 2000 | 10 | 10 | Gnesta |
null | null | null | null | 20 | Moskva |
3 | Sam | 1000 | 30 | 30 | Pyongyang |
En höger-ytter-join (engelska: right outer join) behåller alla rader i den högra tabellen.
Dessutom finns vänster-ytter-join (engelska: left outer join), som behåller alla rader i den vänstra tabellen, och full yttre join (engelska: full outer join), som behåller alla rader i båda tabellerna.
De har tre olika relationsalgebrasymboler, som skapas genom att ta den vanliga fjärilsliknande join-symbolen (⋈) och lägga på ett par streck på vänster sida för vänster-ytter-join (), på höger sida för höger-ytter-join (
) eller på båda sidorna för full yttre join (
).
Arbetare
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x
Placering=Knr
Kontor
select avg(Lön)
from (Arbetare right outer join Kontor
on Placering = Knr);
avg(Lön) |
1 333 |
Men borde det inte bli 1 000? Summan av lönerna är 1 000 + 2 000 + 1 000, dvs 4 000, och det finns fyra rader? Nej, null är inte 0, utan betyder att det inte är något värde alls. Null-värden ignoreras vid beräkningen av aggregatfunktioner, som om den raden inte alls hade varit med.
select Knamn, avg(Lön)
from (Arbetare right outer join Kontor
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Placering = Knr)
group by Knamn;
Knamn | avg(Lön) |
---|---|
Gnesta | 1 500 |
Moskva | null |
Pyongyang | 1 000 |
Man kan ha flera aggregatfunktioner i samma resultat:
select Knamn, avg(Lön), sum(Lön)
174
from (Arbetare right outer join Kontor
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Placering = Knr)
group by Knamn;
Knamn | avg(Lön) | sum(Lön) |
---|---|---|
Gnesta | 1 500 | 3 000 |
Moskva | null | null |
Pyongyang | 1 000 | 1 000 |
coalesce
5
för att byta ut null-värden mot 0:
select Knamn, avg(Lön), coalesce(sum(Lön), 0)
from (Arbetare right outer join Kontor
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Placering = Knr)
group by Knamn;
Knamn | avg(Lön) | sum(Lön) |
---|---|---|
Gnesta | 1 500 | 3 000 |
Moskva | null | 0 |
Pyongyang | 1 000 | 1 000 |
Vi studerar återigen lokalkontoren och deras arbetare:
select *
from (Arbetare right outer join Kontor
on Placering = Knr);
Anr | Anamn | Lön | Placering | Knr | Knamn |
---|---|---|---|---|---|
1 | Bob | 1 000 | 10 | 10 | Gnesta |
2 | Liz | 2 000 | 10 | 10 | Gnesta |
null | null | null | null | 20 | Moskva |
3 | Sam | 1 000 | 30 | 30 | Pyongyang |
count
kan man räkna antingen rader eller värden. Count(*)
räknar rader:
select Knamn, count(*)
from (Arbetare right outer join Kontor
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Placering = Knr)
group by Knamn;
Knamn | count(*) |
---|---|
Gnesta | 2 |
Moskva | 1 |
Pyongyang | 1 |
Count(X)
räknar antalet värden i kolumnen X. Null-värden räknas inte! Det betyder att det kan bli olika resultat av räknandet, beroende på vilken kolumn man räknar:
select Knamn, count(Knr), count(Anr)
from (Arbetare right outer join Kontor
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Placering = Knr)
group by Knamn;
Knamn | count(Knr) | count(Anr) |
---|---|---|
Gnesta | 2 | 2 |
Moskva | 1 | 0 |
Pyongyang | 1 | 1 |
Vi har också projekt som de anställda kan jobba på:
Projekt | |
---|---|
Pnr | Pnamn |
100 | Apollo |
200 | Manhattan |
300 | Zork |
Varje arbetare kan jobba på flera olika projekt, och flera arbetare kan jobba på varje projekt. Det finns alltså ett många-till-mångasamband mellan arbetarna och projekten, och det uttrycks med en tabell:
176Jobbar | |
---|---|
Arbetare | Projekt |
1 | 100 |
1 | 200 |
2 | 200 |
SQL-kod för att skapa de här två nya tabellerna, och fylla dem med exempeldata: 6
create table Projekt
(Pnr integer,
Pnamn varchar(10),
primary key (Pnr));
insert into Projekt (Pnr, Pnamn)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvalues (100, 'Apollo');
insert into Projekt (Pnr, Pnamn)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvalues (200, 'Manhattan');
insert into Projekt (Pnr, Pnamn)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvalues (300, 'Zork');
create table Jobbar
(Arbetare integer,
Projekt integer,
primary key (Arbetare, Projekt),
foreign key (Arbetare) references Arbetare(Anr),
foreign key (Projekt) references Projekt(Pnr));
insert into Jobbar (Arbetare, Projekt)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvalues (1, 100);
insert into Jobbar (Arbetare, Projekt)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvalues (1, 200);
insert into Jobbar (Arbetare, Projekt)
values (2, 200);
select *
from Arbetare, Jobbar, Projekt
where Anr = Arbetare and Projekt = Pnr;
177
Anr | Anamn | Lön | Placering | Arbetare | Projekt | Pnr | Pnamn |
---|---|---|---|---|---|---|---|
1 | Bob | 1 000 | 10 | 1 | 100 | 100 | Apollo |
1 | Bob | 1 000 | 10 | 1 | 200 | 200 | Manhattan |
2 | Liz | 2 000 | 10 | 2 | 200 | 200 | Manhattan |
Bob jobbar alltså på Apollo-projektet och på Manhattan-projektet. Liz jobbar också på Manhattan-projektet. Sam jobbar ingenstans. Ingen jobbar på Zork-projektet.
Frågan innehåller implicit två (vanliga, inre) joinar, som kopplar ihop de tre tabellerna:
Här är tre olika frågor som kan göra samma sak, men med explicita joinar:
select *
from (Arbetare join Jobbar on Anr = Arbetare)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xjoin Projekt on Projekt = Pnr;
select *
from Arbetare join
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x(Jobbar join Projekt on Projekt = Pnr)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Anr = Arbetare;
select *
from Arbetare join Jobbar on Anr = Arbetare
join Projekt on Projekt = Pnr;
Man kan kombinera, och ha en explicit och en implicit join:
select *
from (Arbetare join Jobbar on Anr = Arbetare), Projekt
where Projekt = Pnr;
select *
from Arbetare, (Jobbar join Projekt on Projekt = Pnr)
where Anr = Arbetare;
Men man får se upp när man har en yttre join. Låt oss säga att jag vill ha med alla arbetarna i resultatet, även dem som inte arbetar på något projekt (dvs Sam), med null-värden i de övriga kolumnerna:
178Anr | Anamn | Lön | Placering | Arbetare | Projekt | Pnr | Pnamn |
---|---|---|---|---|---|---|---|
1 | Bob | 1 000 | 10 | 1 | 100 | 100 | Apollo |
1 | Bob | 1 000 | 10 | 1 | 200 | 200 | Manhattan |
2 | Liz | 2 000 | 10 | 2 | 200 | 200 | Manhattan |
3 | Sam | 1 000 | 30 | null | null | null | null |
select *
from (Arbetare left outer join Jobbar
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Anr = Arbetare),
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xProjekt
where Projekt = Pnr;
Anr | Anamn | Lön | Placering | Arbetare | Projekt | Pnr | Pnamn |
---|---|---|---|---|---|---|---|
1 | Bob | 1 000 | 10 | 1 | 100 | 100 | Apollo |
1 | Bob | 1 000 | 10 | 1 | 200 | 200 | Manhattan |
2 | Liz | 2 000 | 10 | 2 | 200 | 200 | Manhattan |
Ingen Sam! Som vanligt kan man byta ut en implicit join mot en explicit, så frågan är ekvivalent med
select *
from (Arbetare left outer join Jobbar
on Anr = Arbetare)
join Projekt on Projekt = Pnr;
Dvs, först räknar vi ut den yttre joinen:
select * from (Arbetare left outer join Jobbar
on Anr = Arbetare);
Anr | Anamn | Lön | Placering | Arbetare | Projekt |
---|---|---|---|---|---|
1 | Bob | 1 000 | 10 | 1 | 100 |
1 | Bob | 1 000 | 10 | 1 | 200 |
2 | Liz | 2 000 | 10 | 2 | 200 |
3 | Sam | 1 000 | 30 | null | null |
Sen joinar vi den (med en vanlig inre join) med tabellen Projekt:
Projekt | |
---|---|
Pnr | Pnamn |
100 | Apollo |
200 | Manhattan |
300 | Zork |
Projekt = Pnr
, men kolumnen Projekt är null i resultatet av den yttre joinen. Det finns inga rader i tabellen Projekt som matchar.
179
(Det skulle inte göra det, även om det fanns en rad där Pnr är null, för null = null
är falskt i SQL. Värre ändå: null <> null
är också falskt i SQL. Kom ihåg: null är inte ett värde. Alla jämförelser med null, utom den särskilda is null
, ger svaret falskt.)
Prova med en yttre join med tabellen Projekt:
select *
from (Arbetare left outer join Jobbar
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Anr = Arbetare)
left outer join Projekt on Projekt = Pnr;
Anr | Anamn | Lön | Placering | Arbetare | Projekt | Pnr | Pnamn |
---|---|---|---|---|---|---|---|
1 | Bob | 1 000 | 10 | 1 | 100 | 100 | Apollo |
1 | Bob | 1 000 | 10 | 1 | 200 | 200 | Manhattan |
2 | Liz | 2 000 | 10 | 2 | 200 | 200 | Manhattan |
3 | Sam | 1 000 | 30 | null | null | null | null |
select *
from Arbetare left outer join
(Jobbar join Projekt on Projekt = Pnr)
on Arbetare = Anr;
Krångligt? Ja, det tycker en hel del databashanterare också, och gör fel. 7
Vyer är ofta bra för att förenkla krångliga frågor. Med vyer kan man dela upp frågan i flera steg.
create view ArbetareMedProjekt as
select * from Jobbar join Projekt on Projekt = Pnr;
select * from ArbetareMedProjekt;
180
Arbetare | Projekt | Pnr | Pnamn |
---|---|---|---|
1 | 100 | 100 | Apollo |
1 | 200 | 200 | Manhattan |
2 | 200 | 200 | Manhattan |
select *
from Arbetare left outer join ArbetareMedProjekt
on Anr = Arbetare;
Anr | Anamn | Lön | Placering | Arbetare | Projekt | Pnr | Pnamn |
---|---|---|---|---|---|---|---|
1 | Bob | 1 000 | 10 | 1 | 100 | 100 | Apollo |
1 | Bob | 1 000 | 10 | 1 | 200 | 200 | Manhattan |
2 | Liz | 2 000 | 10 | 2 | 200 | 200 | Manhattan |
3 | Sam | 1 000 | 30 | null | null | null | null |
Vem har högst lön? Det är en förhållandevis enkel fråga att skriva:
select *
from Arbetare
where Lön = (select max(Lön) from Arbetare);
Anr | Anamn | Lön | Placering |
---|---|---|---|
2 | Liz | 2 000 | 10 |
max
-funktionen på. Vi måste ju först räkna antalet per kontor:
select Knamn, count(Anr)
from (Arbetare right outer join Kontor
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Placering = Knr)
group by Knamn;
Knamn | count(Anr) |
---|---|
Gnesta | 2 |
Moskva | 0 |
Pyongyang | 1 |
create view Kontorspersonal as
select Knamn, count(Anr)
from (Arbetare right outer join Kontor
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Placering = Knr)
group by Knamn;
Fel: "count(Anr)" är inte ett tillåtet kolumnnamn. Bättre:
create view Kontorspersonal as
181
select Knamn, count(Anr) as Antal
from (Arbetare right outer join Kontor
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Placering = Knr)
group by Knamn;
select * from Kontorspersonal;
Knamn | Antal |
---|---|
Gnesta | 2 |
Moskva | 0 |
Pyongyang | 1 |
Man kan också ange namnen på kolumnerna i vyn på det här sättet:
create view Kontorspersonal(Plats, Antal) as
select Knamn, count(Anr)
from (Arbetare right outer join Kontor
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Placering = Knr)
group by Knamn;
select * from Kontorspersonal;
Plats | Antal |
---|---|
Gnesta | 2 |
Moskva | 0 |
Pyongyang | 1 |
select *
from Kontorspersonal
where Antal = (select max(Antal)
from Kontorspersonal);
Knamn | Antal |
---|---|
Gnesta | 2 |
I en del SQL-dialekter kan man definiera vyer i from-listan, som bara gäller i just den frågan.
SQL-standarden (men inte alla databashanterare) innehåller så kallade Common Table Expressions, eller CTE:er, som är fråge-lokala 182 vyer, dvsvyer som man definierar inuti en SQL-fråga och som bara är tillgängliga i den frågan. Om man vill förenkla en SQL-fråga genom att dela upp den i flera steg med hjälp av en eller flera vyer, och de vyerna inte gör någon nytta annat än just för den frågan, vill man kanske inte skräpa ner databasen med de vyerna. Då är CTE:er bättre.
with Kontorspersonal as
(select Knamn as Plats, count(Anr) as Antal
from (Arbetare right outer join Kontor
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Placering = Knr)
group by Knamn)
select *
from Kontorspersonal
where Antal = (select max(Antal)
from Kontorspersonal);
a1
och a2
, som båda refererar till tabellen Arbetare:
8
select a1.Anamn, a1.Lön
from Arbetare as a1, Arbetare as a2
where a1.Lön = a2.Lön;
Anamn | Lön |
---|---|
Bob | 1 000 |
Bob | 1 000 |
Liz | 2 000 |
Sam | 1 000 |
Sam | 1 000 |
Alla, varav en del två gånger? Jo, a1 och a2 kan "peka" på samma rad. Om vi har med även data från a2-raden syns det tydligare:
183
select a1.Anamn, a1.Lön, a2.Anamn, a2.Lön
from Arbetare as a1, Arbetare as a2
where a1.Lön = a2.Lön;
Anamn | Lön | Anamn | Lön |
---|---|---|---|
Bob | 1 000 | Bob | 1 000 |
Bob | 1 000 | Sam | 1 000 |
Liz | 2 000 | Liz | 2 000 |
Sam | 1 000 | Bob | 1 000 |
Sam | 1 000 | Sam | 1 000 |
Vi måste ändra frågan så att vi inte längre får med kombinationerna av en person med sig själv:
select a1.Anamn, a1.Lön
from Arbetare as a1, Arbetare as a2
where a1.Lön = a2.Lön
and a1.Anr <> a2.Anr;
Anamn | Lön |
---|---|
Bob | 1 000 |
Sam | 1 000 |
null
i SQL lite besvärligt att hantera. Även om vi ofta talar om "nullvärden" är det egentligen inte ett värde, utan betyder att "rutan är tom". Det kan i sin tur betyda olika saker: att saken i fråga inte finns ("den medlemmen har ingen telefon"), att den finns, men värdet är okänt ("hon har telefon, men vi vet inte numret"), eller att den uppgiften inte är tillämplig. Dessutom kan nullvärden dyka upp i resultat av SQL-frågor, till exempel yttre join. Dessa olika betydelser kan orsaka förvirring.
Dessutom har null
särskilda egenskaper i SQL. Alla jämförelser med null blir falska. Exempelvis skulle man kunna tro att följande fråga skulle ge oss alla raderna i tabellen Medlemmar:
select * from Medlemmar
where Telefon = null or Telefon <> null;
I stället ger frågan inga rader alls i resultatet, eftersom alla jämförelser med null ger resultatet falskt. Null är varken lika med eller
184
skilt ifrån null! För att jämföra med null måste man använda is null
, och följande fråga ger alla raderna:
select * from Medlemmar
where Telefon is null or Telefon is not null;
(Konstruktionen Telefon is not null
är bara ett alternativt sätt att skriva not Telefon is null
.)
Här är ett lite längre exempel på oväntade resultat som man kan få när det är nullvärden inblandade. Antag att vi har en tabell med Kloka personer, och en med Vackra:
Kloka | |
---|---|
ID | Namn |
1 | Anna |
2 | Bertil |
3 | Cecilia |
Som vi ser är Anna både klok och vacker, medan Bertil och Cecilia visserligen är kloka, men inte vackra. Vi kan söka fram de kloka och vackra med SQL:
select * from Kloka
where Namn in (select Namn from Vackra);
ID | Namn |
---|---|
1 | Anna |
Vi kan också söka fram dem som är kloka, men inte vackra:
select * from Kloka
where Namn not in (select Namn from Vackra);
ID | Namn |
---|---|
2 | Bertil |
3 | Cecilia |
Vad händer nu om vi lägger till ett nullvärde i tabellen Vackra?
insert into Vackra (ID, Namn) values (3, null);
185
Kloka | |
---|---|
ID | Namn |
1 | Anna |
2 | Bertil |
3 | Cecilia |
select * from Kloka
where Namn in (select Namn from Vackra);
ID | Namn |
---|---|
1 | Anna |
select * from Kloka
where Namn not in (select Namn from Vackra);
ID | Namn |
Nu blev resultatet tomt! Eftersom det finns ett nullvärde i kolumnen Namn i tabellen Vackra, anser SQL alltså att det inte finns några personer som är kloka men inte vackra!
Vill man ha en förklaring som känns logisk kan man tänka så här: Null betyder att det saknas ett namn i tabellen. Det finns alltså en vacker person som antingen inte har något namn, eller vars namn vi inte vet, eller så vet vi inte vilken person det är. Om det är en person som vi inte vet vilken det är, skulle det mycket väl kunna vara någon av Bertil eller Cecilia. Därför kan vi inte veta att vare sig Bertil eller Cecilia inte är vackra, och vi kan inte ta med dem i resultatet med personer som är kloka men inte vackra.
Det kan vara ett gott råd att försöka undvika nullvärden i sina databaser. Man kan ange integritetsvillkoret not null på kolumnerna när man skapar tabeller, och man kan konstruera sin databas på ett sätt som gör att nullvärden undviks (se kapitel 6). En del går så långt att de i stället för null inför särskilda värden i databasen för att markera att värdet saknas, till exempel ett telefonnummer 186 000000 eller TELEFON-SAKNAS. Det ger dock andra problem, så om man verkligen behöver nullvärden ska man inte vara rädd för att använda dem.
select
-sats. Ett exempel är att stega sig igenom en hierarki. När teoretikerna pratar brukar detta kallas transitivt hölje (på engelska transitive closure, eller oftare recursive closure).
Anställda | ||||
---|---|---|---|---|
Nummer | Namn | Lön | Chef | Avdelning |
2 | Stina | 30 000 | null | H |
3 | Sam | 22 000 | 2 | S |
4 | Lotta | 28 000 | 2 | H |
1 | Olle | 20 000 | 3 | S |
8 | Maria | 25 000 | 4 | C |
9 | Ulrik | 26 000 | 8 | C |
10 | Petter | 22 000 | 8 | C |
De anställda utgör en hierarki. Vi kan se att anställd nummer 2, som heter Stina, verkar vara högsta chef i företaget. Hon har två underställda, Sam och Lotta, som i sin tur har underställda, och så vidare.
select boss.Nummer, boss.Namn
from Anställda as proletär, Anställda as boss
where proletär.Nummer = 10
and proletär.Chef = boss.Nummer;
Nummer | Namn |
---|---|
8 | Maria |
Man kan också gå två nivåer upp i hierarkin, och hitta den anställdes chefs chef:
select överboss.Nummer, överboss.Namn
187
from Anställda as proletär,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xAnställda as mellanboss,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xAnställda as överboss
where proletär.Nummer = 10
and proletär.Chef = mellanboss.Nummer
and mellanboss.Chef = överboss.Nummer;
Nummer | Namn |
---|---|
4 | Lotta |
select boss.Nummer, boss.Namn
from Anställda as proletär, Anställda as boss
where proletär.Nummer = 10
and proletär.Chef = boss.Nummer
union
select överboss.Nummer, överboss.Namn
from Anställda as proletär,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xAnställda as mellanboss,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xAnställda as överboss
where proletär.Nummer = 10
and proletär.Chef = mellanboss.Nummer
and mellanboss.Chef = överboss.Nummer;
Nummer | Namn |
---|---|
4 | Lotta |
8 | Maria |
select
-sats. Inte heller den enklare operationen, att bara få fram den högsta chefen, går alltid att göra med en select
-sats. Vi skulle nämligen behöva någon form av repetition eller rekursion, som kan stega sig uppåt i hierarkin godtyckligt många nivåer, ända tills vi når den högsta chefen, och det finns inte i SQL.
Från SQL:1999 finns det i SQL-standarden en mekanism med rekursiva frågor i form av en with recursive
klausul i select
-satsen
188
som kan användas för att beräkna transitivt hölje. Den är implementerad i de flesta moderna relationsdatabaser, inklusive Oracle, SQL Server, Db2, MariaDB och PostgreSQL, men inte i till exempel MySQL och Mimer. Så här uttrycker man en rekursiv fråga som finner högsta chefen för alla anställda:
with recursive BigBoss(Nummer, Superboss) as(
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect a.Nummer, a.Nummer
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Anställda a
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere a.Chef is null
union
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect a.Nummer, b.Superboss
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Anställda a, BigBoss b
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere a.chef = b.Nummer
)
select a.Nummer, a.Namn, b.Superboss
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Anställda a, BigBoss b
where a.Nummer = b.Nummer;
Nummer | Namn | Superboss |
2 | Stina | 2 |
3 | Sam | 2 |
4 | Lotta | 2 |
1 | Olle | 2 |
8 | Maria | 2 |
9 | Ulrik | 2 |
10 | Petter | 2 |
Superboss
och är rekursiv eftersom Superboss
refererar till sig själv i sin select
sats.
Det är lätt att modifiera den rekursiva frågan för att hitta alla som har Stina som superboss:
with recursive BigBoss(Nummer, Superboss) as(
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect a.Nummer, a.Nummer
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Anställda a
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere a.Chef is null
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xunion
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect a.Nummer, b.Superboss
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Anställda a, BigBoss b
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere a.chef = b.Nummer
)
189
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect a.Nummer, a.Namn
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Anställda a, BigBoss b
where a.Nummer = b.Nummer and b.Superboss=2;
Nummer | Namn |
2 | Stina |
3 | Sam |
4 | Lotta |
1 | Olle |
8 | Maria |
9 | Ulrik |
10 | Petter |
En finess med att uttrycka transitiva höljen m.h.a. rekursiva frågor är att cirkulära beroenden upptäcks automatiskt i många databashanterare. Om vi till exempel gör Petter till Marias chef 9 i PostgreSQL blir det ett sådan cirkulärt beroende eftersom Maria också är Petters chef. Svaret på frågan blir då:
Nummer | Namn |
2 | Stina |
3 | Sam |
4 | Lotta |
1 | Olle |
with recursive
. Det kallas för linjär rekursion. Det har visat sig att linjär rekursion är tillräckligt i många fall. Om man inte kan uttrycka sin fråga som linjär rekursion, eller om databashanteraren inte har with recursive
, kan man implementera transitivt hölje som en lagrad funktion, vilket beskrivs i avsnitt 15.5.
Sammanfattningsvis är ett rekursivt hölje (engelska: transitive closure, men ofta recursive closure) en sambandstyp i en databas som är rekursiv, till exempel att anställda är chefer för varandra, som gör kan man skapa djupa hierarkier eller långa kedjor. En anställd kan vara chef för en annan anställd, som i sin tur kan vara chef för ytterligare en anställd, och så vidare. Operationen att stega sig igenom en sådan hierarki eller kedja, och (i det här exemplet) ta 190 fram alla chefer som en viss anställd har över sig, och inte bara den närmaste chefen, kallas transitivt hölje.
grant
och revoke
, som används för att reglera vilka användare som får göra vad med databasen. Dessa tas upp i kapitel 14, Säkerhet i databaser. Triggers tas upp i kapitel 16, Aktiva databaser och triggers, och SQL-kommunikation med en databas från ett program tas upp i kapitel 21, SQL inuti ett program.
SQL:1999 var en mycket större standard än de tidigare (ca 2 200 sidor, jämfört med 600 sidor för SQL-92), och den innehöll många nyheter. De senare standarderna, SQL:2003 och framåt till (när detta skrivs) SQL:2016, bygger vidare på SQL:1999, och innebär inte en lika stor förändring som från SQL-92 till SQL:1999.
Bland nyheterna i SQL:1999 finns stöd för att definiera mer avancerade datatyper, och för att bättre kunna använda klasser och klasshierarkier i relationsdatabaser. (Just den finessen är det dock inte så många som använder.)
Man bör komma ihåg att det snarare är så att standarden följer databashanterarna, än att databashanterarna följer standarden. Att en mekanism kommit med i en ny version av standarden betyder därför inte att den inte funnits i flera olika databashanterare tidigare, men kanske med lite varierande syntax och beteende. Att en mekanism kommer med i en ny version av standarden betyder inte heller att den kommer att finnas tillgänglig i alla databashanterare, eller att den fungerar likadant i alla databashanterare som har den.
191Nytt i SQL:1999: Datadefinition
Det finns flera nyheter som berör datatyper i SQL:1999.
create table like
, som gör att man kan skapa en ny tabell med samma schema som en existerande tabell.
boolean, BLOB
(dvs binary large object) som kan användas för att lagra till exempel bilder och filmsekvenser, CLOB
(dvs character binary large object) som är en BLOB som innehåller text, arrayer, och radtypen row
som är en sorts poster som kan innehålla flera olika fält.
create domain
för att skapa och sedan använda datatypen temperatur:
create domain temperature as int
default 0
check (value >= -273);
create table Temperatures
(ID int primary key,
t temperature);
insert into Temperatures (ID, t) values (1, 17);
insert into Temperatures (ID, t) values (2, -317);
När vi försöker lägga in temperaturen på -317 grader får vi ett felmeddelande:
Mimer SQL error -10102 in function EXECUTE
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xDomain constraint
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdbtek0.SQL_DOMAIN_CHECK_0000064492 violated
for table dbtek0.Temperatures, column t
• Hierarkiskt ordnade tabeller, med undertabeller och supertabeller ("subtables" och "supertables"). Varje rad i en subtabell räknas också som om den var med i supertabellen. Det här kan 192 användas för att representera klasshierarkier i en relationsdatabas.
Nytt i SQL:1999: Predikat, operatorer och kvantifierare
Man har lagt till flera praktiska finesser. Här är ett par exempel:
similar to.
group by
, som beräknar aggregatfunktioner för grupper (till exempel medellönen för de anställda på varje kontor) har utökats med flera nya och mer avancerade grupperingsfunktioner, som är särskilt användbara i datalager (se kapitel 18): group by grouping sets
, som gör att man kan gruppera på flera olika sätt i samma fråga, group by rollup
, som gör att man kan gruppera i flera nivåer, och group by cube
, som grupperar enligt alla kombinationer av de attribut man anger.
create recursive view
, gör att man kan hantera transitivt hölje i SQL.
Många databashanterare har länge haft aktiva regler, eller triggers som de brukar kallas (se kapitel 16, Aktiva databaser och triggers), men med helt olika sätt att skriva dem. I SQL:1999 kom ett standardiserat sätt för hur reglerna ska skrivas och hur de ska fungera.
Nytt i SQL:1999: SQL/PSM, lagrade procedurer
Många databashanterare har länge haft mekanismer för att skriva funktioner och procedurer i ett språk som består av SQL utökat med kontrollstrukturer som loopar och val (se kapitel 15, Lagrade procedurer), men med olika skrivsätt och olika funktion. I SQL:1999 kom ett standardiserat sätt. I standarden kallas de persistent stored modules (PSM), alltså ungefär "moduler som lagras permanent i databasen".
193Nytt i SQL:1999: SQL/CLI, ett API för SQL-anrop från program
SQL:1999 definierar ett nytt gränssnitt för hur datorprogram i C eller C++ kan köra SQL-frågor mot en databas. Det fanns sedan tidigare en standard för ESQL (Embedded SQL), men SQL/CLI påminner mer om ODBC. (I själva verket är det nästan precis likadant som ODBC.) Standarden kallar det för call-level interface (CLI).
Nytt i SQL:1999: Transaktioner
Transaktioner har funnits länge i databaser, men i SQL:1999 standardiserade man flera nya kommandon, till exempelsavepoint
och rollback to savepoint
.
De flesta databashanterare har länge haft inbyggda funktioner för att styra användarnas rättigheter till innehållet i databasen, främst med kommandona grant
och revoke
. SQL:1999 definierar dessutom roller. Med kommandot create role
kan man skapa en roll, till exempel sekreterare, som ger ett paket av rättigheter som man kan dela ut till de användare som är sekreterare.
Utgå från övningen med webbsöktjänsten Gurgel på sidan 155, och formulera följande frågor i SQL:
Definiera gärna vyer om det underlättar, men skapa inte nya tabeller.
5 Det engelska ordet coalesce betyder ungefär att växa ihop eller gemensamt sluta sig samman. I SQL kan man tänka sig att flera värden "sluter sig samman" till ett enda värde.
7 De databashanterare som gjorde fel när vi provkörde detta inför första upplagan av den här boken, MySQL och Mimer, har rättat felet i senare versioner. PostgreSQL och Microsoft SQL Server har gjort rätt hela tiden.
update Anställda set Chef=10 where Nummer=8;
Det här kapitlet är en sammanfattning av SQL, där man kan slå upp skrivsättet för de olika kommandona. För att lära sig SQL är det bäst att läsa kapitel 7 och kapitel 8.
Databashanterare har sina egna SQL-dialekter, snarare än att de följer standarden exakt. Därför har vi visserligen följt standarden, men vi har begränsat oss till de delar som är vanligast i verkliga system. Om man behöver veta detaljerna för en viss databashanterare, måste man läsa manualen för just den.
När vi anger skrivsättet för olika SQL-kommandon följer vi det vanliga sättet att beskriva grammatiken, eller syntaxen, för ett språk i datorsammanhang genom att ange regler. Några exempel:
→ god morgon
betyder att en hälsning alltid skrivs "god morgon".
→ god
tidsspecifikation betyder att en hälsning alltid börjar med "god", och sen får man slå upp i regeln för "tidsspecifikation" för att se vad det är.
→ morgon middag afton
betyder att en tidsspecifikation är antingen "morgon", "middag" eller "afton".
Tillsammans med regeln ovan betyder det att en hälsning är antingen "god morgon", "god middag" eller "god afton".
196
→ god jul [ och gott nytt år ]
betyder att en julhälsning alltid börjar med "god jul", men sen är det valfritt att lägga till "och gott nytt år".
→ hej
[ namn [, namn ] ... ] betyder att en hälsning börjar med "hej", och därefter kan man säga noll, ett eller flera namn, med komma mellan varje.
"..." betyder att det föregående ordet eller uttrycket kan upprepas noll, en eller flera gånger.
select
, i så kallade "frågor" eller "select
-frågor".
select [ distinct all ]
kolumner
from
tabeller
[ where
where-villkor ]
[ group by
kolumner
[ having
having-villkor ]]
[ order by
kolumner
[ asc desc
] ]
select-fråga
union [ distinct all ]
select-fråga
| select-fråga
except [ distinct all ]
select-fråga
| select-fråga
intersect [ distinct all ]
select-fråga
| tabellnamn . kolumnnamn [ as
namn ]
| uttryck [ as
namn ]
197
tabell [ inner ] join
tabell on join-villkor
| tabell
left [ outer ] join
tabell
on
join-villkor
| tabell
right [ outer ] join
tabell
on
join-villkor
•where-villkor kan innehålla jämförelser mellan uttryck bestående av kolumner och konstanter, grupperade med and
och or
, och även underfrågor, som utgörs av select-frågor satta inom parenteser. Även operatorerna in, not in, is null, exists, unique, any
(med sin synonym some) och all
kan användas.
•having-villkor innehåller jämförelser mellan uttryck bestående av kolumner, konstanter och aggregatfunktioner, grupperade med and
och or
, och även underfrågor, som utgörs av selectfrågor satta inom parenteser. De aggregatfunktioner som definieras av SQL-standarden är avg, count, max, min
och sum
, samt (inte lika vanligt) every, any
(med sin synonym some) och grouping
.
•tabellnamn kan vara ett enkelt namn (som Kund
) eller innehålla ett schemanamn (som kundbasschema.Kund
).
•join-villkor kan innehålla jämförelser mellan kolumner och konstanter, grupperade med and
och or
.
•uttryck kan innehålla kolumner, konstanter och aggregatfunktioner, kombinerade med vanliga operatorer som +
och -
.
select * from Personer;
SELECT * FROM personer;
select Namn, Bonus * 100 - 17 from sysadm.Personer;
198
select first_name || ' ' || last_name
from employee;
select Personer.Namn, Adress
from Personer, Avdelningar
where Arbetsplats = Avdelningar.Nummer
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xand Avdelningar.Namn like 'S%';
select "PERSONENS NAMN"
from "SÄRSKILDA PERSONER";
select p.Namn, Adress
from Personer as p, Avdelningar as a
where Arbetsplats = a.Nummer
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xand a.Namn like 'S%';
select Personer.Namn, Adress
from Personer inner join Avdelningar
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Arbetsplats = Avdelningar.Nummer
where Avdelningar.Namn like 'S%';
select Namn, Adress from Personer
where Arbetsplats in (select Nummer
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Avdelningar
where Namn like 'S%');
select sum(Bonus) as Bonussumma from Personer
where Arbetsplats = 8;
select Avdelningar.Namn, sum(Bonus), max(Bonus)
from Personer, Avdelningar
where Arbetsplats = Avdelningar.Nummer
group by Avdelningar.Namn
having avg(Bonus) > 1000
order by Avdelningar.Namn;
insert
används för att lägga in en eller flera nya rader i en tabell. Man kan antingen ange de värden som ska stå på raden, eller en SQL-fråga som körs och returnerar de rader som ska läggas in i tabellen.
Insert
kan inte användas för att hämta data från en fil. Något sådant kommando finns inte i SQL-standarden, men de flesta databashanterare har något eget kommando för att importera och exportera data i olika externa format.
insert into
tabellnamn
3
[ kolumn-lista ] values
värde-lista
| insert into
tabellnamn [ kolumn-lista ] sql-fråga
( kolumnnamn [, kolumnnamn ] ... )
insert into Personer (Nummer, Namn, Adress)
values (1, 'Kalle', 'Vägen 7');
insert into Personer
values (1, 'Kalle', null);
insert into Personer (Nummer, Namn, Adress)
select Id, Förnamn, Hemadress
from Nyinflyttade
where Status > 2;
delete
används för att ta bort noll, en eller flera rader ur en tabell.
delete from
tabellnamn
[ where
where-villkor
4
]
delete from Personer where Nummer = 8;
delete from Personer where Namn like 'L%';
delete from Personer;
delete from Personer
where Status < (select avg(Status) from Personer);
update
används för att ändra data på noll, en eller flera rader i en tabell.
update
tabellnamn
set
kolumn-värdes-lista
[ where
where-villkor
5
]
kolumnnamn = uttryck [, kolumnnamn = uttryck ] ...
201
update Personer set Adress = 'Gränden 5'
where Nummer = 1;
update Personer set Namn = 'Olle',
Adress = 'Gränden 5'
where Nummer = 4;
update Personer set Bonus = Bonus * 2;
update Personer
set Bonus = (select max(Bonus) from Faktura) * 2,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xAdress = 'Torget 1'
where Bonus > 100;
create table
används för att skapa nya tabeller.
create table
tabellnamn
6
( kolumndefinition [, kolumndefinition ] ... [, tabellvillkor ] ... )
namn datatyp [default
default-värde ][ kolumnvillkor ] ...
[ constraint
namn ] not null
|[ constraint
namn ] primary key
|[ constraint
namn ] unique
|[ constraint
namn ] references
tabellnamn ( kolumnnamn )
|[ constraint
namn ] check
villkor
[ constraint
namn ] primary-key-deklaration
|[ constraint
namn ] unique-deklaration
202
|[ constraint
namn ] foreign-key-deklaration
|[ constraint
namn ] tabell-check-villkor
primary key
( kolumnnamn [, kolumnnamn ] ... )
unique
( kolumnnamn [, kolumnnamn ] ... )
foreign key
( kolumnnamn [, kolumnnamn ] ... )
references
tabellnamn ( kolumnnamn [, kolumnnamn ] ... )
[ on delete
referensåtgärd on update referensåtgärd ]
no action
| cascade
| restrict
| set null
| set default
check
villkor
integer
| smallint
| numeric
[( totalt-antal-siffror [, antal-decimaler ])]
| decimal
[( totalt-antal-siffror [, antal-decimaler ])]
| float
[( antal-bitar-i-mantissan )]
| real
| double precision
| blob
[ ( max-antal-bytes )]
| character
[ ( antal-tecken )]
| varchar
( max-antal-tecken )
| clob
[ ( max-antal-tecken ) ]
| date
| time
| timestamp
[ ( antal-sekund-decimaler )]
En del av datatyperna har synonymer:
•Integer
kan förkortas till int
.
203
•Blob
kan även skrivas binary large object
.
•Clob
kan även skrivas character large object
.
•Character
kan förkortas till char
.
•Varchar
kan även skrivas character varying
eller char varying
.
create table Avdelningar
(Nummer integer not null,
Namn varchar(10) unique,
Adress varchar(10),
primary key(Nummer));
create table Personer
(Nummer integer not null primary key,
Namn varchar(10),
Lön integer default 10000 check (Lön > 0),
Chef integer default null
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreferences Personer(Nummer),
Arbetsplats integer not null
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xconstraint avd_ref
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreferences Avdelningar(Nummer));
create table Personer
(Nummer integer not null,
Namn varchar(10),
Lön integer default 10000,
Chef integer default null,
"Arbetar på" integer not null,
primary key(Nummer),
foreign key (Chef) references Personer(Nummer),
constraint avd_ref
foreign key ("Arbetar på")
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreferences Avdelningar(Nummer),
check (Chef <> Nummer));
create view
vynamn [ kolumn-lista ] as
select-fråga
•vynamn kan, precis som tabellnamn, vara antingen ett enkelt namn (som Kund) eller innehålla ett schemanamn (som kundbasschema.Kund
).
create view Datastudenter
as select Nummer, Namn
from Student
where Program = 'C' or Program = 'D';
create view AntalBilarPerKontor
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x(Kontorsnummer, Kontorsnamn, Antal)
as select Kontor.Nummer, Kontor.Namn, count(*)
from Bil, Kontor
where Bil.Placering = Kontor.Nummer
group by Kontor.Nummer, Kontor.Namn;
alter table
används för att ändra på definitionen för en tabell. Man kan lägga till och ta bort kolumner, och lägga till och ta bort integritetsvillkor.
205
alter table
tabellnamn förändring
add [ column
] kolumndefinition
8
| alter [ column
] kolumnnamn
set default
värde
| alter [ column
] kolumnnamn drop default
| drop [ column
] kolumnnamn [ restrict cascade ]
| add
tabellvillkor
9
| drop constraint
villkorsnamn [ restrict cascade
]
alter table Personer add Telefon varchar(20);
ALTER TABLE personer ADD telefon VARCHAR(20);
alter table Personer drop Telefon;
alter table Personer
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xadd foreign key (Chef) references Personer(Nummer);
alter table Personer
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xadd constraint avd_ref
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xforeign key (Arbetsplats)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreferences Avdelningar(Nummer);
alter table Personer drop constraint avd_ref;
create index
och ta bort dem med kommandot drop index.
create [ unique ] index
indexnamn
on
tabellnamn kolumn-lista
206
( kolumnnamn [ riktning ][, kolumnnamn [ riktning ] ] ... )
asc desc
create index Personnamnsindex on Personer (Namn);
CREATE INDEX personnamnsindex ON personer (namn);
create index Foo
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Projektdeltagare (Arbetare, Projekt);
create index Fum
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon Projektdeltagare (Arbetare asc,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xProjekt desc);
create unique index Apnamn on Apa (Namn);
Det finns flera sorters index och olika databashanterare kan hantera olika index. Den vanligaste sortens index är baserade på B-träd, och till exempel SQL Server och Mimer har bara B-trädsindex. Andra databashanterare ger databasadministratören möjlighet att välja mellan olika slags index, till exempel hash-index, R-trädsindex och bitmapsindex. Databasadministratören kan då välja vilket slags index hon vill lägga på en kolumn genom att ange dess sort i create index
. Till exempel i PostgreSQL kan man ge detta kommando för att skapa ett hash-index på kolumnen Projekt
i tabellen Projekt-deltagare:
create index Hindex on Projektdeltagare
using hash (Projekt);
drop
används för att ta bort ett databasobjekt, till exempel en tabell, en vy eller en lagrad procedur. Drop
tar bort objekt ur schemat, och inte data som enskilda rader i en tabell. (Det använder man kommandot delete
för.)
Man kan inte ta bort ett databasobjekt, om det finns något annat databasobjekt som refererar till det objektet. Till exempel kan man inte ta bort en tabell om det finns en trigger för den tabellen, eller en
207
lagrad procedur som använder sig av tabellen, eller om en annan tabell har en främmande nyckel som refererar till tabellen. Då måste man ta bort det refererande objektet först. I fallet främmande nyckel räcker det med att ta bort själva den främmande nyckeln med hjälp av kommandot alter table
.
Ovanstående är det beteende man får om man anger restrict
, vilket också är default-beteendet
10
i de flesta databashanterarna. Om man i stället anger cascade
, kommer både tabellen och de refererande objekten att tas bort, rekursivt så långt det behövs. Cascade
finns inte implementerat i alla databashanterare.
Om man tar bort en tabell, försvinner också de rader som fanns i den. Om man tar bort databasobjekt som inte innehåller några egna lagrade data, till exempel vyer, försvinner inga rader.
|drop index
indexnamn
12
| drop table
tabellnamn [ restrict cascade
]
| drop view
vynamn [ restrict cascade
]
drop table Avdelningar;
DROP TABLE avdelningar;
drop table Avdelningar cascade;
drop table Avdelningar restrict;
drop view Datastudenter;
drop view AntalBilarPerKontor cascade;
drop index Personnamnsindex;
Transaktioner har funnits länge i de flesta av de stora och kända relationsdatabashanterarna, och fanns med redan i SQL-standarden SQL-86. Transaktioner behandlas i kapitel 24 och 25.
Syntax för transaktionskommandon
•start-transaction-kommando 13 →start transaction
commit [ work
14
]
rollback [ work ]
•set transaction
→
set transaction
transaktionssätt [ transaktionssätt ] ...
read only
| read write
| isolation level read uncommitted
15
| isolation level read committed
| isolation level repeatable read
| isolation level serializable
16
Exempel på transaktionskommandon
start transaction;
START TRANSACTION;
set transaction
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xread only
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xisolation level read uncommitted;
set transaction
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xread write
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xisolation level serializable;
commit;
rollback;
grant
och revoke
används för att dela ut respektive återkalla rättigheter, exempelvis rättigheten att ändra innehållet i en tabell, och är den grundläggande säkerhetsmekanismen i SQL. Säkerhet behandlas i kapitel 14.
grant
rättigheter
to
användarlista [ with grant option
]
åtgärd [, åtgärd ] ... on [ table
] tabellnamn
| all privileges on [ table
] tabellnamn
delete
| select
| update
( kolumnnamn [, kolumnnamn ] ... )
| references
( kolumnnamn [, kolumnnamn ] ... )
revoke
rättigheter
from
användarlista [ restrict cascade
]
17
grant select on Anställda to svante;
GRANT SELECT on anställda TO svante;
revoke select on Anställda from svante;
grant update(Lön) on Anställda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xto svante, lotta, stina;
grant insert, delete on Anställda to svante;
create view DataAnställda
210
as select * from Anställda where avdelning = 'Data';
grant select on DataAnställda to kajsa;
grant insert, delete on Anställda to svante
with grant option;
revoke insert on Anställda from svante cascade;
create trigger
för att skapa triggers (se kapitel 16)
create procedure
och create function
för att skapa lagrade procedurer respektive funktioner (se kapitel 15)
create domain
för att skapa namngivna datatyper (se avsnitt 8.18)
create assertion
för att skapa generella integritetsvillkor (se kapitel 13).
Varje databashanterare har sin egen SQL-dialekt, och dialekterna varierar såväl när det gäller hur väl de uppfyller standarden som vilka egna finesser man lagt till. Grunderna är för det mesta desamma, men detaljerna skiljer.
Manualerna för de olika databashanterarna innehåller beskrivningar av såväl syntax som funktion hos kommandona. Olika varianter av syntaxbeskrivningar förekommer. Det här utdraget ur MySQL-manualen beskriverdelete
-kommandot:
211
DELETE [LOW_PRIORITY] [QUICK] [IGNORE] FROM tbl_name
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x[PARTITION (partition_name [, partition_name] ...)]
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x[WHERE where_condition]
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x[ORDER BY ...]
[LIMIT row_count]
Man kan också använda sig av syntaxdiagram för att grafiskt beskriva hur kommandon skrivs. Det här är ett sätt som är hämtat ur referensmanualen för databashanteraren Mimer:
delete
-kommandot kan göra och i hur man skriver det.
full [ outer ] join
samt nyckelordet natural
för naturlig join, men det är mindre vanligt att de är implementerade.
7 Det här är de viktigaste datatyperna, men därutöver innehåller standarden såväl variationer på de här datatyperna som helt andra datatyper. Vilka som sen verkligen finns och vad de heter varierar mellan olika databashanterare.
restrict
eller cascade
, men de flesta SQL-dialekter tillåter att de utelämnas.
select
-satsen) är ett exempel på ett sådant frågespråk. Det ursprungliga relationsdatabasfrågespråket (som hette Alpha) hade syntax i form av rena logiska uttryck. Det ansågs dock att sådan logiksyntax var svårt att förstå för icke-matematiker. Därför utvecklade man frågespråket SQL med en mer naturligt-språk-syntax med många nyckelord för att likna engelska. Select
-satsen i SQL är i sitt grundutförande i själva verket syntaktiskt sockrad predikatkalkyl baserad på Alpha. Det ursprungliga namnet på SQL var SEQUEL, som betydde Structured English Query Language. Namnet SEQUEL visade sig vara upphovsrättskyddat, och därför bytte man till SQL.
Select
-satsen är ett delspråk i SQL för att på ett mycket kraftfullt sätt definiera frågor över databaser baserat på predikatkalkyl. I sitt grundutförande är select
-satsen bara en annan syntax för en form av predikatkalkyl som heter radkalkyl (eng. tuple calculus). Genom att förstå hur en SQL-fråga kan uttryckas som radkalkyl förstår man också vad frågan exakt betyder, dvs. dess semantik. Man har mycket lättare att formulera korrekta frågor i SQL om man behärskar radkalkyl.
214
En viktig egenskap hos radkalkyl och select
-satsen är att frågor uttrycks deklarativt eller icke-procedurellt. Det innebär att användaren med matematisk logik specificerar villkor eller mönster för vad man vill finna i databasen. Det är sedan upp till databassystemets frågeoptimerare att besvara frågan på snabbast möjliga sätt, dvs. frågeoptimeraren bestämmer normalt hur databasens interna datastrukturer ska användas för ett snabbt svar på frågan. Förståelse av radkalkyl och frågeoptimering är ofta mycket viktig för att kunna snabba upp frågor. Man kan läsa mer om frågeoptimering i kapitel 26.
from
-listan binder variabler till rader i de tabeller som anges efter from
. I SQL-frågor och radkalkyl kan man alltså inte ha variabler som är bundna till exempel till tal, utan bara till rader vars attribut innehåller tal.
Det kan bevisas att alla frågor som kan uttryckas i relationsalgebra (se kapitel 11) också kan uttryckas i radkalkyl, och vice versa. Man säger att ett frågespråk är relationellt komplett om man i det kan uttrycka allt som kan uttryckas i radkalkyl eller relationsalgebra.
Select
-satsen i SQL är ett relationellt komplett frågespråk. I själva verket kan select
-frågor innehålla betydligt mer än vad som krävs för att vara relationellt komplett. Således kan uttrycka frågor i SQL som inte kan uttryckas i radkalkyl. Ett exempel på vad man kan uttrycka i SQL som inte kan uttryckas i enkel radkalkyl eller relationsalgebra är aggregatfunktioner som sum
och count
. En annan viktig utvidgning är att resultatet av en fråga i relationsalgebran eller radkalkylen är en mängd rader, vilket i SQL är generaliserat till s.k. påsar (eng. bags) av rader, dvs. samma rad kan förekomma mer än en gång i resultatet av en SQL-fråga. Således behövs ett mer generellt formellt språk än relationsalgebra eller radkalkyl för att representera SQL-frågor.
I resten av detta kapitel förklarar vi hur en delmängd av SQL:s select
-sats kan uttryckas som radkalkyl genom ett antal regler för hur man uttrycker SQL-frågor i radkalkyl. För att det ska bli enkelt gör vi först ett litet exempel.
215
Anstalld | ||||
---|---|---|---|---|
Pnr | Namn | Lön | Placerad | Telefon |
581027-0233 | Stina | 282677 | 4 | 018-565758 |
Avdelning | |
---|---|
Avdnr | Avdnamn |
4 | Skor |
I radkalkyl representerar man relationer (tabeller) i schemat som predikat, i vårt fall som:
Anstalld(Pnr,Namn,Lön,Placerad,Telefon)
Avdelning(Avdnr,Avdnamn)
Radkalkyl är ett teoretiskt språk; i radkalkylen bryr man sig inte om detaljer som datatyper och domäner hos tabellerna, bara strukturen av dem. Man brukar markera nyckeln i relationer med understrykning:
Anstalld(
Pnr,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xNamn, Lön, Placerad, Telefon)
Avdelning(
Avdnr,
Avdnamn)
Vi vill ställa frågan: Finn namn och telefon för alla personer som har högre lön än 100 000.
Frågan kan uttryckas i SQL som:
select a.Namn, a.Telefon
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Anstalld a
where a.Lön > 100000;
I radkalkyl uttrycker man frågan som:
{a.N amn, a.T elef on Anstalld(a) ∧ a.Ln > 100 000}
Generellt uttrycks en fråga i radkalkylen på formen:
{proj(t1, t2, ...) r(t1, t2, ...) ∧ c(t1, t2, ...)}
a
till raderna i relationen
216
Anstalld.
Bindningspredikatet blir således Anstalld(a).
Fromlistan i SQL definierar bindningspredikat.
select
i SQL-frågor definierar projektioner.
from
-listan. Sådana konjunktiva bindningspredikat representerar den kartesiska produkten av raderna i angivna relationer.
Frågan kan uttryckas i SQL som:
select a.Namn, a.Telefon, avd.Avdnamn
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Anstalld a, Avdelning avd
where a.Lön > 100000 and a.Placerad = avd.Avdnr;
{a.N amn, a.T elef on, avd.Avdnamn Anstalld(a) ∧ Avdelning(avd)
∧ a.Ln > 100 000 ∧ a.P lacerad = avd.Avdnr}
I detta fall är bindningspredikatet konjunktionen Anstalld(a)∧Avdelning(avd). Det binder variablerna a och avd till alla kombinationer (kartesiska produkten) av rader i relationerna Anstalld och Avdelning. Variablerna 217 används sedan i sökvillkoret för att matcha (joina) relationerna Anstalld och Avdelning och för att begränsa raderna a till de rader i relationen Anstalld vars attribut a.Ln är större än 100 000 .
I ovanstående SQL-frågor är explicita variabelnamn angivna i fromlistan för alla ingående tabeller. Vidare används alltid formatet t.A för att explicit specificera värdet av attribut A i rad t. Generellt kan man utelämna sådana variabelnamn i SQL, dvs. det är också tillåtet att uttrycka föregående fråga utan radvariabler som:
select Namn, Telefon, Avdnamn
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Anstalld, Avdelning
where Lön > 100000 and Placerad=avdnr;
Vi säger att vi har en fullständig SQL-fråga om vi deklarerat variabler för alla ingående rader i from
-listan och alltid angett attributen explicit i where
-villkoret. En fullständig SQL-fråga kan direkt översättas till motsvarande radkalkylfråga. En icke-fullständig SQL-fråga kan alltid transformeras till en fullständig fråga genom att introducera explicita variabelnamn för varje tabell som anges i from
-listan.
Förutom att lätt kunna översättas till radkalkyl har fullständigt uttryckta SQL-frågor fördelen att de gör det möjligt att lägga till nya kolumner i tabellerna utan att ändra SQL-frågorna. Om vi till exempel lägger till kolumnen Telefon också i tabellen Avdelning blir ovanstående icke-fullständiga SQL-fråga felaktig, eftersom det då blir flertydigt vilken kolumn som avses med Telefon: Antingen den i relationen Anstalld eller den i Avdelning. Motsvarande fullständiga SQL-fråga förblir korrekt. Vi noterar att man får mindre beroende mellan frågor och databasschemat med fullständiga frågor, dvs. man får s.k. logiskt dataoberoende.
En formel kan således vara ett av följande:
Frågan uttrycks i radkalkyl som:
{a.N amn Anstalld(a)∧((∃avd)Avdelning(a)∧avd.Avdnr = a.P lacerad)}
I SQL kan man uttrycka frågan som:
select a.Namn
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Anstalld a
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere exists(select *
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Avdeling avd
where avd.Avdnr = a.Placerad);
7. Om f är en formel, är också ((∀t)f ) en formel och formeln f sägs vara universellt kvantifierad.
Universell kvantifiering är inte direkt utryckbar i alla SQL-versioner på samma sätt som existens-kvantifiering. Dock är det nödvändigt att kunna uttrycka frågor som är ekvivalenta med universell kvantifiering för att SQL ska vara relationellt komplett.
Hur uttrycks då universell kvantifiering i SQL? Antag till exempel att vi vill ställa frågan: Finn de avdelningar där alla anställda tjänar mer än 50 000.
219I radkalkyl uttrycker man den omformulerade frågan som:
{avd.Avdnamn Avdelning(avd) ∧
¬((∃a)(Anstalld(a) ∧ a.P lacerad = avd.Avdnr ∧ a.Ln < 50 000)}
I SQL kan man uttrycka den omformulerade frågan som:
select avd.Avdnamn
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Avdeling avd
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere not exists (select *
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Anstalld a
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere a.Placerad = avd.Avdnr and
a.Lön < 50000);
220
2 + 3x + x2- y
, räknar man med tal. Variablerna står för olika tal, och resultatet när man beräknar värdet av ett uttryck är också ett tal. I relationsalgebran räknar vi i stället med relationer, dvs tabeller. Varje variabel står för en relation, och resultatet av ett uttryck är också en relation.
Relationsalgebra är alltså ett sätt att räkna med relationer, så att man får fram nya relationer. Till exempel kan man slå ihop och kombinera relationer på olika sätt, eller välja ut vissa rader eller kolumner.
Innan du läser det här kapitlet bör du ha läst kapitlet om relationsmodellen, kapitlen om SQL och kapitlet om relationskalkyl. Vi kommer nämligen att återknyta till SQL, och jämföra det vi gör i relationsalgebra med motsvarande operationer i SQL. Relationsalgebra har samma uttryckbarhet som radkalkyl, dvs allt som kan uttryckas i radkalkyl kan också uttryckas i relationsalgebra och vice versa.
De flesta som arbetar med databaser, och som behöver skriva frågor uttryckta i ett frågespråk, använder ett deklarativt frågespråk, och då vanligtvis SQL. Det är mycket ovanligt att databashanterare låter användarna mata in uttryck i relationsalgebra. Men det som sker inuti en relationsdatabashanterare uttrycks ofta tydligast i relationsalgebra. 222 Till exempel översätter databashanteraren de SQL-frågor den tar emot till något som åtminstone starkt påminner om relationsalgebrauttryck. Om man ska förstå hur en databashanterare arbetar internt, till exempel för att kunna få ut bästa möjliga prestanda vid körningen av en fråga, är det därför bra att känna till relationsalgebra.
Eftersom delar avselect
-satsen är baserade på relationsalgebra, kan det ibland vara svårt att formulera avancerade frågor i SQL om man inte kan relationsalgebra. Till exempel behöver man ibland använda explicit yttre join i SQL. Då måste man veta hur en yttre join fungerar, och yttre join är en operation i relationsalgebran.
I den ursprungliga, teoretiska relationsmodellen är en relation en matematisk mängd av tupler, och det går inte att ha dubbletter. I moderna relationsdatabaser, och i SQL, arbetar man i stället med påsar (efter den engelska termen bag), som är mängder som kan innehålla dubbletter. Operationerna i relationsalgebran fungerar lite olika, beroende på om man baserar algebran på mängder eller på påsar. I det här kapitlet går vi igenom den mer grundläggande mängdbaserade relationsalgebran.
Normalt använder man alltså inte relationsalgebra när man söker efter data i en databas. Det är vanligare att man använder ett deklarativt frågespråk som SQL-frågor, där man kan ange vad man vill ha fram, men inte behöver tala om hur det ska räknas ut. Relationsalgebra däremot är inte deklarativ, utan man måste ange i vilken ordning de olika delresultaten ska beräknas.
Relationsalgebran är alltså procedurell, i den meningen att ordningen som operationerna utförs i har betydelse. Den är inte procedurell på samma sätt som Java eller C++, där ett uttryck kan ha sidoeffekter. Relationsalgebraoperatorerna har inga sidoeffekter, utan värdet av ett relationsalgebrauttryck är alltid detsamma, oberoende av var det uppträder i ett uttryck. Detta kallas referensgenomskinlighet, på 223 engelska referential transparency. Relationsalgbran är därför egentligen ett funktionellt språk utan sidoeffekter, där funktionerna är relationsalgebraoperatorer över relationer.
En skillnad mellan relationsalgebran och radkalkylen (se avsnitt 10.2) är att i relationsalgebran uttrycks funktioner där variabler är bundna till hela tabeller medan man i radkalkylen formulerar logiska uttryck där variabler är bundna till rader (tupler) i tabeller. Man kan se ett relationsalgebrauttryck som ett antal funktionsanrop som tar hela relationer som argument och returnerar nya relationer som resultat.
Man brukar skilja mellan logisk och fysisk relationsalgebra. Den logiska algebran som behandlas i det här kapitlet definieras helt i termer av relationer, och säger inget om lagringsstrukturer. För att kunna utföra en fråga effektivt måste emellertid databashanteraren översätta frågan vidare till operationer i termer av hur relationerna fysiskt är lagrade i databashanterarens interna lagringsstrukturer. Detta kallas fysisk relationsalgebra, eller ibland utvidgad relationsalgebra eftersom den innehåller ytterligare, lagringsstrukturrelaterade, funktioner, och vi återkommer till den i kapitel 26.
Till skillnad mot den logiska relationsalgebran är fysisk relationsalgebra inte referensgenomskinlig. Operatorer i fysisk relationsalgebra får ha sidoeffekter.
σ
En av de enklaste operationerna (funktionerna) i relationsalgebran är operationen selektion, som väljer ut vissa rader i en tabell. Den kallas select på engelska, och den skrivs med den grekiska bokstaven lilla sigma, σ, som motsvaras av s i vårt alfabet.
Titta på den här tabellen, som heter Medlemmar och som innehåller data om medlemmarna i en klubb:
224Medlemmar | ||
---|---|---|
Nummer | Namn | Telefon |
1 | Olle | 260088 |
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta1 | 74590 |
Om vi gör en selektion med villkoret att attributet Namn ska ha värdet "Lotta",
σ
Namn
="Lotta"(Medlemmar)
Nummer | Namn | Telefon |
---|---|---|
4 | Lotta | 174590 |
σ
Nummer
>2(Medlemmar)
Nummer | Namn | Telefon |
---|---|---|
3 | Sam | 260088 |
4 | Lotta | 174590 |
Operationen selektion kan aldrig öka antalet rader i relationen. Antalet rader i resultatet är mindre än, eller lika stort som, i argumentet.
select * from Medlemmar where Nummer > 2;
Operationen projektion väljer ut en eller flera kolumner ur en tabell. Den kallas project på engelska, och den skrivs med den grekiska bokstaven lilla pi, π, som motsvaras av p i vårt alfabet.
π
Namn,Telefon
(M edlemmar)
Namn | Telefon |
---|---|
Olle | 260088 |
Stina | 282677 |
Sam | 260088 |
Lotta | 174590 |
Precis som för selektion, kan antalet tupler aldrig öka. Man skulle kunna förvänta sig att antalet tupler i resultatet alltid skulle vara detsamma som i ursprungsrelationen, men det är inte säkert:
π
Telefon
(Medlemmar)
Telefon |
---|
260088 |
282677 |
174590 |
Här fanns telefonnumret 260088 med två gånger, men eftersom raderna i en tabell är en mängd, räknas inte dubbletter. (Men se avsnitt 11.2 om påsar.) Alltså innehåller mängden av rader bara tre element.
select distinct Telefon from Medlemmar;
De exempel på relationsalgebra som vi har sett ovan är förstås uttryck, men man kan kombinera flera operationer till ett mer komplicerat uttryck. Antag att man vill ha namnet på de medlemmar i tabellen Medlemmar som har medlemsnummer mindre än 3. Då kan man göra en selektion och sen en projektion:
226
π
Namn
(σ
Nummer
<3(Medlemmar))
Namn |
---|
Olle |
Stina |
Ett annat alternativ är att utföra arbetet steg för steg, och spara mellanresultatet i en variabel som vi kan kalla Temp: 1
Temp ← σ
Nummer<3
(Medlemmar)
Resultat ← π
Namn
(Temp)
Vänsterpilen (←) brukar användas som tilldelningsoperator i relationsalgebra. En C- eller Java-programmerare skulle kanske hellre skriva A = B
i stället för A ← B
.
En SQL-fråga som gör samma sak som exemplet ovan ser ut så här:
select distinct Namn
from Medlemmar
where Nummer < 3;
Man kan inte göra projektionen först, så det här försöket blir alldeles fel:
σ
Nummer<3
(π
Namn
(Medlemmar))
Projektionen tar ju bort kolumnen Nummer, som vi ska använda i selektionen, så det här uttrycket går inte att beräkna. Vi kan jämföra med vanlig algebra som räknar med tal. I uttrycket sin(log(x))
måste vi beräkna logaritmen innan vi beräknar sinus-funktionen. log(sin(x))
ger (vanligen) ett helt annat resultat.
Antag att vi har två tabeller, Medlemmar och Nyanmälda, som ser ut så här:
Medlemmar | ||
---|---|---|
Nummer | Namn | Telefon |
1 | Olle | 260088 |
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta | 174590 |
Nyanmälda | ||
---|---|---|
Nummer | Namn | Telefon |
1 | Olle | 260088 |
5 | Hjalmar | null |
6 | Hulda | 281000 |
∪
.
Nu kan vi få fram vilka medlemmar vi har totalt, genom att beräkna
Medlemmar ∪ Nyanmalda
Nummer | Namn | Telefon |
---|---|---|
1 | Olle | 260088 |
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta | 174590 |
5 | Hjalmar | null |
6 | Hulda | 281000 |
Olle var med i båda tabellerna, men han kommer bara med en gång i resultatet. Dubbletter räknas inte i mängder. 2 Notera också att ordningen på raderna har råkat ändras lite i utskriften. Ordningen spelar ju ingen roll i en mängd.
I det här fallet hade tabellerna Medlemmar och Nyanmälda samma schema, och deras kolumner var likadana. Att kolumnerna är likadana är ett krav för att man ska kunna använda mängdoperationer på två tabeller, och det kallas att de är unionkompatibla. Tabeller som inte är unionkompatibla kan dock "slås samman" på ett liknande sätt, med en operation som brukar kallas yttre union (se nedan).
228 Operationen snitt skrivs normalt med en sorts upp-och-ned-vänt U:∩
.
Medlemmar ∩ Nyanmalda
Nummer | Namn | Telefon |
---|---|---|
1 | Olle | 260088 |
Det var ju bara Olle som fanns med i båda tabellerna. Om vi vill få fram vilka av de gamla medlemmarna som inte anmält sig på nytt, beräknar vi differensen mellan Medlemmar och Nyanmälda:
Medlemmar − Nyanmalda
Nummer | Namn | Telefon |
---|---|---|
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta | 174590 |
För att man ska kunna beräkna unionen av två tabeller måste de vara unionkompatibla, dvs ha likadana attribut: lika många och av samma typ. Om vissa, men inte alla, attributen är gemensamma, kallas tabellerna partiellt unionkompatibla. Om två tabeller är partiellt unionkompatibla kan man beräkna en så kallad yttre union. Den tar med alla kolumnerna från båda tabellerna, och fyller på med null-värden där det inte finns några värden att stoppa in från ursprungstabellerna.
Exempel: Antag att vi har två tabeller, Anställda och Konsulter, som ser ut så här:
Anställda | ||
---|---|---|
Nr | Namn | Chef |
1 | Hjalmar | 1 |
2 | Hulda | 1 |
3 | Svenne | 2 |
4 | Lotta | 2 |
Konsulter | ||
---|---|---|
Nr | Namn | Företag |
5 | Pentti | Ericsson |
6 | Antti | null |
7 | Pekka | Telia |
Den yttre unionen mellan dem blir då följande:
Nr | Namn | Chef | Företag |
---|---|---|---|
1 | Hjalmar | 1 | null |
2 | Hulda | 1 | null |
3 | Svenne | 2 | null |
4 | Lotta | 2 | null |
5 | Pentti | null | Ericsson |
6 | Antti | null | null |
7 | Pekka | null | Telia |
×
Antag att vi förutom tabellen Medlemmar, som såg ut så här,
Medlemmar | ||
---|---|---|
Nummer | Namn | Telefon |
1 | Olle | 260088 |
2 | Stina | 282677 |
3 | Sam | 260088 |
4 | Lotta | 174590 |
Sektioner | ||
---|---|---|
Sektionskod | Namn | Ledare |
A | Bowling | 4 |
B | Kickboxing | 4 |
C | Konstsim | 2 |
Ledare är ett referensattribut, som refererar till kolumnen Nummer i tabellen Medlemmar.
Operationen kartesisk produkt skrivs med ett kryss,×
. Den kartesiska produkten av två tabeller ger en tabell som består av alla kolumnerna från båda tabellerna och alla kombinationer av raderna i tabellerna:
Sektioner × Medlemmar
230
Sektionskod | Namn | Ledare | Nummer | Namn | Telefon |
---|---|---|---|---|---|
A | Bowling | 4 | 1 | Olle | 260088 |
A | Bowling | 4 | 2 | Stina | 282677 |
A | Bowling | 4 | 3 | Sam | 260088 |
A | Bowling | 4 | 4 | Lotta | 174590 |
B | Kickboxing | 4 | 1 | Olle | 260088 |
B | Kickboxing | 4 | 2 | Stina | 282677 |
B | Kickboxing | 4 | 3 | Sam | 260088 |
B | Kickboxing | 4 | 4 | Lotta | 174590 |
C | Konstsim | 2 | 1 | Olle | 260088 |
C | Konstsim | 2 | 2 | Stina | 282677 |
C | Konstsim | 2 | 3 | Sam | 260088 |
C | Konstsim | 2 | 4 | Lotta | 174590 |
Ja, nu blev det två kolumner som heter likadant, nämligen Namn. Det kan man egentligen inte ha, men det struntar vi för tillfället i. Vill man skilja dem åt kan man kalla dem Sektioner.Namn och Medlemmar.Namn, som i SQL. Se också avsnittet om namnbyte nedan.
Man inser snabbt att kartesiska produkter ibland kan bli ganska stora. Om två tabeller har tusen rader var, innehåller deras kartesiska produkt en miljon rader. Dessutom är kartesiska produkter inte särskilt användbara, för det är inte så ofta man är intresserad av alla kombinationer av rader. I stället brukar det vara vissa kombinationer som är intressanta, nämligen de rader som hör ihop på något sätt, och det ska vi titta på nedan i avsnittet om operationen join.
Om vi utgår från den kartesiska produkten mellan tabellerna, kan vi utnyttja kopplingen mellan tabellerna genom att bara behålla de rader där Ledare och Nummer har samma värde. Vi gör alltså operationen selektion med villkoret Ledare = Nummer:
231
σ
Ledare=Medlemsnummer
(Sektioner × Medlemmar)
Sektionerskod | Namn | Ledare | Nummer | Namn | Telefon |
---|---|---|---|---|---|
C | Konstsim | 2 | 2 | Stina | 282677 |
A | Bowling | 4 | 4 | Lotta | 174590 |
B | Kickboxing | 4 | 4 | Lotta | 174590 |
Och plötsligt har vi fått en fin tabell med data om de olika sektionerna och deras ledare!
Operationen att kombinera raderna i två tabeller, men inte behålla alla de möjliga kombinationerna, utan bara dem som uppfyller ett visst villkor, kan alltså uttryckas som en kartesisk produkt följd av en selektion. Men den har också blivit en egen operation, som kallas join, och skrivs med ett tecken som ser ut som en fjäril: .
Sektioner ⋈
Ledare=Nummer
Medlemmar
Villkoret, i det här fallet Ledare = Nummer, kan kallas joinvillkor.
Joinvillkoret kan innehålla alla möjliga jämförelser mellan kolumner. Eftersom man vanligtvis gör hopkopplingar via referensattribut (men det behöver inte vara några referensattribut inblandade), så är det vanligaste att två kolumner ska vara lika. En join som bara innehåller likhetsjämförelser kallas ibland equijoin. Om man även tillåter andra jämförelser, till exempel "mindre än" och "större än", talar man ibland om en theta-join, där theta (även skriven thäta på svenska) är den grekiska bokstaven θ, som här används som symbol för vilken jämförelseoperator som helst.
Så fort vi vill koppla ihop två tabeller (och det gör man vanligen med hjälp av ett referensattribut) måste vi ha med en join-operation. (Eller en kartesisk produkt följd av en selektion, men det är ju egentligen samma sak.) Om vi till exempel vill ha reda på telefonnumret till bowlingsektionens ledare, kan vi skriva så här i relationsalgebra:
π
Telefon
[(σ
Namn
="Bowling"(Sektioner))⋈
Ledare=Nummer
Medlemmarl
I SQL kan man skriva på flera olika sätt för att koppla ihop tabeller på samma sätt som med relationsalgebrans join-operation. Följande 232 tre SQL-satser ger alla samma resultat som relationsalgebrauttrycket ovan:
select Telefon
from Sektioner, Medlemmar
where Ledare = Nummer
and Sektioner.Namn = 'Bowling'
select Telefon from Medlemmar
where Nummer in (select Ledare from Sektioner
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere Namn = 'Bowling');
select Telefon
from (Sektioner inner join Medlemmar on Ledare = Nummer)
where Sektioner.Namn = 'Bowling';
Åtminstone tidigare var det vanligt att ett referensattribut alltid hade samma namn som den kolumn det refererade till. Operationen naturlig join utnyttjar detta, och innebär en join där alla kolumner med samma namn ska vara lika, samtidigt som dubblerade kolumner bara kommer med en gång i resultatet.
Naturlig join kan antingen skrivas på samma sätt som en vanlig join, men utan joinvillkor (R1 ⋈ R2
), eller med en stjärna (R1 * R2
).
Om man försöker göra en naturlig join mellan tabellerna Sektioner och Medlemmar från exemplen ovan, blir resultatet inte så lyckat. De två kolumner som heter likadant, nämligen kolumnerna Namn, kommer att jämföras, men de innehåller ju helt olika saker: namn på sektioner respektive namn på medlemmar. Resultatet kommer inte att innehålla några rader alls, utom om vi råkar ha någon stackars medlem som råkar heta likadant som en av sektionerna. ("Kickboxing", kanske?)
En del databaskonstruktörer tycker att ett referensattribut ska ha samma namn som det attribut i en annan relation som det refererar till. Då blir också naturlig join mer, eh, naturlig att använda. Men det finns också nackdelar med den regeln:
ρ
I exemplen på kartesisk produkt och join fuskade vi lite, och brydde oss inte om att två attribut i resultatet hade samma namn. Egentligen får ju två attribut i en relation inte heta likadant, så man måste byta namn på en av dem.
Operationen namnbyte skrivs med den grekiska bokstaven lilla rho (även skriven rå på svenska)ρ,
som motsvaras av r i vårt alfabet.
Vi kan använda namnbytesoperationen för att byta namn på attribut, men också för att ge ett nytt namn till hela tabellen. För att ge det nya namnet Nya till tabellen som heter Gamla, kan man skriva
ρ
Nya
(Gamla)
För att ge attributen i tabellen Gamla de nya namnen A, B och C kan man skriva
ρ
(A,B,C)
(Gamla)
Och för att både ge attributen och själva tabellen nya namn kan man skriva
ρ
N ya(A,B,C)
(Gamla)
234
Tidigare kopplade vi ihop tabellerna Sektioner och Medlemmar med en join-operation, för att se vem som är ledare för varje sektion. Eftersom båda tabellerna innehåller en kolumn som heter Namn, kommer det att finnas två Namn-kolumner i svaret, och för att undvika det byter vi namn på de kolumnerna:
lρ
(Sektionskod,Snamn,Ledare)
(Sektioner)l
⋈
Ledare=Nummer
lρ
(Nummer,M namn,T elef on)
(Medlemmar)l
Sektionskod | Snamn | Ledare | Nummer | Mnamn | Telefon |
---|---|---|---|---|---|
C | Konstsim | 2 | 2 | Stina | 282677 |
A | Bowling | 4 | 4 | Lotta | 174590 |
B | Kickboxing | 4 | 4 | Lotta | 174590 |
Som exempel tar vi tabellen A (som är en förkortning för Anställda). De anställda har nummer, namn och löner, och de jobbar på olika lokalkontor:
A | |||
---|---|---|---|
Nr | Namn | Lön | Kontor |
1 | Hjalmar | 100 | A |
2 | Hulda | 300 | C |
3 | Svenne | 100 | A |
4 | Lotta | null | C |
Man kan använda aggregatfunktionen sum för att summera alla lönerna:
Relationsalgebra | Motsvarande SQL | Resultat |
F
sum(Lon)
(A)
|
select sum(Lön) from A
|
Om man vill ta bort dubbletter 3 innan man använder aggregatfunktionen, till exempel för att räkna antalet olika löner, kan man börja med att göra en project-operation. Den ger ett delresultat som är en liten tabell som bara består av lönekolumnen. Som vanligt kommer den inte att innehålla några dubbletter. Här använder vi aggregatfunktionen count, som räknar rader, först utan och sen med en project:
Relationsalgebra | Motsvarande SQL | Resultat |
F
count(Lon)
(A)
|
select count(Lön) from A
|
|
F
count(Lon)
(π
Lon
(A))
|
select count(distinct Lön) from A
|
Notera att null-värden fortfarande ignoreras. Det finns fyra rader i tabellen A, och alltså fyra "lönerutor", men Lottas lön är null, och det betyder att den "lönerutan" är tom. Alltså ger aggregatfunktionen count svaret 3 när den räknar löner. Om man verkligen vill räkna rader, skriver man count(*).
Aggregatfunktioner arbetar på en hel kolumn, och ger normalt ett enda resultat, baserat på samtliga värden i hela kolumnen. Men man kan också göra en uppdelning och beräkna flera olika resultat, grupperade efter något av attributen i relationen. Till exempel kan man beräkna inte den totala lönesumman för alla anställda, utan lönesumman för alla anställda på varje kontor. Det motsvarar SQL:s group by-konstruktion:
236Relationsalgebra | Motsvarande SQL |
---|---|
Kontor
F
sum(Lon)
(A)
|
select Kontor, sum(Lön) from A group by Kontor
|
Resultat |
---|
Kontor | sum(Lön) |
A | 200 |
C | 300 |
Det går att beräkna flera aggregatfunktioner på en gång:
Relationsalgebra | Motsvarande SQL |
---|---|
Kontor
F
sum(Lon)
,
count(*)
(A)
|
select Kontor, sum(Lön), count(*) from A group by Kontor
|
Resultat |
---|
Kontor | sum(Lön) | count(*) |
---|---|---|
A | 200 | 2 |
C | 300 | 1 |
Operationen join slår ihop de rader i två tabeller som hör ihop med varandra, enligt ett visst joinvillkor.
De rader som inte hör ihop med någon rad i den andra tabellen försvinner ur resultatet.
En yttre join (outer join på engelska) behåller även de raderna, och fyller på med null-värden.
Vi har redan gått igenom hur yttre join fungerar, i avsnitt 8.9, så här ska vi bara repetera notationen.
En vanlig inre join skrivs med symbolen ⋈.
Man lägger på ett par streck på vänster sida för vänster-ytter-join (), på höger sida för höger-ytter-join (
), och på båda sidorna för full yttre join (
).
Vi tänker oss att vi har ett företag med anställda, och de anställda jobbar på olika projekt. Det råder ett många-till-många-samband mellan anställda och projekt: varje anställd kan jobba på flera projekt, och på varje projekt kan det jobba flera anställda.
I en relationsdatabas representeras JobbarPå-sambandstypen av en tabell, som kan se ut så här. Vi ser till exempel att den anställde med numret a1 jobbar på de fyra projekten p1, p2, p3: och p4.
JobbarPå | |
---|---|
Anställd | Projekt |
a1 | p1 |
a1 | p2 |
a1 | p3 |
a1 | p4 |
a2 | p1 |
a2 | p2 |
a3 | p4 |
a4 | p1 |
a4 | p2 |
a4 | p3 |
En av de anställda är Bengt. Bengt är, som en del skulle uttrycka det, en klippa. Han jobbar i flera olika projekt. Vi skapar en särskild tabell som innehåller Bengts projekt:
BengtsProjekt |
---|
Projekt |
p1 |
p2 |
p3 |
(Övning: Vem av de anställda, a1 till a4, är det som är Bengt? 4 )
Nu inträffar den sorgliga händelsen att Bengt blir sjuk. Vi måste ersätta Bengt med någon, och därför letar vi efter någon som jobbar på 238 alla de projekt som Bengt jobbar på (och kanske ytterligare projekt också).
Resultatet avJobbarPa÷ BengtsP rojekt:
Anställd |
---|
a1 |
a4 |
Ett ER-diagram för en sån hierarki kan se ut så här:
239Översatt till relationsmodellen blir det en tabell med ett referensattribut, Chef, som refererar tillbaka till primärnyckeln, Nr, i samma tabell. Vi kallar tabellen A, som i "Anställda":
A | ||
---|---|---|
Nr | Namn | Chef |
1 | Hjalmar | null |
2 | Hulda | 1 |
3 | Svenne | 1 |
4 | Lotta | 1 |
5 | Bengt | 2 |
6 | Maja | 2 |
Antag att vi nu vill ha svar på frågan Vad heter Bengts chef? Vi måste gå upp i hierarkin en nivå, från Bengt räknat. Vi letar först reda på "Bengt-raden", där vi ser att Bengts chef är anställd nummer 2, och sen går vi till "2-raden", och ser att den anställde med nummer 2 heter Hulda. Svaret på frågan är alltså Hulda.
Om det hade funnits två olika tabeller, en med de anställda och en med deras chefer, hade det varit enkelt att lösa med en joinoperation. Men här har vi bara en enda tabell, som ska kopplas ihop med sig själv, och för att göra det måste vi ha två olika namn på samma tabell. Därför använder vi namnbytesoperatornρ
, och ger tabellen två olika alias: P (för "proletär") och B (för "boss").
De här tre olika relationsalgebrauttrycken ger alla det önskade resultatet:
πBnamn[(σPnamn="Bengt"(ρP (Pnr,Pnamn,Pchef)(A)))
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x⋈Pchef=Bnr
(ρB(Bnr,Bnamn,Bchef)(A))]
eller, med en mindre vanlig notation:
πB.Namn[(σP.Namn="Bengt"(ρP(A)))⋈P.Chef=B.Nr(ρB (A))]
240
P ← ρP(Pnr,Pnamn,Pchef )(A)
B ← ρB(Bnr,Bnamn,Bchef )(A)
J ← σPnamn=" Bengt"(P)
C ← J ⋈Pchef =BnrB
R ← πBnamn(C)
Resultat (men namnet på attributet i svaret blir olika i de olika alternativen):
select B.Namn
from A as P, A as B
where P.Chef = B.Nr
and P.Namn = 'Bengt'
Vi joinar alltså tabellen A med sig själv, både i SQL-frågan och i de olika relationsalgebrauttrycken. Det är som att joina två tabeller med samma namn och samma attributnamn. Därför måste de döpas om. (
ρP(A)⋈...ρB(A)
är en början, men då har de fortfarande samma attributnamn.)
Nu ska vi gå upp i hierarkin två nivåer: Vad heter Bengts chefs chef? Nu behöver vi tre alias för tabellen A: P för proletären Bengt, MB (som i "mellanboss") för hans chef, och OB (som i "overboss") för nästa chef i hierarkin.
De här två olika relationsalgebrauttrycken ger båda det önskade resultatet:
πOB.Namn (([σNamn="Bengt"(ρP(A))]
⋈P.Chef=MB.Nr
[ρMB(A)])
⋈MB.Chef=OB.Nr
[ρOB (A)])
P ← ρP(Pnr,Pnamn,Pchef)(A)
MB ← ρB(Bnr,Bnamn,Bchef)(A)
OB ← ρOB(OBnr,OBnamn,OBchef)(A)
J ← σPnamn="Bengt"(P)
C1 ← J⋈Pchef =Bnr MB
241
C2 ← C1⋈Bchef=OBnrOB
R ← πOBnamn(C2)
Resultat (men namnet på attributet i svaret blir olika i de olika alternativen):
select OB.Namn
from A as P, A as MB, A as OB
where MB.Chef = OB.Nr
and P.Chef = MB.Nr
and P.Namn = 'Bengt'
Om vi vill ha fram Bengts chefer på båda nivåerna direkt ovanför honom, kan vi använda mängdoperationen union för att slå samman de båda svaren, såväl i relationsalgebra som i SQL. På samma sätt kan man hitta Bengts chefer tre, fyra eller tio nivåer upp. Dock blir relationsalgebrauttrycken och SQL-frågorna ganska stora, se 8.16. Så länge vi vet hur många nivåer upp vi ska gå, kan vi skriva ett relationsalgebrauttryck, eller en SQL-sats.
Men om uppgiften är att hitta Bengts alla chefer, oberoende av om de befinner sig en, två eller kanske hundra nivåer upp i hierarkin, blir det svårare. Den typen av operation, att ta sig igenom en hierarki eller en kedja i godtyckligt många nivåer tills man kommer till slutet, kallas transitivt hölje (på engelska oftast recursive closure) och den går inte att göra i (vanlig) relationsalgebra eller i (vanlig) SQL. I SQL:1999 introducerades enwith recursive
klausul i select
-satsen för uttrycka transitivt hölje som beskrivs i avsnittet 8.16. För transitiva höljen skulle vi behöva någon form av repetition eller rekursion, som kan stega sig uppåt i hierarkin godtyckligt många nivåer, ända tills vi når den högsta chefen, och det finns inte i ursprungliga relationsalgebran.
Däremot kan man utöka relationsalgebran med en särskild operation för transitivt hölje. Dessutom kan man använda sig av loopar eller rekursion i lagrade procedurer, och i kapitel 15 om lagrade procedurer ska vi se hur man kan använda en loop för att beräkna transitivt 242 hölje. Ytterligare ett alternativ är att använda SQL från ett värdspråk (se kapitel 21) och utnyttja värdspråkets mekanismer.
I SQL finns kommandona insert, delete och modify för att ändra på innehållet i en databas. I relationsalgebran använder vi helt enkelt tilldelningsoperatorn. För att lägga till raderna i tabellen Nybörjare till tabellen Anställda skriver vi:
Anstallda ← Anstallda ∪ Nyborjare
Operation | Symbol |
Projektion | π |
Selektion | σ |
Namnbyte | ρ |
Union | ∪ |
Snitt | ∩ |
Division | ÷ |
Tilldelning | ← |
Operation | Symbol |
Kartesisk produkt | × |
Join | ⋈ |
Vänster-yttre join |
![]() |
Höger-yttre join |
![]() |
Full yttre join 6 |
![]() |
Semijoin |
![]() |
En konsultfirma har ett antal konsulter, som utför uppdrag åt olika kunder. I samband med uppdragen har konsulterna utgifter, till exempel för resor och hotell, som delas upp på olika konton.
243Uppdrag | ||||
---|---|---|---|---|
Unr | Stad | Start | Slut | Konsult |
1 | Gnesta | 2017-02-10 | 2017-02-19 | 20 |
2 | Ånge | 2017-03-22 | 2017-03-29 | 10 |
3 | Moskva | 2017-03-30 | 2017-04-03 | 10 |
Konsulter | |||
---|---|---|---|
Knr | Namn | Startår | Avdelning |
10 | Kajsa | 1999 | 2 |
20 | Kalle | 2016 | 1 |
Utgifter | ||
---|---|---|
Uppdrag | Konto | Belopp |
1 | Hotell | 1400 |
1 | Resor | 1900 |
2 | Hotell | 1700 |
3 | Resor | 3200 |
3 | Resor | 4500 |
3 | Diverse | 1100 |
Kostnaden för ett uppdrag är uppdelad på olika konton. För att räkna ut kostnaden för ett uppdrag måste man därför summera alla beloppen på det uppdragets rader i tabellen Utgifter.
Normalformer och normalisering är en teori för relationsdatabaser, som man kan använda för att undvika vissa typer av dum design på sin databas. Med "dum design" menar vi här att man skapar tabeller så att vissa data kommer att dubbellagras, medan andra data kanske inte alls går att lagra.
Det kan vara bra att läsa kapitel 5, Relationsmodellen, först.
På 1970-talet, när både relationsdatabaser och teorin om normalisering var nya, brukade man konstruera databaser genom att först göra en dålig databas, kanske med alla data i en enda stor tabell. Därefter gjorde man om databasen till en bättre design genom att normalisera den, dvs att (mer eller mindre algoritmiskt) dela upp tabellerna i flera. Om man i stället börjar med att rita ett ER-diagram (eller motsvarande) som man sen översätter till tabeller, blir det för det mesta en bättre databas redan från början. Man slipper många normaliseringsproblem om man börjar med att rita ett ER-diagram, i stället för att direkt försöka pussla ihop tabeller. De problem som ändå uppstår kan man ofta hantera om man följer den enkla grundregeln om en typ av sak per tabell, och en sån sak per rad.
248Men teorin om normalisering behövs ändå:
Vi vill hålla reda på vilka varor (till exempel bilar) vi köper, och från vilka leverantörer (till exempel Volvo) vi köper dem. Vi kan köpa varje vara från flera olika leverantörer.
Dessutom vill vi veta varornas pris. Priset för en vara är olika, beroende på från vilken leverantör vi köper den.
Vi vill också lagra leverantörernas adresser i databasen, dvs i vilken stad varje leverantör ligger. Vi antar att varje leverantör bara finns i en enda stad.
Kanske behöver vi plötsligt beställa väldigt många bilar från Volvo. Då kan det vara bra att veta hur många människor som bor i staden där Volvo ligger, så att vi vet om Volvo snabbt kan nyanställa folk för att tillverka bilarna. Därför lagrar vi folkmängden för varje stad.
Vi provar med en tabell som vi kallar Inköp:
249Inköp | ||||
---|---|---|---|---|
Vara | Leverantör | Pris | Stad | Folkmängd |
Bilar | Volvo | 100 000 | Torslanda | 80 000 |
Bilar | Saab | 150 000 | Södertälje | 50 000 |
Lastbilar | Saab | 400 000 | Södertälje | 50 000 |
Magnecyl | Astra | 10 | Södertälje | 50 000 |
Vara och Leverantör bildar tillsammans primärnyckel.
Men den tabellen är inte någon bra lösning på hur vi ska lagra vår information. Det finns flera problem:
Lösningen är förstås att dela upp tabellen i flera. Men hur?
Grundregeln är att varje tabell ska beskriva en typ av sak, varje rad i tabellen ska innehålla data om en enda sådan sak, och de data vi lagrat för varje sak ska finnas på en enda rad. Exempelvis kan vi ha 250 en tabell som beskriver leverantörer, där varje rad innehåller data om en leverantör. Alltså: en typ av sak per tabell, en sak per rad, och en rad per sak.
Exempel: Om vi ska lagra information om anställda, så skapar vi en tabell som heter Anställd, och där varje rad handlar om en anställd. Vi kan till exempel ha de här kolumnerna:
Vi ska inte ha de här kolumnerna:
Ofta räcker det med det den här enkla regeln. Om man bara följer regeln om en typ av sak per tabell, en sak per rad, och en rad per sak, så kommer ens databaser att få en bra design, och man undviker problemen med redundans, saker som inte går att lagra, och tabeller som är svåra att förstå.
Men ibland är det svårt att riktigt veta vad det är för "saker" man vill lagra, och vilka data som egentligen hör ihop med dem. Då har vi nytta av teorin om normalisering. Den hjälper oss att se exakt hur de olika kolumnerna i tabellen hör ihop, och visar oss hur vi ska dela upp tabellen för att slippa problemen. Därför börjar vi nu titta på de olika normalformer som teorin om normalisering beskriver. Normalformer är villkor som en tabell kan uppfylla. Det enklaste villkoret är första normalformen, och genom att lägga på fler villkor 251 kan man definiera andra normalformen, tredje normalformen, och så vidare.
Första normalformen säger bara att tabellen ska innehålla atomära värden, dvs högst ett värde per ruta. Exempelvis kan vi i tabellen ovan inte peta in både Volvo och Saab i samma ruta, även om vi köper bilar från båda leverantörerna. Vi måste använda två olika rader för att lagra detta. I de flesta relationsdatabashanterare går det helt enkelt inte att stoppa in mer än ett värde i varje ruta, så alla tabeller är i första normalformen.
Eller nästan i alla fall. Det går förstås att göra ett textfält och sen stoppa in flera namn, med till exempel komma mellan dem: Saab,Volvo,Renault. Ett problem med det är att det blir ganska svårt att göra sökningar i databasen, eftersom SQL-frågorna blir krångliga. Det är inte meningen att man ska göra så, och databashanterarna är inte byggda för att klara det på ett enkelt sätt.
Om vi tittar på vår exempeltabell, tabellen Inköp på sidan 249, inser vi att på varje rad där det står Södertälje i kolumnen Stad, så kommer det att stå 50 000 i kolumnen Folkmängd. Det kallas att kolumnen Folkmängd är funktionellt beroende av Stad.
Mer formellt kan vi säga att om värdet på ett (eller flera) attribut A entydigt bestämmer värdet på ett annat attribut B, är B funktionellt beroende av A. "Entydigt bestämmer" betyder att om värdena på A på två rader i tabellen är lika, måste värdena på B också vara lika. Det kan skrivas med en pil: A → B. Vi kallar A för en determinant, eftersom den bestämmer ("determinerar") B.
I exempeltabellen finns dessa funktionella beroenden:
252Vi ritar upp attributen, med de funktionella beroendena som pilar:
I fortsättningen förkortar vi ibland "funktionellt beroende" till "fb".
Men vänta nu! Det finns ju fler funktionella beroenden i den tabellen!
Vi har sett att på varje rad där det står Södertälje i kolumnen Stad, kommer det att stå 50 000 i kolumnen Folkmängd. Detta betydde, sa vi, att kolumnen Folkmängd är fb av Stad.
Men då kan vi också säga så här:
Vi har sett att på varje rad där det står Södertälje i kolumnen Stad, och där det står Bilar i kolumnen Vara, kommer det att stå 50 000 i kolumnen Folkmängd. Alltså är kolumnen Folkmängd fb av kombinationen Stad och Vara!
Det är ju lite fånigt, så därför definierar vi något som kallas fullständigt funktionellt beroende, som är ett funktionellt beroende där man inte kan ta bort några attribut ur determinanten om det fortfarande ska vara ett funktionellt beroende. Man kan också säga att determinanten är minimal. Den mer matematiskt sinnade vill kanske säga att en kolumn B är fullständigt funktionellt beroende av en annan kolumn eller kolumnkombination A, om och endast om B är funktionellt beroende av A, men inte av någon äkta delmängd av A.
I fortsättningen talar vi hela tiden om fullständiga funktionella beroenden, och när vi talar om en determinant menar vi en (eller flera) 253 kolumner som en annan kolumn är ffb av. I fortsättningen förkortar vi ibland "fullständigt funktionellt beroende" till "ffb".
Vilka fullständiga funktionella beroenden finns i den här tabellen?
A | B | C | D |
---|---|---|---|
1 | 4 | 10 | 100 |
2 | 5 | 20 | 50 |
3 | 6 | 20 | 200 |
1 | 4 | 10 | 200 |
2 | 6 | 20 | 0 |
3 | 6 | 20 | 300 |
1 | 4 | 10 | null |
2 | 6 | 20 | 50 |
3 | 6 | 20 | 50 |
Svaret är att det vet vi inte! Vilka beroenden som finns beror inte på vilka data som för tillfället råkar finnas i tabellen, utan det beror på logiken bakom tabellen.
Tänk på att determinanter kan vara sammansatta av flera attribut. I exemplet ovan finns det dock inga sådana ffb. Exempelvis kan det inte finnas ett ffb {A, B} → D, eftersom det för samma värde på {A, 254 B} förekommer olika värden på D. Det kan inte heller finnas ett ffb {B, D} → C, men det beror på att det finns ett ffb B → C, och alltså är {B, D} → C visserligen ett funktionellt beroende, men inte ett fullständigt sådant.
Titta på tabellen Inköp på sidan 249 igen. För varje vara som vi köper från Saab, måste vi upprepa informationen att Saab ligger i Södertälje. Det beror på att informationen om Saab "hänger ihop" med namnet Saab, och all den Saab-informationen följer med varje gång vi har med Saab i tabellen. Om tabellen hade handlat om leverantörer, med en leverantör på varje rad, så hade Saab bara varit med en gång, och då hade Saab-informationen bara stått på ett ställe. Men nu handlar tabellen om inköp, och vi köper flera varor från Saab, så därför kommer Saab-informationen med flera gånger.
Andra normalformen säger att en tabell, förutom att vara i första normalformen, inte får innehålla några fullständiga funktionella beroenden på delar av primärnyckeln. Om man ritar upp de fullständiga funktionella beroendena mellan attributen, får det alltså inte finnas några pilar från delar av primärnyckeln, bara från hela primärnyckeln.
Dessa strider mot 2NF, så vi delar upp tabellen Inköp och skapar två nya tabeller.
255En ny tabell Inköp, där Vara och Leverantör fortfarande är primärnyckel:
Inköp | ||
---|---|---|
Vara | Leverantör | Pris |
Bilar | Volvo | 100 000 |
Bilar | Saab | 150 000 |
Lastbilar | Saab | 400 000 |
Magnecyl | Astra | 10 |
Och en tabell Leverantörer, med Leverantör som primärnyckel. Kanske vill man byta namn på kolumnen Leverantör till Namn, för det är ju namnet på leverantören, inte leverantörens leverantör.
Leverantörer | ||
---|---|---|
Leverantör | Stad | Folkmängd |
Volvo | Torslanda | 80 000 |
Saab | Södertälje | 50 000 |
Astra | Södertälje | 50 000 |
Dessa tabeller uppfyller 2NF. Nu står det bara på ett ställe att Saab ligger i Södertälje. Vi kan lägga in Gnesta-Kurres korvkiosk som leverantör, trots att vi ännu inte köper några varor därifrån.
Någon kanske tycker att vi fortfarande har redundans, eftersom namnet Saab står med två gånger i den nya tabellen Inköp. Informationsmässigt är det egentligen ingen redundans, eftersom varje förekomst av namnet Saab ger den nya informationen att en viss vara levereras av Saab. Däremot är det förstås så att långa namn (till exempel Svenska Aeroplanaktiebolaget) tar upp plats, särskilt om man tagit till lite extra på textfältets storlek. Därför är det vanligt att man hittar på ett särskilt nummer, som man kan använda som nyckel i en tabell. I det här exemplet kan man ge varje leverantör ett nummer, och sen använder man det numret för att referera till leverantören. Så här:
256Inköp | ||
---|---|---|
Vara | Leverantör | Pris |
Bilar | 1 | 100 000 |
Bilar | 2 | 150 000 |
Lastbilar | 2 | 400 000 |
Magnecyl | 3 | 10 |
Leverantörer | |||
---|---|---|---|
Nummer | Namn | Stad | Folkmängd |
1 | Volvo | Torslanda | 80 000 |
2 | Saab | Södertälje | 50 000 |
3 | Astra | Södertälje | 50 000 |
Definitionen av 2NF här ovanför gäller i en tabell med en enda kandidatnyckel, som då förstås också är primärnyckel. Denna primärnyckel kan förstås vara sammansatt av flera attribut, men det finns ingen annan (minimal) kolumnkombination som garanterat är unik för varje rad.
Men det kan ju finnas flera kandidatnycklar i en tabell. I så fall måste vi tänka på alla kandidatnycklarna och inte bara primärnyckeln, om vi verkligen ska få bort de problem som 2NF ska lösa.
Det kan vi se genom ett exempel. Antag att vi inför ett unikt nummer på varje inköpssamband, och lägger till det som en kolumn i den ursprungliga tabellen Inköp. Då blir det numret en kandidatnyckel, förutom kombinationen av Vara och Leverantör:
Om vi väljer kombinationen Vara och Leverantör som primärnyckel, precis som förut, så går det bra. Tabellen uppfyller inte 2NF, och måste delas upp. Men om vi väljer attributet Nummer som primärnyckel, kommer tabellen att vara i 2NF redan från början! Alla icke-nyckelattributen är ju beroende av hela primärnyckeln. (Något annat vore konstigt, eftersom primärnyckeln inte är sammansatt!)
257Alla problemen, till exempel med redundans, kvarstår, trots att tabellen är i 2NF.
Därför vill vi ha en definition av 2NF som fungerar även med flera kandidatnycklar. Vi ändrar den gamla definitionen genom att prata om alla kandidatnycklarna i stället för om primärnyckeln:
När vi gjorde om den ursprungliga tabellen till 2NF försvann en del problem. Men vi är inte klara än. Det står fortfarande på flera ställen att det bor 50 000 personer i Södertälje, och vi kan inte lägga in en stad där vi inte har några leverantörer. För att lösa dessa problem måste vi ta till tredje normalformen.
Tredje normalformen säger att en tabell, förutom att vara i andra normalformen, inte får innehålla några transitiva beroenden till icke-nyckelattribut. Det får alltså inte finnas några pilar som går mellan attribut utanför de olika kandidatnycklarna, bara (antingen) från kandidatnycklar till attributen utanför, eller från attributen utanför in i kandidatnycklarna. (Det betyder att om man har en sammansatt primärnyckel, kan man ha pilar som pekar på ett av attributen i nyckeln.)
Tabellen Leverantörer på sidan 255 bryter mot tredje normalformen genom beroendet Stad → Folkmängd, så vi delar upp den och skapar två nya tabeller. Först en ny tabell Leverantörer, med attributet Leverantör som primärnyckel:
Leverantörer | |
---|---|
Leverantör | Stad |
Volvo | Torslanda |
Saab | Södertälje |
Astra | Södertälje |
Dessutom en tabell Städer, med attributet Stad som primärnyckel:
258Städer | |
---|---|
Stad | Folkmängd |
Torslanda | 80 000 |
Södertälje | 50 000 |
Man kan se det som att man "tar tag" i det (eller de) ffb som bryter mot normalformen man vill uppnå, och "drar ut" det ur tabellen, tillsammans med determinanten och det bestämda attributet, till en ny tabell. Man måste också lämna kvar en kopia på determinanten, i den ursprungliga tabellen, så det fortfarande går att se vilka data i de två tabellerna som hör ihop. Som exempel kan vi ta uppdelningen av den ursprungliga tabellen Inköp på sidan 249:
Leverantörer | |
---|---|
Leverantör | Stad |
Volvo | Torslanda |
Saab | Södertälje |
Astra | Södertälje |
Och sen en tabell Städer, också med attributet Leverantör som primärnyckel! I den tabellen kan man läsa hur stor folkmängden är i den stad som en viss leverantör finns i:
259Städer | |
---|---|
Leverantör | Folkmängd |
Volvo | 80 000 |
Saab | 50 000 |
Astra | 50 000 |
Dessa tabeller uppfyller också 3NF! Det finns ju inga transitiva beroenden. Men uppenbarligen var det en ganska korkad uppdelning. Alla problemen som vi försökte lösa med hjälp av 3NF finns kvar.
Regel: Låt därför bli att göra korkade uppdelningar. Tänk på vad tabellerna betyder.
En ännu dummare uppdelning vore den här:
Leverantörer | |
---|---|
Leverantör | Stad |
Volvo | Torslanda |
Saab | Södertälje |
Astra | Södertälje |
Folkmängd |
Folkmängd |
80 000 |
50 000 |
Tabellerna uppfyller 3NF, men med de här två tabellerna kan vi inte återskapa informationen i den ursprungliga leverantörstabellen. Vi vet inte vilken folkmängd som hör till vilken stad. Rader i tabellerna i en relationsdatabas har ju ingen bestämd ordning, så vi vet inte om 80 000 hör ihop med Torslanda eller med Södertälje.
3NF tillät ju fullständiga funktionella beroenden in i en kandidatnyckel, dvs det var tillåtet med pilar från icke-nyckelattribut till attribut i nyckeln. Boyce-Codds normalform, BCNF, förbjuder dessa, och är alltså ett hårdare villkor än 3NF. Den förhindrar vissa problem som kan förekomma i 3NF.
De tabeller som vi nu skapat, och som är i 3NF, uppfyller faktiskt också kraven för BCNF. Om man designar en databas som är i 3NF, kommer den oftast att också vara i BCNF.
Den stora fördelen med BCNF är nog att definitionen är enkel:
• Enklare definition av BCNF: 1NF, plus att varje determinant ska vara en kandidatnyckel.
Annorlunda uttryckt: Rita upp alla fullständiga funktionella beroenden som pilar. Nu ska alla pilar gå från kandidatnycklar. Om du hittar en pil som går från något annat än en kandidatnyckel, så är tabellen inte i BCNF.
Fördelen med BCNF, jämfört med 3NF, är kanske inte så mycket de problem med dålig databasdesign som den löser, utan att den är enklare att definiera. Därför kan det vara svårt att hitta bra exempel på en tabell som är i 3NF men inte i BCNF. Här är i alla fall ett exempel. Tabellen används för att lagra längden på svenska gator. Gatunamn är unika i varje stad, men inte i hela Sverige. Det kan alltså inte finnas två Storgatan i Gnesta, men det kan finnas en i Gnesta och en i Linköping. Varje gata ligger (antar vi) helt och hållet inom ett och samma postnummerområde. Det kan finnas flera postnummerområden i en ort.
Gator | |||
---|---|---|---|
Gatunamn | Postnummer | Ortsnamn | Längd |
Rydsvägen | 58248 | Linköping | 19 km |
Mårdtorpsgatan | 58248 | Linköping | 700 m |
Storgatan | 58223 | Linköping | 1 500 m |
Storgatan | 64631 | Gnesta | 14 m |
Det finns två kandidatnycklar, som båda består av två attribut. Den ena är Gatunamn och Postnummer. Den andra är Gatunamn och Ortsnamn.
261Tabellen innehåller följande fullständiga funktionella beroenden:
Det finns alltså ett attribut, Postnummer, som bestämmer ett nyckelattribut, Ortsnamn. Det är tillåtet i 3NF. Men som vi ser finns det redundans i tabellen: det står på två ställen att postnummer 58248 finns i Linköping. Dessutom kan man inte lagra data om ett postnummerområdeutanattsamtidigtlagradataomminstengata.
Dela upp tabellen i två: dels en tabell med gator, dels en tabell med postnummerområden.
Gator | ||
---|---|---|
Gatunamn | Postnummer | Längd |
Rydsvägen | 58248 | 19 km |
Mårdtorpsgatan | 58248 | 700 m |
Storgatan | 82231 | 500 m |
Storgatan | 64631 | 14 m |
Postnummerområden | |
---|---|
Postnummer | Ortsnamn |
58248 | Linköping |
58223 | Linköping |
64631 | Gnesta |
När man analyserar en tabell för att hitta funktionella beroenden, och avgöra vilka normalformer som tabellen uppfyller, måste man 262 hela tiden tänka på vad tabellen och dess kolumner egentligen betyder.
Inköp | ||||
---|---|---|---|---|
Vara | Leverantör | Pris | Stad | Folkmängd |
Bilar | Volvo | 100 000 | Torslanda | 80 000 |
Bilar | Saab | 150 000 | Södertälje | 50 000 |
Lastbilar | Saab | 400 000 | Södertälje | 50 000 |
Magnecyl | Astra | 10 | Södertälje | 50 000 |
Såväl schema som data är likadana, men i den här Inköp-tabellen betyder den första raden inte att vi köper bilar från Volvo för 100 000 kronor, och att Volvo ligger i Torslanda, och att Torslanda har 80 000 invånare. Här betyder den första raden i stället att vi köper bilar från Volvo för 100 000 kronor, och Volvos bilfabrik (men inte till exempel Volvos lastbilsfabrik) ligger i Torslanda, och Torslanda hade 80 000 invånare den dag vi började köpa bilar från Volvo.
Den här tabellen har helt andra fullständiga funktionella beroenden (Övning: Vilka? 1 ), och den uppfyller BCNF. Den skadliga redundansen från den andra tabellen Inköp finns därför inte här. Det står visserligen fortfarande 50 000 tre gånger som Södertäljes folkmängd, men nu är det inte redundant information, utan det betyder att folkmängden i Södertälje råkade vara samma (nämligen 50 000) de tre dagar vi började köpa bilar, lastbilar respektive magnecyl från företag i Södertälje.
Det finns fler normalformer, främst fjärde normalformen och femte normalformen, men de som tagits upp i det här kapitlet är de som har störst praktisk användning. Både fjärde och femte normalformen är ganska komplicerade att förklara, och man har inte heller så stor nytta av dem.
Ibland är det bra att inte normalisera. I ett adressregister vill man kanske ha med både postnummer och ort i samma tabell, trots att det egentligen strider mot 3NF. Designen blir förmodligen klarare då:
Kunder | ||||
---|---|---|---|---|
Nummer | Namn | Gatuadress | Postnummer | Ortsnamn |
2 | Olle | Prästgatan 3D | 83131 | Östersund |
7 | Stina | Mårdgatan 7 | 58248 | Linköping |
8 | Jens | Undertorget 1 | 58248 | Linköping |
Ett annat skäl till att välja en lägre normaliseringsgrad är prestanda. Om man har höga krav på att sökningar ska gå snabbt, bör man tänka på att det normalt tar längre tid för databashanteraren att söka i flera tabeller än i en enda tabell. När man delat upp en tabell i två, som vi visat tidigare, så måste databashanteraren joina 2 ihop de två tabellerna för att återskapa den ursprungliga tabellen. Men var mycket försiktig med den här typen av "optimering" av prestanda, för databasen får en sämre och mer svårarbetad struktur. Prestandaoptimeringar ska man inte göra förrän man vet (dvs har provkört och mätt) att det går för långsamt, och att det är just det här som orsakar det.
Regel: Använd därför teorin med förnuft. Ibland är det bra att inte normalisera. Men om du väljer en lägre normaliseringsgrad bör du ha goda skäl till det, och du bör vara medveten om vilka problem som kan uppstå. När du dokumenterar ditt system bör du sedan ange att du valt en lägre normaliseringsgrad, varför du gjorde det, och vilka problem du förutsett.
Normalform (engelska: normal form). En regel som förbjuder vissa typer av dum design i en databas. Det finns flera olika normalformer, till exempel BCNF.
Normalisering (engelska: normalisation). En teori för (främst) relationsdatabaser som kan användas för att undvika vissa typer av dum design i en databas. Även namn på processen att göra om en databas från en lägre normalform till en högre.
Redundans (engelska: redundancy). Upprepning av data eller funktion. Kan vara nyttig eller skadlig, avsiktlig eller oavsiktlig.
Första normalformen eller 1NF (engelska: first normal form). En av de normalformer som används i relationsmodellen. En tabell som är i 1NF får bara innehålla atomära värden.
Andra normalformen eller 2NF (engelska: second normal form). En av de normalformer som används i relationsmodellen. En tabell som är i 2NF ska vara i 1NF, och dessutom måste varje ickenyckelattribut vara fullständigt funktionellt beroende av alla kandidatnycklar.
Tredje normalformen eller 3NF (engelska: third normal form). En av de normalformer som används i relationsmodellen. En tabell som är i 3NF ska vara i 2NF, och dessutom får inget icke-nyckelattribut vara fullständigt funktionellt beroende av något annat ickenyckelattribut.
Boyce-Codds normalform eller BCNF (engelska: Boyce-Codd's Normal Form). En av de normalformer som används i relationsmodellen. En tabell som är i BCNF ska vara i 1NF, och dessutom måste varje determinant vara en kandidatnyckel.
Funktionellt beroende, fb (engelska: functional dependency, fd). Ett förhållande mellan två attribut eller attributkombinationer, A och B i en tabell. Om B är fb av A, bestämmer A entydigt B, dvs om värdena på A på två rader i tabellen är lika, måste värdena på B också vara lika.
Fullständigt funktionellt beroende, ffb (engelska: full functional dependency, ffd). Ett förhållande mellan två attribut eller attributkombinationer, A och B i en tabell. Om B är funktionellt beroende av A, och inget attribut kan tas bort ur A om det fortfarande ska vara ett funktionellt beroende, är B ffb av A.
Det finns några personer. De äger bilar. Varje person kan äga flera bilar. Vi gör tre olika försök att lagra detta i en tabell.
Personer | ||
---|---|---|
Nummer | Namn | Bilar |
1 | Olle | RFN540 |
2 | Stina | |
3 | Sam | SQL123, DBA456, WCA912 |
Personer | ||||
---|---|---|---|---|
Nummer | Namn | Bil1 | Bil2 | Bil3 |
1 | Olle | RFN540 | null | null |
2 | Stina | null | null | null |
3 | Sam | SQL123 | DBA456 | WCA912 |
Personer | ||
---|---|---|
Nummer | Namn | Bil |
1 | Olle | RFN540 |
3 | Sam | SQL123 |
3 | Sam | DBA456 |
3 | Sam | WCA912 |
Vilka problem finns med dessa lösningar? Finns det något bättre sätt? Och vad har detta med normalformer att göra?
Ibland råkar man lägga in felaktiga data i databasen. Till exempel kanske man skriver in att Bengt har 100 000 kronor i lön när han egentligen bara har 1 000. Eller så står Bengts lön på två ställen, och på ett ställe står det 1 000 och på ett annat 100 000, eller att Bengt har negativ lön. Visst vore det bra om databashanteraren kunde hjälpa oss, genom att helt enkelt hindra oss från att lägga in felaktiga data i databasen?
Databashanteraren kan förstås inte lyckas helt och hållet med att hålla databasen fri från felaktiga uppgifter. Den kan inte hoppa ut ur datorn och springa i väg och kolla upp Bengts lön. (Åtminstone inte med dagens teknik. I framtiden kanske städernas gator är fulla av databashanterare som rusar fram och tillbaka och kontrollerar löneuppgifter. Övning: Rita en bild av en gata i framtiden, med databashanterare och svävande bilar!) Men databashanteraren kan hjälpa till lite i alla fall.
Den som skapar och ansvarar för databasen (databasadministratören) kan berätta för databashanteraren vilka regler som alla data i databasen måste uppfylla. Reglerna kallas integritetsvillkor. Om nu en persons lön kan stå på två ställen, kan vi ha integritetsvillkoret att båda uppgifterna måste vara lika. Eller så har vi helt enkelt en regel som säger att alla löner måste ligga mellan 0 och 70 000. (ABB-direktörer och chefsöverläkare får inte vara med i databasen.)
268Integritetsvillkor ("integrity constraints" på engelska) är alltså villkor som begränsar vilka data som kan lagras i databasen. Åtminstone en del av integritetsvillkoren är egentligen också begränsningar som gäller i den riktiga världen, och inte bara i databasen, för databasen beskriver en del av världen. Om man har ett integritetsvillkor som säger att alla löner måste ligga mellan 0 och 70 000, säger man ju också att ingen människa, av dem som ska vara med i databasen, har en lön som ligger utanför det intervallet.
Här är tre vanliga typer av integritetsvillkor i relationsdatabaser:
Tabellen Anställda innehåller data om anställda, och tabellen Avdelningar innehåller data om avdelningar. JobbarPå är ett referensattribut till Nummer i Avdelningar. Som det ser ut nu jobbar Svea på avdelningen Data, Sten jobbar på avdelningen Ekonomi, och Bengt jobbar ingenstans.
Anställda | ||
---|---|---|
Nummer | Namn | JobbarPå |
1 | Svea | 1 |
2 | Sten | 3 |
3 | Bengt | null |
Även om man inte kan så mycket om databaser verkar det väl rimligt att:
De fyra första villkoren kallas nyckelvillkor, och det sista kallas referensintegritetsvillkor.
Så här anger vi ett nyckelvillkor när vi skapar en tabell med SQL:
create table Avdelningar
270
(Nummer integer
not null,
Namn varchar(10),
primary key (Nummer));
Om primärnyckeln består av en enda kolumn, kan man använda ett kompaktare skrivsätt:
create table Avdelningar
(Nummer integer
not null primary key,
Namn varchar(10));
Nu har vi alltså skapat tabellen Avdelningar, och talat om för databashanteraren att kolumnen Nummer är primärnyckel. Databashanteraren kommer nu att se till att varje avdelning har ett unikt nummer.
Vi stoppar in data i databasen:
insert into Avdelningar values (1, 'Data');
insert into Avdelningar values (2, 'Städning');
insert into Avdelningar values (3, 'Ekonomi');
/* Följande tre kommandon ger fel */
insert into Avdelningar (Namn) values ('Lager');
insert into Avdelningar values (3, 'Lager');
update Avdelningar set Nummer = 2
where Namn = 'Data';
De tre sista kommandona kommer att misslyckas, eftersom databashanteraren hindrar oss:
Så här anger vi ett referensintegritetsvillkor när vi skapar en tabell med SQL:
create table Anställda
(Nummer integer not null,
Namn varchar(10),
JobbarPå integer,
primary key (Nummer),
foreign key (JobbarPå) references Avdelningar(Nummer));
Om den främmande nyckeln består av en enda kolumn, kan man använda det här mer kompakta skrivsättet: 1
create table Anställda
(Nummer integer not null,
Namn varchar(10),
JobbarPå integer
references Avdelningar(Nummer),
primary key (Nummer));
alter table Anställda
add foreign key (JobbarPå)
references Avdelningar(Nummer);
alter table Anställda
add constraint Anställd_till_avdelning
foreign key (JobbarPå)
references Avdelningar(Nummer);
Nu har vi alltså skapat tabellen Anställda, och talat om för databashanteraren att kolumnen JobbarPå är ett referensattribut som refererar till tabellen Avdelningar. Databashanteraren kommer nu att se till att varje anställd jobbar på en avdelning som faktiskt finns i avdelningstabellen. (Undantag: Vi sa aldrig not null för kolumnen JobbarPå, så man kan också lämna tomt i rutan, om en anställd inte jobbar på någon avdelning alls.)
272Ett referensattribut refererar till en nyckel i en annan tabell (eller, ibland, samma tabell). Det brukar alltid vara primärnyckeln som den refererar till, men om man har flera nycklar kan man referera till en annan än primärnyckeln. Den har alltså samma domän som nyckeln i den andra tabellen. Därför kallas ett referensattribut ibland för främmande nyckel (på engelska: foreign key).
Vi stoppar in data i databasen:
insert into Anställda values (1, 'Svea', 1);
insert into Anställda values (2, 'Sten', 3);
insert into Anställda (Nummer, Namn) values (3, 'Bengt');
insert into Anställda values (4, 'Sergio', 5);
/* Ger fel */
update Anställda set Avdelning = 7
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere Namn = 'Svea';
/* Ger fel */
delete from Avdelningar where Namn = 'Data';
/* Ger fel */
update Avdelningar set Nummer = 9
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere Namn = 'Data';
/* Ger fel */
De fyra sista kommandona kommer att misslyckas, eftersom databashanteraren hindrar oss:
Som vi ser finns det flera olika sorters ändringar i databasen som kan göra att referensintegriteten bryts: insättningar, borttagningar och uppdateringar av rader. Vilken tur att databashanteraren kontrollerar allihop!
Vi sa ovan att kommandona "misslyckas" när databashanteraren upptäcker att ett referensintegritetsvillkor är brutet. Men vad innebär det att kommandot "misslyckas"?
Det brukar betyda att transaktionen avbryts, och att ingen ändring görs i databasen. Beroende på vad man använder för databashanterare, och vad det är för sorts integritetsvillkor, sker det antingen direkt när man försökte göra ändringen, eller när hela transaktionen försöker göra commit.
Men man kan också tala om för databashanteraren att den inte ska avbryta operationen, utan "laga" databasen på lämpligt sätt. Titta på referensintegritetsvillkoret från exemplet ovan:
foreign key (JobbarPå) references Avdelningar(Nummer)
I stället kan man skriva på följande olika sätt:
1.foreign key (JobbarPå) references Avdelningar(nummer) on delete set null
2. foreign key (JobbarPå) references Avdelningar(Nummer) on delete cascade
Om jag tar bort en avdelning som det finns anställda som jobbar på, tas även dessa anställda bort.
3.foreign key (JobbarPå) references Avdelningar(Nummer) on delete set default
4. foreign key (JobbarPå) references Avdelningar(nummer) on update cascade
Alla fyra varianterna gör alltså att databashanteraren upprätthåller referensintegriteten genom att ta bort, eller ändra i, de refererande 274 raderna, alltså raderna i Anställda-tabellen! Detta trots att ändringarna som databashanteraren reagerar på görs i tabellen Avdelningar. Men referensvillkoret hör ju till tabellen Anställda, så kanske är det egentligen ganska naturligt att ändringarna görs i just den tabellen.
Man kan också angeno action,
som i on delete no action
och on update no action,
vilket betyder samma som default-beteendet, alltså att försöket att ta bort eller ändra data avbryts.
2
Som exempel på on delete cascade tar vi tabellen Konsulter. Den innehåller data om konsulter som är inhyrda av de olika avdelningarna. Man kan inte ta bort en avdelning hur som helst om det finns anställda som jobbar där, men konsulter är det bara att sparka och ta bort ur databasen:
create table Konsulter
(Nummer integer not null,
Namn varchar(10),
InhyrdAv integer,
primary key (Nummer),
foreign key (InhyrdAv) references Avdelningar(Nummer)
on delete cascade);
insert into Konsulter values (3, 'Sture', 2);
insert into Konsulter values (4, 'Sally', 2);
insert into Konsulter values (5, 'Sune', 3);
delete from Avdelningar where namn = 'Städning';
/* Kaskad!
*/
Nyckelvillkor och referensintegritetsvillkor är enkla att formulera, och i de flesta relationsdatabashanterare är det enkelt att specificera sådana integritetsvillkor. Men det finns andra integritetsvillkor, som kan vara svårare att specificera.
Ibland talar man om allmänna semantiska integritetsvillkor (på engelska: "general semantic integrity constraints"). Det är vilka villkor som helst, som beror på hur det ser ut i den verklighet som databasen ska modellera. "Semantik" har ju med "betydelse" att göra, och de här villkoren bestäms av vad databasens data betyder. Eftersom den riktiga världen kan vara hur komplicerad som helst, kan också de här integritetsvillkoren vara hur komplicerade som helst. Dessa villkor kallas ibland på engelska för business rules, alltså ungefär "regler för affärsverksamheten".
Exempel på allmänna semantiska integritetsvillkor:
Det första av de tre villkoren är exempel på ett lokalt check-villkor där man begränsar tillåtna värden i ett attribut lokalt på varje rad i en tabell. Exempel:
create table Anställda
(Nummer integer primary key,
Namn varchar(10) not null,
Lön integer check (Lön between 0 and 70000));
Ett annat exempel finns på sidan 149. Lokala check-villkor kan kontrolleras mycket effektivt av databashanteraren när tabellen uppdateras, eftersom de bara får referera till attribut i en rad i tabellen.
Både det första och det andra av de tre villkoren är exempel på statiska villkor (engelska: "state constraint", dvs "tillståndsvillkor"). Sådana kan man kontrollera genom att titta på databasens innehåll. Databashanteraren kan kontrollera att villkoret är uppfyllt efter varje uppdatering, men det kan vara mycket dyrbart (det vill säga ta lång tid), beroende på hur villkoret ser ut.
Det tredje villkoret, om att löner bara kan höjas, är ett dynamiskt villkor (på engelska: "transition constraint", dvs "ändringsvillkor"). Det spelar bara in vid ändringar i databasen, och för att kontrollera 276 det måste man jämföra innehållet i databasen före och efter ändringen.
SQL-standarden innehåller så kallade assertions för att specificera en del allmänna semantiska integritetsvillkor. När man specificerat ett sådant villkor, kommer databashanteraren sen att kontrollera villkoret automatiskt, på samma sätt som med nyckelvillkoren och referensintegriteten som vi beskrev ovan.
Vi tänker oss att tabellen anställd innehåller varje anställds lön och närmaste chef:
Anställda | |||
---|---|---|---|
Nummer | Namn | Lön | Chef |
1 | Svea | 34 000 | null |
2 | Sten | 28 000 | 1 |
3 | Bengt | 25 000 | 2 |
create assertion checksalary
check (not exists (select *
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Anställd as proletär,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xAnställd as boss
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere proletär.Chef = boss.Nummer
and proletär.Lön > boss.Lön));
Det är alltså ett villkor, uttryckt i SQL, som databashanteraren nu har i uppgift att kontrollera, och hela tiden se till att det är sant. Om någon transaktion försöker ändra i databasen så att någon får högre lön än sin chef, kommer den transaktionen att avbrytas.
Här bör sägas att generella assertions inte tillåts av alla databashanterare. Dessutom kan de resultera i synnerligen långsamma uppdateringar. En enkel men dum implementering av ovanstående assertion är att köra frågan för varje uppdatering. Det blir dock mycket långsamt eftersom hela tabellen då måste genomsökas. Emellertid kan ovanstående assertion testas effektivt genom att analysera villkoret. (Övning: Hur?) Man bör kolla att ens databashanterare verkligen testar villkoret på ett effektivt sätt, för annars lär man få klagomål. Den dumma metoden skulle ju medföra att databashanteraren tar väldigt lång tid på sig att göra enkla ändringar i Anställdatabellen.
Ett villkor som skapats medcreate assertion
kan referera till flera olika tabeller. Om villkoret bara refererar till en enda tabell är det ett lokalt check-villkor som specificeras som ett check-villkor inuti create table
-kommandot. (Se sidan 149.)
Dynamiska villkor, som begränsar vilka ändringar som får göras i databasen, kan normalt inte uttryckas med en assertion. Det beror på att man måste titta på tillståndet i databasen både före och efter ändringen för att kunna kontrollera villkoret. Dynamiska villkor kan dock hanteras av triggers, vilket vi ska titta på i nästa avsnitt.
Därifrån är inte steget långt till att införa en liknande mekanism, inte bara för integritetsvillkor, utan för vilka villkor som helst. Man skriver en aktiv regel, eller trigger, som anger ett villkor och en åtgärd, och när det villkoret är uppfyllt, så kommer databashanteraren att utföra åtgärden. Då har vi det som kallas en aktiv databas. (Läs mer om aktiva databaser i kapitel 16.)
Om vi använder en aktiv databas som låter oss ange triggers, kan vi skriva en trigger som kontrollerar villkoret att löner bara kan höjas: 3
create trigger salarycheck
after update on Anställda
referencing old table as o new table as n
for each row
begin atomic
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare oldsalary integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare newsalary integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare blaj integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Lön into oldsalary from o;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Lön into newsalary from n;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif (newsalary < oldsalary) then
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xset blaj = 0 / 0;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xend if;
end;
Om någon transaktion försöker ändra kolumnen Lön i tabellen Anställda så att det nya värdet blir lägre än det gamla, kommer regeln att utlösas. Eftersom den då försöker dividera med noll, uppstår ett fel, och transaktionen avbryts. 4
I en aktiv databashanterare kan man göra mer än bara avbryta transaktionen. Man kan till exempel ändra ovanstående trigger så 279 att lönen automatiskt höjs med 10 procent om någon försöker sänka den. Ofta finns så kallade lagrade procedurer, som är små (eller stora) programsnuttar som man kan lagra i databasen, och som kan köras när villkorsdelen av en regel är uppfylld. Reglerna kan därför användas till mycket annat än att bara kontrollera integritetsvillkor. Till exempel kan en aktiv databashanterare automatiskt beställa mer varor när lagret i en butik börjar bli tomt.
Det finns två olika sätt att ange integritetsvillkoren i en databas: procedurellt och deklarativt.
Alternativet är att ange integritetsvillkoren procedurellt, dvs med vanlig programkod som kontrollerar att de är uppfyllda. Det kan vara i ett programmeringsspråk som Java, Python eller C i ett applikationsprogram, eller kanske inuti en lagrad procedur i databasen. Programmeraren måste skriva programkod som kontrollerar varje ändring som ska göras, så att den uppfyller alla integritetsvillkor.
Helst vill man att databashanteraren ska sköta om kontrollen, automatiskt och utan att användare eller applikationsprogrammerare behöver bry sig. Det har flera fördelar med det:
Ibland måste man förstås göra kontrollen procedurellt, till exempel om villkoren är för komplicerade för att uttrycka i enkla regler, eller för att man vill åtgärda brott mot reglerna på ett mer avancerat sätt än vad databashanteraren klarar av: man kanske vill skicka e-post till den lokala fackklubben om någon försöker sänka lönen, eller epost till bossen om någon försöker höja den. Dessutom kan det ju hända att just den databashanterare man använder inte har stöd för alla integritetsvillkor man behöver.
Ibland vill man också göra en procedurell kontroll av integritetsvillkoren, förutom att specificera dem deklarativt, för att kunna ge bättre felmeddelanden till användaren. När databashanteraren upptäcker att man försöker ta bort en avdelning där det fortfarande finns anställda, kanske den spottar ur sig ett felmeddelande i stil med Referential constraint SQL_FOREIGN_KEY_0000064503 violated, och man vill i stället meddela användaren att Det går inte att ta bort avdelningar där det fortfarande finns anställda. Då kan man behöva kontrollera om det finns anställda på avdelningen som ska tas bort, redan innan man försöker ta bort den.
Integritetsvillkor, vare sig de kontrolleras av databashanteraren eller av annan programkod, kan vara en stor hjälp för att hålla databasen konsistent, och undvika att motsägande eller felaktiga uppgifter lagras i databasen. Men se upp med att man senare under databasens drift kanske vill kunna lägga in uppgifter som man inte trodde skulle behöva lagras, och som man därför skapat integritetsvillkor som förbjuder.
Ta en idrottsklubb som har medlemsavgifter, som varje medlem måste betala. Därför lägger vi in ett referensintegritetsvillkor från tabellen Medlemmar till tabellen Betalningar, så ingen medlem kan 281 finnas i databasen utan motsvarande betalning. Men vad händer när vi vill utse någon till hedersmedlem, som inte behöver betala medlemsavgift? Där skulle man kanske inte varit så snabb med att skriva not null. Vilken tur att man kan ändra sig medalter table
och ta bort not null
efteråt utan att bygga om databasen.
Tänk för varje integritetsvillkor noga igenom om det verkligen är säkert att det aldrig får brytas. Integritetsvillkoren ingår i databasens schema, och den kan vara svårt att ändra schemat när databasen väl satts i drift 5 Tänk också på att världen ändras med tiden, och databasen kanske finns kvar länge. Om tjugo år, när lönerna höjts av inflationen, är det förmodligen normalt att tjäna över 70 000. 6
Hittills har vi talat om integriteten för uppgifter i databasen, och att de inte får strida mot integritetsvillkoren. Men alla databashanterare har också olika typer av interna datastrukturer. Till exempel finns det index, som inte är synliga för den vanliga användaren, men som används av databashanteraren när den söker efter data i databasen. Indexen, och alla andra interna datastrukturer, måste förstås vara korrekta.
Varje index måste stämma överens med den riktiga tabell som det pekar in i. Om man lägger till eller tar bort rader i tabellen, och indexet av någon anledning inte ändras för att reflektera detta, så kan databashanteraren kanske bli så förvirrad att den kraschar.
Därför måste databashanteraren vara mycket noga med att upprätthålla integriteten på de interna datastrukturerna. Annars kanske man inte längre kan komma åt några data alls i databasen.
I det här kapitlet har vi pratat om "integritet" i betydelsen "dataintegritet", som ungefär innebär "utan inre motsägelser" eller "stämmer med integritetsvillkoren för den här databasen". Men som vi redan nämnt används det svenska ordet "integritet" ofta i en annorlunda betydelse, som när man pratar om "personlig integritet". Personlig integritet heter "privacy" på engelska, och det betyder ungefär att uppgifter om mig, till exempel min adress och min lön, och min religion och mitt brottsregister, inte ska vara tillgängliga för vem som helst hur som helst. Jag ska få ha mitt privatliv i fred.
Vi tar inte upp så mycket om personlig integritet här, men vi ska i alla fall nämna de svenska personnumren, som ibland ger upphov till både debatt och förvirring. Är personnummer hemliga? Är författaren dum när han nu talar om att hans personnummer är 631211-1658?
Nej, svenska personnummer är inte hemliga. De är inte alls hemliga. Om du vet namn och adress på en person, så att det går att avgöra vem det är, kan du ringa till skattemyndigheten och få den personens personnummer. Du behöver inte tala om vem du är eller vad du ska ha personnumret till.
Eventuella problem och risker med personnummer handlar inte om att personnummer är hemliga, utan om att de är unika. De fungerar som en nyckel eller ett unikt namn, inte som ett lösenord.
Om det finns en fara med personnummer så är det att det blir lätt att se att den där 631211-1658 som skriver databasböcker är samma person som den där 631211-1658 som har årskort på Klubb Läderhamster. Och det kanske jag inte vill att alla ska veta. Därför vill jag kanske inte att Klubb Läderhamster ska använda mitt personnummer som medlemsnummer, och trycka det på medlemskortet.
När detta skrivs är dataskyddsförordningen ännu inte införd, och därför har vi ingen erfarenhet av hur den kommer att fungera i praktiken. Men den har stora likheter med PUL. Nyheterna är till stor del byråkratiska, om hur personregister ska dokumenteras och övervakas.
Dataskyddsförordningen reglerar alla typer av personregister. Bland annat begränsar den hur man får lägga ut uppgifter om andra personer på webben. Det är till och med förbjudet att lägga ut bilder på andra personer – om man inte skaffat ett utgivingsbevis för webbplatsen, med en ansvarig utgivare, för då gäller inte dataskyddsförordningens regler, utan i stället samma regler som för en tidning.
284Den svenska offentlighetsprincipen är också viktig i sammanhanget. Den säger att allmänna handlingar, dvs information i olika former som skapats av, inkommit till, eller bara förvaras hos en svensk myndighet eller kommunalt bolag, och som inte särskilt blivit sekretessbelagd, är offentlig, så att vem som helst har rätt att få se den. Till exempel blir polisutredningar, så kallade förundersökningsprotokoll, offentliga i samband med rättegångar, och man kan få dem på papper eller som pdf-fil från domstolen. Särskilt känsliga uppgifter, som bilder på brottsoffer, brukar då ha blivit sekretessbelagda, och därför borttagna ur den kopia man får.
Integritet. Se antingen dataintegritet eller personlig integritet.
Dataintegritet (engelska: integrity, data integrity). Att innehållet i en databas ska hänga ihop på rätt sätt, och inte innehålla motsägelser.
Personlig integritet (engelska: privacy, privacy of information). Att enskilda människor ska få ha sitt privatliv i fred, genom att uppgifter om dem inte är tillgängliga hur som helst.
Integritetsvillkor (engelska: integrity constraint). En regel som talar om vilka data som kan lagras i databasen, till exempel regeln att varje anställd måste ha ett unikt nummer, eller regeln att varje bil måste ha en och endast en person som ägare.
Nyckelvillkor (engelska: key constraint). Villkoret att en kandidatnyckel alltid måste ha unika värden för alla rader i en tabell.
Referensintegritet (engelska: referential integrity). Om två tabeller är hopkopplade med referensattribut, ska det värde som refereras till alltid existera. Om det till exempel står i tabellen Anställda att en viss anställd jobbar på avdelning nummer 17, ska det också finnas en avdelning med nummer 17 i tabellen Avdelningar.
Aktiv databas, aktiv databashanterare (engelska: active database, active DBMS). En databashanterare där man inte bara kan stoppa in data, och söka i dem, utan där man också kan ange regler, 285 så kallade triggers, för att databashanteraren själv ska göra saker (till exempel ändra på data) när vissa villkor är uppfyllda.
1 Se upp med att detta inte fungerar i MySQL! Man kan skriva så, och man får inget felmeddelande om det, men inget integritetsvillkor skapas.
Innehållet i en databas kan vara mycket värdefullt. Data som försvinner eller skadas kan vara svåra, dyra eller till och med omöjliga att ersätta. Ett postorder- eller webbföretag som tappar bort en enda dags beställningar från kunderna gör kanske miljonförluster, och förlorar kundernas förtroende. Ett företag som blir av med sitt kundregister kommer kanske inte att överleva.
Det är inte bara förlust av data som kan orsaka skador och stora kostnader. Även felaktiga ändringar, eller till och med bara möjligheten att det kan ha skett felaktiga ändringar, kan bli dyra. Tänk till exempel på en bank, där någon obehörig lyckas ta sig in och ändra i de summor som finns på bankkontona. Eller tänk på en bank, där databasen var oskyddad några dagar, och där någon obehörig skulle ha kunnat gå in och ändra i bankkontona.
Det behöver inte handla om avsiktlig förstörelse av databasen. Misstag är lätta att göra, och ju fler personer som har möjlighet att göra ändringar, desto större är risken att någon ska råka göra en felaktig ändring.
En del data är dessutom hemliga, så att det räcker med att någon obehörig fått ta del av dem, utan att ha gjort några ändringar. Ett företags kundregister som kommer i händerna på en konkurrent kan orsaka stora förluster för företaget, för att inte tala om vilket obehag det kan innebära för kunderna. Om databasen innehåller kreditkortsnummer, och de sprids, kan det orsaka mycket skada. Eller ta FBI:s databas över var de gömt undan personer som vittnat i maffiarättegångar.
288Det finns många exempel på databaser som kommit på vift, till exempel med kreditkortsnummer, men ett särskilt spektakulärt exempel är USA:s Office of Personnel Management, dvs kontoret för personaladministration, som 2015 råkade ut för ett intrång. Obehöriga fick tillgång till, och kopierade, deras databas med detaljerade uppgifter om alla som antingen var eller hade varit anställda av den amerikanska federala administrationen eller hade genomgått säkerhetskontroller för skyddsklassade arbeten. Det var över 20 miljoner personer, bland annat alla USA:s militärer och alla anställda på FBI och CIA.
Ytterligare en risk berör databasens tillgänglighet, dvs att det faktiskt går att komma åt databasen när man behöver den. Även om data varken skadats eller kommit på avvägar, kan stora skador och kostnader uppstå om systemet är "nere".
Allt det här gör att databasens data måste skyddas. Särskilt viktigt, och svårt, är det eftersom många databaser har flera användare, och olika användare kanske ska få se, och inte se, olika delar av databasen.
De flesta relationsdatabashanterare har liknande funktioner, men betydligt mer avancerade än i ett operativsystem. Databashanteraren tillåter olika användare att logga in och arbeta med databasen, och styr sedan deras åtkomst till tabellerna, men till skillnad mot filåtkomst i de flesta operativsystem kan databashanteraren styra vilka rader och vilka kolumner som användarna får komma åt. Den kan också skilja mellan rätten att lägga till nya poster, rätten att ändra i existerande poster, och rätten att ta bort poster.
289I en databashanterare som skiljer på olika användare, måste användaren identifiera sig med någon form av inloggning innan hon över huvud taget kommer in i databasen. Det vanligaste är att man får ange användarnamn och lösenord, på samma sätt som när man loggar in i andra typer av datasystem.
Det är förstås databasadministratören, DBA, som ansvarar för säkerheten. Som vilken systemansvarig som helst ska DBA lägga upp användare, och ge dem rättigheter.
Obligatoriska säkerhetsmekanismer går ut på att dela in såväl användare som data i olika nivåer, till exempel nivåerna unclassified, confidential, secret och top secret. En användare som har säkerhetsklassen secret får då se data av klasserna unclassified, confidential och secret, men inte top secret. Att detta kallas obligatoriska säkerhetsmekanismer beror på att allt och alla måste tillhöra en viss nivå. Det går inte att lägga in data som man struntar i att klassificera, och det går inte att ge just den där användaren, som visserligen bara har säkerhetsklassen secret, tillgång till just dessa top secret-data, som hon behöver tillgång till i sitt arbete.
Obligatoriska säkerhetsmekanismer, med säkerhetsnivåer, ger ett säkert men oflexibelt system. Det används mest inom den amerikanska militären och liknande organisationer, som arbetar med känslig information och har höga krav på säkerhet. De brukar inte finnas med i vanliga databashanterare, utan bara i särskilda "extrasäkra" system.
Valfria säkerhetsmekanismer är mer flexibla. Man kan direkt styra vilka användare som får tillgång till vissa data, utan att behöva bry sig om några säkerhetsnivåer. Den vanligaste mekanismen för detta är SQL-kommandona grant och revoke. Eftersom det är det vanligaste i databastillämpningar, åtminstone utanför militären, går vi igenom det noggrannare i nästa stycke.
Den som har rättighet att skapa tabeller i databasen kan också dela ut rättigheter till tabellerna. Med SQL-kommandot grant kan hon ge åtkomst av olika slag per tabell (eller vy) och per användare. De rättigheter som finns är främst dessa:
exempel drop, som ger rätt att ta bort hela tabellen.
grant select on Anställda to svante;
revoke select on Anställda from svante;
grant update on Anställda to svante;
grant update(Lön) on Anställda to svante;
grant insert, delete on Anställda to svante;
Kommandona är standardiserade i SQL-standarden, men detaljerna i syntaxen kan ändå variera mellan olika databashanterare. Här är ett exempel från databashanteraren MySQL. Exemplet ger användaren studentbasenuser rätt att göra allt med tabellerna i databasen studentbasen, om hon är inloggad på samma dator som databasservern. Om hon arbetar med databasen via nätverket, från någon annan maskin, får hon komma åt innehållet i tabellerna, men inte ändra på det. För att identifiera användaren krävs i bägge fallen ett lösenord.
grant select,insert,update,delete,create,drop
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xon studentbasen.*
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xto studentbasenuser@localhost
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xidentified by 'spazoghooop';
grant select on studentbasen.*
to studentbasenuser@'%' identified by 'kobfnorbl';
291
(Exemplet råkar vara från en webbtillämpning, där en webbplats visar data som hämtas ur en databas (se kapitel 19). Då är det webbservern, och inte de enskilda användarna, som ansluter till databasen. Om webbservern kör på samma dator som databashanteraren, tillåter ovanstående grant
-kommandon att man ändrar innehållet i tabellerna med de funktioner som finns på webbplatsen.)
grant
-kommandot ange att användaren bara får hämta data från vissa rader i tabellen.
Man kan dock åstadkomma den sortens mer finkornig åtkomstkontroll med hjälp av vyer. Det går att definiera en vy, och sen ge åtkomsträttigheter för vyn, men inte för de tabeller som den baseras på. Till exempel vill vi kanske låta användaren kajsa se nummer och namn på de anställda som arbetar på dataavdelningen och som har lägre lön än 100 000, utan att ge henne rätt att se hela Anställda-tabellen. Då skapar vi en vy, och ger henne select-rättigheter till den vyn:
create view DataAnställda
as select Nummer, Namn
from Anställda
where Avdelning = 'Data'
and Lön < 100000;
grant select on DataAnställda to kajsa;
En finess med grant-kommandot är att man kan ge en användare rätt att i sin tur dela ut rättigheter.
292Antag att databasadministratören ger användaren svante rätt att lägga till och ta bort rader i tabellen Anställda. DBA skriver:
grant insert, delete on Anställda to svante
with grant option;
Nu kan Svante ge även användaren kurt rätt att lägga till rader i tabellen. Svante skriver:
grant insert on Anställda to kurt;
Nu har alltså Svante rätt att lägga till och ta bort rader, och Kurt har rätt att lägga till rader.
Databashanteraren måste nu hålla reda på vem som delat ut vilka rättigheter till vem. Om databasadministratören återtar en rättighet från Svante, ska den även återtas från eventuella andra användare som Svante skickat rättigheten vidare till. Antag att databasadministratören tar bort Svantes insert-rättighet. DBA skriver:
revoke insert on Anställda from svante;
Att dela ut rättigheter till enskilda tabeller och kolumner till varje användare kan bli ganska plottrigt. Det finns ett behov av att kunna dela ut "paket" av rättigheter till användarna.
En lösning på det är roller, som finns i SQL-standarden från och med SQL:1999. Med kommandotcreate role
kan man skapa en roll, till exempel sekreterare, som ger ett paket av rättigheter som man kan dela ut till de användare som är sekreterare.
En statistisk databas är en databas som främst används för att sammanställa statistik av olika slag. Databasen kanske innehåller löner för alla personer i Sverige, men vi är normalt inte intresserade av en enskild persons lön, utan vi vill kunna göra sammanställningar, till exempel för att få reda på hur genomsnittslönen för olika yrkesgrupper skiljer sig mellan olika delar av landet.
Eftersom databasen kan innehålla uppgifter om individer som är känsliga, vill man inte tillåta åtkomst av individdata, men ändå ge möjlighet till statistiska sammanställningar. Man ska kunna få fram till exempel genomsnittslönen för läkare i Linköping, men man ska inte kunna slå upp grannens lön.
select avg(Lön)
from Personer
where Yrke = 'läkare'
and Ort = 'Linköping'
and Kön = 'man'
and Födelseår = '1978'
and Bostad = 'villa'
and Bil is null
and Favoritmaträtt = 'blodpudding'
294
• Ett motmedel mot den sortens en-persons-avgränsningar är att endast tillåta aggregatfunktioner som baseras på en grupp med minst ett visst antal personer, till exempel tre. Då går det inte längre att direkt få fram en viss persons lön. Tyvärr räcker inte det heller, i det generella fallet, för med hjälp av flera frågor med olika grupper kan man fortfarande sluta sig till grannens lön:
select sum(Lön)
from Personer
where Bil = 'Volvo';
select sum(Lön)
from Personer
where (Yrke = 'läkare'
and Ort = 'Linköping'
and Kön = 'man'
and Födelseår = '1978'
and Bostad = 'villa'
and Bil is null
and Favoritmaträtt = 'blodpudding')
or Bil = 'Volvo';
• För att man inte ska kunna sluta sig till enskilda personers data genom att ställa flera olika frågor, kan man hindra följder av frågor som upprepade gånger refererar till samma personer. Ett annat sätt är att databashanteraren avsiktligt lägger på ett litet, slumpmässigt fel i aggregatfunktionerna. Ytterligare ett sätt är att partitionera databasen, så att det som lagras inte är data om enskilda personer utan om små grupper av personer.
SQL-injektion (SQL injection på engelska) är en säkerhetsrisk som man måste tänka på när man skriver ett tillämpningsprogram eller en webbplats som arbetar med en databas, och där användare får mata in data i någon form av textfält. SQL-injektion innebär att användaren skriver in särskilda tecken i sin inmatning, och därigenom lurar tillämpningsprogrammet att köra helt andra SQL-frågor än vad programmeraren hade tänkt sig. Det kan inträffa i ett system där inmatningen textmässigt stoppas in i SQL-frågans text, och där alltihop sen skickas som text till SQL-gränssnittet.
Som exempel kan vi ta en webbplats för att söka i telefonkatalogen. Telefonkatalogen består av tabellen Abonnenter, med kolumnerna Namn, Adress, Telefonnummer och HemligtNummer, där HemligtNummer är en flagga som är satt till true för hemliga nummer. Hemliga nummer får inte visas.
Antag nu att webbplatsen fungerar så att användaren matar in ett namn i ett formulär, och sen visas namn, adress och telefonnummer för alla abonnenter som har det namnet – men bara de som inte har hemligt telefonnummer. Programmeraren som byggde webbplatsen har skrivit den här SQL-frågan:
select Namn, Adress, Telefonnummer
from Abonnenter
where Namn = '$SÖKT_NAMN'
and HemligtNummer = false;
SÖKT_NAMN
är en variabel, som webbservern kommer att ersätta med det namn som användaren matar in. Om man matar in namnet Olle Karlsson, kommer alltså den här SQL-frågan att köras:
select Namn, Adress, Telefonnummer
from Abonnenter
where Namn = 'Olle Karlsson'
and HemligtNummer = false;
Men vad händer om en lömsk användare i stället matar in det underliga namnet Olle Karlsson' or 'a'='a' or 'a'='a? Jo, precis som förut stoppas namnet in i SQL-frågan, som då ser ut så här:
select Namn, Adress, Telefonnummer
from Abonnenter
where Namn = 'Olle Karlsson' or 'a'='a' or 'a'='a'
and HemligtNummer = false;
296
Citationstecknen som användaren matade in i namnet gör att frågan gör något helt annat än vad programmeraren hade tänkt. Eftersom jämförelsen 'a'='a'
alltid är sann och operatorn and har högre prioritet än operatorn or, kan vi förenkla where
-villkoret till true
. Det är alltså alltid sant, och vi får fram samtliga abonnenter, inklusive dem som har hemligt nummer!
En varning: Det är inte bara textfält som är känsliga. Även numeriska fält måste kontrolleras. Antag att programmeraren har tänkt att användaren ska mata in numret på en kund, och så ska den kunden raderas ur databasen. Användaren skriver in kundnumret i en ruta, kundnumret läggs i variabeln BORTNUMMER
, och så stoppas BORTNUMMER
in i den här SQL-frågan:
delete from Kund where Nummer = $BORTNUMMER;
Vad händer nu om användaren, av elakhet eller av misstag, matar in texten Nummer i rutan? Jo, SQL-frågan kommer att se ut så här:
delete from Kund where Nummer = Nummer;
Alla rader i tabellen Kund försvinner.
Man slipper problemen med SQL-injektion om man kan använda tillämpningsprogramsgränssnitt som ODBC, JDBC eller ESQL på så sätt att man inte skickar dynamiskt konstruerade SQL-satser som text till databashanteraren, utan i stället förkompilerar satserna och sedan skickar parametrar till de förkompilerade SQL-satserna till databashanteraren. Att göra så blir också betydligt effektivare. Mer om detta följer i kapitel 21.
Grant. Ett kommando i SQL som används för att ge en användare rättigheten att göra vissa saker med en databas. Se även motsatsen revoke.
Revoke. Ett kommando i SQL som används för ta ifrån en användare rättigheten att göra vissa saker med en databas. Se även motsatsen grant.
297SQL-injektion. Genom att mata in konstiga data i ett program eller på en webbsida kan man lura programmet eller webbsidan att köra oönskade SQL-frågor mot en databas.
Många databashanterare erbjuder lagrade procedurer. En lagrad procedur är inte en del av ett program som kopplar upp sig mot databasservern, utan själva proceduren lagras i servern, och proceduren kan sen köras av servern oberoende av några externa program. Det kan ske antingen genom ett direkt kommando från en användare eller ett program, genom anrop från en annan procedur, eller genom att en trigger (kapitel 16) i databasen anropar proceduren.
När man bakar in SQL-satser i ett vanligt språk, med till exempel ODBC eller ESQL (se kapitel 21), är det ofta tydligt att det handlar om två olika språk. Det kan vara krångligt att till exempel överföra data från en SQL-fråga till det omgivande programmet. Lagrade procedurer skrivs i stället med ett språk som är skapat som en utökning av SQL, och det gör att man kan integrera SQL och resten av språket på ett bättre sätt, till exempel så att SQL-frågorna och de andra delarna av programmet kan använda samma variabler.
300I en del moderna databashanterare är det i stället (eller dessutom) möjligt att definiera funktioner i ett vanligt programmeringsspråk som C eller Java, som sedan kan användas i SQL-frågor. Det brukar kallas användardefinierade funktioner eller UDF (efter engelskans User-Defined Function). Användardefinierade funktioner ger stor kraft att utvidga databashanteraren, men också risk för att man förstör något, till exempel genom att skriva sönder minnet i C. Vidare är språk som C och Java inte så väl lämpade för att skriva databasoperationer, jämfört med en utvidgning av SQL.
Tyvärr varierar implementationen av lagrade procedurer mellan olika databashanterare, såväl när det gäller vad de klarar av som exakt hur man skriver. I SQL-standarden (från SQL:1999) talar man om persistent stored modules (PSM eller SQL/PSM), vilket är standardens namn på lagrade procedurer. Där definieras ett visst skrivsätt, och de flesta databashanterarnas SQL-dialekter kommer förmodligen så småningom att anpassas efter detta.
Här är ett enkelt exempel på en lagrad procedur, som följer syntaxen som anges av SQL-standarden. Proceduren räknar ut genomsnittslönen för de anställda, och sänker sedan lönen för alla som har högre lön än så, genom att sätta deras lön lika med genomsnittslönen. För att skapa proceduren använder vi kommandot create procedure: 1
create procedure Utjamning()
modifies sql data
begin
301
declare medel integer;
select avg(Lön) into medel from Anställda;
update Anställda set Lön = medel
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere Lön > medel;
end
Vi har angett modifies sql data
, som anger på vilket sätt proceduren använder sig av databasens data. De alternativ som finns är:
Modifies sql data
, som betyder att proceduren både får läsa och ändra i data.
(Det måste den här proceduren göra, i och med att den ska ändra på lönerna i Anställda-tabellen.)
Reads sql data
, som betyder att proceduren får läsa, men inte ändra i, data.
Contains sql
, som betyder att den varken läser eller ändrar i databasens data, vilket kanske inte är så användbart.
2
select
-sats och en update
-sats mitt i alltihop. Man kan alltså blanda det som brukar kallas procedurell programmering, det vill säga vanlig steg-för-stegprogrammering, med SQL-satser. Notera hur SQL-satserna (select
och update
) kan använda samma variabler som resten av proceduren.
En viktig skillnad mellan procedurer och resten av SQL är att variabler i procedurer får vara bundna till vilken datatyp som helst, medan bland annat SQL:s select&
#x002D;sats bygger på radkalkyl (se kapitel 10) där variabler bara får vara bundna till rader i tabeller. Ovan är till exempel variabeln medel
bunden till ett heltal. Ursprunget till procedurer är "vanliga" programmeringsspråk, medan det mesta av SQL baseras på predikatkalkyl.
Vi provar att köra proceduren med ett direkt kommando. Kommandot heter call:
call Utjamning();
302
De olika databasgränssnitten för tillämpningsprogram (även kallade API:er, se kapitel 21) brukar ha särskilda mekanismer för att anropa lagrade procedurer. I JDBC använder man en speciell klass, CallableStatement, och liknande konstruktioner finns i ODBC och ESQL.
En del programmeringsspråk, som Pascal, skiljer på olika typer av subrutiner: de som returnerar ett värde, och de som inte gör det. Subrutiner som returnerar ett värde kallas funktioner, och subrutiner som inte returnerar något värde kallas procedurer. Det gäller även SQL. (Andra språk, som C, kallar alla subrutiner för funktioner, oavsett om de returnerar något eller inte.)
En funktion i SQL returnerar alltså ett värde:
create function kvadrat(i integer)
returns integer
contains sql
begin
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn i * i;
end
Vi kan använda funktionen i uttryck, till exempel i en vanlig select
-sats:
select Lön, kvadrat(Lön) from Anställda;
Lön | kvadrat(Lön) |
---|---|
29 000 | 841 000 000 |
28 000 | 784 000 000 |
25 000 | 625 000 000 |
select
-satser, utan bara från andra procedurer, från tillämpningsprogram och från triggers. Vidare får funktioner inte uppdatera databasen för man får inte ha sidoeffekter i select
-satser.
303
update Anställda set Lön =
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x(select avg(Lön) from Anställda)
where Lön > (select avg(Lön) from Anställda);
with recursive
i select-satsen. Om databashanteraren inte har with recursive
är ett alternativ att göra en lagrad funktion som hittar superbossen. Vi kan alltså skriva en lagrad funktion som stegar sig igenom hierarkin med hjälp av SQL:s vanliga select
-satser och en repetitionssats. Superboss här nedan tar numret på en anställd som indata, och som utdata returnerar den numret på denne anställdes högsta chef. Vi gör Superboss som en funktion och inte som en procedur, för att det ska gå att använda den inuti select
-satser.
Vi använder MySQL-syntax när vi definierar funktionen Superboss. Eftersom man inte bör använda svenska bokstäver i tabell- och kolumnnamn med MySQL, har vi bytt ä
mot a och ö
mot o
. Notera också att man temporärt måste ändra satsavslutaren till exempelvis $
när man skapar funktioner i MySQL, eftersom funktionsdefinitioner innehåller tecknet ;.
delimiter $
create function Superboss (AnstalldNummer integer)
returns integer
begin
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare nr integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare boss integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x-- Finns denna anstallda alls i databasen?
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Nummer, Chef into nr, boss
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Anstallda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere Nummer = AnstalldNummer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif (nr is null) then
304
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn null; -- Nej, det gjorde hon inte
elseif (boss is null) then
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn nr; -- Jo, men hon ar sin egen chef
else
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhile (boss is not null) do
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xset nr = boss;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Chef into boss
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Anstallda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere Nummer = nr;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xend while;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn nr;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xend if;
end;
$
delimiter ;$
Som synes innehåller funktionen flera select
-satser, ett par lokala variabler, en repetitionssats (med nyckelordet while
) och en villkorssats (med nyckelordet if
). Det är kanske inte nödvändigt att gå in i detalj på hur proceduren är uppbyggd, men notera hur konstruktionerna för val och repetition, och de lokala variablerna, kan blandas med vanlig SQL.
Nu kan vi använda funktionen för att lista varje anställds högsta chef. (Om det finns flera separata chefshierarkier i företaget, kan olika anställda ha olika högsta chefer.)
select Nummer, Namn, Superboss(Nummer)
from Anstallda;
Nummer | Namn | Superboss(Nummer) |
2 | Stina | 2 |
3 | Sam | 2 |
4 | Lotta | 2 |
1 | Olle | 2 |
8 | Maria | 2 |
9 | Ulrik | 2 |
10 | Petter | 2 |
with recursive
i stället, fast då måste förstås databashanteraren tillåta rekursiva frågor.
305
Det går också att skriva som en lagrad procedur, men då kan man inte använda proceduren i frågor.
Eftersom lagrade funktioner och procedurer körs inuti databasservern, kan man göra både beräkningar och bearbetningar av data på databasservern. Det ger flera fördelar:
select
-fråga, oavsett om den finns inuti i en lagrad procedur eller någon annanstans, kan ibland producera svar med väldigt många rader. Det är kanske till och med mer troligt inuti procedurer, eftersom man där kan arbeta med ett mellanresultat, snarare än ett slutligt svar som ska visas för användaren (och som därför, kan man anta, inte skulle få innehålla alltför många miljoner rader).
För att slippa att skapa hela resultatet och lagra det (vilket brukar kallas att materialisera datamängden), kan man i SQL använda en cursor, som är en speciell markör som anger vilken av raderna i resultatet som man just nu arbetar med. (Läs mer om cursors i avsnitt 21.9.) På det viset behöver man inte köra frågan på ett sätt som tar fram alla rader på en gång, utan man kan köra den strömorienterat, så att frågan producerar en rad i taget. Det blir mer skalbart, vilket innebär att det fungerar med rimliga prestanda även för stora datamängder.
Följande procedur beräknar medelvärde och varians av lönerna i en avdelning:
create procedure lönestatistik(
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xin avd varchar(10),
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xout medel real,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xout varians real)
reads sql data
begin
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare lön integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare antal integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare summa integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare kvadratsumma integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare rader integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare c cursor for
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Lön from Anställda, Avdelningar
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere JobbarPå = Avdelningar.Nummer
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xand Avdelningar.Namn = avd;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xset antal = 0;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xset summa = 0.0;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xset kvadratsumma = 0.0;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xopen c;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xL:
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xloop
307
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfetch c into lön;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xget diagnostics rader = row_count;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif rader = 0 then
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xleave L;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xend if;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xset antal = antal + 1;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xset summa = summa + Lön;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xset kvadratsumma =
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xkvadratsumma + Lön * Lön;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xend loop;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xset medel = summa / antal;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xset varians = kvadratsumma / antal
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x- kvadrat(summa / antal);
end
Declare
-satsen för cursorn c
innehåller själva select
-frågan, men det är först i open-satsen som (den redan kompilerade och optimerade) frågan faktiskt börjar köras, och börjar returnera raderna i resultatet. Varje fetch
-sats hämtar en ny rad för bearbetning.
Som alternativ till lösningen ovan med declare
-satsen, open
-satsen och loopen med fetch
, kan man använda den enklare for
-satsen:
for c as
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Lön from Anställda, Avdelningar
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere JobbarPå = Avdelningar.Nummer
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xand Avdelningar.Namn = avd
do
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xset antal = antal + 1;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xset summa = summa + Lön;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xset kvadratsumma =
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xkvadratsumma + Lön * Lön;
end for;
Lagrad procedur (engelska: stored procedure). En programsnutt som man kan lagra i databasen, och som sen kan anropas och köras. SQL-standarden innehåller ett särskilt språk för lagrade procedurer, och det ser ut som en blandning mellan SQL och ett vanligt 308 programmeringsspråk. Vissa databashanterare tillåter att man utvidgar systemet med egen kod skriven i ett vanligt programmeringsspråk. Sådana utvidgningar kallas användardefinierade funktioner eller UDF:er. Att göra UDF:er i C kräver mycket hög programmerings- och databaskompetens.
1 När man matar in detta i ett SQL-textgränssnitt, måste man oftast avgränsa create procedure
-kommandot på något sätt.
Normalt används semikolon för att avsluta ett SQL-kommando, men eftersom det kan finnas flera semikolon inuti create procedure
-kommandot, måste detta kommando avslutas på något annat sätt.
Ett exempel är databashanteraren Mimer, där man ska skriva tecknet @ både för att inleda och avsluta create procedure
-kommandot.
I en del andra databashanterare, som MySQL, kan man ge kommandot delimiter
för att tillfälligt byta satsavslutare från semikolon till något annat, och efteråt använder man delimiter
på nytt för att byta tillbaka till semikolon.
I en databas som hanteras av en databashanterare kan man lagra data, och sen söka bland dessa data och göra olika typer av sammanställningar. Det är användaren, eller eventuellt ett program som kopplar upp sig mot databasen, som är aktiv, och databasen är passiv. Den gör inget på eget initiativ.
Det fungerar så att man definierar aktiva regler, ofta kallade triggers, där man anger ett villkor och en åtgärd. Databashanteraren kommer att kontrollera villkoret, och så fort det är uppfyllt utförs åtgärden.
Åtgärden kan innefatta olika saker:
Många moderna databashanterare har triggers, bland andra Oracle, Db2, Microsoft SQL Server, MySQL och Mimer. Microsoft Access, som har mer begränsad funktionalitet, har inte triggers.
Man brukar tala om ECA-regler, som innehåller tre delar:
insert
), om en eller flera rader tas bort (med delete
), eller om innehållet på en eller flera rader ändras (med update
). Regeln är normalt knuten till en viss tabell, så att regeln bara kontrolleras om just den tabellen ändras. När händelsen inträffat kontrollerar databashanteraren om villkoret är uppfyllt. Om villkoret är uppfyllt utförs åtgärden, som kan vara ett SQL-kommando som till exempel ändrar på innehållet i en tabell.
Det har även funnits system med CA-regler, som alltså inte är knutna till någon händelse, utan bara har ett villkor och en åtgärd. Det är svårare att bygga en databashanterare som klarar CA-regler, för då är det ju inte bara vid vissa händelser som regeln måste kontrolleras, utan villkoret kan bli uppfyllt när som helst. Vidare är det besvärligt att få reda på varför en regel körs eftersom man i CA-reglerna inte har tillgång till de händelser som inträffat.
create trigger
.
311
I kommandot anges en händelse och en kropp, som innehåller åtgärden som ska utföras. Om man vill ha med ett villkor, får man skriva det med en if
-sats i kroppen.
Tyvärr varierar implementation av triggers mellan olika databashanterare, såväl när det gäller vad de klarar av som exakt hur man skriver. Eftersom SQL-standarden definierar ett visst skrivsätt, kommer de flesta databashanterarnas SQL-dialekter förmodligen så småningom att anpassas efter detta.
Här är ett exempel på hur en regel kan se ut, enligt standarden.
Vi antar att vi vill hålla reda på inte bara vilka som är anställda i företaget just nu, utan även dem som tidigare varit anställda.
Vi skapar därför en aktiv regel som säger att varje gång en rad tas bort ur tabellen Anställda, ska innehållet på den raden kopieras till tabellen TidigareAnställda.
Vi använder SQL-kommandot create trigger:
1
create trigger SparaAnställda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xafter delete on Anställda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreferencing old table as o
for each statement
begin atomic
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xinsert into TidigareAnställda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Nummer, Namn from o;
end
Triggern har ett namn, SparaAnställda, så att man kan referera till den, exempelvis för att ta bort den.
Den händelse den hör ihop med är borttagning av rader ur tabellen Anställda
(delete on Anställda).
Att det står after
betyder att regeln körs efter borttagningen.
När regeln körs finns de borttagna raderna alltså inte kvar i tabellen Anställda.
Alternativt kan man skriva before,
vilket betyder att regeln körs före borttagningen. De borttagna raderna finns inte kvar i tabellen Anställda när regeln körs, men man kan ändå komma åt dem i det som kallas old table
.
Denna old table
fungerar som en tabell som innehåller just de rader som nyss togs bort.
Motsvarigheten new table
innehåller nya rader som lagts till (med insert.
)
Om rader uppdaterats, med update
-kommandot, finns den gamla versionen av raderna i old table
och de nya i new table.
For each statement
betyder att kroppen i regeln körs en gång för varje delete
-kommando.
Alternativet är for each row, som betyder att kroppen körs en gång för varje rad som (i det här fallet) togs bort.
Om man anger for each row
, innehåller tabellerna new table
och old table
högst en rad var.
En av många saker som triggers kan användas till, är att hålla en materialiserad vy aktuell.
Antag att det finns en tabell Anställda, som innehåller anställda.
Tabellen har en kolumn Lön, som innehåller de anställdas löner.
Om vi behöver räkna ut den sammanlagda lönesumman skriver vi en SQL-fråga som använder aggregatfunktionen sum.
select sum(Lön) as salsum
from Anställda;
salsum |
---|
296 688 |
Om vi ofta behöver den där lönesumman kan vi göra en vy av frågan:
create view salsumview as
select sum(Lön) as salsum
from Anställda;
select * from salsumview;
salsumview |
---|
salsum |
296 688 |
En vy i SQL är egentligen bara en namngiven SQL-fråga, så varje gång man tittar på vyn körs hela summeringen. Det kan vara för 313 långsamt om det är en stor tabell, om frågan är mer komplicerad än så här, eller om den ska köras mycket ofta.
Vi kan materialisera vyn, vilket betyder att man räknar ut och lagrar resultatet. I en vanlig relationsdatabas gör man det genom att skapa en tabell, och lagra resultatet i den.
create table salsumtable (salsum integer);
insert into salsumtable (salsum)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect sum(Lön) from Anställda;
select salsum from salsumtable;
salsumview |
---|
salsum |
296 688 |
Det är alltså tabellen salsumtable som är den materialiserade vyn.
insert)
, om en eller flera rader tas bort (med delete)
, eller om innehållet på en eller flera rader ändras (med update).
Om åtkomst till databasen sker via ett program, kan vi förstås lägga in salsumtable-uppdateringar överallt där innehåller i Anställda ändras. Men det är problematiskt:
(foregin key
i SQL) med villkoret on delete cascade.
Det betyder att om vi tar bort en avdelning, kommer automatiskt alla anställda som arbetar på den avdelningen också att tas bort.
Har vi verkligen kommit ihåg att lägga in kod för att ändra i salsumtable när vi tar bort en avdelning?
314
En bättre lösning är att använda triggers. De har fördelen att de alltid kommer att köras, oavsett hur ändringarna i databasen görs. Dessutom har de den ytterligare fördelen att de finns samlade på ett ställe, i stället för att samma funktion åstadkommes med kod utspridd i ett eller flera program.
I vissa databashanterare finns det emellertid ett mycket enkelt sätt att deklarativt specificera att en vy ska vara materialiserad: Man anger helt enkelt nyckelordetmaterialized
när man definierar vyn. Som exempel kan man skriva så här i Oracle:
create materialized view salsumview as
select sum(Lön) as salsum
from Anställda;
Vi skapar triggers. De ska höra ihop med tabellen Anställda, för det är bara ändringar i den tabellen som kan påverka lönesumman. Vi måste skapa en trigger för varje operation (insättning av rader, borttagning av rader och uppdatering av rader) som kan påverka lönesumman. Vi börjar med regeln för insättning:
create trigger emp_insert after insert on Anställda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreferencing new row as n
for each row
begin atomic
315
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare newsal integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Lön into newsal from n;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xupdate salsumtable set salsum = salsum + newsal;
end
I kroppen på regeln har vi deklarerat en lokal variabel, newsal. Vi använder den för att mellanlagra lönen på den nyanställda personen.
Reglerna för borttagning och uppdatering av rader:
create trigger emp_delete after delete on Anställda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreferencing old row as o
for each row
begin atomic
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare oldsal integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Lön into oldsal from o;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xupdate salsumtable set salsum = salsum – oldsal;
end
create trigger emp_update after update on Anställda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreferencing old row as o new row as n
for each row
begin atomic
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare oldsal integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare newsal integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Lön into oldsal from o;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Lön into newsal from n;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xupdate salsumtable set salsum =
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xsalsum – oldsal + newsal;
end
Det kan vara ineffektivt att använda for each row
-regler, om till exempel många rader ändras i ett update
-kommando, eftersom regeln då måste köras många gånger.
Det kan vara bättre att använda for each statement
-regler.
Det räcker inte att bara ändra nyckelordet row
i regeln till statement.
Det skulle till exempel inte fungera att ändra regeln emp_update här ovanför så att den ser ut så här:
create trigger emp_update after update on Anställda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreferencing old table as o new table as n
for each statement
begin atomic
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare oldsal integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare newsal integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Lön into oldsal from o;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Lön into newsal from n;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xupdate salsumtable set salsum =
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xsalsum – oldsal + newsal;
end
Problemet med den regeln är att den inte alls klarar av att hantera update
-kommandon som ändrar mer än en enda rad!
En SQL-sats som select Lön into newsal from n
kräver att n bara innehåller en enda rad.
Om man ändrar flera rader på en gång, kommer vi därför att få ett felmeddelande.
Här är ett körexempel från databashanteraren Mimer:
SQL>
update Anställda set Lön = Lön * 1.10;
MIMER/DB error -14043 in function EXECUTE
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xRoutine signaled SQLSTATE: 21000
MIMER/DB error -14703 in function EXECUTE
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xAn exception occurred during
the execution of a trigger
Vi kan (om vi är dumma) skriva om den update
-regeln med hjälp av loopar, som loopar igenom de olika raderna som ändrats. Då blir det så här komplicerat:
create trigger emp_update after update on Anställda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreferencing old table as o new table as n
for each statement
begin atomic
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare o_c cursor for select Lön from o;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare n_c cursor for select Lön from n;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare oldsal integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare newsal integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare rows integer;
open o_c;
L1:
loop
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfetch o_c into oldsal;
317
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xget diagnostics rows = row_count;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif rows = 0 then
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xleave L1;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xend if;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xupdate salsumtable set salsum = salsum – oldsal;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xend loop;
open n_c;
L2:
loop
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfetch n_c into newsal;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xget diagnostics rows = row_count;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif rows = 0 then
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xleave L2;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xend if;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xupdate salsumtable set salsum = salsum + newsal;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xend loop;
end
I just det här fallet är det dock onödigt att använda loopar.
De två looparna i den här regeln kan ersättas med två SQL-satser, som summerar lönerna i old table
respektive new table.
Det är nästan alltid enklare att använda en enda SQL-fråga i stället för en loop som kör många frågor, och det är ofta mycket, mycket effektivare.
create trigger emp_update after update on Anställda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreferencing old table as o new table as n
for each statement
begin atomic
create trigger emp_update after update on Anställda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreferencing old table as o new table as n
for each statement
begin atomic
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xupdate salsumtable set salsum =
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xsalsum – (select sum(Lön) from o);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x+ (select sum(Lön) from n);
end
Triggern emp_update
fungerar dock inte för nyanställda när ny rad läggs in i tabellen Anställda.
Man måste skriva en separat after insert
trigger för detta.
Den gäller heller inte när någon slutar, vilket kräver en before delete
trigger.
update Anställda set Lön = 1;
select * from salsumtable;
salsumtable |
---|
salsum |
25 |
Det råkade finnas 25 anställda i Anställda-tabellen, så lönesumman blir mycket riktigt 25.
Som ännu ett exempel skriver vi en regel som hindrar oss från att sänka lönerna för de anställda. Om vi försöker, ska lönen i stället höjas med 1.
create trigger apa after update on Anställda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreferencing new row as n old row as o
for each row
begin atomic
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare oldsal integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare newsal integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdeclare empnr integer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Lön into oldsal from o;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Lön into newsal from n;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect Nummer into empnr from n;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif (oldsal > newsal) then
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xupdate Anställda set Lön = oldsal + 1
319
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhere Nummer = empnr;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xend if;
end
Vi provkör för att se att det fungerar:
select * from Anställda where Namn = 'Lotta';
Nummer | Namn | Lön | Chef | Avdelning |
---|---|---|---|---|
4 | Lotta | 28 000 | 2 | H |
update Anställda set Lön = 20000
where Namn = 'Lotta';
select * from Anställda
where Namn = 'Lotta';
Nummer | Namn | Lön | Chef | Avdelning |
---|---|---|---|---|
4 | Lotta | 28 000 | 2 | H |
Det här visar också att aktiva regler kan ge ett oväntat och förvirrande beteende i databasen! Vi försökte göra en helt vanlig ändring, men i stället hände något helt annat – utan att vi fick någon varning eller något felmeddelande. Man bör vara försiktig med att skriva regler som ger den typen av beteende.
Vidare kan det vara trixigt att hitta alla situationer där en trigger behövs för att få den effekt man önskar sig. I vårt exempel är det till exempel inte tillräckligt att göra en trigger förupdate
-situationen, om man vill hindra att någon sänker lönen genom att ersätta update
kommandot med delete
följt av insert.
Som en allmän regel kan man säga att aktiva regler är trixiga, och de bör inte användas i onödan.
Aktiv databas, aktiv databashanterare (engelska: active database, active DBMS). En databashanterare där man inte bara kan stoppa in data, och söka i dem, utan där man också kan ange regler, så kallade triggers, för att databashanteraren själv ska göra saker (till exempel ändra på data) när vissa villkor är uppfyllda.
Aktiv regel eller trigger (engelska: active rule, trigger). En regel i en aktiv databashanterare, för att databashanteraren själv ska 320 göra saker (till exempel ändra på data) när vissa villkor är uppfyllda.
ECA-regel (engelska: ECA rule). En aktiv regel som innehåller en händelse (Event), ett villkor (Condition) och en åtgärd eller aktion (Action).
1 På samma sätt som med create procedure
-kommandot (se sidan 300) måste create trigger
-kommandot avgränsas på något sätt.
Normalt används semikolon för att avsluta ett SQL-kommando, men eftersom det kan finnas flera semikolon inuti create trigger
-kommandot, måste detta kommando avslutas på något annat sätt.
Ett exempel är databashanteraren Mimer, där man ska skriva tecknet @ både för att inleda och avsluta create trigger
-kommandot.
I en del andra databashanterare, som MySQL, kan man ge kommandot delimiter
för att tillfälligt byta satsavslutare från semikolon till något annat, och efteråt använder man delimiter
på nytt för att byta tillbaka till semikolon.
I en databas kan man använda olika datamodeller, dvs olika sätt att beskriva världen, och därigenom olika sätt att organisera databasens data. Olika databassystem använder olika datamodeller, men den överlägset vanligaste i dag är relationsmodellen, som går ut på att man beskriver världen med hjälp av tabeller.
I det här kapitlet går vi igenom de två huvudtyperna av databassystem som kan hantera objekt. De kallas objektorienterade respektive objektrelationella databassystem.
Det finns många tillämpningar som hanterar data som inte passar i en traditionell relationsdatabas. Man vill lagra och söka mycket mer än bara enkla tabeller i databasen. Till exempel vill man kanske lagra data om multimediaobjekt som ljud- och bildfiler, tidsserier, kalendrar, kartor, dokument och numeriska data. Många av dessa 322 datatyper passar inte bra att lagra i en normaliserad relationsdatabas. Objektrelationella databaser gör det möjligt att bygga ut databashanteraren så att den kan hantera nya datatyper och samtidigt göra det möjligt att enkelt och snabbt söka inuti dessa nya dataobjekt.
Objektorienterade databaser har mindre funktionalitet är objektrelationella databaser speciellt när det gäller sökmöjligheter. Objekt-orienterade databaser är främst till för att lagra datastrukturer man skapar i sitt program på disk så att man snabbt kan återskapa dem, utan några sökmöjligheter. Men även om databasfunktionaliteten alltså är begränsad, finns det stora fördelar jämfört med att lagra data i vanliga filer.
En objekt-relationell databashanterare är normalt en relationsdatabashanterare där man, förutom relationsfunktionaliteten, lagt till möjligheter för objektorientering. De senaste versionerna av flera av de stora, vanliga relationsdatabashanterarna, till exempel Db2, Oracle och PostgreSQL, har sådana mekanismer, och de finns också med i SQL-standarden från och med SQL:1999.
323Objekt-modellen är mycket användbar för modellering av data i en databas. EER-modellen, som vi introducerade i avsnitt 2.17, är en objektorienterad datamodell där man infört arv för att modellera att en entitetstyp (ofta bara kallad entitet) är ett specialfall av en annan entitetstyp. Figuren ger ett exempel på ett EER-schema som beskriver en filmdatabas, med en något annat notation än i exemplen i avsnitt 2.17. Till exempel är en repris ett specialfall av en film, och en producent, regissör, stjärna, författare eller cinematograf är ett specialfall av en filmperson. Man säger att entitetstypen filmperson är överentitetstyp (eller bara överentitet) till producent och omvänt att producent är underentitetstyp (eller bara underentitet) till filmperson, etc. Arvet ger följande semantik:
324Objektorienterad programmering använder orden klasser, objekt, attribut och metoder. Objekten motsvarar saker i verkligheten och klasserna motsvarar typer av saker. Uttryckt i databastermer är klasserna entitetstyper och objekten är data. Klasserna motsvarar entitetstyper i EER-modellen. En sak som tillkommer jämfört med EER-modellen är att i en objektorienterad datamodell brukar man även ha med beteende, dvs klasserna har inte bara attribut utan också funktioner och procedurer som innehåller kod som appliceras på objekten. Dessa brukar kallas metoder. Även metoderna kan ärvas.
Till skillnad från relationsmodellen, där en rad i en tabell bara kan identifieras med hjälp av de data den innehåller, har varje objekt i en objektorienterad databas en unik objektidentitet, en OID. Man kan referera till ett visst objekt med dess OID, och behöver inte upprepa värdet på en nyckel som med relationsmodellens referensattribut. Ett objekt kan motsvara en post som innehåller data om en sak i verkligheten, men det kan också vara en mängd andra objekt, en lista, osv.
325Här går vi inte djupare in på hur objektorienterad programmering fungerar, men för den som inte känner sig säker på alla detaljer finns det en introduktion på bokens webbplats. 1 Den består av ungefär 10 sidor, med figurer och förklaringar.
Relationsdatabaserna var ursprungligen främst inriktade mot administrativa tillämpningar. Typexemplet är en bank, med miljoner bankkonton (som bara består av några numeriska värden på en rad), och som man gör miljoner insättningar och uttag på (som bara är enkla adderingar eller subtraktioner).
Dessa tillämpningar har följande egenskaper:
Emellertid har nya typer av tillämpningar dykt upp som ställer nya krav på databashanteraren. Exempel på sådana nya tillämpningar är:
Följande figur illustrerar vad olika tillämpningar kräver av en databashanterare när det gäller komplexitet hos datastrukturer respektive behov av frågespråk. Rubriken i varje ruta visar vilken typ av system som kan vara lämpligt för att tillgodose tillämpningarnas behov.
Frågor | ||
Inga frågor | ||
Enkla data | Komplexa data |
I den nedre vänstra rutan märkt filsystem har vi tillämpningar som arbetar på enkla datalagringsstrukturer och inte kräver databasfrågor, till exempel enkla beräkningsprogram, texteditorer och bildvisningsprogram som bara läser och kanske skriver data sekventiellt. Sådana tillämpningar klarar sig i regel med vanliga filer för sin datalagring och någon databashanterare behöver då inte användas. 328 Mycket stora filer (till exempel för video-on-demand) kan emellertid behöva speciella skalbara filsystem som vanligen baseras på databasteknik.
I den övre vänstra rutan märkt relationsdatabaser har vi tillämpningar som klarar sig med relativt enkla datalagringsstrukturer i form av normaliserade tabeller men som också kräver ett generellt frågespråk som SQL och transaktionshantering. Här återfinns en mycket stor mängd tillämpningar för affärsverksamhet, vetenskap, privata data, etc.
I den nedre högra rutan märkt objektlager har vi tillämpningar som kräver lagring av komplexa användardefinierade datastrukturer men inte avancerade frågor. Till exempel cad-system behöver lagra komplexa datastrukturer för att representera produktdetaljer, och avancerade beräkningssystem behöver lagra beräkningsresultat i arraystrukturer. Man har inte så stort behov av avancerade frågemöjligheter, utan vill helt enkelt kunna spara de primärminnesdatastrukturer man skapar i sina program på disk. (Det är ju programmet som gör jobbet.) Transaktionerna är mycket långa och man har få samtidiga användare.
I den övre högra rutan märkt OR-databaser har vi tillämpningar som kräver lagring av data på mer avancerat sätt än i form av normaliserade tabeller och samtidigt kräver avancerade frågemöjligheter och annan funktionalitet som en databashanterare tillhandahåller. Många moderna tillämpningar inom till exempel multimedia och vetenskap har dessa egenskaper. Här är några exempel:
Idén med ett objektlager är att man utgår från ett vanligt objekt-orienterat programmeringsspråk som C++ eller Java. 4 Man utvidgar sedan programmeringsspråket så att det också fungerar som en databas. Man skriver sitt C++-program ungefär som vanligt, men i stället för att alla objekten försvinner när programmet avslutas, sparas de undan i databasen och finns tillgängliga nästa gång programmet startas. Man säger att man har persistenta objekt.
Det sätt man "söker" bland sina objekt i ett objektlager är genom att följa pekare eller anropa metoder som i ett vanligt C++ eller Java-program. Man använder således inte något frågespråk utan programmerar sökningarna direkt i C++. Det är upp till programmeraren att se till att sökningarna blir effektiva, och ändrar man i sina datastrukturer måste man sen ofta ändra sitt program. Ibland 330 tillhandahåller objektlagret frågemöjligheter, men de kan vara rätt primitiva jämfört med relationsdatabaser.
En vanlig arkitektur för objektlager är att objekten finns i klientens adressrymd, precis som vanliga objekt i C++, men databashanteraren ser till att spara dem på disk om de ändras, och läsa in dem igen vid behov. Då får man höga prestanda. Normalt skrivs inte alla objekt till databasen, utan bara de som ändrats eller som programmeraren angivit ska sparas. Det kan ske per objekt, per klass, eller på så sätt att alla objekt som är nåbara genom att följa pekare från något persistent objekt sparas.
Objektlager har följande för- och nackdelar jämfört med relationsdatabaser:
Ett exempel på en objektorienterad databas är ObjectStore; en annan heter Objectivity. Dokumentdatabaser som MongoDB kan också ses som en form av objektlager där man har JavaScript som värdspråk och lagrar objekt i form av JSON-objekt. MongoDB har också ett primitivt frågespråk med JSON-syntax. Så kallade keyvalue stores som DynamoDB och MemcacheDB är också en form av distribuerade objektlager utan frågespråk.
I ObjectStore har man ansträngt sig att göra persistent objekthantering i C++ så genomskinlig och effektiv som möjligt. 5 Följande exempel visar hur det kan se ut:
[1] class PERSON {...};
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x...
[2] {PERSON p;
[3] p->age=35;
[4] }
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x...
[5] static PERSON q;
[6] q->age=38;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x...
[7] persistent PERSON r;
[8] r->age=40;
På rad [1] deklareras en C++-klass PERSON.
På rad [2] skapas ett nytt lokalt objekt p av klassen PERSON inom ett block. På rad [3] sätts attributet age till 35. När blocket lämnas på rad [4] avallokeras p automatiskt av C++. Livslängden hos ett lokalt C++-objekt är det block i vilket det deklarerats.
332På rad [5] deklareras ett globalt C++-objekt q av klassen PERSON. På rad [6] sätts dess attribut age till 38. När programmet avslutas avallokeras alla sådana globala C++-objekt.
På rad [7] skapas ett nytt persistent C++-objekt, och på rad [8] sätts attributet age till 40. Skillnaden är här att när programmet avslutas sparar databashanteraren automatiskt undan objektet i databasservern om det har ändrats. Nästa gång programmet startas upp får man tillgång till de persistenta objekten igen. Livslängden för ett persistent objekt är fram till att det explicit tas bort från databasen.
ObjectStore hanterar persistenta objekt antingen genom en speciell förprocessor till C++ som bland annat tillhandahåller nyckelordetpersistent
, eller genom ett klassbibliotek för hantering av persistenta objekt. Vad ovanstående exempel visar är att man med förprocessorn använder exakt samma syntax för att komma åt persistenta objekt som för att komma åt transienta. Således behöver man bara göra små ändringar i ett C++-program för att göra dess transienta datastrukturer persistenta.
Den teknik man använder för att implementera persistenta objekt så att de ser ut som vilka C++-objekt som helst kallas object swizzling eller pointer swizzling. Man kan inte utan vidare på disk spara pekare till C++-objekt som skapats i primärminnet under en körning. Om man sparar adressen till ett objekt är det inte alls säkert att objektet får samma adress när det läses in från databasen i nästa körning. Därför måste man se till att pekare till persistenta objekt konverteras till OID:er när de lagras på disk och konverteras tillbaks till pekare när de senare läses in från databasen. Med object swizzling är dessa OID:er representerade som ogiltiga adresser. Vid inläsning från databasen görs ingen konvertering alls utan man får in en massa objekt med pekare till ogiltiga adresser. Om man sedan i sitt C++-program följer en sådan OID-pekare till en ogiltig adress får man ett avbrott. Med pointer swizzling fångas avbrottet och OID:n tolkas. Om OID:n refererar ett persistent objekt som redan lästs in i primärminnet ersätter systemet OID:n med pekare direkt till objektet. Man säger att pekaren swizzlas. Om OID:n inte redan lästs in läser systemet först in det block där det refererade objektet ligger på disken, innan pekaren swizzlas.
Med objektswizzling kommer alla viktiga pekare att swizzlas efter en uppvärmningsperiod där alla viktiga data lästs in från databasservern. När en pekare väl är swizzlad, är den en vanlig C++-pekare, och är därför så snabb som pekare kan bli i C++.
333Objektrelationella databaser är mer fullständiga databashanterare. Idén är att utvidga funktionaliteten hos relationsdatabashanterare så att man kan arbeta med klasser och objekt förutom vanliga tabeller.
En viktig princip är att utvidga SQL för att hantera klasser och objekt förutom vanliga tabeller. Det är viktigt att gamla databastillämpningar fortfarande fungerar. Från och med SQL:1999 har man därför infört objekt-relationella utvidgningar av SQL.
En viktig funktionalitet med objektrelationella databaser är att göra databashanteraren utbyggbar på olika sätt för att uppfylla krav hos nya typer av tillämpningar. Följande aspekter är därvid viktiga:
2. När man har abstrakta datatyper behöver man också kunna göra egna frågeoperatorer som söker eller ändrar i lagringsstrukturerna. Om man till exempel lagrar bilder i databasen vill man också kunna jämföra bilder i frågespråket. Man vill alltså kunna bygga ut frågespråket med tillämpningsberoende sökfunktioner. Detta görs vanligen med hjälp av användardefinierade funktioner (på engelska User Defined Functions, UDF:er) implementerade i C++, Java eller annat programmeringsspråk och som sedan kan anropas från SQL-frågor och söker inuti dataobjekt på databasservern. Man kan till exempel ha en UDF similarity som jämför två bilder och returnerar ett mått på hur lika bilderna är. I följande fråga antas att album1 och album2 är tabeller som innehåller bilder i attributet image:
select t1.image, t2.image
from album1 t1, album2 t2
where similarity(t1.image,t2.image)>0.9;
334
Hur man definierar likhet mellan bilder är kunskap som finns implementerad i funktionen similarity. Den används här för parvis att jämföra alla bilder i två album.
Egna frågefunktioner kan kräva att man informerar frågeoptimeraren om hur dyrbart det är att anropa en UDF och hur selektiv den är. Objekt-relationella databaser tillhandahåller därför vissa begränsade möjligheter att påverka frågeoptimeringen.
En objekt-relationell databashanterare tillhandahåller egen-definierade datatyper genom objektorientering tillsammans med gränssnitt för att plugga in användarkod i databasservern för att definiera UDF:er, index och optimeringstumregler. Dessa inpluggningar går under olika namn: Oracle kallar dem data cartridges, Db2 kallar dem data extenders, och PostgreSQL kallar dem extensions rätt och slätt.
Både PostgreSQL, Oracle och IBM:s Db2 stöder objektorientering som i SQL:1999. Microsoft SQL Server stöder för närvarande inte objektorientering som i SQL:1999. De flesta moderna databashanterare har UDF:er.
Objektorienterad databas eller datalager (engelska: object-oriented database eller object store). Man utgår från ett objektorienterat 336 programmeringsspråk, som C++ eller Java, och lägger till en del databasfunktionalitet, i första hand persistens.
Objektrelationell databas (engelska: object-relational database. Man utgår från en relationsdatabashanterare, med dess frågespråk och övriga databasfunktionalitet, och lägger till mekanismer för objektorientering.
Impedance mismatch. Problemen med att lagra ett objektorienterat programs data i en relationsdatabas. Man måste översätta fram och tillbaka mellan programmets objekt och databasens tabeller.
Användardefinierad funktion (engelska: user-defined function, UDF). En funktion skriven i något vanligt programmeringsspråk, som C, C++ eller Java, som kan länkas ihop med databashanteraren och sen anropas från SQL-frågor för att arbeta inuti dataobjekt.
De data som samlas i en databas brukar ha ett visst syfte. Till exempel kan ett företag ha en databas för att hantera sin försäljning, och den databasen innehåller de data om kunder, varor och order som behövs för att administrationen av försäljningen ska fungera bra. Men ibland kan samma data användas för andra ändamål, som för olika slags analys för att hitta nya samband och härleda ny kunskap.
Vanliga databaser brukar inte vara konstruerade med tanke på sådan analys, och därför kan man samla ihop data på ett sätt som underlättar analys. En sådan samling data brukar kallas datalager, på engelska data warehouse. Analysen, när man går igenom data i datalagret på jakt efter ny kunskap, kallas datautvinning, på engelska data mining.
Datalager och datautvinning är två sorters tillämpningar av databasteknik som varit mycket omtalade de senaste åren, och de ser ut att komma att användas mer och mer. Genom den ökade kapaciteten hos modern datorhårdvara, och genom den mer allmänt tillgängliga kunskapen och medvetenheten inom området, har de här tillämpningarna 338 numera kommit inom räckhåll inte bara för de största företagen och organisationerna.
Samtidigt skiljer sig teknikerna på det här området en del från andra databastillämpningar. Eftersom datalager används på ett annat sätt än vanliga databaser, till exempel genom att frågorna för det mesta arbetar med mycket större mängder data, måste man bygga datalagren på ett annat sätt än vanliga databaser. De regler och metoder som vi lärt oss för att bygga databaser fungerar inte alltid så bra för datalager, och man måste lära om. Det kan till exempel handla om att man ibland bör undvika att normalisera tabeller.
Ett företag eller en organisation har ofta data i en eller flera databaser som används i det dagliga arbetet. Dessa "vanliga databaser" brukar kallas driftsdatabaser. Det kan vara databaser för försäljning, orderhantering, lagerhantering, och så vidare. Ibland används i stället termen operativa databaser, efter engelskans operational databases.
Driftsdatabaser är i regel byggda för transaktionshantering, där man både använder och ändrar databasens data. Det brukar kallas OnLine Transaction Processing. En typisk OLTP-databas är en driftsdatabas för att hantera affärstransaktioner, som bankuttag eller detaljhandelsköp. OLTP-databaser kännetecknas av:
Ett företag har typiskt alla sina driftsdata lagrade i databasen, och databassystemet garanterar att data är konsistenta och säkra. Databasen återspeglar exakt vad som finns i lager, kassor, kundregister och så vidare vid varje givet tillfälle. Man lagrar troligen inte bara nuvarande saldo med mera vid varje tillfälle, utan också tidpunkten för varje transaktion, vem som tog ut pengar, hur mycket som 339 togs ut, och så vidare. Man lagrar kanske också ett revisionsspår (på engelska audit trail) som beskriver historiska data (kommer att beskrivas i kapitel 20) om vad som hänt för varje konto under senaste tiden. På så vis kan man till exempel spåra felaktigheter.
Om ett företags alla data lagras i en databas, uppstår möjligheten att göra mer avancerad analys av dessa data. Man kan vilja basera beslut på sådan analys av data från databasen.
Det finns en generell term, beslutsstöd (på engelska decision support), som också brukar användas när man menar programsystem som används för att hjälpa organisationer att fatta beslut. Ibland kallar man det business intelligence, vilket kan direktöversättas med affärsunderrättelseverksamhet, men brukar kallas omvärldsbevakning.
Beslut bygger ofta på trender i lagerhålling, kontobalans, och så vidare över tiden. Till exempel vill man kunna uppskatta när en viss vara för något varuhus beräknas ta slut så att man (helst automatiskt) kan beställa nya varor "just-in-time" till de varuhus där brist uppstår, innan de tar slut. På samma sätt kan produktion och transport av en vara planeras med utgångspunkt från var och när en brist beräknas uppstå. Sådan analys kan uttryckas som frågor som aggregerar över revisionsspåret för varje vara och försäljningsställe. Givet historiska data i databasen kan man göra frågor som analyserar till exempel försäljningstrender för olika varor, distrikt och försäljare, för att till exempel uppskatta när varor tar slut, eller var och när produktionsvolymen bör ändras. Dessa frågor kan uttryckas som mer eller mindre avancerade SQL-frågor innehållande aggregatfunktioner. De skiljer sig från OLTP-frågor genom att de kan vara komplexa, med många aggregatfunktioner över stora mängder data, och de kan ta lång tid att utföra. Skickar man komplexa frågor till en driftsdatabas kommer man att störa transaktionshanteringen så att transaktionerna blir långsamma. Vidare är kraven på konsistens betydligt lägre för beslutsstöd än för driftsdatabaser: det går i regel bra att fatta beslut baserat på ungefärliga och inte helt färska data.
340För att underlätta beslutsstöd kan man behöva skapa en samling av stora mängder data, kanske hämtade från flera olika databaser, och organisera dem på ett annat sätt än i en vanlig databas. Då säger man inte att man lagrar data i en databas, utan i ett datalager, på engelska data warehouse. 1 Att bygga upp och driva ett datalager kallas på engelska data warehousing.
Ett datalager är alltså en separat databas, som skapats med data från en eller flera driftsdatabaser, och som används för beslutsstöd. För att undvika att störa driftsdatabaserna med stora analysfrågor kopierar man med jämna mellanrum över alla ändrade data från driftsdatabaser till datalagret. Man ser då också till att märka upp kopierade data med tidpunkt för överföringen, varifrån överföringen kom, med mera. Sådana data om data kallas i datalagersammanhang för meta-data. Ett datalager är alltså en mycket stor databas som innehåller historiska data för alla artiklar, konton, försäljningsställen, och så vidare i företaget. Till datalagret skickar man stora avancerade aggregeringsfrågor som analyserar dessa data.
OLAP-databaser har följande egenskaper:
Till skillnad från en vanlig databastillämpning, där det ofta är ett fåtal sökningar som görs många gångar, och som är förprogrammerade, arbetar man i datalager ofta med ad hoc-frågor, dvs sökningar i databasen som man kommer på och formulerar direkt. (Det kan ske antingen direkt i SQL, eller med hjälp av ett analysverktyg som genererar SQL-frågor).
Ett datalager används på ett helt annat sätt än en driftsdatabas och det är därför inte säkert att samma databassystem är lämpligt att hantera både OLTP och OLAP. Det finns till exempel speciella OLAP-system som inte alls är baserade på databasteknik. Ett mycket vanligt verktyg för dataanalys är kalkylsystem (på engelska spreadsheet programs) som Excel. Ett problem med sådana separata dataanalysverktyg kan vara att de inte skalar upp, dvs de fungerar inte effektivt när mängden data blir stor. Om dataanalysverktyget inte skalar upp, kan man i stället basera analysen på att manuellt (med SQL) ladda ner valda delar av data från ett datalager, vilket Excel har stöd för.
Moderna databassystem kan hantera bägge typerna av databasbehandling, och dessutom skalar de upp. Dock måste man i regel konfigurera hård- och mjukvara olika för OLTP och OLAP. När ett databassystem används som ett datalager lönar det sig till exempel att aggressivt spara data i cache 2 i stort primärminne, så att så stora delar av ett datalager som möjligt kan traverseras snabbt av databassystemet 342 på längden och tvären. Eftersom det inte förekommer uppdateringar behöver databassystemet inte bekymra sig för att hålla sparade data konsistenta med loggfil som för driftsdatabaser. För driftsdatabaser är situationen annorlunda därför att bara en mycket liten del av databasen berörs i varje transaktion och det lönar sig att spara bara de data som berör aktiva transaktioner.
Ett datalager skiljer sig från ett typiskt vanligt databassystem genom att det är konstruerat särskilt för analys. Schema och data är anpassade för komplicerade sammanställningar, snarare än uppdateringar eller sökningar efter enskilda poster. Kanske är det förhållandevis ineffektivt att lägga in nya data.
Att överföra data från källsystemen till datalagret är inte alltid helt lätt, och det finns flera saker som kan ställa till problem. Exempel:
För att råda bot på dessa brister kan de data som ska överföras till datalagret behöva behandlas ganska mycket. Man säger ibland att man tvättar data. Datatvättningen görs i form av tillämpningsprogram som tar data från källsystemen och transformerar dem till lämpligt format för att införas i datalagret. Dessa tillämpningsprogram körs med jämna mellanrum. Man ska inte underskatta arbetsinsatsen för datatvättning. Till exempel kan tolkning av data som ligger i vanliga filer kräva en stor arbetsinsats. Vidare kan konvertering mellan olika format vara mer eller mindre besvärlig.
När man konstruerar en vanlig databas brukar man modellera världen med hjälp av ER-modellen, eller någon liknande datamodell, och sen översätter man ER-schemat till ett relationsschema, det vill säga till tabeller som kan lagras i en relationsdatabas. Det har visat sig att det inte är det bästa sättet att betrakta data vid dataanalys. Schemat kan vara krångligt att förstå, och frågorna besvärliga att formulera. Vid dataanalys använder man i stället en annan datamodell, den så kallade dimensionsmodellen, som vi ska beskriva här. Dimensionsmodellen är enklare än relationsmodellen och ER-modellen, och den ger databaser som alla har samma struktur.
Man kan likna dimensionsmodellen vid de kalkylblad (på engelska spreadsheets) som finns i kalkylprogram som Microsoft Excel. Där betraktar man data i form av två-dimensionella arrayer, över vilka man utför olika operationer, som summeringar. Sådana kalkylblad har visat sig mycket användbara för att bygga analysmodeller, men de har flera viktiga begränsningar: data kan bara representeras i två dimensioner, datamängden får inte vara för stor och data måste ändras manuellt genom att man ändrar i kalkylbladet.
Som illustration av dimensionsmodellen tänker vi oss att ett glassbolag för statistik över sin glassförsäljning, och vi vill analysera den. Vi skulle kunna göra en tabell som visar försäljningen per månad:
345Försäljning per månad | |
---|---|
Månad | Antal |
januari | 57 |
februari | 30 |
mars | 21 |
april | 39 |
maj | 19 |
juni | 84 |
juli | 134 |
augusti | 86 |
september | 33 |
oktober | 3 |
november | 2 |
december | 101 |
Alternativt kan vi göra en tabell som visar försäljningen av de olika sorterna av glass:
Försäljning per glass | |
---|---|
Glass | Antal |
Gräddpinne | 71 |
Hallonpinne | 115 |
Julmuststrut | 117 |
Lakritsbomb | 109 |
Pistagestrut | 49 |
Bautastrut | 115 |
Spenatbåt | 33 |
Båda de ovanstående sammanställningarna av försäljningen är endimensionella: vi delar upp försäljningen efter en enda faktor, nämligen tid respektive vara. Om vi kombinerar försäljningen per månad och försäljningen per glass, får vi en tvådimensionell uppställning som ser ut så här:
346J | F | M | A | M | J | J | A | S | O | N | D | S:a | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Gräddpinne | 0 | 0 | 2 | 3 | 6 | 18 | 28 | 10 | 4 | 0 | 0 | 0 | 71 |
Hallonpinne | 0 | 0 | 2 | 11 | 3 | 22 | 40 | 26 | 11 | 0 | 0 | 0 | 115 |
Julmuststrut | 17 | 0 | 2 | 0 | 0 | 3 | 4 | 0 | 0 | 0 | 0 | 91 | 117 |
Lakritsbomb | 38 | 27 | 11 | 12 | 3 | 7 | 3 | 1 | 0 | 0 | 0 | 7 | 109 |
Pistagestrut | 0 | 0 | 0 | 0 | 1 | 8 | 16 | 19 | 4 | 0 | 0 | 1 | 49 |
Bautastrut | 0 | 0 | 2 | 11 | 3 | 22 | 40 | 26 | 11 | 0 | 0 | 0 | 115 |
Spenatbåt | 2 | 3 | 2 | 2 | 3 | 4 | 3 | 4 | 3 | 3 | 2 | 2 | 33 |
S:a | 57 | 30 | 21 | 39 | 19 | 84 | 134 | 86 | 33 | 3 | 2 | 101 | 609 |
Den här tvådimensionella uppställningen är inte en tabell i en relationsdatabas. Vi har till exempel både rad- och kolumnrubriker, och i en relationsdatabas är det bara kolumnerna som är namngivna. (I en relationsdatabas skulle man snarare ha skapat en tabell där varje ruta i uppställningen ovan motsvaras av en rad, men vi ska titta mer på detta senare.) Tabellen liknar snarare en mycket vanlig sorts tvådimensionell uppställning som man brukar göra med kalkylblad i kalkylprogram som Excel.
Vi kan också ha fler dimensioner, till exempel en geografisk dimension med landskap eller städer. När vi utökar den tvådimensionella 347 uppställningen med ännu en dimension, blir den förstås tredimensionell:
Man talar om en datakub eller bara kub. 3 Man kan likna vår datakub vid ett tredimensionellt kalkylblad, vilket kalkylprogram som Excel inte tillåter.
När vi även tittar på den geografiska dimensionen kan det visa sig att Hallonpinne bara säljs i Göteborg, eller att Bautastrut är mest populär i Norrland. Kanske hittar man ett intressant samband mellan tid och plats, så att glassförsäljningen vinter respektive sommar ser helt olika ut i olika landsändar? Eller mellan alla tre dimensionerna glass, tid och ort, så att försäljningen av de olika glassarna flyttar sig genom Sverige under året?
Om det finns fler faktorer som man vill kunna analysera, kan man lägga på ytterligare dimensioner, till exempel en dimension för vilka reklamkampanjer som bedrivits, eller en dimension för vilka kunder som köpt glassarna. När en kub får fler än tre dimensioner kallar matematikerna den för en hyperkub, men i datalagersammanhang säger man fortfarande kub eller datakub.
Vi såg tidigare att en tvådimensionell uppställning inte kan ersättas med två endimensionella, och på liknande sätt kan inte heller en kub ersättas med flera uppställningar med färre dimensioner. Man 348 vill kunna vrida och vända på kuben, och göra analyser som innefattar vilka som helst kombinationer av alla dimensionerna. Är det olika kategorier av kunder som köper glass under olika månader? Är effekten av reklamkampanjer olika i olika landsdelar? Att gå från endimensionell uppställning till tvådimensionell som i vårt exempel illustrerar att borra sig ner (på engelska drill down) och motsatsen är ett exempel på att rulla up (på engelska roll up).
En databas uppbyggd av datakuber kallas på engelska multi-dimensional database eller bara dimensional database. På svenska kan man säga flerdimensionell databas eller dimensionsdatabas.
Ett datalager representerar data i en eller flera datakuber, till exempel en som beskriver försäljningen och en som beskriver tillverkningen. Om det finns flera datakuber kan de ha helt olika dimensioner, men ofta förekommer en eller flera av dimensionerna (till exempel tid) i flera datakuber.
För dataanalys kan det alltså vara bra att presentera data som i en flerdimensionell kub. En sådan kub kan lagras direkt i en databas med specialiserade datastrukturer i en särskild databashanterare som är avsedd för flerdimensionella databaser. Det kallas MOLAP, eller Multi-dimensional On-Line Analytical Processing.
För att på ett flexibelt och skalbart sätt kunna hantera analys över stora datamängder är det vanligaste att man använder en vanlig relationsdatabashanterare, som Oracle eller Microsoft SQL Server, också för OLAP. Det kallas ibland ROLAP, eller Relational On-Line Analytical Processing. Moderna relationsdatabashanterare har också börjat få funktioner som särskilt är avsedda att underlätta arbetet med dimensionsmodellen, som den så kallade kuboperatorn (se avsnitt 18.10).
Men hur modellerar man en datakub med hjälp av tabeller? Tabellerna i en relationsdatabas är ju (på sin höjd) tvådimensionella?
Vi skrev tidigare att den tvådimensionella uppställningen av försäljning uppdelat på glassar och månader inte är en tabell i en relationsdatabas. Om man ska lagra sådana tvådimensionella försäljningsuppgifterna i en relationsdatabas kan man förstås skapa en tabell som påminner om den tvådimensionella uppställningen, med 349 tolv kolumner för olika månader. Det är dock en lösning som kan bli mycket opraktisk när man ska formulera SQL-frågor, till exempel för att räkna ut vilken månad som haft den högsta försäljningen. (Försök gärna själv!)
Försäljning | ||
---|---|---|
Månad | Glass | Antal |
januari | Gräddpinne | 0 |
januari | Hallonpinne | 0 |
januari | Julmuststrut | 17 |
... | ... | ... |
Frågan om vilken månad som haft den högsta försäljningen kan nu formuleras så här:
create view Månadsförsäljning
as select Månad, sum(Försäljning) as Försäljning
from Försäljning
group by Månad;
select Månad
from Månadsförsäljning
where Försäljning = (select max(Försäljning)
from Månadsförsäljning);
Den här lösningen på lagring kan generaliseras till fler än två dimensioner. Varje cell i datakuben lagras som en rad. Eftersom tabellen innehåller faktauppgifter (till exempel om vad som sålts, och när, och hur, och till vem) kallas den faktatabell. I exemplet med tre dimensioner (månad, glass och ort) ser tabellen ut så här:
350Försäljning | |||
---|---|---|---|
Månad | Glass | Ort | Antal |
januari | Julmuststrut | Örebro | 3 |
januari | Julmuststrut | Gnesta | 2 |
januari | Julmuststrut | Karesuando | 8 |
januari | Lakritsbomb | Örebro | 7 |
... | ... | ... | ... |
För att hålla ordning på de olika dimensionerna skapar vi också en tabell för varje dimension. De tabellerna kallas dimensionstabeller. En dimensionstabell innehåller en rad för varje värde som kan förekomma i den dimensionen.
I vårt tredimensionella exempel med dimensionerna Glass, Månad och Ort skulle vi alltså förutom faktatabellen Försäljning ha tre dimensionstabeller, som vi lämpligen kallar Glassar, Månader och Orter. Tabellen Glassar innehåller alla glassorterna, tabellen Månader innehåller alla månaderna, och tabellen Orter alla orter. Vi skapar också numeriska nycklar i dimensionstabellerna, och byter ut värdena på dimensionerna i faktatabellen mot motsvarande referensattribut:
Försäljning | |||
---|---|---|---|
Månad | Glass | Ort | Antal |
1 | 3 | 2 | 3 |
1 | 3 | 1 | 2 |
1 | 3 | 3 | 8 |
1 | 4 | 2 | 7 |
... | ... | ... | ... |
Månader | |
---|---|
Id | Namn |
1 | januari |
2 | februari |
3 | mars |
... | ... |
Glassar | |
---|---|
Id | Namn |
1 | Gräddpinne |
2 | Hallonpinne |
3 | Julmuststrut |
... | ... |
Orter | |
---|---|
Id | Namn |
1 | Gnesta |
2 | Örebro |
3 | Karesuando |
... | ... |
Nycklarna i dimensionstabellerna i ett datalager bör inte hämtas från källdatabaserna. Även om det redan finns till exempel ett unikt artikelnummer för varje glass, skapar vi en egen, ny nyckel, en så kallad surrogatnyckel. I datalagret ska vi nämligen behålla gamla försäljningsdata under lång tid, och vi kan sällan vara säkra på att nycklarna i källdatabaserna aldrig kommer att ändras eller återanvändas.
351Så här kan man rita upp schemat:
I glassexemplet ovan hade vi en ganska enkel faktatabell. Datakuben, som faktatabellen baseras på, innehöll ju bara ett enda faktum i varje cell i kuben, nämligen antalet sålda glassar. Det kan dock finnas fler fakta, till exempel priset på de sålda glassarna.
Alla fakta, till exempel hur många glassar vi sålde, finns (förstås) i faktatabellen. Det är dessa fakta man arbetar med när man analyserardata.
Dimensionstabellerna är egentligen bara ett stöd, och används för att hålla reda på vilka värden som kan förekomma i de olika dimensionerna, och vad de värdena betyder. En dimensionstabell innehåller en rad för varje värde som kan förekomma i den dimensionen. 352 Det är användbart både för en människa som ska jobba med databasen, och för program och SQL-frågor som automatiskt ska göra sammanställningar av olika slag.
I exemplet på en stjärna ovan hade vi de tre dimensionerna Månad, Glass och Ort, med mycket enkla dimensionstabeller som bara bestod av en nyckel och ett namn. I verkligheten är dimensionstabellerna ofta mycket mer komplicerade, och kan innehålla hundratals kolumner!
Extra uppgifter som folkmängd, kommun och län kan vara bra att ha när man försöker hitta mönster i försäljningen genom att göra olika sammanställningar. Kan man se skillnader inte bara mellan orter, utan mellan hela kommuner och län? Beror glassförsäljningen på hur stor tätorten är?
Länets namn är egentligen helt redundant information, och är bara med för att underlätta för en mänsklig användare. I och med att länsnamnet finns i databasen, kan ett tillämpningsprogram eller analysverktyg presentera hela länsnamnet för användaren, och inte bara en kryptisk länsbokstav.
Den uppmärksamme läsaren (som studerat kapitel 12 om normalformer) ser genast att den nya, bredare versionen av tabellen Orter inte uppfyller tredje normalformen. Det finns till exempel ett funktionellt beroende mellan Kommun och Län. En kommun ligger, vid 353 ett och samma tillfälle, i ett enda län. Samtidigt kan en kommun innehålla flera olika tätorter. Därför kommer uppgiften om vilket län en viss kommun ligger i att upprepas en gång för varje tätort i kommunen. Till exempel står det på fyra ställen i exempelraderna att kommunen Kiruna ligger i Norrbottens län (BD).
Ytterligare ett brott mot tredje normalformen är det funktionella beroendet mellan Län och Länsnamn. Länsnamnet för ett län måste upprepas en gång för varje ort som ligger i det länet.
I och med brotten mot tredje normalformen uppstår alltså en del redundans i dimensionstabellen Orter. I vanliga databaser brukar man vilja normalisera tabellerna så att de uppfyller tredje normalformen eller BCNF, bland annat just för att motverka redundans. Det beror delvis på att man inte vill slösa med plats, men främst på att uppdateringar blir snabbare och säkrare eftersom det bara blir ett ställe man måste ändra på. Å andra sidan går ju normalisering ut på att dela upp en onormaliserad tabell i flera normaliserade, och det kan ge sämre prestanda när man gör sökningar där tabellerna måste slås samman igen.
En sökning i en vanlig databas handlar ofta om enstaka poster (var bor den här kunden?) , medan sökningar i ett datalager oftare berör väldigt många poster (vad är den totala försäljningen för varje månad?). Därför är prestanda ännu viktigare i ett datalager, och man måste optimera databasens struktur med tanke på den typen av stora frågor snarare än med tanke på uppdateringar.
Vikten av prestanda för stora sökningar, och den låga uppdateringsfrekvensen, gör att man i ett datalager ibland vill frångå de vanliga normaliseringsreglerna. Faktatabellen brukar vara normaliserad som vanligt (med tredje normalformen eller BCNF), för den kan innehålla så många rader att det blir viktigt att spara plats, medan dimensionstabellerna ofta bara uppfyller andra normalformen. Man brukar säga att dimensionstabellerna är denormaliserade.
Man kan normalisera dimensionstabellerna genom att (som vanligt) dela upp dem i flera tabeller. Referensattributen i faktatabellen kommer då att referera till dimensionstabellerna, som i sin tur innehåller referensattribut som refererar vidare till andra tabeller med ytterligare information. I stället för en stjärna påminner det schemat om en snöflinga, och därför kallar man den konstruktionen för snöflingeschema eller bara snöflinga. Den strukturen brukar dock inte rekommenderas, eftersom den blir både långsammare och svårare för användarna att förstå.
354I och med att datalager blivit en allt viktigare tillämpning för relationsdatabaser, har funktioner som understöder dimensionsmodellen lagts in i relationsdatabashanterarna. Det handlar särskilt om utvidgningar av group by för att kunna göra mer avancerade aggregeringar. Här ska vi titta på kub-operatorn, cube.
Kuboperatorn används i en SQL-fråga för att "bygga ihop" en datakub utifrån ett stjärnschema. Man kan säga att den genererar en kub-vy, baserat på tabellerna.
Vi börjar med en vanliggroup by
-fråga, som ger oss antalet sålda glassar för varje kombination av glass och månad:
4
select Glass, Månad, sum(Antal) as Antal
from Försäljning
group by Glass, Månad;
Glass | Månad | Antal |
---|---|---|
Gräddpinne | januari | 0 |
Gräddpinne | februari | 0 |
Gräddpinne | mars | 2 |
Gräddpinne | april | 3 |
... | ... | ... |
Det finns tolv månader och sju sorters glass, vilket ger 84 kombinationer, och svaret från frågan innehåller också 84 rader. Det motsvarar glass-månads-rutorna i uppställningen på sidan 346. Däremot fick vi inte med några aggregeringar, alltså summorna totalt per glass och totalt per månad. Men det är just de summorna som man kan få fram med kuboperatorn.
355Vi lägger till with cube i frågan: 5
select Glass, Månad, sum(Antal) as Antal
from Försäljning
group by Glass, Månad
with cube;
Denna fråga med with cube
ger följande svar, där null markerar aggregeringar:
Raden med Gräddpinne som glass och null som månad visar summan av försäljningen av Gräddpinne, för alla månader. Raden med null som glass och januari som månad visar summan av hela januaris glassförsäljning, för alla glassar. Raden med null både som glass och som månad visar totalsumman av all glassförsäljning, för alla glassar och alla månader.
356
Man kan se cube
-operatorn som en generaliserad group by
-operator som skapar en datakub i form av en vy där aggregatfunktioner appliceras på alla dimensioner.
På så sätt får man ett oberoende mellan hur data lagras i relationsdatabasen och hur en användare vill betrakta sin datakub.
Man får flexibilitet genom att kunna designa datalagret oberoende av hur en speciell tillämpnings datakub ser ut.
Givetvis kan man också ha flera aggregatfunktioner om man vill.
Ett viktigt begrepp i designen av ett datalager är finkornighet (på engelska grain), eller i hur små enheter som dimensionsaxlarna är graderade. I glassexemplet har vi till exempel en tidsdimensionen som är graderad i månader.
Det är en ganska grovkornig gradering, som kanske räcker för de sammanställningar som företaget brukar göra, men det blir förstås svårt att hitta samband som beror på dagar eller timmar. Är till exempel försäljningen större på veckoslut än på vardagar? Är det någon skillnad på storhelger? Varierar glassförsäljningen mellan förmiddag och eftermiddag?
De frågorna kan vi aldrig få svar på, för de uppgifterna finns helt enkelt inte i datalagret. I stället är allt hopklumpat månadsvis.
För att det ska vara möjligt att göra nya sammanställningar, och hitta nya samband som man inte tänkt på tidigare, brukar man rekommendera att lagra data i datalagret så finkornigt som möjligt. Om källdatabasen innehåller försäljning per timme, bör man behålla den detaljeringsgraden i datalagret. Det går alltid att aggregera timvisa data till dagar, veckor och månader, men tvärtom går förstås inte.
För att analysera köpmönster kan det till och med vara lämpligt att lagra alla köp varje kund gör i datalagret. Det blir en stor databas det!
Ett dataförråd är mindre än ett datalager, och avsett för en viss avdelnings eller en viss funktions behov. Dataförrådet kan antingen vara fristående eller en del av hela företagets stora datalager, och innehåller kanske en enda datakub. Den engelska termen för dataförråd är data mart, och den används ofta också på svenska. En annan svensk term som föreslagits är dataskafferi.
Ett sätt att använda dataförråd är att man inte har något centralt datalager, utan varje avdelning eller funktion har ett eget dataförråd, och det är alla de olika dataförråden som tillsammans bildar företagets datalager. Då är det också viktigt att ha en gemensam informationsarkitektur och gemensamma standarder, så att man faktiskt kan tala om ett datalager och inte bara en samling inkompatibla datamängder. Om man ska kunna göra analyser som berör flera av förråden, måste de dimensioner som används i flera dataförråd ha samma definition överallt. Även de fakta man lagrar i de olika faktatabellerna måste ha en gemensam definition. Till exempel måste man bestämma om priser anges med eller utan moms.
Det är också viktigt att de där "dataförråden" över huvud taget går att komma åt. Det är inte så ovanligt att olika avdelningar upprätthåller egna kalkylblad i Excel med lokala data utan samordning sinsemellan. Det gör data mycket svåra att kombinera!
Datautvinning, eller data mining som är den engelska termen och som ofta används även på svenska, innebär att man studerar stora mängder data för att hitta nya intressanta samband och därigenom få ny kunskap. Det kan handla om att analysera konsumtionsmönster för att göra mer effektiva reklamkampanjer, och det kan handla om att analysera sjukdomsstatistik för att studera hur behandlingar för olika sjukdomar interagerar med varandra.
Det klassiska exemplet på datautvinning går ut på att analysera alla inköp som kunder gjort i ett varuhus. För varje kund lagrar man vilka varor hon köpt. Detta kan illustreras som en oerhört stor tabell med en kolumn för varje vara i varuhuset och en rad för varje kundbesök. Man markerar med en 1:a vilka varor kunden köpt vid varje tillfälle. Datautvinningsproblemet är i detta fall att upptäcka vilka samband det finns mellan olika inköp. Till exempel finns det ett starkt samband mellan inköp av strumpor och skor. Man är bara 358 intresserad av tillräckligt starka samband, till exempel att sannolikheten att köpa en vara A är större än X när vara B också köps. Den typen av analys brukar kallas statistisk inferens och har gjorts av statistiker sedan länge. Skillnaden här är att den statistiska inferensen görs över väldigt stora datamängder som ofta hämtas från ett datalager. Utmaningen är att kunna göra sådan storskalig statistisk inferens med rimlig effektivitet. Programsystemen som gör datautvinningen kan med fördel köras separat från datalagret, till exempel över en extraherad fil av ovanstående köpmönster.
Här är ett par exempel på kunskap som datautvinning kan ge:
Ibland används termen datautvinning slarvigt också för att inkludera databassökningar i allmänhet, det vill säga att en användare letar ny kunskap med hjälp av SQL-frågor. Här skiljer vi dock mellan att ställa frågor mot ett databassystem och att utvinna data. Skillnaden mellan datautvinning och databassökning kan sammanfattas så här:
För datautvinning använder man sig ofta av analysverktyg för presentation av statistik på olika sätt, till exempel histogram och andra grafiska presentationer, eller i form av statistiska tabeller. Det är viktigt att användaren inte är låst till vissa förutbestämda sökningar och sammanställningar, vare sig av verktygen eller av datalagrets innehåll och struktur, utan användaren ska fritt kunna välja hur hon vill studera datamängden. Genom frågespråket får man ett flexibelt sätt att extrahera data från datalagret för datautvinning. För att upptäcka trender över tiden används ofta statistiska metoder baserade på till exempel regressionsanalys, flytande medelvärden och kurvanpassning.
Se avsnitt 33.2 på sidan 673 för detaljer om böckerna.
3622 En cache innebär att data från disken tillfälligt lagras i datorns primärminne, för att snabba upp frågor. Cache rimmar på krasch och har ingenting med cash (kontanter) att göra. Egentligen betyder det gömställe (för proviant eller liknande) eller härbre.
4 Frågan är något förenklad jämfört med stjärnschemat på sidan 351. Egentligen borde man koppla ihop faktatabellen Försäljning med dimensionstabellerna Glassar och Månad, för att få med namnen på glassarna och månaderna i frågan, men för enkelhets skulle låtsas vi som om de finns direkt i faktatabellen.
En databasbaserad webbplats är en webbplats som lagrar data i en databas, och där åtminstone en del av de webbsidor som finns på webbplatsen är föränderliga, och baseras på innehållet i databasen. När innehållet i databasen ändras, kommer också webbsidorna att ändras. Ibland kan man också lägga in data i databasen via webbplatsen, till exempel genom att fylla i ett formulär på en webbsida.
Exempel på databasbaserade webbplatser är webbutiker, som bokhandeln Amazon.com ( www.amazon.com, eller, för oss i Europa, www.amazon.co.uk). Information om varor, kunder, lager och beställningar hanteras av en databas, och man kan söka bland varorna och göra beställningar via webben.
Alla som läser det här har säkert använt en webbläsare, som Mozilla eller Microsoft Internet Explorer, för att titta på webbsidor. Till exempel kanske ett företag har en webbsida för att skryta med sin försäljning:
364Det som händer när man skriver in en webbadress (till exempel http://www.nekotronic.se/tpm.html) eller klickar på en länk till den sidan, är att webbläsaren, även kallad klienten, kopplar upp sig via internet mot datorn som heter www.nekotronic.se och skickar en begäran om att få se webbsidan som heter tpm.html. Denna begäran hanteras av en webbserver, till exempel av typen Apache, Microsoft IIS eller node.js. Webbservern är ett program som ständigt är i gång på serverdatorn och väntar på uppkopplingar. Den svarar genom att skicka källkoden för den önskade webbsidan till webbläsaren, och webbläsaren ritar sen upp webbsidan på skärmen.
En vanlig webbsida är statisk, dvsdess källkod finns lagrad fix och färdig för webbservern att skicka i väg, och den skickas oförändrad 365 till webbläsaren. Det kan vara helt vanliga filer på en hårddisk eller SSD, men det blir också vanligare att lagra webbsidorna i någon form av versionshanteringssystem, som hjälper till att hålla reda på webbsidorna, deras versioner, och vem som har skrivit dem. (Man skulle också kunna lagra de statiska sidorna i en databas. Det ger kanske en del fördelar när det gäller versionshantering och indexering, men statiska webbsidor i en databas är inte det som vi menar med en databasbaserad webbplats.)
I alla vanliga webbläsare kan man titta på källkoden, till exempel genom att klicka på View Page Source eller (med en ganska tveksam översättning) Visa Källa. Källkoden består av HTML, eller Hypertext Markup Language, och förutom texten som ska visas innehåller den styrkommandon, så kallade taggar. Exempel på taggar är <ol>, som anger en lista med numrerade element, och dess slut-tagg </ol>, som anger slutet på listan. Försäljningswebbsidan har kanske den här källkoden:
<html>
<head>
<title>Försäljning</title>
</head>
<body>
<h1>Försäljning</h1>
Hittills i år har vi sålt för 5528587 kronor.
<p>
De tre bästa försäljarna:
<ol>
<li>Hulda,
394878 kronor.</li>
<li>Mohammed,
375651 kronor.</li>
<li>Hjalmar,
360557 kronor.</li>
</ol>
</body>
</html>
366
Det är förstås besvärligt att hålla en webbsida som försäljningswebbsidan uppdaterad. Någon måste sitta och räkna ihop försäljningen och redigera filen i en texteditor, och informationen blir snabbt inaktuell. Därför vore det förstås bättre att låta ett program göra det automatiskt. Alla data om försäljningen finns förmodligen i en databas, och den önskade informationen går enkelt att få fram med hjälp av ett par SQL-frågor.
En lösning är att skriva ett program skapar en ny statisk webbsida, antingen med jämna mellanrum, eller varje gång data i försäljningsdatabasen ändras. Men det är vanligare att använda en dynamisk webbsida, vars källkod skapas, eller i alla fall skrivs om, för varje ny begäran som kommer in till webbservern.
Bland de tidigaste metoderna för dynamiska webbsidor är CGI, som betyder Common Gateway Interface. I stället för att lagra en fil med webbsidans källkod, skapar man ett program, ett CGI-program, som genererar källkoden. När webbservern får en begäran från en webbläsare om att få titta på webbsidan, startar webbservern CGI-programmet. Webbservern tar hand om programmets utmatning, och det är den utmatningen som blir webbsidans källkod och som skickas till webbläsaren. Eftersom CGI-programmet är ett fristående program kan det skrivas i vilket programmeringsspråk som helst, men det är vanligt att använda så kallade skriptspråk (engelska: scripting languages) som PHP, Perl, eller Python. Därför, och även eftersom CGI-program ofta är ganska korta och enkla, kallas de ofta för CGI-skript.
Eftersom CGI-programmet är ett fristående program, kan det göra allt som ett vanligt program kan. (Så länge det inte tar mer än några få sekunder, för i andra änden av datornätet sitter en otålig 367 människa och väntar på att få se webbsidan.) Till exempel kan CGI-programmet göra sökningar i en databas med SQL, på de sätt som vi läst om i kapitlet om SQL inuti program. Det finns inget i CGI-standarden som säger att CGI-programmet måste ha något alls med databaser att göra, utan det kan hämta sina data från en fil, eller låta bli att använda några externa data alls, men eftersom det här är en databasbok antar vi att programmet gör en sökning i en databas. Så här kan man rita upp arkitekturen för det fallet:
Eftersom webbsidan genereras på nytt varje gång någon begär att få titta på den, kommer dess innehåll alltid att vara aktuellt. Om uppgifter om försäljningen läggs in i databasen direkt när en vara sålts, kan en försäljare alltså slå in en vara i kassan, och sen direkt titta på försäljningswebbsidan om hon kommit upp på försäljarnas topplista.
CGI-standarden innehåller även sätt att skicka information från webbservern till CGI-programmet, så att programmet kan anpassa den genererade webbsidan på olika sätt. Till exempel kanske användaren har markerat på en webbsida att hon vill ha mer information om en viss produkt i en webbutik, och då skickas den markeringen från webbläsaren till webbservern och vidare till CGI-programmet.
368Vi ska nu titta på hur man kan skriva ett CGI-program för försäljningswebbsidan. Databasen innehåller tabellerna Vara, Forsaljare och Forsaljning, med de kolumner som man kan förvänta sig, Vi börjar med att skriva de SQL-frågor som behövs, och prova ut dem interaktivt:
select sum(Antal * Pris) as Summa
from Forsaljning, Vara
where Forsaljning.Vara = Vara.Id;
select Forsaljare.Id, Forsaljare.Namn,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xsum(Antal * Pris) as Summa
from Forsaljning, Vara, Forsaljare
where Forsaljning.Vara = Vara.Id
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xand Forsaljning.Forsaljare = Forsaljare.Id
group by Forsaljare.Id, Forsaljare.Namn
order by Summa desc limit 3;
Id | Namn | Summa |
14 | Hulda | 394 878 |
13 | Mohammed | 375 651 |
11 | Hjalmar | 360 557 |
I den andra frågan måste kolumnen Id från tabellen Forsaljare vara med i group by, för annars räknas alla försäljare med samma namn som en enda. Att den sen är med i resultatet beror bara på att de kolumner som används för att gruppera med group by måste vara med. Det behövs inte i alla SQL-dialekter, men vi tar med det här i alla fall för att göra exemplet mer generellt.
Vi skulle kunna skriva programmet i C, som i en del av exemplen i kapitlet om SQL inuti program, men det är vanligare med olika skriptspråk. Särskilt Perl och Python används ofta. Vi väljer dock ett skriptspråk som är enklare att läsa än Perl för den som kan C, C++ eller Java, nämligen språket Pike som körs på webbserverern Roxen . 1 Så här ser programmet ut:
#!/usr/local/bin/pike
int main() {
369
write("Content-type: text/html\r\n");
write("\r\n");
write("<html>\n");
write("<head>\n");
write("<title>Försäljning</title>\n");
write("<head>\n");
write("\n");
write("<body>\n");
write("\n");
write("<h1>Försäljning</h1>\n");
write("\n");
Sql.sql db =
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSql.sql("mysql://fbuser:zSplor7zk&"
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"dbserver/forsaljningsbasen");
Sql.sql_result result =
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdb->big_query("select sum(Antal * Pris) as Summa"
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" from Forsaljning, Vara"
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" where Forsaljning.Vara = Vara.Id");
array row = result->fetch_row();
write("Hittills i år har vi sålt för " + row[0] +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" kronor.\n");
write("\n");
write("<p>\n");
write("\n");
result =
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdb->big_query("select Forsaljare.Id,"
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" Forsaljare.Namn,"
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" sum(Antal * Pris) as Summa"
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" from Forsaljning, Vara, Forsaljare"
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" where Forsaljning.Vara = Vara.Id"
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" and Forsaljning.Forsaljare = Forsaljare.Id"
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" group by Forsaljare.Id, Forsaljare.Namn"
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" order by Summa desc limit 3");
write("De tre bästa försäljarna:\n");
write("\n");
write("<ol>\n");
while ((row = result->fetch_row()))
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwrite("<li>" + row[1] + ", " + row[2] +
370
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" kronor.</li>\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwrite("</ol>\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwrite("\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwrite("</body>\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwrite("</html>\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn 0;
} // main
Eftersom det är programmets utmatning som blir den HTML-källkod som skickas till webbläsaren, måste hela källkoden (taggar, data från databasen och fasta textstycken) skrivas ut med write. Programmet använder ett ganska traditionellt databas-API: det ansluter till databasen, det skickar in SQL-frågor, och det hämtar resultatet en rad i taget. För att förenkla har vi inte tagit med all den felhantering som ska vara med i ett riktigt program, för att användaren ska få tydliga, informativa och rättvisande felmeddelanden.
Eftersom CGI-program är fristående program, är de dåligt integrerade med webbservern, och de kan ge webbplatsen dåliga prestanda. Ett alternativ är att bygga ut själva webbservern genom att skriva program, i C eller något annat språk, som antingen länkas ihop med webbservern eller laddas in dynamiskt. Dessa program utökar webbservern med ny funktionalitet, till exempel extra taggar som hanteras av servern och ersätts med annan text.
Det är lite opraktiskt att behöva skriva ett helt CGI-program, eller att skriva ett utökningsprogram till webbservern, bara för att ställa ett par enkla SQL-frågor och lägga in deras resultat i en webbsida. Dessutom kan CGI-program, som vi såg ovan, ge webbplatsen dåliga prestanda. Därför vill man hellre integrera databasstödet i webbservern, och ett sätt är att utöka webbservern så att man kan skriva SQL-satser direkt i webbsidorna, och låta webbservern förstå att här kommer det SQL-kod som man måste göra något med innan webbsidan kan skickas i väg.
Det finns flera olika olika sätt att bädda in antingen programsnuttar skrivna i något skriptspråk, eller rena SQL-satser, i HTML-koden på en webbsida. Webbservern läser då HTML-koden, men i stället för att skicka den direkt till webbläsaren hanterar webbservern själv programsnuttarna eller SQL-satserna. Om det är programsnuttar skrivna i ett skriptspråk som webbservern förstår, kan webbservern själv köra de programsnuttarna, och ersätta dem med resultatet av körningen. Det brukar kallas server-side scripting, eftersom programsnuttarna alltså körs i webbservern innan webbsidan skickas till klienten, till skillnad från till exempel vanlig JavaScript, vilken körs i klienten efter det att webbsidan kommit dit.
Vare sig det är programsnuttar eller rena SQL-satser kommer SQL-frågorna att skickas från webbservern till en databasserver, som 372 kör frågorna och returnerar resultatet. Webbservern kommunicerar alltså direkt med databasservern, utan att något extra program behöver startas eller vara i gång.
För att ytterligare förbättra prestandan kan webbservern låta bli att koppla ned förbindelsen till databasservern, utan behålla den öppen. På det sättet slipper man göra en ny uppkoppling nästa gång någon begär en webbsida som innehåller SQL-frågor som ska köras mot samma databas. Eftersom webbservern ofta arbetar med flera förfrågningar om webbsidor parallellt, behövs kanske flera samtidiga uppkopplingar mot databasservern. Existerande förbindelser kan då samlas i en gemensamt åtkomlig grupp, så kallad connection pooling.
En PHP-sida är som en vanlig statisk webbsida, men mellan <? php och ? > kan man stoppa in programkod i PHP. Om vi återgår till exemplet med försäljningswebbsidan, kan vi skriva en PHP-webbsida som ser ut så här. De delar som inte består av statisk, oföränderlig HTML, utan av PHP-kod, är markerade med fetstil.
<html>
<head>
373
<title>Försäljning</title>
</head>
<body>
<h1>Försäljning</h1>
Hittills i år har vi sålt för
<?
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x $db = mysql_connect("dbserver", "fbuser", "zSplor7zk");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x mysql_select_db("forsaljningsbasen", $db);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x $result =
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x mysql_query("select sum(Antal * Pris) as Summa
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x from Forsaljning, Vara
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x where Forsaljning.Vara = Vara.Id", $db);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x echo mysql_result($result, 0, "Summa");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x ?>
kronor.
<p>
De tre bästa försäljarna:
<ol>
<?
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x $result =
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x mysql_query("select Forsaljare.Id, Forsaljare.Namn,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x sum(Antal * Pris) as Summa
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x from Forsaljning, Vara, Forsaljare
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x where Forsaljning.Vara = Vara.Id
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x and Forsaljning.Forsaljare = Forsaljare.Id
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x group by Forsaljare.Id, Forsaljare.Namn
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x order by Summa desc limit 3", $db);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x while ($myrow = mysql_fetch_row($result))
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x printf("<li>%s, %s kronor.</li>", $myrow[1], $myrow[2]);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x ?>
</ol>
</body>
</html>
374
Webbsidan påminner om den statiska webbsida som vi såg först i det här kapitlet, men den innehåller två block med PHP-kod, där anrop görs till MySQL-servern och SQL-frågor körs. Webbservern skickar SQL-frågan till databashanteraren, och tar emot raderna i resultatet.
Notera att det finns två olika sorters "källkod" inblandad. Vi har dels den HTML-kod med inbäddade SQL-satser som lagras i anslutning till webbservern. Vi kan kalla den serverkällkod. Denna serverkällkod skrivs om av webbservern, som byter ut PHP-koden och andra serverkommandon mot vanlig HTML-kod. Det är resultatet av den omskrivningen som skickas via datornätverket till klienten, webbläsaren, och som webbläsaren använder när den sen ritar upp webbsidan på skärmen. Vi kan kalla denna omskrivna källkod för klientkällkod. Om man i webbläsaren tittar på källkoden med View Page Source eller Visa Källa, är det klientkällkoden man får se.
En populär teknikkombination är att använda operativsystemet Linux, webbservern Apache, databashanteraren MySQL och skriptspråket PHP. Det brukar kallas för LAMP, efter begynnelsebokstäverna i de olika teknikerna.
Microsoft har en liknande teknik som heter Active Server Pages, eller ASP. En ASP-sida är som en vanlig webbsida, men mellan <% och %> kan man stoppa in programkod i Visual Basic, JavaScript, Perl eller andra språk. Med hjälp av ODBC kan den koden ansluta till en databashanterare.
JavaScript är i praktiken det standardspråk som körs i webbläsare för att göra nerladdade webbsidor dynamiska. JavaScript kan också användas på den numera mycket vanliga webbservern node.js. 2 I en node.js webbserver används Googles högpresterande JavaScriptmotor som heter V8. Med node.js får man således JavaScript både på webbservern och i webbläsarna, vilket ger stor flexibilitet. Det finns gränssnitt från node.js mot de vanliga relationsdatabashanterarna liksom mot NoSQL-databashanterare som till exempel MongoDB.
Det är också populärt att skapa dynamiska webbsidor som fungerar med hjälp av Java, i synnerhet för lite större och mer komplicerade webbtillämpningar. Man kan använda JavaServer Pages (JSP) som på ett liknande sätt som PHP och ASP blandar vanlig HTML-kod med särskilda extra taggar, som ska hanteras av servern. En JSP-sida kompileras till en Java-servlet, som är ett Java-program som körs på webbservern. (Till skillnad från en Java-applet, som körs på klienten.) Ett system för att använda JSP och servlets på en webbserver är Jakarta Tomcat.
Ett exempel på hur man kan lägga in SQL-satser i webbsidor, utan hjälp av något skriptspråk som PHP, ges av Roxen WebServer 3 och dess RXML (Roxen Markup Language). De intressanta delarna av försäljningswebbsidan kan i Roxen Webserver skrivs så här. De delar som inte består av statisk, oföränderlig HTML är markerade med fetstil.
<h1>Försäljning</h1>
Hittills i år har vi sålt för
<emit source="sql" host="forsaljningsbasen"
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x query="select sum(Antal * Pris) as Summa
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x from Forsaljning, Vara
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x where Forsaljning.Vara = Vara.Id">
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x &_.Summa;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x </emit>
kronor.
<p>
De tre bästa försäljarna:
<ol>
<emit source="sql" host="forsaljningsbasen"
376
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x query="select Forsaljare.Id, Forsaljare.Namn,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x sum(Antal * Pris) as Summa
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x from Forsaljning, Vara, Forsaljare
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x where Forsaljning.Vara = Vara.Id
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x and Forsaljning.Forsaljare = Forsaljare.Id
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x group by Forsaljare.Id, Forsaljare.Namn
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x order by Summa desc limit 3">
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x <li>&_.Namn;, &_.Summa; kronor.</li>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x </emit>
</ol>
Roxen har en speciell emit-tagg, som kan användas för att köra SQL-frågor. Den tar ett namn på en databas och en SQL-fråga som argument. Roxen har ett administrationsgränssnitt där man anger vilka databaser man vill kunna arbeta med, och med vilket användarnamn och lösenord man ska ansluta sig.
CGI-program och SQL-satser som tolkas av webbservern är långt ifrån det enda sättet att bygga en databasbaserad webbplats. Ett annat alternativ är att ladda ner ett program till klientdatorn. Som exempel kan man ta Java-appletar, som är små program som kan köras inuti webbläsaren, till exempel för att erbjuda mer avancerad interaktion med användaren än vad webbläsaren själv klarar av. Just Java-appletar är inte längre så vanliga, men visar principen. Java-appleten kan också koppla upp sig mot en databashanterare via datornätet, och så kan kommunikationen med databasen ske med hjälp av appleten.
377Java-appletar har fördelelen att vara (någorlunda) oberoende av vilken typ av webbläsare och vilket operativsystem som körs på klientdatorn, och eftersom de körs i en skyddad omgivning inuti webbläsaren är de också (någorlunda) säkra. Men man kan förstås tänka sig att man laddar ner ett vanligt program av något slag till klienten, och använder det för åtkomsten av databasen.
En teknik som blivit mycket spridd, inte bara för webbtillämpningar utan inom många olika områden, är XML. XML står för Extensible Markup Language (vilket ungefär betyder "utvidgningsbart uppmärkningsspråk") och är egentligen ett sätt att formatera data som text.
XML är i sig inte ett språk för att formatera data, utan det är ett sätt att konstruera dataformat. XML-baserade textformat kan användas för att lagra och överföra alla möjliga typer av strukturerade data, till exempel dokument, kalkylblad, tekniska ritningar och inställningar för program.
378XML påminner mycket om HTML när man ser det, med taggar som avgränsas av "<" och ">". Den stora skillnaden jämfört med HTML är att nya XML-taggar definieras för varje tillämpning, så att ett helt nytt språk bildas.
Om vi tänker oss att vi vill ha en lista med begagnade bilar, kan vi skriva den som HTML:
<ol>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<li>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<ul>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<li>RFN540</li>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<li>Renault Scenic</li>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<li>1999</li>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<li>10000</li>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<li>80000</li>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x</ul>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x</li>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<li>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<ul>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<li>RKE899</li>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<li>Volvo V70</li>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<li>1998</li>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<li>80000</li>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<li>10000</li>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x</ul>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x</li>
</ol>
I en webbläsare kommer HTML-koden ovan att visas ungefär så här:
379HTML-koden talar om hur texten ska formateras av webbläsaren, men den säger inget om vad den betyder. Vi kan gissa oss till vad som är registreringsnummer, bilmodell och årsmodell, men vilka siffror är det som anger miltal och vilka är det som anger pris?
Med XML kan man skapa ett särskilt bilspråk, med taggar som anger vad allting betyder:
<bilar>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<bil>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<regnr>RFN540</regnr>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<modell>Renault Scenic</modell>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<arsmodell>1999</arsmodell>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<mil>10000</mil>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<pris>60000</pris>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x</bil>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<bil>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<regnr>RKE899</regnr>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<modell>Volvo V70</modell>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<arsmodell>1999</arsmodell>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<mil>60000</mil>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x<pris>10000</pris>
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x</bil>
</bilar>
380
Ett visst XML-språk, som bilspråket ovan, definieras med hjälp av en så kallad DTD, som står för Document Type Definition och är en grammatik (även den skriven i XML) som beskriver språket. XML-verktyg kan jämföra XML-koden med DTD:n för att kontrollera att koden är riktig.
XML-koden ovan säger inte så mycket om hur listan ska visas, men man kan använda stylesheets (CSS) för att ange hur ett XML-språk ska visas, på liknande sätt som man kan använda stylesheets för HTML. Dessutom finns det olika metoder för att ange hur ett XML-språk ska konverteras, till exempel till HTML.
Man kan tycka att XML är ett ganska "pratigt" språk, med mycket onödig text. Det är med avsikt. Visserligen är det meningen att XML-filer ska läsas och hanteras av program, men tack vare ordrikedomen i XML går det i nödfall för en människa att läsa och förstå filen direkt. Filerna kan sedan komprimeras, med Zip eller liknande program, vilket sparar lagringsutrymme och överföringskapacitet.
Ett exempel på användning av XML i webbsammanhang är att en webbplats kan lagra innehållet i webbsidorna som XML. Sen, när en webbsida ska skickas ut till klienten, skapar man vanlig HTML utifrån XML-innehållet. Med data lagrade som XML kan man ha en mycket bättre kontroll av innehållet än med HTML, till exempel så att varje webbsida måste ha en rubrik och en ingress, och man slipper som med HTML att blanda innehåll, struktur och webbsidornas utseende.
Som ett annat exempel på XML, som inte har med webben att göra, kan vi ta ritprogrammet Dia. De flesta figurerna i den här boken är ritade med Dia. Dia använder XML för att spara bilderna man ritat. För några år sedan skulle ett program som Dia ha använt något helt eget format för att lagra sina data, men nu finns XML, som har en mängd färdiga verktyg och bibliotek som underlättar arbetet.
XML kräver ingen licens, det är plattformsoberoende, och det finns ett brett stöd i form av program, verktyg och kunskap. Ytterligare en fördel med XML är att utbyte av data, såväl mellan olika program som mellan olika organisationer, underlättas när man har ett gemensamt sätt att lagra dem.
Ett XML-dokument kan ses som en databas, med XML som datamodell och med DTD:n som schema. Eftersom XML kommit att användas mer och mer, och för större och större datamängder, har det uppstått behov av att hantera dessa stora XML-datamängder på ett 381 effektivt sätt. Därför finns det numera databashanterare som arbetar med XML. Många relationsdatabashanterare kan exportera och importera data i XML-format, men här talar vi alltså om databashanterare som har XML (och inte relationsmodellen) som datamodell. En del av dem lagrar XML-dokumenten som text, medan andra använder någon annan form av internformat.
Ett alternativ till XML för att serialisera data, som blivit populärt och som många anser vara bättre än XML, är JSON. JSON är en förkotning av JavaScript Object Notation, men är inte knutet till JavaScript utan kan användas i många olika programmeringsspråk.
Här är en motsvarighet i JSON till XML-exemplet med begagnade bilar på sidan 379:
{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"bilar": [
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"regnr": "RFN540",
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"modell": "Renault Scenic, "arsmodell": 1999,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"mil": 10000,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"pris": 60000
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x},
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"regnr": "RKE899",
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"modell": "Volvo V70", "arsmodell": 1999,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"mil": 60000,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"pris": 10000
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x]
}
WWW (står för World Wide Web) eller webben. Ett klient/serverbaserat informationssystem med hypertext, dvs text som innehåller länkar till andra texter. På norska: den verdensvide veven! Dokument som kallas webbsidor och är skrivna i språket HTML (Hypertext Markup Language) skickas från en webbserver (engelska web server) till en webbklient, även kallad webbläsare (engelska web browser). En samling webbsidor som hör ihop kallas en webbplats (engelska: web site).
Databasbaserad webbplats (engelska: database-backed web site). En webbplats som lagrar data i en databas, där "databas" används i betydelsen av data som hanteras av en databashanterare, och där åtminstone en del av de webbsidor som finns på webbplatsen är föränderliga och baseras på innehållet i databasen. Ibland kan man också lägga in data i databasen via webbplatsen.
Statisk webbsida (engelska: static web page). En webbsida som lagras fix och färdig i, eller i anslutning till, webbservern. Webbsidan kan, men måste inte, lagras som en vanlig fil. När webbklienten begär att få se sidan, skickas den i oförändrad form från webbservern.
Dynamisk webbsida (engelska: dynamic web page). En webbsida som inte finns fix och färdig för webbservern att skicka i väg, utan som först måste genereras eller åtminstone skrivas om varje gång en webbklient begär att få se sidan. En dynamisk webbsida kan, men måste inte, innehålla SQL-satser som körs för att få fram data ur en databas. Dessa data ingår därefter i webbsidans innehåll.
CGI (står för Common Gateway Interface). Ett standardiserat system för dynamiska webbsidor. I stället för att lagra webbsidans innehåll i till exempel en fil, så skriver man ett program, det så kallade CGI-programmet eller CGI-skriptet, som genererar webbsidans innehåll när det körs. När webbservern får en begäran från en webbläsare om att få titta på webbsidan, startar webbservern CGI-programmet. Webbservern tar hand om programmets utmatning, och det är den utmatningen som blir webbsidans källkod och som skickas till webbläsaren. CGI-programmet kan, men måste inte, koppla upp sig till en databas för att hämta delar av webbsidans innehåll.
383ASP (står för Active Server Pages). Microsofts teknik för dynamiska webbsidor. (Kan också betyda Application Service Provider, som är något helt annat.)
Connection pooling. Kanske "gemensam anslutningssamling" på svenska? När en webbserver ställt en SQL-fråga mot en databasserver och fått resultatet, kopplar den inte ned förbindelsen till databasservern, utan behåller den så att den kan återanvändas för fler frågor. Flera förbindelser samlas i en "pool" i väntan på att återanvändas.
XML (står för Extensible Markup Language). Ett sätt att konstruera textformat för lagring av data. XML påminner om HTML, men man definierar nya taggar för varje tillämpning.
JSON (står för JavaScript Object Notation). Ett annat sätt att konstruera textformat för lagring av data, med en mindre otymplig notation än XML.
Det är vanligt att man vill kunna lagra och hantera tid av olika slag i databaser. Det kan handla om klockslag, datum eller tidsintervall. Det kan vara allt från enkla tidsuppgifter, som vilken dag en faktura skrevs, till mer komplicerad användning, som att hålla reda på vilka uppgifter databasen innehållit tidigare, och när de skrevs in eller ändrades, så att man kan gå tillbaka och studera vad databasen innehöll vid en viss tidigare tidpunkt.
I SQL-standarden (SQL:2003) finns flera tidsdatatyper som man kan använda som typer på kolumner. Det här är de vanligaste, och de brukar finnas i alla SQL-dialekter:
De här tidsdatatyperna är byggelement som vi kan använda i de databaser vi konstruerar, precis som heltal och teckenfält. Men vi behöver också lära oss vad man kan bygga med de byggelementen, och hur man gör. Det är det som resten av det här kapitlet handlar om.
De databaser vi tittat på i den här boken har för det mesta innehållit data som gäller just nu. Om det står i databasen att Olle har telefonnummer 260088, så har han det just nu. Kanske hade han ett annat telefonnummer tidigare, men det var då det, och sådana gamla uppgifter finns inte kvar i databasen. Exempel:
Nummer | Namn | Telefon |
1 | Olle | 260088 |
2 | Stina | 282677 |
Det här kallas ibland för en snapshot-databas. Den beskriver ju världen vid en enda tidpunkt, ungefär som ett fotografi.
Man kan förstås diskutera exakt vilken tidpunkt det är som databasen egentligen beskriver. Man hinner kanske inte lägga in nya uppgifter direkt, så kanske är databasen inte riktigt aktuell, utan det är mer verkligheten från i går eller för en vecka sen som databasen beskriver. Dessutom läggs alla nya uppgifter inte alltid in samtidigt, så kanske innehåller databasen en del uppgifter från i dag, en del från i går, och några som är kvar sen förra året. En del uppgifter lägger man kanske in i förväg, och de börjar inte gälla förrän i morgon! Men även om databasens "nu" alltså kan vara både föråldrat och "suddigt", brukar man tänka sig att det är världens tillstånd precis nu som beskrivs.
Den enklaste typen av tidshantering i en databas är uppgifter som innehåller, eller gäller vid, olika tidpunkter. Det kan till exempel 387 vara fakturor, där varje faktura skrivits en viss dag. Ett annat exempel är mätvärden från vissa tidpunkter, till exempel uppmätta temperaturer, som var och en mättes upp vid en viss tidpunkt.
Det brukar vara ganska enkelt att hantera den sortens tid. Lägg bara till en kolumn med den intressanta tidsuppgiften:
Fakturor | |||
Nummer | Kund | Belopp | Datum |
1 | 17 | 260.00 | 2002-01-19 |
2 | 13 | 114.50 | 2002-02-07 |
3 | 101 | 2116.50 | 2002-02-07 |
4 | 17 | 100.00 | 2002-02-13 |
Ibland har man hela serier av uppgifter, som samlats in med förutbestämda tidsintervall. Det kan vara temperaturer som mäts varje timme, eller aktiekurser som matas in en gång varje dag. En sådan serie av uppgifter kallas en tidsserie.
Ofta vill man använda uppgifterna till att göra beräkningar som har med tid att göra. Till exempel kanske man vill beräkna medeltemperaturen för varje dygn, eller analysera trender i aktiernas kursutveckling.
Traditionella databashanterare har sällan något särskilt bra stöd för att hantera tidsserier, och man har länge varit hänvisad till specialskrivna program som arbetar med vanliga filer. Numera finns det tillägg för bearbetning av tidsserier till flera av de stora databashanterarna, till exempel nyare versioner av Oracle och SQL Server. Fr.o.m. SQL:2011 ingår temporala databaser i SQL-standarden.
De databaser vi tittat på i den här boken har alltså för det mesta varit snapshot-databaser. Uppgifterna i databasen gäller just nu:
Nummer | Namn | Telefon |
1 | Olle | 260088 |
2 | Stina | 282677 |
Olle har telefonnumret 260088. Kanske hade han ett annat telefonnummer tidigare, men det var då det, och om den uppgiften över huvud taget funnits i databasen så är den borta nu.
Men ibland vill man lagra information som gäller för olika tidsperioder. Vi kan, till exempel av skatteskäl, behöva behålla uppgifter om vad anställda har haft för löner under året. Eller, om vi ska fortsätta med telefonnummer, så behöver vi kanske känna till de gamla telefonnumren för att kunna sammanställa en korrekt telefonräkning i efterhand. I så fall måste databasen förstås innehålla uppgifter inte bara om vad Olle har för telefonnummer just nu, utan om Olles telefonnummer vid olika tidpunkter.
Den här tabellen i databasen måste alltså innehålla flera versioner av varje rad. Vi kan säga att Olle har en enda logisk rad, men att den finns i flera radversioner. Den radversion som gäller nu kallas aktuell radversion, och de gamla radversionerna kallas historiska.
Nummer | Namn | Telefon | |
april | 1 | Olle | 174433 |
maj | 1 | Olle | 174433 |
juni | 1 | Olle | 260088 |
juni | 2 | Stina | 282677 |
Om man vill det kan man i stället se det som att hela tabellen finns i flera versioner:
april | Nummer | Namn | Telefon |
1 | Olle | 174433 |
maj | Nummer | Namn | Telefon |
1 | Olle | 174433 |
juni | Nummer | Namn | Telefon |
1 | Olle | 260088 | |
2 | Stina | 282677 |
Oberoende av hur man ser det, finns det alltså flera versioner av samma uppgift i databasen. De olika uppgifterna gäller olika tider i verkligheten. Man säger att de har olika giltighetstid (på engelska: valid time).
Giltighetstid handlar inte om vad databasen innehöll för data vid olika tidpunkter. Vårt exempel säger inte att i april trodde databasen att Olle hade telefonnumret 174433. Det exemplet säger är att i april hade Olle telefonnumret 174433. (Eller, om man ska vara noga och ta hänsyn till att databasen kan innehålla felaktigheter, att databasen just nu tror att i april hade Olle telefonnumret 174433.) Det går att hantera vad databasen har trott vid olika tidpunkter, men det kallas transaktionstid och beskrivs längre fram i det här kapitlet.
En databas som innehåller information med en eller flera tidsdimensioner brukar kallas för en temporal databas. Det finns databashanterare med inbyggda funktioner för att hantera temporala databaser, men de är inte så vanliga. Om man använder en vanlig relationsdatabashanterare måste man i stället hantera tidsdimensionerna själv, genom att till exempel lägga till kolumner för giltighetstiden.
Om vi ska hantera uppgifter i flera versioner med olika giltighetstid i en vanlig relationsdatabashanterare, som inte har inbyggda funktioner för detta, får vi lägga till kolumner för att hålla reda på giltighetstiden för varje rad. Det vanliga sättet är att ha en kolumn med 390 den tid när uppgiften börjar att gälla, och en kolumn med den tid när uppgiften upphör att gälla. Vi kan kalla dem GällerFrån och GällerTill. På engelska kallas de ibland VST (Valid Start Time) och VET (Valid End Time).
Nummer | Namn | Telefon | GällerFrån | GällerTill |
1 | Olle | 174433 | 7 april | 24 maj |
1 | Olle | 260088 | 24 maj | nu |
2 | Stina | 282677 | 2 juni | nu |
3 | Cecilia | 516323 | 1 mars | 16 mars |
I exemplet använder vi en datatyp med enbart dag och månad, men det är bara för att spara plats i boken. Annars skulle vi antagligen ha valt datatypen TIMESTAMP, vilket gör att vi kan ange på sekunden när en uppgifts giltighetstid börjar och slutar, men det går också att använda DATE, som bara ger dagar, eller någon annan tidstyp som finns tillgänglig.
Om vi tittar på tabellen ser vi att Olle hade telefonnumret 174433 mellan 7 april och 24 maj. Den 24 maj bytte han telefonnummer till 260088, och sen dess har han haft det numret. Värdet nu i kolumnen GällerTill är ett specialvärde, som betyder att uppgiften fortfarande gäller. Vi har alltså inte lagt in aktuellt datum och aktuell tid i den kolumnen, för då skulle vi ju behöva ändra på det en gång varje dag – eller varje sekund. Om databashanteraren inte har något speciellt nu-värde, kan man i stället använda ett vanligt null-värde och låta det betyda nu.
En rad med nu i kolumnen GällerTill kallas också aktuell rad. Det är ju uppgifterna på den raden som är aktuella och gäller just nu.
Som vi ser har Olle och Stina var sin aktuell rad. Det är bara dessa två rader som skulle varit med om tabellen bara innehöll aktuella uppgifter. Alla de andra raderna består ju av gamla, historiska, uppgifter. Man kan tala om den icke-temporala tabellen, när vi så att säga filtrerat bort informationen om giltighetstid och bara har kvar de uppgifter som gäller nu. Den motsvarar det vi tidigare kallat för en snapshot-tabell. Man kan kalla raderna i den icke-temporala tabellen för icke-temporala rader. Så här ser den icke-temporala tabellen ut:
391Nummer | Namn | Telefon |
1 | Olle | 260088 |
2 | Stina | 282677 |
Notera att Cecilia inte finns med alls. Cecilia hade telefonnummer 516323 mellan den 1 mars och den 16 mars, men numera har hon tydligen ingen telefon. Därför finns hon inte heller med i tabellen med aktuella data.
Eftersom det finns flera versioner av varje "personrad", är den tidigare primärnyckeln, kolumnen Nummer, inte längre unik. Kolumnen Nummer kallas för den icke-temporala nyckeln, och den är ju fortfarande i någon mening den "logiska" nyckeln. Men det fungerar att skapa en primärnyckel som består av den icke-temporala nyckeln plus kolumnen GällerFrån, alltså Nummer + GällerFrån. Denna sammansatta nyckel kallas för temporal nyckel.
Vi får själva se till att giltighetstiderna för två versioner av en persons uppgifter aldrig överlappar varandra. Överlappande giltighetstider skulle ju betyda att det vid vissa tidpunkter skulle finnas mer än en gällande version av uppgifterna. Det vore bra om databashanteraren kunde sköta det åt oss, men det är svårt att ange som ett enkelt villkor i en databashanterare som inte har särskilt stöd för tidsdimensioner.
Vi antar för enkelhets skull att om man ändrar i databasen vill man lägga in uppgifter som börjar gälla precis när man lägger in dem, och man vill ta bort uppgifter som slutar gälla precis när man tar bort dem. Man kan också göra ändringarna i förväg eller i efterhand, och då får man använda tiden när uppgifterna börjar eller slutar gälla, i stället för den aktuella tiden.
GällerTill is null
, om vi använder null för att betyda nu).
Då får vi bara med aktuella rader.
Om vi i stället är intresserade av uppgifter som gällde en viss tidpunkt, får vi ställa upp ett lämpligt villkor som innefattar GällerFrån och GällerTill.
Som synes blir det en del krångel, både i datamodelleringen och i användningen. Man kan få en del hjälp av aktiva regler och integritetsvillkor, om nu databashanteraren stöder det, men det mesta får man skriva själv i SQL-koden och kanske i applikationsprogrammet. Inbyggt stöd, så att databashanteraren skötte en del av arbetet automatiskt, skulle vara praktiskt.
Vi har sett att giltighetstid i en databas går ut på att uppgifter kan gälla olika tidsperioder i verkligheten. En uppgift kan finnas i flera versioner, och varje version gäller för en viss tidsperiod. Olle hade ett telefonnummer i maj och ett annat i juni, och bägge de uppgifterna lagras i databasen.
Men ibland vill man i stället lagra flera versioner av en uppgift, som motsvarar gamla data i databasen. Vi hade lagt in Olles telefonnummer, men sen ändrade vi det i databasen, och vi vill ha kvar en ändringshistorik som visar när ändringen gjordes och vad den gamla 393 uppgiften var. Om databasen fick en tidsdimension i och med att uppgifterna gällde olika perioder i tiden, nämligen giltighetstid, så är det här en annan tidsdimension, som beskriver vad databasen faktiskt har innehållit. Eftersom det handlar om vilka ändringar som gjorts i databasen, kallas den här tidsdimensionen för transaktionstid.
Precis som med giltighetstid finns det databashanterare med inbyggda funktioner för att hantera transaktionstid, men de är inte så vanliga. Om man använder en vanlig relationsdatabashanterare får man hantera transaktionstiden själv, genom att lägga till kolumner i tabellen.
Om vi ska behålla gamla versioner av uppgifter i en vanlig relationsdatabashanterare, som inte har inbyggda funktioner för detta, får vi lägga till kolumner för att hålla reda på när varje rad lades in, ersattes eller togs bort. ("Ersattes" och "togs bort" i en logisk mening, för fysiskt ligger de ju kvar i databasen.)
Ungefär som med giltighetstid kan vi lägga till två kolumner: en som talar om när uppgiften börjar gälla i databasen, och en som talar om när uppgiften slutar gälla i databasen. På engelska kallas de ibland TST (Transaction Start Time) och TET (Transaction End Time), men här kallar vi dem för Inlagd och Borttagen. Kolumnen Inlagd fungerar som en tidsstämpel för när raden lades in i databasen.
Nummer | Namn | Telefon | Inlagd | Borttagen |
1 | Olle | 174433 | 7 april | 24 maj |
1 | Olle | 260088 | 24 maj | tv |
2 | Stina | 282677 | 2 juni | tv |
3 | Cecilia | 516323 | 1 mars | 16 mars |
Data i tabellen är samma som i exemplet med giltighetstid, och man arbetar med dem på ungefär samma sätt, men tidsuppgifterna betyder nu något annat. Dessutom skriver vi tv, som står för "tills vidare", och inte nu i kolumnen Borttagen för de aktuella raderna, 394 för de aktuella raderna är ju inte borttagna nu, utan de finns kvar tills man ändrar dem. (På engelska kan man skriva uc, som står för "until changed", i stället för tv.)
Vi ser att den 7 april la någon in i databasen att Olle har telefonnummer 174433. Den 24 maj ändrade någon den uppgiften till att han hade telefonnummer 260088. Ändringen i databasen kan ha berott på att den tidigare uppgiften var fel och rättades, eller på att Olle faktiskt fick ett nytt telefonnummer i verkligheten. Vi vet inte vilket, utan bara att ändringen gjordes.
Hanteringen av transaktionstid liknar väldigt mycket hanteringen av giltighetstider, förutom att tiderna nu handlar om uppgifternas existens i databasen och inte när de gällde ute i den riktiga världen. På samma sätt som med värdet nu i kolumnen GällerTill, är en rad med tv i kolumnen Borttagen en aktuell rad, och en rad med en angiven borttagningstid, det vill säga med något annat värde än tv i kolumnen Borttagen, är en historisk eller stängd rad.
En skillnad mellan en sån här transaktionstidstabell och en giltighetstidstabell är att man aldrig ändrar eller tar bort gamla uppgifter i en transaktionstidstabell. Man bara lägger till nya rader. (Och ändrar förstås den gamla aktuella radens Borttagen-tid från tv.) Men i en giltighetstidstabell får man ändra gamla data. Om vi till exempel får reda på att Olle faktiskt inte hade telefonnummer 174433 under tiden 8 april till 24 maj, som det stod i databasen, utan det var felskrivet och han hade egentligen 174434, så kan vi ändra den historiska raden i tabellen och byta ut 174433 mot 174434. En giltighetstidstabell handlar ju inte om vad som stod i databasen, utan hur det verkligen var i världen, och vi kan ändra felaktiga uppgifter om det. Men en transaktionstidstabell handlar just om vad som stod i databasen. Då får man förstås inte ändra en gammal uppgift, för det är ju just den uppgiften som har stått i databasen hela tiden.
Notera att raderna i tabellen inte kan ha lagts in i den ordning de står! (Övning: Varför? 1 )
Ibland behöver man båda tidsdimensionerna: både giltighetstid, för att hålla reda på hur världen såg ut vid olika tillfällen, och transaktionstid, 395 för att hålla reda på vad databasen trodde vid olika tillfällen. En tabell som innehåller både giltighetstid och transaktionstid kallas för en bi-temporal tabell.
För att hantera en bitemporal tabell i en vanlig relationsdatabas, kombinerar vi helt enkelt teknikerna för giltighetstidstabeller och transaktionstidstabeller. Tabellen får alltså fyra tidskolumner: GällerFrån, GällerTill, Inlagd och Borttagen.
För att förstå hur en bi-temporal tabell fungerar, går vi igenom steg för steg hur data läggs in i tabellen. Vi börjar med en helt tom tabell, och så tittar vi på arbetsdagen 7 april. Den dagen skaffar Olle telefon, och vi lägger in detta i databasen:
Nummer | Namn | Telefon | GällerFrån | GällerTill | Inlagd | Borttagen |
1 | Olle | 174433 | 7 april | nu | 7 april | tv |
Den 24 maj får Olle ett nytt telefonnummer, men vi hinner inte med att lägga in ändringen i databasen förrän några dagar senare, den 30 maj. Det som händer den 30 maj är att vi först ändrar tv i Borttagen-kolumnen till dagens datum för att markera att den raden inte gäller längre. Sen lägger vi in den nya versionen av raden med Olles gamla telefonnummer, den rad som nu visar att den 24 maj slutade Olle att ha telefonnummer 174433. Dessutom lägger vi in den första versionen av raden som visar Olles nya telefonnummer, det som gäller från 24 maj. Båda de nya raderna får förstås dagens datum i Inlagd-kolumnen:
Nummer | Namn | Telefon | GällerFrån | GällerTill | Inlagd | Borttagen |
1 | Olle | 174433 | 7 april | nu | 7 april | 30 maj |
1 | Olle | 174433 | 7 april | 24 maj | 30 maj | tv |
1 | Olle | 260088 | 24 maj | nu | 30 maj | tv |
Den 2 juni skaffar Stina telefon, med telefonnummer 282677. Samma dag lägger vi också in detta i databasen, i form av en rad där det står att hon har det numret från 2 juni och framåt, och att den uppgiften gäller från 2 juni och framåt:
396Den 4 juni upptäcker vi att Cecilia har haft telefon under perioden 1 mars – 16 mars. Hon har inte längre kvar telefonen, men vi lägger in en rad med hennes telefonnummer, den tid det gällde i verkligheten, och den tid uppgiften lades in:
Den 14 juni kommer vi på att en uppgift i databasen är fel. Det telefonnummer som Olle hade under tiden 7 april – 24 maj var inte alls 174433, som det står i databasen, utan det var 174434. Vi får inte ändra uppgiften direkt där den står, för det ska fortfarande gå att se vad databasen tidigare trodde att Olles nummer var under den perioden, men vi kan lägga till en ny rad. Den aktuella raden för Olles nummer under den perioden görs till en historisk rad genom att vi ändrar tv i Borttagen-kolumnen till dagens datum, och så lägger vi in en ny rad med den rättade uppgiften:
Nedan visas samma tabell med transaktionstidsdimensionen, som alltså utgör versionsinformation för uppgifterna, bortfiltrerad. Då återstår bara giltighetstidsinformationen, och vi får en tabell över vad databasen just nu tror om hur världen sett ut vid olika tillfällen:
397Nummer | Namn | Telefon | GällerFrån | GällerTill |
1 | Olle | 260088 | 24 maj | nu |
2 | Stina | 282677 | 2 juni | nu |
3 | Cecilia | 516323 | 1 mars | 16 mars |
1 | Olle | 174434 | 7 april | 24 maj |
Samma tabell med även giltighetstidsdimensionen bortfiltrerad. Då återstår bara aktuella data, dvs vad databasen just nu tror om hur världen ser ut just nu:
Nummer | Namn | Telefon |
1 | Olle | 260088 |
2 | Stina | 282677 |
Vi har sett att temporala tabeller kan byggas i en vanlig relationsdatabas genom att man lägger till kolumner med giltighets- eller transaktionstid. Men det finns flera möjligheter. Till exempel kan man dela upp den logiska tabellen i två, och lägga alla aktuella rader i en tabell och alla historiska rader i en annan.
Att separera aktuella och historiska data på det sättet har flera fördelar. Frågor som bara behandlar aktuella rader blir enklare, och kanske också snabbare, eftersom man slipper att samtidigt hantera alla de historiska raderna. Däremot blir ändringar lite krångligare, eftersom man måste flytta data mellan tabellerna. Om man har en aktiv databashanterare, kan triggers underlätta genom att kapsla in operationerna och automatisera en del av arbetet.
Det är ganska många saker som är blir krångliga i en temporal databas, om databashanteraren inte har inbyggt stöd för att hantera dem. Frågorna blir komplexa, eftersom de ska hänsyn till en eller kanske två tidsdimensioner. Integritetstvillkoren kan också bli både komplexa och besvärliga. Som exempel på det ska vi titta på hur referensintegritet fungerar mellan tabeller som innehåller information om giltighetstid.
398Tabellen Anställd innehåller data om anställda, och tabellen Avdelning innehåller data om avdelningar. De anställda jobbar på avdelningarna, och JobbarPå i Anställd är ett referensattribut till Nummer i Avdelning.
Anställd | ||||
Nummer | Namn | JobbarPå | GällerFrån | GällerTill |
1 | Svea | 1 | 12 april | 1 maj |
1 | Svea | 3 | 1 maj | 27 juni |
2 | Sten | 3 | 12 april | 20 juni |
3 | Bengt | 1 | 1 maj | nu |
Avdelning | |||
Nummer | Namn | GällerFrån | GällerTill |
1 | Data | 1 mars | nu |
2 | Städning | 1 mars | nu |
3 | Ekonomi | 3 april | 20 juni |
Vi kan utläsa ur tabellerna att ekonomiavdelningen bara fanns fram till 20 juni. Sen las den ner. (Eller brann upp eller vad som nu hände.) Men samtidigt ser vi att Svea arbetade på just ekonomiavdelningen fram till 27 juni. Under tiden mellan 20 juni och 27 juni arbetade Svea alltså på en avdelning som inte fanns. Även om alla värden i kolumnen JobbarPå finns med i kolumnen Nummer i tabellen Avdelning, är referensintegriteten bruten om man tar hänsyn till giltighetstiden.
I en databashanterare som låter oss formulera generella integritetsvillkor med check, antingen i create table eller i create assertion, går det i och för sig att formulera ett integritetsvillkor som hindrar den här sortens brott mot den temporala referensintegriteten, men det blir ganska krångligt. Och det är ändå något så förhållandevis enkelt som referensintegritet.
Snapshot-databas (engelska: snapshot database). En databas vars innehåll beskriver världens utseende vid en enda tidpunkt. En snapshot-databas som är en relationsdatabas innehåller tabeller som man kan kalla snapshot-tabeller.
Temporal databas (engelska: temporal database). En databas vars innehåll beskriver världens utseende vid olika tidpunkter, eller 399 som innehåller en historik för när uppgifterna i databasen ändrades, eller båda dessa. Informationen i databasen har alltså en eller flera tidsdimensioner. En temporal databas som är en relationsdatabas innehåller tabeller som man kan kalla temporala tabeller.
Tidsserie (engelska: time series). En serie av uppgifter som samlats in med med förutbestämda tidsintervall, och som man ofta vill beräkna någon form av tidsrelaterad statistik på. Exempel: Temperaturer som mäts och lagras varje timme, och där man vill beräkna medeltemperaturen per dygn.
Giltighetstid (engelska: valid time). En tidsdimension som beskriver världens utseende vid olika tidpunkter. En tabell som innehåller information om världens utseende vid olika tidpunkter kan kallas en giltighetstidstabell (engelska: valid time table eller valid time relation).
Transaktionstid (engelska: transaction time). En tidsdimension som beskriver databasens innehåll vid olika tidpunkter. En tabell som har kvar tidigare innehåll, tillsammans med information om när ändringar skedde, kan kallas en transaktionstidstabell (engelska: transaction time table eller transaction time relation).
Aktuell rad (engelska: current row version). I en giltighetstidstabell kan samma logiska rad finnas i flera versioner, som innehåller samma uppgifter men för olika tidsperioder. Den radversion som gäller nu kallas aktuell rad. Övriga raderversioner kallas historiska rader eller historiska radversioner (engelska: history row versions) eller stängda rader eller stängda radversioner (engelska: closed row versions). I en transaktionstidstabell kan samma logiska rad finnas i flera versioner, som innehåller tidigare versioner av raden. Även där talar man om aktuella och historiska (eller stängda) rader.
Icke-temporal nyckel (engelska: non-temporal key). I en temporal tabell kan samma logiska rad förekomma i flera versioner. Den vanliga nyckeln i tabellen, även kallad den icke-temporala nyckeln, är därför inte längre garanterat unik. Därför måste den kombineras med en eller flera kolumner som anger tidsinformation för de olika versionerna av den logiska raden.
Bi-temporal databas (engelska: bi-temporal database). En databas som har två tidsdimensioner, normalt giltighetstid och transaktionstid. Databasen innehåller alltså både uppgifter om världens utseende vid olika tidpunkter, och en historik för när uppgifterna i 400 databasen ändrades. En bitemporal databas som är en relationsdatabas innehåller tabeller som man kan kalla bitemporala tabeller (engelska: bi-temporal relations).
Temporal nyckel (engelska: temporal key). I en temporal tabell kan samma logiska rad förekomma i flera versioner. Den vanliga nyckeln i tabellen, även kallad den icke-temporala nyckeln, är därför inte längre garanterat unik. Därför måste den kombineras med en eller flera kolumner som anger tidsinformation för de olika versionerna av den logiska raden. Den sammansatta nyckeln kallas temporal nyckel.
Vi har tidigare sett hur man kan ställa frågor till en relationsdatabas med hjälp av SQL, och vi har då tänkt oss att man skriver in SQL-koden på tangentbordet, och sen kör databashanteraren frågan och skriver ut svaret. Även om experter och databasadministratörer ibland arbetar på det sättet, är det betydligt vanligare att SQL-frågorna är inbakade i ett program av något slag, eller i en webbsida. Den som ska arbeta med databasen kör det programmet, eller tittar på webbsidan, och matar kanske in sina data i ett grafiskt gränssnitt i stället för att skriva in SQL-frågorna direkt. Det är alltså programmeraren som skrev programmet eller webbsidan som har formulerat SQL-frågorna, i förväg. Användaren som kör programmet ser aldrig någon SQL.
402Det finns flera skäl till att man ibland vill baka in SQL i ett procedurellt språk:
Det finns i huvudsak fyra sätt att kommunicera med relationsdatabaser genom SQL:
Tillämpningsprogrammeringsgränssnitten, eller API:erna, kan se olika ut. Ett API kan bestå av ett bibliotek med procedurer, funktioner och datatyper som ett program kan använda för att kommunicera med databashanteraren. För objektorienterade programmeringsspråk är det oftast ett klassbibliotek. Oavsett hur det är gjort måste API:et uppfylla följande krav:
Kapitlet ger en översikt av de olika teknikerna, och en jämförelse mellan dem, men den som ska utveckla annat än enklare program enligt någon av de här metoderna måste förmodligen läsa vidare i produktspecifika manualer. Det finns också mycket information på webben.
De flesta databashanterare har ett eller flera API:er för att kunna köra SQL-satser från inuti ett program skrivet i ett vanligt programspråk som C, C++, C# eller Java, eller i ett skriptspråk som PHP, Perl, Python eller Pike. 1 Det finns nästan alltid ett API som kan användas från språket C, och ofta även API:er för flera andra språk.
Bland de enklaste sätten att prova på SQL inuti ett program är att använda databashanteraren SQLite i programspråket Python, och därför ska vi börja med det. (Man behöver inte kunna Python för att förstå exemplet.) När man installerar Python följer SQLite med, och man kan komma i gång direkt bara genom att starta Python-tolken och börja skriva kommandon. Databashanteraren behöver inte konfigureras eller startas separat.
I ett typiskt API mot en relationsdatabas är SQL inte särskilt väl integrerat med det språk, "värdspråket" (på engelska "host language"), som resten av programmet, "värdprogrammet", är skrivet i. SQL-satserna är tydligt skilda från värdspråket, och finns i textsträngar som via anrop till ett bibliotek skickas i väg någonstans för att köras. När en SQL-sats körts, och producerat ett resultat i 406 form av en tabell, är det resultatet inte direkt tillgängligt, utan programmet måste hämta det rad för rad. Det kan också vara ganska krångligt att flytta över resultatet till värdspråkets variabler.
Vi börjar med att ansluta till databasen, så vi startar Python och skriver:
import sqlite3
anslutningen = sqlite3.connect('testbas.db')
Raden med import är för att ge Python tillgång till modulen sqlite3, som innehåller SQLite-API:et. Funktionen som ansluter till databasen heter connect, och man skickar med namnet på den databas man vill arbeta med. SQLite är en mycket enkel databashanterare, och i API:er för andra, mer komplicerade, databashanterare kanske man behöver skicka med fler uppgifter, till exempel användarnamn och lösenord för att logga in. Variabeln anslutningen används för att lagra själva anslutningen, så att vi sen kan jobba vidare med den.
Vi kan koppla upp oss mot flera olika databaser, och flera olika databasservrar av samma typ, i ett och samma program. I och med att anslutningen till databasen lagras i en variabel, kan vi skilja de olika uppkopplingarna åt.
Efter anropet till connect bör man kontrollera om uppkopplingen misslyckades, och hantera eventuella fel på lämpligt sätt. Just uppkopplingen är ett av de ställen i programmet där fel lätt kan uppstå, till exempel till följd av nätverksproblem, att servern gått ner, eller att det blivit problem med användarens rättigheter till databasen. Det hoppar vi dock över här. Med SQLite är det dessutom ganska liten risk att uppkopplingen misslyckas, för SQLite har ingen separat server, inga användare med lösenord, och databasen lagras som en vanlig fil. Om det inte fanns någon fil med namnet testbas.db, kommer den att skapas.
När vi är anslutna till databashanteraren kan vi köra SQL-frågor. Resultatet returneras inte som en färdig datastruktur i form av en array eller lista, utan som ett dataobjekt av något slag som man sen kan hämta rader från. Metoden som SQLite3-API:et använder heter cursor, och en cursor är något som anger den aktuella raden i resultatet, så man kan hämta en rad i taget. (Det står mer om hur en cursor fungerar i avsnitt 21.9.) Man kan också se det som ett "frågeobjekt", som representerar en SQL-fråga och dess resultat.
cursorn = anslutningen.cursor()
407
cursorn.execute('SELECT Nummer, Namn, Telefon ' + 'FROM Personer')
Liksom uppkopplingen mot databasen är frågekörning ett ställe som det ofta uppstår fel på (särskilt under utvecklingen av programmet), till exempel för att man angett felaktiga tabell- och kolumnnamn. Därför bör man kontrollera att frågan gick bra att köra. Även det hoppar vi över här.
Lägg märke till att SQL-satsen inte är vanlig programkod som resten av programmet. I stället är den instoppad i en textsträng, och vad Python-programmet anbelangar är SQL-koden vilken text som helst. Python gör ingen kontroll av att SQL-koden är riktig, utan den kontrollen sker först när programmet körs, och textsträngen med SQL har skickats till databashanteraren.
Nu kan vi hämta resultatet från frågan, antingen med en metod som heter fetchall och som hämtar alla raderna i resultatet, eller med metoden fetchone som hämtar en rad i taget. Om vi placerar fetchone i en while-loop kan vi hämta rad efter rad, tills det inte finns fler rader att hämta:
raden = cursorn.fetchone()
while raden:
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprint("Nummer %d, namn %s, tel %s" %
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x(raden[0], raden[1], raden[2]))
raden = cursorn.fetchone()
(Det gör inget om läsaren inte genast förstår precis vad Pythonkoden gör. Det räcker att veta att vi hämtar en rad i taget med fetchone, tills fetchone inte returnerar fler rader, och för varje rad skriver programmet ut det nummer, namn och telefonnummer som finns med i SQL-frågans svar.)
Så här ser utskrifterna från programmet ut:
Nummer 17, namn Hjalmar, tel 174590
Nummer 4711, namn Hulda, tel 019-94639
Nummer 99, namn Sten, tel 08-222000
Som alternativ kan vi hämta alla raderna på en gång, och därefter loopa igenom dem:
alla_raderna = cursorn.fetchall()
for raden in alla_raderna:
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprint("Nummer %d, namn %s, tel %s" %
(raden[0], raden[1], raden[2]))
408
Resultat från SQL-frågor kan vara mycket stora, med kanske många miljarder rader. Om hela svaret finns lagrat, i minne eller på disk, talar man om att det är materialiserat. Det är viktigt att hålla reda på om hela svaret från SQL-frågan hämtas från servern och lagras i klientprogrammet, eller om servern bara skickar lite data i taget. Ett API som alltid hämtar hela resultatet skulle vara svårare att använda, för i så fall skulle man alltid behöva tänka på att skriva SQL-frågorna så att storleken på svaret begränsas. Om resultatet blir så stort att det inte får plats, fungerar inte programmet.
anslutningen.commit()
Till sist kopplar vi ner från databasen. Det görs automatiskt av skräpsamlaren när programmet avslutas, men i ett program som körs länge innan det avslutas, till exempel en webbserver, bör man koppla ner så här, så att inte anslutningen ligger kvar och binder resurser. Om vi har fler frågor att köra, vill vi förmodligen behålla anslutningen, och använda samma anslutning även för de andra frågorna, för det tar lång tid att göra en uppkoppling.
anslutningen.close()
Python är ett objektorienterat språk, och API:et för SQLite utgörs av ett klassbibliotek. Vi använder oss av objekt, till exempel objektet som lagras i variabeln anslutningen, och man kan anropa metoder i objektet, till exempel metoden close. Som alternativ kan man tänka sig funktioner som man får skicka med alla data till, som vi kommer att se i C-API:et för MySQL i avsnitt 21.4 nedan.
Som ett lite längre exempel visar vi ett fullständigt Python-program som letar reda på högsta chefen för en anställd i en chefshierarki. (Men det behövs bättre felhantering, med tydliga felmeddelanden, om programmet ska användas av slutanvändare.) Vi har en tabell Anställda, och i den finns en kolumn Chef som anger vem som är den anställdes närmaste chef:
409Anställda | |||
Nummer | Namn | Lön | Chef |
1 | Stina | 30 000 | null |
2 | Sam | 22 000 | 1 |
3 | Lotta | 28 000 | 1 |
4 | Ulrik | 26 000 | 3 |
5 | Petter | 22 000 | 3 |
6 | Hjalmar | 20 000 | null |
7 | Hulda | 19 000 | 6 |
De anställda utgör en hierarki. Vi kan se att anställd nummer 1, som heter Stina, verkar vara högsta chefen för de flesta i företaget. Hon har två underställda, Sam och Lotta, och Lotta har i sin tur underställda, och så vidare. Hjalmar och Hulda bildar en egen hierarki, separat från alla andra!
Att hitta högsta chefen, oavsett hur många nivåer upp hon är från den anställda vi börjar med, är ett bra exempel på SQL inuti ett program, eftersom det är svårt att ställa den frågan i vanlig SQL. SQL-standarden innehåller en mekanism med rekursiva frågor (se avsnitt 8.16) som kan användas, men den finns inte i alla SQL-dialekter.
# Hitta högsta chefen för en anställd import sqlite3
def main():
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprint("Detta program visar en anställds högsta chef.")
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xnummer = int(input("Ange ett anställningsnummer: "))
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xcon = sqlite3.connect('personal.db')
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xcur = con.cursor()
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x# Finns denna anställda alls i databasen?
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xcur.execute('select Namn, Chef from Anställda' +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x' where Nummer = ' + str(nummer))
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xraden = cur.fetchone() # Nummer är nyckel
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif not raden:
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprint('Det finns ingen anställd med nummer ' + str(nummer))
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xelse:
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhile raden:
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xnamn = raden[0]
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xchef = raden[1]
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif not chef:
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprint('Högsta chefen heter ' + namn)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xbreak
410
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xnummer = chef
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xcur.execute('select Namn, Chef' +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x' from Anställda' +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x' where Nummer = ' +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xstr(nummer))
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xraden = cur.fetchone()
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprint('Klart!')
main()
Precis som de flesta andra databashanterare har MySQL ett C-API, och vi ska nu titta på hur ett fullständigt C-program, som ska hämta data ur en MySQL-databas med SQL, kan se ut. (Olika databashanterare har sina alldeles egna API:er, men principiellt är de ganska lika.) Programmet ska göra samma sak som i exemplet ovan med Python-API:et till SQLite, nämligen hämta och skriva ut innehållet i tabellen Personer med de tre kolumnerna Nummer, Namn och Telefon.
C– program brukar vara lite omständligare än Python-program, och det här är inget undantag. MySQL är också en mer avancerad databashanterare än SQLite, med en separat server som man måste logga in i med användarnamn och lösenord. Dessutom blir programmet längre eftersom vi den här gången har med felhanteringen, som utgör en ganska stor del av programmet. (Så blir det ofta med riktiga program.)
Som vi skrev i förra avsnittet om SQLite är det viktigt om hela resultatet från en fråga skickas till klienten, och mellanlagras i väntan på att programmet vill hämta raderna, eller om raderna skickas i mindre grupper från servern. Det gäller även MySQL. I båda fallen får C-programmet tillgång till raderna en i taget med hjälp av anrop till mysql_fetch_row, men i det förra fallet hämtar mysql_fetch_row inte raderna direkt från servern, utan från en lokal buffert där alla raderna har mellanlagrats. I det här exemplet har vi valt att hämta och mellanlagra hela resultatet, och därför anropar vi funktionen mysql_store_result, som hämtar hela resultatet och lagrar det i klientprogrammets minne.
#include <stdlib.h>
411
#include <stdio.h>
#include "mysql.h"
int main(void) { MYSQL* c;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xMYSQL_RES* result;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xMYSQL_ROW row;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xc = mysql_init((MYSQL*)NULL);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif (c == NULL) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprintf("Kunde inte skapa anslutningsobjektet.\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_FAILURE;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif (mysql_real_connect(c, "localhost",
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"root", "SpGlk2Az",
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"min_databas", 0,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x(const char *)NULL, 0)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x== NULL) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprintf("Kunde inte ansluta till databasen.\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprintf("Felmeddelande: %s\n", mysql_error(c));
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_FAILURE;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif (mysql_query(c, "select Nummer, Namn, Telefon "
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"from Personer") != 0) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprintf("Frågan misslyckades.\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprintf("Felmeddelande: %s\n", mysql_error(c));
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_FAILURE;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif ((result = mysql_store_result(c)) == NULL){
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprintf("Kunde inte hämta resultatet.\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprintf("Felmeddelande: %s\n", mysql_error(c));
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_FAILURE;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprintf("Personer:\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhile ((row = mysql_fetch_row(result)) != NULL) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprintf("Nummer %s, med namnet %s"
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" och telefon %s.\n",
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xrow[0], row[1], row[2]);
412
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x} /* while rows in the result */
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xmysql_free_result(result);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xmysql_close(c);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_SUCCESS;
} /* main */
Så här ser utskrifterna från programmet ut:
Personer:
Nummer 17, med namnet Hjalmar och telefon 174590.
Nummer 4711, med namnet Hulda och telefon 019-94639.
Nummer 99, med namnet Sten och telefon 08-222000.
I en klient/server-lösning går det till så att tillämpningsprogrammet, som också kallas klientprogram, anropar funktionerna i API:ets funktionsbibliotek, och sen kommunicerar programkoden som finns inuti de funktionerna med databasservern. Databasservern är ett program som kan finnas på samma fysiska dator, eller på en annan, och i så fall sker kommunikationen via ett datornät. (Det finns även lösningar där hela databasservern ligger i samma process, alltså i samma körande program, som tillämpningsprogrammet.)
API:erna för SQLite och MySQL, som vi såg i de tidigare avsnitten, var knuntna till just de databashanterarna. Med den sortens databashanterarspecifika API:er har olika databashanterare helt olika API:er. I ett och samma program brukar det gå bra att ansluta sig till flera olika servrar av samma typ, till exempel flera olika MySQL-servrar, men om programmet behöver ansluta till servrar av olika typ måste man alltså använda två olika API:er i samma tillämpningsprogram. Det kan ibland vara lite krångligt att få det att fungera, förutom förstås dubbelarbetet att lära sig två olika API:er. Om man skrivit ett tillämpningsprogram som ansluter till en databashanterare av en viss typ och vill byta till en annan, måste man skriva om programmet.
Som alternativ finns flera olika databasoberoende API:er, dvs API:er som är standardiserade och som kan användas med flera olika typer av databashanterare. Ett av dem är JDBC, en förkortning som står för Java DataBase Connectivity. JDBC är ett API för att använda 413 SQL för att arbeta med en databas från ett Java-program. Samma Java-program ska, kanske helt utan förändringar, kunna användas tillsammans med olika databashanterare.
Namnet JDBC är inspirerat av ODBC, Open DataBase Connectivity, som är en äldre standard som huvudsakligen används med värdprogram skrivna i C (se avsnitt 21.7). Den första implementeringen av JDBC gjordes ovanpå ODBC. Java brukar vara enklare att använda än C (men inte lika enkelt som Python), och JDBC är enklare än ODBC.
Databashanterare är olika, och databasservrar av olika typ kommunicerar på olika sätt med sina klientprogram. För att ett JDBC-program ska kunna koppla upp sig mot en viss databashanterare måste tillverkaren av databashanteraren därför skapa en så kallad drivrutin (på engelska driver) som förstår just den databashanterarens kommunikationsprotokoll och andra egenheter. Tillämpningsprogrammet använder JDBC-API:et, men man så att säga "pluggar in", eller laddar, rätt drivrutin i JDBC-ramverket, och sen går kommunikationen med databashanteraren genom den drivrutinen.
Varje databashanterare som ska kunna användas med JDBC behöver en egen drivrutin. Om man byter drivrutin, kan samma Javaprogram använda en annan databashanterare. I idealfallet behöver man inte göra några ändringar alls i Java-programmet, förutom att byta namnet på den drivrutin som ska laddas in. (Det är inte alltid det är så enkelt. Olika databashanterare har olika egenheter, även om JDBC-API:et försöker dölja dem.) Om ett program behöver arbeta med flera olika typer av databashanterare, laddar man helt enkelt in flera olika JDBC-drivrutiner samtidigt.
Här är ett JDBC-program som, i likhet med exempelprogrammen tidigare i kapitlet, hämtar och skriver ut innehållet i tabellen Personer. I det här exemplet använder vi ännu en databashanterare, nämligen Mimer, men det finns JDBC-drivrutiner för nästan alla 414 databashanterare. Om det inte finns en JDBC-drivrutin, men en ODBC-drivrutin, kan man ändå använda JDBC med hjälp av en brygga till ODBC.
Anropet till Class.forName laddar in drivrutinen, och man ser på namnet com.mimer.jdbc.Driver att vi använder en Mimer-databas. Drivrutinen kan komma som en .jar-fil, och den filen måste läggas in i omgivningsvariabeln CLASSPATH för att Java ska hitta den.
Strängen i variabeln url innehåller namnet på serverdatorn, namnet på den databas på den servern som vi vill använda, användarnamn och lösenord. Detta är en URL, Uniform Resource Locator, som vi också använder för webbadresser, men då börjar de medhttp://
eller https://.
Anropet till DriverManager.getConnection skapar anslutningen till databasservern. Den kopplar upp sig mot databasen via nätverket och loggar in med det angivna användarnamnet och lösenordet. DriverManager är den Java-klass som håller reda på drivrutinerna och uppkopplingarna.
Anropet till metoden createStatement skapar ett Statement-objekt, som används för att representera SQL-frågan. Metoden executeQuery kör frågan, genom att skicka den till databashanteraren, och returnerar svaret i form av ett ResultSet-objekt. Det objektet representerar resultatet från frågan, dvs en (ordnad) mängd av rader. Vi går igenom raderna i svaret med en loop som använder metoden next för att kontrollera om det finns fler rader kvar att hämta, och i så fall stega fram till nästa rad. Metoden getString hämtar vär-det på den angivna kolumnen, dvs ett enskilt värde i svaret, på den aktuella raden, i form av en sträng. När det gäller kolumnen Nummer, som är ett heltal, kunde vi i stället ha hämtat den i form av ett heltal med metoden getInt.
import java.sql.*;
class JDBCTest {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xpublic static void main(String[] args) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xtry {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xClass.forName("com.mimer.jdbc.Driver");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xcatch (java.lang.ClassNotFoundException cnf) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSystem.err.println("Hittade inte " +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"JDBC-drivrutinen.");
415
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xString url = "jdbc:mimer://tompa:SpGlk2Az@basen.oru.se/" +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"min_databas";
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xConnection con; try {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xcon = DriverManager.getConnection(url);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xcatch (java.sql.SQLException sqe) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSystem.err.println("Kunde inte ansluta " +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"till databasen.");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSystem.out.println("Personer:");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xtry {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xString query = "select Nummer, Namn, Telefon " +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"from Personer";
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xStatement stmt = con.createStatement();
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xResultSet rs = stmt.executeQuery(query);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhile (rs.next()) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSystem.out.println("Nummer " +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xrs.getString("Nummer") + ",
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xmed namnet " +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xrs.getString("Namn") +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" och telefonnummer " +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xrs.getString("Telefon") +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x".");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xrs.close();
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xstmt.close();
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xcatch (java.sql.SQLException sqe) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSystem.err.println("Ett JDBC-problem uppstod.");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xtry {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xcon.close();
416
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xcatch (java.sql.SQLException e) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSystem.err.println("Kunde inte koppla ner.");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
} // class JDBCTest
Om resultatet från frågan är stort, kommer JDBC inte att hämta alltihop från servern på en gång, utan lite i taget. JDBC-drivrutinen väljer själv ett lämpligt antal rader, men det finns en särskild metod, setFetchSize, som man kan anropa för att föreslå vad man tycker kan vara ett lämpligt antal rader att hämta åt gången.
I anropet till executeQuery skickar man med SQL-frågan som en textsträng. Den textsträngen måste analyseras av databashanteraren, tolkas som en fråga, och optimeras, dvs översättas till en exekveringsplan. Processen att analysera och optimera en SQL-fråga kallas att förbereda eller preparera frågan. Ibland kallar man hela processen för att kompilera frågan. Detta kan vara mycket tidskrävande, och kan ibland ta mycket längre tid än att sen faktiskt köra frågan.
Om man ska köra samma fråga många gånger, till exempel inuti en loop, vill man undvika att upprepa arbetet med att förbereda frågan. I stället för att skapa ett Statement-objekt kan man skapa ett PreparedStatement-objekt. Det gör man med metoden prepareStatement, med SQL-frågan i en textsträng som parameter. Då sker analysen och optimeringen.
Därefter kan man anropa executeQuery i PreparedStatementobjekt. Till skillnad från executeQuery i Statement-objekt ska man inte skicka med någon textsträng, utan SQL-frågan är redan klar att köras.
Det här är de ändrade rader som behövs i exemplet med Statement från förra stycket:
PreparedStatement stmt = con.prepareStatement(query);
ResultSet rs = stmt.executeQuery();
I övrigt ser programkoden likadan ut. (Det fullständiga programmet finns på bokens webbplats.)
I JDBC-exemplet ovan överfördes data från databasen till programmet. Medlemmarnas nummer, namn och telefon hämtades från databasen med en SQL-fråga, och omvandlades därefter till vanliga Javasträngar med anrop till metoden getString, så att de kunde skrivas ut med Javas println. Till skillnad från SQL-frågan i det exemplet innehåller de flesta SQL-frågor konstanter av något slag. Till exempel är man sällan intresserad av alla personer, utan bara en viss person, eller de personer som uppfyller ett visst villkor. Då måste man ange dessa villkor i frågan, till exempel nummer och namn på de personer som söks.
SQL-frågan skrivs, som vi sett, som en vanlig Java-sträng. Man kan bygga upp en ny sträng när programmet körs, med de konstanter man är intresserad av just den gången:
int wantedNumber = ...
String wantedName = ...
String query = "select Nummer, Namn, Telefon" +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" from Personer"
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" where Nummer = " + wantedNumber +
" or Namn = '" + wantedName + "'";
Om konstanterna varierar mellan de tillfällen när frågan ska köras, är det inte längre samma fråga, och den måste förberedas (dvs analyseras och optimeras) på nytt. Vi kan också råka ut för SQL-injektion (se avsnitt 14.7). Därför är det bättre att parametrisera frågan. Det går till så att man ersätter de värden som kan variera med frågetecken:
String query = "select Nummer, Namn, Telefon" +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" from Personer" +
" where Nummer = ? or Namn = ?";
Nu är frågan förberedd och klar att köras, så snart man angett vilka värden man ska söka efter den här gången. Det gör man med särskilda "set"-metoder:
tmt.setInt(1, wantedNumber);
stmt.setString(2, wantedName);
418
Ettan och tvåan i anropen står för första respektive andra parametern i frågan.
Därefter är det bara att köra frågan som vanligt med executeQuery. Man kan köra den gång på gång, med nya värden att söka efter som man anger med setInt och setString.
Nu har vi sett flera olika API:er som tillåter att man bakar in SQL-kommandon inuti ett program skrivet i ett vanligt programspråk som C eller Java. I samtliga dessa är SQL-koden och det omgivande värdspråket ganska dåligt integrerade. Det är krångligt att överföra data från värdspråkets variabler till SQL-kommandon, och det är krångligt att överföra data från ett SQL-resultat till värdspråkets variabler.
Det finns också mer integrerade språk, där överföringen av data är lättare. Microsoft SQL Server (se kapitel 31) har en SQL-dialekt kallad Transact-SQL eller T-SQL, och den innehåller konstruktioner som liknar vanliga programspråk, till exempel if-satser, whileloopar och print-satser. Här är ett script i Transact-SQL som hämtar och skriver ut innehållet i tabellen Personer:
DECLARE wantedNumber INTEGER;
DECLARE wantedName NVARCHAR(10);
SET wantedNumber = 4711;
SET wantedName = 'Hjalmar';
DECLARE &personCursor as CURSOR;
SET &personCursor = CURSOR FOR
SELECT Nummer, Namn, Telefon
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xFROM Personer
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xWHERE Namn = &wantedName
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xOR Nummer = &wantedNumber;
OPEN &personCursor;
PRINT 'Personer som matchar sökkriterierna:'
419
FETCH NEXT FROM &personCursor
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xINTO &number, &name, &telephone;
WHILE &&FETCH_STATUS = 0
BEGIN
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xPRINT 'Nummer ' + cast(&number as VARCHAR(50)) +
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x', namn ' + &name + ', telefon ' + &telephone;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xFETCH NEXT FROM &personCursor
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xINTO &number, &name, &telephone;
END
CLOSE &personCursor;
DEALLOCATE &personCursor;
Till skillnad från de tidigare exemplen är det här inte ett värdspråk där man använder värdspråkets vanliga mekanismer, exempelvis strängar och funktionsanrop, för att baka in SQL, utan här är det snarare så att man utgått från SQL och utvidgat det med vanliga programspråkskonstruktioner. Variabler från skriptet, som wantedNumber, kan enkelt användas i SELECT-satsen, och resultatet från SELECT-frågan, som kolumnen Nummer, kan ganska enkelt överföras till skriptets variabler. Man ser skillnad på skriptets variabler och kolumnnamn i databasen genom att namnen på skriptets variabler börjar med &.
Skriptet är inte en lagrad procedur. Det körs av databashanteraren, men det finns inte lagrat i databasen så att det bara är att anropa det. Varje gång det ska köras måste det skickas till databashanteraren.
JDBC, som beskrevs i avsnitt 21.5 ovan, är en standard för att kommunicera med olika relationsdatabashanterare inifrån ett program skrivet i Java. Före JDBC fanns ODBC, som betyder Open DataBase Connectivity. ODBC är också en standard för att kommunicera med olika relationsdatabashanterare, men från C-program. ODBC är visserligen tänkt att kunna användas med olika värdspråk, men används huvudsakligen med C. ODBC kommer ursprungligen från Microsoft, men har blivit mycket spritt i databasvärlden. ODBC fungerar inte bara med Windows, utan man kan använda sig av ODBC 420 även med andra operativsystem, till exempel i ett program som körs under Linux. Nästan alla relationsdatabashanterare har en implementation av ODBC, så det går att kommunicera med dem med hjälp av ODBC.
ODBC ingår (nästan) i SQL-standarden. Från och med SQL:1999 innehåller standarden något som kallas CLI eller SQL/CLI, och som är en nästan exakt kopia av ODBC. CLI står för Call-Level Interface, "Anropsnivågränssnitt", och med det menar man ett gränssnitt som består av funktioner eller procedurer, och där interaktionen genom gränssnittet sker genom funktionsanrop. Även om CLI alltså ingår i SQL-standarden, brukar det vara "riktig" ODBC som används.
ODBC är vid det här laget ganska gammalt, och även om det fortfarande används i många tillämpningar är det inte längre så vanligt att använda ODBC i nya projekt. Det används ibland för att kommunicera med databaser som är inbyggda i program, till exempel med SQLite, och där databasen inte är åtkomlig med vanliga verktyg. ODBC ingår ibland också som en dold del i andra system, till exempel för att kunna använda JDBC via en brygga till ODBC, om det inte finns en särskild JDBC-drivrutin (se sidan 414).
Med ODBC betraktas en databas som en så kallad datakälla, på engelska data source, och meningen är att alla datakällor ska se likadana ut för den programmerare som skriver ett tillämpningsprogram, oberoende av vilken databashanterare som hanterar just den databasen. Till skillnad från i JDBC hanteras datakällorna centralt på den dator man kör, och i kontrollpanelen i Windows hittar man en särskild hanterare för datakällkorna.
ODBC specificerar hur SQL-frågorna får se ut. Den SQL-dialekt som specificeras av ODBC-standarden är omfattande, men det är inte nödvändigt att varje typ av datakälla implementerar allt. Om en viss databashanterare har en SQL-dialekt som inte stämmer överens med ODBC-standarden, kan SQL-frågorna konverteras av ODBC innan de skickas i väg till databashanteraren.
Ett program som använder ODBC påminner mycket om ett som använder ett databaseget C-API, till exempel MySQL:s. Man skickar SQL-frågor inuti strängar i funktionsanrop på liknande sätt, och man hämtar en rad i taget. Men ODBC är alltså standardiserat, så samma program kan kommunicera med datakällor av flera olika typer, och man kan byta typ av datakälla utan att skriva om programmet.
Funktionen SQLAllocHandle används för att allokera resurser, till exempel det objekt som ska hålla reda på anslutningen till databasen. 2 SQLConnect kopplar upp sig mot en datakälla, och SQLExecDirect kör en SQL-fråga. SQLFetch hämtar en rad i resultatet, och SQLGetData överför data från en av kolumnerna på en hämtad resultatrad till C-programmets variabler. Funktionen SQLDisconnect kopplar ner anslutningen till datakällan. Funktionerna SQLFreeEnv, SQLFreeConnect och SQLFreeStmt lämnar tillbaka de resurser som allokerats av programmet.
Så här kan man tänka sig de olika allokerade resurserna i ODBC-programmet. (Därmed inte sagt att det fysiskt ser ut exakt så här.) Omgivningen, som allokeras med SQLAllocEnv, innehåller alla andra allokerade resurser, till exempel anslutningen till databasen ("databaskopplet"), som allokerats med SQLAllocConnect. De allokerade resurserna kallas "handtag" ("handles"), för de är ju ett sätt att "ta tag" i sakerna när man ska göra något med dem. Variablerna, till exempel ch, är pekare till handtagen.
422Så här ser själva programmet ut:
#include <stdlib.h>
#include <stdio.h>
#if defined(_MSDOS) || defined(_WIN32)
#include <windows.h>
#endif
#include "sqlext.h"
int main(void) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQLHENV eh; /* Environment handle */
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQLHDBC ch; /* Connection handle */
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQLHSTMT sh; /* Statement handle */
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif (SQLAllocHandle(SQL_HANDLE_ENV,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQL_NULL_HANDLE, &eh)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x!= SQL_SUCCESS) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfprintf(stderr, "Kunde inte allokera "
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"en ODBC-omgivning.\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_FAILURE;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif (SQLSetEnvAttr(eh,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQL_ATTR_ODBC_VERSION,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x(SQLPOINTER)SQL_OV_ODBC3,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQL_IS_INTEGER) != SQL_SUCCESS) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfprintf(stderr,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"Kunde inte sätta ODBC-versionen.\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_FAILURE;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
423
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif (SQLAllocHandle(SQL_HANDLE_DBC, eh, &ch)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x!= SQL_SUCCESS) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfprintf(stderr, "Kunde inte allokera "
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"ett anslutningsobjekt.\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_FAILURE;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif (SQLConnect(ch, (SQLCHAR*)"Persondatabasen",
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQL_NTS,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x(SQLCHAR*)"root", SQL_NTS,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x(SQLCHAR*)"SpGlk2Az", SQL_NTS)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x!= SQL_SUCCESS) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfprintf(stderr, "Kunde inte ansluta " "till datakällan.\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_FAILURE;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x/* Allocate statement handle */
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif (SQLAllocHandle(SQL_HANDLE_STMT, ch, &sh)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x!= SQL_SUCCESS) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfprintf(stderr, "Kunde inte allokera "
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"ett statement handle.\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_FAILURE;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif (SQLExecDirect(sh, (SQLCHAR*)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"select Nummer, Namn, Telefon "
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"from Personer", SQL_NTS)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x!= SQL_SUCCESS) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfprintf(stderr, "Kunde inte köra frågan.\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_FAILURE;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprintf("Personer:\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhile (SQLFetch(sh) == SQL_SUCCESS) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQLINTEGER number;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQLCHAR name[10 + 1]; SQLCHAR phone[10 + 1];
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQLINTEGER number_size, name_size, phone_size;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQLGetData(sh, 1, SQL_C_SLONG, &number,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xsizeof number, &number_size);
424
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQLGetData(sh, 2, SQL_C_CHAR, name,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xsizeof name, &name_size); SQLGetData(sh, 3, SQL_C_CHAR, phone,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xsizeof phone, &phone_size);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprintf("Nummer %d, med namnet %s "
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" och telefon %s.\n",
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x(int)number, name, phone);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x} /* while rows in the result */
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQLFreeHandle(SQL_HANDLE_STMT, sh);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQLDisconnect(ch);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQLFreeHandle(SQL_HANDLE_DBC, ch);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQLFreeHandle(SQL_HANDLE_ENV, eh);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_SUCCESS;
} /* main */
SQLExecDirect, som vi använde i exempelprogrammet ovan, tar en SQL-fråga i form av en textsträng, översätter den till ett internformat, optimerar den, och kör den. Att förbereda en SQL-fråga, dvs analysera och optimera den, kan ta lång tid, och om man ska köra samma fråga många gånger, till exempel inuti en loop, vill man undvika att upprepa arbetet med att förbereda frågan. Därför innehåller ODBC de två funktionerna SQLPrepare och SQLExecute, som förbereder respektive kör frågan. En fråga som föreberetts med ett anrop till SQLPrepare kan sedan köras många gånger genom anrop till SQLExecute.
Användningen av SQLExecDirect i exempelprogrammet kan ersättas med den här programkoden:
if (SQLPrepare(sh, (SQLCHAR*)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"select Nummer, Namn, Telefon "
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"from Personer", SQL_NTS)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x!= SQL_SUCCESS) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfprintf(stderr, "SQLPrepare misslyckades.\n"); return EXIT_FAILURE;
}
if (SQLExecute(sh) != SQL_SUCCESS) {
425
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfprintf(stderr, "Kunde inte köra frågan.\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_FAILURE;
}
I exemplet ovan med SQLPrepare och SQLExecute körde vi en SQL-fråga för att få nummer, namn och telefon till alla personerna i persontabellen. De flesta SQL-frågor innehåller dock konstanter av något slag. Till exempel är man sällan intresserad av alla personer, utan bara en viss person, eller de personer som uppfyller ett visst villkor. Då måste man ange dessa villkor i frågan, till exempel nummer och namn på de personer som söks.
Om frågan varierar mellan de tillfällen när den ska köras, är det inte längre samma fråga, och den måste förberedas på nytt. Men för att slippa detta kan man, på liknande sätt som det vi såg i avsnitt 21.5.4 om JDBC, parametrisera frågan, och därigenom ändå förbereda den med SQLPrepare. Det fungerar så att man markerar parametrarna i SQL-frågan med frågetecken, och med hjälp av funktionen SQLBindParameter binder dessa parametrar till variabler i C-programmet.
Om vi antar att de två variablerna name och number innehåller ett namn och ett nummer, kommer följande programkod att köra en SQL-fråga som tar fram alla personer som har detta namn eller detta nummer. Ettan och tvåan i anropen till SQLBindParameter står för första respektive andra parametern i frågan.
SQLBindParameter(sh, 1, SQL_PARAM_INPUT,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQL_C_CHAR, SQL_CHAR,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x20, 0, name,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xsizeof name, &name_size);
SQLBindParameter(sh, 2, SQL_PARAM_INPUT,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQL_C_SLONG, SQL_INTEGER,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x0, 0, &number,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xsizeof number, &number_size);
name_size = SQL_NTS;
number_size = sizeof number;
if (SQLPrepare(sh, (SQLCHAR*)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x"select Nummer, Namn, Telefon "
426
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" from Personer "
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" where Namn = ? or Nummer = ?",
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xSQL_NTS)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x!= SQL_SUCCESS) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfprintf(stderr, "SQLPrepare misslyckades.\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_FAILURE;
}
if (SQLExecute(sh) != SQL_SUCCESS) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfprintf(stderr, "Kunde inte köra frågan.\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_FAILURE;
}
På bokens webbplats finns fler exempel på ODBC-program, där bland annat dessa funktioner visas.
Databashanterare är olika, och databasservrar av olika typ kommunicerar på olika sätt med sina klientprogram. För att ett ODBC-program ska kunna koppla upp sig mot en viss databashanterare måste tillverkaren av databashanteraren därför skapa en så kallad drivrutin (på engelska driver) som förstår just den databashanterarens kommunikationsprotokoll och andra egenheter. Tillämpningsprogrammet använder ODBC-API:et, men man så att säga "pluggar in", eller laddar, rätt drivrutin i ODBC-ramverket, och sen går kommunikationen med databashanteraren genom den drivrutinen.
Varje databashanterare som ska kunna användas med ODBC behöver en egen drivrutin. Om man byter drivrutin, kan samma tillämningsprogram 427 använda en annan databashanterare. Om ett program behöver arbeta med flera olika typer av databashanterare, laddar man helt enkelt flera olika ODBC-drivrutiner samtidigt.
Om man tittar lite i programkoden till ODBC-programmet i exemplet, ser man att det ska arbeta med en datakälla som kallas Persondatabasen. Men det säger inget om var den finns, eller vad det är för sorts databas. Det kan vara en Access-databas som finns i form av en fil på samma dator, det kan vara en databas som administreras av en MySQL-server på en annan maskin, eller något annat. Innan vi kan köra ODBC-programmet i exemplet måste vi på något sätt tala om vad namnet Persondatabasen står för. Det fungerar olika i olika operativsystem, men eftersom ODBC ursprungligen kommer från Microsoft finns det särskilt bra stöd för ODBC i Windows, och det är lätt att skapa datakällor. Datakällor administreras centralt i operativsystemet, så när man skapat en datakälla på en dator så kan alla program och alla användare använda sig av den. (Men som vi strax ska se finns det systemdatakällor och användardatakällor, där användardatakällor bara är tillgängliga för den användare som skapade den.)
Exakt hur man gör varierar en aning beroende på vilken version av Windows man använder. I en svenskspråkig Windows 10 börjar man med att öppna kontrollpanelen genom att högerklicka på Windows-symbolen nere till vänster och välja Kontrollpanelen i menyn. Därefter är det enklast att skriva "ODBC" i sökfältet, och då får man välja mellan Konfigurera ODBC-datakällor (32-bitars) och Konfigurera ODBC-datakällor (64-bitars). Oftast använder man man 64-bitars datakällor, men vissa ODBC-drivrutiner finns bara i 32-bitarsversion. När man valt ett av alternativen kommer det upp ett fönster där man kan administrera sina datakällor och ODBC-drivrutiner:
428Som synes finns det ännu ingen systemdatakälla som heter Persondatabasen. Vi skapar den genom att klicka på knappen Lägg till (eller Add, i ett engelskspråkigt Windows). Då får vi först välja vilken drivrutin som ska användas. Om drivrutinen inte finns måste den först installeras. Vi antar nu att vår databas är skapad med Microsoft SQL Server, och därför väljer vi rätt drivrutin för den:
I ett eller flera följande fönster får vi nu mata in bland annat namnet på datakällan ("Persondatabasen"), och olika uppgifter som behövs för att ansluta till databasen. För en databas som hanteras av en databasserver, enligt den vanliga klient/server-modellen, anger vi vad servern heter, och hur man autentiserar sig mot den. För en Microsoft Access-databas, som inte har en server utan bara är lagrad som en fil, får vi en vanlig filväljardialog där vi kan ange var databasfilen finns.
Om vi har flyttat databasen från Microsoft SQL Server till en annan databashanterare, till exempel MySQL, behöver vi inte röra något av programmen. Det enda vi behöver göra är att gå till datakälleadministratören i kontrollpanelen (eller dess motsvarighet, om vi kör något annat operativsystem än Windows) och ändra där, så att datakällan Persondatabasen nu använder sig av en MySQL-drivrutin och kopplar upp sig mot rätt server. Nästa gång ett program på den här datorn anropar SQLConnect i ODBC-API:et, kommer det att kopplas till den nya MySQL-databasen i stället för den gamla SQL Server-databasen. I övrigt fungerar allting precis som förut.
Tillämpningsprogrammet anropar ODBC-funktionerna, dvs SQLConnect med flera, men inte direkt i drivrutinen, utan i ett mellanskikt som kallas för drivrutinhanteraren (på engelska driver manager). Drivrutinhanteraren hanterar förstås drivrutinerna (det hörs ju på namnet), men dessutom tar den emot funktionsanropen från tillämpningsprogrammet. Såväl drivrutinhanteraren som de olika drivrutinerna är funktionsbibliotek, men de flesta funktionerna i drivrutinhanteraren gör inte så mycket när de anropas, utan de anropar bara i sin tur motsvarande funktion i drivrutinen. Drivrutinhanteraren kan länkas ihop statiskt med tillämpningsprogrammet, medan drivrutinerna måste länkas dynamiskt för att man ska kunna växla mellan dem utan statisk omlänkning av programmet.
Drivrutinen sköter kommunikationen med databashanteraren. Den kopplar upp sig, den kanske skriver om SQL-frågorna från ODBC-anropet till en SQL-syntax som passar just den här databashanteraren, den skickar SQL-frågorna till databashanteraren, och den tar emot svaret.
• Det finns databaser som inte hanteras av en ständigt körande databasserver, utan som bara finns i form av en fil på datorns hårddisk eller SSD. Det gäller till exempel Microsoft Access. Med den typen av databaser får ODBC-drivrutinen själv öppna filen, läsa och skriva data från den, och sköta all SQL-hantering själv. Det kallas för en enskiktsdrivrutin (på engelska one-tier driver), eftersom kommunikationen med själva databasen sker i ett enda skikt, nämligen drivrutinen.
Det innebär förstås att databasfilen måste finnas direkt tillgänglig på den dator som ODBC-programmet körs på, antingen på en lokal disk eller på en nätverksmonterad disk. 3
Det finns ODBC-drivrutiner inte bara för relationsdatabaser. Till exempel finns det drivrutiner för textfiler och Excel-filer, så att man kan använda dem som datakällor.
Bilden nedan visar ODBC-arkitekturen. Vi tänker oss att vi har tre olika tillämpningsprogram, som arbetar med fyra olika datakällor: en Oracle-databas, en Microsoft Access-databas, och två Microsoft SQL Server-databaser. (Flera tillämpningsprogram kan också arbeta med samma datakälla.) Programmet i mitten arbetar med två datakällor på en gång, till exempel för att kopiera data från en databas till en annan. Programmen använder alltid ODBC-gränssnittet, som det implementeras av drivrutinhanteraren, men sen har vi pluggat in olika drivrutiner beroende på vilken typ av databas som datakällorna utgör. Drivrutinerna för Oracle och SQL Server är av tvåskiktstyp, medan Access-drivrutinen är av enskiktstyp.
432Vi har sett exempel på API:er för språken Python, C och Java, Nästan alla databashanterare har ett API som fungerar med språket C (och samma API fungerar oftast även med C++), men det brukar också finnas API:er för flera andra språk. Till exempel finns det MySQL-API:er för (åtminstone) språken C++, Java, Tcl, Eiffel, PHP, Perl, Python och Pike. Vi visar inga exempel här, men på sidan 372 i kapitel 19 om webbdatabaser finns exempel på SQL-anrop från PHP-programkod, och på sidan 368 i samma kapitel finns ett så kallat CGI-skript med SQL-anrop från språket Pike.
Ett viktigt begrepp när man använder SQL inuti ett program är cursor, eller "markör". Vi har ju sett hur man skickar en SQL-fråga till databashanteraren, till exempel med MySQL-API:ets mysql_query 433 eller ODBC:s SQLExecDirect, och sen hämtar en rad i taget från resultatet. En cursor anger den aktuella raden i resultatet.
Det påminner om hur man arbetar med filer i ett vanligt programspråk: först öppnar man filen, sen läser man en post eller en rad i taget, man kan hoppa framåt och bakåt (i språket C med funktionen fseek) och sen stänger man filen. En cursor motsvarar alltså en öppen fil, men i stället för de data som finns på filen är det raderna i resultatet från en SQL-fråga som man arbetar med.
I alla teknikerna ovan, där man försöker kombinera ett vanligt programspråk med SQL, är SQL-koden och det omgivande värdspråket ganska dåligt integrerade. Bland annat är det krångligt att överföra data från ett SQL-resultat till värdspråkets variabler. Titta till exempel på ODBC, där det kan behövas ganska många anrop till SQLGetData med flera funktioner i API:et för att C-programmet över huvud taget ska få tillgång till resultatet av en SQL-fråga.
434Embedded SQL, eller ESQL, är ett försök att integrera SQL och C 4 på ett bättre sätt. (Embedded betyder "inbäddad", "innesluten" eller "inkapslad" på svenska, och syftar förstås på att SQL-koden är inbäddad i C-koden. Eftersom Embedded SQL är ett namn, är det sällan någon använder något annat än den engelska termen.) ESQL är en gammal teknik, mycket äldre än till exempel ODBC, och ESQL är numera mest av historiskt intresse. Vi tar ändå upp det här som ett exempel på ett mer integrerat språk, och för att visa hur det kan fungera internt.
Eftersom ESQL är en så gammal standard, kan det hända att vissa ESQL-verktyg inte fungerar tillsammans med C++, och i så fall måste man skriva programmet, eller i alla fall de delar av det som ska använda ESQL, i ren C. 5
Principen bakom ESQL är att man skriver SQL-koden mitt bland den vanliga C-koden, men markerad med exec sql. En SQL-sats för att ta bort alla anställda med namnet Bengt ser alltså ut så här när man stoppar in den i C-programmet:
exec sql delete from Anstallda where Namn = 'Bengt';
Det där förstår ju inte C-kompilatorn, så innan man kan kompilera programmet körs det genom en preprocessor. Preprocessorn översätter exec sql-raderna till funktionsanrop som påminner om ODBC eller ett databaseget C-API. Avancerade ESQL-implementationer förbereder redan här frågan, och lagrar den färdigoptimerade frågan i databasen. Sen måste det översatta, "preprocessade", programmet kompileras med en vanlig C-kompilator, och länkas med det bibliotek där de anropade funktionerna finns.
Man måste ange vilka C-variabler som ska användas av ESQL-delarna. De variablerna deklareras på vanligt C-sätt, men inuti en deklarationssektion som markeras med särskilda exec sql-satser. Om man till exempel vill ha en heltalsvariabel som heter avdelningsnummer, 435 som ska användas för att hämta heltal från eller skicka heltal till databasen, skriver man så här:
exec sql begin declare section;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xint avdelningsnummer;
exec sql end declare section;
Nu kan man referera till den variabeln, till exempel i ett SQL-kommando:
exec sql delete from Anstallda
where Avdelning = :avdelningsnummer;
Kolonet inuti SQL-koden betyder att avdelningsnummer är en variabel som finns i värdspråket, till skillnad från Avdelning som är en kolumn i databasen. När SQL-kommandot ska köra hämtas innehållet i den variabeln och stoppas in i SQL-kommandot.
Låt oss nu titta på ett kort, men fullständigt, ESQL-program. Programmet gör samma sak som vi redan sett flera exempel på, nämligen att hämta och skriva ut innehållet i tabellen Personer.
#include <stdio.h>
#include <stdlib.h>
exec sql begin declare section;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xstatic char sqlstate[6];
exec sql end declare section;
int main(void) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xexec sql begin declare section;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xint nummer;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvarchar namn[11];
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xvarchar telefon[11];
exec sql end declare section;
exec sql whenever sqlerror goto error_exit;
exec sql connect to 'min_databas'
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xuser 'ROOT' using 'SpGlk2Az';
exec sql declare person_cursor cursor for
436
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect nummer, namn, telefon
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom personer;
exec sql open person_cursor;
printf("Personer:\n");
while (1) {
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xexec sql fetch person_cursor
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xinto :nummer, :namn, :telefon;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif (strcmp(sqlstate, "02000") == 0)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xbreak; /* Slut på rader */
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xprintf("Nummer %d, med namnet %s "
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x" och telefon %s.\n",
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xnummer, namn, telefon);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_SUCCESS;
error_exit:
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfprintf(stderr, "Det har uppstått ett fel.\n");
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xreturn EXIT_FAILURE;
}
För att köra en SQL-fråga börjar man med att deklarera en cursor. Det görs med "exec sql declare ...", och där talar man också om hur SQL-frågan ser ut. Sen öppnar man denna cursor, med "exec sql open ...", och hämtar (som vanligt i såna här sammanhang) en rad i taget, med "exec sql fetch ...". Man slipper krångla med motsvarigheter till ODBC:s SqlGetData, eftersom man skriver direkt i fetch-satsen var de data som SQL-frågan hämtar ur databasen ska placeras.
Variabeln sqlstate innehåller en felkod från den senaste operationen. Koden 02000 betyder att det var slut på rader att hämta.
Låt oss också titta på vad den där preprocessorn egentligen gör med programmet. Detaljerna ser olika ut i olika databashanterares olika implementationer av ESQL, men en viss databashanterare (Mimer) gör följande översättning. Vi utgår från den här ESQL-koden ur exempelprogrammet ovan:
437
exec sql declare person_cursor cursor for
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xselect nummer, namn, telefon
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom personer;
exec sql open person_cursor;
Den översätts till den här kompilerbara C-koden:
/****
*exec sql declare person_cursor cursor for
* select nummer, namn, telefon
* from personer;
****/
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x/* Transitional SQL-92 */
/****
*exec sql open person_cursor;
****/
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x/* Transitional SQL-92 */
{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xsqlpaa[0] = (void*)sql006;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xsqlpaa[1] = (void*)sql007;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xsqlrcv[0] = 11;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xdbapi4(sqlrcv,sqlstate,sql008,sql009,sqlpaa);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xsqlstate[5] = '\0';
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif (sqlrcv[4] == 3) goto error_exit;
}
Tidigare i den av preprocessorn genererade koden har C-variabeln sql008 fått textsträngen "select nummer, namn, telefon from personer" som innehåll. Den preprocessade ESQL-koden innehåller alltså funktionsanrop med SQL-frågor i strängar, på ungefär samma sätt som i ODBC eller i ett databaseget C-API. Allt det som vi fick krångla med i ODBC sker också i ESQL, men preprocessorn skriver det mesta av den koden, vilket gör livet lättare för den mänskliga programmeraren.
En skillnad mellan å ena sidan ESQL och å andra sidan ODBC och databasegna C-API:er är att statiska och dynamiska SQL-frågor hanteras olika i ESQL. En statisk SQL-fråga är en som man vet hur den ska se ut redan när man skriver programmet. Alla ESQL-frågor vi sett ovan har varit statiska. En dynamisk fråga däremot finns inte tillgänglig förrän programmet körs. Det kan bero på att frågan byggs upp av programmet, med lämpliga värden instoppade direkt i 438 frågesträngen, eller på att en användare matar in hela frågan i ett fönster. Om frågorna är vanliga textsträngar i C-koden, som i ODBC, spelar det ingen roll om den strängen finns färdig vid kompileringen eller om den byggs ihop medan programmet körs. När man kommer så långt som till funktionsanropet till SQLExecDirect eller motsvarande, hanteras alla strängar likadant. I ESQL är ju SQL-frågorna mer en del av själva programkoden, och ska i vissa ESQL-implementationer till och med optimeras vid kompileringstillfället, och därför kan man inte bygga ihop dem hur som helst när programmet körs. Det finns dock metoder att använda dynamiska SQL-frågor i ESQL, vilket brukar kallas Dynamic SQL, som till exempel i den här programsnutten:
exec sql begin declare section;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xchar my_query[201];
exec sql end declare section;
printf("Mata in ett SQL-kommando:\n");
fgets(my_query, sizeof my_query, stdin);
exec sql execute immediate :my_query;
Ytterligare några exempel på ESQL-program finns på bokens webbplats.
Om en fråga är komplicerad, kan det ta en hel del tid att preparera den. I ODBC och JDBC innebär detta att det tar tid att starta program som först gör en massa prepareStatement.
Förprocessorn, som känner igen och hanterar de exec sql-märkta SQL-satserna, kan nämligen även preparera SQL-satserna, och de preparerade satserna kan sparas i databasserven. Därefter kompileras det förprocessade programmet som vanligt.
439När man vid senare tillfälle exekverar programmet sker ingen preparering alls, utan det sker direkt parametriserade anrop av preparerade satser i databasservern. Om en exekveringsplan blivit inaktuell till följd av omfattande databasuppdateringar sker ompreparering dynamiskt när exekveringsplanen anropas nästa gång. Att amortera av optimeringskostnaden är den viktigaste anledningen att använda Embedded SQL.
Det är dock inte alla databashanterares ESQL-implementationer som arbetar så här avancerat. En del översätter bara till API-anrop, utan att för-preparera frågorna vid kompileringstillfället.
Lagrad procedur (engelska: stored procedure). En programsnutt som man kan lagra i databasen, och som sen kan anropas och köras.
Gränssnitt (på engelska interface). En skiljelinje mellan två delar i ett system, till exempel mellan ett tillämpningsprogram och en databashanterare, eller mellan ett system och dess omgivning, till exempel mänskliga användare. Till gränssnittet hör både var gränsen dragits, och hur kommunikationen över gränsen sker. All interaktion mellan de två delsystemen sker genom gränssnittet. Gränssnittet kan till exempel bestå av ett antal klassdefinitioner, eller ett antal funktioner eller subrutiner som går att anropa.
API (står för Application Programming Interface). "Tillämpningsprogrammeringsgränssnitt". Skiljelinjen mellan ett tillämpningsprogram och (till exempel) en databashanterare. API:et tillhandahålls av databashanteraren, och kan bestå av ett bibliotek med datatyper 440 och funktioner som tillämpningsprogrammet kan använda för att kommunicera med databashanteraren.
JDBC (Java DataBase Connectivity). En standardiserad metod för att lägga in SQL-satser i ett Java-program. Påminner mycket om ODBC.
ODBC (står för Open DataBase Connectivity). En standardiserad metod för att lägga in SQL-satser i ett program skrivet i C eller C++. ODBC kommer från Microsoft, men är en öppen standard och fungerar på många olika system och med många olika databashanterare.
SQL/CLI, eller bara CLI (står för Call-Level Interface). "Anropsnivågränssnitt", ett gränssnitt som består av funktioner eller procedurer, och där interaktionen genom gränssnittet sker genom funktionsanrop. Detta är den nästan exakta kopia av ODBC som definieras i SQL-standarden.
Datakälla (på engelska data source). I ODBC betraktas en databashanterare som en datakälla, och meningen är att alla datakällor ska se likadana ut för den programmerare som skriver ett tillämpningsprogram.
Handtag (på engelska handle). I ODBC är ett handtag ett dataobjekt som representerar en allokerad resurs, till exempel en anslutning till en databas eller en SQL-fråga.
Drivrutin (på engelska driver). Ett bibliotek av funktioner som används för att kommunicera med en hårdvara eller mjukvara. I ODBC och JDBC används en drivrutiner för att kommunicera med databashanterare av en viss typ. Olika databashanterare kommunicerar på olika sätt, men drivrutinerna hanterar och döljer de olikheterna, så att alla databashanterare ser likadana ut för de program som använder dem. Drivrutinerna kan enkelt bytas, utan att tillämpningsprogrammet behöver skrivas om.
Drivrutinhanterare (på engelska driver manager). Hanterar de olika drivrutinerna i ODBC respektive JDBC, och tar dessutom emot funktionsanrop från tillämpningsprogrammen. Såväl drivrutinhanteraren som de olika drivrutinerna är funktionsbibliotek, men de flesta funktionerna i drivrutinhanteraren gör inte så mycket när de anropas, utan de anropar bara i sin tur motsvarande funktion i drivrutinen.
441Enskiktsdrivrutin (på engelska one-tier driver). I ODBC: En drivrutin som arbetar direkt med en fil.
Tvåskiktsdrivrutin (på engelska two-tier driver). I ODBC: En drivrutin som kopplar upp sig mot en databasserver.
Treskiktsdrivrutin (på engelska three-tier driver). I ODBC: En drivrutin som kopplar upp sig mot en gateway.
Gateway. I ODBC: Ett program som ser ut att vara en databashanterare när man kopplar upp sig mot det, och man kan ställa SQL-frågor och få svar. Men i stället för att verkligen hantera en databas, skickar en gateway vidare frågorna till en eller flera andra databashanterare, kanske efter att ha skrivit om frågorna på olika sätt. På det sättet kan man med hjälp av en gateway få en databashanterare att se ut som en annan.
Cursor. Används i ODBC, JDBC med flera för att hålla reda på den aktuella raden i resultatet från en fråga. Vissa typer av cursors kan stegas både fram och tillbaka, och hoppa bland raderna.
Förbereda en SQL-fråga (på engelska prepare). Att analysera och optimera frågan, så att den därefter kan köras många gånger utan att det arbetet måste göras varje gång.
Parametriserad SQL-fråga. En SQL-fråga där man ersatt konstanter, till exempel namn som man söker efter, med ett frågetecken. Därefter får man ange dessa konstanter med särskilda anrop när man ska köra frågan.
Länkning (på engelska linking). När ett program kompilerats, dvs översatts från källkod till exekverbar maskinkod, måste de olika delarna av programmet, och eventuella funktions- eller klassbibliotek som ska användas, byggas ihop till ett helt, körbart program. Det kallas länkning. Programmet som gör detta kallas länkare. Länkning kan vara statisk, vilket betyder att den görs i förväg, så att hela programmet finns färdigt som en enda fil, eller dynamisk, vilket betyder att den görs precis när programmet startas, eller till och med medan det körs.
Embedded SQL, ESQL. "Inbäddad SQL". En standardiserad metod för att lägga in SQL-satser i ett C-program, på ett mer integrerat sätt än med vanliga CLI-API:er som ODBC. ESQL finns även för andra programspråk. Man skriver SQL-koden mitt bland den vanliga C-koden, men markerad med orden exec sql. Exec sql-satserna 442 översätts sen av en preprocessor till C-kod, huvudsakligen funktionsanrop, som kan kompileras med en vanlig C-kompilator.
Preprocessor. Termen brukar användas om ett program som översätter ett högnivåspråk (till exempel C++) till ett annat högnivåspråk (till exempel C). Påminner om en kompilator, men utmatningen är alltså inte körbar maskinkod, utan ett program i ett annat språk. I ESQL används en preprocessor för att översätta ett C-program med inbäddad SQL-kod till ett vanligt, kompilerbart Cprogram.
Statisk SQL-fråga (på engelska: static SQL query). En SQL-fråga som man vet hur den ser ut redan när man skriver programmet.
Dynamisk SQL-fråga (på engelska: dynamic SQL query). En SQL-fråga som man inte vet hur den ser ut redan när man skriver programmet, utan som på något sätt genereras under programkörningen.
import sqlite3
". Andra databashanterare och programmeringsspråk kan kräva omfattande installation och konfigurering. Python är dessutom ett av de lättaste språken att börja med om man inte redan kan programmera.
2 Det här gäller ODBC version 3. I ODBC version 2 använde man i stället flera olika funktioner, SQLAllocEnv, SQLAllocConnect och SQLAllocStmt, för att allokera olika typer av resurser.
3 I fallet Microsoft Access behöver man däremot inte nödvändigtvis ha Microsoft Access installerad på datorn! I vissa versioner av Windows räcker det att använda den ODBC-drivrutin som följer med själva operativsystemet. De funktioner som behövs för att tolka och köra SQL-frågor finns i det fallet tillgängliga redan från början, utan att man behöver installera Access.
I en relationsdatabas kan man skapa tabeller (med kommandot create table
), lägga in data i dem (med kommandot insert
), och sen göra sökningar (med select
-frågor).
Så länge man har en liten databas räcker det med det.
Men om mängden data växer, eller om frågorna är mycket komplicerade, kan sökningarna ta lång tid, helt enkelt eftersom det är mer data för databashanteraren att leta igenom.
Man säger då att databasen har dåliga prestanda.
Då måste man hjälpa databashanteraren genom att ange mer i detalj hur databashanteraren internt ska lagra tabellerna.
Man säger att man väljer en fysisk design på databasen, eller man väljer lagringsstrukturer.
Det gör man främst genom att skapa index till tabellerna.
Ett index fungerar som registret i en bok. (Ett sånt register heter ju mycket riktigt "index" på engelska.) Om du har en lärobok om databaser, och vill veta vad en databasadministratör gör, kan du förstås börja på första sidan och sen läsa igenom hela boken tills du hittar 446 avsnittet om databasadministratörer. Med tanke på hur tjocka typiska databasböcker brukar vara, tar det nog flera veckor.
Eller så kan du leta upp ordet "databasadministratör" i registret, och se att det står om databasadministratörer på sidan 717. Sen är det bara att bläddra fram till sidan 717, så hittar du databasadministratörsavsnittet där. Det sättet går naturligtvis mycket fortare än att läsa igenom hela boken. (När författaren provade nu, med en databasbok som han råkade ha med sig, tog det 13 sekunder. Det är ju lite snabbare än flera veckor.)
Anledningen till att det går fortare är förstås att registret är sorterat i bokstavsordning. En annan anledning är att registret är mycket mindre än hela boken. Det är bara några få sidor att bläddra i, i stället för de många hundra sidorna i hela boken.
I en relationsdatabas fungerar ett index som en extra tabell, med "pekare" in i huvudtabellen (i stället för sidhänvisningar som i registret i en bok). Antag att vi har en tabell med kunder, som databashanteraren internt har sorterat på kundnummer:
Kunder | ||
---|---|---|
Nummer | Namn | Adress |
4 | Lotta | Vägen 7 |
7 | Hjalmar | Vägen 7 |
60 | Diana | Slottet |
67 | Rex | Bengali |
107 | Saida | Allén 5 |
110 | Diana | Bengali |
113 | Hulda | Stora torget 18 |
Raderna i tabellen är sorterade på kundnummer, så databashanteraren kan snabbt hitta en kund med ett visst kundnummer. En sån här SQL-fråga går alltså snabbt:
select Namn, Adress
from Kunder
where nummer = 7;
(Det här är bara ett exempel. En tabell med sju rader är förstås väldigt liten, och ingen databashanterare har problem med att hantera 447 en så liten tabell. Databashanterarna bara skrattar åt vår löjliga tabell. Men tänk dig i stället att det finns tiotusentals rader i tabellen, eller kanske en miljard.)
Om vi vill söka efter en kund med ett visst namn, har databashanteraren ingen nytta av att tabellen är sorterad på kundnummer. Då måste den läsa igenom alla raderna i tabellen, rad för rad (så kallad sekventiell sökning). En sån här SQL-fråga går alltså ganska långsamt:
select Nummer, Adress
from Kunder
where Namn = 'Hjalmar';
Vi kan därför använda kommandot create index för att skapa ett index som gör att man snabbt kan hitta en kund med ett visst namn. Man säger att man skapar ett "index på kolumnen namn", eller bara "index på namn".
create index Kundnamnsindex on Kunder(Namn);
(Kundnamnsindex i kommandot ovan är bara ett namn, och indexet kan heta vad som helst.) Databashanteraren bygger nu en "extra tabell", som den använder internt när den ska söka efter namn i kundtabellen:
Eftersom indexet är sorterat på namn, går det snabbt att hitta "Hjalmar". Sen följer man bara referensen till rätt rad i kundtabellen, och där står den information om Hjalmar (nummer och adress) som vi ville ha fram.
Databashanteraren kommer automatiskt att använda det här nya indexet varje gång man ställer en SQL-fråga som söker efter namn i kundtabellen. Den kommer också automatiskt att ändra innehållet i indexet om man ändrar innehållet i kundtabellen, så att indexet hela tiden är aktuellt och pekar rätt.
Om de kolumner som används för att söka i en tabell är indexerade, går det snabbt att söka även i stora mängder data. Till exempel har vi provkört databashanteraren MySQL med sju miljarder rader i en tabell, för att simulera Jordens befolkning. Med tabellen lagrad på en vanlig, långsam, mekanisk hårddisk tog det ungefär en tiondels sekund att hitta en rad med ett visst värde i en indexerad kolumn. Om kolumnen inte var indexerad, så att databashanteraren behövde läsa igenom hela tabellen, tog det två timmar.
Ett annat ord som ibland används för index är inverterade filer. En form av NoSQL-databaser (se kapitel 28) som kallas nyckel-värdesdatabaser (på engelska key-value stores, sidan 639) är i själva verket distribuerade 1 index där man för en given nyckel finner motsvarande dokument. I sådana databaser finns det inget frågespråk, utan programmeraren får hantera indexering och sökstrategier procedurellt i sitt program.
Man kan undra varför vi måste specificera index på det här viset. Kan inte databashanteraren göra det själv, när den gör så mycket annat automatiskt?
Jo, databashanteraren skulle kunna skapa index automatiskt, kanske ett index på varje kolumn. Men alla indexen kanske inte behövs, och det finns också nackdelar med index:
Vilka index som verkligen behövs beror på vilka sökningar som kommer att göras. Databashanteraren är trots allt bara ett program, och den kan inte gissa hur vi har tänkt oss att databasen ska användas. Därför är det ofta bättre att databasadministratören talar om för databashanteraren vilka index som egentligen behövs.
Databashanteraren kan samla statistik om de sökningar som görs, och sen skapa och ta bort index utifrån den statistiken. Men den sortens gissningar som databashanteraren kan göra blir grövre än vad en människa kan åstadkomma. Till exempel vet databashanteraren inte vilka av de gjorda sökningarna som det är viktigt att utföra snabbt, och vilka som kan få ta lite längre tid. Att skapa ett index kan också ta ganska lång tid (dygn!) om det är stora tabeller, och man vill kanske inte tillåta databashanteraren att besluta om så stora jobb på egen hand. Däremot kan moderna databashanterare som SQL Server och Db2 numera ge råd om det behövs index någonstans, baserat på vilka frågor databashanteraren får.
Vilka index som behövs beror på vilka sökningar som kommer att göras. Därför räcker det inte med att bara titta på databasens schema och innehåll. Man måste även veta hur databasen kommer att användas, och bestämma index utifrån det. När databasen sen är 450 i drift måste man förmodligen göra justeringar, både på grund av ändrade förutsättningar och på grund av att man inte lyckats perfekt med valet av index.
Det gäller alltså att bestämma vilka tabeller man ska skapa index för, och på vilka kolumner. Ofta är det en avvägning mellan att ha index (för att sökningarna ska gå snabbare) och att inte ha index (för att spara plats och för att ändringar i databasen ska gå snabbare).
3. Vilka kolumner används i sökningarna? Vi bryr oss bara om de kolumner som används i where-villkoren eller i villkoren för join-operationer, inte de som bara är med i resultatet. Dessa kolumner bör ha index.
select Anställda.
namn,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xAnställda.
adress
from Anställda, Avdelningar
where Anställda.
namn
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x= 'Bengt'
and Anställda.
jobbarpå
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x= Avdelningar.
avdnr;
Här kan kolumnerna namn och jobbarpå i tabellen Anställda, och kolumnen avdnr i tabellen Avdelningar, vara lämpliga kandidater till att ha index. Kolumnen adress i tabellen Anställda används bara i svaret, och behöver därför inget index. (Övning: Varför behöver kolumner som bara används i svaret inget index? 2 )
char(500)
, bör helst inte indexeras.
Man måste ju, i indexet, upprepa värdena från den indexerade kolumnen: om man tittar i figuren ovan står det till exempel Hjalmar både i huvudtabellen och i indexet.
Därför kan ett index på en bred kolumn ta stor plats.
(Övning: Ävadårå?
Disk är billigt.
3
)
Ibland räcker inte de här ganska enkla reglerna, utan man måste ta till mer avancerade metoder:
select sum(lön)
from Svenskar;
Det tar kanske en minut. Då kan det vara bättre att lagra summan separat, och sen se till att uppdatera den varje gång man ändrar i tabellen med svenskar. I en aktiv databas kan man definiera regler, som ser till att databashanteraren automatiskt gör den uppdateringen varje gång tabellen med svenskar ändras.
Hela det här avsnittet om index har handlat om att välja lagringsstrukturer så att sökningar (och uppdateringar) i databasen går så snabbt som möjligt. Men även om man har gjort det absolut bästa 453 möjliga valet av lagringsstrukturer, tar varje sökning (och uppdatering) en viss tid att köra. Om det är många som använder databasen samtidigt, kanske databashanteraren inte hinner med att köra alla sökningarna.
Ett exempel: En enskild sökning går snabbt, på 20 millisekunder (0.02 sekunder). Men nu kommer det tusen användare per sekund och gör en sån sökning. Eftersom varje sökning tar 0.02 sekunder för databashanteraren att göra, skulle databashanteraren nu behöva arbeta 0.02 * 1 000, dvs 20 sekunder, varje sekund. Annorlunda uttryckt skulle databashanteraren behöva vara 20 gånger snabbare för att precis hinna med alla sökningarna. I stället för 0.02 sekunder får användarna nu kanske vänta i minuter eller timmar på svaret. Den som betalat sina räkningar via webben i slutet av en månad kanske känner igen fenomenet. Det blir så att säga "lång kö till kassan".
(Notera: Vi har gjort vissa förenklingar. Ett databassystem som körs på en dator med flera diskar och flera processorer, så att flera sökningar kan göras samtidigt, eller där resultatet av en sökning kan användas som svar även på andra sökningar, kan hinna med.)
För att upptäcka den här sortens problem kan man göra en belastningsanalys. Då försöker man räkna ut belastningen på databasen, det vill säga hur många operationer som utförs per tidsenhet i databasen, eller i en del av databasen, till exempel en tabell eller en hårddisk. Till exempel kan man titta på hur många läsningar som görs per sekund från hårddisken. Man måste ta hänsyn till
För att göra en rättvisande belastningsanalys krävs det ganska goda kunskaper om exakt hur databashanteraren arbetar internt. Därför är det vanligare att man helt enkelt provkör och ser hur mycket databashanteraren hinner med.
Men även en enkel belastningsanalys kan ge värdefulla ledtrådar till om en tänkt databaslösning är realistisk. Kanske kommer vi fram till att databasen kommer att göra en miljon läsningar i sekunden 454 från en viss tabell som ligger lagrad på en mekanisk hårddisk. Om vi räknar med att varje läsning tar 10 millisekunder, dvs 0.01 sekunder, skulle hårddisken behöva arbeta 0.01 * 1 000 000, dvs 10 000 sekunder (ungefär tre timmar) per sekund. Annorlunda uttryckt: Hårddisken skulle behöva vara 10 000 gånger snabbare för att precis hinna med. Vi bör därför fundera lite på om vi verkligen ska försöka bygga systemet på det sättet.
Index (engelska: index). En datastruktur som används internt i en databashanterare för att hitta data snabbare än genom sökning i hela datamängden.
2 Svar: I de flesta databashanterare lagras hela innehållet på en hel rad tillsammans på ett ställe. Om man väl hittat raden, går det därför fort att ta fram värdet på vilken som helst av kolumnerna på den raden. En kolumn som inte används för att välja ut rader behöver därför inget index. Index hjälper databashanteraren att hitta rätt rader, inte att hitta kolumner.
3 Svar: Hårddiskar är kanske billiga, men felet är inte bara att man kanske får köpa en större disk. Det blir också långsammare för databashanteraren att leta i indexet, om det är stort.
Är förresten hårddiskar verkligen så billiga?
Att köpa en stor och snabb disk som man ska sätta i sin hemdator är mycket riktigt billigt. Men tänk på att ett företag som använder en servermaskin i sin verksamhet har delvis andra krav. Kanske är egenskaperna hos vanliga billiga diskar inte tillräckliga. Kanske vill man ha ett serviceavtal för sina servrar, så att man får garantier för att allt fungerar, men då kan det också vara ganska dyrt om man vill ha en ny disk. Och vad händer när diskarna inte längre får plats i servern? Och vad händer när innehållet på diskarna inte längre får plats på backupbanden? Eller när backupen tar så lång tid att skriva att man inte hinner ta backup så ofta som man behöver? Vad kostar det att införa en ny backuplösning på ett företag, med hårdvara, mjukvara, utbildning av personalen, och konsulter som tar 1 400 kronor i timmen? Vad kostar det att inte införa en ny backuplösning, och kanske tappa alla data från en arbetsdag?
Man kan använda en databashanterare mycket utan att alls bry sig om hur databashanteraren lagrar sina data internt. Databashanteraren sköter en massa saker automatiskt. Skapar man till exempel en tabell i en relationsdatabashanterare, så kommer databashanteraren förstås att lagra tabellens data på något sätt internt, men till en början kan man strunta i exakt hur det går till.
I kapitel 22, Index och prestanda, gick vi igenom index och prestanda ur ett användarperspektiv, dvs vad man behöver veta som användare av ett databashanteringssystem. 1 I det här kapitlet går vi vidare med hur saker fungerar inuti databashanteraren.
Innan man läser det här kapitlet om fysiska lagringsstrukturer i databaser, bör man känna till grunderna för lagringsstrukturer i allmänhet. Man bör veta vad som menas med träd och hashtabeller, och vilka egenskaper de har. Man bör veta vad som menas med ett binärt träd och ett B-träd, och förstå skillnaden mellan dem.
Vi tar inte upp det i den här boken, men det finns en introduktion till lagringsstrukturer på bokens webbplats. 2 Den består av ungefär 20 sidor, med figurer och förklaringar.
Det finns databashanterare som lagrar hela databasen i primärminnet, men det vanligaste är att databasen lagras som en eller flera filer på sekundärminnet, dvs på en eller flera SSD:er eller mekaniska hårddiskar. Databashanteraren läser bara in de data som den precis ska arbeta med i primärminnet, och när ändringar görs i databasen 457 skrivs de ut till sekundärminnet direkt (eller i alla fall nästan direkt). Även en primärminnesdatabashanterare brukar kunna spara databasen på sekundärminne, för att skydda mot plötsliga krascher eller bara så att man kan stänga av datorn utan att förlora alla sina data.
En rad i en relationsdatabas, eller ett objekt i en objektorienterad databas, eller vad det nu är för datamodell man arbetar med, lagras som en post. I en tabell som ser ut som den här:
Anställda | |||
---|---|---|---|
Nummer | Namn | Lön | Avdelning |
23 | Laban | 34 000 | 3 |
17 | Labolina | 22 000 | 3 |
1 | Hjalmar | 14 000 | 1 |
lagras raderna förmodligen i poster som ser ut som den här:
En post är helt enkelt de olika fälten, placerade efter varandra. Det kan vara i primärminnet, på en hårddisk, eller som en dataström som sänds över ett datornät. På en disk kan man inte läsa eller skriva enskilda byte eller enskilda poster, utan man läser och skriver alltid hela block. Ett fysiskt diskblock, som SSD:er och mekaniska hårddiskar jobbar med, brukar vara antingen 512 eller 4 096 byte stora. 512 byte motsvarar ungefär texten i det här stycket. Operativsystemet eller databashanteraren brukar sen gruppera ihop några diskblock till ett logiskt diskblock, även kallat sida, som kan vara upp till ungefär 64 kilobyte (65 536 byte) stort. (Terminologin, med block och sidor, kan variera lite i olika sammanhang.)
Man stoppar in så många poster som får plats i ett (logiskt) diskblock. Antalet poster som får plats kallas ibland blockningsfaktor. Om det blir plats över i blocket, lämnas den tom. Det går att dela upp en post på flera block, men det är vanligare att man bara har hela poster i varje block.
458I figuren får det plats fyra poster i blocket, men i verkligheten kan det vara många fler.
Exempel: Ett heltal tar ofta upp 32 bitar, dvs 4 byte. Om namnfältet i posttypen ovan är 16 tecken, och varje tecken lagras som en byte, tar en hel post upp 28 byte. Om ett logiskt diskblock innehåller 16 kilobyte, får det alltså plats 585 poster i blocket (16 * 1 024 / 28, avrundat neråt). Det brukar också gå bort några byte i blocket för en "header" med information till exempel om hur många poster som faktiskt finns i blocket. För att lagra 1 000 poster, går det åt 2 datablock. För att lagra en miljon poster går det åt 1 710 datablock.
Raderna i en tabell i en relationsdatabas lagras i en följd av diskblock, en så kallad fil. En sådan fil kan, men måste inte, vara lagrad som en av operativsystemets filer.
En mekanisk hårddisk behöver i genomsnitt 5-10 millisekunder för att hämta ett block med data, när blocken inte lagrats eller läses i någon särskild ordning. När det gäller att läsa data i sekvens, dvs där blocken man hämtar ligger i sekvens på disken, beror det mer på typen av anslutning (till exempel SATA). Där kan en mekanisk hårddisk hämta ett par hundra megabyte per sekund.
SSD:er är snabbare. Att läsa ett enskilt block tar mindre än 0.1 millisekunder. En SSD är alltså ungefär 100 gånger snabbare än en 459 mekanisk hårddisk när det gäller åtkomst av enskilda block av data. När det gäller att läsa data i sekvens, är SSD:n bara ungefär 10 gånger snabbare än den mekaniska hårddisken, och kommer kanske upp i någon gigabyte per sekund.
När detta skrivs (2017) används mekaniska hårddiskar fortfarande i många tillämpningar. SSD:er är mycket snabbare och kommer förmodligen snart att helt ersätta mekaniska hårddiskar, men SSD:erna är än så länge mindre och dyrare. Därför behöver vi fortfarande studera egenskaperna för mekaniska hårddiskar.
Nu ska vi titta på hur diskblocken placeras på en mekanisk hårddisk. Vi skruvar isär vår dator, tar av locket på hårddisken, och ser efter hur den ser ut inuti:
Hårddisken består av en eller flera metallplattor. Plattorna snurrar. I många moderna hårddiskar snurrar de med 120 varv i sekunden, eller 7200 varv per minut som det ofta uttrycks.
Det finns också en arm som rör sig fram och tillbaka över disken, ungefär som tonarmen på en gammaldags vinylskivspelare. På armen sitter huvuden, eller läs- och skrivhuvuden, som fungerar som pickupen på skivspelaren. Varje sida på varje platta har ett eget huvud.
Armen kan flytta sig fram och tillbaka och läsa de olika spåren på disken. Det finns kanske hundra spår på disken. Eftersom vi redan 460 förstört hårddisken genom att öppna den, kan vi lika gärna ta en penna och rita upp ett av spåren:
Längs varje spår placeras diskblock:
För att läsa eller skriva ett visst block på disken, måste man alltså flytta armen till rätt spår, och sen vänta tills det rätta blocket snurrat fram. Om disken snurrar med 120 varv i sekunden, tar det alltid mindre än en hundradels sekund för det rätta blocket att snurra fram. Det som tar längst tid är för armen att flytta sig. Hur lång tid det tar varierar ganska mycket, beroende på var armen stod från början.
Datorer blir snabbare för varje år. Den berömda Moores lag säger ungefär att datorers prestanda, både när det gäller minnesutrymme och snabbhet, blir fyra gånger bättre på tre år. Men det gäller elektroniken. Mekaniska hårddiskar innehåller ju rörliga delar, särskilt den där armen som ska flyttas fram och tillbaka. Därför är det inte lika lätt att göra hårddiskarna snabbare. SSD:er saknar rörliga delar, och har potential att bli mycket snabbare, men än så länge är de oftast inkopplade med samma anslutningar som mekaniska hårddiskar (till exempel SATA), vilket begränsar snabbheten.
Tiden det tar att flytta armen till rätt spår på en mekanisk hårddisk, och sen vänta tills rätt block snurrat fram, beror förstås på vilket spår det är och var armen stod från början, men i genomsnitt kan man räkna med att det tar ungefär 5 millisekunder för en modern hårddisk. Man bör nog inte räkna med några revolutionerande förbättringar av den tiden de närmaste åren. Om man ska läsa fler block på samma spår behöver man inte flytta armen, så om blocken ligger i följd går det mycket fortare, och läshastigheten begränsas snarare av överföringshastigheten i anslutningen till hårddisken.
Om man jämför med primärminnet i datorn är hårddiskarna enormt långsamma. Att hämta en datauppgift på ett par byte tar kanske tio nanosekunder (dvs tio miljarddelar av en sekund, eller 0.000 000 01 sekunder) i primärminnet, medan det alltså tar i genomsnitt ungefär 5 millisekunder (dvs en halv hundradels sekund, eller 0.005 sekunder) att läsa in diskblocket med samma uppgift från hårddisken. Disken är alltså kring en miljon gånger långsammare. SSD:n är snabbare med 0.1 millisekunder, men det är fortfarande tio tusen gånger långsammare än primärminnet.
462Eftersom diskar är så långsamma, är diskbaserade databashanterare byggda för att inte läsa eller skriva mer än nödvändigt på diskarna. Både de datastrukturer som används, och metoderna för att utföra sökningar i databasen, är konstruerade för att minimera diskarbetet. Till exempel placerar man gärna data som hör ihop i samma diskblock, så att de kan läsas i en enda blockläsning. Det är en av orsakerna till att man bara brukar ha hela poster i varje block, och hellre lämnar lite tom plats i diskblocken än delar upp en post på flera block.
Moderna databashanterare har möjlighet att spara mycket data i primärminnet, vilket man kallar att de cachar data. Ofta går det att konfigurera hur mycket cache som ska användas.
När vi talar om lagringsstrukturer är en nyckel ett fält, eller en kombination av fält, som är garanterade att ha unika värden bland alla posterna i filen. Det är samma definition som i relationsmodellen, även om man där talar om kolumner (eller attribut) i stället för fält, rader (eller tupler) i stället för poster, och tabeller (eller relationer) i stället för filer.
Primärnyckeln är en nyckel som man dessutom har sorterat filen efter. Primärnyckeln kan också kallas fysisk primärnyckel eller sorteringsnyckel. Det är inte riktigt samma sak som i relationsmodellen, för där finns det ju ingen bestämd ordning mellan raderna i en tabell, och i relationsmodellen är primärnyckeln bara en kandidatnyckel som man valt som den man ska använda när man refererar till rader i tabellen. Eftersom relationsmodellens primärnyckel inte är samma sak som den fysiska primärnyckeln, behöver primärnyckeln i en tabell inte vara densamma som primärnyckeln i filen som tabellen lagras som.
Det kan bara finnas en enda fysisk primärnyckel i en fil, för den går ju bara att sortera i en ordning. (Däremot kan den vara sammansatt av flera olika fält.)
Ett sorteringsfält är ett fält (eller en kombination av fält) som man sorterat filen efter. Det behöver inte vara en nyckel.
Ett vanligt operativsystem som Unix eller Windows har filer, som kan användas för enkel lagring av data. Operativsystemet brukar erbjuda operationer för att öppna en fil, läsa och skriva data, för att stänga filen, och för att hoppa fram och tillbaka i den. Det sista brukar kallas för seek på engelska, och är till för att man inte ska behöva stega sig igenom hela filen för att läsa, eller lägga till, en post på slutet eller mitt i.
När en databashanterare arbetar med filer, menar man en följd av poster som till exempel motsvarar raderna i en tabell. Då behövs de enkla operationerna för att läsa och skriva poster, plus ytterligare operationer för att söka efter poster med vissa värden. Skillnaden mot de enkla filerna i vanliga operativsystem är att databashanteraren har mer avancerade datastrukturer att hålla reda på än bara en enkel sekvens av poster. Det handlar om hashtabeller, index som pekar in i filen, osv. All hantering av databashanterarens filer måste ta hänsyn till de datastrukturerna. När man stoppar in en post måste den stoppas in på rätt ställe, och indexen måste uppdateras. Om det till exempel är en sorterad fil, går det förstås inte att skriva dit nya poster var som helst.
Ett vanligt sätt att bygga en databashanterare är att man använder operativsystemets filer som grund. Man kanske har en (operativsystems-)fil per tabell. Eller så använder man en och samma fil för hela databasen, och en tabell lagras då i ett avsnitt av den filen. Sen bygger man på med mer avancerad filhantering i databashanteraren, där man till exempel utnyttjar index för att snabbt hitta poster vid sökning. Den lösningen motsvaras av den vänstra uppdelningen i figuren nedan: operativsystemet erbjuder grundfunktionerna, och databashanteraren bygger vidare på dem.
Operativsystemets filer ska fungera för många olika ändamål, och är kanske inte tänkta för att utnyttjas till de avancerade datastrukturer som används i en databashanterare. Därför kanske operativsystemets filhantering inte passar riktigt för databashanterarens behov. Till exempel innehåller operativsystemet ofta olika sorters buffring, som innebär att data inte alltid skrivs till disken direkt, utan operativsystemet väntar ibland länge med själva skrivoperationen. Det kan skapa problem för databashanteraren, om den ska garantera att saker som sparats i databasen aldrig försvinner, även om strömmen går eller datorn kraschar. Därför finns det lösningar där databashanteraren sköter hela filhanteringen själv. Typiskt 464 allokeras en partition 3 på disken, och sen arbetar databashanteraren direkt mot den partitionen. Det motsvarar den mellersta uppdelningen i figuren nedan.
Man kan också tänka sig att operativsystemet erbjuder mer avancerade filoperationer, till exempel sökning. Det motsvarar den högra uppdelningen i figuren nedan, men den är mindre vanlig i dag.
Den avancerade filhanteringen utförs alltså oftast av ett delsystem i databashanteraren. Det kallas storage manager på engelska, vilket kan översättas med hanterare av lagrade data.
De uppräknade operationerna arbetar med en post i taget. Ibland finns det också operationer som arbetar med en hel mängd av poster åt gången, som operationen att hitta och hämta alla poster i filen som matchar ett visst värde i ett visst fält.
När man lagrar poster i en fil med ett vanligt program, som man skriver i ett vanligt programspråk som C++ eller Java, brukar operativsystemet lagra posterna i diskblock som ligger i en följd på disken. Ibland går inte det, eftersom det inte finns tillräckligt många lediga diskblock i följd, och då måste operativsystemet placera diskblocken som hör till den filen på olika ställen, här och där på disken. Då tar det längre tid att läsa igenom hela filen, eftersom armen med läshuvudet måste röra sig mer. Det är det som kallas fragmentering av disken.
En databashanterare gör likadant. Många relationsdatabashanterare skapar en vanlig fil, med hjälp av operativsystemet, för varje tabell, och lagrar sen posterna i den filen. Ibland används inte en särskild fil för varje tabell, utan man lagrar hela databasen i en enda fil. Ytterligare andra databashanterare arbetar direkt mot disken, utan att gå via operativsystemets filhanteringsfunktioner. Men oavsett om posterna för raderna i en tabell ligger i en egen fil, i en del av en större fil, eller är allokerade på annat sätt, ligger de i någon form av följd av diskblock, och vi talar därför om filen som de lagras i. Eftersom det ibland är andra filer inblandade, till exempel för att lagra index, kallar vi den här filen för huvudfil. Huvudfilen kan också kallas för datafil, för det är ju i den filen som själva dataposterna finns.
Huvudfilen kan i princip organiseras på fyra olika sätt:
466I de följande avsnitten i det här kapitlet kommer vi att visa hur dessa olika filorganisationer fungerar och vilka egenskaper de har. Vi kommer att visa olika typer av hashtabeller, och olika sätt att organisera filen enligt ett index.
Förutom huvudfilen (och det index som i det fjärde alternativet ovan används för att tala om var posterna ska placeras) kan man ha fler index som refererar in i huvudfilen. Men mer om det senare.
Här är ett exempel på en osorterad fil, eller heap-fil. Vi har lagt in posterna utan någon särskild ordning, allteftersom vi skapat dem. I det sista blocket finns det lite tom plats kvar.
467
24
|
Laban
|
34000
|
3
|
15
|
Labolina
|
22000
|
3
|
1
|
Hjalmar
|
14000
|
1
|
10
|
Hulda
|
14000
|
3
|
7
|
George
|
29000
|
2
|
2
|
Tony
|
14000
|
1
|
56
|
Valdemar
|
34000
|
3
|
20
|
Sam
|
22000
|
2
|
3
|
Hjalmar
|
13600
|
1
|
32
|
kajsa
|
25000
|
4
|
60
|
Hjalmar
|
24500
|
1
|
För att hitta en post med ett visst värde i ett visst fält, till exempel med nummer 56 i det första fältet, måste vi läsa igenom hela filen. Antalet diskläsningar blir lika med antalet block i hela filen, vilket i sin tur är lika med antalet poster delat med blockningsfaktorn. Om vi hittar en post med rätt värde, och vet att det fältet är en nyckel, kan vi förstås sluta läsa. Medelvärdet för antalet diskläsningar blir då antalet block delat med två. Men det gäller bara om vi vet att fältet är en nyckel, och om en post med det värdet faktiskt existerar.
Att stoppa in en post i filen är lätt. Vi bara lägger in den sist. Det kräver en blockläsning och en blockskrivning: först läser vi in det sista blockets innehåll till primärminnet, vi lägger till den nya posten, och till sist skriver vi tillbaka det nya blockinnehållet på disken. 5 Det går alltså mycket snabbt att lägga in nya poster. Om det sista 468 blocket i filen redan är fullt, allokerar vi ett nytt block på disken, och då kan vi klara oss utan någon läsning. Om vi ska lägga in flera poster, kan vi vinna ännu mer prestanda genom att vänta tills vi fyllt ett helt block med poster innan vi skriver det till disken. På så vis kan vi få mindre än en diskskrivning per inlagd post. Å andra sidan: Om något fält är deklarerat som nyckel, och måste hållas unikt av databashanteraren, måste vi först leta igenom hela filen för att kontrollera att det värde vi ska stoppa in inte redan finns. I så fall går instoppning av nya poster tvärtom mycket långsamt.
Att ta bort en post i filen kan vara en dyr operation. Om vi inte får ha några tomma platser i filen, skulle vi vara tvungna att flytta upp alla efterföljande poster ett steg, vilket innebär en kopiering av hela den delen av filen, och det skulle ta mycket lång tid. I stället kan man använda någon form av borttagningsmarkör: en flagga i varje post som talar om ifall den används eller inte. En post kan då raderas genom att man bara markerar den som borttagen, och inga data behöver flyttas. Men dessutom måste vi hitta posten som ska tas bort. Då behövs det först en sökning i filen, och det krävde ju en genomläsning av alla blocken.
Att gå igenom posterna i ordning kräver att vi sorterar hela filens innehåll. (Om det inte finns ett index som är sorterat i rätt ordning.)
Här är samma poster som i exemplet ovan med den osorterade filen, men sorterade efter det första fältet. Men man behöver inte använda just det första fältet som sorteringsfält.
469
1
|
Hjalmar
|
14000
|
1
|
2
|
Tony
|
14000
|
1
|
3
|
Hjalmar
|
13600
|
1
|
7
|
George
|
29000
|
2
|
10
|
Hulda
|
14000
|
3
|
15
|
Labolina
|
22000
|
3
|
20
|
Sam
|
22000
|
2
|
24
|
Laban
|
34000
|
3
|
32
|
kajsa
|
25000
|
4
|
56
|
Valdemar
|
34000
|
3
|
60
|
Hjalmar
|
24500
|
1
|
Om vi ska hitta en post med ett visst värde i ett visst fält, kan vi binärsöka bland diskblocken i filen. Man börjar med att läsa in det mittersta diskblocket. Om vi till exempel söker efter en post med nummer 56 i det första fältet, ser vi att posterna i det inlästa mittersta blocket alla har mindre värden än så. Alltså måste den sökta posten finnas i andra halvan av filen, om den finns alls. Om filen var större än de tre blocken i exemplet, skulle vi fortsätta att halvera filen genom att läsa in mittenblocket i filens andra halva, och så vidare. Antalet lästa block blir, i genomsnitt, två-logaritmen för antalet block. För en fil med tusen block blir antalet läsningar 2log(1 000), vilket är ungefär 10. Det är mycket bättre än en osorterad fil, men det finns andra filorganisationer där det går ännu snabbare att söka.
Att stoppa in en post i filen är mer komplicerat. Först måste vi hitta rätt block att lägga den i, men om det blocket är fullt måste vi flytta ner alla efterföljande poster ett steg, vilket innebär en kopiering av hela den delen av filen, och det skulle ta mycket lång tid. Det kan man delvis lösa genom att lämna en del tomma platser i 470 filen, där man sen kan stoppa in nya poster, eller genom att använda en särskild transaktionsfil eller spillfil (på engelska overflow file) för de nya posterna. (Sökoperationer måste då även ta hänsyn till spillfilen, och blir därför långsammare.)
Att gå igenom posterna i ordning är förstås mycket effektivt, eftersom de redan är sorterade i rätt ordning. Ja, om den ordning vi vill gå igenom dem i är den ordning som filen är sorterad i. Om vi vill ha någon annan ordning, till exempel bokstavsordning på namn, måste vi förstås sortera hela innehållet, precis som om filen varit helt osorterad. (Om det inte finns ett index sorterat på namn.)
En sorterad fil är alltså bra om vi vill gå igenom posterna i samma ordning som filen redan är sorterad i, men krånglig vid instoppning och borttagning av poster. Den är bättre än en helt osorterad fil på sökningar efter poster, men det finns andra filorganisationer som är mycket snabbare. Därför är enkla sorterade filer, utan kompletterande indexstrukturer, ovanliga i databassammanhang.
Här har vi organiserat posterna i huvudfilen som en hashtabell. Att lagra en hashtabell på disk kallas ibland för extern hashning (på engelska external hashing), medan en hashtabell i primärminne kan kallas för intern hashning (på engelska internal hashing). Varje position i hashtabellen utgörs av ett helt diskblock. Eftersom en position i tabellen alltså rymmer flera poster, brukar den kallas för en bucket på engelska (eller "hink" på svenska, men den termen är inte så vanlig). Alla poster som hashas till den positionen lagras i det diskblocket, tills det blir fullt. Vi har här använt den enkla hashfunktionen h(x) = x mod N, där x är värdet på sökfältet och N är antalet positioner i hashtabellen.
För att få bra prestanda i en hashtabell brukar man dimensionera den så att det ska bli en del tomrum, och det visar vi genom att göra den fyra positioner stor, så att den alltså upptar fyra diskblock, 471 i stället för de tre som de mer kompakta sorterade och sorterade filerna.
Att hitta en post med ett visst värde går mycket snabbt. För att till exempel hitta post nummer 10 beräknar vi hashfunktionen för 10, som är 2, och går direkt till block nummer två. Det behövs bara en enda diskläsning, vilket tar i genomsnitt 5 millisekunder. En enkel sökning i hashtabellen tar alltså 5 millisekunder, oavsett hur stor tabellen är. Åtminstone så länge just det blocket inte blivit överfullt, för då måste vi leta i kedjan av spillblock. Kedjan av spillblock kan bli lång om hashtabellen är mycket full, eller om hashfunktionen är dålig, eller om vi helt enkelt har otur med värdena. Exempel: Om vi stoppar in hela Sveriges befolkning (tio miljoner personer) i vår hashtabell med fyra positioner och med plats för fyra poster i varje block, kommer varje spillkedja att innehålla mer än en halv miljon block, om vi har en bra hashfunktion som sprider hashvärdena 472 jämnt. Om varje diskläsning tar 5 millisekunder, tar det nästan en timme att läsa igenom en sådan spillkedja.
Att stoppa in en post i filen går också snabbt, om tabellen inte är överfull. Vi använder hashfunktionen för att hitta rätt block, läser in det innehåll som just nu finns i blocket, lägger till den nya posten, och skriver tillbaka blocket. Om blocket redan var fullt, allokerar vi ett spillblock någonstans på disken, och placerar en pekare till det nya blocket i det gamla.
Att ta bort en post är också enkelt. Det går fort att hitta rätt block med hjälp av hashfunktionen, man läser in blocket i primärminnet, och sen skriver man tillbaka det på disken, men utan den borttagna posten.
Som vi ser på bilden ser ordningen mellan posterna i en hashtabell ut att vara slumpmässig. För att gå igenom posterna i ordning måste de alltså sorteras först. Eftersom en hashtabell brukar innehålla mer tomrum än till exempel en osorterad heap-fil, tar den upp fler block, och det tar längre tid att läsa igenom den. Alltså är en hashtabell riktigt dålig om man ska gå igenom posterna i en viss sorteringsordning. Det gäller även om man vill hitta ett intervall, till exempel alla värden mellan 10 och 30, eller alla värden som är större än 25.
Ytterligare en nackdel med en hashtabell är att man alltid måste känna till hela nyckeln för att kunna hitta en post. För heltal, som i exemplet, verkar det kanske inte så underligt, men om man hashar på en sträng måste man veta hela strängens exakta lydelse när man söker efter en post. Om man söker efter någon som heter Andersson, Anderson eller kanske Anderzon, kan man alltså inte söka efter något som börjar på Ander.
En hashtabell är alltså överlägset snabb om man vill hitta, stoppa in eller ta bort enskilda poster. Däremot fungerar den dåligt för att gå igenom poster i ordning, för intervall, och om man bara känner till en del av sökvärdet.
Vanliga hashtabeller kräver dessutom att man ger dem en lämplig storlek redan från början, eller att man organiserar om dem när de börjar bli överfulla. Annars får de väldigt dåliga prestanda efter ett tag. Det finns varianter av hashtabeller som växer automatiskt när man lägger in mer data, men det tar vi upp senare i det här kapitlet.
Det fjärde sättet att organisera huvudfilen är, som vi skrev på sidan 465, att låta ett index styra var posterna placeras. Det vanligaste är att man använder sig av ett index i form av ett B+-träd. Vi ska visa hur det fungerar senare, men först ska vi förklara hur index fungerar.
Ett index är, som vi såg i kapitel 22, som registret i en bok. Som användare av en relationsdatabashanterare kan man se ett index som en extra tabell, som pekar in i den "riktiga" tabellen, och som databashanteraren automatiskt använder sig av i sökningar. När vi tittar på hur data lagras internt i databashanteraren, kan vi se ett index som en extra fil, som innehåller pekare till blocken i huvudfilen. Indexfilen är lagrad i block, som vi kan kalla indexblock, och den innehåller poster, som vi kan kalla indexposter.
Man talar också om täta och glesa index:
Sekundärindex är förmodligen den typ av index som är lättast att förstå. Huvudfilen är kanske ordnad efter fältet Namn, men vi vill snabbt kunna hitta en post med ett visst värde på fältet Nummer. Alltså skapar vi ett sekundärindex, med Nummer som indexeringsfält:
Om man vet värdet på Nummer för en post går det snabbare att hitta en post via indexet: dels för att indexet är sorterat på nummer, dels eftersom indexposterna (normalt) är mindre än dataposterna, och 475 därför fyller upp färre block som man måste leta bland. På bilden tar indexet upp två block, jämfört med datafilens tre. I verkligheten kan skillnaden vara mycket större.
Notera att diskbaserade databaser nästan alltid arbetar med pekare till diskblock, och inte till enskilda poster. När man väl hittat diskblocket och läst in det till minnet, går det jämförelsevis mycket snabbt att hitta den rätta posten i blocket, och därför slipper man gärna det extra arbete och den extra plats som skulle behövas för att lagra och upprätthålla postpekare. (Extra plats? Ja, om disken exempelvis innehåller tio miljoner diskblock, får en pekare till ett diskblock plats i ett 32-bitars heltal. Om ett diskblock sen kan innehålla upp till tusen poster, kan hela disken innehålla tio miljarder poster, och då får en postpekare inte plats i de 32 bitarna.)
Här är ett sekundärindex på ett fält som inte är ett nyckelfält. Eftersom det finns mer än en Bengt-post i datafilen, måste vi peka ut flera olika diskblock. Man kan göra det genom att helt enkelt ha flera olika Bengt-poster i indexfilen, eller genom ett extra mellansteg som här, där man fyller ett diskblock (eller en del av ett diskblock) med pekare till block med Bengt-dataposter.
Ett primärindex är sorterat i samma ordning som primärnyckeln i huvudfilen, och har alltså samma sorteringsordning som huvudfilen. Därför räcker det att i indexet upprepa sökvärdena för den första posten i varje block, den så kallade ankarposten:
Vad är primärindex bra för, när huvudfilen redan är sorterad på samma sätt? Jo, eftersom indexposterna (oftast) är mindre än dataposterna, och eftersom det bara behövs indexposter för att peka ut ankarposterna, tar indexet upp mindre plats än huvudfilen, och alltså är det färre block att leta bland. (Jämför med A-ASA, ASBBIS och så vidare som står på ryggarna till de olika banden i ett uppslagsverk. Det är ett primärindex!)
Ett primärindex är sorterat i samma ordning som huvudfilen, men sorteringsfältet måste vara en nyckel. Om man indexerar på ett fält som huvudfilen är sorterad på, men som inte är en nyckel, kallas indexet för ett klustrat, eller grupperat, index (clustered index på engelska):
477Ibland räknas även primärindex, där indexeringsfältet är en nyckel, som klustrade index.
Ett index i en diskbaserad databas är en fil, och de index vi sett hittills har varit organiserade som enkla sorterade filer. Det är inte så vanligt, utan index i databaser brukar vara organiserade antingen som hashtabeller eller (vilket vi ska se senare) som B+-träd. Här ser vi ett exempel på ett hashindex. Huvudfilen är osorterad, men vi har ett sekundärindex i form av en hashtabell med tre positioner. Vi har använt den enkla hashfunktionen h(x) = x mod N, där x är värdet på sökfältet och N är antalet positioner i hashtabellen.
478
SQL-frågan select * from Personer where Nummer = 7
kommer nu att kräva två diskläsningar.
Vi beräknar hashfunktionens värde för 7, som är 1, så vi läser block nummer 1 i hashtabellen.
Där hittar vi indexposten för 7, med en pekare till rätt block i huvudfilen, och så läser vi det blocket också.
Om varje diskläsning tar 5 millisekunder, och vi ignorerar all bearbetning i primärminnet, tar SQL-frågan alltså 10 millisekunder att köra.
B+-träd är populära i diskbaserade databashanterare. Det är en bra datastruktur just för diskar. Diskar är ju dels långsamma, och dels arbetar de blockvis, så man vill använda datastrukturer som kräver så få blockläsningar som möjligt. I ett B+-träd kan man använda ett helt diskblock till varje nod, och då får man plats med många värden och många pekare i varje nod. B+-träd av ordningen flera hundra är inget ovanligt. Eftersom varje nod kan ha flera hundra underträd blir trädet bara några få nivåer högt, även för mycket stora datamängder, och man behöver inte titta i så många noder. Man kan alltså hitta sina data med bara några få läsningar från disken.
Exempel: En fil som indexeras med hjälp av ett B+-träd har en nyckel som utgörs av ett 32-bitars heltal. Den tar upp 4 byte. En pekare till ett diskblock är, antar vi, 6 byte stor. Om ett diskblock är 1 024 byte stort, kan ett diskblock rymma (1 024 – 6) / (6 + 4) + 1, dvs 102, pekare till nästa nivå:
B+-trädets ordning blir alltså 102, men i genomsnitt är en B+-trädsnod bara två tredjedels full, så den "effektiva" ordningen i exemplet blir bara 68. Men det räcker ändå för att indexera hela Sveriges befolkning med bara fyra nivåer i trädet, och hela världens befolkning med sex nivåer!
B+-träd är visserligen inte riktigt lika snabba som hashtabeller på uppslagningar av ett visst värde. En hashtabell kan ju (i idealfallet) hitta rätt post med bara en enda läsning från disken. Men B+-träd är å andra sidan bättre om man vill söka efter ett intervall eller om man vill gå igenom data i ordning.
Som vi sett tidigare är primärindex och klustrade index sorterade i samma ordning som huvudfilen. Därför behöver ett sånt index inte innehålla en blockpekare för varje datapost, utan det räcker med 480 en pekare per datablock. Det gäller även när indexet är organiserat som ett B+-träd. Här är ett primärindex i form av ett B+-träd:
Det fjärde av de fyra sätten att organisera huvudfilen (se sidan 465) är efter ett index, och det betyder normalt ett primärindex i form av ett B+-träd. När en ny post ska läggas in, söker man sig via indexet fram till rätt datablock, och lägger in posten. Om datablocket redan var fullt, delas det upp i två, och en pekare till det nyallokerade datablocket läggs in i indexnoden på den lägsta indexnivån. Om även indexnoden var full, delas den också upp i två, och en pekare till det nyallokerade indexblocket läggs in i indexnoden på nästa nivån. Om nödvändigt kommer indexblocken att delas upp i två hela vägen upp till rotnoden, och om även den var full måste också den delas upp. För att kunna peka ut både den gamla rotnoden och den nya noden, allokeras ännu en indexnod på en nivå ovanför den gamla rotnoden, och den noden blir trädets nya rotnod.
Eftersom ett sekundärindex är sorterat i en annan ordning än huvudfilen, måste indexet innehålla en pekare för varje datapost. Det räcker inte med en pekare per diskblock. Det gäller förstås även när indexet är organiserat som ett B+-träd. I lövnoderna i B+-trädet finns indexposter för alla dataposterna i datablocken. Därför brukar ett B+-träd som är ett sekundärindex vara djupare än ett B+-träd som är ett primärindex. Här är ett sekundärindex i form av ett B+träd:
481(Av en lustig händelse heter alla personerna Sten. Så det kan bli.)
Jämför med samma data, men nu sorterade i en annan ordning, så att vi kan använda ett primärindex. Nu blir det bara en enda nivå i indexet:
Hashtabeller har ju den nackdelen att de kan bli överfulla, och då kan deras prestanda försämras kraftigt. Om man vet i förväg hur mycket data som ska lagras, kan man anpassa storleken, men det vore också bra om hashtabellen kunde växa i storlek när man stoppar in mer data. Därför har det utvecklats flera metoder för att göra detta:
Alla tre metoderna arbetar genom att hashfunktionen beräknar ett värde med ganska många bitar, en så kallad pseudonyckel. Hashfunktionen ger alltså inte en tabellposition direkt.
I dynamisk hashning, som alltså inte betyder hashning med dynamiskt växande tabell i allmänhet, utan är en specifik algoritm, tittar man på bitmönstret i den uträknade pseudonyckeln. Posterna hamnar i olika block beroende på vilka bitar pseudonyckeln börjar med, och man bygger ett träd för att hitta de olika blocken. Exempelvis ligger alla poster med en pseudonyckel som börjar på 0 i det översta blocket i figuren, och alla poster med en pseudonyckel som börjar på 10 i block nummer två ovanifrån. Man hittar till rätt block med hjälp av ett binärt "0/1-träd":
När ett block blir fullt delar man på det, och delar upp posterna på de två nya blocken, beroende på nästa bit i pseudonyckeln. I nästa bild har 10-blocket delats upp i två. Alla poster med en pseudonyckel som börjar på 100 placeras i det ena blocket, och alla poster med en pseudonyckel som börjar på 101 placeras i det andra blocket:
483En nackdel med dynamisk hashning är att man gåste gå via 0/1trädet för att hitta till rätt block, och det är alltså inte bara att direkt läsa in rätt block som med en vanlig hashtabell. Om tabellen är stor, blir trädet djupt. Man behöver kanske flera diskläsningar, och det innebär alltså att uppslagningar blir långsammare. Då försvinner hashtabellens stora fördel gentemot B+-träd.
Extendible hashing (som inte har något bra svenskt namn) är egentligen samma sak som dynamisk hashning, men i stället för att indexet för att hitta blocken lagras som ett träd, använder man en tabell. Notera att eftersom det finns ett enda block för alla pseudonycklar som börjar på 0, pekar både 00- och 01-posterna på det blocket.
När ett block blir fullt delar man på det, på samma sätt som i dynamisk hashning. Kanske måste man också bygga om tabellen till att använda sig av fler bitar:
484Linjär hashning är en mycket bättre metod, och det är den som brukar användas numera för att göra hashtabeller som växer automatiskt. Man slipper det särskilda indexet för att hitta datablocken.
Principen bakom linjär hashning är att hashtabellen automatiskt organiseras om när den blir för full, och växer så att den blir dubbelt så stor. Varje position i tabellen motsvaras av två nya, och posterna i varje gammalt block kommer alltså att delas upp på de två nya blocken. Men, och det är det smarta med linjär hashning, man organiserar inte om hela tabellen på en gång, utan en position i taget!
Här har vi en hashtabell. Antalet positioner i tabellen, M, är 5. Överfulla block hanteras som vanligt i diskbaserade hashtabeller, genom att ett eller flera spillblock allokeras.
För att hitta rätt position i tabellen för ett visst värde, beräknar vi först pseudonyckelns värde, K, som är en funktion av den riktiga nyckeln, k. Sen ges rätt position av hashfunktionen h(K) = K mod M.
När hashtabellen börjar bli för full, delar vi på det första blocket:
485Den grå skuggningen ska visa elementen från det gamla blocket på position noll, som nu delats upp på position noll och position fem. Om även nästa block, det på position ett, delas upp i två, ser tabellen ut så här:
Förutom tabellstorleken, M, måste vi också hålla reda på hur många block vi delat upp, n. Nu är n lika med 2. För att hitta rätt position i tabellen för ett visst värde, beräknar vi först pseudonyckelns värde, K. Sen beräknar vi ett initialt hashvärde, h 0 (K) = K mod M. Om h 0 (K) < n, betyder det att blocket på position h 0 (K) är uppdelat. Då använder vi i stället hashfunktionen h 1 (K) = K mod 2M.
Exempel: En post har nyckeln Bengt. Den körs genom hashfunktionen, som genererar pseudonyckeln 1619824738. 1619824738 mod 5 är 3, och alltså hör posten hemma i position 3 i hashtabellen.
Exempel: En annan post har nyckeln Sten-Henrik. Den körs genom hashfunktionen, som genererar pseudonyckeln 2013847096. 2013847096 mod 5 är 1. Men 1 är mindre än 2, och alltså handlar det om en position i tabellen som blivit uppdelad i två. För att hitta den 486 rätta beräknar vi 2013847096 mod 10, som är 6. Posten hör alltså hemma i position 6 i hashtabellen.
Som kanske framgår av figurerna är linjär hashning inte en metod för att hantera enskilda överfulla block. I stället reglerar man hur fulla blocken är i genomsnitt. Man kan definiera en fyllnadsgrad (engelska: file load factor) för hela hashtabellen, som anger hur stor del av platserna för poster (förutom i spillblocken) som är upptagna. Om r är antalet poster i filen, bfr är blockningsfaktorn, dvs antalet poster som får plats i ett block, och N är antalet block i hashfilen (dvs M + n, ges fyllnadsgraden l av formeln l = r / (bfr * N). Fyllnadsgraden bör åtminstone hållas under 1. Genom att dela upp block i hashtabellen när fyllnadsgraden når över en viss nivå, och slå ihop block om man tar bort poster så att fyllnadsgraden går under en annan nivå, kan man hålla fyllnadsgraden inom ett visst intervall, till exempel mellan 0.8 och 0.9. En linjär hashtabell får därför jämna prestanda, oberoende av hur mycket data som läggs in.
En linjär hashtabell har inget särskilt index som man behöver hålla reda på, utan de enda data man behöver förutom själva tabellen är tabellstorleken M och antalet uppdelade block n. Frånvaron av ett index gör att linjära hashtabeller lämpar sig för distribuerade databaser, där positionerna i tabellen inte utgörs av diskblock utan av hela datorer i ett datornät. Då behövs det ingen central dator som håller reda på var data hamnat (och som skulle bli en "hotspot" 6 i systemet), utan det räcker att alla inblandade känner till vilka datorer som finns och deras "positionsnummer", och så talen M och n.
I det här kapitlet har vi koncentrerat oss på diskbaserade databaser, men det finns också databashanterare som lagrar hela databasen i primärminnet. I en diskbaserad databas är det mycket stor skillnad mellan hur snabbt man kommer åt data från disk jämfört med data som råkar ligga i primärminnet. Det kan vara miljontals gånger snabbare att hitta data i primärminnet än att hämta data till primärminnet från disk. De lagringsstrukturer som en databashanterare använder är optimerade för att ta hänsyn till den stora skillnaden i åtkomsthastighet mellan primär- och sekundärminne.
487Emellertid har moderna datorarkitekturer betydligt snabbare minnesåtkomst för data som ligger i processorns cacheminne än data i resten av primärminnet, fast här rör det sig "bara" om hundratals gånger snabbare. Detta har resulterat i att liknande lagringsstrukturer och implementeringstekniker som är utvecklade för diskbaserade databassystem numera också bör användas i primärminnesdatabaser. En skillnad är att blockstorleken är betydligt mindre för primärminnesän för diskdatabaser.
Moderna primärminnesdatabaser är ofta distribuerade, dvs databasen är lagrad på många primärminnen i kluster eller datacenter med snabb kommunikation mellan dem. Kombinationen av primärminne och snabb kommunikation ger mycket skalbara och snabba databaser.
De index vi har sett hittills har varit fysiska index, som innehåller fysiska pekare, dvs minnesadresser. För diskbaserade databaser brukar det vara adressen till ett diskblock, och för primärminnesdatabaser en position i primärminnet. Det betyder att om posten flyttas fysiskt, dvs till ett annat diskblock eller till en annan position i minnet, måste indexet uppdateras.
Därför kan det ibland löna sig att använda ett logiskt index, som inte innehåller fysiska pekare. I stället innehåller indexet värdet på postens primärnyckel, som huvudfilen är organiserad efter. Därefter krävs ytterligare en uppslagning, med hjälp av primärnyckeln, för att hitta den sökta posten.
488
George
|
7
|
Hjalmar
|
1
|
Hulda
|
10
|
Labolina
|
15
|
Sam
|
20
|
Tony
|
2
|
1
|
Hjalmar
|
14000
|
1
|
2
|
Tony
|
14000
|
1
|
7
|
George
|
29000
|
2
|
10
|
Hulda
|
14000
|
3
|
15
|
Labolina
|
22000
|
3
|
20
|
Sam
|
22000
|
2
|
Om vi har få olika värden i ett fält, fungerar de typer av index vi sett hittills dåligt. Ett exempel på det är en tabell med en kolumn kön, som kan innehålla värdet man eller kvinna. Om man använder en hashtabell, antingen som huvudfilens organisation eller som ett sekundärindex, kommer hälften av alla poster att hamna i en position i tabellen, och den andra hälften hamnar i en annan position. Resten av tabellpositionerna är tomma. Oberoende av hur stor man gör hashtabellen, blir alla positioner utom dessa två tomma. Ett sekundärindex i form av ett B+-träd fungerar inte heller särskilt bra. Ett B+-träds-primärindex fungerar lite bättre, men man kan bara ha ett enda primärindex, och fältet kön är inte det bästa att sortera dataposterna efter.
I sådana situationer kan man använda ett bitindex (på engelska bitmapped index). För varje värde i fältet man vill indexera skapar man en area som innehåller en bit för varje post i huvudfilen. För de poster som har det värdet i fältet sätter man den biten till 1, och för de poster som inte har det värdet i fältet sätter man den till 0. En fil med en miljon poster och ett kön-fält kommer alltså att behöva två gånger en miljon bitar, dvs drygt 244 kilobyte, för att lagra ett bitindex för det fältet.
Alla index vi sett hittills har varit endimensionella, dvs de har arbetat med ett fält i posterna, och de har använts för att snabbt hitta en eller flera poster med ett visst värde, eller ett intervall av värden, i det fältet. Men vad händer om vi vill hitta poster baserat på värden 489 i flera kolumner? Det kan handla om kartor där vi vill kunna hitta vad som finns på en viss longitud och latitud. Det kan också vara till exempel en tabell med personer där vi vill hitta personer med ett visst namn som bor på en viss adress.
Antag att vi har en tabell som heter Personer:
Personer | ||
---|---|---|
Pnr | Namn | Hemort |
631211-1658 | Thomas | Örebro |
631211-1758 | Bengt | Stöde |
... | ... | ... |
Nu vill vi ha tag på alla personer som heter Bengt och som bor i Säffle. Vi ställer följande SQL-fråga:
select *
from Personer
where Namn='Bengt' and Hemort='Säffle';
Om vi har ett index på kombinationen av Namn och Hemort, skapad med kommandot
create index NamnStadIndex on Personer(Namn, Hemort);
så kan databashanteraren använda det indexet i frågan. Det kommer att fungera likadant som om vi hade slagit ihop de två kolumnerna till en, och skapat ett index på den kolumnen.
Men vad händer om vi i stället har två olika index?
create index NamnIndex on Personer(Namn);
create index StadIndex on Personer(Hemort);
I databasen kommer det att se ut så här med två index:
490Vi kan använda det ena indexet för att hitta alla block som innehåller Bengt-poster, eller så kan vi använda det andra indexet för att hitta alla block som innehåller Säffle-poster. Vi kan också, med lite mer arbete, beräkna snittet av blockpekare och på så sätt använda båda indexen för att hitta alla block som innehåller minst en Bengtpost och minst en Säffle-post. Men det finns inget sätt att hitta alla de poster som innehåller både Bengt och Säffle.
Man måste använda ett specialiserat tvådimensionellt index. En variant på det är en så kallad gridfil. (En svensk översättning skulle kunna vara rutnätsfil.) I varje ruta finns en kombination av ett namn och en stad. Man kan också använda intervall, och till exempel ha en kolumn för alla städer i bokstavsordning mellan Abisko och Boden. Eftersom kombinationen av namn och hemort inte är en nyckel, har vi använt ett mellansteg i form av ett diskblock eller en del av ett diskblock, där vi lagrar själva pekarna till datablocken. Om värdena är ojämnt fördelade kan flera rutor i rutnätet peka på samma diskblock, som alltså innehåller blockpekare för flera olika värdekombinationer.
491Det där handlade om enkla tvåkolumnsfrågor, med namn och adresser. Men den viktigaste användningen av flerdimensionella index i databaser är nog lagring av kartor. En karta innehåller objekt, till exempel städer, som finns på en viss position med en x-koordinat och en y-koordinat. En del av objekten, som vägar och sjöar, har dessutom utsträckning. Man kan också tänka sig en höjdangivelse, vilket ger en tredje dimension, eller ännu fler dimensioner. För enkelhets skull begränsar vi oss till två dimensioner här.
Varför lagrar man inte bara kartorna som bilder, till exempel i JPEG- eller GIF-format? Det kan man förstås göra, men för det första har man då ingen möjlighet att enkelt söka efter saker som finns i databasen, som till exempel städer med ett visst namn eller städer som finns inom ett visst område. För det andra kommer det att fungera dåligt att zooma in på ett ställe i kartan. Titta här vad som händer när vi har en karta i form av en bild, och zoomar in för att se fler detaljer i Moskva:
492I stället ska saker på kartan, som städer och vägar, försvinna och dyka upp beroende på hur mycket man zoomar, och då kan kartan inte lagras som en (enda) bild. Kartan är inte lagrad som en bild alls. I stället har man lagrat alla de olika sakerna i kartan (städer, vägar, sjöar och så vidare), och sen ritas de upp som en bild precis när kartan ska visas. Beroende på vilken förstoring man valt, tas bara vissa saker med. Ju mindre vägar och städer, desto större förstoring krävs för att de ska komma med.
Ett quad-träd (som kanske skulle kunna kallas kvadrant-träd på svenska) är ett träd av ordning fyra, dvs med upp till fyra underträd under varje nod. En nod delar upp kartans yta i fyra delar, och i varje underträd lagras alla objekt i den fjärdedelen av kartan. I figuren nedan kan vi se hur ytan delas upp i fyra delar av de streckade linjerna. Eftersom det finns mer än ett enda objekt i en av fjärdedelarna, delar vi upp den fjärdedelen ännu en gång, med de prickade linjerna.
493Om objekten är ojämnt utplacerade, kan ett quad-träd komma att innehålla många tomma platser. En datastruktur som ibland är bättre är k-d-träd. Ett k-d-träd fungerar som ett binärt träd, men i stället för att som ett vanligt binärt träd dela upp en enda dimension i finare och finare delar, delar varannan nivå i k-d-trädet upp en yta i x-led, och varannan nivå delar upp ytan i y-led.
I figuren nedan delar den första noden i trädet upp kartans yta enligt den streckade linjen, i en vänsterhalva och en högerhalva. De två noderna på nästa nivå i trädet delar upp varje halva enligt de prickade linjerna, i en nedre och en övre halva. På den tredje nivån är det återigen dags att dela upp ytan i x-led, enligt den streckprickade linjen.
Binära k-d-träd blir, precis som vanliga binära träd, djupa, och lämpar sig därför inte så bra i diskbaserade databaser. Det är också komplicerat att hålla dem balanserade. En variant, k-d-B-träd, är ett B-träd som, precis som de binära k-d-träden, omväxlande delar in en yta i x-led och i y-led. Eftersom ett B-träd kan ha mycket högre ordning än ett binärt träd, delar varje nivå av k-d-B-trädet in kartytan i många fler delar än två.
De flerdimensionella index vi sett hittills arbetar med punkter på kartan, och kan därför passa bra för bergstoppar och liknande. Men objekt med utsträckning, som sjöar och länder, är svårare att hantera. För det finns en annan datastruktur, r-träd, där "r" står för "rektangel". Ett r-träd påminner om ett B-träd, men i stället för att som ett B-träd dela upp en enda dimension i mindre och mindre segment, delar det upp en yta i mindre och mindre rektanglar.
För att illustrera hur några vanliga datastrukturer fungerar, ska vi räkna ut hur lång tid det tar att ställa några enkla SQL-frågor i en databas med en tabell som innehåller Sveriges befolkning. Vi lägger alltså in 10 miljoner rader i tabellen Personer:
Personer | ||
---|---|---|
Personnummer | Namn | Folkbokföringsort |
770118-1220 | Lotta Larsson | Säffle |
140211-1491 | Gottfrid Svensson | Borlänge |
631211-1658 | Thomas Padron-McCarthy | Örebro |
... | ... | ... |
Vi söker efter en person med ett visst personnummer, med hjälp av den här SQL-frågan:
select * from Personer where Personnummer = '631211-1658'
Hur lång tid tar den sökningen om tabellen Personer, som innehåller alla svenskar, är lagrad som
För att kunna svara på frågan måste vi veta en del om den dator och den databashanterare som används. Vi börjar med att anta att datorn använder en mekanisk hårddisk, och att storleken på ett logiskt diskblock är 8 kilobyte, dvs 8 192 byte. Det gäller både för data- och indexblock. Att läsa ett godtyckligt block tar i genomsnitt 5 millisekunder. Om man läser block som ligger i följd på disken, kan man överföra 100 megabyte per sekund, vilket motsvarar en blockläsningstid på ungefär 0.1 millisekunder (8 kilobyte delat med 100 megabyte per sekund).
Vi antar också att fältet Personnummer är ett char
- eller varchar
fält som är 11 tecken brett.
Fältet Namn är 40 tecken brett, och fältet Folkbokföringsort är 19 tecken.
En datapost är alltså 70 tecken.
Blockningsfaktorn bfr för datablocken blir 8 192 / 70, dvs 117.
Det går alltså in 117 dataposter i ett datablock.
Först beräknar vi tiden i heap-fallet. Tabellen innehåller 10 miljoner poster, vilket betyder att huvudfilen upptar 85 471 block. Om vi antar att det sökta personnumret finns i tabellen, och vi vet att personnummerfältet är en nyckel, räcker det i genomsnitt med att läsa hälften av blocken, 42 735 stycken. Om de ligger i följd på disken tar det 0.1 ms att läsa varje block, vilket ger en genomsnittlig tid för frågan på cirka 4 sekunder. (Men i värsta fall tar det 8 sekunder.)
Om tabellen är lagrad som en hashfil, organiserad på fältet Personnummer, kan vi hitta en post med ett visst värde på Personnummer genom att beräkna hashfunktionen för det givna personnumret, och värdet på hashfunktionen är numret på den position i hashtabellen där posten finns. Vi läser in det diskblock i tabellen som ligger på den positionen, vilket tar i genomsnitt 5 millisekunder, dvs en halv hundradels sekund, så hela frågan tar alltså så lång tid att köra. (Men om vi har många spillblock i den tabellpositionen, antingen beroende på att hashtabellen är överfull eller på att vi bara haft otur med fördelningen av hashvärden, kan det ta mycket längre tid.)
För att beräkna tiden i fallet med B+-trädet måste vi beräkna trädets ordning, för det varierar beroende på storleken på det fält som vi indexerar på. Om ett personnummer tar upp 11 byte, och en diskblockspekare 4 byte, blir ordningen på B-trädet (8 192 – 4) / (4 + 11) + 1, dvs 546. Men ett B-träd byggs dynamiskt: när en nod är full delas den upp i två (som till en början bara är halvfulla), och därför 496 är noderna bara i genomsnitt två tredjedels fulla. Den "effektiva ordningen" av B-trädet, som man bör använda när man räknar ut hur många nivåer som behövs, blir därför 364. Vi räknar alltså med att varje indexnod innehåller pekare till 364 indexnoder, eller datablock.
Blockningsfaktorn för datablocken är ju, som vi räknade ut ovan, 117. Det finns plats för 117 dataposter i ett datablock. Men om huvudfilen, dvs datablocken, har byggts upp med hjälp av B+-trädsindexet, så att vi använt indexet för att bestämma i vilket datablock en post ska placeras, gäller samma resonemang som för indexblocken.
Datablocken delas också i två när de blir fulla, och kommer därför i genomsnitt att vara två tredjedels fulla. Den "effektiva blockningsfaktorn" blir två tredjedelar av 117, dvs 78. Alltså har vi 128 205 datablock.
Den effektiva ordningen på trädet är 364, så för att peka ut de 128 205 datablocken behövs 352 indexblock på den lägsta indexnivån. På nästa nivå i indexet räcker det med ett enda block för att peka ut de 352 blocken, och det blocket blir indexets rotnod. Indexet får alltså bara två nivåer, och för att hitta en datapost baserat på dess personnummer behövs det tre läsningar från disken: två indexblock och sen det utpekade datablocket. Eftersom läsning av ett block tar i genomsnitt 5 millisekunder, kan vi räkna med att hela SQL-frågan tar 15 millisekunder att köra, dvs 0.015 sekunder, eller en och en halv hundradel av en sekund.
Vad händer med söktiderna om vi vill söka på intervall i stället, till exempel för att hitta alla som är födda den 11 december 1963? Det motsvaras av SQL-frågan
497
select *
from Personer
where Personnummer like '631211-%'
select *
from Personer
where Personnummer >= '631211-0000'
and Personnummer <= '631211-9999'
Intervallsökningen ger de här söktiderna:
I det här fallet, med en intervallsökning, var det alltså B+-trädet som var överlägset bäst.
Vad händer med tiderna för sökning efter ett visst personnummer om databasen plötsligt blir tusen gånger större? Tabellen innehåller nu tio miljarder poster i stället för tio miljoner.
498Ja, det gör det, men inte så mycket fortare att man kan sluta bry sig om index och lagringsstrukturer. SSD:er är av storleksordningen hundra gånger snabbare än en mekanisk hårddisk på att läsa ett godtyckligt block, och av storleksordningen tio gånger snabbare på att läsa block i följd. Vi tar den stora databasen ovan, med tio miljarder poster, och söker efter ett visst personnummer i de olika lagringsstrukturerna. Vi behåller de logiska diskblockens storlek på 8 kilobyte. Nu använder vi en SSD, så vi räknar med att det tar i genomsnitt 0.1 millisekunder att läsa ett godtyckligt block, och 0.01 millisekunder per block vid sekventiell läsning.
Vi har beskrivit ovan hur tiden för att söka i olika typer av filer ändras när filens storlek ändras. Till exempel blir sökning i en osorterad fil tusen gånger långsammare när filen blir tusen gånger större, men tiden för sökning i en hashtabell påverkas inte alls när den blir tusen gånger större.
För att hantera den här typen av mått brukar man använda ett skrivsätt med O (uttalas ordo). Man säger att tidskomplexiteten för sökning i en osorterad fil är O(n) (uttalas ordo n eller stora ordo av n) där n är antalet poster i filen. Det betyder (ungefär) att söktiden är proportionell mot n.
Sökning i en hashtabell har tidskomplexiteten O(1), vilket betyder att den inte alls beror på antalet poster n. Sökning i ett B+-träd har tidskomplexiteten O(log n).
Att en algoritm har en viss tidskomplexitet, låt oss säga O(n2), betyder inte att man kan räkna ut tiden exakt, till exempel så att med n = 7 tar algoritmen 49 millisekunder (eller vad man nu mäter i för enhet) och med n = 100 tar den 10 000 millisekunder. I stället är det ett mer ungefärligt mått. (Egentligen säger definitionen av O att om funktionen f (n) är O(n2), är kvoten f (n)/n2 begränsad för stora n. Läs mer i någon lämplig matematikbok.)
Fält (engelska: field. En plats där man kan lagra ett enkelt värde.
Post (engelska: record). Flera fält som lagrats tillsammans, efter varandra. En rad i en tabell i en relationsdatabas kan representeras som en post.
500Fil (engelska: file). En följd av poster som lagrats, normalt på en hårddisk. En fil i den här betydelsen kan, men måste inte, motsvaras av en fil i operativsystemet.
Diskblock På en mekanisk hårddisk eller en SSD kan man inte läsa eller skriva enskilda byte eller poster, utan man arbetar alltid med hela diskblock. Hårdvaran arbetar med fysiska diskblock, som brukar vara 512 byte stora på mekaniska hårddiskar, men större, till exempel 2 kilobyte, på SSD:er. Operativsystem och databashanterare brukar gruppera ihop flera fysiska diskblock till ett större logiskt diskblock.
Blockningsfaktor (engelska: blocking factor). Antalet poster som får plats i ett diskblock.
Spår (engelska: track). Diskblocken på hårddisken är placerade längs ett antal koncentriska, cirkulära spår.
Nyckel (engelska: key). Ett fält eller en kombination av fält med unika värden, det vill säga att två poster inte kan ha likadana värden i fälten.
Primärnyckel (engelska: primary key). En nyckel som man dessutom har sorterat filen efter. För att skilja denna typ av primärnyckel från den primärnyckel som förekommer i relationsmodellen, talar man ibland om sorteringsnyckel eller fysisk primärnyckel.
Sorteringsfält (engelska: ordering field). Ett fält (eller en kombination av fält) som man har sorterat filen efter.
Huvudfil eller datafil (engelska: main file or master file). Filen med dataposterna. Det kan finnas ytterligare filer, till exempel filer med indexposter.
Osorterad fil eller heap. En fil där posterna inte har någon särskild ordning. Typiskt lägger man helt enkelt till nya poster sist i filen.
Sorterad fil. En fil där posterna sorterats enligt ett sorteringsfält.
Hashtabell. En typ av lagringsstruktur där det går mycket snabbt att hitta en post med ett visst värde på det fält man söker på. En hashtabell kan lagras i primärminnet (så kallad intern hashning) eller som en fil på hårddisken (så kallad extern hashning).
Bucket (betyder hink, men brukar inte översättas). En position i en hashtabell eller liknande, där det får plats flera poster. I hashfiler brukar en bucket utgöras av ett helt diskblock.
501Overflow-block eller spill-block. Om fler poster hashas till en viss position i en hashtabell än vad som får plats, kan de överskjutande posterna lagras i ett spill-block.
Index (engelska: index). En datastruktur som innehåller referenser till innehållet i huvudfilen. Index används internt i en databashanterare för att hitta data snabbare än genom sökning i hela datamängden.
Indexpost. Precis som huvudfilen är indexfilerna organiserade som en följd av poster, och en sådan post kallas indexpost.
Indexfil. Posterna i ett index lagras oftast i en följd, en så kallad indexfil.
Indexeringsfält (engelska: indexing field). Om det finns ett index som pekar in i huvudfilen, och som är sorterat eller på annat sätt organiserat efter ett visst fält (eller en kombination av fält) i huvudfilen, kallas detta (eller dessa) fält för indexeringsfält.
Primärindex (engelska: primary index). Ett index som är sorterat i samma ordning som primärnyckeln i huvudfilen. Indexeringsfältet är alltså unikt.
Sekundärindex (på engelska secondary index). Ett index som är sorterat i en annan ordning än huvudfilen.
Tätt index (på engelska dense index). Ett index där varje datapost motsvaras av en indexpost.
Glest index (på engelska sparse index eller non-dense index). Ett index där varje datapost inte behöver motsvaras av en indexpost.
Hashindex. Ett index där indexfilen är organiserad som en hashtabell.
B+-träd En trädstruktur där trädet är balanserat och själva dataposterna ligger i löven.
Dynamisk hashning. En metod för att låta en hashtabell växa och krympa dynamiskt beroende på hur mycket data den innehåller.
502Diskbaserad databas. En databas där alla data lagras i sekundärminne, normalt bestående av en eller flera hårddiskar.
Primärminnesdatabas. En databas där alla data lagras i primärminnet.
Fysiskt index. Ett index som refererar till fysiska positioner: antingen diskblock eller platser i primärminnet.
Logiskt index. Ett index som inte refererar till fysiska positioner, utan använder värden på ett eller flera fält för att referera till poster i huvudfilen.
Bitindex (engelska: bitmapped index). Ett index där varje post i huvudfilen motsvaras av en bit i en särskild area.
Flerdimensionellt index. Ett index som möjliggör sökning på flera olika fält, till exempel x- och y-koordinater i en karta.
I räkneexemplet i avsnitt 23.28 antog vi att ett logiskt diskblock var 8 kilobyte, dvs 8 192 byte. Med samma antaganden som tidigare, och 10 miljoner poster i tabellen, hur lång tar det att söka efter en person med ett visst personnummer, om storleken på logiska diskblock i stället är 1 kilobyte, dvs 1 024 byte, när tabellen är lagrad som
Välj själv om du vill använda en SSD eller en gammaldags mekanisk hårddisk.
Vi skapar en tabell med det här SQL-kommandot:
create table Telefonsamtal
(pid integer primary key,
sid integer,
starttid timestamp,
sluttid timestamp,
uppringarnummer char(10),
uppringtnummer char(10),
kommentar char(100));
Databashanteraren skapar automatiskt ett primärindex i form av ett B+-träd på primärnyckeln pid. Det finns en miljard (109) rader i tabellen. Gör lämpliga antaganden om blockstorlek med mera, bestäm om du vill använda en SSD eller en gammaldags mekanisk hårddisk, och räkna ut hur lång tid följande två frågor tar att köra:
select * from Telefonsamtal where pid = 99181888;
select * from Telefonsamtal where sid = 21001999;
Kan man förvänta sig en skillnad mellan de två frågornas tider, och i så fall varför?
Vi skapar ett index på kolumnen sid i tabellen från förra övningen:
create index T2 on Telefonsamtal(sid);
Indexet är ett sekundärindex i form av ett B+-träd. Hur lång tid tar det nu att köra de två frågorna i övning 2?
Kan man förvänta sig en skillnad mellan de två frågornas tider, och i så fall varför?
Antalet telefonsamtal växer till en biljon (1012). Hur lång tid tar det nu att köra samma två frågor?
3 En partition är en del av disken som kan formateras separat. Om man har en Windows-dator med en enda hårddisk, och den hårddisken är uppdelad i två partitioner, brukar de synas som C: och D:.
5 Det kan dock, som alltid med alla diskbaserade datastrukturer, tillkomma en eller flera ytterligare läsningar från disken för att hämta meta-data, till exempel om var det där sista diskblocket finns. Å andra sidan kan den sortens metainformation sen finnas kvar i primärminnet, så att senare läsningar och skrivningar inte kräver extra diskaccesser.
När man arbetar med en databas, i synnerhet när man gör ändringar i databasen, är det ofta så att en följd av operationer hör ihop som en enhet. Ett exempel kan vara att flytta pengar från ett bankkonto till ett annat. Då drar man först bort beloppet från det ena kontot, och adderar det sen till det andra kontot. En sådan följd av operationer som hör ihop kallas en transaktion.
De flesta databashanterare har inbyggda mekanismer för att underlätta transaktioner. Man kan, med särskilda kommandon, tala om för databashanteraren att en viss följd av operationer hör ihop och utgör en transaktion.
Det här kapitlet tar upp grunderna om transaktioner. Hur transaktioner hanteras inuti databashanteraren tas upp i kapitel 25.
Man brukar tala om ACID-transaktioner. Bokstäverna i ACID står för fyra egenskaper som transaktioner bör ha, och som man vill att databashanteraren automatiskt ska garantera:
De flesta vanliga relationsdatabashanterarna har den här transaktionshanteringen inbyggd.
I databassammanhang brukar konsistens betyda att alla integritetsvillkor ska vara uppfyllda. Alla data i databasen ska alltså följa integritetsvillkoren. Något annat skulle vara just en inre motsägelse, eftersom integritetsvillkoren i så fall skulle säga en sak, och de data som bryter mot dem skulle säga en annan.
Man brukar också räkna in databasens interna datastrukturer i konsistensen. Till exempel måste ett index stämma överens med den riktiga tabell som det pekar in i. Om man lägger till eller tar bort rader i tabellen, och indexet av någon anledning inte ändras för att reflektera detta, så kan databashanteraren kanske bli så förvirrad att den kraschar. Det är en särskilt allvarlig form av inkonsistens, eftersom den kan göra att det man inte kommer åt några data alls i databasen.
När en transaktion har påbörjats finns det flera sätt som den kan avslutas på:
Commit
1
betyder att användaren, vare sig den "användaren" är en person som sitter och skriver SQL-kommandon eller ett program som arbetar med databasen, anser sig vara färdig, och talar om det för databashanteraren.
I SQL kan man använda kommandot commit.
Databashanteraren måste nu se till att alla ändringar verkligen sparas ordentligt (tänk på D:et, "hållbarhet", i ACID) och att databasen är konsistent (C:et, "konsistensbevarande", i ACID).
Rollback (även kallat "abort") betyder att användaren (som fortfarande kan vara en människa eller ett program) har ångrat sig och vill avbryta transaktionen.
Det kan till exempel bero på att användaren upptäckt att det inte finns tillräckligt med pengar på bankkontot för att göra den där överföringen.
I SQL kan man använda kommandot rollback.
Om det gjorts några ändringar i databasen, måste databashanteraren nu ändra tillbaka till hur det såg ut före transaktionen (A:et, "atomicitet", i ACID).
Att ändra tillbaka kallas också att "rulla tillbaka" transaktionen.
Notera att databashanteraren själv kan bestämma sig för att avbryta en transaktion. Antag till exempel att användaren vill "committa" 2 en transaktion, och databashanteraren då upptäcker att användaren gjort ändringar i databasen som strider mot integritetsvillkoren, och som därför inte är tillåtna. Då kan databashanteraren behöva avbryta transaktionen och rulla tillbaka alla ändringar som gjorts.
SQL är som default oftast inställt på "auto-commit", vilket betyder att en automatisk commit görs efter varje SQL-kommando.
Det innebär att varje SQL-kommando bildar en egen transaktion.
I ett interaktivt SQL-gränssnitt kan kommandot start transaction
ofta användas för att påbörja en transaktion som omfattar flera kommandon.
Transaktionen kan också avbrytas av en krasch av något slag, till exempel om strömmen går eller datorn kraschar. När man sen startar datorn och databashanteraren igen, ser databasen som finns på disken ut precis som den såg ut i kraschögonblicket. Den kan alltså innehålla en eller flera halvfärdiga transaktioner, som höll på att köras när kraschen inträffade. Det kan dels vara sådana transaktioner som avbröts mitt i, men som hann med att göra en del av sina ändringar i databasen, och det kan också vara transaktioner som egentligen är avslutade, men där alla ändringarna inte hann skrivas på disken. Detta strider mot både A:et (atomicitet) och D:et (hållbarhet) i ACID. För att råda bot på detta genomför databashanteraren en återhämtning (recovery på engelska).
Under återhämtningen måste alla halvfärdiga transaktioner åtgärdas:
På det här viset kan databashanteraren alltså klara att man drar ur strömsladden till datorn, utan att några data går förlorade. Det går förstås att göra samma sak själv när man skriver ett program (för databashanteraren är ju ett program den också), men det är ganska krångligt att få det rätt.
Databashanteraren måste alltså kunna ändra tillbaka saker som ändrats i databasen. Det går att göra på flera sätt, men det vanligaste är att man använder en särskild loggfil. En loggbok ombord på ett skepp är ju en bok där man skriver upp allt som händer, och en loggfil fungerar på liknande sätt. Varje transaktion som vill göra en ändring i databasen skriver först en notering om det i loggfilen. Där står (fast det kan variera lite mellan olika databashanterare):
Databashanteraren börjar med att skriva noteringen i loggfilen, och först därefter görs ändringen i databasen. (Övning: Varför? 4 ) Detta brukar kallas write-ahead logging, ungefär "skriv-i-förväg-loggning".
Loggfilen kallas ibland också journal. Det ordet återfinns också i termen journalfilsystem (engelska journaling file system), även kallat loggande filsystem. Det innebär att operativsystemet använder en loggfil, på liknande sätt som i en databas, för att notera de ändringar som görs i filerna på hårddisken. Det gör att filsystemet blir tåligare mot skador om datorn kraschar, hänger sig eller plötsligt stängs av.
Vi nämnde ovan att databashanteraren helst ska klara av även en diskkrasch utan att tappa bort några transaktioner. Ett sätt att få det att fungera är att regelbundet göra en reservkopia, eller "backup" som det heter på engelska, av databasen.
Det här förutsätter att vi inte har placerat loggfilen och själva databasen på samma hårddisk, för om vi gjort det så blir det förstås 511 svårt. Det finns också ett annat skäl till att ha loggfilen och databasen på olika diskar, nämligen prestanda. Eftersom alla ändringar ska noteras i loggfilen, innebär det att varje ändring i databasen leder till att vi skriver på disken två gånger: en gång för att göra noteringen i loggfilen, och en gång för att göra själva ändringen. Eftersom hårddiskar är mycket långsammare än primärminne brukar det vara just kommunikationen med hårddisken som är flaskhalsen i en databashanterare. Genom att lägga loggfilen och databasen på var sin disk kan man skriva till båda diskarna parallellt. Alltså kan systemet arbeta dubbelt så fort.
Transaktionerna ska hållas isolerade från varandra. Även om databashanteraren utför flera transaktioner samtidigt, får en transaktion aldrig se en annan transaktions halvfärdiga ändringar.
Detta kan databashanteraren åstadkomma på flera sätt, men det vanligaste är att man använder olika typer av lås. När en transaktion vill arbeta med ett dataobjekt i databasen, till exempel behållningen på ett bankkonto, låser transaktionen det dataobjektet. Nu får ingen annan transaktion göra något med dataobjektet. Om en annan transaktion också vill arbeta med dataobjektet, får den vänta tills den första transaktionen är färdig och låser upp låset. Då kan nästa transaktion låsa objektet.
Här ovan har vi tänkt oss att transaktionerna ska vara helt isolerade från varandra, men SQL-standarden definierar flera olika isoleringsnivåer som man kan använda i en transaktion.
Den lägsta (sämsta) isoleringsnivån är read uncommitted,
som låter transaktionen fritt läsa andra transaktioners halvfärdiga data.
Den högsta nivån kallas serializable,
och innebär att transaktionerna är helt isolerade från varandra.
Det är vad SQL-standarden anger, men vad som finns kan variera mellan olika databashanterare, och även vad som är default om man inte anger något.
Transaktion (engelska: transaction). En följd av operationer som hör ihop som en enhet. De flesta databashanterare erbjuder stöd för att gruppera operationer till transaktioner.
ACID, ACID-transaktion, ACID-egenskaper (engelska: ACID, ACID transaction). Transaktioner bör ha ACID-egenskaperna, dvs de ska vara atomära (A), konsistensbevarande (C), isolerade från varandra (I) och hållbara (D). De flesta databashanterare garanterar automatiskt att transaktioner har dessa egenskaper.
Commit (engelska: commit). Ett kommando till databashanteraren som betyder att en transaktion är färdig, och därefter ska finnas lagrad permanent i databasen. Motsatsen till rollback. "Commit" används också om själva arbetet som databashanteraren gör när den avslutar en transaktion och ser till att den finns permanent lagrad.
Rollback (engelska: rollback). Kallas ibland abort. Ett kommando till databashanteraren som betyder att en transaktion ska avbrytas, och att de ändringar som eventuellt hunnit göras i databasen ska ändras tillbaka. Motsatsen till commit.
Återhämtning, eller kanske återställning eller återstart (engelska: recovery). När databashanteraren lagar databasen efter en krasch.
Loggfil (engelska: log file eller log). Många databashanterare noterar alla ändringar som görs i databasen i en särskild fil, loggfilen. Om databashanteraren behöver göra rollback av en transaktion, kan den ta reda på vilka ändringar som gjorts genom att läsa i loggfilen. Loggfilen används även vid återhämtning.
Lås (engelska: lock). Om flera transaktioner arbetar med samma data, kan databashanteraren använda lås för att bara låta en transaktion i taget komma åt dessa data.
Konsistens (engelska: consistency) "Konsistens" betyder ungefär "utan inre motsägelser" eller "följer de uppställda reglerna". Konsistens 513 innebär att om samma uppgift står på två ställen, till exempel vilken lön en viss person har, ska det stå samma lön på båda ställena. I en databas brukar man vara mer specifik, och säga att alla integritetsvillkor ska vara uppfyllda. Ett integritetsvillkor är en begränsning av vilka data som får lagras i databasen. Att databasen är konsistent (engelska: consistent) innebär att alla data uppfyller integritetsvillkoren. (Synonymer till "konsistens" är "konsekvens" och "logisk koherens".)
Inkonsistens (engelska: inconsistency). Inre motsägelse. Om en databas innehåller inkonsistenser är den inkonsistent (engelska: inconsistent), dvs inte konsistent. En inkonsistent databas är en databas som antingen
4 Svar: Om databashanteraren börjar med att skriva i själva databasen, och strömmen sen går precis innan databashanteraren hinner göra noteringen i loggfilen, går det inte att veta att något ändrats i databasen. Det kommer bara att stå ett nytt värde där, till exempel att Lotta har 50 kronor på sitt konto i stället för 100. Men det går inte att veta att detta är ett ändrat värde, eller hur man ska göra för att ändra tillbaka.
Kapitel 24 går igenom vad som menas med en transaktion, och de fyra ACID-egenskaperna som säger att transaktioner bör vara atomära, konsistensbevarande, isolerade från varandra samt hållbara. Där tar vi också upp att commit, rollback, krascher och återstarter måste kunna hanteras. Det här kapitlet handlar om hur databashanteraren internt hanterar transaktioner för att åstadkomma ACID-egenskaperna.
I ett databassystem där det aldrig körs mer än en samtidig transaktion, behöver man inte bry sig om I:et i ACID-egenskaperna: att transaktionerna ska isoleras från varandra. Däremot har man fortfarande behov av atomicitet (A:et), hållbarhet (D:et) och egenskapen att bevara konsistens (C:et). I det här kapitlet kommer vi att börja med de egenskaper som alltid behövs, och behandla isoleringsproblemet sist. Det är också det mest komplicerade, och det som kommer att behandlas grundligast.
För att förstå hur transaktioner hanteras måste vi titta lite på transaktionens ofta korta men, får vi hoppas, meningsfulla liv. En transaktion kan under sin livslängd befinna sig i ett av flera tillstånd, som vi ser i figuren nedan.
Transaktionen påbörjas.
I SQL kan det antingen ske explicit med kommandot start transaction,
eller implicit genom att vi helt enkelt ger ett kommando, till exempel select
eller update,
som läser eller skriver i databasen.
Om man använder en loggfil, skriver databashanteraren en notering i den om att transaktionen har startat.
När transaktionen alltså påbörjats är den aktiv. Den läser och skriver i databasen. Om man använder en loggfil, noteras åtminstone alla skrivoperationer, men ibland också alla läsoperationer, i loggfilen.
Om vi börjar med tillbakarullningen, kan den initieras antingen explicit med rollback
-kommandot eller genom att det uppstått ett fel, till exempel att ett integritetsvillkor som kontrollerades visade sig vara falskt.
Då måste alla ändringar som eventuellt hunnit göras i databasen ändras tillbaka, vilket vi kallar rollback-processen.
Transaktionen är då avbruten.
När den processen är avslutad, övergår transaktionen till tillståndet tillbakarullad.
Inga ändringar som transaktionen gjort finns nu kvar i databasen.
Om vi i stället antar att allt gick bra medan transaktionen var aktiv, avslutas den med "commit".
Commit kan ske antingen explicit med commit
-kommandot eller implicit, till exempel genom att man använder auto-commit, som innebär att varje SQL-kommando blir en egen transaktion.
Nu är transaktionen färdig, ur användarens
517
eller applikationsprogrammets synvinkel, men det återstår en del arbete för databashanteraren, den så kallade commit-processen.
Transaktionen är då delvis committad.
Här ska integritetsvillkor kontrolleras, och i en del metoder för isolering av transaktioner ska man här kontrollera att det inte uppstått några kollisioner mellan den här och andra transaktioner.
Därför kan det även uppstå fel i commit-processen, till exempel om databashanteraren upptäcker att databasens data strider mot något integritetvillkor, och i så fall ska transaktionen rullas tillbaka.
I så fall går man över till rollbackprocessen.
Om commit-processen går bra, går vi över till tillståndet committad. Om databashanteraren använder en loggfil, skriver den i loggfilen att transaktionen är committad, och det är i och med att den noteringen finns i loggfilen som transaktionen räknas som committad. Detta kallas commit-punkten. Därefter ska den vara "hållbar", och dess ändringar får aldrig försvinna ur databasen.
Ett problem som måste hanteras är de buffertar som av prestandaskäl finns mellan databashanteraren och den fysiska hårddisken. När ett program (och databashanteraren är ju ett program) skriver data på disk, sker det inte direkt. De små magnetiserbara områdena på hårddiskens yta, eller flashminnescellerna om man har en SSD, får inte genast nya värden. I stället skrivs data till ett minnesutrymme. Senare, till exempel när det samlats ihop tillräckligt mycket data för att det ska "löna sig" att faktiskt skriva dem, skrivs de på disken.
Det kan finnas buffertar i flera olika nivåer.
Om man programmerar i C, ett språk som ändå brukar betraktas som "hårdvarunära", och skriver data exempelvis med funktionerna fwrite eller fprintf,
kan man råka ut för tre olika buffertar:
stdio
-biblioteket, som funktionerna fwrite
och fprintf ingår i, har en egen buffert för filen man skriver på.
Den går visserligen att stänga av, eller att tömma med ett
518
särskilt funktionsanrop, men default-tillståndet på många system är att när man skriver på en fil på disken samlas det ihop en viss mängd data, till exempel 4 096byte, innan dessa data skickas i väg.
stdio
-biblioteket, skickas de inte direkt till hårddisken, utan det som sker är att operativsystemets skriv-funktion anropas.
På Unix-liknande system heter den operationen write.
Men operativsystemet har egna buffertar, och write
-anropet innebär ingen garanti för att något faktiskt hamnat på hårddisken, utan bara att data flyttas till operativsystemets buffertar och snart ska skrivas på hårddisken.
Det här innebär naturligtvis problem om programmet kraschar, eller om strömmen går. Har data verkligen hunnit skrivas på hårddisken, så att de finns kvar när systemet kommer upp igen, eller hade de bara hunnit fram till någon av de olika buffertarna?
Såväl operativsystem som hårddiskar brukar erbjuda sätt att åtminstone till en del stänga av eller kontrollera buffringen, och vi får väl anta att tillverkarna av databashanterare undviker onödig buffring på programmeringsnivån. Men det innebär ändå att det är svårt att vara säker på när en skrivning på disken verkligen är klar.
I ett databassystem som aldrig kraschar är egenskaperna atomicitet
(A) och hållbarhet (D) ganska lätta att åstadkomma.
Atomicitet innebär att antingen ska alla operationer i transaktionen genomföras, eller inga. Om en transaktion avbryts mitt i, måste man alltså ändra tillbaka alla ändringar som den hunnit göra. Eftersom 519 loggfilen innehåller en lista på alla ändringar, med det gamla värdet på alla data som ändrades, är det bara att gå igenom den, och ändra tillbaka varje ändring.
D– egenskapen, hållbarhet, är ännu mindre något problem. Data skrivs på disken, och så ligger de där.
Problem med atomicitet och hållbarhet blir det däremot när systemet kraschar, antingen det nu är databashanteraren själv som kraschar eller hela datorn, till exempel genom ett strömavbrott. Då krävs att databashanteraren, när den kommer i gång igen efter kraschen, kan återställa databasen på ett sätt som garanterar såväl atomicitet som hållbarhet, även för de transaktioner som den höll på med just vid kraschen.
roll-back,
där alla ändringar i databasen hann ändras tillbaka före kraschen.
roll-back,
där alla ändringar i databasen inte hann ändras tillbaka före kraschen.
Till följd av buffringen, och även den oundvikliga fördröjningen mellan skrivning i loggfilen och skrivning i databasen, går det inte att veta vilka av de skrivoperationer som noterats i loggfilen som faktiskt genomförts i databasen. Därför måste man behandla alla skriv-operationer 520 som om de kanske inte hann genomföras, och göra om dem. Inte heller kan man räkna med att skrivoperationer som står i loggfilen inte hanns med. Därför måste återhämtningsprocessen gå igenom loggfilen och behandla varje transaktion:
En skrivoperation kan alltså komma att utföras flera gånger: en gång under transaktionens vanliga livstid, en gång när systemet återstartas, och kanske ännu fler gånger om strömmen gick en gång till under återhämtningsprocessen, så att återhämtningsprocessen måste köras på nytt! Därför måste de skrivoperationer som noteras på loggfilen vara idempotenta, vilket betyder att de kan utföras en eller flera gånger utan att resultatet ändras. Exempelvis kan man ha en operation sätt datavärdet X till 17, men inte öka datavärdet X med 1.
Återhämtningsprocessen som beskrivs i avsnitt 25.5 ovan behöver inte göras riktigt på det sättet. Den gäller om databashanteraren utför skrivoperationerna i den riktiga databasen, så kallad omedelbar uppdatering. Ett alternativ är att bara notera skrivoperationerna, eller göra dem i en transaktionslokal kopia av data, och sen skriva ändringarna i databasen först när transaktionen har committats, så kallad fördröjd uppdatering.
521Vid fördröjd uppdatering kan inga ocommittade transaktioner ha gjort några ändringar i databasen, och det innebär en fördel för återhämtningsprocessen. Man behöver inte bry sig om de ocommittade transaktioner som finns i loggfilen.
Men den metoden skulle innebära att alla transaktioner som körts sen databashanteraren startades måste behandlas på nytt, och att samtliga skrivoperationer måste göras om. En databashanterare som skriver i databasen för full kapacitet från klockan åtta på morgonen, och sen råkar ut för en krasch efter sju timmar, klockan tre på eftermiddagen, skulle alltså i värsta fall ta sju timmar att starta om. Det är förstås ganska opraktiskt, så för att undvika att hela loggfilen måste gås igenom använder man kontrollpunkter, eller checkpoints. Databashanteraren upphör tillfälligt att starta nya transaktioner, väntar på att alla transaktioner som var i gång ska köras färdigt, ser till att alla data verkligen skrivs ut på disk, och gör sen en checkpoint-notering på loggfilen.
T. nr. 628: Skriver X. Gamla värdet = 1. Nya värdet = 2.
T. nr. 629: Transaktionen startar.
T. nr. 629: Skriver Y. Gamla värdet = 9. Nya värdet = 8.
T. nr. 628: Skriver Z. Nytt dataobjekt. Värde = 2.
T. nr. 403: Skriver T. Gamla värdet = 0. Nya värdet = 4.
T. nr. 629: Skriver Y. Gamla värdet = 8. Nya värdet = 17.
522
När man ser checkpoint-noteringen, vet man att alla transaktioner som står före den i loggfilen är färdiga och finns med i databasen. Alltså behöver återhämtningsprocessen inte bry sig om några transaktioner utom dem som står efter den sista checkpointen i loggfilen.
I databassammanhang brukar konsistens betyda att alla integritetsvillkor ska vara uppfyllda.
Det vanliga är att integritetsvillkoren kontrolleras direkt vid ändringar i databasen.
Enligt SQL-standarden kan man ange att integritetsvillkor, till exempel referensintegritet, ska kontrolleras antingen direkt (immediate
), eller vid transaktionens slut, när användaren eller applikationsprogrammet vill committa transaktionen (deferred
).
Alla databashanterare följer inte den delen av standarden, men ibland finns det alternativa sätt att stänga av den omedelbara kontrollen av integritetsvillkor.
Om det till exempel finns två tabeller, Anställda och Avdelningar, och det dels finns ett integritetsvillkor som säger att varje anställd måste arbeta på en avdelning, dels ett villkor som säger att varje avdelning måste ha en chef, är det svårt att lägga in data i databasen.
Man kan inte lägga in en anställd innan det finns en avdelning där hon kan jobba, och man kan inte lägga in en avdelning innan det finns en anställd som kan vara chef för den.
Om man kan gruppera flera operationer till en transaktion, och sedan kontrollera referensintegriteten vid transaktionens slut, löser sig problemet, men om varje referensintegritetsvillkor kontrolleras omedelbart, blir det förstås svårare.
I MySQL kan man använda kommandot set foreign_key_checks = 0;,
för att tillfälligt stänga av kontrollen av referensintegritet, men en del i andra databashanterare måste man ta bort villkoren och sen lägga tillbaka dem.
Så har vi då kommit fram till hur man hanterar samtidiga transaktioner. Som vi nämnt tidigare måste databashanteraren hindra samtidiga transaktioner från att störa varandra, vilket de kan göra till exempel genom att skriva över varandras ändringar. Databashanteraren måste isolera transaktionerna från varandra genom någon form av samtidighetskontroll (concurrency control på engelska), och resten av det här kapitlet handlar om hur det går till.
Alla dataobjekt lagras i databasen, men för att en transaktion ska kunna studera eller modifiera ett dataobjekt måste det först läsas, dvs kopieras till transaktionens egna, lokala variabler. Om en ändring görs, måste objektet sedan skrivas, dvs kopieras tillbaka till databasen.
För enkelhets skull antar vi att dataobjekten är tal, som 50 och 100, och att de har namn som X och Y. Vi antar också att transaktionens lokala variabler har samma namn som dataobjekten i databasen.
524Operationen Läs(X) innebär att det dataobjekt som finns på platsen som heter X i databasen, kopieras till transaktionens lokala variabel X. (I figuren är det talet 50 som kopieras.) Operationen Skriv(Y) innebär att det dataobjekt som finns lagrat i transaktions lokala variabel Y kopieras till platsen Y i databasen.
Som vi ser i figuren kan det hända att transaktionens lokala variabler inte stämmer överens med vad som faktiskt finns i databasen. Transaktion 1 "tror" att Y har värdet 50, men i den riktiga databasen har Y värdet 100. Det kan bero på att transaktion 1 ändrat på värdet i sin lokala variabel, men det kan också bero på att någon annan transaktion ändrat på Y i databasen någon gång efter det att transaktion 1 läste Y
525Det här är förstås ett något förenklat scenario. I verkligheten läser och skriver databashanteraren sällan enstaka heltal, åtminstone inte i en diskbaserad databas, utan det rör sig snarare om hela diskblock, eller kanske rader i en tabell.
I ett vanligt centraliserat databassystem, där databasen är lagrad i en enda kopia på en eller flera fysiska hårddiskar, och det finns en enda databasserver som hanterar databasen, är det normalt ingen risk att två läs- eller skrivoperationer ska krocka med varandra så att till exempel två skrivoperationer, som Skriv(X) ovan, sker samtidigt och ger ett resultat på disken som är en hopblandning av de två dataobjekt som skulle skrivas. Även om det finns flera transaktioner som vill läsa och skriva i databasen är det ju en enda databashanterare som verkligen utför läsningarna och skrivningarna, och den serialiserar operationerna, dvs utför dem en i taget, efter varandra.
Därmed inte sagt att läs- och skrivoperationerna aldrig kan krocka. Situationen är annorlunda till exempel i en primärminnesdatabas, där alla data ligger i datorns primärminne, och där systemet kör på ett multiprocessorsystem, dvs en dator som har flera fysiska processorer. Då kanske olika transaktioner körs av olika processorer som har gemensam åtkomst av minnet, och då beror det på datorns konstruktion hur stora datamängder som kan skrivas och läsas atomärt i primärminnet, om man inte lägger till ytterligare samtidighetskontroll.
Om transaktionerna inte är tillräckligt isolerade från varandra, och tillåts läsa och skriva data hur som helst, kan det uppstå olika problem. Ungefär ordnade med det allvarligaste först är det dessa problem som vi talar om:
Nedan går vi igenom vart och ett av dem.
Vi ska se ett exempel på hur illa det kan gå om man inte bryr sig om att isolera transaktioner från varandra. Vi tänker oss att Kalle vill ge bort 50 kronor till Lotta. Han går in på bankkontoret med sin femtiolapp. Bankkassören lägger femtiolappen i kassavalvet och kör en transaktion som ökar Lottas konto, L, med 50 kronor:
Ungefär samtidigt kommer Lottas arbetsgivare in på ett annat bankkontor, och betalar Lottas lön:
Transaktion 1 | Transaktion 2 |
---|---|
Läs(L) | |
L = L + 50 | |
Skriv(L) | |
Läs(L) | |
L = L + 10 000 | |
Skriv(L) |
Om operationerna på databasen utförs i den här ordningen, kommer allt att gå bra: först ökas Lottas konto med Kalles femtiolapp, och 527 sen med lönen på 10 000 . Att det går bra är egentligen inte så konstigt, för alla operationerna i Kalles transaktion sker före alla operationerna i löneutbetalningstransaktionen. Transaktionerna körs alltså inte samtidigt utan efter varandra, och det kan inte uppstå några kollisioner.
Men tänk nu vad som händer om operationerna råkar utföras i en lite annan ordning:
Transaktion 1 | Transaktion 2 |
---|---|
Läs(L) | |
L = L + 50 | |
Läs(L) | |
L = L + 10 000 | |
Skriv(L) | |
Skriv(L) |
Vad händer nu? Blir Lotta glad?
Nej, Lotta blir inte glad. Vi tar det steg för steg. Från början har Lotta (till exempel) 100 kronor på sitt konto:
Transaktion 1 startar, och läser behållningen på Lottas konto:
528Transaktion 1 ökar sin lokala kopia av Lottas behållning med 50 kronor:
Därefter startar transaktion 2, och läser behållningen på Lottas konto den också:
529Transaktion 2 ökar sin lokala kopia av Lottas behållning med 10 000 kronor:
Transaktion 2 skriver det nya värdet på Lottas konto, 10 100 kronor, i databasen:
530Det som händer är alltså att Kalles femtiokronorstransaktion skriver över den ändring som tiotusenkronorstransaktionen hade gjort, så att Lottas lön försvinner! Denna sorgliga händelse är exempel på ett generellt problem som brukar kallas för förlorad uppdatering (på engelska lost update). Förlorad uppdatering brukar räknas som 531 den allvarligaste typen fel när transaktioner inte är isolerade från varandra.
Läsning av smutsiga data, eller smutsig läsning, är mer känt under det engelska namnet dirty read. Det går ut på att man läser data som en annan transaktion har ändrat, innan den har hunnit committa sina ändringar. Sådana data brukar kallas smutsiga data (dirty data).
Problemen som kan uppstå beror på att en transaktion kan avbrytas och rullas tillbaka. Om den transaktionen gjort ändringar i databasen, kommer de ändringarna att ändras tillbaka i samband med tillbakarullningen. Men om en annan transaktion hunnit läsa ändringarna, kommer den att basera sitt arbete på data som inte längre finns (och, kan man tolka det, egentligen aldrig har funnits).
Transaktion 1 | Transaktion 2 |
---|---|
Läs(X) | |
X = X + 1 | |
Skriv(X) | |
Läs(X) | |
Y = X*3 | |
Skriv(Y) | |
Rollback |
Transaktion 1 ändrar X, som därefter läses av transaktion 2. Det X-värdet ligger sedan till grund för det värde på ett annat objekt, Y, som transaktion 2 skriver i databasen. Men transaktion 1 avbryts med rollback, och databashanteraren ändrar tillbaka värdet på X till det gamla värdet. Transaktion 2 kommer alltså att ha gjort en ändring av Y som baseras på ett X-värde som inte längre finns.
Transaktion 1 | Transaktion 2 |
---|---|
Läs(X) | |
Läs(X) | |
X = X + 1 | |
Skriv(X) | |
Commit | |
Läs(X) |
Notera att transaktion 2 gjort commit före transaktion 1:s andra läsning. Annars skulle det vara ett fall av smutsig läsning.
Felaktig summa är en variant på ett mer generellt problem som kan kallas inkonsistent analys, och som uppstår när en transaktion läser flera olika data, och en annan transaktion hinner ändra en del av dessa data så att de inte hänger ihop längre. Om en transaktion till exempel försöker summera pengar på bankens alla bankkonton, och en annan transaktion samtidigt flyttar pengar från ett bankkonto till ett annat, kan summan bli fel:
Transaktion 1 | Transaktion 2 |
---|---|
Summan = 0 | |
Läs(X) | |
Summan = Summan + X | |
Läs(X) | |
X=X50 | |
Skriv(X) | |
Läs(Y) | |
Y=Y+ 50 | |
Skriv(Y) | |
Commit | |
Läs(Y) | |
Summan = Summan + Y |
Problemet påminner om oupprepbar läsning, men här är det alltså olika dataobjekt som man läser.
Som vanligt med spökerier har det en naturlig förklaring, och det är förstås att en annan transaktion gör ändringar i databasen. Här är ett exempel, med SQL-kommandon:
Transaktion 1 | Transaktion 2 |
---|---|
select * from T where A = 17; | |
insert into T (A) values (17); | |
select * from T where A = 17; |
Spökposter är luriga att hantera. Om databashanteraren använder lås för kontrollen av samtidighet, räcker det inte att transaktion 1 låser alla data som den jobbar med. De data som transaktion 2 la in i databasen fanns ju inte från början, och därför kunde transaktion 1 förstås inte låsa dem!
För att motverka spökposter räcker det alltså inte med att databashanteraren håller reda på samtidig åtkomst av data, men man kan undvika spökposter genom att även kontrollera åtkomsten av sökvägarna till dessa data. Om databasen använder index, och åtkomsten till data sker via ett index, kan transaktion 1 låsa hela eller delar av indexet, och då kan transaktion 2 inte lägga in sin nya rad förrän transaktion 1 låst upp indexet.
Den ordning som läs- och skrivoperationerna utförs kallas tidsschema, eller (med ett inlånat engelskt ord) schedule. Ett tidsschema kan vara seriellt, vilket betyder att transaktionerna körs en i taget, i serie efter varandra. Annars är det motsatsen, icke-seriellt. I ett icke-seriellt tidsschema har åtminstone någon transaktion åtminstone en operation som körs innan alla operationer från alla tidigare transaktioner är avslutade.
I ett seriellt tidsschema finns inga samtidiga transaktioner, och alltså kan det inte uppstå några samtidighetsproblem. Men även om transaktionerna körs seriellt kan de köras i olika ordning, och det kan ge olika resultat.
534Ta till exempel en banktillämpning, med en transaktion som sätter in 100 kronor på ett bankkonto och en annan transaktion som beräknar och lägger till tre procents ränta på kontot. Med SQL-kommandon:
Transaktion 1 | Transaktion 2 |
---|---|
update Bankkonton set Pengar = Pengar + 100 where Nummer = '46274623'; | |
update Bankkonton set Pengar = Pengar * 1.03 where Nummer = '46274623'; |
Med den här ordningen, där insättningen sker före ränteberäkningen, får vi ränta på den insatta hundralappen. Om transaktionerna däremot körs i omvänd ordning, med insättningen efter ränteberäkningen, får vi ingen ränta på hundralappen, så till slut kommer behållningen på kontot att vara tre kronor mindre än med det andra tidsschemat.
Olika seriella tidsscheman kan alltså ge olika resultat, och vilket resultat som är rätt i just den tillämpningen beror på omständigheter i verkligheten. Hann jag till banken med min hundralapp före stängningsdags, så den kom med i ränteberäkningen? De omständigheterna har dock inget att göra med samtidig exekvering av transaktioner i databashanteraren, och därför räknar vi, från samtidighetssynpunkt, alla seriella tidsscheman som korrekta.
Vi sa i avsnittet ovan att olika seriella tidsscheman kan ge olika resultat, men alla seriella tidsscheman är korrekta. Men även ett icke-seriellt schema kan vara korrekt, om det är ekvivalent med något seriellt schema. (Vi ska strax förklara närmare vad som menas med att två tidsscheman är ekvivalenta, men de ger i alla fall samma slutresultat i databasen.)
Notera att det icke-seriella tidsschemat alltså är korrekt om det är ekvivalent med något seriellt schema. Eftersom två olika seriella scheman för samma transaktioner kan ge olika resultat, och ändå vara korrekta, kan alltså även två olika icke-seriella scheman för samma transaktioner ge olika slutresultat i databasen, och ändå vara korrekta.
535Ett tidsschema som är ekvivalent med något seriellt tidsschema, och alltså korrekt enligt vår definition på korrekthet, kallas serialiserbart. Man skulle ju kunna köra transaktionerna i schemat seriellt, och få samma resultat i databasen.
Man kan tala om flera olika typer av ekvivalens mellan tidsscheman. Det som först kanske känns naturligast är resultat-ekvivalens, som helt enkelt säger att två tidsscheman är ekvivalenta om de ger samma resultat i databasen.
Resultat-ekvivalents är dock inget bra mått, för två tidsscheman kan råka ge samma resultat av slump, och det kan bero på vilka data som man matade in till transaktionerna. Titta till exempel på tidsschemat på sidan 527 som ledde till att Lottas lön slarvades bort. Om det hade råkat vara så att Lottas lön den här månaden var noll kronor, skulle det tidsschemat vara resultatekvivalent med ett seriellt schema. Men med ett annat värde på Lottas lön (till exempel 10 000 kronor), är schemat inte längre resultatekvivalent.
I följande tidsschema är de motstridiga operationerna markerade med fetstil. Dels strider de två läs- och skrivoperationerna på dataobjektet Y mot varandra, och dels strider två skrivoperationerna på dataobjektet Z mot varandra.
536Transaktion 1 | Transaktion 2 | Transaktion 3 | Transaktion 4 |
---|---|---|---|
Läs(X) | |||
Läs(X) | |||
Läs(X) | |||
Läs(Y) | |||
Skriv(Y) | |||
Skriv(Z) | Skriv(Z) | ||
Läs(W) | |||
Skriv(W) | |||
Skriv(W) |
Två tidsscheman är konflikt-ekvivalenta om varje par av operationer som är motstridiga kommer i samma ordning i båda schemana. Dessutom måste operationerna inom varje transaktion komma i samma ordning i båda schemana. Följande tidsschema är konfliktekvivalent med det förra schemat:
Transaktion 1 | Transaktion 2 | Transaktion 3 | Transaktion 4 |
---|---|---|---|
Läs(X) | |||
Skriv(Z) | |||
Läs(X) | |||
Läs(X) | |||
Läs(Y) | |||
Läs(W) | |||
Skriv(W) | |||
Skriv(Y) | |||
Skriv(W) | |||
Skriv(Z) |
Det är egentligen inte så konstigt att motstridiga operationer måste komma i samma ordning:
Två tidsscheman som är konflikt-ekvivalenta ger alltid samma slutresultat i databasen.
Ett tidsschema som är konflikt-ekvivalent med något seriellt tidsschema kallas konflikt-serialiserbart. När vi i fortsättningen talar om serialiserbarhet, menar vi konflikt-serialiserbarhet.
Ett serialiserbart tidsschema garanterar att samtidiga transaktioner inte stör varandra. Å andra sidan kan man få bättre prestanda i ett databassystem om man släpper lite på kravet på serialiserbarhet, och tillåter lite mer interaktion mellan transaktionerna. Då kan man nämligen tillåta mer parallellitet, så att databashanteraren kan göra fler saker samtidigt, och transaktionerna behöver inte vänta lika mycket på varandra.
SQL-standarden definierar fyra isoleringsnivåer, som man kan ange i kommandotset transaction
eller i kommandot start transaction.
Read uncommitted
är den lägsta nivån av isolering.
Den tillåter andra transaktioner att läsa en transaktions ocommittade ändringar.
Det ger mest parallellitet, och det behövs inga lås (om vi antar att databashanteraren använder lås för samtidighetskontroll). Read uncommitted
kan därför ge bäst prestanda, men man kan få alla de problem som vi räknade upp i avsnitt 25.11: smutsiga läsningar, oupprepbara läsningar, felaktiga summor och spökposter.
Emellertid säger SQL-standarden att en transaktion med isoleringsnivån read uncommitted
måste vara deklarerad som read only.
Den kan alltså inte göra några ändringar i databasen, och därför kan det inte uppstå några förlorade uppdateringar (se avsnitt 25.11.1).
Read committed
tillåter inte läsning av ocommittade data, och med den isoleringsnivån slipper man förutom förlorade uppdateringar även smutsiga läsningar.
Däremot kan man fortfarande drabbas av oupprepbara läsningar, felaktiga summor och spökposter.
538
Repeatable read
tillåter, som man kan gissa av namnet, inte oupprepbara läsningar, och inte heller felaktiga summor.
Spökposter kan fortfarande förekomma.
Serializable
innebär full serialiserbarhet.
Inga samtidighetsproblem, inte ens spökposter, får förekomma.
Den högsta isoleringsnivån, serializable,
är default, enligt SQL-standarden, och det är den enda som SQL-standarden kräver att alla databashanterare klarar.
Alla databashanterare följer inte detta, utan till exempel har Microsoft SQL Server read committed
som default.
Vilken isoleringsnivå ska man då välja?
En allmän regel för programmering är att man först ska se till att programmet gör rätt, och sen kan man, om det faktiskt visar sig att det behövs, fundera på prestanda.
Den regeln gäller även för databasprogrammering, och därför är vår rekommendation att i första hand använda isoleringsnivån serializable
för transaktioner i SQL.
Om det faktiskt visar sig att det behövs bättre prestanda kan man använda lägre isoleringsnivåer:
Read uncommitted
kan användas för stora transaktioner som tar lång tid att köra, och där det inte är så noga att man får exakt rätt svar.
repeatable read.
serializable,
om inget annat särskilt anges.
Det finns flera olika metoder för att garantera serialiserbarhet mellan transaktioner:
Om transaktionerna körs i serie, alltså en i taget, är tidsschemat inte bara serialiserbart, utan det är seriellt. Eftersom inga transaktioner körs samtidigt kan det inte heller uppstå några skadliga krockar mellan dem.
Det här är ekvivalent med att varje transaktion låser hela databasen, så att andra transaktioner får vänta tills den är klar. Det ger förstås ingen parallellitet i exekveringen, och kan ge dåliga prestanda. Flaskhalsen i en databashanterare är ju ofta läs- och skrivoperationer mot disk, och medan en transaktion väntar på att den långsamma hårddisken ska bli klar med en operation, skulle en annan transaktion kunna hinna emellan med en del av sitt arbete. Med seriell exekvering går inte det. Och ännu värre: Skulle man ha transaktioner som väntar på inmatning från en användare kanske hela databasen står still tills den användaren kommit tillbaka från kafferasten!
Men även om seriell exekvering inte alltid är lämplig, finns det fall där det kan vara ett bra alternativ, till exempel i en primärminnes-databas 540 där databashanteraren aldrig behöver vänta på långsamma diskoperationer. Man vinner ju också en del på att det inte behövs några lås eller liknande i databasen.
Även webbtillämpningar (se kapitel 19) kan fungera med serialiserade transaktioner utan lås. Kommunikationen mellan användaren och webbservern sker webbsidesvis, och webbservern kanske bara hanterar en sådan inmatning från en användare åt gången. I så fall serialiserar alltså webbservern transaktionerna innan den anropar databashanteraren, och bara en transaktion i taget kommer att köras. (MySQL, som är populär i databasbaserade webbplatser, hade länge ingen särskit bra transaktionshantering, men har ändå fungerat bra i åratal på många tusen olika webbplatser.)
Det finns en enkel algoritm för att avgöra om ett givet tidsschema är serialiserbart. Alltså kan man ta ett tidsschema och kontrollera i förväg om det innehåller skadliga kollisioner. Normalt går det inte att göra så, eftersom man oftast inte vet i förväg vilka transaktioner som ska köras eller vilka läs- och skrivoperationer de kommer att utföra. Men i ett realtidssystem, där det finns tidsgränser som man måste vara säker på att man hinner med, skulle man kanske behöva göra så, och till exempel ha förutbestämda transaktioner som har fasta tidsluckor för när de ska utföra sitt arbete.
Här är problemet förstås att om man upptäcker att det uppstått skadliga kollisioner mellan transaktioner, måste alla ändringar backas tillbaka med rollback, och sen måste transaktionerna göras om. Det är inte alltid det går, i synnerhet om transaktionerna inte bara har bestått av ändringar i databasen, utan också har inneburit att saker hänt i verkligheten. (När sedelautomaten betalat ut pengar till bankkunden är det svårt att göra rollback på verkligheten.) Men med optimistiska metoder (som beskrivs nedan i avsnitt 25.20) är det ändå ungefär så man gör, även om man då inte kontrollerar hela tidsschemat på en gång, utan varje transaktion kontrolleras för sig när den ska committas.
Lås är den vanligaste metoden för databashanterare att isolera transaktioner från varandra.
Den enklaste formen av låsning använder så kallade binära lås. De heter så eftersom de har två lägen: låst och olåst. Om en transaktion ska arbeta med ett dataobjekt, oavsett om det är för att uppdatera det eller bara läsa dess värde, måste transaktionen låsa det dataobjektet, och då kan ingen annan transaktion arbeta med det.
Om någon annan transaktion redan låst dataobjektet, måste transaktionen vänta på att den andra transaktionen låser upp dataobjektet, eller släpper låset som man också säger. Om det är flera transaktioner som väntar på samma dataobjekt, bildas någon form av kö.
I det här exemplet arbetar två transaktioner med dataobjektet X:
Transaktion 1 | Transaktion 2 |
---|---|
Låser X | |
Läs(X) | |
Försöker låsa X ... | |
Skriv(X) | Väntar... |
Låser upp X | Väntar... |
Får låset på X | |
Läs(X) | |
Låser upp X |
Lås är inte gratis. Dels behövs det plats i databasen för att lagra informationen om att ett visst dataobjekt är låst, och dels måste databashanteraren hela tiden kontrollera om de objekt som en transaktion vill läsa eller skriva är låsta.
Ovan talade vi om "dataobjekt" som man arbetar med, låser, läser och skriver. De dataobjekten kan variera ganska mycket i storlek, och som vi nämnde tidigare talar man om granularitet eller finkornighet på operationerna. En databashanterare kan låsa allt från enskilda värden (en ruta i en tabell) via rader och hela tabeller till hela databasen. Alternativt kan man låsa fysiska datastrukturer, som diskblock.
Eftersom varje lås kostar, i tid och plats, vill man ha så få lås som möjligt. Det betyder att man vill att varje lås ska låsa en stor del av 542 databasen. Stora lås ger å andra sidan sämre samtidighet, och därför vill man också att varje lås ska låsa en så liten del av databasen som möjligt!
Binära lås fungerar, men kan ge onödigt dåliga prestanda eftersom flera transaktioner utan problem skulle kunna läsa samma data, så länge ingen skriver dem. Det är först när en eller flera transaktioner behöver ändra i data som det kan uppstå skadliga kollisioner.
Därför brukar databashanterare i stället använda sig av läs- och skrivlås. En transaktion som bara vill läsa ett dataobjekt behöver ett läslås. Hur många transaktioner som helst kan ha läslås på samma dataobjekt, och därför kallas läslås ibland för delade lås. Men om någon transaktion behöver ändra dataobjektet, måste den skaffa ett skrivlås, även kallat exklusivt lås. Det kallas exklusivt lås eftersom ett skrivlås kräver att transaktionen är ensam om att arbeta med det dataobjektet: ingen annan transaktion får ha vare sig läs- eller skrivlås på det objektet.
Ett specialfall är om en transaktion har läslåst ett dataobjekt, och behöver skriva det. I så fall kan den, om ingen annan transaktion har några lås på det objektet, uppgradera läslåset till ett skrivlås. Om en eller flera andra transaktioner har låst objektet, måste transaktionen vänta på att alla transaktioner som har lås släpper dem.
Transaktion 1 | Transaktion 2 |
---|---|
Läs-låser X | |
Läs(X) | |
Läs-låser X | |
Försöker skriv-låsa X ... | |
Väntar... | Läs(X) |
Väntar... | Låser upp X |
Får skriv-låset på X | |
Skriv(X) | |
Låser upp X |
Man skulle kunna tro att det räcker att låsa varje dataobjekt medan man arbetar med det, och sen låsa upp det när man är klar med det. Men, visar det sig, så enkelt är det inte.
Vi tänker oss att Kalle och Lotta, i ett anfall av ömsesidig altruism, vill ge bort sina pengar till varandra. Lotta skänker alla sina pengar till Kalle, och Kalle skänker alla sina pengar till Lotta:
Lottas transaktion | Kalles transaktion |
---|---|
Läs(L) | Läs(K) |
Tmp = L | Tmp = K |
L = 0 | K = 0 |
Skriv(L) | Skriv(K) |
Läs(K) | Läs(L) |
K = K+ Tmp | L= L+ Tmp |
Skriv(K) | Skriv(L) |
Läsaren blir kanske (med rätta) en smula orolig för vad som kan hända om man kör bägge transaktionerna helt parallellt, som det antyds i uppställningen ovan. Därför gör vi ett första försök med (binära) lås. Inga läs- eller skrivoperationer får utföras utan att vi har låst det dataobjekt som ska läsas eller skrivas. När vi är klara med ett objekt, låser vi upp det. Lottas transaktion ser nu ut så här, med låsoperationerna:
Lottas transaktion |
---|
Vi lägger till låsningar och upplåsningar på samma sätt även i Kalles transaktion:
Kalles transaktion |
---|
När de två transaktionerna körs råkar det bli enligt följande tidsschema:
545Ingen transaktion gör något med något dataobjekt (K och L) utan att först ha låst det.
Före tidsschemat ser databasen ut så här:
Vi lämnar detaljerna som en övning till läsaren, men efter tidsschemat ser databasen ut så här:
546Kalle och Lotta har bytt pengar med varandra. Kalle hade 50 kronor och Lotta hade 100, men nu har Kalle 100 kronor och Lotta har 50. Kalle och Lotta är kanske nöjda med detta, för de ville ju faktiskt ge bort sina pengar till varandra, men är det rätt?
Nej, det är fel. Vi sa tidigare att ett korrekt tidsschema definieras som ett serialiserbart tidsschema, och det finns inget sätt att åstadkomma det här resultatet genom att köra transaktionerna seriellt, efter varandra. Om man kör transaktionerna seriellt kommer nämligen alla pengarna att till slut hamna antingen hos Kalle eller hos Lotta. Först ger ju den ena personen sina pengar till den andra, så att han eller hon sitter med alla pengarna, och sen ger den personen tillbaka alltihop. Tidsschemat är inte serialiserbart, och alltså fel, trots vårt försök med lås.
Man kan se det så här: Lottas transaktion hämtar behållningen på Lottas konto och lagrar den i variabeln Tmp. Sen sätter den Lottas konto i databasen till noll, och släpper låset på Lottas konto. Det betyder att Lottas transaktion "kommer ihåg" hur Lottas konto såg ut förut, före ändringen. Men eftersom den släppt låset, kan en annan transaktion (i det här fallet Kalles transaktion) hinna ändra tillståndet i databasen, så att när Lottas transaktion fortsätter att köra, och gör fler ändringar i databasen, baseras (via Tmp) de ändringarna på ett tillstånd i databasen som inte längre gäller. Lottas transaktion låste alltså upp Lottas konto för tidigt.
Vi måste använda tvåfaslåsning, vilket innebär att så fort en transaktion släppt ett lås, får den inte skaffa några nya lås. Annorlunda uttryckt kan transaktionens livstid indelas i två faser: låsningsfasen (där den låser dataobjekt) och upplåsningsfasen (där den låser upp dataobjekt). Så snart transaktionens låst upp ett dataobjekt, har den lämnat låsningsfasen och gått över i upplåsningsfasen, och får inte låsa några nya objekt.
547Om man använder tvåfaslåsning är det resulterande tidsschemat garanterat serialiserbart. (Men se nästa stycke om vad som kan hända om det förekommer avbrutna transaktioner.) Om man vill undvika spökposter (se avsnitt 25.11.5) måste man dessutom tänka på att inte bara låsa dataobjekten, utan även de indexstrukturer som används som sökvägar.
I riktiga databashanterare använder man läs- och skrivlås, snarare än binära lås som i beskrivningen ovan. Då räknar man uppgradering från ett läslås till ett skrivlås som ett nytt lås; alltså får en sådan uppgradering bara ske i låsningsfasen. Nedgradering från skrivlås till läslås (i den mån det förekommer) räknas som att man släpper ett lås, och det får alltså bara ske i upplåsningsfasen.
Vi skrev ovan att tvåfaslåsning garanterar serialiserbart. Men det finns ett annat problem, som kan uppstå om transaktioner kan avbrytas och rullas tillbaka. Med avbrutna transaktioner fungerar tvåfaslåsning, som vi beskrev den, inte så bra.
548Transaktion 1 | Transaktion 2 |
---|---|
Lås X | |
Läs(X) | |
Lås Y | |
Skriv(Y) | |
Lås upp Y | |
Lås Y | |
Läs(Y) | |
Rollback | |
Rollback! |
Tvåfaslåsning i sin enklaste form tillåter smutsiga läsningar, alltså läsning av ocommittade data. I exemplet släpper transaktion 1 låset på Y innan den transaktionen committar. Därför kan transaktion 2 låsa, och läsa, Y. Men eftersom transaktion 1 ännu inte committats, kan den fortfarande avbrytas och rullas tillbaka, vilket också sker i exemplet. Det betyder att transaktion 2 läst, och kanske baserat sitt fortsatta arbete, på ett värde som inte längre finns i databasen.
Om vi nöjer oss med isoleringsnivån read uncommitted
enligt SQL-standarden får vi stå ut med de fel som uppstår till följd av den smutsiga läsningen, men om vi vill ha serialiserbarhet har vi inget annat val än att avbryta och rulla tillbaka även transaktion 2.
Det är ett exempel på kaskad-rollback.
Men det är värre än så. Ännu fler transaktioner kan dras in i kaskaden, till och med transaktioner som committats och därför, enligt hållbarhetsegenskapen D i ACID, aldrig får försvinna ur databasen.
549Transaktion 1 | Transaktion 2 | Transaktion 3 |
---|---|---|
Lås X | ||
Läs(X) | ||
Lås Y | ||
Skriv(Y) | ||
Lås upp Y | ||
Lås Y | ||
Läs(Y) | ||
Lås Z | ||
Skriv(Z) | ||
Lås upp Z | ||
Lås Z | ||
Läs(Z) | ||
Lås W | ||
Skriv(W) | ||
Commit | ||
Rollback | ||
Rollback! | ||
Rollback? |
Transaktion 1 skriver Y, och sen läser transaktion 2 Y. Transaktion 2 skriver Z, med ett värde som kanske baseras på Y, och sen läser transaktion 3 Z. Transaktion 3 skriver W, med ett värde som kanske baseras på Z, och därigenom kanske också på Y, som transaktion 1 skrev. Transaktion 3 committar.
Därefter avbryts transaktion 1, och dess ändring av Y rullas tillbaka. Då måste också transaktion 2 avbrytas, och dess ändring av Z rullas tillbaka. Det leder i sin tur till att transaktion 3 måste avbrytas, och dess ändring av W rullas tillbaka – men transaktion 3 är redan committad, och då får dess ändringar inte försvinna!
Egentligen räcker det att bara behålla skrivlåsen tills transaktionen är avslutad, för det är ju bara ändringar av data som riskerar att rullas tillbaka och påverka andra transaktioner. Läslås kan släppas innan transaktionen är klar. Att behålla alla skrivlås tills transaktionen är klar är principen bakom strikt tvåfaslåsning:
Ett problem i system med samtidiga transaktioner är att det kan uppstå deadlock, som innebär att två eller flera transaktioner står still och väntar på varandra. Antag att två transaktioner båda arbetar med två dataobjekt, X och Y. Ett tidsschema kan se ut så här:
Transaktion 1 | Transaktion 2 |
---|---|
Lås X | |
... | |
... | Lås Y |
... | ... |
Försöker låsa Y ... | ... |
Väntar... | ... |
Väntar... | Försöker låsa X ... |
Väntar... | Väntar... |
Transaktion 1 låser X, och jobbar vidare. Transaktion 2 låser Y, och jobbar vidare. Sen vill transaktion 1 även arbeta med Y, och försöker därför låsa Y, men eftersom transaktion 2 redan har låst Y, får transaktion 1 stå still och vänta på att transaktion 2 ska bli klar med Y. Ännu lite senare vill transaktion 2 arbeta med X, och försöker därför låsa X, men eftersom transaktion 1 redan har låst X, får transaktion 2 nu stå still och vänta på att transaktion 1 ska bli klar med X. Men eftersom transaktion 1 redan står still och väntar, kommer den aldrig att bli klar med X.
551Transaktion 1 står alltså still och väntar på att transaktion 2 ska bli klar med Y, och släppa det låset, samtidigt som transaktion 2 står still och väntar på att transaktion 1 ska bli klar med X, och släppa det låset. Så kommer det att fortsätta tills universum går under eller någon startar om databashanteraren, vilket som nu kommer först.
Man kan rita en så kallad wait-for-graf ("väntar-på-graf "?) som visar hur transaktionerna väntar på varandra. Om det finns en cykel i grafen har det uppstått deadlock. I det här fallet är wait-for-grafen mycket enkel, men i verkligheten kan en cykel innefatta fler än två transaktioner.
Varje datasystem som innehåller samtidiga trådar eller transaktioner, med resurser som kan låsas av de trådarna eller transaktionerna, måste hantera deadlockproblemet på något sätt. Det kan göras antingen genom att undvika eller genom att upptäcka (och sen åtgärda) deadlock.
Det finns flera olika metoder för att undvika deadlock. En låsbaserad databashanterare kan undvika deadlock om alla transaktioner alltid låser dataobjekten i en viss (godtycklig) ordning. Alternativt kan transaktionerna låsa alla dataobjekt de behöver på en gång, vilket används i konservativ tvåfaslåsning (conservative two-phase locking på engelska). Bägge dessa metoder kräver dock att man vet i förväg vilka data transaktionen kommer att behöva tillgång till, och det är sällan möjligt. Därför finns det andra metoder som, när ett försök att låsa ett dataobjekt innebär att transaktionen får vänta, kontrollerar om det kan uppstå deadlock. I så fall avbryts antingen den transaktionen, eller någon annan.
552En säkrare metod (som inte riskerar att avbryta transaktioner i onödan) är att undersöka wait-for-grafen. Det kan antingen göras så fort en transaktion försöker låsa ett dataobjekt som redan är låst, så att den får vänta, eller så kombinerar man det med en time-out, och gör kontrollen när transaktionen stått still ett tag. Om det då visar sig att det finns en cykel i wait-for-grafen, måste cykeln brytas genom att någon transaktion avbryts. Det behöver inte vara den transaktion som försökte låsa, eller den transaktion som har det önskade låset, utan det kan till exempel vara den yngsta av transaktionerna i cykeln.
MySQL har, som de flesta databashanterare, samtidighetskontroll med hjälp av lås. Här visar vi ett körexempel med två transaktioner som ger några SQL-kommandon: 1
Transaktion 2:s insert
-kommando står alltså still och väntar på att transaktion 1 ska committa.
Transaktion 1 har ju låst raden med värdet 2 på primärnyckeln, och transaktion 2 får inte läsa den, ens för att kontrollera om den över huvud taget finns i tabellen.
Det är först när transaktion 1 committar, och därigenom släpper låset, som transaktion 2 kan köra vidare.
Då får man ett felmeddelande – inte för något som har med transaktionshanteringen att göra, utan för att vi försökte lägga in två rader med samma värde på primärnyckeln:
ERROR 1062 (23000): Duplicate entry '2' for key 1
Optimistiska metoder för kontroll av samtidig exekvering går ut på att man hoppas att det inte ska bli några kollisioner mellan transaktionerna. Därför bryr man sig inte om att göra några kontroller medan en transaktion körs. Men hur optimistisk man än är kan det förstås ändå inträffa kollisioner, så kontrollen måste göras någon gång. Varje transaktion kontrolleras därför när den är klar. Om det då visar sig att det hade uppstått kollisioner, måste transaktionen avbrytas, och den får köras igen senare. Under körningen får transaktionen inte göra några ändringar i den riktiga, gemensamma databasen, utan varje skrivoperation sparas som en egen, lokal kopia av de data som skrevs. Det är först efter körningen, om kontrollen visar att inga kollisioner uppstod, som de där lokala kopiorna läggs in i den riktiga databasen.
Optimistiska metoder kan ge bättre prestanda, eftersom man inte behöver hålla på med lås och annat krångel i samband med körningen av transaktionerna, och det finns inga hinder för maximal parallellitet. Å andra sidan kan prestanda snabbt försämras vid hög belastning om det börjar uppstå kollisioner, för då måste en del transaktioner göras om, vilket leder till ännu högre belastning och kanske ännu fler kollisioner. Som man kan gissa lämpar sig optimistiska metoder bäst för system med ganska liten belastning, eller där transaktionerna mest läser och inte ändrar så mycket på data, eller där transaktionerna inte så ofta arbetar med samma data. Dessutom krävs det förstås att transaktionerna kan köras på nytt om de misslyckas, och detta kan kräva att man programmerar transaktionerna lite annorlunda än man brukar.
Det finns flera olika optimistiska metoder, och här ska vi bara presentera en av dem.
I det här optimistiska protokollet delas varje transaktion in i tre faser:
Valideringen handlar egentligen inte om att hitta kollisioner, utan om att garantera att inga kollisioner kan ha uppstått. Om man inte kan ge en sån garanti, har valideringen misslyckats och transaktionen kan inte committas.
När en transaktion, som vi kan kalla T, ska valideras, måste vi därför kontrollera den mot alla andra transaktioner, som vi kan kalla U1, U2 och så vidare. 2
När T ska valideras, kontrollerar vi den mot alla andra transaktioner som finns i databasen:
Om något av de fyra fallen är uppfyllt, kan T committa. Om inget av de fyra fallen är uppfyllt, kan vi inte vara säkra på att T inte har haft kollisioner med den andra transaktionen, och T måste därför avbrytas. Någon särskild rollback behöver inte göras, eftersom transaktionen inte gjort några ändringar i den riktiga databasen.
Nästan alla vanliga databashanterare använder lås för sin samtidighetskontroll, men databashanteraren Mimer använder faktiskt en optimistisk metod. Här är ett exempel som visar hur två transaktioner försöker göra motstridiga ändringar i databasen.
557
Transaktion 2:s insert
-kommando kommer (som det verkar) att lyckas, trots att transaktion 1 redan lagt in en rad med samma primärnyckel.
Transaktion 1:s commit
kommer också att lyckas.
I valideringsfasen för transaktion 2 upptäcks konflikten, och transaktion 2:s commit
misslyckas med felmeddelandet Transaction aborted due to conflict with other transaction.
Jämför detta med exemplet på sidan 552 i avsnitt 25.19.6, där vi gav samma kommandon till en databashanterare som använder lås.
Där stod transaktion 2:s insert
-kommando still och väntade på att transaktion 1 skulle committa.
Om det är ett applikationsprogram som utför transaktionerna, behövs det felhantering i programmet både med den optimistiska metoden och med låsen. Kanske blir den felhanteringen lite enklare i varianten med lås, eftersom vi inte behöver köra om hela transaktionen från början. (Å andra sidan kan man då få deadlock, så att transaktionen avbryts på grund av det, och då måste man i alla fall kunna köra om hela transaktionen från början.)
Ett alternativ till loggfilen är skuggsidor. På engelska heter metoden shadow paging. Den är särskilt användbar i samband med optimistiska metoder för samtidighetskontroll, eftersom den på ett effektivt sätt ger oss både transaktionsegna kopior av de data som skrivs, och ett enkelt sätt att rulla tillbaka transaktionen.
Vi antar att databasen är uppbyggd av ett antal diskblock, som pekas ut av en katalog:
558När en transaktion startas, sparas först en kopia av katalogen, en så kallad skuggkatalog (shadow directory på engelska). När transaktionen sen gör en ändring i databasen, görs ändringen inte i det riktiga diskblocket, utan det gamla diskblocket lämnas orört, och i stället skrivs diskblocket, med det nya innehållet, på ett annat ställe på disken. Den aktuella katalogen ändras också så att det nya diskblocket pekas ut.
Om flera transaktioner är aktiva samtidigt, kan varje transaktion få sin egen aktuella katalog. På det viset syns de gjorda ändringarna inte för de andra transaktionerna.
Det finns några varianter på samtidighetskontroll som använder sig av tidsstämplar. Tidsstämplar är markeringar som man sätter på ett dataobjekt för att hålla reda på när det senast lästes eller uppdaterades.
Det fungerar så att varje transaktion får en viss tidsstämpel, som till exempel kan vara klockslaget när transaktionen startades. Det är dock enklare, och fungerar minst lika bra, att numrera transaktionerna (1, 2, 3 och så vidare), och låta tidsstämpeln vara transaktionens nummer.
Transaktion nummer 2 stämplar alltså en tvåa på dataobjektet, för att visa att det senast var just transaktion 2 som gjorde något med det dataobjektet. Det kanske inte låter som att den tidsstämpeln har så mycket med tid att göra, men man kan se det som att tvåan är klockslaget när transaktion nummer 2 startades.
Det finns tre olika sorters tidsstämplar att hålla reda på:
Tanken är nu att varje gång en transaktion vill läsa eller skriva ett dataobjekt, kontrollerar databashanteraren tidsstämplarna på det 560 dataobjektet för att se om det uppstått en kollision med en annan transaktion. Vi ska strax beskriva detaljerna.
Man kan tänka sig att vi startar en transaktion varje dag. Transaktionen T1 startas dag 1, och får tidsstämpeln TS(T1) = 1, och så vidare. Om varje transaktion avslutas samma dag som den startade, får vi ett seriellt tidsschema. Tidsschemat för några transaktioner kan till exempel se ut så här:
Läs- och skrivoperationerna i tidsschemat är ritade som svarta prickar.
Nu tänker vi oss att en del transaktioner inte hinner färdigt med alla sina operationer den dag de startades. (Egentligen är ju "dagarna" klocktick, eller bara ordningsnummer, så i verkligheten är det inte så konstigt om de inte hinner klart innan nästa transaktion startas.) Då kan vissa operationer förskjutas till senare dagar, som till exempel T1:s Skriv(X) och T2:s Skriv(Y):
561T1:s skrivoperation Skriv(X) kom ursprungligen före T2:s läsoperation Läs(X), men nu har de bytt ordningsföljd. Eftersom dessa operationer är motstridiga, och inte kommer i samma ordning i de två tidsschemana, är tidsschemana inte konfliktekvivalenta. Tidigare läste transaktion 2 det värde på X som transaktion 1 skrev, men nu hinner transaktion 1 inte skriva det värdet innan transaktion 2 läser X.
Ytterligare ett problem är att T2:s skrivoperation Skriv(Y) ursprungligen kom före T3:s Skriv(Y), men nu kommer även de i omvänd ordning. Tidigare var det alltså T3 som skrev sist, och det var T3:s värde som till slut fanns kvar i databasen. Nu är det i stället T2 som skriver sist, och det kommer att vara T2:s värde som till slut finns kvar i databasen. Alltså är även dessa både operationer motstridiga.
Om vi vill att ett sånt här förskjutet tidsschema ska vara konfliktekvivalent med det ursprungliga, måste vi hålla koll på de motstridiga operationerna, och hindra att de byter ordningsföljd. Det är det som tidsstämpelmetoden går ut på, och den garanterar därför serialiserbarhet genom att se till att tidsschemat är konfliktekvivalent med ett schema där transaktionerna körs seriellt i nummerordning. Vi har tidigare definierat serialiserbarhet som konfliktekvivalens med något seriellt tidsschema, men tidsstämpelmetoden ger alltså en lite starkare garanti, nämligen konfliktekvivalens med just det tidsschema där transaktionerna körs i "rätt" ordning.
Om man vill kan man se de förskjutna läs- och skrivoperationerna som en sorts tidsresor. Operationerna reser i tiden till en dag i framtiden.
Alla som läst science fiction vet att vid tidsresor måste man se upp med paradoxer. Jag måste till exempel vara mycket försiktig om jag reser tillbaka i tiden och träffar farfar. Skulle jag råka hindra honom från att träffa farmor, så kommer far, och därmed jag själv, aldrig att finnas. Men samtidigt var jag ju där och hindrade farfars romans med farmor, och hur kan jag göra det om jag inte finns?
Om till exempel en läsoperation läser ett dataobjekt dag 1, och en skrivoperation ändrar på det dataobjektet dag 2, måste de fortfarande komma i samma ordning: läsoperationen först och skrivoperationen sist. Annars läser läsoperationen ett dataobjekt som egentligen skrevs senare, och det är en tidsreseparadox – ungefär som om jag skulle kunna läsa morgondagens tidning redan i dag.
Varje gång en transaktion vill läsa eller skriva ett dataobjekt, måste den först jämföra dataobjektets tidsstämplar med sin egen, för att kontrollera att det inte uppstått en tidsreseparadox. Följande regler används:
• Om transaktionen T vill skriva dataobjektet X:
– Om SkrivTS(X) > TS(T), så har en transaktion från framtiden redan skrivit objektet. Om vi skriver över den ändringen, blir databasens sluttillstånd fel. Därför måste transaktion T avbrytas, och rullas tillbaka.
Alternativt kan man resonera som så att om vår ändring ändå kommer att skrivas över i framtiden, kan vi strunta 563 i att göra ändringen, och bara köra vidare. Detta kallas Thomas skrivregel. 3
• Om transaktionen T vill läsa dataobjektet X:
För att visa hur tidsstämpelmetoden fungerar återanvänder vi exemplet med Kalle och Lotta som vill ge bort sina pengar till varandra. På sidan 545 finns ett tidsschema som visserligen använder lås, men inte tvåfaslåsning, och som inte är serialiserbart. Om vi tar bort låsoperationerna, och i stället använder tidsstämpelmetoden, borde tidsstämpelmetoden upptäcka att tidsschemat inte är korrekt, och avbryta (åtminstone) den ena transaktionen.
Så här ser tidsschemat ut utan låsoperationer:
564Lottas transaktion, T1 | Kalles transaktion, T2 |
---|---|
Läs(L) | |
Tmp = L | |
L = 0 | |
Skriv(L) | |
Läs(K) | |
Tmp = K | |
K=0 | |
Skriv(K) | |
Läs(L) | |
L=L+ Tmp | |
Skriv(L) | |
Läs(K) | |
K=K+ Tmp | |
Skriv(K) |
Steg | Transaktion T1 | Transaktion T2 | K | LäsTS(K) | SkrivTS(K) | L | LäsTS(L) | SkrivTS(L) |
---|---|---|---|---|---|---|---|---|
50 | 0 | 0 | 100 | 0 | 0 | |||
1 | Läs(L) | 50 | 0 | 0 | 100 | 1 | 0 | |
2 | Tmp = L | 50 | 0 | 0 | 100 | 1 | 0 | |
3 | L= 0 | 50 | 0 | 0 | 100 | 1 | 0 | |
4 | Skriv(L) | 50 | 0 | 0 | 0 | 1 | 1 | |
5 | Läs(K) | 50 | 2 | 0 | 0 | 1 | 1 | |
6 | Tmp = K | 50 | 2 | 0 | 0 | 1 | 1 | |
7 | K=0 | 50 | 2 | 0 | 0 | 1 | 1 | |
8 | Skriv(K) | 0 | 2 | 2 | 0 | 1 | 1 | |
9 | Läs(L) | 0 | 2 | 2 | 0 | 2 | 1 | |
10 | L=L+ Tmp | 0 | 2 | 2 | 0 | 2 | 1 | |
11 | Skriv (L) | 0 | 2 | 2 | 50 | 2 | 2 | |
12 | Läs(K) |
Men, ack och ve, T1 har skrivit L, i steg 4, och T2 läste sen det värdet, i steg 8. Alltså har T2 baserat en del av sitt resultat på T1:s data, och när T1 rullas tillbaka, och dess ändringar i databasen försvinner, måste även T2 rullas tillbaka – trots att T2 redan är klar och committad. Vi har fått kaskad-rollback, och så får det förstås inte gå till. Därför måste vi modifiera tidsstämpelmetoden på något sätt.
Som vi kanske kommer ihåg från avsnitt 25.19.4 kan det grundläggande tvåfaslåsningsprotokollet ge kaskadrollback, men det slipper man om man låter varje transaktion behålla sina skrivlås tills den antingen har committat eller rullats tillbaka. Detta kallas strikt tvåfaslåsning.
567Det finns även en strikt tidsstämpelmetod (på engelska strict timestamp ordering). På ungefär samma sätt som med strikt tvåfaslåsning måste en transaktion som vill läsa eller skriva ett dataobjekt som en annan aktiv transaktion har skrivit, vänta tills denna andra transaktion har committat (eller rullats tillbaka).
Det är bara om dataobjektet (låt oss kalla det X) är skrivet av en tidigare transaktion, dvs om SkrivTS(X) < TS(T), som transaktionen T måste vänta. Om det är skrivet av en senare transaktion har vi ju en tidsparadox, och kommer i stället att avbryta transaktionen T. Eftersom en transaktion bara kan vänta på tidigare transaktioner, alltså med en lägre tidsstämpel, kan det inte uppstå några cykler i grafen över transaktioner som väntar på varandra. Därför kan man inte få deadlock.
Med flerversionsmetoder för hantering av samtidighet (på engelska multiversion concurrency control) kan databasen innehålla flera olika versioner av samma dataobjekt. När en transaktion ändrar i databasen, skrivs den gamla versionen av dataobjektet inte över, utan den finns kvar, och ändringsoperationen ger upphov till en ny version av samma dataobjekt. När en transaktion läser i databasen, kan det alltså finnas flera versioner av varje dataobjekt att välja mellan, och då ska databashanteraren se till att använda en version som gör att tidsschemat blir serialiserbart.
En variant är att kombinera flerversionstekniken med optimistiska metoder, som i versionshanteringssystemet CVS. CVS betyder Concurrent Versions System, och är ett system för att låta flera personer arbeta med gemensamma dokument, till exempel programmerare som arbetar med källkodsfiler i ett programmeringsprojekt. De olika filer som programmet består av lagras i ett centralt bibliotek eller magasin (repository på engelska). Programmerarna kan sen checka ut filer för att arbeta med dem. När de har gjort ändringar måste de checka in filerna igen i det centrala biblioteket. (Kommandot för det heter commit.) Eftersom många programmerare kan behöva tillgång till samma filer, kanske dagar eller veckor i sträck, skulle det 568 vara opraktiskt att använda lås. Därför kan flera olika programmerare checka ut samma filer, och även göra ändringar i dem, samtidigt.
När de sen checkar in filerna kan det uppstå konflikter. Två programmerare kan ha ändrat i samma fil. Lyckligtvis behöver man inte (som i avsnitt 25.20 om optimistiska metoder) kasta allt arbete som den ena programmeraren gjort, och sen göra om det. Om de ändrat i olika delar av filen, kan CVS automatiskt införa bägge ändringarna i sin centrala kopia. Om de ändrat på samma ställe i filen, måste en av programmerarna titta på ändringarna och slå samman de olika versionerna för hand.
I många databassammanhang är transaktionerna korta, mätt i tid. De består av ett antal SQL-satser som körs i följd, och det hela är klart på bråkdelar av en sekund, eller på sin höjd på några få sekunder. Med så korta transaktioner är det rimligt att använda lås, så att delar av databasen är oåtkomliga för andra transaktioner.
Sådana korta transaktioner får förstås inte innehålla interaktion med en mänsklig användare. Även i bästa fall brukar en människa ta flera sekunder på sig för att svara. I värsta fall går hon i väg på lunch först. Man vill därför gärna undvika att ha interaktion med användaren inuti transaktionen. I stället kan man vänta tills användaren matat in alla uppgifter som behövs, och först därefter startas transaktionen som lägger in uppgifterna i databasen.
Även transaktioner som arbetar med stora mängder data, eller flera olika databaser, kan visa sig ta för lång tid. Då kan man ibland dela upp transaktionen i korta transaktioner, som tillsammans bildar en så kallad saga. 4 Det kräver dock att effekterna av varje kort transaktion kan tas bort igen, med en så kallad kompenserande transaktion. Exempel:
Om man bokar en lång flygresa, som består av flera enskilda flygturer, måste man boka biljetter på var och en av flygturerna. Men det är först när man bokat alla biljetterna som man vet att hela resan går att genomföra, så egentligen måste man göra en transaktion av 569 alla bokningarna. Antingen måste man låsa data för samtliga delresor ända tills den sista är bokad, vilket kan ge dåliga prestanda genom lägre samtidighet i databasen, eller (med optimistiska metoder) så riskerar man att behöva göra om allt arbete. Det är emellertid lätt att skriva kompenserande transaktioner för biljettbokningar, och därför kan det vara bättre att committa varje enskild biljettbokning när den är klar, och sen, om det visar sig att alla bokningarna inte gick att göra, avboka de biljetter man hunnit boka.
Grundläggande transaktionsbegrepp tas upp i kapitel 24.
Transaktionens tillstånd (engelska: transaction states). En transaktion kan vara aktiv, delvis committad (och då befinner den sig i commit-processen), avbruten (och då befinner den sig i rollbackprocessen), committad och tillbakarullad.
Commit-punkten (engelska: commit point). Den tidpunkt efter vilken transaktionen är committad. Brukar definieras som när commitnoteringen skrivs på loggfilen.
Materialiserad databas (engelska: materialized database). Databasen som den ser ut när systemet kommer i gång igen efter en krasch. Den kan innehålla ändringar gjorda av avbrutna transaktioner, och alla committade transaktioner har kanske inte hunnit göra alla sina ändringar. Återhämtningsprocessen måste reda upp i röran.
Omedelbar uppdatering (engelska: immediate update). Att skrivoperationer direkt ändrar i den riktiga databasen.
Fördröjd uppdatering (engelska: deferred update). Att skrivoperationer inte ändrar i den riktiga databasen, utan ändringarna görs först när transaktionen gjort commit.
Checkpoint. En notering i loggfilen om att alla transaktioner är avslutade, och alla deras ändringar har skrivits på disken. Underlättar återhämtningsprocessen.
Skuggsidor. Om databasen är uppdelad i diskblock kan man spara de gamla blocken, i stället för att skriva över dem, och då blir det lätt att rulla tillbaka transaktionen.
570Granularitet eller finkornighet (engelska: granularity). Storleken på de dataobjekt som databashanteraren låser, läser och skriver.
Samtidighetskontroll (engelska: concurrency control). Att hindra samtidiga transaktioner från att störa varandra.
Förlorad uppdatering (engelska: lost update). Att en transaktion skriver över en annan transaktions ändringar.
Smutsig läsning, läsning av smutsiga data eller läsning av ocommittade data (engelska: dirty read). Att en transaktion läser en annan transaktions ändringar, innan den transaktionen har gjort commit.
Oupprepbar läsning (engelska: non-repeatable read). Att en transaktion kan få olika resultat när den läser samma data, eftersom en annan transaktion hunnit ändra i databasen, och committa, mellan läsningarna.
Felaktig summa (engelska: incorrect summary). Ett fall av läsning av inkonsistenta data. Transaktionen läser data, och sen, innan den hinner läsa andra data som hänger ihop med de första, har en annan transaktion hunnit ändra i databasen och committa.
Spökpost eller fantompost (engelska: phantom). Data som dyker upp och försvinner på mystiska sätt till följd av otillräcklig låsning av sökvägar.
Tidsschema (engelska: schedule). Den ordning som operationerna i flera samtidiga transaktioner utförs, i synnerhet läs- och skrivoperationernaidendeladedatabasen.
Seriellt tidsschema (engelska: serial schedule). Ett tidsschema utan samtidiga transaktioner. Transaktionerna körs efter varandra, i serie.
Icke-seriellt tidsschema (engelska: non-serial schedule). Ett tidsschema där två eller flera transaktioner överlappar varandra i tiden.
Serialiserbart tidsschema (engelska: serializable schedule). Ett tidsschema som är ekvivalent med något seriellt tidsschema. Ett tidsschema är korrekt om det är serialiserbart.
Motstridiga operationer (engelska: conflicting operations). Två operationer i två olika transaktioner där ordningen mellan dem har betydelse för resultatet. De måste arbeta med samma dataobjekt, och minst en av operationerna måste vara en skrivoperation.
571Konflikt-ekvivalens (engelska: conflict equivalence). Ekvivalens mellan tidsscheman som innebär att motstridiga operationer kommer i samma ordning. Garanterar att bägge schemana ger samma resultat.
Konflikt-serialiserbarhet (engelska: conflict serializability). Ett tidsschema som är konflikt-ekvivalent med något seriellt tidsschema är konflikt-serialiserbart.
Isoleringsnivåer (engelska: isolation levels). SQL-standarden definierar fyra olika isoleringsnivåer read uncommitted, read committed, repeatable read och serializable.
Binärt lås (engelska: binary lock). Ett lås med bara två tillstånd, låst och upplåst, till skillnad från läs- och skrivlås som har tre tillstånd: skrivlåst, läslåst och upplåst.
Läslås eller delat lås (engelska: read lock eller shared lock). Ett lås där flera transaktioner samtidigt kan låsa samma dataobjekt. Används när transaktionerna bara ska läsa dataobjektet.
Skrivlås eller exklusivt lås (engelska: write lock eller exclusive lock). Ett lås där bara en transaktion samtidigt kan låsa samma dataobjekt. Används när transaktionerna ska ändra på dataobjektet.
Tvåfaslåsning (engelska: two-phase locking eller 2PL). Ett låsprotokoll som garanterar serialiserbarhet. Så fort en transaktion låst upp ett lås, får den inte skaffa några nya lås.
Kaskadrollback (engelska: cascading rollback). När tillbakarullning av en transaktion tvingar fram tillbakarullning av en annan, i värsta fall redan committad, transaktion, som baserat sitt arbete på den först tillbakarullade transaktionens (del-)resultat.
Rigorös tvåfaslåsning (engelska: rigorous two-phase locking). Ett låsprotokoll som garanterar kaskadfrihet. Varje transaktion behåller alla sina lås tills efter commit-punkten.
Strikt tvåfaslåsning (engelska: strict two-phase locking). Ett låsprotokoll som garanterar kaskadfrihet. Varje transaktion behåller alla sina skrivlås tills efter commit-punkten.
Deadlock (samma ord på engelska). Att två eller flera transaktioner står still och väntar på varandra.
Optimistiska metoder (engelska: optimistic methods). Olika metoder för samtidighetskontroll som har det gemensamt att man kontrollerar efteråt ifall det gick bra.
572Pessimistiska metoder (engelska: pessimistic methods, pessimistic locking). Metoder för samtidighetskontroll som går ut på att man kontrollerar i förväg om det kommer att gå bra. Normalt används lås.
Valideringsfas (engelska: validation phase). När en transaktion, i ett system med optimistisk samtidighetskontroll, kontrolleras för att se om det gick bra eller om det uppstod några krockar med andra transaktioner.
Tidsstämpelmetoden (engelska: timestamp ordering). En metod för samtidighetskontroll där varje transaktion och varje dataobjekt förses med en tidsstämpel, och kontrollen sker genom att jämföra dessa tidsstämplar.
Strikta tidsstämpelmetoden (engelska: strict timestamp ordering). En variant av tidsstämpelmetoden som undviker kaskadrollback. Ingen transaktion får läsa, eller skriva över, en ocommittad transaktions ändringar i databasen.
Flerversionsmetoder (engelska: multi-version methods). Metoder för samtidighetskontroll där databasen kan innehålla flera versioner av samma dataobjekt.
Databasfrågor i SQL och andra frågespråk uttrycks icke-procedurellt (även kallat deklarativt). Det innebär att SQL-användaren uttrycker vad som ska göras, och databashanteraren bestämmer sedan hur en given fråga ska utföras. "Vad" innebär i detta fall en specifikation av vilka tabeller som ingår i en fråga och vilka datavärden som ska matchas för att forma svaret på frågan. "Hur" innebär att databashanteringssystemet för en given fråga genererar ett (snabbt) sökprogram som traverserar de interna datastrukturer som representerar tabellerna i databasen, för att kombinera och hitta eftersökta datavärden. Man kan säga att detta är en form av automatisk programmering eftersom systemet automatiskt genererar sökprogram för en given icke-procedurell sökspecifikation i form av en fråga. De genererade sökprogrammen kallas exekveringsplaner. 1
För en given icke-procedurell databasfråga finns det ofta ett stort antal korrekta exekveringsplaner. Olika exekveringsplaner kan ta mycket olika tid att köra. Frågeoptimering går ut på att databashanteraren automatiskt genererar den effektivaste exekveringsplanen (eller i alla fall en tillräckligt effektiv) för en given fråga.
Frågeoptimering är av central betydelse för databassystemets uppbyggnad. Vill man förstå hur en databashanterare fungerar måste man också förstå hur frågeoptimering fungerar.
Även den som (hur obegripligt det än kan låta!) inte är intresserad av att förstå databashanterarens inre arbete har praktisk nytta av att känna till hur frågeoptimering går till. Det kan nämligen vara nödvändigt att förstå lite om hur frågeoptimeringen fungerar för att kunna specificera maximalt effektiva SQL-frågor. Två olika frågor som egentligen är ekvivalenta (eller, vilket också är vanligt, nästan ekvivalenta) kan vara olika svåra för databashanteraren att optimera, och kan därför ge olika exekveringsplaner. Skillnaden i exekveringstid mellan olika exekveringsplaner för en och samma SQL-fråga kan vara enorm, och en dåligt optimerad fråga kan lätt ta 1 000 gånger längre tid att utföra än en optimal exkveringsplan.
I moderna databashanterare kan man för en given fråga be att få ut exekveringsplanen för att se om den ser bra ut. Om så inte är fallet kan man antingen formulera om frågan eller ge tips (pragma) till frågeoptimeraren för att frågan ska bli snabbare att utföra.
Olika möjliga exekveringsplaner har olika komplexitet 2 med avseende på databasens storlek. Till exempel visar vi senare att en naiv exekveringsplan kan ha komplexitet O(N 2) där N är antal rader i de tabeller som berörs av frågan, medan den optimala planen kanske har komplexitet O(logN ). Eftersom N ofta är mycket stort i databassammanhang ger frågeoptimering oerhört stora effektivitetsvinster redan vid måttligt stor databas (till exempel redan med N=10 000). Har man att göra med mycket små datamängder så har frågeoptimering inte så stor betydelse och man kan använda en förutbestämd ooptimerad statisk traverseringsordning genom hela databasen. Exempelvis bygger programmeringsspråket Prolog på en inbyggd sådan förutbestämd datatraverseringsordning. 3 Eftersom även en databas med N=10 000 anses vara mycket liten duger statiska strategier inte för databaser. Frågorna måste alltså optimeras.
575Varför inte procedurella sökprogram? I procedurella programmeringsspråk, till exempel Java eller C, får man explicit definiera sina sökalgoritmer. En exekveringsplan kan ses som ett automatiskt genererat sådant procedurellt sökprogram i ett speciellt programmeringsspråk för databasaccess. Innan relationsdatabaserna slog igenom var procedurella sökprogram den metod som användes för databasutsökning. Man kan hävda att automatisk frågeoptimering aldrig kan ge bättre prestanda än ett optimalt manuellt programmerat procedurellt sökprogram. Faktum är att sådana argument restes då relationsdatabaserna började utvecklas. Till exempel var det samma företag, IBM, som tidigare utvecklat det ledande "procedurella" databassystemet, IMS, som började utveckla de första relationsdatabaserna i slutet av 1970-talet. IMS-utvecklarna kunde med fog hävda att man alltid kan göra ett manuellt procedurellt IMS-program som är lika snabbt som den optimala exekveringsplanen för motsvarande relationsdatabas. Relationsdatabasutvecklarna vid IBM stod således inför en rejäl utmaning att automatiskt generera lika bra exekveringsplaner som optimal IMS-kod. Utmaningen ledde till utveckling av så kallad kostnadsbaserad frågeoptimering, vilken är den dominerande metoden att optimera databasfrågor.
Kostnadsbaserad frågeoptimering: Kostnadsbaserad frågeoptimering bygger på att optimeraren har en inbyggd metod för att uppskatta kostnaden för en given exekveringsplan, en så kallad kostnadsmodell. Med kostnad menar man normalt den förväntade tiden för att köra frågan. Kostnadsmodellen är en matematisk modell för att uppskatta kostnaden att utföra en exekveringsplan, baserat på statistiska data om databasens innehåll samt kunskap om hur olika kommandon i en exekveringsplan uppför sig. Statistiska data kan till exempel vara att databassystemet vet hur många rader det finns i varje tabell eller hur datavärden är statistiskt fördelade i en kolumn. Vad kostnadsbaserad frågeoptimering går ut på är att från rymden av alla möjliga exekveringsplaner för en given databasfråga välja den billigaste med avseende på kostnadsmodellen. Problemet är här att antalet möjliga exekveringsplaner kan vara mycket stort och exponentiellt beroende av storleken på SQL-frågan, till exempel hur många villkor den har. Om frågans storlek är Q, är komplexiteten hos kostnadsbaserad frågeoptimering i värsta fall O(Q!) , alltså exponentiellt beroende av frågans storlek. Kostnadsbaserad frågeoptimering lönar sig i alla fall om databasen är stor eftersom en optimerad fråga kanske tar O(log(N )) att utföra, där N är databasens storlek, medan en ooptimerad fråga tar O(N 2). Det lönar sig därför att N >> Q.
576Heuristisk frågeoptimering: Som alternativ till kostnadsbaserad frågeoptimering kan man tänka sig att använda heuristisk frågeoptimering där man har ett antal tumregler för hur en exekveringsplan ska genereras. Heuristiska metoder har i allmänhet betydligt lägre komplexitet är kostnadsbaserad optimering (typiskt O(Q2)). Emellertid ska man ha klart för sig att en dålig exekveringsplan kan vara 1 000-tals gånger långsammare än en optimal plan och dålig optimering (till exempel med heuristiska metoder) kan leda till oacceptabla prestanda. För att vara konkurrenskraftiga måste således databasföretagen tillhandahålla mycket bra frågeoptimerare; det har funnits många exempel där en given databasfråga har blivit oacceptabelt långsam efter byte till databashanterare med sämre optimerare. Detta har lett till att databasföretagen utvecklat oerhört avancerade kostnadsbaserade optimerare för att vara konkurrenskraftiga. Dessa optimerare innehåller också en del heuristiska metoder för att snabba upp själva optimeringen, men dessa heuristiska metoder är noggrant analyserade så att de inte genererar oacceptabelt dyra exekveringsplaner.
Manuell frågeoptimering: Man kan fråga sig om det inte skulle duga att låta användaren explicit påverka optimeringen, till exempel genom att manuellt ordna om villkorsuttrycken i frågan. Sådan halvmanuell frågeoptimering tillämpades i tidiga relationsdatabassystem. Programmeringsspråket Prolog tillämpar en liknande metod där programmeraren explicit kan påverka effektiviteten genom att ordna om villkor. Det allvarligaste problemet med manuella metoder är att det blir mycket svårt att manuellt optimera en fråga som anropar vyer vilka i sin tur är manuellt optimerade. Vidare beror exekveringseffektiviteten till stor del på vilka interna datastrukturer och algoritmer som används vid frågeexekveringen. En modern databashanterare kan använda många olika sådana exekveringsalgoritmer så uppgiften för användaren att manuellt påverka exekveringsplanen kan bli överväldigande. Ytterligare ett problem är att exekveringsstrategin också kanske måste ändras om databasens innehåll ändras mycket. Efter stora uppdateringar som påverkar exekveringsstrategierna måste man således gå in och ändra alla manuella "hack" i alla påverkade frågor och vyer.
Figur 26.1 illustrerar de olika faserna av frågebearbetningen i en modern frågeoptimerare.
Först överför en parser SQL-frågan till en intern representation, i likhet med vad som sker för de flesta programmeringsspråk. Därvid sker kontroll att frågan är syntaktisk korrekt och inte innehåller typfel. Parse-trädet är väsentligen en intern representation av SQL-frågan i utvidgad relationskalkyl . SQL är mer kraftfullt än klassisk relationskalkyl, till exempel genom att arbeta med påsar (mängder med duplikat), null-värden, aggregeringsoperatorer, sortering, med mera. Därför kan inte klassisk relationskalkyl användas för att internt representera SQL, utan en utvidgad relationskalkyl måste användas. 578 Den utvidgade relationskalkylen är fortfarande deklarativ så det parsade uttrycket innehåller inte någon information om hur det ska exekveras. Eftersom den utvidgade relationskalkylen i princip är ekvivalent med motsvarande SQL-frågor använder vi nedan SQL-notation för att illustrera frågeomskrivningar.
Relationskalkylen förenklas och transformeras sedan av en frågeomskrivare, som gör olika transformationer av frågan. Transformationerna påverkar inte frågans resultat, men de är garanterade att förbättra dess prestanda. Exempel på viktig sådan transformation är vyexpansion där referenser till vyer ersätts med vydefinitionerna. Moderna optimerare gör också en hel del mer eller mindre avancerade andra frågeomskrivningar, till exempel för att ta bort ekvivalenta deluttryck i frågor.
En algebragenerator transformerar därefter relationskalkyluttrycket till ett relationsalgebrauttryck. För ett givet relationskalkyluttryck finns det en systematisk översättning (se nedan) till relationsalgebra. Relationsalgebran är ett funktionellt språk där de inbyggda funktionerna tar tabeller som argument och returnerar en tabell som resultat. 4 Vidare är relationsalgebran procedurell i den meningen att för ett givet relationsalgebrauttryck finns det en väl definierad ordning i vilket det ska exekveras. Liksom för relationskalkylen är den klassiska mängdorienterade relationsalgebran inte tillräcklig vare sig för att representera alla SQL-frågor eller för att duga som bas för frågeoptimering. Därför behövs en utökad relationsalgebra som innehåller fler operatorer än den traditionella, till exempel sortering, hantering av påsar (mängder med duplikat), null-värden, aggregeringsoperatorer, och eliminering av duplikat. Vi kallar detta utökad relationsalgebra. För att representera exekveringsplaner behövs ytterligare operatorer (funktioner) som tar hänsyn till olika fysiska lagringsstrukturer och index som används av databassystemet. Ibland används termen fysisk relationsalgebra, till skillnad från den vanliga logiska relationsalgebran.
Den systematiska översättningen till utökad relationsalgebra skulle kunna tolkas (interpreteras) direkt och ge korrekt svar på databasfrågan. Emellertid är den systematiska planen så gott som alltid icke-optimal. Därför utförs kostnadsbaserad frågeoptimering i samband med översättningen. Där appliceras ett antal heuristiska och kostnadsbaserade transformationer (omskrivningar) på det utökade relationsalgebrauttrycket, för att generera ett optimalt ekvivalent 579 uttryck. Frågeoptimeringen väljer vidare vilka algoritmer, till exempel för tabellhopslagning (join) som ska användas i den slutliga exekveringsplanen. Den utökade relationsalgebran innehåller således många olika join-operatorer. Den kostnadsbaserade frågeoptimeringen kan mycket radikalt förbättra frågeprestanda och är kritisk för effektiv frågeutförande. Principerna för kostnadsbaserad frågeoptimering kommer att förklaras närmare nedan.
I den sista fasen tolkas (interpreteras) det optimala utökade relationsalgebrauttrycket för att producera frågeresultatet. En viktig detalj här är att mellanresultat av algebraoperatorer kan vara mycket stora tabeller som inte får plats i primärminnet. Därför är tolkningen strömmad, 5 dvs raderna i resultaten från algebraoperatorerna produceras en åt gången snarare än att temporära tabeller byggs upp (eller materialiseras, som det kallas).
Persondata(pnr,namn)
Anställning(pnr,avdelning,lön)
En enkel fråga över dessa tabeller är:
select lön
from Persondata p, Anställning a
where p.pnr = a.pnr
and namn = "Kalle Persson"
Vi antar att det finns 100 000 rader i båda tabellerna. Vi antar vidare att det bara får plats 10 rader per diskblock 6 och att tabellens rader ligger lagrade sekventiellt på disk ordnat efter personnummer.
580För snabbast möjliga sökningar finns det index (B-träd) för samtliga kolumner i båda tabellerna. I vårt exempel antas varje nod rymma 100 nyckel/pekar-par. 7
För att illustrera principen för vyexpansion definierar vi en vy över våra exempeltabeller:
create view Löner
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xas select namn, avdelning, lön
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Persondata p, Anställning a
where p.pnr = a.pnr
En typisk fråga över ovanstående vy är:
select lön from Löner where namn = 'Kalle Persson'
Som tidigare nämnts är vyexpansion en viktig form av frågeomskrivning. Vyexpansion ersätter vyreferenser med dess definitioner. I exemplet ovan substituerar systemet in definitionen av vyn Löner i frågan, varvid man får följande omskrivna fråga:
select a.lön
from Persondata p, Anställning a
where p.pnr = a.pnr and
p.namn = 'Kalle Persson'
Vyexpansion gör det möjligt för den efterföljande kostnadsbaserade frågeoptimeraren att upptäcka alla index som kan påverka utformningen av den optimala exekveringsplanen. I detta fall kan optimeraren se att det finns index för kolumnerna pnr och namn i de lagrade tabellerna.
Vyer är ofta definierade i termer av andra vyer. I sådana fall expanderas alla vyer rekursivt ända tills enbart lagrade tabeller refereras i den expanderade frågan. Till exempel skulle vi kunna ha en annan vy:
581
create view Administratörslöner
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xas select namn, lön
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xfrom Löner
where avdelning = 'adm'
select lön from Administratörslöner
where namn = 'Kalle Persson'
I detta fall sker vyexpansion rekursivt i två steg, så att den slutliga expanderade frågan blir:
select a.lön
from Persondata p, Anställning a
where p.pnr = a.pnr and
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xa.avdelning = 'adm' and
p.namn = 'Kalle Persson'
När det gäller effektiv användning av index skiljer man mellan klustrade och oklustrade index, där klustrade index, till skillnad mot oklustrade index, har (i stort sett) samma ordning som raderna i tabellen. 8
582Figur 26.2 illustrerar ett klustrat index där varje diskblock antas ha plats för tre indexnycklar och två tabellrader. 9 I figuren ser vi fem datablock med rader, som vi kallar radblock, som indexeras av ett index med indexblock bestående av ett rotblock och tre lövblock. Radblocken är hoplänkade för snabb sekventiell genomsökning av tabellen. Antag att antal indexnycklar per block är I och att det finns card(T ) rader i tabellen. 10 Då har lägsta nivån i indexet card(T )/I block och antal nivåer i indexet, indexets djup D, blir logI (card(T )).I figur 26.3 är card(T ) = 9 och I = 3 och således är djupet log3(9) = 2. Antal block i indexet, B blir B = 1 + I1 + I2 + ... + ID−1 = (1 − ID )/(1 − I). Eftersom card(T ) = ID får vi följande formel för antal noder i indexet: B = (1 − card(T ))/(1 − I). Indexet i figur 26.2 är förenklat i den meningen att antal tabellposter (9) är en exponent av förgreningsfaktorn i indexet (3). I praktiken är inte situationen så ideal och våra formler blir då approximativa.
För klustrade index gäller att det att om man m.h.a. indexet söker reda på ett intervall av X indexnyckelvärden och det får plats med B tabellrader i ett diskblock, behövs det X/B läsningar av radblock för att hämta motsvarade tabellrader. Om man till exempel i figur 26.2 traverserar hela tabellen sekventiellt genom indexet måste man läsa 4 indexblock och 5 radblock. 11
Indexet i vårt exempel är något förenklat i den meningen att det antas att det är unikt, vilket innebär att för varje indexnyckel finns exakt en motsvarande rad. 12 I verkligheten finns ofta mer än en matchande rad, och då måste man också hantera detta. Om man har ett index över kolumnen med personnamn, kan flera personer ha samma namn. För att förenkla vår diskussion antar vi dock i fortsättningen att alla index är unika.
Figur 26.3 illustrerar motsvarande oklustrade index. Accesstiden för oklustrade index är långsammare än för klustrade index då oklustrade index slumpvis refererar till diskblock i tabellen. Antal indexnoder är detsamma som för klustrade index, men genomsökning av indexet i indexnyckelordning resulterar i att ett nytt block måste läsas för varje indexnyckel. Om man således m.h.a. indexet söker reda på X indexnyckelvärden, behöver man också läsa X diskblock för att hämta motsvarade tabellrader.
583Normalt är bara primärindexet klustrat. Således är indexen för pnr de enda som är klustrade i vårt exempel.
Med vårt antagande i exemplet att I = 100 finns det följande antal block på de olika indexnivåerna:
nivå | block |
---|---|
1 | 1 |
2 | 100 |
3 | 10 000 |
Totala antalet indexblock blir I = 1 + 100 + 10 000 = 10 101. Eftersom det fanns 100 000 rader i tabellen pekar varje lövblock i indexet till i genomsnitt 10 tabellrader. 13
Vi är nu mogna att diskutera betydelsen av kostnadsbaserad optimering och hur den fungerar. Kostnadsbaserad optimering tillämpas vanligen efter vyexpansion så att information av betydelse för optimeringen inte är gömd inuti vydefinitioner. Kostnaden påverkas mycket signifikant av tillgängliga indexstrukturer.
I princip utför kostnadsbaserad frågeoptimering följande steg:
584
Bara sådana exekveringsplaner som producerar korrekt svar på databasfrågan är möjliga exekveringsplaner.
Olika exekveringsplaner traverserar databasen i olika ordning med olika kostnad.
Detta resulterar i sin tur i olika ordning på de rader som produceras som resultat.
Eftersom resultatet av deluttryck i regel är påsar eller mängder av rader, har ordningen i allmänhet inte någon betydelse.
Om slutresultatet emellertid har en order by
-klausul är ordningen signifikant, och systemet kan då behöva införa en explicit sorteringsoperator i exekveringsplanen.
Figur 26.4 visar exempelfrågan i sektion 26.4 systematiskt översatt till relationsalgebra. Algoritmen för systematisk översättning är:
Den ooptimerade exekveringsplanen kan tolkas (interpreteras) direkt. Varje operator i algebraträdet producerar därvid en ström av rader 14 som representerar mellanresultat. För att uppskatta kostnaden att utföra planen måste vi för varje nod i algebraträdet räkna ut följande:
2. Totala kostnaden att utföra en algebraoperator beror på hur många rader som producerats i strömmen från argumentnoderna under. Kostnaden beräknas i termer av kostnadsenheter och inte direkt i riktiga sekunder. Vi antar genomgående att det kostar 1 000enheter att läsa 1 diskblock och 1 kostnadsenhet att läsa en rad från resultatström producerad av algebraoperator. 585
I vårt exempel har tabellerna Persondata och Anställning 105 rader var, dvs. 100 000/10 = 10 000 diskblock var. Vi får då följande uppskattningar:
586Den totala kostnaden blev 1011 + 1010 + 105 + 1. Om vi antar att 1 enhet tar 10 mikrosekunder (10−5 sekunder) att utföra, tar den ooptimerade frågan 1.1 miljoner sekunder (ca 306 timmar) att utföra.
En uppenbar ineffektivitet med planen i figur 26.4 är formeringen av kartesisk produkt. I exemplet lönar det sig att utföra en join för att matcha kolumnvärden i tabellerna som i figur 26.5. Det finns ett antal olika sätt att joina tabeller. I detta fall vet vi att raderna i båda tabellerna ligger lagrade sorterade i pnr-ordning. Vi kan därför läsa igenom raderna i Persondata och Anställning parallellt för att hitta matchande rader. Den joinmetoden kallas sort merge join 16 och beskrivs detaljerat senare. Joinoperatorn i den utvidgade relationsalgebran är därför markerad SMJ för att indikera att sort merge join ska användas. Kostnadsuppskattningen blir denna:
587Den totala kostnaden i detta fall är 20 000 000+100 000+1 = 20 100 001 enheter. Om vi fortfarande antar att 1 enhet tar 10 mikrosekunder (10−5 sekunder) att utföra, tar den här exekveringsplanen 200 sekunder att köra. Det är 5 500 gånger snabbare än den ooptimerade planen.
I vårt exempel är argumenten till joinoperatorn hämtade från databastabeller. Argument kan emellertid också vara delresultat levererade som strömmar från andra delplaner, vilket påverkar kostnaden. Till exempel kan det hända att delresultat är mycket litet eller osorterat, vilket starkt påverkar val av join-metod, etc. Optimeraren väljer rätt metod baserat på en uppskattning av storleken på delresultat.
588En viktig observation är nu att vi kan snabba upp exekveringen ytterligare genom att flytta ner selektionen av Kalle Persson som i figur 26.6. Det kallas selektionsnedflyttning (på engelska selection pushing). Selektionen kan i detta fall göras mycket effektivt genom att det finns B-trädsindex på namn som kan användas för att snabbt hitta Kalle Persson. I den utvidgade relationsalgebran betecknar vi sådan indexselektion med σI namn='kallePersson'.. Kostnadsuppskattning:
Den totala kostnaden blir i detta fall 8 001 enheter (motsvarande 0.08 sekunder), vilket är ca 14 miljoner gånger snabbare än ooptimerad kod. Exemplet visar således klart att frågeoptimering lönar 589 sig. Anledningen till den enorma prestandaförbättringen är att SQL-frågor utgör icke-procedurella specifikationer av sökningar över den interna representationen av relationstabellerna. För varje sådan specifikation (fråga) finns det många olika sökstrategier och olika strategier är optimala beroende på vilka data som finns i databasen. Vidare har olika sökstrategier olika komplexitet och frågeoptimeringen förbättrar således komplexiteten hos sökningen genom att ändra sökalgoritm.
En enkel komplexitetsanalys visar att i vårt exempel har den sämsta strategin i figur 26.4 komplexitet O(N2) där N är databasens storlek. 17 Efter elimination av kartesisk produkt går komplexiteten ner till O(N) i figur 26.5. 18 Den optimala strategin i figur 26.6 har komplexitet O(log(N )). 19
Komplexitetsförbättringen innebär att frågebearbetningen skalar upp exekvering av frågor, dvs. databasens storlek kan öka utan att databasfrågorna blir för långsamma, vilket är fallet om sökningen är O(N) eller O(N 2 ). Skalbarhet är centralt för databassystem.
Man kan sammanfatta optimeringen i det hittillsvarande exemplet med att vi tillämpat två tumregler: eliminering av kartesisk produkt och selektionsnedflyttning. Man kan förledas tro att det räcker med att tillämpa sådana heuristiska regler och att den mer noggranna kostnadsbaserade optimeringen inte behövs.
Låt oss variera vårt exempel så att vi inte har något index för namn. Då blir kostnaden för planen i figur 26.5 oförändrat 20 100 001 enheter.
För planen i figur 26.6 måste vi byta algebraoperatorn σI namn='kallePersson' (indexselektion) mot σI namn='kallePersson', vilket betecknar oindexerad selektion som läser igenom hela tabellen Persondata. Detta ökar kostnaden att selektera Kalle Persson drastiskt till kostaden 107 enheter då 104 block måste läsas.
Eftersom övriga kostnader för planen i figur 26.6 blir oförändrade blir den nya totala kostnaden 10 000 000 + 4 000 + 1 = 10 004 001. Det 590 är fortfarande dubbelt så snabbt som plan 26.5, så selektionsnedflytting lönar sig fortfarande, men med andra tabeller och andra data kan det hända att det faktiskt blir långsammare. Det är alltså inte säkert att en heuristik som selektionsnedflytting ger en snabbare plan.
Moderna optimerare grovklassificerar först den bearbetade frågan för att utröna huruvida selektionsnerflyttning lönar sig. Det lönar sig i allmänhet om den selekterade kolumnen är indexerad.
Det finns till och med fall då det lönar sig att behålla en kartesisk produkt framför att göra join. Antag till exempel att Anställning innehåller högst en rad. Då blir det billigast att göra kartesisk produkt. Sådant kan inträffa om indata produceras som resultat från underliggande delplan.
Det används vanligtvis tre olika joinmetoder i moderna databashanterare: sort merge join, nested loop join och hash join. Vi går nu igenom dessa algoritmer och jämför när de är användbara.
Ovanstående exempel illustrerade sort-merge-join. Pseudokoden för att göra sort merge join mellan tabellerna T och U (skrivs T ⋈ SMJ U)
{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xStream T, U, R;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xTuple t, u;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xopen(R,'o'); // Öppna resultatström R
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xopen(T,'i'); // Öppna 1:a inströmmen
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xopen(U,'i'); // Öppna 2:a inströmmen
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xt = next(T); // läs 1:a raden till t
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xu = next(U); // detsamma för u
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhile (not(eof(T)) and not(eof(U))) // så länge båda
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x// inströmmarna
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x// har mer data
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif(t.k > u.k) u = next(U); // om nyckelfältet är
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x// mindre i u så
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x// flytta fram U
591
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xelse if(t.k < u.k) t = next(T); // detsamma för T
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xelse emit(t + u, R); // skicka t och u
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x// konkatenerade som
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x// nästa resultatrad
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xclose(T);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xclose(U);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xclose(R); // stäng alla strömmar
}
Funktionen next
returnerar nästa rad i en ström. Alla strömoperationer är buffrade så att det finns en intern buffert för varje ström med storlek
20
B till vilken raderna först läses, vilket är viktigt för att snabba upp strömmar från disk, eller från andra noder i en distribuerad databas. Funktionen eof
testar om strömmen är slut. Funktionen emit
sänder ny rad till resultatström (här R
), som normalt också är buffrad så att data inte levereras till ovanförliggande operator förrän bufferten fyllts.
21
För att sort merge join ska fungera måste indata vara sorterade. Om så inte är fallet kan optimeraren lägga in explicit sorteringsoperator i exekveringsplanen. Kostnaden för sortering måste då tas med i kostnadskalkylen; i allmänhet lönar sig inte sort merge join om indata inte redan är sorterade. Detta beskrivs i sektion 26.7.4.
Om indata är strömmar mot tabeller med blockstorlek B, och kostnaden för att läsa diskblock är Bcost, 22 blir kostnaden Bcost*(card(T )+ card(U ))/B. Om indata är sorterade resultatströmmar från andra algebraoperatorer, och kostnaden för att accessa strömelement är Scost, 23 blir kostnaden Scost * (card(T) + card(U)) om vi antar att strömningen sker i primärminne.
Nested loop join T ⋈NLJ U traverserar ena operandströmmen. För varje rad där söker algoritmen matchande rad i den andra operanden. Algoritm:
{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xStream T, U, R;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xTuple t, u;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xopen(R,'o');
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xopen(T,'i');
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhile (not(eof(T)))
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xt = next(T);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xopen(U,'i');
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhile (not(eof(U))
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xu = next(U);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif(t.K == u.K) emit(t + u, R);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xclose(U);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xclose(T);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xclose(R);
}
Ovanstående definition används då man gör join över strömmar eller oindexerade tabeller.
Metoden är inte symmetrisk och den första strömmen (T
) bör vara mindre.
Kostnaden blir (card(T)/B+card(T)* card(U )/B) * Bcost om operanderna är tabeller och Scost * card(T) + Scost * card(T) * card(U) om de är strömmande mellanresultat.
Om en av tabellerna är tillräckligt liten för att få plats i primärminnet, är det mycket fördelaktigt att ha den som andra (inre) operand. Hur stor blir kostnaden då?
Den andra operanden refererar dock vanligtvis till tabell indexerad på jämförelsevillkoret. I sådana fall kan man använda en variant av nested loop join som utnyttjar indexet för att hitta matchande rader i den inre tabellen. Detta kallas indexerad nested loop join, T⋈ INLJ U. Algoritm:
593
{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xStream T, U, R;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xTuple t, u, r;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xopen(R,'o');
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xopen(T,'I');
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhile (not(eof(T)))
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xt = next(T);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xopenIndexScan(U, U.k, t.k);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhile (not(eof(U))
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xr = next(U);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xu = getRow(U,r);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xemit(t + u, R);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xclose(U);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xclose(T);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xclose(R);
}
I detta fall öppnar funktionen openIndexScan
en ström till ett index över namngiven indexerad kolumn k i tabellen U,
U.k,
som and-ra argument samt aktuellt nyckelvärde i indexet t.k
som tredje argument.
Funktionen next
hämtar i detta fall nästa matchande indexpost genom att traversera B-trädet.
Kostnad: Om varje nod i B-trädet innehåller I nyckel/pekar-par kommer B-trädet att ha djup log I (card(U)) och det behövs således log I (card(U))+1 diskblocksläsningar för att traversera B-trädet och hämta en rad från U . Med selektivitet av ett villkor P, s(P ), avser vi hur stor andel rader som finns kvar efter det att P applicerats. Om selektiviteten av join-villkoret är s(J (T, U )) kommer det i genom-snitt att finnas card(U ) * s(J (T, U )) matchande rader i U för varje rad i T . Om B betecknar antal rader per block som förut blir totala kostnaden:
Bcost*(card(T )/B +card(T )*card(U )*s(J (T, U ))*(log I (card(U ))+1)).
I specialfallet att exakt en rad matchar (som vi antog i vårt exempel) är card(U ) * s(J (T, U )) = 1. Vi får då kostnaden:
Bcost * (card(T )/B + card(T ) * (log I (card(U )) + 1))
Vi antar här i våra kalkyler att alla block måste läsas in i minnet när de accessas; i praktiken har databashanteraren plats för ett antal 594 block i primärminnet, vilket minskar kostnaden. I synnerhet lönar det sig att alltid hålla rotblocket i index i minnet då de ju alltid måste traverseras för att hämta nya data genom indexet. Hur stor blir kostnaden då?
{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xStream T, U, R;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xTuple t, u;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xHashTable h;
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xopen(R,'o');
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xh = createHashTable();
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xopen(T, 'i');
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhile (not(eof(T)))
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xt = next(T);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xputHash(h,t.k,t);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xopen(U, 'i');
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xwhile (not(eof(U))
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x{
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xu = next(U);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xt = getHash(h, u.k);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xif(t != NULL) emit(t + u, R);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x}
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xclose(U);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xclose(T);
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xclose(R);
}
Problemet med hashning är att metoden kräver att hashtabellen helt får plats i primärminnet; i annat fall uppstår "thrashing", vilket innebär att delar av processens primärminnesdata flyttas fram och tillbaka mellan primärminnet och disken, med mycket dåliga prestanda som följd. Det finns dock partitionerade varianter av hash join som hanterar detta också.
595Kostnad: Bcost * (card(T)/B + card(U)/B) under förutsättning att hashtabellen h får plats i primärminnet och att båda operanderna är tabeller. Vad blir kostnaden om de är strömmar?
order by
-klausul. Ofta kan systemet dock utnyttja att det finns B-trädsindex på olika fält för att generera rader i en viss ordning.
Eftersom det i allmänhet inte finns minne för ett helt mellanresultat måste man använda så kallad samsortering (på engelska external sorting eller merge sort) där materialet gås igenom i ett antal faser där man blandar (eng. merge) ökande sorterade sviter parvis. Samsortering har i allmänhet kostnad Bcost * N/B * logB (N ), där N = card(T). Antal faser är log B (N) eftersom B rader får plats i minnet åt gången för sortering där och N/B block måste läsas i varje fas. Notera att B i praktiken kan vara ett ganska stort tal, till exempel 500. Hur många faser behövs det då för att sortera 108 rader?
Försäljning(pnr, namn, kr)
Vad man kanske vill ha reda på är vilka som säljer mest och man ställer därför denna fråga:
select pnr, namn, kr
from Försäljning
order by kr descending;
Antag att vi sätter ett (oklustrat) B-trädsindex på kr. Vi jämför planerna i figur 26.7 och 26.8.
I figur 26.7 utnyttjar vi index scan av B-trädsindexet på kr (ISkr ) för att slippa sortera. Antag att vi har 100 000 rader med 10 rader per radblock och 100 rader per indexblock som förut. Vi måste traversera hela indexet, vilket kräver 1 + 100 + 10 000 = 10 101 diskblock, dvs 107 kostnadsenheter. Eftersom indexet är oklustrat måste emellertid dessutom 1 diskblock läsas för varje rad i tabellen, dvs 105 block, vilket kostar 108 kostnadsenheter.
596Således blir kostnaden för IS kr 1.01 * 10 8 .
Nu jämför vi hur mycket det kostar att läsa igenom tabellen sekventiellt och sedan sortera med flerfasig samsortering. Eftersom det bara får plats 10 rader att sortera i primärminnet åt gången kan vi anta att sorteringen kan göras i fem pass med 10 000 lästa block i varje pass (först 10 000 sviter, därefter 1 000, därefter 100, därefter 10, och därefter resultatsviten) och således måste 5 000 block läsas, vilket kostar 5 * 107 kostnadsenheter vilket är 5 gånger billigare än att traversera det sorterade, men oklustrade, indexet.
Det kan vara lämpligt att ordna raderna i sina tabeller enligt den sorteringsordning som vanligtvis behövs. SQL ger möjlighet till att definiera sådan explicit ordning när man skapar tabellen.
En exekveringsplan är ett funktionellt uttryck i utvidgad relationsalgebra. Den utvidgade relationsalgebran är ett parameterfritt funktionellt språk som utnyttjar länkade block, index och andra datastrukturer som representerar databasens innehåll. Alla operatorer är i allmänhet strömmade. Strömning gör att mellanresultat i allmänhet använder begränsade minnesresurser. I vissa lägen materialiseras dock mellanresultat som temporära tabeller när det är fördelaktigt, m.h.a. en speciell materialiseringsoperator, MAT (S), som tar en ström S som argument och lagrar dess resultat i en tillfällig tabell, mot vilken resultatströmmen sedan kopplas.
För att uppskatta kostnaden av en exekveringsplan behöver kostnadsmodellen beräknas baserat på statistik av databasens innehåll. Bland annat följande statistik upprätthålls av databashanteraren:
Kostnaden är exempelvis mycket beroende av hur mycket data som produceras av en given relationsalgebraoperator, och därför ingår det i kostnadsmodellen att uppskatta hur selektivt varje villkor är, dvs selektivitet, s(pred). Selektiviteten uppskattas m.h.a. databasstatistiken. Till exempel med rektangulärfördelade data i T.c blir
s(T.c ='x') = 1/D(T.c). I vårt exempel: om vi antar att alla namn är unika, är selektiviteten av villkoretnamn = 'Kalle Persson'
1/100 000. För att selektivitet av olikheter, till exempel villkor som namn>'I' and namn<'K'
utnyttjar optimeraren kunskap om datafördelningen i kolumnen.
Databasstatistiken uppdateras genom ett systemprogram som läser alla tabeller för att samla in statistik. Eftersom databasstatistiken inte ändras speciellt intensivt kan statistikinsamlingen köras i bakgrunden vid lämpliga tillfällen, till exempel en gång i månaden eller efter stora inladdningar av data i databasen. Databasadministratören kan bestämma när statistikinsamling ska ske.
Kostnadsmodellen uppskattar kostnaden som ett vägt medelvärde av dessa faktorer. Normalt dominerar tiden att läsa block från disk.
För att korrekt uppskatta kostnaderna är följande information av stor betydelse:
Man kan här konstatera att komplexa statistiska modeller ger precisa optimerare, till priset av kostnaden att beräkna statistiken. Databasstatistiken behöver emellertid inte vara exakt då den inte i första hand används för att exakt uppskatta kostnaden av ett uttryck, utan snarare är till för att jämföra kostnader för olika strategier. Om databasstatistiken är inaktuell, blir frågorna eventuellt långsammare, men svaren är fortfarande korrekta. Moderna kostnadsmodeller är därför designade att producera användbar statistik för lägsta möjliga beräkningskostnad. Man har till exempel utvecklat metoder att inkrementellt upprätthålla approximativa histogram över datafördelningar i kolumner.
Kostnadsbaserad optimering är NP-komplett över frågans storlek, Q (dvs komplexitet O(2 Q ) eller sämre). Så kallad dynamisk programmering snabbar upp kostnadsbaserad optimering jämfört med naiv generering av alla möjliga exekveringsplaner, men komplexiteten är fortfarande NP-komplett fast O(Q 2 ) i bästa fall. Detta har till följd att kostnadsbaserad optimering bara kan tillämpas på relativt enkla frågor; det brukar i allmänhet fungera mycket bra upp till 8 join. För större uttryck gör optimerarna i allmänhet bara partiell kostnadsbaserad optimering och tillämpar heuristiska metoder på delar av frågan, eller lämnar delar av frågan ooptimerad.
Heuristiska metoder för sökning bland exekveringsplanerna i samband med kostnadsbaserad optimering (ej att förväxla med den typ av heuristisk optimering som går ut på att göra om exekveringsplanen enligt tumregler) baseras på att starta med en initial plan och sedan systematiskt modifiera den så länge som kostnaden går ner bland intilliggande planer (hill climbing). Man får då förstås definiera vad man menar med 'intilliggande' så att de kan genereras systematiskt. De har i allmänhet komplexitet O(Q 2 ) men kan ge suboptimala exekveringsplaner.
601Slumpmässiga metoder (även kallade Monte Carlo-metoder) genererar i princip slumpmässigt olika exekveringsplaner, uppskattar deras kostnader m.h.a. kostnadsmodellen, och väljer den billigaste. Slumpmässiga metoder har fördelen att de konvergerar mot en optimal plan och kan därför avbrytas när som helst. En bra strategi är att slumpmässigt generera en plan och sedan göra heuristisk hill climbing för att hitta den billigaste planen som kan nås så länge kostnaden går ner.
Fråga (engelska: query). En deklarativ specifikation, uttryckt i ett frågespråk som SQL, av det svar man önskar få från en sökning i databasen.
Exekveringsplan (engelska: execution plan). En steg-för-stegbeskrivning av hur en fråga ska köras av databashanteraren. En och samma fråga brukar kunna översättas till många olika exekveringsplaner.
Frågeoptimering (engelska: query optimization). Processen när databashanteraren väljer bland de olika exekveringsplaner som en fråga kan översättas till, för att hitta den snabbaste. (Eller i alla fall en som är tillräckligt snabb.)
Heuristisk frågeoptimering (engelska: heuristic query optimization). Frågeoptimering som görs genom att databashanteraren tillämpar några tumregler för hur en exekveringsplan ska se ut, till exempel att operationen selektion (σ) bör utföras före operationen join (⋈). Heuristisk frågeoptimering är relativt enkel och snabb att utföra, jämfört med kostnadsbaserad frågeoptimering, men är å andra sidan sämre på att hitta optimala exekveringsplaner. Tar normalt inte hänsyn till lagringsstrukturer, eller alternativa algoritmer för de operationer som ska utföras.
Kostnad (engelska: cost). Kostnaden för att köra en fråga kan mätas på olika sätt, men för det mesta är det bara den förväntade tiden att köra frågan som man bryr sig om. Kostnaden kan räknas i sekunder, eller i mer abstrakta kostnadsenheter. I en vanlig diskbaserad databas är det åtkomst av disken som tar mest tid, och kostnaden kan därför också räknas i antalet diskaccesser.
602Kostnadsbaserad frågeoptimering (engelska: cost-based query optimization). Frågeoptimering som görs genom att databashanteraren jämför den uppskattade kostnaden för olika exekveringsplaner, och väljer den billigaste (dvs snabbaste). Kostnadsbaserad frågeoptimering är relativt komplicerad och långsam att utföra, jämfört med heuristisk frågeoptimering, men är å andra sidan bättre på att hitta optimala exekveringsplaner.
Kostnadsmodell (engelska: cost model). Den matematiska modell som en kostnadsbaserad frågeoptimerare använder för att uppskatta kostnaden för en exekveringsplan.
6 I verkligheten beror antalet rader som får plats per diskblock på längden av raderna. Vidare kan det finnas oanvänt utrymme i varje block. För att förenkla beskrivningarna antar vi dock här alltid att antalet rader per block är konstant.
9 Blockstorlek tre i bilden är vald för att förenkla illustrationen. I vårt räkneexempel nedan antar vi i stället den mer realistiska blockstorleken 100 för antal indexnycklar per block, samt 10 rader per block.
15 Det blir billigare om man kan läsa in någon av eller bägge tabellerna i primärminnet först. Vi antar emellertid för enkelhets skull genomgående att det bara finns plats för ett diskblock i primärminnet och att det således inte finns tillräckligt primärminne för att läsa in några tabeller alls i förväg.
En databas är en samling data som hör samman på något sätt. En distribuerad databas är också en samling data som hör samman på något sätt, men där dessa data är fysiskt utspridda på flera olika datorer som är sammankopplade med ett datornät. En distribuerad databashanterare är det program, eller system av program, som hanterar den distribuerade databasen. På engelska kallas det DDBMS eller Distributed Database Management System. Motsatsen till en distribuerad databas är en "vanlig" eller "centraliserad" databas, där hela databasen lagras på en enda dator.
• "Riktiga" distribuerade databaser som erbjuder placeringstransparens. Det betyder att man kan skriva frågor som om det var en vanlig centraliserad databas, utan att behöva ange var databashanteraren ska hämta data ifrån. För att klara detta måste databashanteraren ha en hårdare kontroll över var data lagras, och det måste finnas ett globalt schema, som beskriver hela databasens uppbyggnad.
Här är några viktiga termer som används i samband med distribuerade databassystem:
Vi kommer i det här kapitlet att förutsätta att den distribuerade databasen är en relationsdatabas, men liknande tekniker kan även användas för andra modeller, till exempel för objektorienterade databaser. NoSQL- och molndatabaser är ofta distribuerade, men här går vi igenom grunderna, och använder relationsdatabaser i exemplen.
Fördelar med distribuerade databaser, jämfört med ett centraliserat system:
Det finns också nackdelar med distribuerade databaser:
• Det är också svårare att bygga en databashanterare för distribuerade databaser:
När man konstruerar en vanlig, centraliserad databas brukar man börja med att göra en konceptuell beskrivning, till exempel ett ER-diagram. Sen översätter man den konceptuella beskrivningen till den datamodell som databashanteraren använder, oftast relationsmodellen med tabeller. Till slut väljer man fysiska lagringsstrukturer.
I fallet med en distribuerad databas tillkommer ett steg till, nämligen att bestämma
Normalt är det inte någon person som sitter och bestämmer att den här raden ska hit och den där raden ska dit, utan databashanteraren har fått en uppsättning regler som talar om hur det ska göras. De reglerna utgör ett fragmenteringsschema och samtidigt att placeringsschema. På samma sätt kan man tala om ett replikeringssche-ma.
Fragmentering eller sharding 2 på engelska kan göras per tabell, så att en tabell lagras helt och hållet på en viss dator. En tabell kan också delas upp och lagras på olika ställen, och då brukar man skilja mellan horisontell och vertikal fragmentering. Vid horisontell fragmentering delar man tabellen med ett eller flera "horisontella streck", och placerar hela rader på olika platser. Vid vertikal fragmentering delar man i stället upp tabellen med "vertikala streck", och placerar kolumnerna på olika ställen.
Ett fragmenteringsschema för en tabell skulle, till exempel, kunna tappa bort en del av raderna genom att inte lägga dem i något av fragmenten. Den sortens felaktig fragmentering vill man förstås undvika, och därför måste den som skapar ett fragmenteringsschema tänka på följande tre villkor för korrekt fragmentering:
Det enklaste sättet att fragmentera en tabell är att man placerar olika rader på olika ställen enligt en regel som tittar på de data som finns på varje rad. Vi tänker oss att tabellen Avdelningar ska fragmenteras:
Avdelningar | ||
---|---|---|
Anr | Anamn | Stad |
1 | Data | Stockholm |
2 | Städning | Tokyo |
3 | Ekonomi | Tokyo |
4 | Reklam | Stockholm |
Beroende på var varje avdelning är placerad, placerar vi den avdelningens rad i rätt fragment. Data om avdelningar i Stockholm ska läggas i fragment 1, kallat Avdelningar1, och data om avdelningar i Tokyo ska läggas i fragment 2, kallat Avdelningar2:
Avdelningar1 | ||
---|---|---|
Anr | Anamn | Stad |
1 | Data | Stockholm |
4 | Reklam | Stockholm |
Avdelningar2 | ||
---|---|---|
Anr | Anamn | Stad |
2 | Städning | Tokyo |
3 | Ekonomi | Tokyo |
En sån här uppdelning skulle man kunna ha om man hade två noder: en i Stockholm och en i Tokyo. Data som handlar om de avdelningar som finns i respektive stad hamnar också på noden i den staden.
Om det här fragmenteringsschemat med sina två fragment ska vara korrekt, måste regeln om fullständighet vara uppfylld. Det innebär att de enda städer som får förekomma i tabellen Avdelningar är just Stockholm och Tokyo.
I exemplet hade vi bara två fragment, men man kan ha många fler.
Den ursprungliga tabellen måste gå att återskapa, och man brukar tala om ett återskapandeprogram (på engelska reconstruction program). Återskapandeprogrammet är ett relationsalgebrauttryck som anger hur fragmenten ska sättas ihop för att vi ska få ett resultat som är lika med den ursprungliga, ofragmenterade tabellen. I fallet horisontell fragmentering är det unionen av fragmenten, alltså den sammanlagda mängden av rader från alla fragmenten:
Återskapandeprogrammet kallas också materialiseringsprogram.
Personer | ||
---|---|---|
Pnr | Pnamn | JobbarPå |
1 | Svea | 1 |
2 | Sten | 3 |
3 | Bengt | 1 |
4 | Olle | 2 |
5 | Lotta | 2 |
Avdelning 1 och4 låg ju i fragment Avdelningar1, medan avdelning 2 och 3 låg i fragment Avdelningar 2 . Om vi nu delar upp tabellen Personer efter detta, får vi två fragment: Personer 1 och Personer 2:
Personer1 | ||
---|---|---|
Pnr | Pnamn | JobbarPå |
1 | Svea | 1 |
3 | Bengt | 1 |
Personer2 | ||
---|---|---|
Pnr | Pnamn | JobbarPå |
2 | Sten | 3 |
4 | Olle | 2 |
5 | Lotta | 2 |
Det man gör är alltså att man behåller de rader i Personer som vid en join skulle gå att kombinera med någon rad i respektive fragment av Avdelningar. Den operationen är praktisk i samband med distribuerade databaser, och har därför fått ett eget namn. Den kallas semijoin, och skrivs med en "öppen" join-symbol: ⋉. "Semi" betyder "halv" på svenska (jämför till exempel med "semifinal"), och man skulle kunna kalla operationen för "halvjoin". Det är ju en join där man bara får halva svaret, nämligen kolumnerna från den första av de två joinade tabellerna.
613Med semijoin kan vi skriva fragmenteringsschemat för Personer så här:
Vid vertikal fragmentering av en tabell placeras olika kolumner på olika noder. Eftersom raderna i en tabell i en relationsdatabas inte har någon speciell ordning, måste man upprepa primärnyckeln i varje fragment, för att det ska gå att se vilka rader som hör ihop.
Vi skulle som exempel kunna dela upp tabellen Personer i två vertikala fragment:
PersonerA | |
---|---|
Pnr | Pnamn |
1 | Svea |
2 | Sten |
3 | Bengt |
4 | Olle |
5 | Lotta |
PersonerB | |
---|---|
Pnr | JobbarPå |
1 | 1 |
2 | 3 |
3 | 1 |
4 | 2 |
5 | 2 |
Återskapandeprogrammet för en vertikal fragmentering består av en join mellan fragmenten, med villkoret att primärnyckeln från de båda fragmenten ska vara lika. En join är ju just en operation som kombinerar ihop rader som hör ihop. För att slippa att kolumnen (eller kolumnerna) i primärnyckeln kommer med två gånger, kan vi använda operationen naturlig join:
Man kan kombinera både horisontell och vertikal fragmentering. Det kallas hybridfragmentering. Man kan se det som två eller flera fragmenteringar efter varandra, och behandla det så, men det kan vara mer effektivt att i ett verkligt system behandla en hybridfragmentering som en enda operation.
Vi tänker oss nu att den distribuerade databasen har ett globalt schema bestående av de "hela" eller "ursprungliga" tabellerna, som Personer och Avdelningar. Vi kallar dem för globala tabeller. En fråga mot databasen, som är formulerad i termer av det globala schemat och alltså använder de globala tabellerna, kan inte köras direkt. De globala tabellerna finns ju inte, annat än som något som kan återskapas från fragmenten. Därför måste frågan med de globala tabellerna översättas till en fråga som arbetar med fragmenten.
Ett återskapandeprogram var ju ett relationsalgebrauttryck som bygger ihop en global tabell utifrån dess fragment. Det enklaste (men inte smartaste) sättet att lokalisera en fråga är att ersätta varje global tabell med dess återskapandeprogram.
615Som exempel tar vi den här SQL-frågan, som ger namnet på de avdelningar som finns i Tokyo:
select *
from Avdelningar
where Stad = 'Tokyo';
Så här kan man översätta SQL-frågan till relationsalgebra:
Återskapandeprogrammet för Avdelningar var Avdelningar1∪Avdelningar2. Genom att sätta in det i frågan får vi:
Fragmentet Avdelningar1 finns i Stockholm och fragmentet Avdelningar 2 finns i Tokyo. Vi kan rita upp beräkningsgången så här. Notera att vi alltså börjar med att återskapa den globala tabellen, och sen genomför vi den selektion av rader som uttrycktes i SQL-frågan:
I figuren ser det ut som om data skickas från Tokyo och Stockholm nånstans ut i luften, där union-operationen sen utförs. I verkligheten kommer man snarare att skicka en del data från Tokyo till Stockholm, eller kanske tvärtom, och sen utföra beräkningar på någon av noderna, men det tar vi upp mer om i avsnittet om optimering här nedan. Nu koncentrerar vi oss bara på vilka operationer som utförs, inte var de utförs.
Den naiva lokaliseringen i föregående avsnitt är inte det bästa sättet att köra frågan. I stället kan vi skriva om relationsalgebrauttrycket, notera att vissa deluttryck ger resultat som inte innehåller några rader, och sen reducera frågan genom att ta bort dessa deluttryck.
Från vanlig algebra med tal känner vi igen regeln att uttrycket x(y+z) är ekvivalent med xy + xz. På liknande sätt är relationsalgebrans σvillkor (R ∪ S) ekvivalent med σ villkor (R) ∪ σ villkor (S). Vi kan utnyttja det till att skriva om
σ Stad=" Tokyo" (Avdelningar 1 ∪ Avdelningar 2 )
σ Stad="Tokyo" (Avdelningar 1 )∪σ Stad="Tokyo" (Avdelningar 2 )
Men eftersom all information om Tokyo-avdelningar lagras i fragmentet Avdelningar 2 , på Tokyo-noden, vet vi att deluttrycket σ Stad="Tokyo" (Avdelningar 1 ) inte kommer att ge några rader i resultatet. Alltså kan vi reducera bort det ur uttrycket, som nu blir
σ Stad="Tokyo" (Avdelningar 2 )
617
select Pnamn, Anamn
from Personer, Avdelningar
where JobbarPå = Anr;
Så här kan man översätta SQL-frågan till relationsalgebra:
π Pnamn,Anamn (Personer ⋈ JobbarPa=Anr Avdelningar)
Återskapandeprogrammet för Personer var Personer 1 ∪ Personer 2 , och återskapandeprogrammet för Avdelningar var Avdelningar 1 ∪ Avdelningar 2 . Genom att sätta in dem i frågan får vi:
π Pnamn,Anamn [(Personer 1 ∪Personer 2 )⋈ JobbarPa=Anr (Avdelningar 1 ∪ Avdelningar 2 )]
618Om fragmenten Anstalld 1 och Avdelningar 1 finns i Stockholm, och fragmenten Anstalld 2 och Avdelningar 2 finns i Tokyo, kan vi rita upp beräkningsgången så här. Notera att vi alltså börjar med att återskapa de två globala tabellerna, och sen genomför den join som uttrycktes i SQL-frågan:
Från vanlig algebra med tal känner vi igen regeln att uttrycket x(y + z) är ekvivalent med xy + xz. Vi såg tidigare att en liknande regel finns i relationsalgebran: σ villkor (R ∪ S) är ekvivalent med σ villkor (R) ∪ σ villkor (S). Ännu en liknande regel är att R ⋈ villkor (S∪T) är ekvivalent med (R⋈ villkor S) ∪ (R⋈ villkor T).
619(R⋈ villkor T)∪(R⋈villkor U)∪(S⋈villkor T)∪(S⋈villkor U).
Därför kan vi skriva om det naivt lokaliserade uttrycket i exemplet,
π Pnamn,Anamn [(Personer 1∪Personer 2)⋈ JobbarPa=Anr (Avdelningar 1∪Avdelningar 2)]
π Pnamn,Anamn [(Personer 1⋈ JobbarPa=Anr Avdelningar 1)∪
(Personer 1⋈JobbarPa=Anr Avdelningar 2)∪
(Personer 2⋈ JobbarPa=Anr Avdelningar 1)∪
(Personer 2⋈ JobbarPa=Anr Avdelningar 2)
Eftersom tabellen Anställd var härlett horisontellt fragmenterad baserat på den primära horisontella fragmenteringen av tabellen Avdelningar, inser vi att inga Tokyo-anställda kan finnas i Stockholmsfragmentet, och inga Stockholms-anställda kan finnas i Tokyo-fragmentet. Därför kommer de båda deluttrycken
(Personer 1⋈ JobbarPa=Anr Avdelningar 2)
(Personer 2⋈ JobbarPa=Anr Avdelningar 1)
inte att ge några rader i resultatet, och de kan därför reduceras. Uttrycket blir nu:
π Pnamn,Anamn [(Personer 1⋈ JobbarPa=Anr Avdelningar 1)∪[(Personer 2⋈ JobbarPa=Anr Avdelningar 2)∪
Vi ritar upp den reducerade frågan:
620Det återstår dock att mer i detalj bestämma hur uttrycket ska beräknas. Vi ska ju inte (som vi skojade om på sidan 616) skicka data nånstans ut i luften, och sen utföra beräkningarna där, utan data måste skickas via datornätet från en nod till en annan, och varje beräkning måste utföras på en nod. (Det behöver dock inte vara någon av de två noderna i Tokyo och Stockholm, utan resultatet kanske ska levereras till en tredje nod, och då kanske det blir bäst att utföra beräkningarna där.)
Vi har sett att en SQL-fråga i en distribuerad databas, precis som SQL-frågor i vanliga centraliserade databaser, kan skrivas om till ett relationsalgebrauttryck. Relationsalgebrauttrycket kommer att innehålla globala tabeller. Genom att lokalisera uttrycket kan vi översätta det till ett uttryck som innehåller fragment, och vi kan även förenkla uttrycket genom att reducera bort deluttryck som vi vet inte ger några resultat.
621I kapitel 26, Frågebearbetning, studerade vi hur en databashanterare optimerar frågor, dvs hur databashanteraren väljer det (ungefär) snabbaste av flera möjliga sätt att köra frågan. I en centraliserad, diskbaserad databas är det läsning och skrivning på disken som är långsammast, och som man därför måste koncentrera sig på att minimera. I en distribuerad databas är det inte kommunikationen med disken som är långsammast, utan kommunikationen på datornätet, när data eller styrmeddelanden skickas från en nod till en annan. 3 Därför går frågeoptimering i en distribuerad databashanterare ut på att minimera nätkommunikationen.
Vi börjar med att glömma bort det där med fragmentering. När vi kommit så långt som att vi har ett lokaliserat och reducerat relationsalgebrauttryck, som arbetar med lagrade (del-)tabeller som Personer 1 och Avdelningar 2, spelar det ingen roll om de där tabellerna (Personer 1 med flera) är fragment eller om de är vanliga, "hela" tabeller. Optimeringen, att bestämma det mest effektiva sättet att exekvera uttrycket, blir likadan. Därför struntar vi från och med nu i fragmenteringen, och talar bara om tabeller i allmänhet.
En fråga som arbetar med flera tabeller innehåller (normalt) flera olika join-operationer, och vid frågeoptimering brukar det viktigaste problemet vara att bestämma i vilken ordning dessa joinoperationer ska utföras. Vi ska visa hur en distribuerad databashanterare kan optimera en fråga genom att bestämma ordningen mellan join-operationerna (på engelska join ordering).
Vi tänker oss att den distribuerade databasen innehåller de tre tabellerna A (för Arbetare), D (för Deltar) och P (för Projekt), som befinner sig på tre olika noder. En SQL-fråga ska köras:
select *
from A, D, P
where A.Anr = D.Anr
and D.Pnr = P.Pnr
622
eller, om vi låter joinvillkoren vara underförstådda,
Vi ritar upp databasen som en bild, tillsammans med de två joinoperationer som ska utföras:
Om vi funderar lite kan vi notera två saker:
Sammantaget finns det en mängd olika sätt att köra frågan. Om vi antar att frågan initierades från nod 1, och att det därför är just på nod 1 som svaret ska produceras, kan vi tänka oss bland annat följande fyra sätt:
1. Skicka P från nod 3 till nod 2.
2. Skicka P från nod 2 till nod 3.
3. Skicka D från nod 2 till nod 1.
4. Skicka A från nod 1 till nod 2.
Skicka resultatet av A⋈D från nod 2 till nod 3.
För att bestämma vilken av strategierna som är effektivast, dvs snabbast, måste man ta hänsyn till:
En del join-beräkningar i en distribuerad databas kan effektiviseras genom användning av semijoin.
624Med vanliga join-operationer har vi tre sätt att utföra beräkningen på:
3. Skicka R från nod 1 till en tredje nod, nod 3.
Om vi bara tittar på mängden data som skickas, och ignorerar att resultatet förmodligen också behöver skickas vidare till det ställe där det ska användas, är optimeringen ganska enkel. Om tabell R innehåller mindre data än tabell S, är fall 1 att föredra, annars fall 2. I fall 3 skickas båda tabellerna, så det fallet är sämst.
Man behöver dock inte skicka hela tabeller. Så här kan man göra i stället:
2. Skicka πA (S) från nod 2 till nod 1.
Det som skickas är alltså bara den kolumn (eller de kolumner)
Resultatet, R⋈R.A=S.A S, är (om vi tar bort dubblettkolumnen A) också lika med σR.* (R⋈ R.A=S.A S), vilket är samma sak som semijoinen mellan R och S: R⋉R.A=S.A S. 625
Det som beräknas här är alltså de kolumner i det sökta joinresultatet som härrör från tabell R.
På det här sättet överförs två delresultat, π A (S) och R⋉ R.A=S.A S, över datornätet. Om den sammanlagda mängden data i dessa två delresultat är mindre än datamängden i den minsta av de två tabellerna R och S, tjänar man på att använda semijoin.
En transaktion är, som beskrivs i kapitel 24 och 25, en följd av operationer som hör ihop som en enhet. Om man till exempel flyttar pengar från ett bankkonto till ett annat, innebär det att man subtraherar pengar från det ena kontot, och adderar dem till det andra. Uppenbarligen hör dessa båda operationer ihop.
Det kan vara svårt nog att garantera ACID-egenskaperna i en vanlig, centraliserad databas. Om strömmen går mitt under transaktionen, måste databashanteraren senare gå igenom loggfilen och bland annat se till att inga halvfärdiga ändringar finns kvar i databasen. I en distribuerad databas blir det ännu svårare. En eller flera av noderna kan krascha, medan andra noder fortsätter sitt arbete. Datornätet kan sluta fungera, så att noderna visserligen fortfarande är i gång, men inte längre kan kommunicera med varandra. Även under dessa omständigheter måste databashanteraren upprätthålla ACID-egenskaperna.
På samma sätt som man kan tala om ett globalt schema som beskriver hela databasen, och som består av flera lokala scheman, kan man tala om globala transaktioner. Själva arbetet i en global transaktion utförs av de deltagande noderna, i form av flera lokala (del)transaktioner. Om man flyttar pengar från ett bankkonto till ett annat, och de två bankkontona är placerade på olika noder, kommer en lokal deltransaktion att subtrahera pengar från det ena kontot, och en annan lokal deltransaktion att addera dem till det andra. Den globala transaktionen ska ha ACID-egenskaperna, så att den till exempel blir atomär, och det kräver en del samordning mellan de olika lokala deltransaktionerna.
För att hindra flera transaktioner från att samtidigt arbeta med samma dataobjekt 4 på ett sätt som ger oönskade resultat, till exempel att transaktionerna oavsiktligt skriver över varandras ändringar, använder de flesta databashanterare lås av olika slag (se avsnitt 25.19).
Den enklaste formen av lås låter en transaktion låsa ett dataobjekt, och därefter får inga andra transaktioner arbeta med det dataobjektet. Om någon annan transaktion också vill arbeta med dataobjektet, får den vänta tills den första transaktionen är färdig med sitt arbete, och låser upp låset igen. De flesta databashanterare skiljer dock på lås för läsning och skrivning. Flera olika transaktioner kan utan problem samtidigt läsa samma dataobjekt, men för att ändra i objektet måste en transaktion ha ensam tillgång till det.
För att garantera att transaktionerna är isolerade från varandra, så kallad serialiserbarhet, använder man sig av tvåfaslåsning, som beskrivs i avsnitt 25.19.3. Tvåfaslåsning innebär, enkelt uttryckt, att så fort en transaktion har låst upp något av sina lås, får den inte låsa några nya objekt.
Det fungerar likadant i globala transaktioner, men med det förtydligandet att det gäller alla lås i hela transaktionen. Det räcker alltså 627 inte att använda tvåfaslåsning lokalt inom varje deltransaktion, utan så fort någon lokal deltransaktion har låst upp ett lås, får ingen annan deltransaktion låsa ytterligare dataobjekt.
Detta skulle kunna innebära en del problem med att synkronisera deltransaktionerna, så att de är överens om när de ska sluta låsa nya dataobjekt och i stället kan börja låsa upp dem, men i praktiken löser man det så att en transaktion behåller alla (skriv-)lås tills efter commit, så kallad strikt tvåfaslåsning. Då behövs ingen annan synkronisering än den som ändå måste göras i samband med commit.
Om en distribuerad databas bara innehåller en enda kopia av varje dataobjekt, kan vi lagra informationen om det objektets eventuella lås tillsammans med (dvs på samma nod som) dataobjektet självt. Om vi däremot har mer än en kopia av något dataobjekt, blir det mer komplicerat. Om vi lagrar ett lås tillsammans med varje kopia, skulle två olika transaktioner kunna låsa var sitt dataobjekt och uppdatera det, vilket leder till en inkonsistent databas. Därför måste vi på något sätt kombinera dessa lokala lås och skapa ett globalt lås, som låser samtliga kopior av dataobjektet.
När en transaktion är klar med allt arbete inleds commit-processen (se sidan 517 i avsnitt 25.2). Här ska integritetsvillkor kontrolleras, och i en del metoder för isolering av transaktioner ska man också kontrollera att det inte uppstått några kollisioner med andra transaktioner.
I en distribuerad databas är det värre, i och med att flera noder kan ha deltagit i transaktionen. Noderna kan ha gjort ändringar i sina lokala data, och alla noderna måste committa gemensamt – antingen allihop, eller också ingen av dem. Commit-processen måste alltså koordineras mellan de olika noderna, och därför har ett protokoll som kallas tvåfas-commit (på engelska two-phase commit eller 2PC) utvecklats.
Tvåfas-commit garanterar att en en transaktion som innehåller uppdateringar på flera noder blir ACID. Har man en distribuerad databas och vill garantera att den alltid är konsistent efter alla uppdateringar 629 måste man använda tvåfas-commit. Ett exempel på en modern geografiskt globalt distribuerad relationsdatabashanterare som garanterar global konsistens efter varje transaktion m.h.a. tvåfas-commit är Google Spanner. 5
Ett problem med tvåfas-commit är att väntetiden vid commit kan bli lång eller att transaktionen misslyckas om många distribuerade noder är involverade. Om man ger upp kravet att databasen ska vara konsistent efter varje uppdatering kan man snabba upp transaktionerna betydligt. 6 Det brukar kallas eventuell konsistens (eventual consistency på engelska) och framhålls ofta som en fördel med NoSQL-databaser. Vad man bör ha klart för sig är att eventuell konsistent inte garanterar full ACID-konsistens och således inte är så bra för till exempel banktransaktioner. Däremot fungerar det bra för till exempel webb-omröstningar där det inte är så noga om antal tummar upp eller ner är helt korrekta. Eventuell konsistens är vanlig i NoSQL-databaser som MongoDB, Cassandra, Amazon DynamoDB och CouchDB. En del NoSQL databaser (till exempel Azure Cosmos DB) har full konsistens som en inställning, vilket förstås gör databasen långsammare om den aktiveras. Det svenska ordet eventuellt är här mer korrekt än det engelska eventual då det säger att det inte är säkert att databasen blir konsistent så småningom. 7
Det här är en kort beskriving av tvåfas-commit:
Tyvärr är det (förstås) lite krångligare än så i verkligheten. Vad händer om en nod inte kan committa, eftersom det har uppstått något lokalt problem? Vad händer om en nod inte svarar, eftersom den har kraschat? Vad händer om en nod visserligen svarar att den kan committa, men sen kraschar innan den verkligen hunnit committa sin lokala deltransaktion? Vad händer om koordinatorn kraschar mitt i alltihop? Vad händer om det blir nätverksproblem och koordinatorns meddelanden går fram till en del, men inte alla, noder?
Om man ritar upp protokollet för tvåfas-commit lite mer i detalj, ser det ut som i figuren på nästa sida. De streckade linjerna representerar meddelanden som sänds mellan koordinatorn och de andra noderna.
Det hela börjar med att koordinatorn initierar den lokala commitprocessen genom att skriva BEGIN_COMMIT i sin loggfil, och skicka PREPARE till alla inblandade noder (eller Hejsan grabbar, är ni klara? som vi kallade det i den korta beskrivningen ovan).
Varje nod försöker nu committa sin lokala deltransaktion. Om det går bra, förbereder den sig på commit genom att skriva BEGIN_COMMIT i sin lokala loggfil. Om det inte går bra att committa, skriver den ROLLBACK 8 i den lokala loggfilen och börjar rulla tillbaka den lokala transaktionen. Dessutom "röstar" noden i omröstningen om ifall den globala committen ska lyckas, genom att skicka antingen meddelandet VOTE-COMMIT (Jajamensan, fattas bara!) eller meddelandet VOTE-ROLLBACK till koordinatorn. Det räcker med att en enda nod röstar VOTE-ROLLBACK för att hela den globala transaktionen ska rullas tillbaka.
631Om koordinatorn i stället får VOTE-COMMIT-svar från alla noderna, bestämmer den sig för att göra en global commit. Den skriver en COMMIT-notering i loggfilen, och (precis som i centraliserade databaser) är det precis då som (hela den globala) transaktionen nått sin commit-punkt, och alltså är committad. Efter detta ska alla ändringar som gjorts den globala transaktionen vara hållbara (Degenskapen i ACID), och får aldrig försvinna. Därefter skicka koordinatorn ut ett GLOBAL-COMMIT-meddelande till alla noderna.
De andra noderna tar emot antingen GLOBAL-COMMIT eller GLOBAL-ROLLBACK. Beroende på vilket avslutar de antingen sin lokala commit, eller avbryter den och rullar tillbaka den lokala transaktionen. När en nod genomfört detta, skickar den ett "OK" (meddelandet ACK) till koordinatorn, som står och väntar på att samla in dessa "OK" från samtliga noder, så att den vet att alla antingen committat eller rullat tillbaka sina respektive deltransaktioner.
Beskrivningen ovan säger inget om hur man hanterar uteblivna svar. I verkligheten måste både koordinatorn och de andra noderna vara beredda på att ett förväntat meddelande aldrig kommer, eftersom en annan nod kan ha kraschat och eftersom nätverket kan ha slutat fungera. Det görs genom time-out, dvs att noden står och väntar på svar en viss tid, men om svaret inte kommit under den tiden, avbryter den sin väntan och gör något annat.
Således är noderna inte geografiskt distribuerade. Det innebär att kommunikationstiden mellan noder inte längre är lika kritisk för prestanda som den var för distribuerade databaser. Därför tillhandahåller parallella databaser schema-genomskinlighet (eng. schema transparency), dvs. man behöver inte designa ett distribuerat schema som med geografiskt distribuerade databaser, utan databashanteraren hanterar parallellismen på klustret automatiskt.
Tvåfas-commit används för att garantera konsistens efter uppdateringar. Eftersom noderna ligger i samma kluster är kommunikationstiden mycket kort och tvåfas-commit effektivt.
En problem som uppstår när man kör parallella databaser på kluster med många noder är att risken för att en nod ska gå ner ökar signifikant när många noder är involverade i en transaktion. Detta hanteras med replikering med s.k. heta reserver (eng. hot standby), vilket är replikerade noder som omedelbart tar över när en nod går ner.
Det flesta moderna databassystem som SQL Server, Oracle och PostgreSQL är redan utvecklade för att utnyttja parallellismen hos det datorsystem där det körs. För MySQL finns det svenskutvecklade MySQL:s NDB Cluster som garanterar mycket hög tillgänglighet och prestanda m.h.a. heta reserver.
Ofta kombinerar moderna databassystem parallellism och primärminne. De är parallella primärminnesdatabashanterare där databasens innehåll ligger i många primärminnen på ett kluster. Sådana databaser får mycket hög skalbarhet, prestanda och tillgänglighet. Exempel på parallella primärminnesdatabashanterare är MySQL NDB Cluster och SAP HANA.
Distribuerad databas (engelska: distributed database). En databas som har sina data fysiskt utspridda på flera olika datorer som är sammankopplade med ett datornät.
634Distribuerad databashanterare (engelska: distributed database management system). Det program, eller system av program, som hanterar en distribuerad databas.
Klient/server-system (engelska: client/server system). Ett system där alla data är samlade på ett ställe, servern, men kan nås via ett datornät från en eller flera klienter.
Multidatabas (engelska: multidatabase). Flera självständiga databaser som är sammankopplade så att man kan ställa frågor som hämtar data ur flera databaser på en gång.
Genomskinlighet eller transparens (engelska: transparency). Att något är transparent betyder att det inte märks för användaren. Till exempel innebär placeringsgenomskinlighet eller placeringstransparens att användaren inte märker, och inte behöver bry sig om, på vilken nod som data i en distribuerad databas är placerade.
Nod (engelska: node). En knutpunkt av något slag. I sammanhanget distribuerade databaser är en nod en dator, med sin databas och sin programvara.
Lokal (engelska: local). I sammanhanget distribuerade databaser: någotsomhandlaromenenskildnodidendistribueradedatabasen.
Global (engelska: global). I sammanhanget distribuerade databaser: något som handlar om hela den distribuerade databasen, alltså alla noderna tillsammans, med sina respektive lokala data.
Fragment (engelska: fragmentation). I en distribuerad databas delas databasens data upp i delar, så kallade fragment, för att placeras på olika ställen.
Fragmentering (engelska: fragmentation). Hur data i en distribuerad databas delas upp i fragment.
Placering (engelska: allocation). Var fragmenten i en distribuerad databas ska placeras, dvs på vilken nod.
Replikering (engelska: replication). Om vissa data i en distribueraddatabasskafinnasiflerakopior.
Vertikal fragmentering (engelska: vertical fragmentation). Att en tabell (eller motsvarande) i en distribuerad databas delas upp i fragment genom att olika kolumner i tabellen hamnar i olika fragment.
635Horisontell fragmentering (engelska: horizontal fragmentation). Att en tabell (eller motsvarande) i en distribuerad databas delas upp i fragment genom att olika rader i tabellen hamnar i olika fragment.
Återskapandeprogram eller materialiseringsprogram (engelska: reconstruction program). Ett relationsalgebrauttryck som anger hur flera fragment ska kombineras för att vi ska få ett resultat som är lika med den ursprungliga, ofragmenterade tabellen. I fallet horisontell fragmentering är det till exempel unionen av fragmenten, alltså den sammanlagda mängden av rader från alla fragmenten.
Lokalisering (engelska: localization). Att skriva om en global fråga, dvs en som arbetar med globala tabeller, till en lokaliserad fråga, dvs en som arbetar med de existerande fragmenten.
Reduktion (engelska: reduction). Att skriva om en lokaliserad fråga genom att ta bort deluttryck som ger garanterat tomma resultat.
Join-ordning (engelska: join ordering). Att bestämma i vilken ordning join-operationerna i en fråga ska utföras. Ordningen mellan joinarna är en viktig faktor vid optimering av distribuerade frågor.
Semijoin (engelska: semijoin). En relationsalgebraoperation som är användbar i distribuerade databaser. Skrivs med symbolerna och ⋉.
Definition av ⋉ : R ⋉ S = πR.*(R ⋉ S)
Definition av ⋉ : R ⋉ S = πR.*(R ⋉ S)
Global transaktion (engelska: global transaction). En samling operationer som hör ihop som en enhet, och som är utspridda på olika noder i en distribuerad databas.
Lokal (del-)transaktion (engelska: local (sub-)transaction). De operationer i en global transaktion som utförs på en enskild nod.
Global commit (engelska: global commit). Samtidig commit av en hel global transaktion, som består av flera lokala deltransaktioner.
Tvåfas-commit (engelska: two-phase commit eller 2PC). En algoritm för commit av en global transaktion, som samordnar de lokala deltransaktionerna så att de kan committa gemensamt (eller inte alls).
Det finns även mer specialiserad litteratur. Här är en grundbok om distribuerade databaser:
• M. Tamer Özsu, Patrick Valduriez: Principles of Distributed Database Systems.
NoSQL-databaser är ett alternativ till vanliga relationsdatabaser. Trots vad det låter som handlar det egentligen inte så mycket om "inte frågespråket SQL", utan om "inte relationsmodellen". Men termen NoSQL används på flera olika sätt, och man är inte ens överens om ifall NoSQL betyder "inte SQL" eller om den betyder "Not Only SQL", dvs att man kompletterar snarare än ersätter relationsdatabaser. I vilket fall som helst handlar det, trots en del överdriven entusiasm för några år sen, inte om att sluta använda relationsdatabaser, utan det handlar om att för vissa tillämpningar kan alternativa datamodeller vara bättre lämpade. Relationsmodellen är beprövad och fungerar bra, och fortfarande passar relationsdatabaser bäst i de allra flesta fallen.
Även om relationsmodellen länge varit den dominerande datamodellen för databaser, har det hela tiden funnits alternativ. De äldre modellerna, hierarkiska databaser och nätverksdatabaser, har fortsatt att användas, och från 1980-talet har det också funnits objekt-orienterade databaser som är en form av NoSQL-databaser. (En del 638 trodde att objektorienterade databaser skulle ersätta relationsdatabaserna, men så blev det ju inte.)
Stora databaser har funnits länge, men under 2000-talet började en del företag och organisationer arbeta med ännu större datamängder distribuerade över många datorer, och med många samtidiga användare. Företag som Facebook och Google har förstås mycket data, men de har också många användare: miljarder registrerade användare, och kanske hundratals miljoner som använder tjänsterna samtidigt. Enligt en källa görs det flera miljoner Google-sökningar varje sekund. Med dessa datamängder och belastningar talar man om big data. Ingen vanlig relationsdatabashanterare, på en vanlig server, kan klara av den belastningen. Man behöver en databas som är distribuerad, utspridd på många datorer på olika ställen i världen, och även om en distribuerad relationsdatabas skulle kunna hantera denna enorma belastning var de vanliga relationsdatabashanterarna inte byggda för det.
Termen NoSQL började användas 2009. 1 Det finns många olika datamodeller och system som samlas under termen NoSQL. På webbplatsen http://nosql-database.org/ 2 finns en en sammanfattning av de olika system som kallar sig NoSQL-databaser.
Här är några olika sorters NoSQL-databaser:
• Nyckel-värdes-databaser, på engelska key-value databases eller key-value stores, lagrar data i form av värden, och man kan få fram dem ur databasen med hjälp av nyckeln. Detta liknar mycket index i vanliga databaser (se kapitel 22) och objektlager (se avsnitt 17.5). För skalbar åtkomst av ett lagrat värde för en given nyckel är nyckelvärdesdatabaser normalt distribuerade över många noder, baserat på fragmentering (se avsnitt 27.3) av nycklarna. Fragmentering kallas ofta sharding i samband med NoSQL-databaser. Det finns inga sekundärindex, så det är mycket resurskrävande att hitta data där man inte vet nyckeln. Ofta används JSON som format på värdeobjekten.
Exempel på nyckelvärdesdatabaser är Redis, 4 Memcached, 5 Apache HBase 6 och CouchDB. 7
• Dokumentdatabaser lagrar och hanterar "halvstrukturerade" data, vanligen JSON-dokument. De skiljer sig från rena nyckelvärdesdatabaser genom att det är någon form av struktur på de olika dataobjekten som sedan kan användas i frågor. Man har SQL-liknande egna frågespråk som dock inte är relationellt kompletta (se sidan 214). Sekundärindex (sidan 474) finns normalt inte, vilket gör frågor där nyckeln inte är känd resurskrävande eller omöjliga.
Exempel på dokumentdatabaser är MongoDB 8 (som har sekundärindex och ett JSON-baserat frågespråk), Cassandra 9 och Couchbase. 10
• Mapreduce-system är egentligen inte databashanterare, utan snarare infrastrukturer för vissa sorters parallella beräkningar. Mapreduce är en mycket populär parallell beräkningsmodell för big data, ursprungligen utvecklad av Google. Idén är 640 att man har en mängd filer Di på vilka man vill applicera samma funktion (map-funktionen) F parallellt, och sedan samla ihop resultaten med en aggregeringsfunktion (reducer-funktionen) R. Man vill alltså beräkna R(F (Di)) parallellt för alla datafiler Di, för stora i (dvs, det finns många datafiler). Ofta är beräkningarna mycket resurskrävande så de kan ta lång tid att utföra. Systemet måste därför vara berett på att beräkningsnoder hinner krascha under beräkningen, och kunna återstarta när en beräkningsnod för F eller R kraschar. De mest kända mapreduce-systemen är Hadoop 11 och Spark. 12
Ursprungligen utfördes beräkningarna i form av vanliga program i C++ eller Java som kördes parallellt av mapreduceinfrastrukturen. Oftast använder man mapreduce för att beräkna statistik, dvs. man beräknar aggregeringsfunktioner över input-filerna Di, för vilket SQL är utmärkt (se kapitel 8). Således utförs nu merparten av mapreduce-beräkningarna i SQL-liknande språk, till exempel HiveQL för Hadoop och Spark SQL för Spark. För detta har man utvecklat frågespråkshanterare ovanpå mapreduce-systemen, som genererar exekveringsplaner i form av mapreduce-beräkningar.
• Kolumndatabaser är egentligen inga NoSQL-databaser utan relationsdatabaser där man lagrar tabellrader kolumn för kolumn snarare än rad för rad. Man kan se det som att varje kolumn blir en dynamisk endimensionell array. Man kan återskapa en rad i en tabell genom att hämta det som är lagrat för samma index i alla arrayerna. Lagring rad för rad är effektivt för transaktionsdatabaser eftersom man där vill komma åt ett fåtal rader i varje transaktion. När en databas används för analys som i datalager (se kapitel 18) är det ofta mer fördelaktigt att tabellerna lagras kolumn för kolumn i stället. Till exempel gör man då ofta aggregering över hela kolumner, vilket är snabbare med kolumndatabaser. Kolumnarrayerna brukar komprimeras för snabb aggregering över kolumner.
Exempel på kolumndatabaser är Vertica 13 och Greenplum. 14 En del relationsdatabaser kan konfigureras både som rad- och kolumndatabaser, till exempel MariaDB ColumnStore. 15
• Lösare transaktioner. Relationsdatabaser är byggda med tanke på ACID-transaktioner, som gör att databasen hindras från att få fel och motsägelser. Men det gör att prestandan blir sämre, eftersom alla data måste hållas synkroniserade, även mellan olika kopior av databasen, på olika maskiner. NoSQL-system använder ofta eventual consistency. Det betyder att ändringar skickas ut till de olika kopiorna av databasen, men det kan ta ett tag. Innan en ändring införts överallt, kan en del sökningar ge fel svar och databasen vara felaktig. Det kan vara oacceptabelt för en bank, men en helt acceptabel avvägning när man räknar gilla-markeringar på Facebook eller Youtube (se sidan 629).
En språkligt korrekt översättning av "eventual consistency" är "till slut blir databasen konsistent", men den (språkligt sett) felaktiga översättningen "eventuellt blir databasen konsistent" är egentligen en bättre beskrivning av hur det fungerar. Eftersom andra transaktioner kan arbeta vidare med föråldrade data, till exempel ta ut pengar som egentligen inte längre finns på bankkontot, finns det ingen garanti för att databasen någonsin blir konsistent!
En av anledningarna till att NoSQL-databaser snabbt blev så populära är att de traditionella databashanterarna, som använder relationsmodellen, inte var konstruerade för att klara riktigt stora datamängder och belastningar. De databashanterarna härstammar från 642 en era med helt andra krav och möjligheter än vad som finns nu, och var från början byggda för att köras på en enda dator, med parallellitet och distribution som en sorts extrafiness som inte ingick från början. Det hade kanske gått att använda relationsdatabaser för många tillämpningar där man valde NoSQL-databaser, om bara databashanterarna varit annorlunda skrivna.
Som en reaktion på NoSQL-databaser har man därför börjat konstruera NewSQL-databaser. Tanken är att skapa databashanterare som använder relationsmodellen, men som är mycket mer skalbara och tillgängliga än traditionella relationsdatabashanterare, och som kan konkurrera med NoSQL-databaser. Exempel på NewSQL-databaser är Google Spanner och MySQL Cluster.
En utveckling som vi också ser är, som nämnts, att man bygger SQL ovanpå NoSQL-databashanterare. SQL är så praktiskt och bra att man vill ha det, även i NoSQL-system!
I datorsammanhang menar man med "molnet" IT-tjänster som körs på någon annans datorer, och som man kan nå via internet. Särskilt gäller det IT-tjänster som företag och organisationer tidigare brukade köra på sina egna datorer, men som man nu alltså kan köpa av en molnleverantör. Till exempel kan det handla om lagring av data, men också beräkningar. Ofta är molnleverantörerna stora företag, som Amazon eller Microsoft, och de har inte bara en samling servrar stående på ett ställe i världen, utan för att underlätta dataöverföringen till sina kunder, och över huvud taget få plats, kan de ha hundratals stora datacenter utspridda på kontinenterna, med tusentals serverdatorer i varje.
Med en molndatabas brukar man mena en databas där företaget eller organisationen, vars data det är, varken lagrar sina data eller kör databashanteraren själva, på sina egna datorer, utan allt detta sköts av molnleverantören, av molnleverantörens personal och på molnleverantörens servrar. Kunden, dvs företaget eller organisationen som behöver databasen, bara kopplar upp sig, skapar scheman och lägger in data.
Ett exempel på en molntjänst är Microsofts Azure, som erbjuder en molnversion av SQL Server. Det är en relationsdatabas, men 643 molndatabaser är ofta NoSQL-databaser, med andra datamodeller än relationsmodellen.
MySQL är en databashanterare som är mest känd för att vara gratis, men den finns i flera olika versioner, och det är egentligen bara en av versionerna, Community Server, som är gratis. De andra versionerna kostar pengar. MySQL utvecklades i Sverige, och såldes till ett företag som hette Sun Microsystems, vilket i sin tur köptes upp av databasföretaget Oracle. Nu är det alltså Oracle som äger och kontrollerar MySQL. En alternativ utvecklingsgren av MySQL, som kontrolleras av de ursprungliga utvecklarna, heter MariaDB.
MySQL brukar betraktas som snabb och driftsäker. Från början var det en ganska enkel databashanterare, men numera har den de flesta avancerade funktioner som andra databashanterare har, till exempel triggers och lagrade procedurer. Den finns tillgänglig för alla vanliga operativsystem, som Windows, Linux och Mac OS X, och för många ovanliga. Den kan laddas ner från www.mysql.com men finns också färdigpaketerad i många pakethanteringssystem, så den enkelt kan installeras på till exempel Linux-distributionerna Red Hat och Ubuntu. MySQL kan användas till många olika saker, men den har blivit särskilt populär för webbplatser som behöver lagra data i en databas. Det finns också produkter, till exempel Roxen WebServer, som innehåller en MySQL-server för att hantera systemets interna data.
På MySQL:s webbplats finns hela dokumentationen, inklusive referensmanualen i flera olika format och på flera olika språk. Där finns också artiklar om MySQL, och litteraturlistor.
MySQL är ett klient/server-system. Det betyder att man startar ett serverprogram ("servern") på en dator (som ibland också kallas "servern"), och sen är det programmet i gång hela tiden och hanterar databasen. Det finns flera olika klientprogram som erbjuder olika användargränssnitt. Till exempel kan man använda Microsoft Access för att prata med MySQL via ODBC.
MySQL finns också i en variant som heter Embedded MySQL Server Library, och som inte använder klient/server-arkitekturen. Om man bygger ett program som använder sig av MySQL för att lagra sina data, kan man stoppa in hela MySQL-servern i själva programmet. och behöver inte krångla med att ha en separat server. Embedded MySQL Server Library kommer dock att försvinna i kommande versioner av MySQL.
MySQL Community Server är fri mjukvara ("free software"), eller "open source" som det också brukar kallas. Den kan laddas hem och användas gratis, och man får både titta på källkoden och göra egna ändringar. Men det finns vissa begränsningar på vad man får göra. Om man gör egna ändringar i källkoden, får man till exempel inte distribuera programmet utan att göra källkoden tillgänglig för alla. Vill man göra saker som inte tillåts av reglerna för fri mjukvara, måste man få tillstånd och betala licensavgifter.
MySQL är känd för att vara både snabb och driftsäker, och dessutom enkel att använda och administrera. En reklambroschyr om MySQL skulle kunna ta upp de här punkterna:
MySQL har flera olika interna lagringsformat för tabellerna. De kallas i MySQL för storage engines. Man kan välja mellan olika lagringsformat, med olika prestanda och olika egenskaper. MySQL-manualen innehåller utförliga beskrivningar av vilka som finns, och vilka tillämpningar de är lämpade för. Tidigare var defaultvärdet på Unix-versioner av MySQL ett lagringsformat, MyISAM, som gjorde att referensintegritet med foreign key och ACID-transaktioner inte fungerade. Man fick inget felmeddelande, utan transaktionerna och referensintegriteten ignorerades utan varning. Från och med MySQL version 5.5.5 är detta ändrat, och MySQL använder som default lagringsformatet InnoDB, där referensintegritet och ACID-transaktioner fungerar.
Vi ska visa hur man installerar och kör MySQL på ett Linux-system. I en del Linux-distributioner, till exempel den populära serverdistributionen Red Hat, kan man installera MySQL redan från början, även om det inte alltid är den nyaste versionen. Många Linuxdistributioner har också särskilda funktioner för att enkelt ladda ner och installera programpaket, till exempel MySQL. Man kan också göra installationen steg för steg, genom att ladda ner en fil från MySQL:s webbplats, packa upp den, och installera den. Det är det mest komplicerade, men också mest flexibla, sättet, förutom att kompilera själv. Förutom versioner för olika operativsystem erbjuder MySQL flera alternativ. Bland annat får man välja om man vill ha en stabil, vältestad version, som är lämplig när man ska köra en databas i riktig drift, eller om man vill ha en nyare, men mindre vältestad, 648 version, som är lämplig för utveckling av nya system och tester av nya funktioner.
Vi ska provköra gratisversionen MySQL Community Server på Linuxdistributionen Ubuntu, som är populär bland dem som vill använda Linux på vanliga skrivbordsdatorer eller bärbara datorer. När vi skriver detta (mars 2017) heter den senaste stabila versionen av MySQL 5.7.12, och den senaste LTS-versionen ("Long Term Support")avUbuntuheter16.04.
Eftersom MySQL finns färdigpaketerad i Ubuntus pakethanteringssystem för mjukvara, är det lätt att installera. Ge bara kommandot sudo apt install mysql-server i ett kommandofönster. Därefter får man mata in sitt eget lösenord, och välja ett lösenord för MySQL:s administratörskonto, vilket (precis som administratörskontot i Linux) heter root. Därefter är MySQL-servern installerad och i gång, och vi kan ansluta till den med något klientprogram. Det finns flera olika klientprogram, inklusive grafiska klienter med fönster och knappar, som MySQL Workbench:
Ibland är det praktiskt att arbeta med en ren textklient. Därför startar vi MySQL:s textklient genom att ge det här kommandot i ett kommandofönster:
649
mysql -u root -p
Kommandoradsargumenten -u root anger vilken användare man vill logga in i MySQL som, och -p betyder att man vill mata in ett lösenord. När man matat in det rätta lösenordet blir man inloggad, och kan ge kommandon. MySQL-servern kan hantera flera olika databaser, så vi börjar med att skapa en databas att arbeta med, och ansluter till den. Användarens inmatning visas här med fetstil:
mysql>
create database testbasen;
Query OK, 1 row affected (0,00 sec)
mysql>
use testbasen
Database changed
Nu kan vi använda SQL för att skapa tabeller, mata in data, och ställa frågor:
mysql>
create table Personer
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x->
(Nummer integer,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x->
Namn varchar(30),
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x->
Telefon varchar(30),
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x->
primary key (Nummer));
Query OK, 0 rows affected (0,04 sec)
mysql>
insert into Personer (Nummer, Namn, Telefon)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x->
values (17, 'Hjalmar', '174590');
Query OK, 1 row affected (0,01 sec)
mysql>
insert into Personer (Nummer, Namn, Telefon)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x->
values (4711, 'Hulda', '019-94639');
Query OK, 1 row affected (0,01 sec)
mysql>
select * from Personer;
2 rows in set (0,00 sec)
650
mysql>
insert into Personer (Nummer, Namn, Telefon)
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x->
values (17, 'Hulda', '019-94639');
ERROR 1062 (23000): Duplicate entry '17' for key 'PRIMARY'
Även referensintegritet fungerar:
mysql>
create table Båtar
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x->
(Namn varchar(30) primary key,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x->
Ägare integer,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x->
foreign key (Ägare) references Personer(Nummer));
Query OK, 0 rows affected (0,04 sec)
mysql>
insert into Båtar values ('Wasa', 17);
Query OK, 1 row affected (0,01 sec)
mysql>
insert into Båtar values ('Enterprise', 18);
ERROR 1452 (23000): Cannot add or update a child row:
a foreign key constraint fails ('testbasen'.'Båtar',
CONSTRAINT 'Båtar_ibfk_1' FOREIGN KEY ('Ägare')
REFERENCES 'Personer' ('Nummer'))
mysql>
drop table Båtar;
Query OK, 0 rows affected (0,01 sec)
mysql>
create table Båtar
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x->
(Namn varchar(30) primary key,
x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#xx#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x->
Ägare integer references Personer(Nummer));
Query OK, 0 rows affected (0,04 sec)
mysql>
insert into Båtar values ('Enterprise', 18);
Query OK, 1 row affected (0,00 sec)
Trots att det inte finns någon person nummer 18, går det bra att lägga in en båt med den ägaren.
MySQL förstår endast tabellvillkor med foreign key,
som i det första exemplet ovan.
Om man skriver referensvillkoret direkt i en kolumndefinition, som i exemplet omedelbart här ovanför, ignoreras
651
referensintegriteten.
Man får ingen varning eller felmeddelande.
1
Det här är ett av många exempel på att databashanterare, inte bara MySQL, har egenheter som man måste se upp med, och att om man ska använda en databashanterare måste man lära sig just den databashanterarens egenheter.
MySQL var länge den dominerande gratisdatabashanteraren. (Egentligen handlar det inte om att den är gratis, utan att den är fri mjukvara, "free software", eller "open source" som det också brukar kallas.) Men efter att MySQL köptes upp av Oracle kan man fundera över hur MySQL:s framtid ser ut, hur fri och öppen den kommer att vara i framtiden, och hur mycket ny teknik som kommer att komma in i den fria versionen.
Ett annat alternativ till databashanterare med öppen källkod är PostgreSQL. Många anser att PostgreSQL är överlägsen MySQL, bland annat för att den har mer avancerade funktioner, som stöd för objekt-relationella tekniker (se avsnitt 17.6), att den är utvidgningsbar med egna tillägg, och att den följer standarder bättre. Dessutom har PostgreSQL en mer tillåtande licens än många andra produkter med öppen källkod, till exempel så att man kan göra egna förbättringar och distribuera det förbättrade systemet, utan att man behöver göra ändringarna i källkoden tillgängliga.
Ursprungligen kommer PostgreSQL från ett forskningsprojekt på 1970-talet vid Berkeley-universitetet i USA som hette Ingres. Uppföljaren, på 1980-talet, kallades Postgres, och namnet har sedan dess ändrats till PostgreSQL, för att tydliggöra att det är en relationsdatabashanterare med frågespråket SQL. Den kallas ofta fortfarande för Postgres.
654Man kan ladda ner PostgreSQL från den officiella webbplatsen, www.postgresql.org, och där finns också hela dokumentationen, inklusive referensmanualen i flera olika format och på flera olika språk. Där finns också artiklar om PostgreSQL och litteraturlistor. Som de flesta stora och avancerade databashanterare, förutom Microsofts produkter, fungerar PostgreSQL på alla vanliga operativsystem.
PostgreSQL är ett avancerat system, och innehåller förstås alla de vanliga finesserna som man brukar hitta i stora och avancerade databashanterare, till exempel triggers. Men den har också en lång rad andra finesser, och här tar vi upp några av de mest intressanta:
select * from Anställda where Lön + Bonus > 1000
, skulle man inte ha någon nytta alls av ett index på kolumnen Lön, på kolumnen Bonus, eller på båda, men däremot av ett index på summan av lön och bonus.
I PostgreSQL kan man skapa ett index på uttrycket Lön + Bonus, med create index Anställdindex on Anställda((Lön + Bonus)).
På liknande sätt som med annan öppen källkod kan man ladda ner källkoden till PostgreSQL och kompilera den själv, men ett stort och komplext system som en databashanterare är ofta både svårt och tidskrävande att bygga på det sättet, och därför brukar nästan alla ladda hem färdigkompilerade program som det bara är att installera. Man kan ladda hem olika versioner av PostgreSQL från PostgreSQL:s webbplats, men många Linux-distributioner har särskilda funktioner för att enkelt ladda ner och installera programpaket, och det ska vi använda i vårt exempel.
Vi ska provköra PostgreSQL på Linux-distributionen Ubuntu. När vi skriver detta (november 2017) heter den senaste LTS-versionen ("Long Term Support") av Ubuntu 16.04, och den senaste stabila versionen av PostgreSQL heter 10.1. När vi använder Ubuntus pakethanteringssystem för mjukvara får vi dock en något äldre version, 9.5.10.
Vi börjar med att öppna ett kommandofönster och ge Linux-kommandot sudo apt-get install postgresql
för att installera PostgreSQL.
Man måste mata in sitt eget lösenord, och därefter är PostgreSQL installerad, och servern startad.
Vi kan nu ansluta till den med något klientprogram.
Det finns flera olika klientprogram, inklusive grafiska klienter med fönster och knappar, men vi väljer att använda en ren textklient, psql.
PostgreSQL har flera olika sätt att identifiera och autentisera användare, men det som kanske är enklast är att använda sig av den användare som man är inloggad som i Linux-systemet.
Därför använder vi kommandot sudo -u postgres psql
för att starta psql som Ubuntu-användaren postgres.
Nu är vi inloggade i PostgreSQL, och kan börja ge kommandon.
En PostgreSQL-server kan hantera flera separata databaser, och från början är vi anslutna till default-databasen.
Nu skapar vi en ny databas som heter testbasen och ansluter till den med psqlkommandot \c.
Användarens inmatning visas med fetstil.
postgres=#
create database testbasen;
CREATE DATABASE
657
postgres=#
\c testbasen
You are now connected to database "testbasen"
as user "postgres".
testbasen=#
Vi skapar en tabell och lägger in ett par rader:
testbasen=#
create table Personer
testbasen-#
(Nummer integer primary key,
testbasen(#
Namn varchar(30),
testbasen(#
Telefon varchar(30));
CREATE TABLE
testbasen=#
insert into Personer
testbasen=#
values (17, 'Hjalmar', '174590');
INSERT 0 1
testbasen=#
insert into Personer
testbasen=#
values (4711, 'Hulda', '019-94639');
INSERT 0 1
testbasen=#
select * from Personer;
nummer
|
namn
|
telefon
|
17
|
Hjalmar
|
174590
|
4711
|
Hulda
|
019-94639
|
(2 rows)
testbasen=#
testbasen=#
insert into Personer
testbasen=#
values (17, 'Hulda', '019-94639');
ERROR: duplicate key value violates unique
constraint "personer_pkey"
DETAIL: Key (nummer)=(17) already exists.
testbasen=#
För att kontrollera att kontrollen av referensintegritet fungerar som den ska 1 skapar vi också en tabell med båtar, som ägs av personerna, och provar att lägga in en båt som ägs av en person som inte finns.
testbasen=#
create table Båtar
testbasen-#
(Namn varchar(30) primary key,
testbasen(#
Ägare integer references Personer(Nummer));
658
CREATE TABLE
testbasen=#
insert into Båtar values ('Wasa', 17);
INSERT 0 1
testbasen=#
insert into Båtar
testbasen=#
values ('Boaty McBoatface', 18);
ERROR: insert or update on table "båtar" violates
foreign key constraint "båtar_Ägare_fkey"
DETAIL: Key (Ägare)=(18) is not present in
table "personer".
testbasen=#
Microsoft SQL Server är en relationsdatabashanterare som, förstås, kommer från Microsoft. Den kallas ofta bara "SQL Server", vilket kan vara lite missvisande eftersom de flesta andra relationsdatabashanterare också är "SQL-servrar". Ibland kallas den till och med bara "SQL", vilket är ännu mer missvisande, för SQL är ett språk och inte en databashanterare. Den brukar räknas som en av "de tre stora", både i fråga om hur många funktioner den har och i fråga om marknadsandelar.
SQL Server har många avancerade funktioner, till exempel för data mining, och en omfattande SQL-dialekt. Den kan hantera stora datamängder och många samtidiga användare, men kanske inte de allra största installationerna med högst belastning.
Till skillnad från de flesta andra populära databashanterare, som alla fungerar på flera olika plattformar, kan Microsoft SQL Server bara köras på operativsystem från Microsoft. Den finns numera också i en version för Linux, men den har än så länge inte alla finesser som finns i Windows-versionerna, och det finns än så länge ganska få erfarenheter av hur bra det fungerar. Nyare versioner av SQL Server finns bara i 64-bitarsversion, så man kan inte köra den på 32-bitarsversionen av Windows.
660Den senaste versionen av SQL Server heter, när detta skrivs i oktober 2017, SQL Server 2017. Den finns i flera olika versioner, bland annat:
• Azure SQL Database. Förutom att köra en egen SQL Server på en server som man själv administrerar, kan man köpa SQL Server som en molntjänst i Microsofts molntjänst Azure. Då lagras ens data med en databashanterare som körs av Microsoft på Microsofts egna datorer.
Olika databashanterare har olika dialekter av SQL, med olika finesser och ibland olika sätt att göra även ganska grundläggande saker. SQL Servers SQL-dialekt kallas Transact-SQL, ofta förkortat T-SQL.
Några finesser och egenheter i Transact-SQL:
go.
[Antal båtar].
Det motsvarar SQL-standardens dubbla citationstecken, som också fungerar i SQL Server, men som det är ovanligt att någon använder: "Antal båtar
".
TOP
, som i select top 10 from Anställda order by Namn
, eller, om man vill ha med även eventuella rader som delar tiondeplatsen, select top 10 with ties from Anställda order by Namn.
try
och catch
, så man kan skriva även komplicerade skript.
Större program skriver man dock normalt i ett riktigt programmeringsspråk, till exempel C#, och inte i Transact-SQL.
set transaction isolation level.
Man kan koppla upp sig mot SQL Server och ge kommandon, till exempel select-frågor, med ett program som heter sqlcmd, och som är ett rent textgränssnitt. Men det vanligaste sättet att arbeta mot SQL Server, om man inte går via ett applikationsprogram, är SQL Server Management Studio. Även här kan man skriva textkommandon, men i en redigeringsruta. För de flesta är det en behagligare upplevelse.
662Här är ett exempel på hur Management Studio kan se ut. Vi ser att det finns en databas som heter Thomas-testbasen, som innehåller de två tabellerna Anställda och Avdelningar. För att titta på innehållet i Avdelningar har vi högerklickat på tabellnamnet och valt Select Top 1000 Rows i menyn som kommer upp. Management Studio genererar då automatiskt en SQL-fråga som hämtar de första tusen raderna i tabellen:
Express Edition är gratisversionen av SQL Server. Den är bra om man vill provköra. Den får även användas i skarp drift, men har en del begräsningar, till exempel på databasens storlek.
Vi laddar ner SQL Server 2017 Express Edition från Microsofts webbplats. Microsoft byter ibland adresserna på webbsidorna, så vi ger ingen adress här, utan rekommenderar att man söker efter "SQL Server 2017 Express Edition" antingen på Microsofts webbplats eller med Google. När vi provkörde detta i oktober 2017 hette den nedladdade filen SQLServer2017-SSEI-Expr.exe och var knappt 5 megabyte stor.
Vi installerade på Windows 10 genom att dubbelklicka på installationsfilen för Express Edition, behålla alla defaultalternativen, och därefter klicka på knappen "Connect Now". Då startas textgränssnittet sqlcmd, och man kan ge kommandon med Transact-SQL.
Vi laddar också ner SQL Server Management Studio och installerar den När vi provkörde detta var det version 17.3. Namnet på den 664 nedladdade filen var SSMS-Setup-ENU.exe, och den var drygt 800 megabyte stor.
I det här kapitlet listar vi några av de vanligaste och mest kända databashanterarna. Det är nästan bara relationsdatabashanterare. Det finns fler system, en del av dem av stort historiskt och tekniskt intresse, men det här är några av de databashanterare som man har störst chans att komma i kontakt med i dag.
Det kan vara svårt att avgöra vilken databashanterare som är populärast, och det beror på vad och hur man mäter, men enligt webbplatsen DB-Engines var när detta skrivs (december 2017) Oracle populärast, tätt följd av MySQL och därefter Microsoft SQL Server. 1 Därefter är steget långt till fjärdeplatsen, PostgreSQL. Alla dessa är relationsdatabashanterare. (Oracle och PostgreSQL är objektrelationella databashanterare; se avsnitt 17.6.) Först på femte plats kommer en NoSQL-databashanterare, MongoDB.
Enligt denna lista, som kanske bör tas med en stor nypa salt, är alltså Oracle, MySQL 2 och Microsoft SQL Server de tre populäraste databashanterarna. Det finns också ett uttryck, "de tre stora", som beskriver de dominerande databashanterarna. De är "stora" både i fråga om marknadsandelar och i fråga om funktioner, och kanske också prislappen. Exakt vilka databashanterare som räknas till dessa har varierat med tiden, och det har länge innefattat Oracle, Microsoft SQL Server och inte MySQL utan Db2, som kommer från IBM.
Db2 är en relationsdatabashanterare från IBM. Tillsammans med Oracle och Microsoft SQL Server har den länge räknats som en av "de tre stora", både i fråga om hur många funktioner den har och i fråga om marknadsandelar.
Namnet "Db2", som står för "IBM Database 2", användes första gången 1982, men systemet har en en lång historia bakom sig hos IBM redan innan dess, och hette från början "System R". Edgar F. Codd, som uppfann relationsmodellen, gjorde det medan han arbetade på IBM.
Db2 finns i flera olika varianter, och man kan tala om flera olika produkter i en familj. Alla stöder relationsmodellen, men en del har objektrelationstillägg och även alternativa datamodeller som JSON och XML. Db2 finns för många olika plattformar, inklusive som molndatabas, dvs att man lagrar sina data med en databashanterare som körs av IBM på IBM:s egna datorer.
IMS betyder "Information Management System" och är en hierarkisk databashanterare utvecklad av IBM på 1960-talet. Det är alltså inte en relationsdatabashanterare. Den används fortfarande, särskilt av banker, men det är få som i dag väljer att basera nya tillämpningar på IMS. På webbplatsen DB-Engines lista över vilka databaser som är populärast, har IMS numera (oktober 2017) plats 102.
Informix är en relationsdatabashanterare, som under 1990-talet var en föregångare inom objektrelationell databasteknik. Företaget misssköttes, gick dåligt ekonomiskt, och köptes upp av IBM. Delar av den 667 objektrelationella tekniken från Informix finns i IBM:s databashanterare Db2, men IBMdistribuerar fortfarande nya versioner av Informix.
Det är få som i dag väljer att basera nya tillämpningar på Informix.
Ingres är en relationsdatabashanterare som utvecklades i ett forskningsprojekt under 1970-talet. Källkoden var tillgänglig mot en mindre avgift, och många företag baserade sina egna databasprodukter på Ingres, bland dem Informix.
1982 bildades ett företag för att vidareutveckla och sälja Ingres som kommersiell produkt. De heter numera Actian. Den kommersiella versionen av Ingres släpptes också som öppen källkod.
Det är få som i dag väljer att basera nya tillämpningar på Ingres.
En fördel med InterBase är att den tar mindre plats, såväl på disk som i minnet, än många andra databashanterare, och att den också är relativt enkel att administrera. InterBase finns för Windows, Mac OS X och Linux, och även för Android och iOS. InterBase använder, till skillnad från de flesta andra relationsdatabashanterare, optimistiska metoder i stället för lås för kontrollen av samtidig åtkomst.
En variant av InterBase finns som öppen källkod och heter Firebird (se ovan).
En populär databashanterare med ett lättanvänt grafiskt användargränssnitt, som man kan utveckla tillämpningar i utan så mycket programmering. Microsoft Access var tidigare mycket vanlig, och den första upplagan av boken hade ett helt kapitel om Microsoft Access. På senare år har det blivit något mindre populärt att bygga databastillämpningar med Microsoft Access, och numera gör man hellre webbtillämpningar, men på bokens webbplats 3 finns en genomgång av Microsoft Access.
Jet är en av världens mest spridda och använda databashanterare, men trots det har de flesta nog aldrig ens hört namnet. Jet är nämligen den databashanterare som finns inuti Microsoft Access. Microsoft Access är egentligen ett skal runt databashanteraren, snarare än en databashanterare i sig.
Microsoft SQL Server är en relationsdatabashanterare som, förstås, kommer från Microsoft. Den kallas ofta bara "SQL Server", vilket kan vara lite missvisande eftersom de flesta andra relationsdatabashanterare också är "SQL-servrar". Ibland kallas den till och med bara "SQL", vilket är ännu mer missvisande, för SQL är ett språk och inte en databashanterare. Den brukar räknas som en av "de tre stora", både i fråga om hur många funktioner den har och i fråga om marknadsandelar.
669Till skillnad från alla de andra populära databashanterarna, som alla fungerar på flera olika plattformar, kan Microsoft SQL Server (och Microsoft Access) bara köras på operativsystem från Microsoft. 4
En databashanterare som utvecklas av ett svenskt företag. En egenskap som framhålls i reklamen är att Mimer anstränger sig för att följa SQL-standarden. Mimer använder, till skillnad från de flesta andra relationsdatabashanterare, optimistiska metoder i stället för lås för kontrollen av samtidig åtkomst.
Ett så kallat dokumentlager, på engelska "document store", eller dokumentorienterad databashanterare. Det är alltså inte en relationsdatabashanterare, och databasen saknar ett formellt schema, så olika dataobjekt kan ha olika utseende. Olika poster kan ha olika kolumner.
En mycket populär databashanterare, som från början var gratis och hade öppen källkod. Det finns fortfarande en gratisversion av den. MySQL har länge saknat en del viktiga funktioner, men den har utvecklas till att bli mer och mer fullständig. MySQL beskrivs mer ingående i kapitel 29.
En objektorienterad databashanterare. Det är alltså inte en relationsdatabashanterare. Man använder den tillsammans med ett program skrivet i C++ eller Java för att lagra och hantera programmets objekt.
Oracle är en relationsdatabashanterare från ett företag som har samma namn. Den brukar räknas som en av "de tre stora", både i fråga om hur många funktioner den har och i fråga om marknadsandelar. Oracle är känd för att vara relativt komplicerad att installera och administrera.
En populär databashanterare, som är gratis och har öppen källkod. Den har fler avancerade funktioner än den mer kända gratisdatabashanteraren MySQL. PostgreSQL beskrivs mer ingående i kapitel 30.
En enkel databashanterare i form av ett C-bibliotek som arbetar med en fil. Den kan enkelt länkas ihop med ett annat program, och på så sätt ge (enkelt) SQL-stöd för en applikation. Bland saker som inte fungerar i SQL-dialekten är referensintegritet och typkontroll av de värden man lägger in i en tabell.
SQLite är gratis och med källkoden tillgänglig. Till skillnad från andra databaser med öppen källkod, som MySQL och PostgreSQL, är SQLite "public domain", vilket innebär att man kan göra vad man vill med den, inklusive att göra förbättringar och sedan sälja eller distribuera dem utan att göra den nya källkoden tillgänglig.
Sybase var egentligen namnet på ett företag som utvecklade och sålde en familj av databashanterare, bland dem den som brukar kallas "Sybase". Företaget är numera uppköpt av SAP, och databashanteraren Sybase kallas numera SAP Adaptive Server Enterprise.
Tidiga versioner av Microsoft SQL Server var en omdöpt Sybase.
Det finns många databasböcker, såväl på grundnivå som mer avancerade och specialiserade. Det finns också olika resurser tillgängliga på internet, alltifrån samlingar av artiklar och vanliga frågor, till databashanterare som man kan ladda hem gratis.
Det finns många olika databasböcker som tar upp databaser från grunden. Allihop är på engelska och ungefär tusen sidor långa. Det här är de bästa och/eller populäraste:
Om det finns många grundböcker om databaser, så finns det ännu fler böcker om mer specialiserade ämnen inom databasområdet. Här är några av dem.
• Peter Gulutzan & Trudy Pelzer: SQL-99 Complete, Really. An Example-Based Reference Manual of the New Standard. R&D Books, 1999. ISBN 0-87930-568-1. Finns på webben: https://mariadb.com/kb/en/sql-99/
En referensbok om SQL-standarden. Över tusen sidor, och dessutom följer det med en cd-skiva med sju extra appendix. Det har kommit flera nya versioner av SQL-standarden efter SQL99, men de har inte rönt lika stort intresse.
• Martin Fowler: Patterns of Enterprise Application Architecture, Addison-Wesley, 2003. ISBN 0-321-12742-0. Handlar egentligen om mönster för att lösa olika vanliga problem när man programmerar. Den tar däribland upp hur man översätter datastrukturerna i ett objektorienterat program till tabeller för lagring i en relationsdatabas, med de olika alternativ som finns och deras fördelar och nackdelar.
En sammanfattning av alla mönstren i boken finns på webben: http://www.martinfowler.com/eaaCatalog/
• Den här boken har en hemsida som finns på adressen
http://www.databasteknik.se/boken
Där finns kompletterande material som kodexempel, svar till övningar och eventuella rättelser.
• Thomas Padron-McCarthy: En webbkurs om databaser. De grundläggande kapitlen i den här boken baseras på den här webbkursen, som motsvarar en bok på 100-150 sidor. Den tar inte upp särskilt mycket om hur en databashanterare arbetar internt, utan det handlar om att använda databashanteraren: för att skapa egna databaser, stoppa in data i dem, och söka i dem. Finns på adressen
• James Hoffman: Introduction to Structured Query Language. En (alltmer inaktuell) webbkurs som flyter runt i olika versioner på nätet. Den nyaste vi hittar just nu (juni 2017) har versionsnummer 4.76 och finns bland annat på adressen
Det finns mängder av resurser på webben, och nya tillkommer hela tiden. Det mesta är på engelska. Man brukar kunna hitta svar på många av sina frågor om man söker med några väl valda sökord i en sökmotor som Google.
En särskilt bra resurs är fråge-och-svars-webbplatsen Stack Overflow: http://stackoverflow.com/
De har även en specialiserad webbplats för databasadministratörer: http://dba.stackexchange.com/
Dessutom finns det flera olika databashanterare som kan laddas ner gratis, med olika typer av licenser och ibland med källkod. MySQL är den mest kända av dessa gratisdatabashanterare, men långt ifrån den enda. Även databashanterare som normalt kostar pengar brukar kunna laddas ner för personligt bruk eller för provkörning.
all
(i SQL), 133, 165
alter table
, 149, 204, 271
any
(i SQL), 133, 161
as
(i SQL), 137, 143, 182
asc
(i SQL), 134
assertion
(i SQL), 276
avg
(i SQL), 161
begin transaction
, 144
BLOB
(i SQL), 191
boolean
(i SQL), 191
char
(i SQL), 148
CLOB
(i SQL), 191
commit
(i SQL), 144, 208, 507
count
(i SQL), 161, 174
create domain
, 191
create index
, 205
create procedure
, 300
create role
, 193, 292
create table
, 147, 201
create table like
, 191
create trigger
, 311
create view
, 142, 204
cube
(i SQL), 192, 354
date
(i SQL), 385
daytime
(i SQL), 385
default
(i SQL), 148
delete
(i SQL), 137, 200
desc
(i SQL), 134
distinct
(i SQL), 130
domain
(i SQL), 191
drop
(i SQL), 206
every
(i SQL), 161
except
(i SQL), 164
foreign key
(i SQL), 122, 148, 271, se främmande nyckel
full outer join
(i SQL), 173
grant
(i SQL), 209, 290
grant option
(i SQL), 292
group by
(i SQL), 167
group by cube
, 192
group by grouping sets
, 192
group by rollup
, 192
having
(i SQL), 168
in
(i SQL), 127, 128, 130
information_schema
, 152
insert
(i SQL), 137, 139, 199
intersect
(i SQL), 164
left outer join
(i SQL), 173
like
(i SQL), 118
limit
(i SQL), 135
materialized
(i SQL), 314
max
(i SQL), 161
min
(i SQL), 161
not exists
(i SQL), 131
not in
(i SQL), 130
on delete
(i SQL), 273, 274
on update
(i SQL), 273
order by
(i SQL), 134
outer join
(i SQL), 172
primary key
(i SQL), 152, 269, se primärnyckel
read committed
(i SQL), 537
read uncommitted
(i SQL), 511, 537
references
(i SQL), 148
repeatable read
(i SQL), 538
revoke
(i SQL), 209, 290
right outer join
(i SQL), 173
rollback
(i SQL), 144, 208, 507
rollback to savepoint
, 193
row
(i SQL), 191
savepoint
(i SQL), 193
select
(i SQL), 115, 117, 196
select
-fråga, 115
serializable
(i SQL), 511, 538
set transaction
, 208, 537
similar to
(i SQL), 192
some
(i SQL), 133, 161
start transaction
, 144, 208, 537
sum
(i SQL), 161
time
(i SQL), 385
timestamp
(i SQL), 385
top
(i SQL), 135
union
(i SQL), 164, 226
union all
(i SQL), 166
unique
(i SQL), 148, 152, 197, 201
update
(i SQL), 137, 140, 200
varchar
(i SQL), 148
where
(i SQL), 118
Detta verk är skyddat av upphovsrättslagen. Kopiering, utöver lärares och studenters begränsade rätt att kopiera för undervisningsändamål enligt Bonus Copyright Access kopieringsavtal, är förbjuden. För information om avtalet hänvisas till utbildningsanordnarens huvudman eller Bonus Copyright Access.
Vid utgivning av detta verk som e-bok, är e-boken kopieringsskyddad.
Studentlitteratur har både digital och traditionell bokutgivning. Studentlitteraturs trycksaker är miljöanpassade, både när det gäller papper och tryckprocess.
© Författarna och Studentlitteratur 2005, 2018