5. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
1. Introduktion till optimering
1.1. Bakgrund
En dåligt designad databasprodukt kan orsaka fördröjningar för den enskilda användaren
och även påverka andra applikationer som körs på samma dator eller i samma nätverk.
Avsikten med denna föreläsning är att gå igenom de grundläggande detaljerna i
optimering av SQL databaser samt att visa vilka fel och brister databasprogrammerare ofta
gör och hur man kan förebygga dessa. Föreläsningen förutsätter grundläggande kunskap i
SQL och kännedom om relationsdatabasmodellen. De exempel och eventuella
rekommendationer som nämns i föreläsningen är främst SQL optimeringar och är inte
specifika för någon viss databashanterare, om ej annat framgår.
1.2. Finjustering och optimering
Det är möjligt att skilja på begreppen finjustering och optimering i databassammanhang.
Finjustering är vad som görs åt en databas (exempelvis justering av buffertstorlekar,
uppdatering av index) medan optimering är vad som oftast görs i ett program (exempelvis
justering av SQL förfrågningar, effektivering av tillgängliga resurser, omskrivning av
programkod).
Med ordet finjustering avses ofta i databassammanhang en hastighetsökning.
Hastighet kan definieras på två olika sätt:
− Svarstid: Den tid som löper sen dess att klienten skickat en förfrågan till
databashanteraren tills klienten fått svaret.
− Genomströmning: Antalet operationer och förfrågningar databashanteraren hinner
utföra under en viss tidsenhet.
I ett stort månganvändarsystem är det databashanterarens genomströmningshastighet av
förfrågningar som utgör grunden för svarstiden till de enskilda klienterna. En god
förhandsoptimering av alla SQL förfrågningar ökar genomströmningshastigheten i
databashanteraren och minskar samtidigt svarstiden till klienterna.
Oavsett vilken databashanterare som används så kan prestanda alltid höjas genom att alltid
göra de rätta sakerna och utföra de korrekta förfrågningarna.
”In our experience (confirmed by many industry experts) 80% of the performance gains on SQL Server
come from making improvements in SQL code, not from devising crafty configuration adjustments or
tweaking the operating system.”
- Kevin Kline et al., Transact-SQL Programming, O’Reilly & Associates
“Experience shows that 80 to 90 per cent of all tuning is done at the application level, not the database
level.”
- Thomas Kyte, Expert One on One: Oracle, Wrox Press
Sida 5 / 79
6. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Optimering av prestanda är ett brett område. Det omfattar bland annat:
− Att kunna bygga SQL satser utan att göra sånt som är vedertaget känt för att vara
resurskrävande
− Att förstå den fysiska strukturen och tillvägagångssättet i en typisk databas
− Att kunna lösa de verkliga problemen istället för de imaginära problemen.
2. Enkla sökningar och operatorer
2.1. Begränsningar av kapitlet
I detta kapitel berättas hur man kan syntaxoptimera SQL satser. Många
sökningsförfrågningar är det ingen idé att försöka optimera, eftersom det är endast vissa
sökningsförfrågningar som har sådana parametrar som går att optimera. Fastän sökningar
som består av flera tabeller med kopplingar (joins ) och underförfrågningar (subqueries) är
de mest resurskrävande, tas de inte upp i det här kapitlet. För optimering av joins och
subqueries finns det skilda kapitel, här tas endast enkla sökningar i en tabell upp.
Dessutom behandlar detta kapitel endast sökvillkor med WHERE satsen. Satserna
HAVING, IF och ON kommer senare.
Kapitlet börjar med optimeringar som utförs på en mera generell nivå och i slutet av
kapitlet finns specifika optimeringar för de vanligaste SQL kommandon som används.
På grund av portabilitetsproblem så är en del av de lösningar och tips som presenteras i
detta kapitel inte tillgängliga för alla databashanterare. Dessutom kan det finnas små
variationer mellan olika plattformer.
2.2. Allmän optimering av villkor och sökningar
I denna del av kapitlet nämns sådana saker som man bör hålla i minnet när man skriver
enkla sökningar.
De bästa sökningarna är de som påverkar det minsta antalet rader i en tabell och med
sökvillkor som har en låg kostnad. Med låg kostnad avses låg komplexitet, enkelt
utförande och snabb exekvering.
Tabellen nedan visar en typisk rangordning av sökvillkor (tabellen baserar sig på
tillverkarnas egna manualer och är vedertagen känd i databassammanhang). Desto högr e
poäng en operator eller en operand har, desto effektivare är de att använda i sökningar.
Operator Poäng Operand Poäng
= 10 Ensam direkt given operand 10
> 5 Ensam kolumn 5
>= 5 Ensam parameter 5
< 5 Multioperand 3
<= 5 Exakt numerisk datatyp 2
LIKE 3 Annan numerisk datatyp 1
<> 0 Temporär datatyp 1
Textbaserad datatyp 0
NULL 0
Sida 6 / 79
7. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Ur poängtabellen kan man läsa att den bästa och effektivaste sökningen skulle vara
någonting i den här stilen:
... WHERE smallint_column = 12345
Det här exemplet skulle enligt poängtabellen få hela 27 poäng!
Poängen har beräknats enligt följande:
− 5 poäng för att kolumnen (smallint_column) är ensam på vänster sida
− 2 poäng för kolumnens datatyp (exakt numerisk datatyp)
− 10 poäng för likhetsoperatorn (=)
− 10 poäng för den direkta operanden (12345) på höger sida
Här kommer ett annat exempel:
... WHERE char_column >= varchar_column || ‘x’
Det här sökvillkoret ger enligt samma tabell endast 13 poäng.
− 5 poäng för att kolumnen (char_column) är ensam på vänster sida
− 0 poäng för den textbaserade datatypen (CHAR) på vänster sida
− 5 poäng för operatorn större än eller lika med (>=)
− 3 poäng för multioperand-uttrycket (varchar_column || ’x’) på höger sida
− 0 poäng för den textbaserade datatypen (VARCHAR) på höger sida
Den exakta poängen för alla operatorer och operander varierar något mellan olika
tillverkare på databashanterare. Rangordningen är dock den samma för de flesta
tillverkare.
Med hjälp av dessa enkla kunskaper kan man bestämma sig för den mest optimala
ordningen i uttrycken och om det lönar sig att byta ut en del av uttrycket mot något annat
uttryck som gör samma sak. De flesta förfrågningar och sökningar kan skrivas på många
olika sätt, men de får alla olika poäng. En del databashanterare kan själva optimera en
dåligt skriven förfrågan innan den utförs, men om uttrycken är tämligen komplexa så kan
det nog hända att databashanteraren inte klarar av att omforma uttrycket effektivare.
2.3. Transformationer
En SQL förfrågan kan alltså oftast skrivas om på många olika sätt utan att resultatet
förändras. För detta finns det en formell regel som man bör följa:
IF (A <operator> B) IS TRUE AND (B <operator> C) IS TRUE
THEN (A <operator> C) IS TRUE AND NOT (A <operator> C) IS FALSE
Som operator duger en av de följande jämförelseoperatorerna: =, >, >=, <, <=
Dessa operatorer duger inte i transformationslagen: <> och LIKE
Transformationslagen säger alltså att man kan byta ut B till C utan att påverka resultatet.
Om en transformation innebär att en konstant flyttas kallas processen konstantöverföring.
Sida 7 / 79
8. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Följande två exempel ger samma resultat, men det senare exemplet ger en högre poäng
eftersom en kolumn har bytts ut till en konstant.
Uttryck #1
... WHERE column1 < column2
AND column2 = column3
AND column1 = 5
Uttryck #2
... WHERE 5 < column2
AND column2 = column3
AND column1 = 5
Uttryck ett och två är transformationer av varandra och ger således exakt samma resultat,
men uttryck två utför en effektivare och snabbare sökning, i alla fall i några
databashanterare. Som tidigare nämnts i detta kapitel, så kan de flesta databashanterare
göra enkla transformationer som denna. Däremot så försöker inte de flesta
databashanterare utföra transformationer om uttrycket innehåller en eller flera nivåer av
parenteser samt nyckelorder NOT.
Detta exempel kan vara ett långsamt uttryck:
SELECT * FROM Table1
WHERE column1 = 5
AND NOT (column3 = 7 OR column1 = column2)
Genom att tillämpa transformationsprinciperna uppstår detta uttryck:
SELECT * FROM Table1
WHERE column1 = 5
AND column3 <> 7
AND column2 <> 5
I de flesta databashanterare är det transformerade uttrycket betydligt snabbare. Det kan
med andra ord ibland löna sig att själv tillämpa transformationslagen. I vissa fall kan det
hända att konstantöverföringe n av ett flyttal inte medför någon ökning av prestandan i
uttrycket, eftersom det på grund av avrundningar ibland är möjligt att en jämförelse,
beroende på vad man jämför med, blir både större än och lika med samtidigt.
2.4. Datum
En SQL förfrågan som innehåller dagens datum kan göras snabbare genom att inte
använda databashanterarens globala variabel för dagens datum. Jämför förfrågan ett och
två nedan:
Förfrågan #1
SELECT * FROM Table1
WHERE date_column = CURRENT_DATE
AND amount * 5 > 100.00
Sida 8 / 79
9. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Förfrågan #2
SELECT * FROM Table1
WHERE date_column = DATE ’2003-03-26’
AND amount * 5 > 100.00
Denna optimering kräver att förfrågan skrivs om varje dag, och är således endast
användbar om förfrågan genereras dynamiskt av ett applikationsgränssnitt.
2.5. Sammanslagning av flere konstanter
Vid sammanslagning av flere konstanter i samma uttryck kan konstantöverföring uppstå.
Därför är sammanslagning av konstanter en lönsam operation. Det enkla uttrycket
... WHERE a – 3 = 5
skall alltså hellre skrivas som
... WHERE a = 8 /* a – 3 = 5 -> a = 5 + 3 */
för att uppnå en högre poäng.
2.6. Datatyper
I en 32-bitars processor fungerar aritmetiken snabbast om processorn kan jobba med 32-
bitars ord. Därför är datatypen INTEGER (32-bitar brett ord med tecken) den snabbaste
datatypen som kan användas av databashanterare vid jämföringar och aritmetiska uttryck.
Datatyperna SMALLINT, DECIMAL och FLOAT är alltså i de flesta databashanterare
långsammare eftersom databashanteraren inte kan arbeta med full ordlängd på 32-bitar.
2.7. AND
När SQL uttrycket endast innehåller lika med operatorer så kommer de flesta
databashanterare att utvärdera villkoren i den ordning de ges i SQL satsen.
Databashanteraren kommer med andra ord att ställa upp alla villkor i en lista som den
därefter går igenom från vänster till höger. Noteras bör att det inte finns några regler som
säger att så här måste en databashanterare göra – de gör bara på det viset (undantag är
Oracle som utvärderar villkoren från höger till vänster om den kostnadsbaserade
förfrågningsoptimeraren är aktiv). Detta beteende kan utnyttjas genom att först ge det
villkor som har minst sannolikhet att uppfyllas. Om alla villkor har samma sannolikhet
sätts det minst komplexa villkoret först. Om uttrycket som blivit satt längst till vänster är
falskt behövs således inga flera testningar av de övriga villkoren eftersom
databashanteraren vet redan i det här skedet att slutresultatet blir falskt – oavsett om de
övriga villkoren är falska eller sanna.
Exempeluttrycket
... WHERE column1 = ’A’ AND column2 = ’B’
omformas till
... WHERE column2 = ‘B’ AND column1 = ‘A’
om det är mindre sannolikt att column2 är lika med ’B’ än att column1 är lika med ’A’.
Sida 9 / 79
10. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
2.8. OR
När man skriver SQL satser med OR sätts det villkor som har störst sannolikhet att
uppfyllas f rst. Detta är den exakta motsatsen till de givna råden för AND operationen
ö
eftersom OR operationen ger upphov till att flera tester utförs om det första villkoret är
falskt, medan AND orsakar flera tester om första villkoret är sant.
Exempeluttrycket
... WHERE column2 = ’B’ OR column1 = ’A’
omformas till
... WHERE column1 = ‘A’ OR column2 = ‘B’
om det är mera sannolikt att column1 är lika med ’A’ än att column2 är lika med ’B’.
OR operationer är även snabbare om villkoren omfattar så få kolumner som möjligt
eftersom det minskar på databashanterarens behov att slå upp i indextabeller. Därför bör
villkor i en längre SQL sats bestående av flera OR operationer som berör samma kolumn
komma efter varandra. Uttryck ett blir effektivare om det omformas till uttryck två i
nedanstående exempel:
Uttryck #1
... WHERE column1 = 1
OR column2 = 3
OR column1 = 2
Uttryck #2
... WHERE column1 = 1
OR column1 = 2
OR column2 = 3
2.9. AND plus OR
Den distributiva lagen konstaterar följande:
A AND (B OR C) = (A AND B) OR (A AND C)
Antag att en SQL förfrågan behöver utföras som består av både AND och OR villkor.
Förfrågningen är formulerad på sånt sätt att AND operationerna måste utföras före OR
operationen kan utföras:
SELECT * FROM Table1
WHERE (column1 = 1 AND column2 = ’A’)
OR (column1 = 1 AND column2 = ‘B’)
Sida 10 / 79
11. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
SQL förfrågningen görs mot följande tabell:
Rad # column1 column2
1 3 A
2 2 B
3 1 C
När databashanteraren gör sökningar som slås upp i index i den ordningen de nämns i
SQL förfrågningen, kan det hända att den behöver gå igenom alla följande steg:
1. Indexsökning: column1 = 1. Resultat = {rad #3}
2. Indexsökning: column2 = ’A’. Resultat = {rad #1}
3. AND operationen körs för att förena de bägge delresultaten. Resultat = { }
4. Indexsökning: column1 = 1. Resultat = {rad #3}
5. Indexsökning: column2 = ‘B’. Resultat = {rad # 2}
6. AND operationen körs för att förena de bägge delresultaten. Resultat = { }
7. OR operationen körs för att förena resultaten av bägge AND operationerna. Resultat =
{}
Genom att tillämpa den distributiva lagen baklänges erhålls följande SQL förfrågan:
SELECT * FROM Table1
WHERE column1 = 1
AND (column2 = ’A’ OR column2 = ’B’)
När databashanteraren gör indexbaserade sökningar enligt det senare uttrycket kan det
hända att endast följand e steg behövs:
1. Indexsökning: column2 = ’A’. Resultat = {rad #1}
2. Indexsökning: column2 = ’B’. Resultat = {rad #2}
3. OR operationen körs för att förena de bägge delresultaten. Resultat = {rad #1, rad #2}
4. Indexsökning: column1 = 1. Resultat = {rad #3}
5. AND operationen körs för att förena delresultaten. Resultat = { }
Två delmoment kan alltså inbesparas genom att tillämpa den distributiva lagen baklänges.
En del av de vanligaste databashanterarna klarar av att göra det här själva, men det skadar
aldrig att färdigt optimera förfrågningarna.
Den här optimeringen gäller inte om det finns kopplingar med i förfrågningen.
2.10. NOT
Det är alltid bekvämt att skriva om NOT till något mera läsligare och lättförståeligare. Den
mänskliga hjärnan har som tendens att bli råddig om uttrycket innehåller allt för mycket
inverteringar av diverse delresultat. Ett enkelt uttryck kan förenklas genom att t.ex. byta
riktning på jämförelseoperatorn:
... WHERE NOT (column1 > 5)
transformeras enkelt till
... WHERE column1 <= 5
Sida 11 / 79
12. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Ett mera komplicerat uttryck kräver försiktighet vid transformeringen. DeMorgan’s
teorem kan tillämpas för att omforma uttryck med NOT.
DeMorgan’s teorem består av två regler:
NOT (A AND B) = (NOT A) OR (NOT B)
NOT (A OR B) = (NOT A) AND (NOT B)
Om DeMorgan’s teorem tillämpas på följande exempel
... WHERE NOT (column1 > 5 OR column2 = 7)
uppstår följande uttryck:
... WHERE column1 <= 5
AND column2 <> 7
Den uppmärksamme kommer nu ihåg att inte lika med operatorn bör undvikas och i de
flesta fall skulle en sådan här transformering ha en negativ effekt. På längre sikt, i vilken
som helst godtycklig mängd av spridda värden och när det är fler än två rader inblandade,
tar de förluster som uppstår av inte lika med operatorn ändå ut de vinster som erhålls a v
lika med operatorerna. På grund av detta så använder inte vissa databashanterare
indextabeller för att utföra jämförelser med inte lika med operatorn, men däremot
använder de nog indextabeller för att utföra jämförelser med mindre än och större än
operatorerna. Därför lönar sig följande transformation:
... WHERE NOT (bloodtype = ’O’)
Transformeras till:
... WHERE bloodtype < ’O’
OR bloodtype > ’O’
2.11. IN
Många tror att det inte är någon skillnad på dessa två uttryck:
Uttryck #1
... WHERE column1 = 5
OR column1 = 6
Uttryck #2
... WHERE column1 IN (5, 6)
Dessa personer har en aningen fel. Hos några databashanterare är IN snabbare än OR. Det
lönar sig alltså alltid att transformera OR till IN om möjligt. De databashanterare där IN
inte är snabbare än OR kommer ändå att transformera tillbaka uttrycket till OR, så inget
går förlorat på att använda IN.
Sida 12 / 79
13. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
När en IN operator har en större mängd heltal (integers) som parametrar så är det klokare
att fråga ”vad som är ute” än ”vad som är inne”. Därför bör uttrycket:
... WHERE column1 IN (1, 3, 4, 5)
transformeras till det något effektivare
... WHERE column1 BETWEEN 1 AND 5
AND column1 <> 2
2.12. LIKE
De flesta databashanterare använder index för att leta efter ett LIKE mönster om mönstret
börjar med ett konkret tecken, men kommer att undvika index om mönstret börjar med ett
jokertecken (antingen % eller _ ). Till exempel, om förfrågningen har ett sådant här
villkor:
... WHERE column1 LIKE ’C_F%’
kommer databashanteraren att försöka leta efter matchande data genom att första leta efter
alla indexnycklar som börjar med ett ‘C’. Därefter filtreras sådana nycklar bort som inte
har ett ’F’ i position tre. Det finns med andra ord inga optimeringstips för en sådan här
sökning.
I vissa specialfall kan LIKE bytas ut mot en vanlig lika med operator. Exempelvis så är
uttryck ett utbytbart mot uttryck två :
Uttryck #1
... WHERE column1 LIKE ’ABC’
Uttryck #2
... WHERE column1 = ’ABC’
Det finns en liten fallgrop här, de är nämligen inte exakt samma uttryck. I standard SQL
beaktar LIKE operatorn inledande tomrum medan lika med operatorn ignorerar inledande
extra tomrum. Dessutom kan det hända att LIKE och lika med operatorn inte använder sig
av samma teckentabell (collation).
Om en kolumn endast är två eller tre tecken bred kan det vara frestande att använda
SUBSTRING istället för LIKE. Hur som helst, så är SUBSTRING ett klassiskt exempel
på en funktion som inte är effektiv om den får en kolumn som parameter. LIKE operatorn
kommer alltid att klå multipla SUBSTRING funktioner. Uttryck ett skall med andra ord
alltid omformas såsom uttryck två lyder:
Uttryck #1
... WHERE SUBSTRING(column1 FROM 1 FOR 1) = ‘F’
OR SUBSTRING(column1 FROM 2 FOR 1) = ‘F’
OR SUBSTRING(column1 FROM 3 FOR 1) = ‘F’
Uttryck #2
... WHERE column1 LIKE ‘%F%’
Sida 13 / 79
14. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Inom en mycket snar framtid så kommer vissa sökningar som idag görs med LIKE att bli
onödiga, eftersom fulltextindex blir allt vanligare.
2.13. SIMILAR
Operatorn SIMILAR introducerades i SQL3 (SQL:1999). Operatorns syntax och agerande
påminner en aning om grep kommandot i Unix. I vissa fall kan användningen av
SIMILAR istället för LIKE ge en prestandaökning, SIMILAR är i alla fall inte
långsammare än LIKE och därför rekommenderas det ofta att använda SIMILAR istället
för LIKE. Exemplet nedan visar ett fall där SIMILAR avsevärt förbättrar förfrågningens
prestanda.
Uttryck #1
.. WHERE column1 = ’A’
OR column1 = ’B’
OR column1 = ’K’
Uttryck #2
... WHERE column1 SIMILAR TO ’[ABK]’
2.14. UNION
En UNION är i SQL ett sätt att förena två resultat till ett resultat. UNION operatorn tar
hand om att inga dubbla rader uppstår i resultatet även om föreningen består av två
sökningar från samma tabell. På grund av detta så är UNION en populär operator vid
förening av data från flera olika sökningar. Men är operatorn verkligen det bästa sättet att
göra detta på?
Ta följande två exempel:
Uttryck #1
SELECT * FROM Table1
WHERE column1 = 5
UNION
SELECT * FROM Table1
WHERE column2 = 5
Uttryck #2
SELECT DISTINCT * FROM Table1
WHERE column1 = 5
OR column2 = 5
Bägge uttrycken ger upphov till samma slutresultat. Uttryck två kommer i så gott som alla
fall att galant slå uttryck ett. Orsaken till detta varierar något mellan olika
databashanterare, men den ligger alltid i databashanterarnas automatiska
förfrågningsoptimerare.
Ena orsaken är att de flesta optimerare endast klarar av att optimera inom samma WHERE
sats. Operatorn UNION i uttryck ett kommer därför att separera de två WHERE satserna
från varandra, vilket leder till att databashanteraren först skannar column1 för värden som
är lika med fem och därefter skannar column2 för värden som är lika med fem.
Databashanteraren hamnar alltså att utföra dubbla skanningar! (Med skanning avses en
sökning genom hela tabellen, rad för rad.) Därför borde uttryck ett ta dubbelt så lång tid att
Sida 14 / 79
15. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
utföra än uttryck två, om kolumnerna inte är indexerade. Om kolumnerna är indexerade så
klarar de flesta databashanterare av att gottgöra denna prestandaförlust.
Andra orsaken, som delvis talar för UNION operatorns fördel, är att några optimerare
totalt kommer att vägra använda index om WHERE satsen innehåller OR. Detta försämrar
dock inte prestandan mera än vad som vinns på att inte använda UNION.
Rådet lyder alltså: Använd hellre OR istället för UNION, speciellt när kolumnerna inte är
indexerade.
2.15. EXCEPT
Vilket som helst uttryck i stilen A AND NOT B kan omformas till ett uttryck med
EXCEPT, och tvärt om. I likhet med UNION så kommer EXCEPT att ge upphov till
dubbla skanningar av en tabell eller flere. Dessutom är det väldigt få av de stora
databashanterarna som överhuvudtaget stöder EXCEPT, eftersom operatorn har så dålig
prestanda. Uttrycken nedan ger bägge samma resultat, uttryck ett rekommenderas.
Uttryck #1
SELECT * FROM Table1
WHERE column1 = 7
AND NOT column2 = 8
Uttryck #2
SELECT * FROM Table1
WHERE column1 = 7
EXCEPT
SELECT * FROM Table1
WHERE column2 = 8
2.16. CASE
Om en förfrågan innehåller mer än ett långsamt funktionsanrop så kan CASE användas för
att undvika att funktionsanropet händer två eller flera gånger. Uttryck två nedan visar hur
dubbla funktionsanrop kan undvikas med CASE:
Uttryck #1
... WHERE slow_function(column1) = 3
OR slow_function(column1) = 5
Uttryck #2
... WHERE 1 =
CASE slow_function(column1)
WHEN 3 THEN 1
WHEN 5 THEN 1
END
Sida 15 / 79
16. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
2.17. Syntax
Det är även viktigt att hålla en konsekvent stil när man skriver SQL förfrågningar. Ta
följande fyra uttryck som exempel:
SELECT column1*4 FROM Table1 WHERE COLUMN1 = COLUMN2 + 7
SELECT Column1 * 4 FROM Table1 WHERE column1=(column2 + 7)
Istället för att exekvera dessa två ovanstående uttryck bör man istället använda sig av
dessa uttryck:
SELECT column1 * 4 FROM Table1 WHERE column1 = column2 + 7
SELECT column1 * 4 FROM Table1 WHERE column1 = column2 + 7
Fastän alla dessa uttryck ger samma resultat, så kommer ibland det sista uttrycket att köra
snabbare. Detta beror på att vissa databashanterare sparar vad som kan kallas
förkompileringar av tidigare körda förfrågningar i en buffert, ibland även riktiga resultat
om datan ännu inte har ändrats sen den senaste förfrågningen. En grundförutsättning för
att denna buffert skall kunna utnyttjas är att förfrågningen ser exakt likadan ut som den
tidigare, inklusive alla mellanslag och stora och små bokstäver.
Förutom att förfrågningarna blir lättare att läsa och förstå, så kan alltså en konsekvent
skrivstil även bidra till att höja förfrågningens prestanda. Här kommer några tips för en
läsbar och entydig syntax:
− Nyckelord med stora bokstäver men kolumner med små bokstäver
− Tabellnamn har stor första bokstav
− Enkla mellanslag mellan varje ord och runtom varje aritmetisk operator
2.18. Datatypkonvertering (CAST)
Ibland hamnar man ut för att behöva konvertera en datatyp till en annan. Microsoft påstår i
sin online dokumentation över SQL Server 2000 att man aldrig skall använda CAST
funktioner om de verkligen inte är nödvändiga. Dessutom avråder de från att skriva ett
uttryck som har samma kolumn på bägge sidorna om operatorn om den ena sidan
innehåller en datakonvertering med CAST och kolumnen ingår som parameter.
Antag att vi har en kolumn innehållande priser, skapad som DECIMAL(7, 2) (7 positioner
för hela tal och två decimaler), och ett svar sökes på frågan ”Vilka priser är jämna
priser?”.
Nedan finns tre olika lösningar till detta problem:
Uttryck #1
... WHERE MOD(decimal_column, 1) = 0
Uttryck #2
... WHERE CAST(decimal_column AS CHAR(7))
LIKE ’%.00%’
Sida 16 / 79
17. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Uttryck #3
... WHERE decimal_column =
CAST(decimal_column AS INTEGER)
Så vilket uttryck är det bästa? Om den kunskap som tidigare presenterats i detta kapitel
tillämpas kan följande konstateras:
Uttryck ett är definitivt det sämsta alternativet. Fastän det inte finns någon CAST
operation så innehåller uttrycket en underförstådd och oundviklig datakonvertering från
DECIMAL till INTEGER, eftersom MOD operatorn enbart fungerar med INTEGER.
Uttrycket innehåller även en underförstådd division, i det här fallet division med ett.
Uttryck två är det näst bästa uttrycket. Vissa databashanterare lagrar datatypen DECIMAL
fysiskt som CHAR, vilket gör datakonverteringen vä ldigt enkel. Nackdelen är att LIKE
operatorn börjar med ett jokertecken, vilket gör den mindre effektiv.
Uttryck tre är den klara vinnaren, även om Microsoft troligtvis skulle hävda något annat.
Om man jämför alla tre uttrycken och beräknar deras poäng enligt tabellen i stycke 2.2 så
kommer uttryck tre att få den högsta prestandapoängen. Den höga poängen uppnås genom
att uttrycket använder sig av lika med operatorn och har de enklaste operanderna.
3. Sortering av data
3.1. Allmänt om sortering
Sortering av data är en av de vanligaste förekommande funktionerna som behövs i alla
sammanhang. Trots att en mängd olika algoritmer, den ena bättre än den andra, har
utvecklats är sortering än idag en resurs- och tidskrävande process. Man bör med andra
ord verkligen vara på det säkra med att sortering verkligen behövs innan man slänger in en
sorteringssats i SQL. Sortering av stora resultatmängder kräver enorma mängder
minnesutrymme och cpu kraft, vilket leder till att även andra användare av databasservern
känner av sorteringens bieffekter.
Ibland inträffar sortering i databashanteraren utan att man kanske tänker på det eller över
huvudtaget vet om det. SQL funktionerna GROUP BY, DISTINCT, CREATE [UNIQUE]
INDEX och UNION kan kalla på samma sorteringsalgoritmer som används av ORDER
BY. Mera sällsynt är de fall där databashanteraren väljer att sortera två listor när den
behöver utföra en inner join eller outer join, men enstaka fall finns. I dessa fall är
sorteringen endast en bieffekt, men det här kapitlet handlar främst om sorteringen som
händer vid ORDER BY.
3.2. Sorteringseffektivitet
Sortering har varit ett populärt ämne för forskning i flera decennier. Det är inte svårt att
hitta en lämplig sorteringsalgoritm för datan som behöver sorteras. Även
databashanterarna besitter en större repertoar över tillgängliga sorteringsalgoritmer.
Det som påverkar sorteringseffektiviteten i en databashanterare är ganska långt hur
komplext sorteringsvillkoret är och hur bred den data är som skall sorteras. Sist och
slutligen så påverkar den osorterade datans ordning ganska så lite hur snabbt sorteringen
blir klar.
Sida 17 / 79
18. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Det är först och främst tre saker som påverkar sorteringens hastighet. Nedan är de nämnda
i viktighetsordning.
1. Antalet rader sorteringen omfattar
2. Antalet kolumner som nämns i ORDER BY satsen
3. Databredden på de kolumner som nämns i ORDER BY satsen
Det lönar sig alltså att försöka omfatta så få rader som möjligt när man sorterar. Generellt
kan man säga att om antalet rader i sorteringen ökar tiofalt så ökar sorteringstiden
tjugofalt.
En ökning av antalet kolumner i ORDER BY satsen ökar behovet av sorteringstid mera än
vad motsvarande ökning i kolumnens databredd gör. Det lönar sig att slå ihop två
kolumner före sorteringen, om det bara är möjligt, för att undvika två kolumner i ORDER
BY satsen.
Sorteringskolumnens databredd påverkar även sorteringstiden. Om det är möjligt kan man
använda exempelvis SUBSTRING för att minska databredden på det data som skall
sorteras. Det hjälper även om några av de första tecknen är unika sinsemellan eller om det
redan finns någon form av ordning bland den osorterade datan.
Genom att lägga till flera processorer och mera minne i databasservern kan man försnabba
sorteringarna. De flesta sorteringsalgoritmer och databashanterare klarar nämligen av att
utnyttja flera processorer och kan dela upp sorteringen i flera parallella trådar. Om
dessutom centralminnet är tillräckligt stort så att all data ryms i minnet går det ännu
snabbare.
3.3. Sorteringshastighet hos olika datatyper
VARCHAR
Det är alltid den definierade databredd som bestämmer hur lång tid det tar att sortera. En
kolumn med varierbar databredd, såsom VARCHAR, har alltid en definierad bredd och en
aktuell bredd. Om en VARCHAR(30) kolumn innehåller strängen ’ABC’ så är dess
definierade bredd 30 tecken medan den aktuella bredden är tre tecken. Vid sortering så är
det den definierade bredden på 30 tecken som bestämmer sorteringstiden. Detta beror
troligen på att de flesta databashanterare redan på förhand allokerar det minnesutrymme
sorteringen behöver. Minnet allokeras enligt det största möjliga behovet, vilket i det här
fallet kan vara 30 tecken per rad.
SMALLINT
I Windows miljö, där en INTEGER är 32-bitar och en SMALLINT är 16-bitar, kunde man
kanske tro att sortering av 16-bitars ord skulle vara snabbare än sortering av 32-bitars ord.
Eftersom processorns ordstorlek i de flesta fall är 32-bitar, och databashanteraren kan
utnyttja fulla bredden, är det vanligen snabbare att jämföra integers.
Sida 18 / 79
19. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
CHAR
Datatypen CHAR är som bekant 8-bitar. Om man skapar en kolumn som CHAR(4) så blir
den totala bredden 4 x 8 = 32-bitar. INTEGER är också 32-bitar. Går det då lika snabbt att
sortera en CHAR(4) kolumn som en INTEGER kolumn? Tyvärr inte, åtminstone inte i de
flesta fall och hos de flesta databashanterare. Orsaken torde ligga i att en teckensträng inte
kan jämföras 4 byte i gången. Däremot går det bra att jämföra 4 byte i gången när man
jämför med INTEGER.
INTEGER
Den stora vinnaren är alltså datatypen INTEGER. Den sorteras alltid snabbast och passar
perfekt in i 32-bitars arkitektur. När dessutom databashanteraren har sina knep för att
utnyttja full databredd så är datatypen oslagbar i prestanda. Däremot tar datatypen fysiskt
upp mera plats än 16-bitars SMALLINT och 8-bitars CHAR och vill man optimera
databasens fysiska storlek skall man givetvis alltid välja så liten datatyp som möjligt.
3.4. ORDER BY
Syntaxen för en ORDER BY sats är följande:
SELECT <column list>
FROM <Table list>
ORDER BY <column expression> [ASC | DESC] [,...]
I SQL-92 var ORDER BY lite mera begränsad än vad den är idag i SQL:1999. I SQL-92
var man tvungen att ha ett kolumnnamn som dessutom måste ingå i SELECT listan. I
SQL:1999 är det tillåtet att ha ett helt uttryck som operand till ORDER BY. För att
exempelvis sortera nummer i fallande ordning finns det idag två olika alternativ att göra
detta på:
SQL-92
SELECT numeric_column, numeric_column * -1 AS num
FROM Table1
ORDER BY num
SQL:1999
SELECT numeric_column
FROM Table1
ORDER BY numeric_column * -1
Resultatet av ORDER BY numeric_column * -1 kan särskilja sig en aning från
resultatet av ORDER BY numeric_column DESC. Skillnaden beror på om NULL
värden existerar och hur databashanteraren behandlar dessa.
3.5. Att sortera eller att inte sortera
”Do not use ORDER BY if the query has a DISTINCT or GROUP BY on the same set of terms, because
they have the side effect of ordering rows.”
- Kevin Kline et al., Transact-SQL Programming, O’Reilly & Associates
Det är ingen idé att sortera data om den redan är sorterad. Det finns en mängd situationer
där så är fallet, där sådana operationer används vilka har som bieffekt att de sorterar data.
Sida 19 / 79
20. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Åtminstone följande satser returnerar data i sorterad ordning utan att innehålla en ORDER
BY sats:
− SELECT column1 FROM Table1
Returnerar sorterad data om Table1 är grupperad (clustered) och column1 är
grupperingsnyckeln (cluster key) eller om tabellen annars råkar vara färdigt sorterad.
− SELECT column1 FROM Table1 WHERE column1 > -32768
Returnerar sorterad data om column1 är indexerad i stigande ordning och
databashanteraren använder index.
− SELECT DISTINCT column1 FROM Table1
Returnerar sorterad data om column1 inte är UNIQUE.
Om ORDER BY fogas till någon utav dessa SQL förfrågningar blir resultatet endast en
försämring i prestanda. Databashanterarna tar heller inte automatiskt bort onödiga
ORDER BY satser.
3.6. Sorteringsnycklar
Utelämnande av ORDER BY satsen och påtvingande av sortering via teckentabeller och
ordlistor är två olika metoder som kan användas för att snabba upp en sortering. Bägge
alternativen är sådana lösningar som kanske inte stöds av alla databashanterare.
Det finns tre saker man kan göra som försnabbar sortering utan att kräva speciellt stöd från
databashanteraren: sorteringsnycklar, påtvingande av index och försortering.
Om man behöver sortera data enligt exotiska teckenuppsättningar, eller annars bara
behöver en djup sortering, sjunker sorteringshastigheten drastiskt. Bägge dessa problem är
lösbara – addera en kolumn som innehåller sorteringsnycklar till tabellen.
En sorteringsnyckel är en teckensträng bestående av en sekvens av en-bytes nummer.
Sorteringsnyckeln representerar den relativa ordningsföljden mellan tecknen.
Det finns en färdig funktion i MS Windows NT API för att konvertera vilken teckensträng
som helst till en användbar sorteringsnyckel. Funktionen heter LCMapString. Funktionens
indata kan vara vilka bokstäver som helst i någon av de teckenuppsättningar som stöds av
Windows NT och funktionens utdata är en sorteringsnyckel automatiskt viktad enligt de
lokaliserade inställningar som gjorts i systemet.
Sida 20 / 79
21. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Nedan finns ett skript för att skapa sorteringsnycklar för varje rad i en tabell.
locale_id = an ID representing country/collation/etc.
...
DECLARE Cursor1 CURSOR FOR
SELECT character_column FROM Table1;
OPEN Cursor1;
for(;;) {
FETCH Cursor1 INTO :character_string;
if(NO_DATA_FOUND)
break;
LCMapString(locale_id, character_string, sort_key);
UPDATE Table1
SET sort_key_column = :sort_key
WHERE CURRENT OF Cursor1;
}
...
För att skriptet skall fungera bör en kolumn sort_key_column ha skapats på förhand
med förhandsinställd binär sortering. Eftersom kolumnen använder sig utav binär sortering
går det alltså lika snabbt att sortera teckensträngarna som med vilken binär sortering som
helst, vilket är mycket snabbare än någon ordlistbaserad sortering. När tabellen innehåller
en kolumn med sorteringsnycklar kan teckensträngarna hämtas i sorterad ordning med
följande förfrågan:
SELECT * FROM Table1
ORDER BY sort_key_column
3.7. Sortering med index
Sorteringen kan snabbas upp om man kan uppmuntra databashanteraren att utnyttja
eventuella index. Nyckelordet WHERE orsakar i de flesta databashanterare ett försök att
hitta posten i ett index. Uttryck två kan vara snabbare än uttryck ett, om coulumn1 är
indexerad:
Uttryck #1
SELECT * FROM Table1
ORDER BY column1
Uttryck #2
SELECT * FROM Table1
WHERE column1 >= ’’
ORDER BY column1
Sida 21 / 79
22. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
I kapitel 3.5 konstaterades att en sökning med WHERE returnerar resultatet i sorterad
ordning om kolumnen är indexerad. Detta kan dock inte tas för en självklarhet och
ORDER BY kan därför inte lämnas bort. Hur som helst så blir hela sorteringen snabbare
eftersom ORDER BY utförs snabbare om raderna redan är något så när sorterade. Ett par
saker är värt att noteras om detta trick:
− ORDER BY satsen kan inte utelämnas eftersom det inte finns några garantier för att
databashanterare faktiskt använder indexsökningar. WHERE satsen skall närmast ses
som en uppmuntran till användning av index.
− WHERE satsen orsakar att alla NULL värden filtreras bort vilket kanske inte är
ändamålet alltid.
4. Gruppering av data
4.1. GROUP BY
Eftersom GROUP BY kan orsaka extra sorteringar så stämmer en stor del av de saker som
nämndes for ORDER BY i föregående kapitel även för GROUP BY. Det lönar sig med
andra ord att så långt som möjligt undvika GROUP BY och om de används bör de vara
uppbyggda så enkla som möjligt.
GROUP BY kan direkt användas i SQL förfrågningar:
SELECT column1 FROM Table1
GROUP BY column1
Gruppering inträffar ibland även underförstått utan att själva nyckelordet GROUP BY
finns med i klartext. För exempel, HAVING och dataihopsamlingsfunktioner såsom AVG,
COUNT, MAX, MIN, SUM och andra ger upphov till gruppering av data:
SELECT COUNT(*) FROM Table1
HAVING COUNT(*) = 5
Vissa databashanterare stöder extra parametrar och uttryck till GROUP BY och en del kan
ha extra inbyggda funktioner för dataanalys, till exempel STDEV för standardavvikelse.
4.2. Optimal gruppering
Precis som för ORDER BY så blir GROUP BY långsammare för varje extra kolumn som
läggs till i listan över grupperingskolumner. Grupperingslistan kan hållas så kort som
möjligt genom att se till att inga överflödiga kolumner sätts till. Följande exempel visar ett
fall där en överflödig kolumn finns i grupperingslistan:
SELECT secondary_key_column, primary_key_column, COUNT(*)
FROM Table1
GROUP BY secondary_key_column, primary_key_column
Sida 22 / 79
23. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Eftersom kolumnen innehållande primärnyckeln är unik och inte kan innehålla NULL
värden, är secondary_key_column i grupperingslistan överflödig och kunde
uteslutas. Problemet är att om secondary_key_column tas bort ut grupperingslistan
genereras ett felmeddelande. Alla databashanterare (förutom MySQL och Sybase)
kommer att vägra ha secondary_key_column i selectlistan om den inte är med i
grupperingslistan. En lösning, som är snabbare, kunde vara följande:
SELECT MIN(secondary_key_column), primary_key_column,
COUNT(*)
FROM Table1
GROUP BY primary_key_column
GROUP BY har som tendens att resultera i ett färre antal rader och JOIN tenderar att
utöka antalet rader i ett resultat. Den korrekta ordningen vore alltså att första utföra alla
grupperingsfunktioner (GROUP BY) innan föreningsfunktionerna (JOIN) utförs.
Databashanteraren är tvungen att första utvärdera resultaten från FROM och WHERE
innan grupperingsfunktioner kan inledas, vilket gör det svårt att utnyttja detta tänkande.
I vissa fall är det möjligt att påverka ordningsföljden och således optimera förfrågningen.
Om en databashanterare med stöd för mängdfunktioner (UNION, EXCEPT,
INTERSECT) används så kan en SQL förfrågan som består av både GROUP BY och
JOIN skrivas om till två förfrågningar som enbart utför GROUP BY, förenade av
INTERSECT.
Uttryck #1
SELECT SUM(Table1.column2), SUM(Table2.column2)
FROM Table1
INNER JOIN Table2
ON Table1.column1 = Table2.column1
GROUP BY Table1.column1
Uttryck #2
SELECT column1, SUM(column2), 0
FROM Table1
GROUP BY column1
INTERSECT
SELECT column1, 0, SUM(column2)
FROM Table2
GROUP BY column1
Uttryck två utförs snabbare på grund av att ingen JOIN behöver utföras. Dessutom sparar
operationen den mängd arbetsminne som behövs för att utföra förfrågningen. Här finns
dock ett stort portabilitetsproblem. Många utav de stora tillverkarna för databashanterare
stöder inte INTERSECT. Dessa är åtminstone Informix, Ingres, InterBase, Microsoft,
MySQL och Sybase. För dessa databashanterare fungerar alltså inte den optimering som
här presenterades.
När man grupperar resultat som uppstått via en JOIN är det effektivast om
grupperingskolumnen är från samma tabell som kolumnen på vilken en eventuell
mängdfunktion tillämpas på. Det här tipset nämns i en del tillverkares manualer.
Sida 23 / 79
24. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Prestandan kan som bekant ökas genom att undvika JOIN. Eftersom GROUP BY inträffar
vid användning av mängdfunktioner så kan uttryck ett skrivas om till uttryck två, om
Table1.column1 är unik, för att undvika en JOIN.
Uttryck #1
SELECT COUNT(*) FROM Table1, Table2
WHERE Table1.column1 = Table2.column1
Uttryck #2
SELECT COUNT(*) FROM Table2
WHERE Table2.column1
IN (SELECT Table1.column1 FROM Table1)
4.3. HAVING
De flesta databashanterare sammanfogar inte WHERE och HAVING satser. Följande två
uttryck ger exakt samma resultat, men uttryck två är snabbare.
Uttryck #1
SELECT column1 FROM Table1
WHERE column2 = 5
GROUP BY column1
HAVING column1 > 6
Uttryck #2
SELECT column1 FROM Table1
WHERE column2 = 5
AND column1 > 6
GROUP BY column1
Endast sådana fall där jämförelsen av column1 är svår och tidskrävande kan dra nytta av
att utföra jämföringen först i HAVING skedet.
4.4. Alternativ till GROUP BY
Om man skriver en förfrågan som inte involverar mängdfunktioner kan man ofta använda
DISTINCT som ett alternativ till GROUP BY. DISTINCT har tre fördelar framom
GROUP BY:
1. Operatorn är mycket enkel
2. Operatorn kan användas i mera komplicerade uttryck
3. Databashanterare kör ofta DISTINCT snabbare än GROUP BY
Det är effektivare att använda uttryck två istället för uttryck ett:
Uttryck #1
SELECT column1 FROM Table1
GROUP BY column1
Uttryck #2
SELECT DISTINCT column1 FROM Table1
Sida 24 / 79
25. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
4.5. Gruppering och sortering
När en GROUP BY utförs så kommer databashanteraren att behöva leta efter dubbletter av
data. Därför utförs nästan alltid en sortering före grupperingen kan utföras. Tabellen nedan
visar två listor, en osorterad och en sorterad.
Osorterad lista Sorterad lista
Belgrad Belgrad
Sofia Budapest
Budapest Sofia
Antag att en gruppering enligt ”Belgrad” skall utföras i den osorterade tabellen. Hur vet
databashanteraren om det finns dubbletter av ”Belgrad”? Jo, databashanteraren är tvungen
att gå igenom hela tabellen och jämför ”Belgrad” med både ”Sofia” och ”Budapest” innan
den vet om det finns dubbletter. Den kan inte vara säker på att inga dubbletter finns förrän
den har gått igenom hela tabellen, eftersom datan ligger i slumpmässig ordning.
Om man däremot utför en gruppering enligt ”Belgrad” i den sorterade tabellen räcker det
med att databashanteraren jämför ”Belgrad” med nästa rad, eftersom datan ligger i
stigande alfabetisk ordning. Så fort som databashanteraren jämfört [n] med [n + 1] är den
klar. Detta betyder alltså att en gruppering går snabbt om tabellen är sorterad.
Ett annat effektivt sätt att utföra grupperingar är att använda en hash-lista för att snabbt
kunna kontrollera om det finns likadana poster. Informix använder sig (enligt uppgifter)
utav hash tekniken.
Eftersom en GROUP BY föregås av en ORDER BY kan man alltså effektivera
grupperingen genom att ge kolumnerna i samma ordningsföljd både i ORDER BY satsen
och i GROUP BY satsen. Skriv inte en SQL förfrågan på det här viset:
SELECT * FROM Table1
GROUP BY column1, column2
ORDER BY column1
Skriv den istället på det här viset:
SELECT * FROM Table1
GROUP BY column1, column2
ORDER BY column1, column2
Den här optimeringen gäller för databashanterare som inte använder sig utav hash listor.
Har man en sådan hanterare skall man använda sig utav den första varianten, alternativ två
blir långsammare i ett hash-baserat system.
4.6. Gruppering med index
De flesta databashanterare använder index vid datagrupperingar. Som redan tidigare
nämndes, så underlättas en gruppering om tabellen är sorterad. Att kunna utnyttja index är
egentligen samma sak som att ha tabellen sorterad, inte riktigt samma sak, men i alla fall
är beteendet i det här sammanhanget väldigt lika. Här är det även värt att tänka på att
GROUP BY har en lägre prioritet än JOIN och WHERE satser. GROUP BY kommer med
andra ord att utföras efter en eventuell JOIN eller WHERE sats.
Sida 25 / 79
26. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Följande exempel kan inte utnyttja ett index för att snabba upp grupperingen:
SELECT column1 FROM Table1
WHERE column2 = 55
GROUP BY column1
Orsaken till att index inte kan användas av GROUP BY torde vara ganska så klar.
Databashanteraren utför WHERE satsen först och den jämförelsen görs via index över
column2. Efter att detta är gjort fortsätter den med GROUP BY satsen, me n nu är det
ingen idé längre att utnyttja index över column1, eftersom indexet är ett index över hela
tabellen och inte över den data som WHERE satsen filtrerat fram.
På grund av detta fenomen är index endast lämpliga för små och enkla grupperingar.
Lyckligtvis är de flesta grupperingar som görs så pass enkla att ett index kan utnyttjas.
Grupperingar där GROUP BY står ensamt och grupperingskolumnerna är i samma
ordningsföljd som de indexerade kolumnerna i selectlistan kommer att utnyttja ett index.
Följande exempel utför grupperingen snabbare, om column1 har ett index:
SELECT column1 FROM Table1
GROUP BY coumn1
4.7. COUNT
Funktionen COUNT kommer att utnyttja ett index om endast en kolumn (eller *) finns i
selectlistan och om inga flera satser följer FROM satsen. Följande förfrågan utförs
snabbare av vissa databashanterare om tabellen har ett index:
SELECT COUNT(*) FROM Table1
COUNT har dessutom väldigt ofta databasspecifika specialoptimeringar, vilket kan göra
det svårt att försöka tillföra ytterligare optimering.
4.8. SUM och AVG
Index
Även funktionerna SUM och AVG kommer att utnyttja ett index om endast en kolumn
finns med i selectlistan och den kolumnen är indexerad. Tyvärr utförs inte funktionerna
snabbare med hjälp av indexet – tvärtom, det tar ofta längre tid att utföra funktionerna om
de utförs på indexerade kolumner. På längre sikt kan man ändå anse att de förluster som
uppstår är väldigt små i förhållande till de vinster som uppnås via index hos COUNT,
MAX och MIN.
Följande exempel utförs ofta långsammare om column1 är indexerad:
SELECT SUM(column1) FROM Table1
WHERE coumn1 > 5
Sida 26 / 79
27. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Precision
Precision kan även bli ett dilemma om man vill summera en större mängd flyttal av
datatypen FLOAT. Speciellt om följande två fall förekommer kan det bli problem med
precisionen: (a) talens exponenter varierar mycket sinsemellan och (b) när många
subtraktionen inträffar på grund av negativa tal. Förståeliga förluster i precision kan även
inträffa när man lagrar tal som avses vara exakta som FLOAT. Ta till exempel det
”exakta” talet 0.1 och lagra det i en FLOAT kolumn. Addition tio gånger av den kolumnen
behöver nödvändigtvis inte ge slutresultatet 1.0, på grund av precisionsförluster.
Högre flyttalsprecision kan uppnås genom att konvertera talen till en datatyp med högre
precision innan summeringsfunktioner tillämpas, exempelvis till datatypen DECIMAL
eller DOUBLE. Märk väl att det är viktigt att konvertera varje enskilt tal före additionen,
inte själva additionsresultatet. Uttryck ett ger alltså ingen bättre precision, men uttryck två
kan förbättra precisionen hos slutresultatet:
Uttryck #1
SELECT CAST(SUM(column1) AS DECIMAL(10)) FROM Table1
Uttryck #2
SELECT SUM(CAST(column1 AS DECIMAL(10))) FROM Table1
Precisionen blir även en aning bättre om man använder SUM(x + y) istället för SUM(x) +
SUM(y) eftersom det totala antalet additioner blir färre. Däremot blir precisionen bättre
om man använder SUM(x) – SUM(y) istället för SUM(x - y) därför att det totala antalet
subtraktioner blir färre.
Med heltal kan precisionsförluster aldrig uppstå. Däremot kan man i långsökta fall få
problem med begränsningar och spill (overflow) kan uppstå. Om man försöker addera två
celler som bägge innehåller talet 2 miljarder så uppstår spill eftersom summan 4 miljarder
kan inte representeras med 32-bitar. Problemet kan lösas med datatypkonvertering. För
system som stöder datatypen BIGINT kan alternativ ett användas. Vill man hålla sig till
standard SQL skall alternativ två användas.
Alternativ #1
SELECT SUM(CAST(column1 AS BIGINT)) FROM Table1
Alternativ #2
SELECT SUM(CAST(column1 AS DECIMAL(10))) FROM Table1
4.9. MAX och MIN
Funktionerna MAX och MIN kommer att utnyttja ett index om funktionen är ensam i
selectlistan och inga andra satser följer efter FROM satsen. Om ett index existerar blir
dessa två funktioner väldigt enkla och snabba att utföra. Funktionen MIN kan direkt läsa
det minimala värdet från det första indexvärdet och funktionen MAX får direkt det
maximala värdet genom att avläsa det sista indexet (eller tvärtom i ett fallande index, som
i IBM).
Databashanterarna löser följande problem väldigt lätt om tabellen har ett index:
SELECT MIN(column1) FROM Table1
Sida 27 / 79
28. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Men följande exempel är en betydligt svårare nöt att knäcka och tar betydligt längre tid att
utföra:
SELECT MIN(column1), MAX(column1) FROM Table1
Villkoret för indexanvändning var ju att funktionen skulle vara ensam i selectlistan, därför
tar detta exempel väsentligt mera tid på sig att utföras. I sådana här situationer är det bättre
att skriva förfrågningen som två skilda SELECT satser, eller förena dem med en UNION.
Följande exempel rekommenderas:
SELECT MIN(column1) FROM Table1
SELECT MAX(column1) FROM Table1
5. Joins (Kopplingar, Föreningar)
5.1. Bakgrundsinformation om föreningar av tabeller
En gång i tiden fanns det en enkel regel för joins – indexera alla kolumner och föreningar
av tabeller kommer att gå snabbt. Medan databashanterarna har utvecklats och blivit mera
sofistikerade har flera olika strategier för olika typer av föreningar tagits fram. De flesta
databashanterare innehar en ganska stor repertoar utav dessa och optimering av föreningar
är idag mera raffinerande än förut.
Det första man bör hålla i färskt minne när man planerar förfrågningar som förenar flera
tabeller är att man inte vill utföra föreningen själv. Vad man däremot vill göra är att ge
databashanteraren de bästa förutsättningarna för att själv kunna välja den mest lämpliga
och effektivaste föreningsplanen, i slutändan kommer detta att påverka effektiviteten en
hel del.
I den förenklade modellen av en förening av två tabeller blir resultattabellens storlek
produkten av de tabeller som förenades. Den förenar alltså alla rader i ena tabellen med
alla rader i den andra tabellen. Om Tabell1 innehåller värdena {A, B} och Tabell2
innehåller {C, D} så blir produkten { (A, C) (A, D) (B, C) (B, D) }. Realisten märker att
en del av dessa rader troligtvis kommer att filtreras bort av ett villkor, som dessutom körs
först efter att tabellerna multiplicerats. Det väsentliga i detta exempel är att visa på den
enorma mängden med tillfälligt minne som behövs för att lagra den temporära tabellen.
Det behövs alltså någon bättre algoritm för att utföra en förening i praktiken.
Valet av vilken algoritm och plan som väljs beror på om tabellen har index, tabellens
storlek samt på indexets selektivitet. Selektivitet är ett mått på olikheten mellan unika
värden i ett index.
Sida 28 / 79
29. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
5.2. Nested-Loop Joins
”Nested-Loop joins” kan översättas till ”förening med inbäddade upprepningar”, eller nåt i
den stilen. Principen är två eller flera upprepningar inuti varandra där varje upprepning går
igenom en specifik tabell. Alla algoritmer enligt ”Nested-Loop joins” principen baserar sig
på någon variant av pseudokoden nedan:
for (each row in Table1) {
for (each row in Table2) {
if (Table1 join column matches Table2 join column) pass
else fail
}
}
− Ordet “matches” betyder vanligtvis “lika med” eftersom majoriteten av alla föreningar
är “lika- med-föreningar”, alltså sådana som använder lika med operatorn. Ett känt
namn på dessa i det engelska språket är ”equijoins”.
− Ordet ”pass” syftar på att raderna från bägge tabellerna skall tillfogas den temporära
resultattabellen. Vanligtvis så sparar databashanterarna endast det ID som förknippas
med raderna.
Redan den här algoritmen är en stor förbättring jämfört med det grundscenario som
beskrevs i introduktionen till detta kapitel. Nu behöver den temporära tabellen inte vara
fullt så stor som produkten av bägge tabellerna. Men fortfarande så behövs ett gigantiskt
antal jämförelser.
Jämförelser är dock snabba operationer, speciellt om bägge talen som skall jämföras ligger
i databashanterarens buffertminne. Genom att utöka den algoritm som nyss presenterades
så att den tar sig an en sida i gången från en tabell kan prestandan höjas betydligt.
Sidstorleken bestäms i databasserverns konfiguration, men typiska värden är 4 KB, 8 KB
eller 16 KB. Genom att öka denna buffert så kan större sidor rymmas i buffertminnet och
algoritmen körs snabbare.
for (each page in Table1) {
for (each page in Table2) {
for (each row in Table1-page) {
for (each row in Table2-page) {
if (join column matches) pass
else fail
}
}
}
}
Denna algoritm utnyttjar buffertminnet genom att dela upp tabellerna i sidor och behandla
en sida från varje tabell i gången. Om dessutom de kolumner som jämförs har index så
behövs ingen skanning av hela sidan, det räcker med att slå upp värdet ur indexet. Den
innersta upprepningen bör bestå av den enklare, alternativt den mindre, tabellen eller
åtminstone ha ett index för att algoritmen skall bli ännu effektivare.
Sida 29 / 79
30. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Oftast finns det ett villkor med i spelet när man förenar tabeller med varandra, förutom det
villkor som definierar föreningen. Den tabell som har ett filtreringsvillkor bör placeras
som den yttre tabellen i upprepningen så att det totala antalet upprepningar av den inre
upprepningen minskas, annars upprepas filtreringsvillkoret om och om igen på tok för
många gånger.
Databashanteraren väljer vilken tabell som blir den yttre och vilken som blir den inre på
basen av följande punkter:
− Den minsta tabellen skall vara i den inre upprepningen, speciellt om den är mycket
liten.
− Tabellen med det bästa indexet skall vara i den inre upprepningen.
− Den tabell som har ett filtreringsvillkor skall vara i den yttre upprepningen.
De här punkterna bör man tänka på när man skriver förfrågningar som förenar tabeller.
Motverka inte databashanterarens vilja genom att placera ett filtreringsvillkor på fel tabell!
Om tabell ett är mindre och har ett bättre index, placera då ett eventuellt filtreringsvillkor
på tabell två istället – tabell två passar ju ändå bättre i den yttre upprepningen.
Så här långt har allt handlat om enkla föreningar av typen Table1.column1 =
Table2.column1. Förståss kan det bli lite mera komplicerat än så här. Det kan till
exempel finns två föreningsvillkor som i följande exempel:
SELECT * FROM Table1, Table2
WHERE Table1.column1 = Table2.column1
AND Table1.column2 = Table2.column2
I det här fallet räcker det inte med att försäkra att column1 och column2 i den inre
upprepningen är indexerade. Om ett sammansatt index skapas för column1 och
column2 i den inre upprepningen, istället för två individuella index, sker den totala
föreningen betydligt snabbare! Det är dessutom mycket viktigt att bägge kolumnerna som
jämförs i den inre upprepningen har exakt samma datatyp och exakt samma storlek –
annars kan det hända att databashanteraren inte alls utnyttjar indexet.
Sida 30 / 79
31. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
5.3. Sort-Merge Joins
En ”Sort-Merge Join” är en aning mera komplicerad än en ”Nested-Loop Join”, men den
går fortfarande att beskriva med ett par rader pseudokod:
sort (Table1)
sort (Table2)
get first row (Table1)
get first row (Table2)
for (;;until no more rows in tables) {
if(join-column in Table1 < join-column in Table2)
get next row (Table1)
elseif (join-column in Table1 > join-column in Table2)
get next row (Table2)
elseif (join-column in Table1 = join-column in Table2) {
pass
get next row (Table1)
get next row (Table2)
}
}
Algoritmen börjar med att sortera de bägge tabellerna som skall förenas. Skedet kallas
”sorteringsskedet”, därifrån kommer den första halvan i namnet ”Sort-Merge”. Andra
halvan kommer givetvis från nästa skede. I föreningsskedet (”Merge” skedet) av en “Sort-
Merge Join” går databashanteraren alltid framåt i bägge tabellerna. Den behöver aldrig gå
igenom en viss rad i en tabell mer än en gång – en stor förbättring jämfört med ”Nested-
Loop Join”.
Nackdelen är den sortering som krävs innan den fina algoritmen kan uträtta sitt arbete.
Effektiva sorteringsalgoritmer borde ändå vara betydligt snabbare än jämförelser av allt
mot allt (som är fallet i ”Nested-Loop Joins”). Det är bevisbart att ”Sort-Merge Join” är
betydligt snabbare än ”Nested- Loop Join” om bägge tabellerna är väldigt stora.
Trots alla fördelar så väljer databashanterarna väldigt ofta att hellre använda ”Nested-
Loop Joins” än ”Sort-Merge Joins” därför att de kräver mindre mängd minne, är flexiblare
och kräver inga extra tunga databehandlingar i startögonblicket. Följande exempel visar en
idealisk situation för att utföra en ”Sort-Merge Join”:
SELECT * FROM Table1, Table2
WHERE Table1.column1 = Table2.column1
Det som gör denna förening ideal för ”Sort-Merge” är lika med operatorn och avsaknaden
av filtreringsvillkor. En typisk SQL förfrågan för att generera en rapport. Om dessutom
Table1 och Table2 är sorterade på förhand enligt samma nyckel så är det den perfekta
situationen för en ”Sort-Merge”.
Sida 31 / 79
32. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Användningen av ”Sort-Merge Joins” kan innebära lite extra jobb för
databasadministratorn. Hos vissa databashanterare måste nämligen ”Sort-Merge Joins”
aktiveras manuellt. Dessutom kan det vara en god idé att se över storleken på serverns
centralminne om man tänkt använda sig utav den här föreningstekniken.
5.4. Hash Joins
En ”Hash-Join” är ännu en algoritm för att skapa en förenad tabell. Processen att förena
Table1 och Table2 går enligt följande modell:
− Beräkna ett hash värde för varje rad i Table1. Spara hash värdet i en tillfällig tabell i
minnet.
− Beräkna ett hash värde för varje rad i Table2. Kontrollera om hash värdet redan
finns i den tillfälliga tabellen i minnet. Om det finns, så finns det en koppling. Om det
inte finns, så finns det ingen koppling.
En ”Hash-Join” är med andra ord en variant av en ”Nested-Loop Join” vars inre tabells
rader är uppslagna via hash värden istället för via index. De flesta databashanterare
upprätthåller inga statiska tabeller med hash värden, så en temporär tabell med hash
värden för Table1 behöver skapas innan föreningen kan inledas. Hash tabellen kan
frigöras när den förenade tabellen är klar.
Följande fyra krav måste alla vara uppfyllda för att en ”Hash-Join” skall fungera vettigt:
1. Det finns tillräckligt med minne reserverat för den temporära tabellen innehållande
alla hash värden. Inställningen är oftast konfigurerbar.
2. Föreningen är en ”equijoin” (den använder lika med operatorn).
3. Värdena i den inre tabellens föreningskolumn är unika och producerar unika hash
värden.
4. Den inre tabellen genomsöks många gånger eftersom den yttre tabellen är betydligt
större och innehåller många rader som inte filtrerats ut utav något filtreringsvillkor.
Krav nummer ett kan kompenseras för genom att dela upp den yttre tabellen i flera bitar
och beräkna hash värden för varje bit i gången. Därefter utförs föreningensmomentet och
när det är klart beräknas nya hash värden för nästa bit av den yttre tabellen o.s.v. Detta kan
ge upphov till att den inre tabellen skannas många gånger, men det är i alla fall bättre än
att ha en gigantisk hash tabell för den yttre tabellen, som dessutom kanske mappas ner på
hårddisken om den inte ryms i centralminnet.
5.5. Undvikning av Joins
En förening av två tabeller kan ibland undvikas genom att transformera en SQL förfrågan
till en enklare, utan förening, men ändå likvärdig. Följande exempel utnyttjar en av
grundprinciperna i SQL optimering, spridning av konstanta värden. Uttryck ett kräver en
förening av Table1 och Table2 medan uttryck två inte behöver någon förening. Ett
krav är dock att bägge kolumnerna är indexerade.
Uttryck #1
SELECT * FROM Table1, Table2
WHERE Table1.column1 = Table2.column1
AND Table1.column1 = 55
Sida 32 / 79
33. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Uttryck #2
SELECT * FROM Table1, Table2
WHERE Table1.column1 = 55
AND Table2.column1 = 55
En sådan här situation uppstår alltid om lika med operatorn utgör filtreringsvillkoret och
samma kolumn även ingår i föreningsvillkoret. Det är trots allt en väldigt liten del av alla
föreningar som kan omformas till icke- föreningar. För att uttryck två över huvudtaget
skall vara snabbare än uttryck ett krävs det att bägge tabellerna har index över column1.
5.6. Gemensamma index för Joins
En del databashanterare har stöd för något som kallas för ”Join Indexes”, bland annat
Microsoft och Oracle stöder dem. Principen är att ha flera tabellers index i samma fysiska
index.
Index 1 Tabell 1 Index 2 Tabell 2
A Rad 1.7 B Rad 14.53
B Rad 6.3 C Rad 16.02
C Rad 8.8 F Rad 19.08
Bilderna ovan visar två vanliga index för Tabell ett och Tabell två.
Bilden nedan visar ett gemensamt index (”Join Index”) för både Tabell ett och Tabell två:
Index Tabell 1 Tabell 2
A Rad 1.7
B Rad 6.3
B Rad 8.8
C Rad 14.53
C Rad 16.02
F Rad 19.08
Antag att följande SQL förfrågan körs, med tillgång till ett gemensamt index för Tabell ett
och två:
SELECT * FROM Table1, Table2
WHERE Table1.column1 = Table2.column1
Sida 33 / 79
34. Finjustering och optimering av SQL databaser 02.04.2003
Krister Karlström
Med tillgång till ett gemensamt index är uppgiften mer eller mindre trivial.
Databashanteraren behöver endast skanna igenom den gemensamma indextabellen och för
varje värde som pekar på en rad i tabell ett, kontrollera om nästa indexvärde är exakt
likadant men pekar på en rad i tabell två. Om så är fallet har den hittat en koppling. Det
här är faktiskt en ”Sort-Merge Join”, där redan både sorteringen och föreningen har utförts
på förhand!
Gemensamma index kan vara mycket användbara och kan underlätta föreningar av
tabeller avsevärt, men det finns alltid en risk för att det blir råddigt när man börjar blanda
ihop data från flera tabeller i samma fil. Om dessutom man behöver utföra enskilda
sökningar i kolumnerna så går det långsammare på grund av all ”skräpdata” från den andra
tabellen som ligger i indexet.
En tumregel för gemensamma index är:
Om mer än hälften av alla förfrågningar på två tabeller innebär en förening av dessa, och
föreningskolumnerna ändrar nästan aldrig, och ingen av tabellerna innehåller interna
grupperingar, skapa då ett gemensamt index för dessa två tabeller.
5.7. Sammansatta tabeller
Ett annat sätt att undvika föreningar (eller ”joins”) är att utnyttja något som ofta kallas för
sammansatta tabeller (composite tables). Principen är mycket enkel, den sammansatta
tabellen innehåller helt enkelt resultatet från en förening utförd på förhand. Principen
beskrivs lättast med lite SQL:
CREATE TABLE Table1 (
column1 INTEGER PRIMARY KEY,
column2 CHARACTER(50),
...)
CREATE TABLE Table2 (
column3 INTEGER PRIMARY KEY,
column4 CHARACTER(50)
...)
CREATE TABLE Composite (
column1 INTEGER,
column3 INTEGER,
column2 CHARACTER(50),
column4 CHARACTER(50),
...)
CREATE UNIQUE INDEX Index1
ON Composite (column1, column3, column2, column4)
Om någon rad i Table1 eller i Table2 ändras, eller om rader adderas eller raderas, så är
den gemensamma tabellen inte längre synkroniserad. För att förhindra detta så måste även
den gemensamma tabellen uppdateras, vilket är enkelt att göra med en triggningsfunktion.
Sida 34 / 79