]> Databasteknik

Databasteknik 2. uppl.

Thomas Padron-McCARTHY Tore Risch
Tore Risch

Information om upphovsrättslagen och om talboken

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.

Slut på informationen.

Baksidestext

illustration

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.

illustration

DATABASTEKNIK

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.

Slut på baksidestext

a

DATABASTEKNIK

THOMAS PADRON-McCARTHY

TORE RISCH

ANDRA UPPLAGAN

illustration
b
Blank sida
i

Innehåll

1

Förord

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.

Webbresurser

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

Den kursen tar upp grunderna för hur man använder en databashanterare, och motsvarar de grundläggande kapitlen i boken.

Förkunskaper

För att förstå det mesta i den här boken räcker det med grundläggande datorkunskaper, så att man till exempel vet vad ett operativsystem är och vad det innebär att starta och köra ett program, och att man kan skilja på primärminne och sekundärminne.

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.

För att förstå kapitel 21, SQL inuti ett program, som handlar om hur man kommer åt en databas med SQL inifrån ett vanligt datorprogram, behövs grundläggande kunskaper i programmering, gärna i något av språken Python, Java, C eller C++.

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.

Läsanvisningar

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.

Konventioner

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
                  
               
            

Återkoppling

Vi tar gärna emot rättelser, klagomål, förbättringsförslag och annan återkoppling på e-post-adressen boken@databasteknik.se

Det går förstås bra att skicka beröm också, men gladast blir vi om vi får tips om saker vi kan förbättra.

Första upplagan 2005

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.

Vi vill också tacka våra kontakter på Studentlitteratur, främst Viktor Jonsson och Inger Jänchen, för uppmuntran och tålmodig väntan på vår ständigt försenade bok.

Andra tryckningen 2007

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.

6

Andra upplagan 2018

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.

Här vill vi främst tacka Göran Hasse, Örebro, för många värdefulla synpunkter och bidrag från ett industriperspektiv, och förstås Jens Fredholm på Studentlitteratur, som tålmodigt väntat i många år på att vi skulle leverera denna andra upplaga av boken.

Noter

1 Nåja, relativt kort.

7

Kapitel 1 Databaser och databashanterare

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.

1.1 Varför ska jag lära mig det här?

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.

8

1.2 Vad är en databas?

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

1.3 Vad är en databashanterare?

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.

Många databashanterare är mycket stora och komplicerade, och är egentligen hela system av olika program.

9

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

1.4 Språkförbistring

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.

1.5 Databashanterare – varför?

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.

Om man exempelvis vill ha ett kundregister med nummer, namn och adress för varje kund, börjar man kanske med att definiera en post:

               
                  
                     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];
                  
                  
                     };
                  
               
            

Sen fortsätter man med ungefär 2000 rader programkod, som sköter dialogen med användaren och som läser och skriver datafilen med kunder.

Det finns ganska många fördelar med att i stället använda en databashanterare. De viktigaste fördelarna är att det är

11

Fördel 1: Enkelt!

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 programmerare skulle säga att schemat motsvaras av datatyper i ett vanligt datorprogram, medan databasens innehåll motsvarar data.

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;
                     
                  
               

Klart!

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

13

Fördel 3: Flexibelt!

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?

Enkelt! Skriv:

                  
                     
                        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.

15

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

Andra fördelar

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:

1.6 Nackdelar med databaser

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.

1.7 Jämförelse med kalkylprogram

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.

1.8 Användningsområden för databasteknik

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:

1.9 Datamodeller

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:

1.10 Tre-schema-arkitekturen

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:

Nu kan vi passa på att definiera logiskt och fysiskt dataoberoende på ett bättre sätt än vi kunde göra tidigare:

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

1.11 Olika typer av användare

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.

1.12 Databashanterare – hur?

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.

illustration

1.13 Hur mycket data kan man ha i en databas?

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.

1.14 Databasen och verkligheten

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.

26

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

1.15 De viktigaste begreppen

Som en repetition och sammanfattning går vi här igenom de viktigaste begreppen från det här avsnittet.

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

1.16 Litteratur

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.

30

Noter

1 Ordet persistent kommer från samma ord på engelska, där det betyder envis eller ihärdig.

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

3 https://db-engines.com/en/ranking, besökt 2017-12-14.

4 DDL står för Data Definition Language.

5 SQL står för Structured Query Language.

6 DML står för Data Manipulation Language.

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.

10 Vanligen har databashanteraren en typ av träd som heter B-träd och beskrivs på bokens webbplats.

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.

12 I en tidigare upplaga av boken var det biljettförsäljare, men sådana finns numera bara på Stockholm C.

13 Ja, det går att lösa Sudoku med en SQL-fråga.

14 Det behöver inte vara implementerat på det sättet. Om man faktiskt lagrar innehållet i vyn kallas det en materialiserad vy.

31

Kapitel 2 ER-modellering

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

32

Den konceptuella beskrivningen görs med hjälp av en konceptuell datamodell, och består av ett schema uttryckt i den modellen.

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

2.2 Varför ska jag lära mig det här?

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.

2.3 Grunderna för hur man ritar ER-diagram

Gör så här:

Rita upp de typer av saker som ska finnas i databasen. De kallas entitetstyper, och ritas som fyrkantiga lådor:

illustration

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:

illustration

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.

2.4 Hur många personer bor i varje hus?

Man kan också rita ut mer exakt vad det är för sorts sambandstyp:

illustration

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

36

2.5 Skilj på ER-diagram och tabeller

Även om man ofta översätter ER-diagram till tabeller, för att kunna lagra sin databas med en relationsdatabashanterare, så har ER-diagrammet i sig inget med tabeller att göra.

Det man ritar ut i ett ER-diagram är alltså inte tabeller och kopplingarna mellan dem, även om en del (men inte alla) av entitetstyperna sen kan översättas till tabeller, och en del (men inte alla) av sambandstyperna kan översättas till kopplingar mellan tabeller.

2.6 Skilj på schema och data

ER-diagrammet är ett schema, och beskriver vilka data som kan lagras. Det är inte en avbildning av data.

Det här ER-diagrammet säger alltså inte att det finns en person som är vän med sig själv, utan det säger att personer kan vara vänner med andra personer:

illustration

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:

37
illustration

2.7 Flera sambandstyper mellan samma entitetstyper

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

illustration

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.

38

2.8 Fullständigt deltagande

Om varje person som är med i databasen måste bo i ett hus, kan man rita ut det villkoret med ett dubbelstreck mellan Person och Bor i:

illustration

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.

illustration

2.9 Attribut

Om sakerna eller sambanden har egenskaper, kallar man dem attribut, och ritar dem som ovaler:

39
illustration

Att NummerPerson ä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:

illustration

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:

40

2.10 Ett vanligt fel

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:

illustration

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.

2.11 Sammansatta attribut

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:

illustration

Telefonnummer kan ju behandlas som en enhet (0707-347013), men om man ofta vill dela upp personerna i databasen efter riktnummer kan det vara bättre att dela upp numren i två delar (0707 och 347013).

41

2.12 Flervärda 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:

illustration

Att telefonnummer är ett flervärt attribut innebär att en person kan ha hur många telefonnummer som helst: noll, ett, två eller kanske en miljard.

2.13 Härledda attribut

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:

illustration

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.

2.14 Svaga entitetstyper

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:

43
illustration

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

2.15 Objektifiering av sambandstyper

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.

44

Till exempel kan vi göra om Bor i-sambandet till en egen entitetstyp, som vi kallar Boende:

illustration

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:

illustration
45

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

illustration

Om vi i stället betraktar en filmtittning som en egen sak, och inte bara en typ av samband mellan personer och filmer, kan vi ha flera olika tittningar där en viss person sett en viss film:

Nu har vi inte ritat ut några attribut, men vi kanske vill hitta på en nyckel till den nya entitetstypen, ett "tittningsnummer" som inte behövdes för sambandet.

Om man har ett flervägssamband, dvs ett samband som binder ihop fler än två entitetstyper (se nedan), är det ofta bättre att objektifiera det.

2.16 Flervägssamband

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

46
illustration

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

illustration

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:

47
illustration

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

illustration
48

2.17 Utvidgade ER-diagram (EER)

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

EER-modellen har en notation med flera olika typer av arv, men vi börjar med en enda: enkelt klass/subklass-samband.

illustration

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:

illustration

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:

illustration

Om vi vill ange samma sak utan arv, skulle vi bli tvungna att rita något i den här stilen:

50
illustration

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

2.18 Mer om arv i EER-diagram

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:

illustration
51

Om personer har någon viss egenskap, till exempel namn, har även studenter, lärare och astronauter samma egenskap, för de är ju allihop personer.

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:

illustration

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.

52

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

illustration

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.

Vi rekommenderar att man använder denna mer uttrycksfulla notation, eftersom den mer exakt kan ange vad databasen kan innehålla.

2.19 Översättning till relationsmodellen

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.

53

2.20 Varför inte ER-diagram hela vägen?

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.

Orsakerna till det är dels tradition, och dels att man har lång erfarenhet av hur man bygger relationsdatabashanterare så att de blir snabba, kraftfulla, säkra och lättanvända.

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.

Ytterligare ett skäl är att det finns bra och etablerade frågespråk (främst SQL) för relationsdatabaser, och man har ännu inte kommit lika långt med hur man gör sökningar i grafiska beskrivningar som ER-diagram.

2.21 Alternativa notationer

Som vi nämnde ovan kan ER-diagram ritas på flera olika sätt, och det gäller särskilt hur man anger kardinalitetsförhållanden (1:N, N:M osv).

Här är några olika sätt att rita samma 1:N-samband mellan personer och hus:

54
illustration

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

2.22 De viktigaste begreppen

Som en repetition och sammanfattning går vi här igenom de viktigaste begreppen från det här avsnittet.

55

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.

56

Den 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

2.23 Övningar

Det finns svar till övningarna, och en del diskussioner kring svaren, på bokens webbplats, men försök lösa uppgifterna själv innan du tittar i facit.

Övning 2: I jultomtens hemliga tjänst

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.

2.24 Litteratur

De angivna kapitlen i böckerna motsvarar det här kapitlet.

Noter

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?

2 Jaja, flera personer kan väl köra samma bil, och en person kan köra flera bilar samtidigt, men det brukar inte bli så bra.

3 Förutom att den svaga entitetstypen kan ha en partiell nyckel, och att man därför kan ha mer än en tittning som hör till en och samma kombination av person, film och biograf.

4 En introduktion till objektorientering finns på bokens webbplats.

5 Det engelska ordet "enhanced" betyder ungefär "förbättrad".

6 Besökt 2017-12-31.

59

Kapitel 3 Mer om datamodellering

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

3.1 Varför ska jag lära mig det hä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.

I det här kapitlet ska vi ge några tips på hur man kan göra, och om vad som kan vara bra och dåligt i några olika fall.

60

3.2 Hierarkier

Ibland behöver man modellera hierarkier, dvs "pyramider", till exempel av typen att anställda i ett företag är chefer för andra anställda, som i sin tur kan vara chefer för ytterligare andra anställda, eller att delar i en maskin består av andra delar, som i sin tur kan bestå av ytterligare andra delar.

Man kan dela in hierarkierna i två olika typer:

3.3 Generalisering och specialisering

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.

64

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

3.4 Generalisering 1: Superklass i EER

Antag att vi har några olika sorters mellanmål:

illustration

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:

65
illustration

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

illustration

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.

66

En bra lösning är att generalisera genom att skapa en mer generell entitetstyp, Mellanmål, som innefattar de tre grupperna frukter, grönsaker och godis.

illustration

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

illustration

De attribut som är gemensamma för alla grupper av mellanmål kan nu ritas ut på den nya mer generella entitetstypen:

67
illustration

Samma sak gäller för de sambandstyper som är gemensamma för alla grupper av mellanmål. Tycker om-sambandet behöver bara ritas ut en gång:

illustration

3.5 Generalisering 2: Att flytta saker från schema till data

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:

illustration

till ett som har en enda entitetstyp för mellanmål, men med ett attribut Grupp som används för att tala om vilken mellanmålsgrupp som ett visst mellanmål tillhör:

illustration

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:

69
illustration

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

70
illustration

I lösningen där mellanmålsgruppen bara är ett attribut på entitetstypen, kan användaren mata in vad som helst som mellanmålsgrupp:

illustration

(Notera felstavningen!)

Och slutligen, om Mellanmålsgrupp är en entitetstyp, där dess instanser representerar de mellanmålsgrupper som finns, kan användaren återigen välja i en lista mellan existerande mellanmålsgrupper, men nu är detta inte en fast del av användargränssnittet, utan mellanmålsgrupperna hämtas ur databasens data:

71
illustration

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

3.6 Specialisering

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.

3.7 Ett annat exempel på generalisering

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:

illustration

Eftersom varje inspektion som sker, sker på en viss plats, kopplar vi ihop varje inspektionstyp med de olika platstyper som inspektionerna kan ske på:

illustration

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:

73
illustration

Nu ä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:

illustration

3.8 De viktigaste begreppen

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.

3.9 Övningar

Det finns svar till övningarna, och en del diskussioner kring svaren, på bokens webbplats, men försök lösa uppgifterna själv innan du tittar i facit.

3.10 Litteratur

Databasböcker på grundnivå brukar alltid ta upp en del om datamodellering, men det är olika hur mycket de går in på hur man modellerar saker.

76
77

Kapitel 4 Designprocessen

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.

4.1 Traditionell databasdesign

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:

illustration

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.

4.2 Object-Relational Mapping, ORM

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.

Exempel på system (man säger ofta "ramverk") för ORM är Hibernate, som främst används med Java, och Microsofts Entity Framework, som används med .NET.

81

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

4.3 Litteratur

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.

82
83

Kapitel 5 Relationsmodellen

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

5.1 Varför ska jag lära mig det här?

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.

84

5.2 Relationer

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.

5.3 Schema och innehåll

I databassammanhang brukar man skilja på schemat, som beskriver vad som kan finnas i databasen, och det innehåll som råkar finnas där vid ett visst tillfälle.

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.

86

5.4 "Medlem", "medlemmar" eller "members"?

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.

5.5 Olika sorters nycklar

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.

De övriga kandidatnycklarna, som inte valdes som primärnyckel, kallas alternativnycklar eller ibland kallas sekundärnycklar.

88

5.6 Kopplingar mellan relationer

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

Vi antar att Sektionskod är primärnyckel, och därför stryker vi under den i tabellhuvudet. Namn är alternativnyckel.

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.

Namnet främmande nyckel på referensattribut beror på att referensattributets domän, dvs vilka värden det kan ha, är samma som primärnyckeln i den relation som det refererar till.

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
89

Medlem är referensattribut till relationen Medlemmar, och Sektion är referensattribut till relationen Sektioner. Medlem och Sektion utgör tillsammans den enda kandidatnyckeln, och blir därför automatiskt primärnyckel.

Det här är ett vanligt sätt att rita upp tabellerna och kopplingarna mellan dem:

illustration

5.7 Vanliga fel

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.

5.8 Varför heter det "relationer"?

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:

91
illustration

En relation i matematiken är också en avbildning från en mängd av värden (definitionsmängden) till en annan mängd av värden (värdemängden), men här kan ett element i definitionsmängden kopplas ihop med flera olika element i värdemängden:

illustration

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

92

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

5.9 De viktigaste begreppen

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

93

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

5.10 Litteratur

Noter

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.

4 I SQL går det också att referera till en alternativnyckel som är deklarerad med UNIQUE.

5 Förutsatt att man skapat lämpliga index. Läs mer i kapitel 22 på sidan 445.

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.

94
Blank sida
95

Kapitel 6 Översättning från ER-modellen till relationsmodellen

Det här avsnittet handlar om hur man översätter ett ER-diagram till tabeller, som det sen går att stoppa in i en relationsdatabashanterare.

Du bör ha läst kapitel 2 om ER-modellering först, och helst också kapitel 5 om relationsmodellen.

6.1 Kort kokbok

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.

6.2 Steg 1: Vanliga entitetstyper

Jättelätt! Varje vanlig entitetstyp blir en tabell, med samma namn som entitetstypen. Vanliga attribut blir kolumner i tabellen. Exempel:

illustration
97

Ö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

98

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

6.3 Steg 2: 1:N-sambandstyper

Också jättelätt! Varje 1:N-sambandstyp blir ett referensattribut i "många"-entitetstypens tabell.

illustration

De två entitetstyperna blir förstås två tabeller. Äger-sambandstypen blir ett referensattribut i Bilar-tabellen:

Personer
Nummer Namn Telefon
17 Hjalmar 174590
4711 Hulda 019-94639
42 Hjalmar 070-7347013
99
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.

6.4 Steg 3: 1:1-sambandstyper

Varje 1:1-sambandstyp blir ett referensattribut i den ena entitetstypens tabell, men ibland kan man i stället slå ihop tabellerna.

Det finns alltså tre olika sätt att göra översättningen:

illustration

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:

100
Personer
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
101

Alla tre översättningarna fungerar, och vilken av dem man väljer beror på hur man ska använda databasen, men i det här exemplet skulle vi nog välja mittenalternativet, alltså att ha ett referensattribut SitterPå i tabellen Näsor.

6.5 Steg 4: N:M-sambandstyper

Varje N:M-sambandstyp blir en egen tabell.

illustration

De två entitetstyperna blir förstås två tabeller. Äger-sambandstypen blir en mellantabell eller sambandstabell, som kopplar ihop dem:

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.

102

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

6.6 Steg 5: Sambandstyper av högre grad än två

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

illustration

Översätt alltså sambandstypen Sett till en egen tabell, som har en sammansatt primärnyckel bestående av primärnycklarna från de tre hopkopplade tabellerna:

103
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

6.7 Steg 6: Attribut på sambandstyper

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:

illustration

Vi översätter precis som vi gjorde förut, men lägger till kolumnen Inköpsår bredvid referensattributet Ägare:

Personer
Nummer Namn Telefon
17 Hjalmar 174590
4711 Hulda 019-94639
42 Hjalmar 070-7347013
104
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:

illustration

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

6.8 Steg 7: Svaga entitetstyper

Varje svag entitetstyp blir en tabell. Primärnyckeln består av den svaga entitetstypens partiella nyckel, kombinerad med den identifierande entitetstypens primärnyckel.

Exempel:

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

6.9 Steg 8: Sammansatta attribut

Sammansatta attribut behandlas som om de bara bestod av delarna.

Exempel:

106
illustration
Personer
Nummer Namn Riktnummer Lokalnummer
17 Hjalmar null 174590
4711 Hulda 019 94639
42 Hjalmar 070 7347013

6.10 Steg 9: Flervärda attribut

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.

Exempel:

illustration
Personer
Nummer Namn
17 Hjalmar
4711 Hulda
42 Hjalmar
Telefon
Person Telefon
17 174590
17 260088
42 070-7347013
42 174590
4711 019-94639
107

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.

6.11 Steg 10: Härledda attribut

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

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

6.12 Steg 11: Arv i EER-modellen

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:

illustration

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

Förklaring:

109

En lärare har inga egna attribut, så hela idén med tabellen Lärare är att visa vilka av personerna från tabellen Personer som är lärare.

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:

110
illustration

Nä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
111

6.13 Litteratur

En mer avancerad bok som bland annat tar upp mycket om hur man översätter datastrukturerna i ett objektorienterat program till tabeller för lagring i en relationsdatabas, med olika alternativ och deras fördelar och nackdelar:

Se avsnitt 33.2 på sidan 673 för detaljer om böckerna.

6.14 Övningar

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.

Noter

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.

2 Den här typen av villkor, som automatiskt begränsar vad man kan lagra i databasen, kallas integritetsvillkor och beskrivs mer ingående i kapitel 13.

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.

112
Blank sida
113

Kapitel 7 Introduktion till frågespråket SQL

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.

7.1 Varför ska jag lära mig det här?

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.

114

7.2 Standard-SQL och SQL-standarder

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.

Dessutom finns standarden ISO/IEC 13249, "SQL Multimedia and Application Packages", som beskriver datatyper för användningsområden som multimedia och data mining, och ISO/IEC TR 19075, som är en serie tekniska rapporter som beskriver SQL-stöd för en del specialiserade tekniker, till exempel reguljära uttryck.

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.

7.3 Förberedelser

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

7.4 De första stapplande stegen

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.

Det har blivit en konvention i många sammanhang att skriva nyckelorden i SQL med stora bokstäver (versaler), som i SELECT och FROM, men av läsbarhetsskäl har vi valt att använda små bokstäver (gemener).

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

Varje rad (eller tupel) beskriver en sak av något slag, i det här fallet en medlem i idrottsklubben, och varje kolumn (eller attribut) beskriver en egenskap hos en sådan sak, exempelvis dess nummer eller namn.

117

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

7.5 Enkla frågor med select

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

Med kommandot "select Namn from Medlemmar" menar vi helt enkelt "välj ut kolumnen Namn ur raderna i tabellen Medlemmar".

Vi kan välja ut mer än en kolumn om vi vill, till exempel namnet och telefonnumret (kolumnerna Namn och Telefon):

               
                  
                     select Namn, Telefon from Medlemmar;
                  
               
            
Namn Telefon
Stina 282677
Sam 260088
Lotta 174590
Olle 260088
Den första frågan vi ställde, "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

Medlemmar.Namn betyder här "kolumnen Namn i tabellen Medlemmar", men eftersom det än så länge inte kan uppstå några missförstånd räcker det med att skriva Namn, utan någon tabellangivelse.

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

Man kan bygga upp mer komplicerade where-villkor med hjälp av and och or, på samma sätt som man brukar kunna göra i andra programmeringsspråk:

               
                  
                     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
120

7.6 Citationstecken

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

7.7 Om man skriver fel

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
                  
               
            

7.8 Frågor med flera tabeller

Vi har en tabell till i databasen, nämligen tabellen Sektioner som beskriver idrottsklubbens olika sektioner:

               
                  
                     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.

122

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

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.

7.9 Ett annat sätt att kombinera tabeller

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.

Vi var ju egentligen bara intresserade av bowlingsektionens ledare, så vi lägger in det också i where-villkoret:

               
                  
                     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
En fråga där man kombinerar (joinar) två tabeller går alltså att skriva på minst två olika sätt: antingen med bägge tabellerna på fromraden 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.

7.10 Repetition

Det här är grunden för hur man skriver enkla SQL-frågor:

Så här:

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

7.11 Ett exempel till: Vad sportar Olle?

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.

Nu vill vi använda alla tre tabellerna i databasen (Medlemmar, Sektioner och Deltar) för att svara på frågan Vilka sporter ägnar sig Olle åt?

127

Fö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
Vi kan skriva samma fråga lite enklare med hjälp av nyckelordet in:
               
                  
                     select Namn from Sektioner
                  
                  
                     where Skod in ('A', 'B', 'C');
                  
               
            
Namn
Bowling
Konstsim
Kickboxing
Det var alltså det dumma sättet. Nu ska vi i stället skriva ihop de tre frågorna till en enda fråga. Först stoppar vi in en inre fråga för att slippa det hårdkodade "('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
Och så stoppar vi in den första frågan, "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

Därmed är frågan färdig, och eftersom det är en enda fråga kan den behandlas snabbt och effektivt av databashanteraren.

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.

130

7.12 in och not in

Vilka 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
Frågan kan också, som vanligt, skrivas om utan någon 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
En fråga som innehåller "... 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.)

7.13 Mer om nästlade frågor: exists och not exists

Förutom 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

En underfråga som är sammankopplad med en yttre fråga på det här sättet, och alltså inte kan köras självständigt, kallas för en relaterad eller sammankopplad fråga, eller på svengelska korrelerad fråga, efter engelskans correlated query.

Eftersom det bara finns en kolumn som heter Medlem, och en som heter Mnr, kan de inte förväxlas med några andra kolumner, och man behöver inte skriva ut vilka tabeller de hör till:

               
                  
                     select * from Medlemmar
                  
                  
                     where exists (select * from Deltar
                  
                  
                          where Medlem = Mnr);
                  
               
            

Om man i stället skriver

               
                  
                     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.

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

7.14 Jämförelser med any, some och all

Ibland behöver man jämföra ett värde med flera andra värden, till exempel för att se om det är större (eller mindre) än alla, eller bara något, av de andra värdena. Då kan man använda 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

Det var bara för Olle, med medlemsnummer 1, som det inte fanns någon annan medlem som hade ett lägre nummer.

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.

134

7.15 Sortering med order by

Ibland vill man ha raderna i svaret sorterade i en viss ordning. Det görs enkelt med konstruktionen order 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
Man kan ange en kombination av flera olika kolumner att sortera efter, och man kan sortera i stigande eller fallande ordning genom att ange 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
En intressant sak är att en 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.

7.16 Tio-i-topp-listor

En annan viktig sak som har med 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 syntaxen stop 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;
                  
               
            

Och i Microsoft SQL Server:

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.

7.17 Överkurs: Tabellnamn och alias i nästlade frågor

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

I det här fallet måste vi ge alias till tabellerna, på samma sätt som vi kommer att se i ett senare stycke:

               
                  
                     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
Ordet 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);
                  
               
            

7.18 Att ändra på databasens innehåll

Vi kan inte bara söka i databasens innehåll, utan vi kan också ändra på innehållet. Vi kan lägga till rader med kommandot 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

Om det fattas ett värde, till exempel om vi vill lägga in medlemmen Nelson som inte har någon telefon?

               
                  
                     insert into Medlemmar values (8, 'Nelson');
                  
               
            

Vi får ett felmeddelande:

               
                  
                     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
                  
               
            

Om vi inte har värden till alla kolumnerna för en rad, måste vi lista namnen på kolumnerna, så att databashanteraren vet vilka kolumner det är vi faktiskt ska lägga in värden i:

               
                  
                     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 i insert-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
Notera att man måste skriva villkoret som "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));
                  
               
            

Här har vi angett vilka kolumner som ska finnas i tabellen, vilka typer av data som går att lägga in i de kolumnerna, och även att Mnr är primärnyckel.

Eftersom det bara är Mnr som måste ha ett unikt värde för varje rad, går det bra att exempelvis lägga in ännu en medlem som heter Nelson:

               
                  
                     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;
                  
               
            

Det brukar också finnas kommandon för att läsa in data från en fil till en tabell, men det varierar mellan olika databashanterare hur man skriver.

Vi kan ta bort rader med kommandot delete:
               
                  
                     delete from Medlemmar where Namn = 'Isaac';
                  
               
            

Databashanteraren svarar, beroende på typ av databashanterare och vilket verktyg vi använder för att ställa SQL-frågor:

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

Databashanteraren svarar:

               
                  
                     1 row updated
                  
               
            

Nu ser tabellen ut så här:

Mnr Namn Telefon
2 Stina 282677
3 Sam 260088
4 Lotta 174590
1 Olle 260088
7 Isaac 281000
8 Nelson null
17 Nelson 281000
Vi provar att använda 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';
                  
               
            

7.19 Vyer

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;
                  
               
            

Nu fungerar vyn som vilken tabell som helst (i alla fall så länge vi bara ska titta på innehållet, och inte försöker ändra det):

               
                  
                     select * from Ledarskap;
                  
               
            
Sektionsnamn Ledarnamn
Konstsim Stina
Bowling Lotta
Kickboxing Lotta
               
                  
                     select Ledarnamn
                  
                  
                     from Ledarskap
                  
                  
                     where Sektionsnamn = 'Bowling';
                  
               
            

Ledarnamn

Lotta

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;
                  
               
            

7.20 Transaktioner med commit och rollback

Det flesta relationsdatabashanterarna har något som kallas transaktioner, vilket bland annat innebär att de kan gruppera flera SQL-satser till en enhet, vars ändringar gemensamt antingen kan sparas i databasen eller kastas bort. För att styra det använder man SQL-kommandona 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);
                  
               
            

Kontrollera innehållet:

               
                  
                     select * from Sektioner;
                  
               
            
Skod Namn Ledare
A Bowling 4
C Konstsim 2
B Kickboxing 4
D Brännboll 3
Ge kommandot 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
146 Nu ångrar vi oss. Vi vill ha tillbaka brännbollen, BASE-hoppning 17 är alltför farligt, och medlemmarna i bowlingsektionen är mycket missnöjda med att deras trevliga bowlingkvällar gjorts om till träning för maratonlopp. Därför kastar vi bort ändringarna sen senaste commit genom att ge kommandot 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

7.21 Skriv ut alla kolumnnamnen!

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.

Ta denna fråga:

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

7.22 SQL som DDL

Förutom ett frågespråk eller datamanipuleringsspråk är SQL även ett datadefinitionsspråk eller DDL, 18 dvs ett språk som man kan beskriva databasens schema med. I en relationsdatabashanterare betyder det att man skapar tabeller, och det gör man med kommandot 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.

Om man har en alternativnyckel, dvs en annan kolumn eller kombination av kolumner som också skulle kunna användas som primärnyckel, kan man specificera det med nyckelordet unique, till exempel som unique (Namn).

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.

Nyckel- och referensvillkor som bara berör en enda kolumn (alltså inte nycklar som är sammansatta av flera kolumner), kan skrivas

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

7.23 Mer om SQL som DDL: Så här skapade vi vår klubb-databas

Som illustration av SQL som datadefinitionsspråk visar vi här hur vi skapade tabellerna i idrottsdatabasen. 20 Som vi ser kan man använda kommandot 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);
                  
               
            

7.24 Hur man gör i praktiken

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

7.25 Så här la vi in data i klubb-databasen

Vi har redan visat hur 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');
                  
               
            

7.26 Scheman

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 heter information_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;
                  
               
            

7.27 Vanliga fel i SQL

Ett vanligt fel i SQL är att försöka skapa ett referensattribut som refererar till en kolumn, eller kombination av kolumner, i en annan tabell, som inte är en nyckel. Man måste ha deklarerat detta med 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 med not 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:

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

7.28 Prestanda i SQL

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.

154

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

7.29 Övningar

Det finns svar till övningarna på bokens webbplats, men försök lösa uppgifterna själv innan du tittar i facit.

En sökmotor med en databas

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:

illustration

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

7.30 Litteratur

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:

157

Se avsnitt 33.2 på sidan 673 för detaljer om böckerna.

Eftersom olika databashanterares SQL-dialekter skiljer sig åt, har man också behov av en bok, eller manual, för just den databashanterare man använder.

158

Noter

1 http://www.itl.nist.gov/div897/ctg/sql_form.htm, besökt 2017-12-31.

2 http://developer.mimer.se/validator/parser99/index.tml, besökt 2017-12-31.

3 Men se till exempel avsnitt 30.2 på sidan 656 om hur man installerar och kommer i gång med PostgreSQL på en Ubuntu-dator.

4 SQL-kommandon för att skapa tabellerna och lägga in exempeldata finns i avsnitt 7.23 och 7.25, och kan även laddas ner från bokens webbplats.

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

6 Det där om att faktiskt läsa felmeddelanden är ett påpekande som man kanske tycker inte borde behövas, men erfarenheten från många års handledning av databas- och programmeringsövningar måste tolkas annorlunda.

7 Vi har formaterat om en del felmeddelanden något för att de ska få plats på bredden i boken.

8 SQL kan skrivas i fritt format, och det finns inget som säger att den måste delas upp i rader på ett visst sätt. Det är också vanligt att man säger from -klausul, efter det engelska ordet clause.

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.

12 Det är inte alls säkert att det kommer att fungera så internt i databashanteraren, men resultatet blir detsamma.

13 Många "tio-i-topplistor" tillåter delade placeringar, och kan därför innehålla fler än tio poster. I Microsoft SQL Server skriver man helt enkelt with ties, dvs "med delade placeringar", men i andra system kan den typen av lista kräva en mer komplicerad lösning än dem som presenteras här.

14 Se upp med att många databashanterare inte klarar tabell- och kolumnnamn som innehåller de svenska tecknen Å, Ä och Ö.

15 Se som vanligt upp med att många databashanterare inte klarar tabell- och kolumnnamn som innehåller de svenska tecknen Å, Ä och Ö.

16 En del databashanterare har begränsningar, till exempel för hur lång texten i en vydefinition får vara.

17 Fallskärmshoppning från broar, utför stup med mera. Räkna inte med att din försäkring gäller.

18 Data Definition Language.

19 Till exempel måste man då ofta använda yttre join som beskrivs i kapitel 8.

20 SQL-kommandona för att skapa databasen kan även hämtas från bokens webbplats.

21 http://www.tpc.org/information/benchmarks.asp, besökt 2017-12-31.

22 Bilarna var programmerade att minska utsläppen om de upptäckte att de blev testade, men inte vid vanlig körning.

23 http://jimgray.azurewebsites.net/benchmarkhandbook/toc.htm, besökt 2017-1231.

159

Kapitel 8 Mer om SQL: Aggregatfunktioner, null-värden, vyer och rekursiva frågor

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.

8.1 Exempeldatabasen: arbetare och kontor

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
160
Kontor
Knr Knamn
10 Gnesta
20 Moskva
30 Pyongyang

Kolumnen Placering refererar förstås till kontorsnumret Knr i den andra tabellen, så att man vet på vilket kontor som varje arbetare arbetar.

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

Lägg märke till att Moskva-kontoret försvann ur resultatet, eftersom ingen arbetare är placerad på Moskva-kontoret:

illustration

8.2 Grunder om aggregatfunktioner

En aggregatfunktion arbetar på en hel kolumn, till exempel för att summera alla värden i kolumnen eller för att räkna ut medelvärdet. SQL-standarden innehåller några aggregatfunktioner: 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;
                  
               
            

max(Anr)

3

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

Maxnumret

3

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 i where-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
nullvärden ignoreras av aggregatfunktioner, som om de raderna inte alls hade funnits. Medelvärdet avg av 2, 4 och null är alltså 3, inte 2.

8.3 Aggregatfunktioner med where

Vi använder aggregatfunktionerna sum och avg för att räkna ut summan och genomsnittet av allas löner:

               
                  
                     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 ett where-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';
                  
               
            

avg(Lön)

1 000

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

avg(Lön)

1 500

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');
                  
               
            
164

8.4 Mängdoperationer i SQL

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.

Tabeller i relationsmodellen kan ses som mängder av rader, och därför kan man även i SQL använda de vanliga mängdoperationerna union, snitt och differens.

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.

I följande två mängder, kallade X och Y, innehåller mängden X de tre elementen 1, 2 och 3, medan mängden Y innehåller de två elementen 3 och 4:

illustration

Följande figurer visar unionen, snittet respektive differensen av X och Y:

illustration

Operationen union finns i många SQL-dialekter, men snitt och differens kan saknas i en del.

165

För att kunna beräkna mängdoperationer på två tabeller i SQL måste de vara unionkompatibla, dvs ha samma antal och datatyp på kolumnerna.

Man 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
Den som studerat matematik och minns sin mängdlära vet att mängdoperationen union och den logiska operationen eller är relaterade: unionen av två mängder är ju de element som finns med i den ena eller den andra mängden, eller båda. Ovanstående SQL-fråga med union är ekvivalent med följande fråga med or:
               
                  
                     select Anr, Anamn
                  
                  
                     from Arbetare
                  
                  
                     where Placering = 10 or Placering = 30;
                  
               
            

8.5 Mängder och påsar

Ä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 en select-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.

8.6 Aggregatfunktioner med group by och having

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;
                  
               
            

avg(Lön)

1 500

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;
                  
               
            

avg(Lön)

1 000

1 500

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

avg(Lön)

1 333

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;
                  
               
            

avg(Lön)

null

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ända union 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;
                  
               
            

avg(Lön)

1 500

1 000

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
168 Man kan fortfarande ha ett 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
Se det som att först körs frågan med 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 nyckelordet having 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
(Ja, 1 000 är inte mer än 1 000.) Man kan ha 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;
                  
               
            

Placering avg(Lön)

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
Kom ihåg: Först frågan med 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

(Samma som tidigare.)

               
                  
                     select Knamn, avg(Lön)
                  
                  
                     from Arbetare, Kontor
                  
                  
                     where Placering = Knr
                  
                  
                     group by Knamn;
                  
               
            
Knamn avg(Lön)
Gnesta 1500
Pyongyang 1000
Igen: Se det som att man kör den vanliga "select * from ... where ..." först, och sen kör man aggregatfunktionerna.
170

8.7 Ett problem: Saker som försvinner

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.

8.8 Explicit join

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:

illustration

Lägg märke till att Moskva-kontoret försvann ur resultatet, för det finns ingen arbetare som är placerad på Moskva-kontoret:

illustration

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

Man kan se det som att först beräknas joinen och blir en ny tabell, och sen ingår den i from-listan precis som en vanlig tabell.

Vi kan också lägga på ett where-villkor som vanligt, till exempel för att bara få med de arbetare som jobbar i Gnesta:

               
                  
                     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

Vi ska senare se hur man kan koppla ihop fler än två tabeller med explicita joinar, eller en blandning av explicita och implicita joinar.

Notera att när vi joinar två tabeller, vare sig implicit,

               
                  
                     select *
                  
                  
                     from Arbetare, Kontor
                  
                  
                     where Placering = Knr;
                  
               
            

eller explicit,

               
                  
                     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.

8.9 Yttre join

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
173

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 (illustration), på höger sida för höger-ytter-join (illustration) eller på båda sidorna för full yttre join (illustration).

Om man skriver joinen som vi gjorde ovan av de två tabellerna med ett relationsalgebrauttryck, ser det ut så här:

               
                  
                     Arbetare
                  
                  
                     x#---6bOYygyTzz-CODE-SPACE-vrjYGfS6Jo---#x							illustration
                        Placering=Knr
                     
                  
                  
                     Kontor
                  
               
            

Vi kan utgå från frågan ovan, med dess yttre join, och beräkna aggregatfunktioner som summor och genomsnitt:

               
                  
                     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.

Som vi lärt oss tidigare kan vi använda group by för att beräkna flera olika genomsnitt, och det kan vi förstås göra även tillsammans med den yttre joinen:

               
                  
                     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
Det kan vara opraktiskt att få null i summan, till exempel om SQL-frågan körs inifrån ett Java- eller C-program och programmet ska jobba vidare med resultatet. Vi vill inte att programmet ska krascha för att det försöker följa en null-pekare! Vi kan använda funktionen 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

8.10 Count kan räkna antingen rader eller värden

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
175 Med aggregatfunktionen 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

8.11 Flera join-operationer i samma fråga

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:

176
Jobbar
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);
                  
               
            

Vem jobbar var?

               
                  
                     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:

illustration

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:

178
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

Vi provar:

               
                  
                     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
Joinvillkoret är 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

Rätt svar!

Man skulle också kunna göra så här, med först en vanlig join mellan Jobbar och Projekt, och sen vänster-ytter-joinar vi Arbetare med resultatet av den inre joinen:

               
                  
                     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

8.12 Vyer

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
Vilket lokalkontor har flest arbetare? Nu blir det svårare, för det finns ingen färdig tabell att applicera 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

Skapa en vy av detta:

               
                  
                     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

Sen använder vi vyn av joinen som om den var en tabell, på samma sätt som när vi ville veta vem som hade högst lön:

               
                  
                     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.

8.13 CTE:er

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.

Så här kan man skriva den sista sökningen i stycket ovan, utan att vyn Kontorspersonal behöver läggas in i databasen:

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

8.14 Join med sig själv

Vilka arbetare har lika lön som någon annan? Vi använder två alias, 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

8.15 Problem med null i SQL

Som vi sett tidigare är 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.)

Dessutom fungerar null på ett sätt som kan vara lite oväntat i samband med aggregatfunktioner, som vi sett ovan med sum.

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

Nu ser tabellerna ut så här:

185
Kloka
ID Namn
1 Anna
2 Bertil
3 Cecilia

Vi söker än en gång fram de kloka och vackra, med precis samma SQL-fråga som förut, och vi får samma resultat som då:

               
                  
                     select * from Kloka
                  
                  
                     where Namn in (select Namn from Vackra);
                  
               
            
ID Namn
1 Anna

Vi försöker också söka fram de kloka, men inte vackra, fortfarande med precis samma SQL-fråga som förut:

               
                  
                     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!

Det beror förstås på att jämförelser med null alltid ger resultatet falskt i SQL, och skrivsättet not in döljer en jämförelse mellan de båda Namn-kolumnerna i tabellerna.

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.

8.16 Rekursiva frågor

Det finns saker som är svåra eller t.o.m. omöjliga att göra som en enkel 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).

Vi har en tabell Anställda, och i den finns en kolumn Chef som anger vem som är den anställdes närmaste chef:

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.

Givet en viss person, till exempel Petter (nummer 10) är det lätt att ta fram den personens chef med en SQL-sats:

               
                  
                     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

Om vi vill ha fram Petters chefer på båda nivåerna direkt ovanför honom, kan vi använda operationen union för att slå samman de båda svaren:

               
                  
                     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
Så länge vi vet hur många nivåer upp vi ska gå, kan vi skriva en SQL-sats. Men om uppgiften är att hitta den anställdes alla chefer, oberoende av om de befinner sig en, två eller kanske hundra nivåer upp i hierarkin, blir det svårare. Det är den typen av operation, att ta sig igenom exempelvis en hierarki i godtyckligt många nivåer, som kallas transitivt hölje, och den går inte alltid att göra i en vanlig 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
Alla har alltså Stina som superboss, inklusive Stina själv. Vad vi gör här är att definiera en delfråga som heter Superboss och är rekursiv eftersom Superboss refererar till sig själv i sin selectsats.

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

Maria, Ulrik och Petter har inte lägre någon superboss eftersom Maria och Petter är varandras chefer.

En begränsning med rekursiva frågor är att man bara får referera sig själv en gång i 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.

8.17 Ännu mer SQL

Det finns ytterligare kommandon i SQL som tas upp på annan plats i den här boken. Till exempel finns kommandona 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.

8.18 Nyheter i SQL:1999

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.

Det sägs att det ännu inte finns en enda databashanterare som klarar hela SQL-92, så man ska nog inte förvänta sig att SQL:1999, eller SQL:2016, ska finnas fullständigt implementerad särskilt snart.

191

Nytt i SQL:1999: Datadefinition

Det finns flera nyheter som berör datatyper i SQL:1999.

Här är ett körexempel från databashanteraren Mimer, som visar hur man kan använda kommandot 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:

Nytt i SQL:1999: Triggers

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

193

Nytt 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 exempel savepoint och rollback to savepoint.

Nytt i SQL:1999: Säkerhet

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.

8.19 Övningar

Det finns svar till övningarna på bokens webbplats, men försök lösa uppgifterna själv innan du tittar i facit.

Noter

1 I tabellen Arbetare har vi en kolumn som heter Lön, men en del databashanterare har problem med svenska tecken i tabell- och kolumnnamn.

2 SQL-kommandona för att skapa tabellerna och lägga in exempeldata kan även laddas ner från bokens webbplats.

3 Ofta, men inte alltid, lika med antalet rader. Null-värden räknas nämligen inte.

4 Databashanteraren arbetar förmodligen annorlunda, och effektivare, internt.

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.

6 SQL-kommandona för att skapa tabellerna och lägga in exempeldata kan även laddas ner från bokens webbplats.

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.

8 SQL-frågor är en form av programmering, och som vanligt när man programmerar ska man försöka skriva så tydligt och begripligt som möjligt, inte minst för att underlätta för sig själv. Därför bör namn på vyer, alias och annat vara meningsfulla och rättvisande.

Därför bör man oftast undvika namn som x, y, a, a1 och a2 , om det inte (som vi tror att det blir här) blir rörigare och mer svårläst med längre namn.

9 update Anställda set Chef=10 where Nummer=8;

9 update Anställda set Chef=10 where Nummer=8;
195

Kapitel 9 Sammanfattning av SQL-kommandon

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:

9.1 Select-frågor

Sökningar och sammanställningar görs med kommandot select, i så kallade "frågor" eller "select-frågor".

Syntax för select-frågor

select-fråga

vanlig-select-fråga

| mängduttryck

vanlig-select-fråga

select [ distinct all ] kolumner from tabeller [ where where-villkor ] [ group by kolumner [ having having-villkor ]] [ order by kolumner [ asc desc ] ]

mängduttryck

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

kolumner

*

| kolumn [, kolumn ] ...

kolumn

kolumnnamn [ as namn ]

| tabellnamn . kolumnnamn [ as namn ] | uttryck [ as namn ] 197

tabeller

tabell [, tabell ] ...

tabell

tabellnamn [ as 1 namn ]

| join-uttryck

•join-uttryck 2

tabell [ inner ] join tabell on join-villkor | tabell left [ outer ] join tabell on join-villkor | tabell right [ outer ] join tabell on join-villkorwhere-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 -.

Exempel på select-frågor

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

9.2 Insert

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

Syntax för insert

insert-kommando

insert into tabellnamn 3 [ kolumn-lista ] values värde-lista | insert into tabellnamn [ kolumn-lista ] sql-fråga

kolumn-lista

( kolumnnamn [, kolumnnamn ] ... )

värde-lista

( värde [, värde ] ... )

Exempel på insert

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

9.3 Delete

SQL-kommandot delete används för att ta bort noll, en eller flera rader ur en tabell.

Syntax för delete

delete-kommando

delete from tabellnamn [ where where-villkor 4 ]

Exempel på delete

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

9.4 Update

SQL-kommandot update används för att ändra data på noll, en eller flera rader i en tabell.

Syntax för update

update-kommando

update tabellnamn set kolumn-värdes-lista [ where where-villkor 5 ]

kolumn-värdes-lista

kolumnnamn = uttryck [, kolumnnamn = uttryck ] ...

201

Exempel på update

               
                  
                     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;
                  
               
            

9.5 Create table

SQL-kommandot create table används för att skapa nya tabeller.

Syntax för create table

create-table-kommando

create table tabellnamn 6

( kolumndefinition [, kolumndefinition ] ... [, tabellvillkor ] ... )

kolumndefinition

namn datatyp [ default default-värde ][ kolumnvillkor ] ...

kolumnvillkor

[ constraint namn ] not null |[ constraint namn ] primary key |[ constraint namn ] unique |[ constraint namn ] references tabellnamn ( kolumnnamn ) |[ constraint namn ] check villkor

tabellvillkor

[ constraint namn ] primary-key-deklaration |[ constraint namn ] unique-deklaration 202 |[ constraint namn ] foreign-key-deklaration |[ constraint namn ] tabell-check-villkor

primary-key-deklaration

primary key ( kolumnnamn [, kolumnnamn ] ... )

unique-deklaration

unique ( kolumnnamn [, kolumnnamn ] ... )

foreign-key-deklaration

foreign key ( kolumnnamn [, kolumnnamn ] ... ) references tabellnamn ( kolumnnamn [, kolumnnamn ] ... ) [ on delete referensåtgärd on update referensåtgärd ]

referensåtgärd

no action | cascade | restrict | set null | set default

tabell-check-villkor

check villkor

datatyp 7

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.

Exempel på create table

               
                  
                     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));
                  
               
            
204

9.6 Create view

En vy kan ses antingen som en virtuell tabell, vars innehåll beräknas på nytt varje gång man använder den, eller som en sparad och namngiven fråga.

Syntax för create view

create-view-kommando

create view vynamn [ kolumn-lista ] as select-fråga

kolumn-lista

( namn [, namn ] ... )

vynamn kan, precis som tabellnamn, vara antingen ett enkelt namn (som Kund) eller innehålla ett schemanamn (som kundbasschema.Kund).

Exempel på create view

               
                  
                     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;
                  
               
            

9.7 Alter table

Kommandot 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

Syntax för alter table

alter-table-kommando

alter table tabellnamn förändring

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 ]

Exempel på alter table

               
                  
                     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;
                  
               
            

9.8 Create index

SQL-standarden innehöll länge inget om index och lagringsstrukturer, men i nästan alla relationsdatabashanterare har man kunnat skapa index med kommandot create index och ta bort dem med kommandot drop index.

Syntax för create index

create-index-kommando

create [ unique ] index indexnamn on tabellnamn kolumn-lista 206

kolumn-lista

( kolumnnamn [ riktning ][, kolumnnamn [ riktning ] ] ... )

riktning

asc desc

Exempel på create index

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

9.9 Drop

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

Syntax för drop

drop-kommando 11

| drop index indexnamn 12 | drop table tabellnamn [ restrict cascade ] | drop view vynamn [ restrict cascade ]

Exempel på drop

               
                  
                     drop table Avdelningar;
                  
                  
                     DROP TABLE avdelningar;
                  
                  
                     drop table Avdelningar cascade;
                  
                  
                     drop table Avdelningar restrict;
                  
                  
                     drop view Datastudenter;
                  
                  
                     drop view AntalBilarPerKontor cascade;
                  
                  
                     drop index Personnamnsindex;
                  
               
            
208

9.10 Transaktioner

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

commit [ work 14 ]

rollback-kommando

rollback [ work ]set transactionset transaction transaktionssätt [ 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;
                  
               
            
209

9.11 Säkerhet med grant och revoke

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

Genom att definiera vyer, och dela ut rättigheter till vyerna, men inte till de tabeller som vyerna baseras på, kan man på ett mycket finkornigt sätt styra rättigheterna.

Syntax för grant och revoke

grant-kommando

grant rättigheter to användarlista [ with grant option ]

användarlista

användare [, användare ] ...

rättigheter

åtgärd [, åtgärd ] ... on [ table ] tabellnamn | all privileges on [ table ] tabellnamn

åtgärd

delete | select | update ( kolumnnamn [, kolumnnamn ] ... ) | references ( kolumnnamn [, kolumnnamn ] ... )

revoke-kommando

revoke rättigheter from användarlista [ restrict cascade ] 17

Exempel på grant och revoke

               
                  
                     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;
                  
               
            

9.12 Några andra kommandon

Här är ytterligare några kommandon, som vi inte beskriver i detalj här, men som både är mycket användbara och förekommer i många databashanterare:

9.13 Syntax i olika databashanterare

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.

Om man behöver veta exakt vilka kommandon som en viss databashanterare klarar, bör man därför läsa manualen för just den databashanteraren, snarare än att slå upp i SQL-standarden eller läsa i en generell databaslärobok.

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 beskriver delete-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:

illustration
Som synes skiljer sig databashanterarna, både i vad delete-kommandot kan göra och i hur man skriver det.

9.14 Litteratur

212

Noter

1 En del databashanterare vill inte ha as här.

2 Det finns även full [ outer ] join samt nyckelordet natural för naturlig join, men det är mindre vanligt att de är implementerade.

3 Ett tabellnamn kan även innehålla ett schemanamn. Se sidan 197 i avsnitt 9.1.

4 Where-villkor beskrivs på sidan 197 i avsnitt 9.1.

5 Ibidem.

6 Ett tabellnamn kan även innehålla ett schemanamn. Se sidan 197 i avsnitt 9.1.

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.

8 Kolumndefinition beskrivs på sidan 201 i avsnitt 9.5.

9 Tabellvillkor beskrivs på sidan 201 i avsnitt 9.5.

10 Enligt SQL-standarden måste man ange antingen restrict eller cascade, men de flesta SQL-dialekter tillåter att de utelämnas.

11 Det finns även kommandon för att ta bort andra typer av objekt, till exempel triggers och lagrade procedurer.

12 Inga objekt kan referera till ett index, så restrict och cascade vore meningslösa för kommandot drop index.

13 Kommandot start transaction heter i stället begin transaction i en del system, som Microsoft SQL Server.

14 Work betyder ingenting, och kan lämpligen utelämnas.

15 Isoleringsnivån read uncommitted är bara tillåten om även read only anges.

16 Av de olika isoleringsnivåerna är det bara serializable som ingår i Core SQL.

17 Enligt SQL-standarden måste man ange antingen restrict eller cascade, men de flesta SQL-dialekter tillåter att de utelämnas, och default-värdet är restrict.

213

Kapitel 10 Relationskalkyl

Relationsdatamodellen är baserad på att betrakta normaliserade tabeller som matematiska relationer. Man kan definiera kraftfulla frågespråk över sådana matematiska relationer baserat på matematisk logik, s.k. predikatkalkyl. SQL-frågor (dvs. 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.

10.1 Varför ska jag lära mig det här?

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.

10.2 Radkalkyl

Den matematiska logik SQL är baserat på kallas radkalkyl. Radkalkylen kännetecknas av att variablerna i SQL-frågor kan enbart vara bundna till rader i tabeller. För SQL innebär detta att 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

Antag att vi har tabellerna:

Anstalld
Pnr Namn Lön Placerad Telefon
581027-0233 Stina 282677 4 018-565758

och

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, ...)}
                  
               
            
För att kombinera flera relationer får man specificera bindningspredikat som en konjunktion av enkla (atomiska) bindningspredikat för varje relation man vill kombinera. Om man på så sätt har mer än ett bindningspredikat, genereras bindningar till alla kombinationer (kartesiska produkten) av raderna från bindningspredikaten. I SQL-frågor motsvarar detta att man listar flera tabeller i from-listan. Sådana konjunktiva bindningspredikat representerar den kartesiska produkten av raderna i angivna relationer.

Antag att vi vill ställa frågan: Finn namn, telefonnummer och avdelning för de personer som har högre lön än 100 000.

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;
                  
               
            

I radkalkyl uttrycks den som:

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

Generellt är ett predikat i radkalkylen uppbyggt av atomer, dvs. enkla predikat (villkor), som kan vara på följande format:

En formel i radkalkylen är antingen en atom eller flera formler sammansatta med de logiska operatorerna ∧ (och), ∨ (eller) och ¬ (icke).

En formel kan således vara ett av följande:

En formel kan också vara sammansatt med kvantifierarna ∃ (läses "det existerar") och ∀ (läses "för alla"):

Som ett exempel, antag att vi vill ställa frågan: Finn de anställda för vilka det existerar en avdelning där de är placerade.

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.

219

Det enklaste sättet att hantera universell kvantifiering är att omformuleras frågan som motsvarande negativa fråga: Finn de avdelningar där ingen anställd tjänar mindre än 50 000.

I 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);
                  
               
            

Vi utnyttjar här en logisk transformation (omskrivning) som innebär att för alla x är detsamma som: det finns inget x sådant att x inte gäller, vilket kan uttryckas i logik som: ∀(f ) <=> ¬∃(¬f )

220
221

Kapitel 11 Relationsalgebra

I vanlig algebra, som till exempel i uttrycket 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.

11.1 Varför ska jag lära mig det här?

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

11.2 Mängd- och påsalgebra

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.

11.3 Relationsalgebra är ett funktionellt språk

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

11.4 Logisk och fysisk relationsalgebra

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.

11.5 Operationen selektion,σ

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:

224
Medlemmar
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)
                  
               
            

får vi resultatet

Nummer Namn Telefon
4 Lotta 174590

Ett annat exempel:

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

En SQL-fråga som motsvarar det andra exemplet ovan, att välja ut de medlemmar som har medlemsnummer större än två, ser ut så här:

               
                  
                     select * from Medlemmar where Nummer > 2;
                  
               
            
225

11.6 Operationen projektion, π

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)
                  
               
            

Resultat:

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)
                  
               
            

Resultat:

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.

En SQL-fråga som motsvarar det andra exemplet ovan, att ta kolumnen Telefon från tabellen Medlemmar, ser ut så här:

               
                  
                     select distinct Telefon from Medlemmar;
                  
               
            

11.7 Relationsalgebrauttryck

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

Resultat:

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.

11.8 Mängdoperationerna union, snitt och differens

Eftersom en tabell i (den ursprungliga) relationsmodellen är uppbyggd av en mängd av rader, kan man förstås använda vanliga mängdoperationer på tabeller: union, snitt och differens.

227

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

Tabellen Medlemmar innehåller medlemmarna i vår idrottsklubb, och tabellen Nyanmälda innehåller nya medlemmar som anmält sig. Null-värdet i Hjalmars telefonnummer-kolumn betyder förstås inte att han har telefonnummer noll, utan att han inte har någon telefon, eller kanske bara att vi inte vet hans telefonnummer.

Operationen union skrivs med en sorts stort U: .

Nu kan vi få fram vilka medlemmar vi har totalt, genom att beräkna

unionen av de två tabellerna:

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

För att få fram vilka av de gamla medlemmarna som dessutom anmält sig på nytt, beräknar vi snittet mellan Medlemmar och Nyanmälda:

               
                  
                     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

11.9 Operationen yttre union

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
229

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

11.10 Operationen kartesisk produkt,×

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

även har en tabell som heter Sektioner, som innehåller data om de olika sektionerna i vår idrottsklubb:

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

Eftersom den ena tabellen hade 3 rader och den andra hade 4, innehåller den kartesiska produkten 3 gånger 4, dvs 12 rader.

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.

11.11 Operationen join (även kallad inre join)

Tabellerna Sektioner och Medlemmar var ju hopkopplade genom att kolumnen Ledare i tabellen Sektioner var ett referensattribut till Kolumnen Nummer i tabellen Medlemmar.

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.

Orsaken till att join blivit en egen operation är dels att den är så vanlig att det är praktiskt att ha ett enkelt skrivsätt för den, dels att man vill undvika att behöva beräkna kartesiska produkter med kanske miljontals rader, för att sen bara kasta bort de flesta av raderna.

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

11.12 Operationen naturlig join

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

11.13 Att byta namn på relationer och attribut: operationenρ

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

               
                  
                     
                  
                  
                     
                        (Sektionskod,Snamn,Ledare)
                     
                  
                  
                     (Sektioner)l
                  
                  
                     
                  
                  
                     
                        Ledare=Nummer
                     
                  
                  
                     
                  
                  
                     
                        (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

11.14 Aggregatfunktioner

En aggregatfunktion arbetar med alla värdena i en hel kolumn, till exempel för att summera dem, eller för att välja ut det största eller minsta av dem.

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

sum(Lön)

500

Notera två saker:

235

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

count(Lön)

3

F count(Lon) Lon (A)) select count(distinct Lön) from A

count(Lön)

2

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:

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

De standard-aggregatfunktioner som finns i SQL är bland annat sum, count, avg (som ger medelvärdet), min och max.

11.15 Operationen yttre join

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 (illustration), på höger sida för höger-ytter-join (illustration), och på båda sidorna för full yttre join (illustration).

237

11.16 Operationen division, ÷

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.

illustration

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

(Övning: Vilka är det? 5 )

Att ta fram vilka anställda som jobbar på (åtminstone) alla Bengts projekt, är ett exempel på operationen division i relationsalgebran.

Resultatet av JobbarPa÷ BengtsP rojekt:
Anställd
a1
a4

Att dividera en relation R1 (som består av attributen A och B), med en annan relation R2 (som består av attributet B), ger alltså som resultat en relation (som består av attributet A), som innehåller alla de A-värden som i R1 fanns med tillsammans med (åtminstone) alla B-värdena från R2.

11.17 Hierarkier

För att visa några mer komplexa relationsalgebrauttryck, som bland annat använder sig av operationen namnbyte, ska vi studera hierarkier i relationsdatabaser.

Om de anställda i ett företag kan vara chefer för varandra, i flera nivåer, har man ett exempel på en hierarki:

illustration

Ett ER-diagram för en sån hierarki kan se ut så här:

239
illustration

Ö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

eller, steg för steg:

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

Namn

Hulda

Motsvarande SQL-fråga:

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

eller, steg för steg:

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

Namn

Hjalmar

Motsvarande SQL-fråga:

               
                  
                     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'
                  
               
            

11.18 Transitivt hölje

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

11.19 Uppdateringsoperationer

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
                  
               
            

11.20 Sammanfattning av notationen

Operation Symbol
Projektion π
Selektion σ
Namnbyte ρ
Union
Snitt
Division ÷
Tilldelning
Operation Symbol
Kartesisk produkt ×
Join
Vänster-yttre join illustration
Höger-yttre join illustration
Full yttre join 6 illustration
Semijoin illustration

11.21 Övningar

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.

243
illustration

Tabeller, med exempeldata:

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

245

11.22 Litteratur

246

Noter

1 På riktigt bör man nästan aldrig kalla en variabel, i relationsalgebra eller annars, för "temp". Använd hellre ett meningsfullt namn som beskriver vad variabeln innehåller.

2 I den ursprungliga, teoretiska relationsmodellen.

3 I den ursprungliga, teoretiska relationsmodellen, med mängder och inte påsar.

4 Svar: a4.

5 Svar: a1, som jobbar i alla Bengts projekt och dessutom i projekt p4, och a4, som är Bengt själv.

6 Semijoin är en operation som främst används i samband med distribuerade databaser. Läs mer på sidan 612 i kapitel 27.

247

Kapitel 12 Normalformer och normalisering

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.

12.1 Varför ska jag lära mig det här?

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.

248

Men teorin om normalisering behövs ändå:

12.2 Ett förklarande exempel

Vi antar att vi har ett företag som köper varor från olika leverantörer, och vi vill hålla reda på våra data med hjälp av en databas.

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.

12.3 Första försöket att göra en databas

Vi provar med en tabell som vi kallar Inköp:

249
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

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?

12.4 En enkel regel för hur tabeller ska se ut

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.

12.5 Första normalformen, "1NF"

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.

12.6 Funktionellt beroende, "fb"

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:

252

Vi ritar upp attributen, med de funktionella beroendena som pilar:

illustration

I fortsättningen förkortar vi ibland "funktionellt beroende" till "fb".

12.7 Fullständigt funktionellt beroende, "ffb"

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

12.8 Hur vet man vilka funktionella beroenden som finns?

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.

Däremot kan man se vilka ffb som kan finnas, genom att de inte motsägs av de data som finns i tabellen.

illustration

Alltså A → C och B → C.

Exempelvis kan det inte finnas ett ffb A → B, eftersom det för samma värde på A (nämligen 2) förekommer olika värden på B (5 och 6).

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.

12.9 Andra normalformen, "2NF" (ett första, dåligt, försök)

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.

Detta är uppenbarligen en dum design, och mot just den här sortens dumma design hjälper andra normalformen.

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.

I vår exempeltabell bestod ju primärnyckeln av de två attributen Vara och Leverantör, medan det fanns två fullständiga funktionella beroenden från attributet Leverantör:

Dessa strider mot 2NF, så vi delar upp tabellen Inköp och skapar två nya tabeller.

255

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

12.10 Redundans i alla fall?

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:

256
Inkö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

12.11 En bättre definition av 2NF

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:

Inköp
Nummer Vara Leverantör Pris Stad Folkmängd
1 Bilar Volvo 100 000 Torslanda 80 000
2 Bilar Saab 150 000 Södertälje 50 000
3 Lastbilar Saab 400 000 Södertälje 50 000
4 Magnecyl Astra 10 Södertälje 50 000

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

257

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

12.12 Tredje normalformen, "3NF"

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

• Definition av 3NF: 2NF, plus att inget icke-nyckelattribut får vara ffb av något annat icke-nyckelattribut.

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:

258
Städer
Stad Folkmängd
Torslanda 80 000
Södertälje 50 000

Dessa tabeller uppfyller 3NF.

12.13 Hur vet man hur man ska göra uppdelningen?

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:

illustration

Tänk om vi hade delat upp tabellen Leverantörer på sidan 255 så här i stället: 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

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:

259
Stä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.

Regel: Låt också bli att göra ännu dummare uppdelningar.

260

12.14 Boyce-Codds normalform, "BCNF"

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.

• En första, krånglig definition av BCNF: 3NF, plus att inte heller nyckelattribut får vara ffb av icke-nyckelattribut.

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.

261

Tabellen innehåller följande fullständiga funktionella beroenden:

illustration

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

12.15 Tänk på vad tabellen betyder!

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.

Ta en tabell som heter Inköp, och som ser precis likadan ut som den tabell Inköp som vi hade som inledande exempel på sidan 249:

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.

12.16 Ännu fler normalformer

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.

263

12.17 Ibland är det bra att inte normalisera

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.

12.18 Inte bara för relationsmodellen

Normalisering är inte bara möjlig i relationsmodellen, utan även för objektorienterade modeller, eller för poster, eller för entitetstyperna i ett ER-diagram.

264

12.19 De viktigaste begreppen

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.

265

12.20 Övningar

Det finns svar till övningarna på bokens webbplats, men försök lösa uppgifterna själv innan du tittar i facit.

266

12.21 Litteratur

Noter

1 Svar: Det finns tre ffb: {Vara, Leverantör} → Pris, {Vara, Leverantör}Stad och {Vara, Leverantör} → Folkmängd.

2 Join är namnet på en operation som går ut på att databashanteraren kopplar ihop raderna i två tabeller med varandra.

267

Kapitel 13 Integritetsvillkor

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

268

Integritetsvillkor ("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:

13.1 Ett exempel: Anställda och avdelningar

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.

13.2 Nyckelvillkor i SQL

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

(I en del databashanterare måste man ange not null för kolumnen Nummer för att den ska kunna deklareras som primärnyckel, eftersom en nyckel aldrig får innehålla null-värden.)

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:

271

13.3 Referensintegritet i SQL

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

Om tabellen Anställda redan finns, kan man använda kommandot alter table för att lägga till ett referensintegritetsvillkor:

               
                  
                     alter table Anställda
                  
                  
                     add foreign key (JobbarPå)
                  
                  
                     references Avdelningar(Nummer);
                  
               
            

Eller, om man vill ge referensintegritetsvillkoret ett eget namn ("Anställd_till_avdelning"), så att man kan ta bort det sen om man skulle behöva:

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

272

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

273

13.4 Vadå "misslyckas"?

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

Det här betyder att om jag tar bort en avdelning som det finns anställda som jobbar på, sätts JobbarPå för alla dessa anställda till 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

Om jag tar bort en avdelning som det finns anställda som jobbar på, sätts JobbarPå för alla dessa anställda till den kolumnens defaultvärde.

4. foreign key (JobbarPå) references Avdelningar(nummer) on update cascade

Om jag ändrar numret på en avdelning som det finns anställda som jobbar på, ändras också JobbarPå för alla dessa anställda till avdelningens nya nummer.

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å ange no 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

13.5 Ajöss med konsulterna!

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! 
                     
                  
                  
                     
                        */
                     
                  
               
            

Det sista kommandot tar bort städavdelningen, men också de båda konsulterna Sture och Sally som jobbade där.

275

13.6 Mer komplicerade villkor

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.

13.7 Hur anger man dessa mer komplicerade villkor?

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
277

Villkoret att ingen får tjäna mer än sin närmaste chef kan, i databashanterare som klarar kommandot create assertion, skrivas så här:

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

278

13.8 Aktiva databaser

Ett integritetsvillkor hanteras ju av databashanteraren genom att villkoret kontrolleras, och om det inte är uppfyllt, utförs någon typ av åtgärd.

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.

13.9 Vem kollar villkoren? Procedurellt eller deklarativt?

Det finns två olika sätt att ange integritetsvillkoren i en databas: procedurellt och deklarativt.

Hittills i det här avsnittet har vi bara sett den deklarativa metoden: att man med olika former av regler talar om för databashanteraren vad som ska gälla, och sen är det databashanterarens uppgift att se till att inga transaktioner kan ändra data så att integritetsvillkoren bryts.

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.

13.10 Ska man alltid skapa integritetsvillkor?

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

13.11 En möjlig uppdelning i olika typer av integritetsvillkor

Om man vill kan man dela upp integritetsvillkor i tre olika klasser, nämligen inneboende, implicita och explicita:

13.12 Databasens interna integritet

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.

13.13 "Integritet" som i "personlig integritet"

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.

Kom ihåg:

283

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.

13.14 Lagar som berör personlig integritet

Dataskyddsförordningen, ibland kallad GDPR efter dess engelska namn General Data Protection Regulation, är en EU-gemensam förordning som från 25 maj 2018 ersätter den svenska personuppgiftslagen, även kallad PUL.

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.

284

Då går det bra att till exempel publicera listor med namn, adress och personnummer på personer som är dömda för rattfylleri!

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

13.15 De viktigaste begreppen

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.

13.16 Litteratur

286

Noter

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.

2 MySQL äldre än version 4.0.18 gjorde annorlunda. Där betydde no action motsatsen, nämligen att ändringen av data kan genomföras trots att den innebär ett brott mot referensintegriteten.

3 Exemplet följer SQL-standarden, men detaljerna kan variera mellan olika databashanterare.

4 Divisionen med noll är ett knep för att avbryta transaktionen. Det hade förstås varit både enklare och vackrare med kommandot rollback, men SQL-standarden tillåter inte att man använder rollback i en trigger. Det finns dock databashanterare där det fungerar.

5 Man kanske måste kalla in en databaskonsult för att göra alter table i samband med löneförhandlingarna.

6 Den som läser detta år 2037, tjugo år efter att vi skrev det, vet om vi hade rätt.

287

Kapitel 14 Säkerhet i databaser

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.

288

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

Lyckligtvis brukar databashanterare innehålla funktioner för att styra åtkomsten, så att man dels kan säkerställa att en användare verkligen är den som hon utger sig för att vara, och dels reglera exakt vilka delar av databasens innehåll som den användaren får se eller ändra i.

14.1 Databashanteraren skyddar databasen

Ett operativsystem som Windows eller Unix innehåller ofta funktioner för att hantera flera olika användare, och för att styra vilka filer som varje användare får komma åt, och om de i så fall får ändra eller bara läsa i filen.

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.

289

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

Det finns två helt olika varianter av hur man ger användarna rättigheter till en databas: valfria säkerhetsmekanismer (på engelska: discretionary access control) och obligatoriska säkerhetsmekanismer (på engelska: mandatory access control).

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.

290

14.2 Grant och revoke

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.

Några exempel:

               
                  
                     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;
                  
               
            

Som synes kan man ge update-rättighet (och, från och med SQL:1999, select-rättighet) för en enskild kolumn.

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

14.3 Mer finkornig kontroll med vyer

Från och med standarden SQL:1999 kan man ge användare rättighet att hämta data bara ur vissa kolumner i en tabell. Det är dock, som vanligt med SQL-standarden, inte implementerat i alla databashanterare. Man kan inte heller direkt i 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;
                  
               
            

På det här viset kan man alltså styra vilka enskilda rutor i tabellerna som varje enskild användare ska få komma åt.

14.4 Grant-rättigheter med grant option

En finess med grant-kommandot är att man kan ge en användare rätt att i sin tur dela ut rättigheter.

292

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

With grant option betyder att det är "med GRANT-rättighet", vilket innebär att Svante får rätt att i sin tur ge andra användare samma rättigheter som han själv har fått.

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;
                  
               
            

Nu har Svante inte längre rätt att lägga till rader i tabellen, och eftersom Kurt fick den rättigheten från Svante, förvinner även Kurts rätt att lägga till rader!

14.5 Roller

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

14.6 Säkerhet i statistiska databaser

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.

Det här betyder att endast aggregering är tillåten: man kan göra sökningar med aggregatfunktioner som genomsnitt (avg i SQL) eller max, men inte för att få fram enskilda rader.

Det här är inte helt lätt, och det finns flera tekniker för att hindra att information om enskilda ändå kan sökas.

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

Grannens lön kan nu beräknas som skillnaden mellan svaret på den första frågan, som är lönesumman för Sveriges alla Volvo-ägare, och svaret på den andra frågan, som är lönesumman för grannen plus Sveriges alla Volvo-ägare.

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

Såvitt vi vet understöder ännu ingen databashanterare ovanstående begränsningar (förutom den första), så de måste göras i tillämpningsprogrammet.

295

14.7 SQL-injektion

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!

Den som som bygger tillämpningar eller webbgränssnitt mot en databas, och där användare får mata in data, måste tänka på att göra lämpliga kontroller av indata innan de skickas vidare till databashanterarens SQL-tolk.

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.

14.8 De viktigaste begreppen

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.

297

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

14.9 Litteratur

298

Noter

1 Besökt 2017-10-23.

299

Kapitel 15 Lagrade procedurer

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.

15.1 Lagrade procedurer och användardefinierade funktioner

De lagrade procedurerna skrivs i ett språk som är särskilt skapat för att fungera som en utökning av SQL, och inte bara något som man kan stoppa in SQL-satser i.

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.

300

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

15.2 Lagrade procedurer i 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.

15.3 Ett enkelt exempel

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: Om man har lite programmeringsvana känner man igen det här som en procedur. Andra namn på samma sak är subrutin och funktion. I vilket fall som helst är det programsnutt som innehåller några programrader som körs, den ena efter den andra i den ordning de står. Men samtidigt har vi stoppat in en 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.

15.4 Procedurer och funktioner

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

Den här sortens funktioner är alltså inte sådana UDF:er som nämndes tidigare, som skrivs i Java eller något liknande programspråk, utan det är vanliga lagrade procedurer, skrivna i SQL, som returnerar ett värde.

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
En viktig skillnad mellan funktioner och procedurer är att funktioner kan användas i frågor. Procedurer däremot kan inte anropas i 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

Funktionen kvadrat var mycket enkel, och proceduren Utjamning skulle egentligen kunna skrivas med en enda SQL-sats:

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

Låt oss därför titta på ett lite större exempel.

15.5 Transitivt hölje som en lagrad funktion

I avsnittet 8.16 visade vi hur man i flera databashanterare kan beräkna transitiva höljen med konstruktionen 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
Notera att funktionen kommer att gå i oändlig loop om det finns cirkulära beroenden, vilket inte händer om man använder 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.

15.6 Fördelar med lagrade procedurer

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:

306

15.7 Cursors i lagrade procedurer

En 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
                  
               
            

När proceduren skickas till databasservern kompileras den, inbäddade frågor optimeras, och optimal kompilerad kod läggs i servern.

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;
                  
               
            

15.8 De viktigaste begreppen

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.

15.9 Litteratur

Noter

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.

2 Man kan till exempel anropa now() för att ta fram tiden.

309

Kapitel 16 Aktiva databaser och triggers

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.

Men det finns också aktiva databaser (som egentligen borde kallas aktiva databashanterare), där databashanteraren kan utföra egna handlingar, till exempel ändra på data.

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.

16.1 Vad är en trigger?

Man brukar tala om ECA-regler, som innehåller tre delar:

Man brukar skilja på tre sorters händelser: om en eller flera rader läggs till i en viss tabell (med SQL-kommandot 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.

16.2 Triggers i SQL

De triggers som är vanliga i dagens relationsdatabashanterare, och som finns med i SQL-standarden, skapas med SQL-kommandot 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.

312

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.

Kroppen som körs finns mellan begin och end, och utgörs av en helt vanlig insert-sats. Atomic betyder att kroppen är atomär: antingen kommer hela kroppen att köras, eller också inget.

16.3 Materialiserad vy

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.

16.4 Varför triggers?

Nu uppstår problemet att hålla tabellen salsumtable uppdaterad, så att lönesumman som lagras i den stämmer med den riktiga summan av lönerna i tabellen Anställda. Lönesumman i salsumtable måste ändras så fort tabellen Anställda ändras: om en eller flera nya rader läggs till (med SQL-kommandot 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:

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 nyckelordet materialized 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;
                  
               
            

Detta är emellertid inte standard, och om databashanteraren inte har inbyggda materialiserade vyer måste man använda triggers.

16.5 Att hålla den materialiserade vyn uppdaterad med triggers

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
                  
               
            

16.6 Flera rader åt gången

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:

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

318

För att kontrollera att den nya emp_update-triggern fungerar, så sätter vi alla de anställdas löner till 1:

               
                  
                     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.

16.7 Ett annat exempel: Tillåt inga lönesänkningar

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ör update-situationen, om man vill hindra att någon sänker lönen genom att ersätta updatekommandot 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.

16.8 De viktigaste begreppen

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

16.9 Litteratur

Noter

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.

321

Kapitel 17 Objektorienterade och objektrelationella databaser

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.

Inom programmering är det däremot det objektorienterade paradigmet som är populärast i dag, och även inom databasområdet finns det olika typer av objektorienterade datamodeller.

I det här kapitlet går vi igenom de två huvudtyperna av databassystem som kan hantera objekt. De kallas objektorienterade respektive objektrelationella databassystem.

17.1 Varför ska jag lära mig det här?

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.

17.2 Skillnaden mellan objektorienterade och objektrelationella databaser

Databashanterare som arbetar med en datamodell som har inslag av objektorientering brukar delas in i två kategorier: objektorienterade respektive objekt-relationella databaser:

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.

323

Vi använder termen objektdatabaser som ett samlingsnamn för objekt-orienterade och objekt-relationella databaser.

17.3 Objektorienterad datamodellering

illustration

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

324

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

325

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

17.4 Problem med relationsdatabaser

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:

Dessa tillämpningar ställer ett antal krav på databashanteraren som bara delvis uppfylls av traditionella relationsdatabaser:

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

Relationsdatabaser

Affärsverksamhet

Private databaser

Många tillämpningar

OR-databaser

Multimedia

Tidsserier

Mätvärden

Egna sökmetoder

Inga frågor

Filsystem

Video-on-demand

Enkla beräkningar

Objektlager

CAD-system

Komplexa beräkningar

Projekthantering

Enkla data Komplexa data

Objekt-relationella databaser

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:

Vad som kännetecknar dessa tillämpningar är att de kräver lagring i användardefinierade datastrukturer, att man kan lagra datastrukturerna i databasen, samt att kan ställa frågor som undersöker innehållet i strukturerna genom anrop till användardefinierade subrutiner.

17.5 Objektlager

Objektorienterade databaser, så kallade objektlager eller på engelska object stores, var den första ansatsen att lösa vissa av de problem relationsdatabaser hade med vissa tillämpningar, främst cadtillämpningar.

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.

332

På 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 nyckelordet persistent, 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++.

333

17.6 Objektrelationella databaser

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

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.

Objekt-relationella databaser har följande för- och nackdelar jämfört med relationsdatabaser och objektlager:

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.

17.7 De viktigaste begreppen

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.

17.8 Litteratur

Noter

1 http://www.databasteknik.se/boken/

2 Termen impedance mismatch kan översättas med dålig impedansanpassning på svenska, och handlar egentligen om elektrisk impedans i riktiga elektriska kopplingar, till exempel att man kopplar ihop sin stereo på fel sätt så att den går sönder eller låter illa.

3 Ordet man i vetenskapsman betyder människa och inte karl, och är avsett att vara könsneutralt.

4 Det första objektlagret var för programmeringsspråket Smalltalk; numera finns det för både C++ och Java.

5 ObjectStore finns även för Java, varvid man anropar C++-kod från Java.

6 En abstrakt datatyp är inte samma sak som en abstrakt klass, vilket är en klass som inte kan instansieras, utan en abstrakt datatyp är en datatyp med egenskaper (attribut) och beteende (metoder), men med dold implementation.

7 Theta-join beskrivs på sidan 231 i kapitel 11.

337

Kapitel 18 Datalager och datautvinning

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.

18.1 Varför ska jag lära mig det här?

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.

18.2 Driftsdatabaser

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.

18.3 Beslutsstöd

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.

340

18.4 Datalager ("data warehouses")

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

Eftersom man i datalager inte lägger till eller ändrar i databasen, utan mest analyserar de data som samlats ihop, talar man om OnLine Analytical Processing, OLAP, till skillnad från OLTP som är vanlig databasdrift med många små uppdateringar.

18.4.1 Skillnader mellan OLTP och OLAP

OLAP-databaser har följande egenskaper:

341

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

Bland de operationer som användaren av ett datalager kan ägna sig åt, och som datalagret därför ska passa för, finns:

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.

18.4.2 Svårigheter med OLAP i driftsdatabaser

Förutom prestandaproblem finns det ytterligare några nackdelar med att använda driftsdatabaser för avancerad dataanalys:

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.

18.4.3 Överföring av data till datalagret från driftsdatabaserna

De data som sammanställs i ett datalager kommer från en eller flera vanliga, driftsdatabaser, kallade källdatabaser eller källsystem.

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.

18.5 Dimensionsmodellen

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:

345
Fö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

Här ser vi att december är den näst bästa glassmånaden, så då ska vi väl tillverka riktigt många av alla våra glassar?

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

Här ser vi att Julmuststrut är den mest sålda glassen, så den ska vi väl tillverka riktigt många av hela året?

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:

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

Notera hur vi, genom att summera försäljningen på längden och på tvären, har fått samma summor (per glass och per månad) som i de två tidigare tabellerna.

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.

Lägg märke till att de endimensionella uppställningarna med försäljning per månad respektive per glasspinne uppträder som summor i den tvådimensionella uppställningen.

Här är några slutsatser som man kan dra av den tvådimensionella uppställningen, men som inte gick att se i de två endimensionella uppställningarna över månader respektive glassar:

Tydligen har man nytta av den tvådimensionella uppställningen, där både tids- och varudimensionerna kan användas samtidigt.

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:

illustration

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.

I varje cell i kuben finns uppgifter som handlar om just den kombinationen av glass, månad och ort, till exempel hur många glassar av sorten Julmuststrut som såldes i Örebro i januari.

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.

18.6 Datakuber i relationsdatabaser

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

I en relationsdatabas skulle vi snarare skapa en tabell där varje ruta i den tvådimensionella uppställningen motsvaras av en rad:

Försäljning
Månad Glass Antal
januari Gräddpinne 0
januari Hallonpinne 0
januari Julmuststrut 17
... ... ...

(I en verklig lösning skulle kolumnen Glass, och kanske också kolumnen Månad, förmodligen inte innehållit namn, utan i stället varit numeriska referensattribut som refererar till en varutabell respektive en månadstabell.)

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:

350
Fö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.

351

Så här kan man rita upp schemat:

illustration

Bilden med faktatabellen i mitten, omgiven av dimensionstabeller, liknar en sorts stjärna, och därför kallar man den konstruktionen för stjärnschema, stjärnstruktur eller bara stjärna.

18.7 Faktatabellen

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.

Om det finns flera olika fakta i varje cell i datakuben, får man också flera olika faktakolumner i faktatabellen.

18.8 Dimensionstabellerna

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!

Här har vi utvidgat dimensionstabellen Orter med fler kolumner, samtidigt som vi visar ytterligare några rader:

Orter
Id Namn Invånare Kommun Län Länsnamn
1 Gnesta 5082 Gnesta D Södermanland
2 Örebro 95354 Örebro T Örebro
3 Karesuando 327 Kiruna BD Norrbotten
4 Jukkasjärvi 541 Kiruna BD Norrbotten
5 Övre Soppero 230 Kiruna BD Norrbotten
6 Kiruna 19148 Kiruna BD Norrbotten
... ... ... ... ... ...

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.

18.9 Redundans och denormalisering

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

354

18.10 Kuboperatorn

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

Som exempel ska vi återskapa den tvådimensionella glass-månadstabellen på sidan 346, som motsvarar en tvådimensionell datakub, genom att ställa en SQL-fråga utgående från stjärnschemat (med tre dimensioner) på sidan 351.

Vi börjar med en vanlig group 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.

355

Vi 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:
Glass Månad Antal
Gräddpinne januari 0
Gräddpinne februari 0
Gräddpinne mars 2
Gräddpinne april 3
... ... ...
Gräddpinne december 0
Gräddpinne null 71
Hallonpinne januari 0
... ... ...
Hallonpinne december 0
Hallonpinne null 115
... ... ...
Spenatbåt januari 2
... ... ...
Spenatbåt december 2
Spenatbåt null 33
null januari 57
null februari 30
... ... ...
null december 101
null null 609

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.

Tabellen representerar alltså exakt samma information som vår tvådimensionella datakub på sidan 346, inklusive alla summeringar.

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.

18.11 Finkornighet

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!

18.12 Dataförråd ("data marts")

Med datalager brukar man mena de data som hela företaget, eller motsvarande organisation, samlat ihop för att kunna analysera.

357

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!

18.13 Datautvinning ("data mining")

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:

359

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.

18.14 De viktigaste begreppen

18.15 Litteratur

Alla de vanliga databasläroböckerna tar upp datalager och datautvinning i större eller mindre grad och på ett mer eller mindre teoretiskt sätt:

361

En bok som vi rekommenderar för en praktisk och mer lättillgänglig genomgång av dimensionsdatabaser och databasdesign för datautvinning:

Se avsnitt 33.2 på sidan 673 för detaljer om böckerna.

En av dem som haft störst inflytande inom området datalager och datautvinning är Ralph Kimball, och en bra källa till mer information med många konkreta tips är (den numera inte längre verksamma) Kimball Groups webbplats,

www.kimballgroup.com.

362

Noter

1 Det engelska ordet warehouse betyder lagerlokal, inte varuhus.

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

3 I en riktig, geometrisk kub ska alla sidorna vara lika långa, men det bryr vi oss inte om här.

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.

5 Vi använder den syntax som finns i Microsoft SQL Server och i Oracle. Db2, och SQL-standarden, ser lite annorlunda ut.

363

Kapitel 19 Databasbaserade webbplatser

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.

19.1 WWW och statiska webbsidor

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:

364
illustration

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

illustration

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 finns också sätt att skicka data från webbläsaren till webbservern, så att man till exempel kan beställa varor via webben, men det tar vi inte upp här.

19.2 Dynamiska webbsidor och databaser

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.

19.3 CGI

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:

illustration

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.

368

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

Summa

5 528 587

               
                  
                     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.

Fördelar med CGI:

Nackdelar med CGI:

19.4 Utbyggbara webbservrar

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.

19.5 SQL integrerad i webbsidorna

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.

illustration

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.

19.5.1 Ett exempel på skriptspråk i webbsidorna: PHP

Ett mycket populärt teknik för att bygga dynamiska webbsidor är att använda skriptspråket PHP (som står för PHP Hypertext Processor).

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.

19.5.5 Ett exempel på SQL direkt i webbsidorna: Roxen WebServer

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.

19.6 Java-appletar och andra klientprogram

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.

377
illustration

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

19.7 XML

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.

378

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

379
illustration

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

19.8 JSON

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

På liknande sätt som med XML kan man skapa ett JSON-schema som anger hur JSON-koden för en viss tillämpning får se ut, och det finns färdiga bibliotek med funktioner för att skapa och hantera JSON-data.

382

19.9 De viktigaste begreppen

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.

383

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

19.10 Litteratur

384

Noter

1 Roxen är en tidig webbserver som utvecklades 1992 av datorföreningen Lysator i Linköping. Nära Linköping finns sjön Roxen med många gäddor.

2 https://nodejs.org/en/, besökt 2017-12-31.

3 3Roxen WebServer är fri programvara och kan laddas ner gratis från http://www.roxen.com/.

385

Kapitel 20 Tid i databaser

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.

20.1 Vanliga tidsdatatyper

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.

20.2 Snapshot-databaser

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.

20.3 Enkla tidsuppgifter

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

20.4 Tidsserier

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.

20.5 En tidsdimension: giltighetstid

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
388

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

Eller så kan man se det som att vår tvådimensionella snapshottabell har fått en tredje dimension, en tidsdimension, som gör att varje rad i den tvådimensionella tabellen kan bestå av flera rader som ligger "under" varandra:

389
illustration

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.

20.6 Giltighetstid i en vanlig relationsdatabas

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.

En rad med en angiven sluttid, det vill säga med något annat värde än nu i kolumnen GällerTill, innehåller uppgifter som inte gäller längre, och en sån rad kallas för en historisk eller stängd rad.

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:

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

Låt oss nu titta på hur man egentligen gör för att lägga till, ta bort, ändra och söka efter uppgifter i en tabell med giltighetstid.

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.

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.

20.7 En annan tidsdimension: transaktionstid

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.

Transaktionstid handlar alltså, i motsats till giltighetstid, om vad databasen innehöll för data vid olika tidpunkter.

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.

20.8 Transaktionstid i en vanlig relationsdatabas

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 )

20.9 Bi-temporala databaser

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.

20.10 Bi-temporala tabeller i en vanlig relationsdatabas

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

Det står alltså att Olle har telefonnummer 174433 från 7 april och framåt, och att den uppgiften gäller från 7 april och framåt.

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:

396
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
2 Stina 282677 2 juni nu 2 juni tv

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

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
2 Stina 282677 2 juni nu 2 juni tv
3 Cecilia 516323 1 mars 16 mars 4 juni tv

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:

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 14 juni
1 Olle 260088 24 maj nu 30 maj tv
2 Stina 282677 2 juni nu 2 juni tv
3 Cecilia 516323 1 mars 16 mars 4 juni tv
1 Olle 174434 7 april 24 maj 14 juni tv

Eftersom varje rad har en tidsstämpel, nämligen Inlagd-kolumnen, som talar om när den raden lades in i databasen, kan vi gå igenom tabellen och se precis vad som har hänt, och hur data lagts in,

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:

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

20.11 Alternativa implementationer

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.

20.12 Referensintegritet i en temporal databas

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.

398

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

20.13 De viktigaste begreppen

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.

20.14 Litteratur

Noter

1 Svar: Kolumnen Inlagd anger datum och klockslag när raden lades in i tabellen, och där står det att den sista raden, den om Cecilia, lades in först.

401

Kapitel 21 SQL inuti ett program

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.

I det här kapitlet ska vi se några olika sätt att baka in SQL-satser i ett program skrivet i ett procedurellt programmeringsspråk.

402

Det finns flera skäl till att man ibland vill baka in SQL i ett procedurellt språk:

21.1 Olika sätt att använda SQL

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:

21.2 Olika sätt att använda SQL inuti ett program

De tekniker för att använda SQL i kombination med ett procedurellt programspråk som vi ska studera i det här kapitlet är dessa:

404

Det finns också andra tekniker som är värda att nämna, även om vi inte tar upp dem i det här kapitlet:

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.

I en del av exemplen i kapitlet använder vi programspråket C. Det underlättar förstås förståelsen om man kan C, eller C++, men framställningen bygger inte på några C-specifika saker, så det räcker i nödfall med att kunna något annat programspråk, till exempel Java eller Basic.

21.3 Ett exempel på ett API: SQLite i Python

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 exemplet ska vi hämta och skriva ut innehållet i en tabell som heter Personer och som har tre kolumner: Nummer, Namn och Telefon.

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.

Om vi gjort ändringar i databasen, kan vi behöva anropa metoden commit för att de ska lagras permanent:

               
                  
                     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.

Hitta högsta chefen

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:

409
Anstä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()
                     
                  
               

21.4 Ett annat exempel: MySQL i C

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

21.5 JDBC, ett databasoberoende API

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.

21.5.2 Ett JDBC-program

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 med http:// 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.

417

21.5.4 Parametriserade SQL-frågor

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 = ?";
                     
                  
               

Precis som i avsnitt 21.5.3 ovan anropar man prepareStatement för att förbereda frågan och skapa ett PreparedStatement-objekt.

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.

(Det fullständiga programmet finns på bokens webbplats.)

21.6 Skriptprogrammering med T-SQL

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.

21.7 ODBC

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.

Fördelar med ODBC:

21.7.1 Ett ODBC-program

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.

I det här avsnittet visar vi ett ODBC-program som hämtar och skriver ut innehållet i tabellen Personer, med de tre kolumnerna Nummer, Namn och Telefon.

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.

422
illustration

Så 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 */
                     
                  
               

21.7.2 Förberedda SQL-frågor i ODBC

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

21.7.3 Parametriserade SQL-frågor i ODBC

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

21.7.6 Datakällor i Windows

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:

428
illustration

Man kan välja mellan systemdatakällor (System-DSN), som är tillgängliga för alla användare på datorn, och användardatakällor som är personliga för den inloggade användaren.

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

illustration
429

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.

illustration

När vi kör vårt ODBC-program, eller vilket annat program som helst som kopplar upp sig mot en datakälla som heter Persondatabasen, kommer den angivna drivrutinen och den angivna servern att användas.

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.

430

21.7.7 ODBC-arkitekturen

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.

Drivrutinerna kan delas in i tre olika klasser beroende på hur många lager eller skikt som kommunikationen med databasen passerar igenom:

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.

432
illustration

21.8 API:er för andra språk

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

21.9 Cursors

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.

I de exempel vi sett hittills har vi stegat fram cursorn implicit, genom att hämta en rad med MySQL-API:ets mysql_fetch_row eller med ODBC:s SQLFetch, men det finns också cursors som kan stegas båda framåt och bakåt, och där man kan hoppa framåt och bakåt bland raderna.

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.

21.10 Embedded SQL

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.

434

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

ESQL är standardiserat, och fungerar tillsammans med många olika relationsdatabashanterare, men som vanligt finns det ibland små skillnader mellan de olika implementationerna, och vissa saker måste ibland hanteras på olika sätt.

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.

21.10.1 Ett ESQL-program

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

"exec sql whenever sqlerror goto error_exit" är en instruktion till preprocessorn om att den ska generera programkod som gör ett hopp till programläget error_exit ifall det uppstår ett fel.

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.

21.11 De viktigaste begreppen

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.

441

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

21.12 Övningar

443

21.13 Litteratur

444

Noter

1 Av någon anledning är det populärt med skriptspråksnamn som börjar på P.

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.

4 Embedded SQL finns även för andra programspråk, som FORTRAN, Cobol och Ada, även om inte alla databashanterartillverkare har med dem. Embedded SQL finns inte för Java, men för Java finns en liknande teknik som kallas SQLJ.

5 C och C++ är två olika språk. Man ser ofta uttrycket "C/C++", men det finns det egentligen ingenting som heter.

445

Kapitel 22 Index och prestanda

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.

22.1 Ett index är som registret i en bok

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.

22.2 Index i databashanterare

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:

illustration
448

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.

449

22.3 Usch så jobbigt!

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.

22.4 Hur vet man vilka index som behövs?

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

Man kan följa de här stegen:

               
                  
                     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 )

452

22.5 Går det inte snabbt nog i alla fall?

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.

22.6 Överkurs (men nödvändig läsning för den som ska bygga en internet-bank): belastningsanalys

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.

22.7 De viktigaste begreppen

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.

22.8 Litteratur

Här kan man läsa mer om hur man, som databasadministratör, väljer lämpliga lagringsstrukturer, men också om hur databashanteraren arbetar med data internt, och om olika typer av lagringsstrukturer.

Noter

1 Att de är distribuerade betyder att de är fysiskt utspridda på olika datorer.

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?

455

Kapitel 23 Fysiska lagringsstrukturer i databaser

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.

23.1 Varför ska jag lära mig det här?

Tre skäl:

23.2 Grunder om lagringsstrukturer

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.

23.3 Vanliga databaser är diskbaserade

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.

23.4 Poster och block

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:

illustration

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.

458
illustration

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

23.5 Hårddiskar och SSD:er

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:

illustration

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:

illustration

Längs varje spår placeras diskblock:

illustration

Så här breda är de inte i verkligheten, utan vi har överdrivit för att de ska vara stora och tydliga.

461

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.

23.6 Både mekaniska hårddiskar och SSD:er är långsamma

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.

462

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

23.7 Nycklar och sorteringsfält

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.

Om vi bygger ett index som pekar in i filen, och som är baserat på ett visst fält, kallas det fältet för indexeringsfält.

463

23.8 Filoperationer

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.

illustration

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.

Här är några operationer som den hanteraren kan erbjuda, så att de kan användas av de andra delarna i databashanteraren:

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.

23.9 Huvudfilen

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:

466

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

23.10 Osorterade filer

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.

Det kan hända att något av fälten, till exempel det första fältet, är en nyckel, och att det alltså inte kan finnas två rader med samma värde i det fältet, men det påverkar inte sorteringsordningen.

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

En osorterad fil är alltså bra om man ska stoppa in data, utan några kontroller av nyckelvillkor eller andra integritetsvillkor, men den är dålig på allt annat.

23.11 Sorterade filer

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 ta bort en post i filen innebär att man först måste hitta rätt post att ta bort, och sen markera den som borttagen.

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.

23.12 Hashfiler

För en grundläggande förklaring av hur hashtabeller fungerar, se introduktionen till lagringsstrukturer som finns på bokens webbplats.

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.

Block nummer 0 blev överfullt, så vi har allokerat ett nytt diskblock, ett overflow-block eller spillblock, och länkat till det.

illustration

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.

473

23.13 Filer som organiseras efter ett index

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.

23.14 Index

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.

Index kan vara uppbyggda med hjälp av olika datastrukturer, som hashtabeller och olika typer av träd, men oberoende av vilken datastruktur som används kan index delas in i olika klasser beroende på hur de förhåller sig till huvudfilen:

Man talar också om täta och glesa index:

23.15 Sekundärindex

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:

illustration

Att ett index är ett sekundärindex betyder inte att det är det andra index man skapar, eller att det finns ett annat index som är primärindex, utan det betyder bara att huvudfilen inte är sorterad efter indexeringsfältet.

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.

illustration
476

23.16 Primärindex

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:

illustration

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

23.17 Klustrade index

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

477
illustration

Ibland räknas även primärindex, där indexeringsfältet är en nyckel, som klustrade index.

23.18 Hashindex

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
illustration

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.

23.19 B+-träd som index

För en grundläggande förklaring om B-träd, B+-träd och träd i allmänhet, se introduktionen till lagringsstrukturer som finns på bokens webbplats.

479

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

illustration

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.

23.20 B+-träd som primärindex

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:

illustration

23.21 Filer som organiseras efter ett index

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.

23.22 B+-träd som sekundärindex

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
illustration

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

illustration

23.23 Hashtabeller som växer automatiskt

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:

De tre sista metoderna fungerar genom att en position i hashtabellen, vilket i diskbaserade databaser brukar motsvaras av ett diskblock, automatiskt delas upp i två vid lämpliga tillfällen.

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.

23.23.1 Dynamisk hashning

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

illustration

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:

483
illustration

Ett block delas alltså upp när det blir fullt, men om pseudonyckeln "tar slut", dvs om fler poster än vad som får plats i ett datablock hashas till samma pseudonyckel, måste man ändå hantera overflow i tabellen.

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

23.23.3 Linjär hashning

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

illustration

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:

485
illustration

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

illustration

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.

23.24 Primärminnesdatabaser

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.

487

Många konventionella datastrukturer utvecklades med antagandet att det var lika snabbt att komma åt data var som helst i primärminnet, till skillnad mot de lagringstrukturer som utvecklades för diskbaserade databaser där data inlästa i primärminnet är mycket snabbare att komma åt.

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

Exempel på distribuerade primärminnesdatabaser är MySQL NDB Cluster, SAP HANA, och Memory-Optimized Tables i SQL Server, och Memcached.

23.25 Fysiska och logiska index

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.

Här är ett logiskt sekundärindex på namn, tillsammans med huvudfilen som är (fysiskt) organiserad efter nummer.

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

23.26 Bitindex

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.

23.27 Flerdimensionella index

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:

490
illustration

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

491
illustration

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

492
illustration

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

Det finns flera olika sorters index som kan användas för att hitta ett objekt i en tvådimensionell karta, givet objektets x- och ykoordinater:

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.

493
illustration

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

illustration

Notera att ett k-d-träd, precis som ett vanligt binärt träd, får olika utseende beroende på i vilken ordning vi lägger in objekten i trädet.

494

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.

23.28 Räkneexempel med söktider

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

23.28.1 Sökning efter ett visst värde

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

Alltså:

Hashtabeller är snabbast om man vill slå upp en post med ett visst bestämt värde på hashnyckeln, med B+-träd som god tvåa.

23.28.2 Sökning efter ett intervall

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

eller

                  
                     
                        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.

23.29 Komplexitet

Ett viktigt teoretiskt hjälpmedel när man studerar lagringsstrukturer, och algoritmer som arbetar med dem, är komplexitet.

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

23.30 De viktigaste begreppen

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.

500

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

501

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

Klustrat index eller grupperat index (engelska: clustered index) Ett index som är sorterat i samma ordning som huvudfilen.

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.

Extendible hashing Ännu en metod för att låta en hashtabell växa och krympa dynamiskt beroende på hur mycket data den innehåller.

502

Linjär hashning En bättre metod för att låta en hashtabell växa och krympa dynamiskt beroende på hur mycket data den innehåller.

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

23.31 Övningar

Det finns svar till övningarna, och en del diskussioner kring svaren, på bokens webbplats, men försök lösa uppgifterna själv innan du tittar i facit.

23.32 Litteratur

Noter

1 I det här fallet betyder "användare" främst databasadministratören.

2 http://www.databasteknik.se/boken/

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

4 Jämför med ordet stack, som betyder en mer ordnad stapel eller trave.

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.

6 I betydelsen "flaskhals", inte anslutningspunkt för trådlöst datornät.

505

Kapitel 24 Transaktioner

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.

24.1 ACID-transaktioner

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.

507

24.2 Konsistens

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.

24.3 Commit och rollback

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.

24.4 Auto-commit i SQL

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.

509

24.5 Krascher och återstarter

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.

24.6 Loggfilen

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

Eftersom alla ändringar som gjorts i databasen finns beskrivna i loggfilen, kan databashanteraren titta i loggfilen och se vad som behöver ändras tillbaka när en transaktion avbryts.

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.

24.7 Reservkopior ("backup")

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.

När disken sen går sönder sätter man i en ny, fungerande, disk i datorn, kopierar dit databasen från reservkopian, och låter sen databashanteraren gå igenom loggfilen och göra alla de ändringar som finns noterade där, sen den tidpunkt då reservkopian gjordes.

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.

24.8 I:et i ACID: Isolering

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.

Genom att specificera en lägre isoleringsnivå, kan man öka prestanda i databasen genom att transaktionerna kan utföra mer arbete parallellt, men man måste också vara beredd på de fel som kan dyka upp på grund av krockar mellan transaktionerna.

512

Isoleringsnivåerna gås igenom mer i detalj i avsnitt 25.14, när vi hunnit förklara mer om hur samtidiga transaktioner kan påverka varandra.

24.9 De viktigaste begreppen

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

24.10 Litteratur

514

Noter

1 Det engelska ordet commit betyder bland annat att slutgiltigt ta ställning för något.

2 Verbet committa är kanske inte så vackert, språkligt sett, men det är en relativt etablerad term på svenska, och vi har valt att använda den i stället för att hitta på ett eget ord som ingen förstår.

3 Termen dataobjekt har här inget med objektorientering att göra, utan beskriver bara en liten del av databasen, till exempel ett diskblock eller en rad i en tabell, som kan läsas och skrivas av transaktionerna.

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.

515

Kapitel 25 Hur transaktioner hanteras inuti databashanteraren

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.

25.1 ACID-egenskaperna och samtidighet

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.

516

25.2 Transaktionens tillstånd

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.

illustration

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.

När transaktionen arbetat klart, och gjort alla läsningar och skrivningar i databasen som den ska, ska den antingen "committas" eller "rullas tillbaka".

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.

Rapporter till användaren, till exempel om att pengarna hon satt in nu är registrerade hos banken, bör inte ges förrän efter att transaktionen nått sin commit-punkt.

25.3 Buffertar och krascher

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:

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.

Dessutom kan databashanteraren själv vänta med att skriva data, antingen av effektivitetsskäl eller som en del av ett protokoll för hantering av samtidiga transaktioner.

25.4 ACID-egenskaperna och krascher

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.

25.5 Krascher och återstarter

Om systemet kraschar, och sen återstartas, kommer vi att ha en databas med data som återspeglar en blandning av färdiga och halvfärdiga transaktioner:

Den databasen, som förstås kan vara inkonsistent, kallas den materialiserade databasen, och det är återhämtningsprocessens ("recoveryprocessen" på svengelska) uppgift att reda upp i röran.

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.

25.6 Omedelbar och fördröjd uppdatering

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

521

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

Däremot kan man fortfarande inte veta om skrivoperationerna faktiskt hann med att göras, och bli färdiga, före kraschen, och därför måste man gå igenom alla committade transaktioner och upprepa alla deras skrivoperationer.

25.7 Checkpoint

Grunden för återhämtning är alltså att databashanteraren går igenom loggfilen, och behandlar alla transaktioner som finns noterade på den.

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.

Så här skulle en del av loggfilen kunna se ut (även om den förmodligen inte skrivs på det här formatet, som är anpassat för bokens läsare snarare än för databashanterarens interna bruk):

...

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. 628: Commit påbörjad.

T. nr. 629: Skriver Y. Gamla värdet = 8. Nya värdet = 17.

T. nr. 628: Commit klar.

T. nr. 629: Commit påbörjad.

522

T. nr. 403: Commit påbörjad.

T. nr. 403: Commit klar.

T. nr. 629: Commit klar.

Checkpoint.

T. nr. 1: Startar.

...

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.

25.8 C:et i ACID: Konsistensbevarande

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.

523

I vilket fall som helst måste integritetsvillkoren kontrolleras, allra senast i commit-processen, för att garantera att transaktionen överför databasen från ett konsistent tillstånd till ett annat.

25.9 I:et i ACID: Isolering

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.

Vi börjar i avsnitt 25.10 nedan med att beskriva hur transaktionerna arbetar, och hur de läser och skriver i databasen.

I avsnitt 25.11 går vi igenom flera olika problem som kan uppstå om transaktionerna inte är tillräckligt väl isolerade från varandra.

Avsnitt 25.12, 25.13 och 25.14 handlar om vad vi egentligen menar med att transaktionerna är isolerade från varandra, och om hur man kan veta ifall databashanteraren gör rätt eller fel.

I avsnitten därefter kommer vi slutligen att titta på flera olika metoder, eller protokoll, för att åstadkomma isoleringen.

25.10 Transaktioner som läser och skriver

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.

524
illustration

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

Dessutom kan en transaktion ha andra data, som inte motsvaras av objekt i databasen, till exempel variabeln Tmp i transaktion 1.

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

Som ett exempel på en transaktion ser vi här de operationer som behövs för att öka innehållet i variabeln X med 1:

525

Läs(X)

X = X + 1

Skriv(X)

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

När det gäller storleken på de dataobjekt som man läser och skriver (och, som vi ska se senare, låser) brukar man tala om finkornighet eller granularitet, på engelska granularity.

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.

25.11 Vad kan gå fel?

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.

25.11.1 Förlorade uppdateringar

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:

Läs(L)

L = L + 50

Skriv(L)

Ungefär samtidigt kommer Lottas arbetsgivare in på ett annat bankkontor, och betalar Lottas lön:

Läs(L)

L = L + 10 000

Skriv(L)

Eftersom transaktionerna kommer att köras nästan samtidigt, ställer vi upp operationerna bredvid varandra i en tabell:

Transaktion 1 Transaktion 2
Läs(L)
L = L + 50
Skriv(L)
Läs(L)
L = L + 10 000
Skriv(L)

En beskrivning av i vilken ordning operationerna utförs, i synnerhet läs- och skrivoperationerna i den delade databasen, kallas ett tidsschema, eller på engelska schedule.

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:

illustration

Transaktion 1 startar, och läser behållningen på Lottas konto:

528
illustration

Transaktion 1 ökar sin lokala kopia av Lottas behållning med 50 kronor:

illustration

Därefter startar transaktion 2, och läser behållningen på Lottas konto den också:

529
illustration

Transaktion 2 ökar sin lokala kopia av Lottas behållning med 10 000 kronor:

illustration

Transaktion 2 skriver det nya värdet på Lottas konto, 10 100 kronor, i databasen:

530
illustration

Och till sist kommer transaktion 1 och skriver det den tycker är det nya värdet på Lottas konto, 150 kronor, i databasen:

illustration

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

25.12 Seriella och icke-seriella tidsscheman

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.

534

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

25.13 Serialiserbarhet

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.

535

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

25.13.1 Konflikt-ekvivalens och konflikt-serialiserbarhet

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.

Vi vill ha en bättre definition på ekvivalens, och den som man brukar använda är konflikt-ekvivalens.

Två operationer på databasen (läs- eller skriv-) är motstridiga eller (med en försvenskning av det engelska conflicting) konfliktande om de tillhör olika transaktioner, om de arbetar med samma dataobjekt, och om minst en av dem är en skrivoperation.

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.

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

25.14 Olika isoleringsnivåer

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 kommandot set transaction eller i kommandot start transaction.

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:

I beskrivningen av hur databashanteraren gör för att isolera transaktionerna från varandra kommer vi hela tiden att arbeta med serialiserbarhet, alltså isoleringsnivån serializable, om inget annat särskilt anges.

25.15 Hur åstadkommer man isoleringen?

Det finns flera olika metoder för att garantera serialiserbarhet mellan transaktioner:

I de följande avsnitten ska vi gå igenom dessa, hur de fungerar och vilka fördelar och nackdelar de har.

25.16 Kör transaktionerna seriellt

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

25.17 Kontrollera tidsschemat i förväg

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.

25.18 Kontrollera tidsschemat efteråt

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.

541

25.19 Lås

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.

25.19.2 Läs- och skrivlås

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.

Alltså:

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.

I det här exemplet, som innehåller samma transaktioner som det med binära lås på sidan 541, arbetar två transaktioner med dataobjektet X:

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

25.19.3 Tvåfaslåsning

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

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

Lås L

Läs(L)

Tmp = L

L= 0

Skriv(L)

Lås upp L

Lås K

Läs(K)

K =K+ Tmp

Skriv(K)

Lås upp K

Vi lägger till låsningar och upplåsningar på samma sätt även i Kalles transaktion:

Kalles transaktion

Lås K

Läs(K)

Tmp = K

K=0

Skriv(K)

Lås upp K

Lås L

Läs(L)

L=L+ Tmp

Skriv(L)

Lås upp L

När de två transaktionerna körs råkar det bli enligt följande tidsschema:

545
Lottas transaktion Kalles transaktion
Lås L
Läs(L)
Tmp = L
L=0
Skriv(L)
Lås upp L
Lås K
Läs(K)
Tmp = K
K= 0
Skriv(K)
Lås upp K
Lås L
Läs(L)
L= L+ Tmp
Skriv(L)
Lås upp L
Lås K
Läs(K)
K= K+ Tmp
Skriv(K)
Lås upp K

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

illustration

Vi lämnar detaljerna som en övning till läsaren, men efter tidsschemat ser databasen ut så här:

546
illustration

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

547

Ett diagram som visar antalet lås som transaktionen har, och hur det varierar under transaktionens livslängd, ser alltså ut ungefär så här:

illustration

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

25.19.4 Kaskad-rollback

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.

Vi visar med ett exempel, som för enkelhets skull använder binära lås och inte de mer realistiska läs- och skrivlåsen.

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

549
Transaktion 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!

Så kan man inte ha det, och vi måste måste alltså modifiera tvåfaslåsningsprotokollet för att undvika kaskadrollback. Rigorös (på engelska rigorous) tvåfaslåsning behåller alla lås tills den har antingen committat eller avbrutits och rullats tillbaka:

illustration
550

Eftersom rigorös tvåfaslåsning behåller alla lås tills transaktionen är avslutad, kan inga läsningar av ocommittade data ske, och det kan inte uppstå kaskadrollback.

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:

illustration

25.19.5 Deadlock

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.

551

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

illustration

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.

Det är enklare, och vanligare, att databashanteraren i stället har funktioner för att upptäcka att deadlock har uppstått, och då hantera det.

En metod är att använda en enkel time-out, så att när en transaktion stått still och väntat på ett lås tillräckligt länge, antas den ha råkat ut för deadlock, och avbryts.

552

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

553

25.20 Optimistiska metoder

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.

Metoder med lås, där man ser till i förväg att inga kollisioner kan uppstå, genom att man låser dataobjekt innan man börjar arbeta med dem, brukar kallas pessimistiska.

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:

25.20.1 En algoritm för valideringen

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

För varje transaktion, till exempel transaktionen T, behöver vi hålla reda på tidpunkterna för fasernas början och slut: Start(T), Valideringsstart(T), Skrivstart(T) och Avslutad(T).

illustration

Dessutom måste vi känna till mängden av alla dataobjekt som transaktionen uppdaterat, Skrivmängd(T), och mängden av alla dataobjekt som transaktionen läst, Läsmängd(T).

555

När T ska valideras, kontrollerar vi den mot alla andra transaktioner som finns i databasen:

illustration

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.

25.20.2 Optimistisk samtidighetskontroll i Mimer

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.

Transaktion 1 Transaktion 2
create table Apa (Nr integer primary key, Vikt integer);
start transaction;
start transaction;
insert into Apa values (1, 10);
insert into Apa values (2, 20);
insert into Apa values (2, 14);
commit;
commit;
Felmeddelande!
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.)

25.20.3 Skuggsidor

Som vi sett kan man använda en loggfil för att göra rollback, antingen det nu bara är en enda transaktion som avbrutits och ska rullas tillbaka, eller om det är efter en systemkrasch och flera transaktioner avbröts.

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:

558
illustration

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

Om transaktionen skrivit i diskblock 2 och 4, kommer det alltså att ha allokerats två nya diskblock, och de gamla diskblocken finns fortfarande kvar, men orörda:

illustration

Om transaktionen nu måste rullas tillbaka, räcker det att kasta den aktuella katalogen, och kopiera tillbaka skuggkatalogen.

559

25.21 Tidsstämpelmetoden

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.

Transaktionen har nu en tidsstämpel, som den använder för att stämpla varje dataobjekt som den läser eller skriver:

illustration

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.

25.21.1 Tidsstämpelmetoden och serialiserbarhet

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:

illustration

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

561
illustration

Om vi vill att det nya tidsschemat, med sina försenade operationer, ska vara konfliktekvivalent (se avsnitt 25.13.1 på sidan 535) med det ursprungliga seriella schemat, måste vi se upp med motstridiga operationer.

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

25.21.3 Algoritmen

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:

Tidsstämpeln LäsTS(X) ska ange den senaste lästiden, och därför sätter vi LäsTS(X) = TS(T) bara om LäsTS(X) < TS(T).

25.21.4 Kalle, Lotta och tidsstämplarna

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:

564
Lottas 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)

Vi kallar Lottas transaktion T1, och ger den tidsstämpeln TS(T1) = 1, och vi kallar Kalles transaktion T2, och ger den tidsstämpeln TS(T2) = 2.

På sidan 565 visar vi, steg för steg, hur tidsstämpelmetoden kommer att uppdatera såväl dataobjekten som deras tidsstämplar.

Några kommentarer:

565
Tabell 25.1: Exempel på tidsstämpelmetoden
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)
566

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.

25.22 Flerversionsmetoder

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.

Det finns flera olika flerversionsmetoder, och normalt baseras de på en annan metod, till exempel tvåfaslåsning eller tidsstämplar, som man alltså utökat med att dataobjekt kan finnas i flera versioner.

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.

25.23 Långlivade transaktioner

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.

25.24 De viktigaste begreppen

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.

570

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

571

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

572

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

25.25 Litteratur

Noter

1 Exemplet är provkört med MySQL 5.0.2. För att transaktionshanteringen ska fungera måste man ange att tabellen ska vara av typen InnoDB.

2 Bara för att U är nästa bokstav i alfabetet efter T.

3 En annan Thomas.

4 Det engelska ordet "saga", som inte riktigt betyder samma sak som det svenska ordet "saga". Engelskans "saga" betyder "fornnordisk saga" eller "hjältesaga". Den sortens sagor brukar vara ganska långa.

573

Kapitel 26 Frågebearbetning

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.

I det här kapitlet går vi igenom hur databassystemet översätter en SQL-fråga till en sådan exekveringsplan så att den körs så snabbt som möjligt.

574

26.1 Varför ska jag lära mig det här?

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.

26.2 Nyttan av frågeoptimering

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.

575

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

576

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

577

26.3 Frågebearbetningsfaser

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

26.4 Exempel

Som ett mycket enkelt exempel att illustrera betydelsen av frågeoptimering, antag att vi har en relationsdatabas med två tabeller med personnummer pnr som nyckel:

               
                  
                     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.

580

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

26.5 Vyexpansion

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'
                  
               
            

Vad som skett här är att definitionen av vyn Löner har substituerats in och villkoret namn = 'Kalle Persson' på vykolumnen namn har ersatts med motsvarande kolumnvillkor i den tabell som lagrar namn i vydefinitionen.

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'
                  
               
            

Antag frågan:

               
                  
                     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'
                  
               
            

26.6 Klustrade index

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

582

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

583

Om man i figur 26.3 traverserar hela tabellen genom indexet, måste man läsa 4 indexblock som förut, men nu hela 9 radblock.

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

26.7 Kostnadsbaserad optimering

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.

26.7.1 Betydelsen av olika exekveringsplaner

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:

Vad en kostnadsenhet motsvarar i verklig tid beror på vilken hårdvara man använder, och är inte kritiskt för frågeoptimerarens funktion, men med moderna diskar, där en diskläsning tar 5-10 millisekunder, motsvarar en kostnadsenhet alltså 5-10 mikrosekunder.

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:

586

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

587

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

588

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

26.7.2 Heuristiska regler otillräckliga

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.

26.7.3 Joinmetoder

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.

Sort merge join

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)

är:

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

Notera vidare att sort-merge-join är symmetrisk i den meningen att det blir exakt samma kostnad och resultat om operanderna kastas om.

592

Nested loop join

Nested loop join TNLJ 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å?

Hash join

Om tillräckligt minne finns för att hålla alla nycklar i joinvillkoret för ena operanden fungerar hash join, T *HJ U, bäst:

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

595

Kostnad: 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?

26.7.4 Betydelse av sorteringsordning

Som framgick av beskrivningen av sort-merge-join kan systemet behöva lägga in en explicit sorteringsoperator, SORT (T ) för att sortera mellanresultat T . Likaså kan slutresultatet behöva sorteras om SQL-satsen innehåller en 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?

Som ett exempel på att sortering kan löna sig, antag att vi har en tabell över olika försäljares framgång, sorterad på pnr:

                  
                     
                        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.

596

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

597

26.7.5 Funktioner i utvidgad relationsalgebra

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.

Exekveringsplaner för moderna optimerare har bland annat följande utvidgade relationsalgebrafunktioner:

598

26.7.6 Databasstatistik

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 villkoret namn = '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.

För den totala kostnaden att utföra ett givet utvidgat relationsalgebrauttryck måste systemet ta hänsyn till flera faktorer, till exempel:

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.

26.8 Optimeringsmetoders komplexitet

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.

601

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

26.9 De viktigaste begreppen

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.

602

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

26.10 Litteratur

Noter

1 Kallas evaluation plan eller execution plan på engelska.

2 Komplexitet förklaras kort i avsnitt 23.29.

3 Prolog traverserar en trädstrukturerad databas djupet först.

4 Man kallar ett språk som tar argument och ger resultat av samma typ för ett slutet språk.

5 Termen pipelined används också.

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.

7 I praktiken beror denna siffra på nyckel- och pekarlängden.

8 Klustrade index innefattar här alltså även det som i avsnitt 23.14 kallades primärindex.

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.

10 Antalet rader i en tabell kallas tabellens kardinalitet, cardinality på engelska.

11 Eftersom radblocken är länkade klarar man sig emellertid i detta fall med att bara läsa de 5 radblocken.

12 Det är alltså ett primärindex, med terminologin i kapitel 23.

13 För enkelhets skull antar vi att alla block innehåller samma mängd data.

14 En sådan ström av rader kallas ibland på engelska scan.

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.

16 Hela termen sort merge join är på engelska, så det är ingen felaktig särskrivning att skriva den så.

17 Domineras av den kartesiska produkten som producerar N 2 tupler.

18 Domineras av sort merge join.

19 Domineras av trädsökning i indexen.

20 I våra exempel antar vi konstant 100 rader, men i praktiken måste man ta hänsyn till radlängden också.

21 Operatorn '+' betyder här radkonkatenering.

22 1000 i våra exempel.

23 1 i våra exempel.

24 Vilket gör att bara max(T .c), min(T .c) och card(T ) behöver lagras.

603

Kapitel 27 Distribuerade databaser

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.

27.1 Vad är en distribuerad databas?

Man brukar skilja på följande olika typer av system, som alla är relaterade genom att datornät är inblandade:

605

Här är en bild som ska illustrera att data i en distribuerad databas är utspridda på olika ställen, och att det går att komma åt dem via datornätet:

illustration

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.

27.2 Fördelar och nackdelar

Fördelar med distribuerade databaser, jämfört med ett centraliserat system:

Det finns också nackdelar med distribuerade databaser:

27.3 Design av 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.

609

27.4 Fragmentering

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.

illustration

27.5 Korrekt fragmentering

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:

27.5.1 Primär horisontell 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 ← σStad=" Stockholm" (Avdelningar)

Avdelningar2 ← σStad=" Tokyo" (Avdelningar)

Resultat:

Avdelningar1
Anr Anamn Stad
1 Data Stockholm
4 Reklam Stockholm
Avdelningar2
Anr Anamn Stad
2 Städning Tokyo
3 Ekonomi Tokyo
611

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.

27.5.2 Härledd horisontell fragmentering

Primär horisontell fragmentering delar upp tabellen baserat på vilka data som finns på raderna, men härledd (på engelska: derived) horisontell fragmentering delar upp tabellen utgående från en annan tabells horisontella fragmentering.

Antag att det finns en tabell med anställda personer, och de anställda personerna jobbar på avdelningarna i avdelningstabellen:

Personer
Pnr Pnamn JobbarPå
1 Svea 1
2 Sten 3
3 Bengt 1
4 Olle 2
5 Lotta 2
612

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

Om vi har en nod i Stockholm och en i Tokyo, och redan har placerat data om Stockholmsavdelningarna på Stockholmsnoden och data om Tokyo-avdelningarna på Tokyo-noden, kan vi använda den här härledda fragmenteringen för att lagra data om de anställda i Stockholm på Stockholmsnoden och data om de anställda i Tokyo på Tokyo-noden.

Med relationsalgebra kan man skriva det som en join, som slår ihop tabellen Personer med ett av Avdelningar-fragmenten, följt av en projektion som bara behåller de kolumner som kommer från Personer:

Personer 1 ← πPersoner. * (Personer ⋈Avdelningar 1 )

Personer 2 ← πPersoner. *(Personer ⋈ Avdelningar 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.

613

Med semijoin kan vi skriva fragmenteringsschemat för Personer så här:

Personer1 ← Personer ⋉ Avdelningar1

Personer2 ← Personer ⋉ Avdelningar2

27.6 Frågebearbetning i distribuerade databaser: lokalisering och reduktion

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.

Detta, att översätta den globala frågan till en lokal fråga, alltså från en fråga med globala tabeller till en fråga med fragment, kallas lokalisering (engelska: localization).

27.6.3 Lokalisering och reduktion av ett join-uttryck

Som exempel på hur en fråga som innehåller en join-operation kan lokaliseras och reduceras tar vi den här SQL-frågan:

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

618

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

illustration

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

Med lite räknande kan vi visa att även den regel från vanlig algebra som säger att (x + y) * (z + t) kan skrivas om som xz + xt + yz + yt, har en motsvarighet i relationsalgebra:

(R ∪ S)villkor (T ∪ U )

är ekvivalent med

(R villkor T)(Rvillkor U)(Svillkor T)(Svillkor U).

Därför kan vi skriva om det naivt lokaliserade uttrycket i exemplet,

π Pnamn,Anamn [(Personer 1Personer 2)⋈ JobbarPa=Anr (Avdelningar 1Avdelningar 2)]

till

π Pnamn,Anamn [(Personer 1 JobbarPa=Anr Avdelningar 1)∪

(Personer 1JobbarPa=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)

och

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

620
illustration

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

27.7 Frågeoptimering i distribuerade databaser

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.

621

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

27.7.1 Join-ordning

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
                     
                  
               

SQL-frågan kan översättas på flera olika sätt till relationsalgebra (till exempel som två kartesiska produkter följda av en selektion), men om uttrycket ska kunna beräknas på ett effektivt sätt är följande översättning lämplig:

622

A⋈ A.Anr=D.Anr D⋈D.Pnr=P.PnrP

eller, om vi låter joinvillkoren vara underförstådda,

ADP

Vi ritar upp databasen som en bild, tillsammans med de två joinoperationer som ska utföras:

illustration

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:

För att bestämma vilken av strategierna som är effektivast, dvs snabbast, måste man ta hänsyn till:

27.7.2 Frågeexekvering med semijoin

En del join-beräkningar i en distribuerad databas kan effektiviseras genom användning av semijoin.

624

Som vi såg på sidan 612 ovan är semijoin en join som bara ger ena "halvan" av resultatet, nämligen de kolumner i resultatet som hör till den ena av de två joinade tabellerna.

Antag att vi ska joina tabellerna R och S, som befinner sig på två olika noder, med joinvillkoret R.A = S.A:

illustration

Med vanliga join-operationer har vi tre sätt att utföra beräkningen på:

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:

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.

27.8 Transaktioner i distribuerade databaser

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.

Transaktioner ska helst ha de så kallade ACID-egenskaperna (se avsnitt 24.1), till exempel att de ska vara atomära (vilket innebär att antingen ska samtliga operationer i transaktionen utföras, eller inga) och isolerade från varandra (vilket, förenklat uttryckt, innebär att ingen transaktion får se en annan transaktions halvfärdiga ändringar).

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.

27.8.2 Distribuerad tvåfaslåsning

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.

27.8.3 Distribuerade lås för replikerade data

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.

Det finns flera olika strategier för att åstadkomma globala lås, och här ska vi bara beskriva dem kort:

27.8.4 Tvåfas-commit

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 centraliserad databas är commit-processen förhållandevis enkel, för även om det kan uppstå problem till exempel med att strömmen går, så att processen plötsligt avbryts, har databashanteraren kontroll över hela processen.

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

Tvåfas-commit har ingenting med tvåfaslåsning att göra, förutom att båda metoderna förstås använder två faser, och att tvåfaslåsning mycket väl kan användas i den transaktion som sen ska tvåfascommittas.

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.

631
illustration

Koordinatorn väntar tills den antingen fått VOTE-COMMIT-svar från alla de andra noderna, eller tills den får ett VOTE-ROLLBACKsvar.

Om koordinatorn fick ett VOTE-ROLLBACK-svar, skriver koordinatorn ROLLBACK i sin loggfil, och skickar ut ett GLOBALROLLBACK-meddelande till alla noderna.

632

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

När koordinatorn nu alltså antingen beordrat GLOBAL-COMMIT eller GLOBAL-ROLLBACK, ställer den sig och väntar på att få svar från alla de andra noderna om att de uppfattat och genomfört den ordern.

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.

27.9 Parallella databaser

Parallella databaser skiljer sig från distribuerade databaser genom att vara databassystem som körs på ett kluster eller datacenter.

633

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.

Optimering av frågor mot parallella databaser är baserad på kostnadsmodeller där parallellismen är inräknad och den fysiska relationsalgebran inkluderar parallella funktioner för till exempel join och selektion.

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.

27.10 De viktigaste begreppen

Distribuerad databas (engelska: distributed database). En databas som har sina data fysiskt utspridda på flera olika datorer som är sammankopplade med ett datornät.

634

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

635

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

636

27.11 Litteratur

De vanliga tjocka grundböckerna om databaser brukar innehålla en genomgång av distribuerade databaser:

Det finns även mer specialiserad litteratur. Här är en grundbok om distribuerade databaser:

• M. Tamer Özsu, Patrick Valduriez: Principles of Distributed Database Systems.

Se avsnitt 33.2 på sidan 673 för detaljer om böckerna.

Noter

1 https://docs.microsoft.com/en-us/azure/cosmos-db/, besökt 2017-12-30.

2 Shard betyder "skärva" på svenska, som i "krukskärva".

3 Det finns undantag, när det faktiskt kan gå snabbare att hämta data via nätet än från en lokal disk, men det ignorerar vi här.

4 Termen dataobjekthar här inget med objektorientering att göra, utan beskriver bara en liten del av databasen, till exempel ett diskblock eller en rad i en tabell, som kan låsas av transaktionerna.

5 https://cloud.google.com/spanner/ besökt 2017-12-28.

6 CAP-teoremetsäger att om man vill ha mycket hög tillgänglighet i ett distribuerat system så får man ge upp konsistensen.

7 Tänk om någon tar ut pengar innan transaktionens ändringar slagit igenom.

8 ROLLBACK kallas också ABORT.

637

Kapitel 28 NoSQL, NewSQL och molndatabaser

Det här kapitlet ger en introduktion till NoSQL-databaser, som ett alternativ till vanliga relationsdatabaser.

28.1 NoSQL

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:

641

Det finns många olika NoSQL-databashanterare, och de skiljer sig åt på många sätt, men några egenskaper som många delar är dessa:

28.2 NewSQL

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!

28.3 Molndatabaser

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.

28.4 Litteratur

• Elmasri/Navathe, sjunde upplagan: kapitel 24-25.

Det finns även mer specialiserad litteratur. Här är en grundbok om NoSQL-databaser:

• Pramod J. Sadalage & Martin Fowler: NoSQL Distilled: A Brief Guide to the Emerging World of Polyglot Persistence.

644

Noter

1 Det fanns en databashanterare från 1998 som hette just NoSQL, men det var en relationsdatabashanterare som inte använde frågespråket SQL, och det är inte den som man i dag menar med NoSQL.

2 Besökt 2017-12-30.

3 https://neo4j.com/, besökt 2017-12-30.

4 https://redis.io/, besökt 2017-12-30.

5 https://memcached.org/, besökt 2017-12-20.

6 https://hbase.apache.org/, besökt 2017-12-30.

7 https://couchdb.apache.org/, besökt 2017-12-31.

8 https://www.mongodb.com/, besökt 2017-12-30.

9 https://cassandra.apache.org/, besökt 2017-12-31.

10 https://www.couchbase.com/, besökt 2017-12-30.

11 https://hadoop.apache.org/, besökt 2017-12-31.

12 https://spark.apache.org/, besökt 2017-12-31.

13 https://www.vertica.com/ besökt 2017-12-30.

14 http://greenplum.org/ besökt 2107-12-30.

15 https://mariadb.com besökt 2017-12-30.

645

Kapitel 29 Introduktion till MySQL

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.

646

29.1 Om MySQL

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.

29.2 Plus och minus

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 saknade länge en del viktiga funktioner, som nästlade SQL-frågor, vyer, triggers och lagrade procedurer, men dessa har tillkommit i senare versioner och fungerar numera bra.

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.

29.3 Linux-exempel

Man kan ladda ner källkoden till MySQL 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.

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:

illustration

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;
                     
                  
               
            
illustration
               
                  
                     2 rows in set (0,00 sec)
                  
               
            

Försöker man mata in flera rader med samma värde på primärnyckeln, upptäcker förstås MySQL det, och man får ett felmeddelande:

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

Däremot fungerar kontrollen av referensintegritet inte om man an- vänder den alternativa syntaxen för att deklarera främmande nycklar:

               
                  
                     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.

652

Noter

1 Verifierat i MySQL Community Server 5.7.17. Om man skickar in en felrapport om detta till MySQL, svarar de att det står i manualen att det fungerar så. Därför är det inte ett fel, menar de, och de kommer inte att ändra det.

653

Kapitel 30 Introduktion till PostgreSQL

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.

654

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

30.1 Några finesser i PostgreSQL

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:

656

30.2 Linux-exempel

PostgreSQL finns tillgängligt för alla vanliga operativsystem, och en del ovanliga, men här ska vi visa hur man laddar ner och installerar PostgreSQL på Linux, som är ett populärt val för den som behöver en databasserver.

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

Om vi försöker lägga in en rad till med samma värde på primärnyckeln, får vi förstås ett felmeddelande:

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

Det fungerar!

Noter

1 MySQL hade ju problem med det.

659

Kapitel 31 Introduktion till Microsoft SQL Server

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.

660

Den senaste versionen av SQL Server heter, när detta skrivs i oktober 2017, SQL Server 2017. Den finns i flera olika versioner, bland annat:

31.1 Transact-SQL

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:

31.2 SQL Server Management Studio

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.

662

Hä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:

illustration

En och samma server kan hantera flera olika databaser, och en databas, som Thomas-testbasen i exemplet, kan innehålla flera scheman. dbo står för "database owner", och är default-schemat som används om man inte skapar ett eget schema.

Man kan också skriva egna SQL-frågor i redigeringsrutan, inklusive skript, och man kan ha flera olika rutor uppe som man byter mellan.

Det finns också olika sätt att arbeta grafiskt med databasen, till exempel här där man kan se hur tabellen Anställda har en främmande nyckel som refererar till tabellen Avdelningar:

663
illustration

31.3 Vi provkör SQL Server Express

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.

När vi installerat Management Studio kan vi använda den för att koppla upp oss mot databashanteraren och ge SQL-kommandon.

31.4 Litteratur

Flera av de vanliga tjocka grundböckerna om databaser tar upp exempel från SQL Server, särskilt den här som har ett helt kapitel enbart om SQL Server:

Det finns också många specialiserade böcker om SQL Server, till exempel:

Se kapitel 33 för detaljer om böckerna.

665

Kapitel 32 Några databashanterare

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.

666

32.1 Db2

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.

32.2 Firebird

Firebird är en variant av InterBase (se nedan) som släppts som öppen källkod, och som därefter utvecklats separat.

32.3 IMS

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.

32.4 Informix

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.

32.5 Ingres

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.

32.6 InterBase

InterBase är en relationsdatabashanterare som länge ägdes och såldes av företaget Borland, som också är känt för sina kompilatorer och programmeringsomgivningar, men den ägs nu av ett företag som heter Embarcadero Technologies.

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

668

32.7 MariaDB

De flesta versionerna av MySQL är inte längre gratis, och utvecklarna bakom MySQL har gjort ett nytt system, MariaDB, som ska vara kompatibelt med MySQL, men open source.

32.8 Microsoft Access

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.

32.9 Microsoft Jet

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.

32.10 Microsoft SQL Server

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.

669

Till 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

Microsoft SQL Server beskrivs mer ingående i kapitel 31.

32.11 Mimer

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.

32.12 MongoDB

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.

32.13 MySQL

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.

32.14 ObjectStore

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.

670

32.15 Oracle

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.

Det finns olika uppgifter om hur stor marknadsandel Oracle har, men den är mycket populär i Sverige, och har varit det länge.

32.16 PostgreSQL

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.

32.17 SQLite

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.

32.18 Sybase

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.

Noter

1 https://db-engines.com/en/ranking, besökt 2017-12-14.

2 Notera att företaget Oracle, som äger databashanteraren Oracle, även äger MySQL.

3 http://www.databasteknik.se/webbkursen/access/

4 Microsoft SQL Server kom 2016 i en testversion som går att installera på Linux.

671

Kapitel 33 Litteratur och resurser

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.

Listorna nedan är inte ordnade i bokstavsordning efter författarnamnen, utan i någon sorts relevans- och kvalitetsordning.

33.1 Tjocka grundböcker

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:

33.2 Böcker om några mer specialiserade ämnen

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.

33.3 Böcker och handledningar på webben

676

33.4 Andra resurser på webben

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.

677

Sakregister

Förlagsinformation

Kopieringsförbud

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.

Den som bryter mot lagen om upphovsrätt kan åtalas av allmän åklagare och dömas till böter eller fängelse i upp till två år samt bli skyldig att erlägga ersättning till upphovsman eller rättsinnehavare.

Studentlitteratur har både digital och traditionell bokutgivning. Studentlitteraturs trycksaker är miljöanpassade, både när det gäller papper och tryckprocess.

Art.nr 31838

ISBN 978-91-44-06919-7

Upplaga 2:1

© Författarna och Studentlitteratur 2005, 2018

studentlitteratur.se

Studentlitteratur AB, Lund

Omslagslayout: Jesper Sjöstrand

Omslagsbild: Shutterstock.com

Printed by Dimograf, Poland 2018