Kapittel 7 · Forelesning 13 · Bratsbergs kompendium kap. 12–14

Aksessmetoder, sortering, join-algoritmer

Hva skjer egentlig mellom SELECT … og resultatet? Vi følger en spørring fra parser (komponenten som leser SQL-teksten) til disk — og regner I/O-kost (antall blokker som må leses/skrives) for hvert valg underveis.

01 · Helhetsbilde

Fra SQL-tekst til resultat

Når du sender en SQL-spørring til databasen, går den gjennom en kjede av komponenter før den faktisk berører data på disk. Hvert ledd har en jobb — og kapittel 7 handler om de siste leddene: hvordan data hentes, sorteres og joines.

Spørringspipeline

1 · Parser

SQL-tekst → algebraisk tre (et internt tre der hver node er en relasjonsalgebra-operasjon som σ, π, ⋈). Sjekker syntaks og slår opp tabeller i SQL-dictionary (databasens metadata-katalog).

2 · Optimizer

Genererer flere planer, leser statistikk, velger den med lavest forventet I/O-kost (antall blokk-aksesser).

3 · Executor

Kjører plan-treet ovenfra. Bruker aksessmetoder mot lagring.

4 · Storage

Heap-fil, B+-tre, hash-fil. Leverer blokker via buffer-poolen (RAM-cachen fra kap. 6).

Optimizeren regnes som «black art» (mørk kunst — den er notorisk vanskelig å lage god) fordi den vurderer alle joinrekkefølger, alle aksessmetoder, alle sorterings­strategier og velger billigst plan. Vi skal forstå hvilke valg den har, og hvorfor de koster det de koster.

Hovedinnsikt

Alt måles i blokk-I/O (Input/Output — hver blokk som leses fra eller skrives til disk). RAM-operasjoner regnes som «gratis» sammenlignet med disk-aksess. Derfor handler så mye av kapittelet om å minimere antall blokker som leses og skrives.

Sjekk forståelsen
Hvorfor lager optimizeren flere planer for samme spørring i stedet for å bruke en fast oppskrift?
Fordi billigste plan avhenger av tabellstørrelser, indekser, fordeling av verdier og bufferstørrelse — ting som varierer mellom spørringer og over tid. En fast oppskrift ville vært god for noen tilfeller og katastrofal for andre. Optimizeren bruker statistikk fra dictionaryet (databasens metadata-katalog) til å estimere kost og velge.
02 · Aksessmetoder

Fire måter å lagre én tabell

Hvor billig en spørring kan kjøres henger på hvordan tabellen er lagret. Pensum bruker fire varianter, og vi skal gjenta det samme eksemplet på alle for å se kostnadsforskjellen.

Liten ordliste: Clustered betyr at radene er fysisk lagret i indeksens nøkkelorden — bladene er tabellen. Unclustered betyr at indeksen peker til rader som ligger spredt i en separat heap-fil. Detaljene er forklart i kap. 6, men disse to ordene dukker opp overalt under.

Eksempel: tabellen Employee

  • 100 000 rader, hver er 80 bytes
  • Blokkstørrelse: 4 KiB = 4096 bytes
  • Skjema: Employee(empno, name, age, depno, salary)
Lagringsalternativ — antall blokker
LagringRecords/blokkBladerIndeks-blokkerTotalt
Heap-fil ⌊4096/80⌋ = 51 ⌈100 000/51⌉ = 1961 1961
Clustered B+-tre ⌊4096·⅔/80⌋ = 34 ⌈100 000/34⌉ = 2942 13 + 1 = 14 2956
Heap + unclustered B+ 170 (i indeks) 589 (indeksblader) 3 + 1 = 4 1961 + 593
Clustered hash ⌊4096·0.8/80⌋ = 40 ⌈100 000/40⌉ = 2500 2500

⅔-faktoren for B+-tre kommer av at noder typisk er to-tredjedeler fulle (gjennomsnittlig fyllingsgrad ved tilfeldig innsetting — se kap 6). Hash bruker 80 % fyllgrad.

Aksessveiene

  • Filscan / tabellscan — les alle blokker i filen. Brukes når det ikke finnes nyttig indeks, eller når vi henter «mye» av tabellen uansett.
  • Indeksscan — gå sekvensielt gjennom bladnivået i en indeks. Nyttig f.eks. for SELECT * med sortert resultat.
  • Range-scan — gå ned i B+-treet til startnøkkelen, og scan bladene sidelengs til endenøkkelen. Bruk pekere til å hente rader om indeksen er unclustered.
  • Indeks-lookup — direkte oppslag på én nøkkel: ned i treet (eller ett hashoppslag), hent rad.

Notasjon — variabler du ser i alle formlene

Før vi regner kostnader trenger vi navn på tingene. Disse seks variablene dukker opp på tvers av hele kapittelet — alle senere formler bygger på dem. Bli komfortabel med dem nå, så slipper du å oversette på nytt under hver formel.

  • |R| — antall rader i tabellen R
  • b — antall blokker tabellen tar på disk
  • bfblocking factor: antall rader som får plass i én blokk
  • h — høyden til et B+-tre (antall interne nivåer over bladene)
  • V(A, R) — antall distinkte verdier av attributt A i R (eks: V(depno, Employee) = antall ulike avdelinger)
  • sfselektivitetsfaktor: andel av tabellen som overlever et predikat (mellom 0 og 1)

Kostnadsformler pr. aksessvei — med utledning

Tabellen over viste tall for Employee. Formlene under regner det samme på nye tall — det er disse du faktisk bruker når eksamensoppgaven kommer med andre tabellstørrelser. Hver formel har en kort utledning slik at du forstår hvorfor den er som den er, ikke bare at den finnes.

Blocking factor  bf = ⌊B / R⌋

Hvorfor: Én blokk er B bytes (typisk 4096), hver rad er R bytes — så det får plass ⌊B/R⌋ rader, avrundet ned fordi vi ikke kan spare på en halv rad. Variant for B+-tre-blader: bruk ⌊B·⅔/R⌋ fordi noder typisk er to tredjedeler fulle (tilfeldig innsetting splitter blokker ujevnt). For hash-filer brukes ⌊B·0.8/R⌋. Antall blokker tabellen krever blir b = ⌈|R|/bf⌉ — denne sammenhengen knytter |R| og b, og du veksler mellom dem hele tiden.

B+-tre-høyde  h = ⌈log_d(b)⌉  (d = fan-out)

Hvorfor: I et balansert tre har et nivå k over bladet kapasitet til d^k blader. Vi vil at det øverste nivået rommer minst b blader ⇒ d^h ≥ bh ≥ log_d(b). Avrunding opp fordi vi ikke kan ha 1.6 nivå. Hvorfor er h alltid lite? Fan-out er typisk 100–300 fordi (key, blokk-peker) er små poster (~12–24 bytes). Med d = 200 rommer 4 nivå 200⁴ = 1.6 milliarder blader. Et oppslag koster h + 1 blokk-aksesser fordi vi i tillegg må lese selve bladet — det er denne +1-en som dukker opp i alle oppslagsformlene.

Punkt-oppslag, clustered indeks  cost = h + 1

Hvorfor: Vi traverserer h interne nivå (én blokk hver) og leser én bladblokk. I et clustered B+-tre er bladet datablokken — raden ligger der vi lander, ingen ekstra hopp. Det er nettopp denne sparsomheten som gjør clustered så attraktivt: siste blokk-aksess gir oss både nøkkelen og raden. På Employee-eksemplet med h = 2: cost = 3 blokker, mot ~981 ved en heap-scan.

Punkt-oppslag, unclustered indeks  cost = h + 1 + matches

Hvorfor: Samme tre-traversering (h + 1) — men nå inneholder bladet (key, RID)-poster, ikke selve raden. Vi må følge hver RID til heap-filen, og hver RID kan i verste fall lande på en ny blokk fordi radene ligger spredt. Hvorfor «matches»? På unik nøkkel: én match ⇒ h + 2. På duplikatnøkler er antall matcher i snitt ⌈|R|/V(A, R)⌉ (uniform-antakelsen) — så for WHERE depno = 5 med 10 avdelinger og 100 000 ansatte: 10 000 matcher = 10 000 ekstra blokk-aksesser worst case. Det er denne kostnaden som gjør at unclustered indeks kan slå tilbake mot full scan, og som motiverer hele neste paragraf.

Range-scan, clustered  cost ≈ h + ⌈sf · b⌉

Hvorfor: Først h for å finne første matchende blad. Deretter scan vi sidelengs gjennom de matchende bladblokkene via «neste-bladet»-pekerne. Bladene er fysisk sortert ⇒ sammenhengende sekvensiell I/O (billig pr. blokk for OS/disk). Vi leser kun andelen sf · b bladblokker, ikke hele tabellen. Sammenheng med formelen over: dette er bare punkt-oppslag (h + 1) pluss «scan til vi er forbi range-grensen» (sf·b − 1) — vi summerer og forenkler. Eksempel: range med 20 % treff på b = 2942 blader ⇒ cost ≈ 2 + 589 = 591. Til sammenligning ville full scan vært 1961 — clustered indeks vinner med faktor 3.

Range-scan, unclustered  cost ≤ h + ⌈sf · b_indeks⌉ + sf · |R|

Hvorfor: De to første leddene er identisk med clustered (traversér tre + scan indeksens blader — indeksen er fortsatt sortert). Det er det tredje leddet som ødelegger: vi følger én RID pr. matchende rad, og hver RID kan lande på en ny heap-blokk (radene er ikke sortert i fysisk lagring). Worst case: sf · |R| ekstra random I/O. Klassisk fallgruve, og koblingen til hovedinnsikten: når sf · |R| > b blir filscan (b) billigere enn å bruke indeksen — selv om indeksen «matcher» predikatet. På Employee med sf = 0.2, |R| = 100 000, b = 1961: indeks-kost ≥ 20 000 random I/O, filscan kun 1961 sekvensielle. Filscan vinner med faktor 10 — og dette er en av de viktigste grunnene til at en god optimizer noen ganger ignorerer en lovlig indeks. Det er også grunnen til at indeks-valg under databasedesign er så avgjørende.

Sjekk forståelsen
Hvorfor er range-scan rask på en clustered B+-tre, men kan være tregere enn full table scan på en unclustered indeks med mange treff?
På et clustered B+-tre ligger radene fysisk sortert — vi scanner bladene sidelengs sekvensielt. På et unclustered B+-tre har vi RID-pekere (Record IDentifier — peker til en bestemt rad i en heap-fil) til radene, og hver match kan kreve én ny tilfeldig blokk-aksess. Med 20 % treff på 100 000 rader kan det bli 20 000 random I/O — full scan av heap-filen er da ofte raskere.
03 · Kostnad pr. spørringstype

Den samme spørringen, ulik lagring

Nå sammenligner vi: hva koster SELECT-en med predikatet WHERE key = c, WHERE key > c og full SELECT * på de fire lagrings­alternativene? Tabellen er den samme Employee-eksemplet (100 000 rader, 1961 heap-blokker).

SELECT * FROM Employee  —  alle rader
LagringStrategiBlokker lest
HeapFull scan1961
Clustered B+Tre nivåer ned, så scan blader2 + 2942 = 2944
Heap + unclusteredBare scan heap (indeksen hjelper ikke her)1961
Clustered hashScan hele hash-fila2500

Heap vinner — ingen indeks-overhead, og ekstra utfylling i B+/hash blir bare ekstra arbeid når vi vil ha alt likevel.

SELECT … WHERE empno = 42  —  punktoppslag på unik nøkkel
LagringStrategiBlokker lest
HeapLineær scan, gj.snitt halvparten≈ 981
Clustered B+Tre nivåer ned (+ binærsøk i blokk)3
Heap + unclustered3 ned i indeks + 1 i heap4
Clustered hashHash → blokk (+ litt overflow)≈ 1.2
SELECT … WHERE empno > 80000  —  range-spørring (20 % treff)
LagringStrategiBlokker lest
HeapFull scan (ingen sortering å utnytte)1961
Clustered B+Ned i tre + scan 20 % av bladene2 + 0.2·2942 = 591
Heap + unclustered2 + 118 + opptil 20 000 RID-er ⟶ velg heller heap-scan!1961 (ikke indeks)
Clustered hashHash bevarer ikke orden — full scan2500
Klassisk feilvalg

Med unclustered indeks og bred range-treff kan optimizeren ende opp med å lese hver datablokk flere ganger. Da er en ren heap-scan nesten alltid raskere — og dette er en sentral grunn til at clustered versus unclustered betyr så mye for ytelsen.

Sjekk forståelsen
Du har en unclustered B+-indeks på salary, og kjører WHERE salary > 0 (alle ansatte tjener noe). Hvorfor velger optimizeren full table scan framfor indeksen?
Fordi en full match betyr at vi i verste fall følger 100 000 RID-pekere — én blokk-aksess pr. rad — totalt mye mer enn ~1961 blokker for full scan. Optimizerens kostnadsmodell ser at indeksen er verre her selv om den teknisk «matcher» predikatet.
04 · External merge sort

Sortering når RAM ikke holder

Mange spørringer trenger sortert resultat: ORDER BY, GROUP BY, DISTINCT, og senere sort-merge join (se §6 nedenfor). Vanlig sortering (quicksort, mergesort i RAM) forutsetter at hele lista er i minnet — men en stor tabell er per definisjon ikke det. Vi må sortere bit-for-bit og bruke disk som arbeidsplass, helst uten å lese samme blokk altfor mange ganger.

External merge sort («ekstern» fordi den går utenfor RAM, til disk) løser dette i to faser:

  1. Initialfase: Les så mye som får plass i bufferet, sorter internt med vanlig sort, skriv ut som en sortert «run». Gjenta til hele tabellen er prosessert.
  2. Mergefase: Flett flere runs om gangen ved å sammenligne front-verdiene — siden inputene er sorterte, holder det med én peker per run. Gjenta inntil vi har én sortert fil.
Hvorfor to faser?

Du kan tenke på det som «sortér det du klarer i RAM, så flett resten på disk». Fase 1 utnytter at intern sortering er gratis (ingen disk-I/O) — men den gir oss bare biter som er sortert. Fase 2 fletter bitene parvis (eller flere på én gang) — dét er billig, fordi når begge inputene er sortert trenger vi bare se på én verdi per input om gangen for å plukke neste-i-rekka. Hver fase «rører» hele tabellen én gang for lesning og én gang for skriving — og det er presist der formelen 2b · (1 + passes) kommer fra.

Notasjon (les denne først)

  • b = antall datablokker i tabellen
  • n_B = antall blokker tilgjengelig i bufferet
  • n_R = antall sorterte «runs» etter initialfasen
  • d_M = mergegrad — hvor mange runs vi kan flette sammen i ett pass

Formler — og hvorfor de stemmer

Antall initialruns  n_R = ⌈b / n_B⌉

Hvorfor: Bufferet rommer maks n_B blokker. For å sortere tabellen tar vi ett bufferet om gangen, sorterer det internt og skriver det ut. Tabellen er b blokker, så vi får akkurat ⌈b/n_B⌉ bufferets — derfor like mange runs. Avrunding opp fordi siste run kan være delvis fylt.

Mergegrad  d_M = n_B − 1

Hvorfor: For å flette k runs samtidig trenger vi én slot per run (for å holde gjeldende blokk og se neste verdi i den runen) — pluss én slot for et output-buffer (der vi samler verdier til neste blokk skal skrives). Totalt k + 1 slots, og det må være ≤ n_B. Største k blir n_B − 1. Den ene «mistede» sloten er prisen for å samle output før vi går til disk.

Antall mergepasser  ⌈log_(d_M)(n_R)⌉

Hvorfor: Hvert mergepass slår sammen d_M runs til én — så antall runs deles på d_M hver runde. Vi er ferdige når det bare står 1 run igjen. Sekvensen blir n_R → n_R/d_M → n_R/d_M² → … → 1. Antall slike «d_M-eringer» er logaritmen i base d_M. Større buffer ⇒ større d_M ⇒ færre passer.

Total I/O  2b · (1 + antall mergepasser)

Hvorfor: Hvert pass — initial eller merge — gjør samme ting på I/O-nivå: leser alle b blokker fra disk, og skriver alle b tilbake. Det gir 2b per pass. Initialfasen teller som ett pass (det er der 1-en kommer fra), pluss antall mergepasser. Sortering i RAM er gratis — det er disken vi betaler for, og hver blokk besøkes nøyaktig én gang per pass i hver retning.

Eksempel fra pensum

Sett n_B = 5, b = 1024 (en tabell på 1024 blokker, buffer på 5).

  • n_R = ⌈1024/5⌉ = 205 initialruns (vi får 205 «bufferets» med data å sortere internt)
  • d_M = 5 − 1 = 4 (én slot reservert til output)
  • Antall mergepasser = ⌈log₄(205)⌉ = 4 (205 → 52 → 13 → 4 → 1)
  • Total I/O = 2·1024·(1+4) = 10 240 blokker (initial + 4 merge = 5 passer, hver à 2·1024)

Merk hvor mye buffer-størrelsen betyr: med n_B = 257 ville d_M = 256 og vi hadde merget alt i ett pass — total I/O 2·1024·2 = 4096. Det er én av grunnene til at databaser allokerer mest mulig RAM til sort-bufferne.

Steg-for-steg-animasjon

Et lite eksempel: b = 6 blokker, hver med 2 verdier; n_B = 3. Da er n_R = 2, d_M = 2, og vi trenger akkurat ett mergepass. Total I/O = 2·6·(1+1) = 24 blokk-aksesser. Klikk «Neste steg» for å se hva som skjer i bufferet og hvordan I/O-telleren stiger — eller «Spill auto» for å la den gå selv.

External merge sort — fra usortert input til sortert fil
Klar — trykk «Neste steg»
Hva animasjonen viser deg

Phase 1 kjører i tre takter per batch: les blokker inn → sorter i RAM (gratis) → skriv ut som sortert run. Etter to batcher er bufferet tomt og to runs ligger på disk. Phase 2 har bufferet delt i 3 slots: én for nåværende blokk av Run A, én for Run B, og én som output-buffer. Hver merge-step sammenligner front-verdien i de to input-slotene, kopierer den minste til output-bufferet, og rykker pekeren fram. Når en input-slot er tom, leses neste blokk fra den runen. Når output-bufferet er fullt, skrives det til disk. Følg I/O-telleren — du ser tydelig at hver blokk krysser disk-grensen nøyaktig 4 ganger (én les + én skriv per pass × 2 passer).

Regneoppgave
Tabellen din er 200 blokker, og du har 11 buffer­blokker. Hvor mange mergepasser trenger du?
n_R = ⌈200/11⌉ = 19 initialruns. d_M = 10. Antall mergepasser = ⌈log₁₀(19)⌉ = 2. Total I/O = 2·200·(1+2) = 1200 blokker.
05 · Nested loop join

For hver rad i ytre … for hver rad i indre

Den mest grunnleggende join-algoritmen, og — som pensum understreker — ofte den raskeste når en av tabellene er liten eller når vi har en god indeks på indre.

Block nested loop

I praksis sammenligner vi ikke én rad om gangen; vi laster en bunke blokker fra ytre tabell inn i bufferet, og scanner indre én blokk om gangen for hver bunke.

Eksempel fra pensum

  • Department = 5 blokker (500 rader, 100 pr. blokk)
  • Employee = 1000 blokker
  • Buffer = 5 blokker (3 til ytre, 1 til indre, 1 til output)
  • Spørring: Employee ⋈ Department WHERE dname='Sales' (⋈ = naturlig join — slå sammen rader der join-attributtet, kolonnen vi joiner på, er likt)
Block nested loop  cost = b_ytre + ⌈b_ytre / (n_B − 2)⌉ · b_indre

Hvorfor — ledd for ledd: Første ledd (b_ytre): vi leser ytre tabell én gang, blokk for blokk, fra start til slutt. Det er uunngåelig — vi må se hver rad i ytre minst én gang. Andre ledd: for hver «bunke» av ytre-blokker som får plass i bufferet, scanner vi indre én gang. Bufferet rommer n_B blokker totalt; vi må reservere 1 til indre-blokk (en om gangen) og 1 til output-blokk (der match-radene samles før de skrives ut). Resten — n_B − 2 — kan brukes til ytre. Antall bunker blir derfor ⌈b_ytre / (n_B − 2)⌉, og hver bunke krever én full scan av indre = b_indre. Konsekvens: indre-tabellen leses gjentatte ganger. Det er antall ganger vi må re-scanne den store siden som dominerer kostnaden — derav den klassiske heuristikken «liten tabell utenfor». Bytter vi ut en stor ytre med en liten ytre, deler vi typisk antall passes med en faktor 100 eller mer.

Tre scenarioer — hvilken rekkefølge?
StrategiRegnestykkeBlokker
Department ytre, Employee indre5 + ⌈5/3⌉·1000 = 5 + 2·10002005
Employee ytre, Department indre1000 + ⌈1000/3⌉·5 = 1000 + 334·52670
Filtrer Department først (én rad)5 (lese Dept) + 1000 (Employee én gang)≈ 1001
Tommelfingerregel

Liten tabell utenfor, stor tabell innenfor. Antall passes over indre tabell er ⌈|ytre| / (n_B − 2)⌉ — minimere dette betyr å minimere ytre.

Animasjon — nested loop med matching

To små tabeller, R og S. Vi joiner på lik nøkkel. Klikk «Neste» og se hvordan ytre står stille mens indre scanner — og hvordan to vinduer av matchende rader fanges opp.

For each block in R: for each block in S: probe

R (ytre)

S (indre)

Sammenligninger: 0 · Treff: 0
Klar

Index nested loop (J2)

Hvis indre tabell har en indeks på join-attributtet, hopper vi over hele indre-scanningen og slår opp direkte for hver ytre rad. Vi bytter altså ut én scan av S pr. ytre bunke mot ett indeks-oppslag pr. ytre rad. Det blir billig når R er liten i radantall, og indeksen er klargjort.

J2 med clustered indeks på indre  cost = b_R + |R| · (h_S + 1)

Hvorfor — bygd ledd for ledd: Det første leddet (b_R) er identisk med J1 — vi må fortsatt lese hele ytre én gang. Det er det andre leddet som endrer seg fundamentalt: i stedet for å re-scanne indre ⌈b_R/(n_B−2)⌉ ganger (J1), gjør vi ett oppslag pr. ytre rad. Hvert oppslag koster h_S + 1 blokker — det er nøyaktig formelen for «punkt-oppslag clustered» fra §02. Vi gjenbruker derfor formelen vi allerede har. Sammenheng med J1: J1 har faktor b_S i andre ledd, J2 har faktor (h_S + 1). Siden h_S + 1 typisk er 3–4 og b_S kan være tusenvis, vinner J2 så lenge |R| ikke er for stor. Hvis join-attributtet kan ha duplikater må vi legge til matches/rad − 1 ekstra bladblokker (rader med samme nøkkel som ikke får plass i én blokk).

J2 med unclustered indeks på indre  cost = b_R + |R| · (h_S + 1 + matches_S)

Hvorfor: Helt parallelt med formelen for «punkt-oppslag unclustered» (§02) — hver matchende RID krever ett ekstra hopp til heap-filen, og hver kan lande på en ny blokk. På likhet med V(A, S) distinkte verdier i join-kolonnen er antall matcher i snitt matches_S = ⌈|S| / V(A, S)⌉. Når blir J2 dårlig? Når matches_S er stor — typisk ved bredt duplikat-mønster — kan |R| · matches_S bli større enn én eller to scans av indre. Da slår både J3 (sort-merge) og J4 (hash) tilbake.

Worked example — J1 vs J2 på samme data

Samme join, samme tabeller — kun algoritmen varierer. Vi har R = 50 blokker (1000 rader), S = 500 blokker, n_B = 5, og det finnes en clustered B+-tre på S.pk med høyde h_S = 2 (3-nivåtre).

  • J1 (block NL), R ytre:  50 + ⌈50/3⌉·500 = 50 + 17·500 = 8550 blokker
  • J2 med clustered S.pk:  50 + 1000·(2 + 1) = 50 + 3000 = 3050 blokker

J2 vinner med 65 % færre I/O — fordi 1000 indeks-oppslag à 3 blokker er mye billigere enn 17 fulle scans av en 500-blokkers tabell.

Men hvor er kryssingspunktet? Hvis R hadde vært 100 000 rader, ville J2 kostet 50 + 300 000 = 300 050 — verre enn J1 (som vokser sakte i ytre). Generelt vinner J2 når |R| · (h_S + 1) < ⌈b_R/(n_B − 2)⌉ · b_S. Venstre side vokser lineært med |R|; høyre side vokser med b_R men er begrenset av b_S. Når |R| er stor nok til at venstre side overstiger høyre, snur dominansforholdet — og optimizeren bytter algoritme.

Når foretrekker optimizeren J2?

Tre forutsetninger må vanligvis stemme samtidig: (1) ytre tabell er liten i radantall (ikke bare i blokker — det er |R| som er ganger-faktor i formelen), (2) indre har en brukbar indeks på join-attributtet (helst clustered, slik at vi slipper RID-hopp), og (3) join-attributtet har høy selektivitet i indre (få matcher pr. ytre rad — så matches_S er lite). Bryter en av disse, vinner ofte block nested loop, sort-merge eller hash. Pedagogisk kobling: alle tre punktene henger sammen med formler du allerede har sett — radantall (§02), oppslagskost (§02), og selektivitet (kommer i §07).

Sjekk forståelsen
Du joiner en tabell på 10 blokker med en på 10 000 blokker. Hvilken bør være ytre, og hvorfor?
Den lille (10 blokker) bør være ytre. Da scanner vi den store kun ⌈10/(n_B−2)⌉ ganger. Hadde vi snudd, måtte vi scannet den lille ⌈10000/(n_B−2)⌉ ganger — flere hundre passer.
06 · Andre join-metoder

Sort-merge og hash join — med formler

Pensum behandler nested loop i detalj. De to neste — sort-merge (J3) og partition-hash (J4) — nevnes ofte «konseptuelt», men eksamen kan be deg sammenligne kost mellom dem. Da trenger du formlene. Begge bygger på byggesteiner du allerede kan: J3 bruker external merge sort fra §04, og J4 minner om en grov hashing-strategi som speiler statisk hash-fil fra kap. 6.

Sort-merge join (J3)

Sorter begge tabellene på join-attributtet, og gå parallelt gjennom dem med to pekere. Når en peker leser et match, lar vi den andre stå stille til alle matchene er parret. Effektivt fordi sortert input gjør at vi aldri må gå tilbake — flettingen er én lineær gjennomgang.

J3 totalkost  cost = sortkost(R) + sortkost(S) + b_R + b_S

Hvorfor — ledd for ledd: De to første leddene er nøyaktig external merge sort fra §04, anvendt på hver side: sortkost(X) = 2·b_X·(1 + ⌈log_(d_M)(n_R)⌉). Vi gjenbruker den formelen direkte. Det tredje og fjerde leddet — b_R + b_S — er flettetrinnet: vi leser begge sorterte filer én gang hver, side om side, med to pekere. Ingen tilbake-skritt fordi sortert input garanterer at hvis nåværende verdi i R er mindre enn i S, vil ingen senere R-rad matche denne S-raden — så vi flytter bare R-pekeren framover. Når sortering er gratis: hvis input alt er sortert (f.eks. resultat av en clustered B+-tre-scan eller en tidligere ORDER BY), faller den respektive sortkost-termen vekk. Det er denne sammenhengen som gjør J3 ekstra attraktiv som «ledd nederst i en plan» når rotnoden uansett krever sortert output.

Eksempel — koblet til external merge sort

R = 100 blokker, S = 500 blokker, n_B = 6 ⇒ d_M = 5.

  • sortkost(R):  n_R = ⌈100/6⌉ = 17,  passes = ⌈log₅(17)⌉ = 2  ⇒ 2·100·(1+2) = 600
  • sortkost(S):  n_R = ⌈500/6⌉ = 84,  passes = ⌈log₅(84)⌉ = 3  ⇒ 2·500·(1+3) = 4000
  • flettetrinnet: 100 + 500 = 600
  • Total: 600 + 4000 + 600 = 5200 blokker

Hvis begge alt var sortert: cost = 0 + 0 + 600 = 600. Faktor 8.7 forskjell. Det er nettopp derfor query-planlegging belønner planer som leverer sortert output billig (clustered indeks, eller en tidligere sortering som kan gjenbrukes).

Partition-hash join (J4)

Hash begge tabeller på join-attributtet — alle rader med samme hash-verdi havner garantert i samme partisjon (uansett hvilken tabell de kom fra). Innenfor en partisjon: bygg en hash-tabell i RAM på den minste siden (build-fasen), scan den andre siden og slå opp mot hash-tabellen (probe-fasen). Når en partisjon er liten nok til å få plass i RAM blir join-en innenfor den «gratis» — vi slipper både sortering og re-scan.

J4 totalkost (grace hash)  cost = 3 · (b_R + b_S)

Hvorfor — tre passes over data: Pass 1 (partisjonering av R): les b_R blokker, hash hver rad og fordel den i en av n_B − 1 output-partisjoner, skriv b_R tilbake til disk ⇒ 2·b_R. Pass 2 (partisjonering av S): samme — 2·b_S. Pass 3 (build + probe): for hver partisjons-par leser vi begge sider én gang hver, og bygger/prober hash-tabellen i RAM ⇒ b_R + b_S. Sum: 3·(b_R + b_S). Forutsetning: minste partisjon må få plass i RAM, omtrent n_B² > b_min. Hvis ikke, må vi rekursivt partisjonere på nytt — én ekstra pass pr. nivå rekursjon. Sammenligning med J3: J4 unngår sortering, og er derfor mer robust når buffer er lite (sortering med få runs vinner; sortering med mange runs taper). Pedagogisk: J3 og J4 er to forskjellige måter å «strukturere data slik at like nøkler havner sammen» — sortering for J3, hashing for J4.

Eksempel — samme tall, ny algoritme

Samme R = 100, S = 500: cost = 3·(100 + 500) = 1800 blokker. Mye billigere enn J3 (5200) i dette scenarioet, fordi sortering var dyrt med så lite buffer (n_B = 6). Hadde n_B vært 30 ville J3 kostet bare ett pass og kommet på samme nivå som J4.

NTNUs Kjell Bratbergsengen er pioner på partition-hash join (publisert på VLDB-konferansen — Very Large Data Bases — i 1984).

Beslutningstabell — når vinner hvilken?

J1–J4 sammenligning — formel, vinnerscenario, fallgruve
AlgoritmeKostformelVinner når …Faller for …
J1 block NL b_R + ⌈b_R/(n_B−2)⌉·b_S én side er liten (få blokker); ingen indeks begge store; mange passes over indre
J2 index NL b_R + |R|·(h_S+1[+m]) R har få rader; S har clustered indeks på join-attr R er stor (lineær i |R|); mange duplikater i S
J3 sort-merge sortkost·2 + b_R + b_S begge alt sortert; må sortere uansett (ORDER BY) liten buffer ⇒ mange merge-passes ⇒ sortering eksploderer
J4 hash 3·(b_R + b_S) begge store; equi-join; partisjon får plass i RAM data-skjevhet (én partisjon eksploderer); ikke equi-join

Equi-join betyr join-betingelsen er likhet (R.a = S.b) — sort-merge og hash krever dette, mens nested loop tåler vilkårlige predikater (<, >, BETWEEN osv.). Pedagogisk innsikt: alle fire algoritmer har b_R + b_S som «gulv» — du kan ikke joine to tabeller uten å lese dem minst én gang hver. Forskjellen er hvor mye ekstra som legges på toppen.

Praktisk merknad

De fleste DBMS-er har alle fire algoritmene tilgjengelig og bytter dynamisk basert på statistikk. MySQL har historisk vært nested-loop-orientert (med en enkel «join buffer» som tilnærmer hash); PostgreSQL og kommersielle systemer (Oracle, SQL Server) bruker alle fire flittig. Pensum forventer at du kjenner formlene og kan velge mellom dem på papir — ikke nødvendigvis at du kjenner hver enkelt implementasjon.

Sjekk forståelsen — sortert input
Begge tabeller er allerede sortert på join-attributtet. Hvilken algoritme er typisk billigst?
Sort-merge join (J3). Sortfasen er hoppet over fordi inputen alt er sortert — vi går bare parallelt gjennom dem med to pekere. Total = b_R + b_S. Hash join koster fortsatt 3·(b_R + b_S), og nested loop skanner indre tabell flere ganger. Dette er hovedårsaken til at en plan som leverer sortert output (f.eks. via clustered B+-tre) kan ha lavere totalkost enn én som ikke gjør det — selv om aksessveien isolert sett er dyrere.
Regneoppgave — J3 vs J4
R = 200 blokker, S = 1000 blokker, n_B = 11 ⇒ d_M = 10. Hvilken vinner: J3 eller J4?

J3: sortkost(R) — n_R = ⌈200/11⌉ = 19, passes = ⌈log₁₀(19)⌉ = 2, cost = 2·200·3 = 1200. sortkost(S) — n_R = ⌈1000/11⌉ = 91, passes = ⌈log₁₀(91)⌉ = 2, cost = 2·1000·3 = 6000. Flettetrinn: 1200. Totalt: 8400.

J4: 3·(200+1000) = 3600.

J4 vinner — sortering på 1000 blokker krever to merge-passes selv med d_M = 10. Den faste «3 passes»-strukturen til hash join slår sortering når sorteringen ikke får gjort jobben på ett pass.

07 · Statistikk & EXPLAIN

Hvordan optimizeren gjetter

Optimizeren sammenligner planer uten å kjøre dem. Den må estimere kost basert på statistikk lagret i SQL-dictionaryet (databasens metadata-katalog — der skjema, indekser og slik statistikk holdes). Denne paragrafen forklarer hvordan den gjetter — selve regnemaskineriet bak alle kostnadstallene vi har sett til nå.

Selektivitetsfaktor — andelen som overlever et predikat

Selektivitet er nøkkelen til alle kostnadsestimat. Hvert predikat (WHERE depno = 5, WHERE alder > 60) reduserer antall rader som går videre. Optimizeren tildeler hvert predikat en sf mellom 0 og 1 — andelen rader som matcher — og bruker den til å estimere både output-størrelse og hvilken aksessmetode som vinner. Hvorfor det er kritisk: alle formlene i §02–§06 har sf, |R| eller matches i seg. Disse beregnes alle fra selektivitet — det er den ene knaggen optimizeren lener seg på når den velger plan.

Likhet på attributt A  sf = 1 / V(A, R)

Hvorfor: Hvis attributtet har V(A, R) distinkte verdier og fordelingen er uniform (alle verdier like sannsynlige), vil hver verdi forekomme i |R|/V rader. Sannsynligheten for at en tilfeldig rad har en bestemt verdi er da 1/V — som er definisjonen på selektivitet. Eksempel: |Employee| = 100 000, V(depno) = 10WHERE depno = 5 estimeres til 10 000 rader (sf = 0.1). Spesialtilfelle: når kolonnen er PRIMARY KEY har vi V = |R|, så sf = 1/|R| (én rad). Uniform-antakelsen er den vanligste kilden til feilestimat — i virkeligheten er fordelinger ofte skjeve (mange ansatte i salgsavdelingen, få i toppledelsen).

Range  sf = (high(A) − c) / (high(A) − low(A))  for predikatet A > c

Hvorfor: Vi antar verdiene er jevnt fordelt mellom low og high (lagret i SQL-dictionaryet pr. indekserte kolonne). Andelen av aksen som er «over c» blir (high − c)/(high − low). Eksempel: low = 0, high = 100, predikat empno > 80sf = 20/100 = 0.2. Tilsvarende formel: for A < c bruk (c − low)/(high − low); for BETWEEN c1 AND c2 bruk (c2 − c1)/(high − low). Histogrammer (lagret som verdifordelinger pr. bucket) gir bedre estimater når fordelingen ikke er uniform — f.eks. mange ansatte har lønn 400–500k, få har over 1M. Histogrammer er essensielt en stykkevis lineær approksimasjon.

AND av predikater  sf_total = sf_1 · sf_2  (antar uavhengighet)

Hvorfor: Hvis to predikater er statistisk uavhengige (det ene sier ikke noe om sannsynligheten for det andre), er sannsynligheten for at en rad oppfyller begge produktet av sannsynlighetene — klassisk sannsynlighetsregning. Eksempel: WHERE depno = 5 AND alder > 50. Hvis 10 % er i avd. 5 og 30 % er over 50 år, gjetter optimizeren 0.10 · 0.30 = 3 % oppfyller begge. Når antakelsen feiler: hvis avd. 5 tilfeldigvis er pensjonist-avdelingen er korrelasjonen høy og det reelle tallet kanskje 25 %. Estimatet blir for lavt, optimizeren undervurderer mellomresultatets størrelse, og den valgte planen blir feil. Dette er en kjent og vanlig kilde til dårlige planer i produksjon — derfor lagrer moderne DBMS-er multi-column statistics som dekker korrelasjon mellom kolonner.

OR av predikater  sf_total = sf_1 + sf_2 − sf_1 · sf_2

Hvorfor: Inklusjon-eksklusjon — vi summerer treffene fra hvert predikat, men trekker fra de som ble talt to ganger (rader som oppfyller begge). Følger samme sannsynlighetsregel: P(A ∪ B) = P(A) + P(B) − P(A ∩ B). For små sf-verdier er sf_1 · sf_2 liten, så sf_total ≈ sf_1 + sf_2 er en rimelig hurtig-tilnærming. Sammenheng med AND: de er duale — AND multipliserer (gjør sf mindre, færre treff), OR summerer (gjør sf større, flere treff).

Hvorfor selektivitet binder hele kapittelet sammen

Selektivitet bestemmer hvor mange rader som «overlever» hvert ledd i planen — og dermed hvor stort input neste ledd må jobbe med. Den dukker opp i: range-scan-kostnad (§02: sf · b), unclustered-fellen (§02: sf · |R|), output-størrelse pr. tabell (input til join), join-output-størrelse (input til sort/projection), og final result size. En faktor 10 i feil estimat på øverste ledd kan multiplisere seg gjennom planen og gi en faktor 100–1000 i feil totalkost. Det er derfor moderne DBMS-er bruker så mye energi på histogrammer, multi-column statistics og adaptive re-planning når estimatet er åpenbart feil.

Statistikk i SQL-dictionaryet

  • For hver tabell: antall rader, antall blokker.
  • For hver indeks: antall distinkte nøkkelverdier, antall blokker, histogrammer (verdifordelinger).
  • For hvert B+-tre: trehøyde, LowKey (laveste forekommende nøkkel), HighKey (høyeste), antall blokker.

Med disse tallene kan optimizeren estimere antall blokk-aksesser for hver kandidatplan og velge den billigste.

EXPLAIN i MySQL

Når noe går tregt, er EXPLAIN verktøyet for å se hva optimizeren faktisk har valgt — uten å kjøre spørringen.

mysql> explain select * from reg natural join loper;
+----+-------------+-------+------+---------+------+----------+---------+
| id | select_type | table | type | key     | rows | filtered | Extra   |
+----+-------------+-------+------+---------+------+----------+---------+
|  1 | SIMPLE      | loper | ALL  | PRIMARY |    3 |   100.00 | NULL    |
|  1 | SIMPLE      | reg   | ALL  | NULL    |   10 |    10.00 | Using   |
|    |             |       |      |         |      |          | join    |
|    |             |       |      |         |      |          | buffer  |
+----+-------------+-------+------+---------+------+----------+---------+

type = ALL betyr full table scan. MySQL leser tabellene i den oppgitte rekkefølgen og bruker en intern join-buffer (en enkel form for hash join) når ingen indeks er tilgjengelig.

Sjekk forståelsen
Optimizeren estimerer 100 rader — i virkeligheten kommer det 1 million. Hva er den vanligste konsekvensen?
Den valgte planen er ofte feil — typisk en nested loop som var billig for 100 rader, men forferdelig for 1M rader (antall sammenligninger vokser med kvadratet — 100·100 = 10 000, men 1M·1M = 10¹²). Statistikk må holdes oppdatert for at optimizeren skal velge riktig.
08 · Regn på en spørring

Slik angriper du oppgaven

Eksamen tester sjelden ett enkelt konsept — den ber deg ofte regne totalkostnaden for en plan, eller velge mellom 2–3 planer. Da må du jobbe deg gjennom verktøykassen i en bestemt rekkefølge. Denne paragrafen samler alt fra §02–§07 til én oppskrift, ett komplett worked example, og et formelark for raskt oppslag.

Sjekkliste — bruk denne på enhver kost-oppgave
  1. Skriv opp kjente størrelser: |R|, b_R, |S|, b_S, n_B, hvilke indekser finnes og om de er clustered, høyde h for hver. Dette er bokføringen — gjør den eksplisitt, ikke i hodet.
  2. Estimer selektivitet for hvert predikat: sf = 1/V (likhet), range-formelen, eller AND/OR-kombinasjon. Beregn forventet output-størrelse sf · |R|.
  3. Velg aksessvei pr. tabell: regn kost for filscan (b), indeks-lookup, range-scan clustered/unclustered. Velg billigst — det er denne du går videre med.
  4. Velg join-algoritme hvis det finnes en join: J1, J2 (hvis indeks finnes på indre), J3 (hvis sortering nyttig), J4. Sammenlign tallene; vinneren går inn i planen.
  5. Legg til sortering dersom ORDER BY/GROUP BY/DISTINCT — med mindre clustered indeks alt gir sortert output (gratis).
  6. Sum opp. Hver operatorkost legges sammen ⇒ totalkostnad. Skal flere planer sammenlignes: gjenta steg 3–5 for hver, sammenlign sluttsummen.

Pedagogisk poeng: oppskriften går «utenfra og inn» — først filtrerer vi (steg 2–3), så kombinerer (steg 4), så ordner (steg 5). Det speiler nøyaktig hvordan optimizeren tenker, og hvordan den fysiske planen er strukturert som et tre nedenfra og opp.

Komplett worked example

La oss kjøre sjekklisten på en realistisk oppgave — én som kombinerer aksess, join, sortering, og selektivitet. Følg med på hvert steg.

Oppgave
SELECT e.name
FROM Employee e JOIN Department d ON e.depno = d.depno
WHERE d.dname = 'Sales'
ORDER BY e.name;

Kjente størrelser: Employee = 100 000 rader / 1961 blokker. Department = 50 rader / 5 blokker. n_B = 10 buffer.
Indekser: Clustered B+ på Employee.empno (h=2), unclustered B+ på Employee.depno (h=2). Ingen indeks på Department.
Statistikk: V(dname, Department) = 50 (hver avdeling har unikt navn). V(depno, Employee) = 50.

Steg 1–2: selektivitet og output-størrelser

  • WHERE d.dname = 'Sales': sf = 1/V(dname, Dept) = 1/50 = 0.02 ⇒ 1 rad ut av Department.
  • Etter join: hver Department-rad matcher i snitt |Employee|/V(depno, Employee) = 100 000/50 = 2000 Employee-rader ⇒ join-output ≈ 2000 rader.

Vi vet allerede før vi regner kost at sluttresultatet bare er 2000 rader. Det er en sterk hint om at vi vil unngå å lese hele Employee om mulig.

Steg 3: aksessvei pr. tabell

  • Department: ingen indeks ⇒ filscan = 5 blokker. Vi filtrerer på dname = 'Sales' mens vi scanner.
  • Employee: hvilken aksessvei vi bruker avhenger av join-algoritmen — derfor regnes den i steg 4 sammen med joinen.

Steg 4: join-algoritme

Department er liten ⇒ ytre. Etter filtrering har vi 1 rad (≈ 1 blokk i mellomresultat). Vi sammenligner alle fire algoritmer:

  • J1 (block NL), filtrert Department ytre, Employee indre:  5 (les Dept) + ⌈1/8⌉·1961 = 5 + 1961 = 1966. Kun én scan av Employee fordi ytre passer i én buffer-bunke.
  • J2 med unclustered indeks på Employee.depno:  ett oppslag pr. ytre rad. matches_S = 2000. 5 + 1·(2 + 1 + 2000) = 2008. Litt verre — fordi 2000 RID-hopp er mye når indeksen er unclustered.
  • J3 sort-merge: må sortere Employee. d_M = 9, n_R = ⌈1961/10⌉ = 197, passes = ⌈log₉(197)⌉ = 3 ⇒ sortkost = 2·1961·4 = 15 688. Verre med god margin — sortering er ikke verdt det her.
  • J4 partition-hash: 3·(5 + 1961) = 5898. Også verre — fast 3 passes-kost slår dårlig ut når en side er bittesmå (J1 utnytter dette bedre).

Vinner: J1 med 1966 blokker. Pedagogisk poeng: når én side er bittesmå (1 rad etter filtrering!), faller alle de «smarte» algoritmene gjennom — den enkleste vinner. Liten ytre × stor indre × ingen revisning er nested loops naturlige hjem.

Steg 5: ORDER BY e.name

Output er 2000 rader. Forutsatt 100 rader pr. blokk ⇒ ~20 blokker. Sortering: n_R = ⌈20/10⌉ = 2, passes = ⌈log₉(2)⌉ = 1, sortkost = 2·20·2 = 80 blokker.

Steg 6: totalkostnad

Total = J1 + sort = 1966 + 80 ≈ 2046 blokker.

Hva-hvis-analyse: hvis Employee.depno hadde vært clustered i stedet for unclustered, ville J2 kostet 5 + 1·(2 + 1) = 8 for å finne riktig blader, pluss å lese 2000-rad-segmentet ≈ 39 sammenhengende blokker — totalt rundt 50 blokker. Det er en faktor 40 forskjell. Det er denne sensitiviteten — at clustering på riktig kolonne kan endre kostnaden så dramatisk — som gjør indeks-design så viktig under databasedesign.

Formelark — alt på én side

For repetisjon rett før eksamen — alle formler i kapittelet samlet, slik at du kan bla rett til den du trenger. Ikke en erstatning for forståelsen, men praktisk når du jobber gjennom en oppgave under tidspress.

Aksessmetoder
OperasjonFormel
Blocking factorbf = ⌊B/R⌋  (B+-tre-blader: ⌊B·⅔/R⌋; hash: ⌊B·0.8/R⌋)
Antall datablokkerb = ⌈|R|/bf⌉
B+-tre-høydeh = ⌈log_d(b)⌉  (d = fan-out)
Filscanb
Punkt-oppslag clusteredh + 1
Punkt-oppslag unclusteredh + 1 + matches
Range-scan clusteredh + ⌈sf·b⌉
Range-scan unclusteredh + ⌈sf·b_indeks⌉ + sf·|R|
Hash punkt-oppslag≈ 1.2 (clustered hash)
External merge sort
StørrelseFormel
Antall initialrunsn_R = ⌈b / n_B⌉
Mergegradd_M = n_B − 1
Antall merge-passes⌈log_(d_M)(n_R)⌉
Total I/O2·b·(1 + #merge-passes)
Join-algoritmer
AlgoritmeFormel
J1 block NLb_R + ⌈b_R/(n_B−2)⌉·b_S
J2 index NL clusteredb_R + |R|·(h_S + 1)
J2 index NL unclusteredb_R + |R|·(h_S + 1 + matches_S)
J3 sort-mergesortkost(R) + sortkost(S) + b_R + b_S
J4 partition-hash3·(b_R + b_S)
Selektivitet
PredikatFormel
Likhet A = csf = 1 / V(A, R)
Range A > csf = (high(A) − c) / (high(A) − low(A))
ANDsf_1 · sf_2
ORsf_1 + sf_2 − sf_1·sf_2
Forventet output-størrelsesf · |R| rader
Matches pr. ytre rad i join|S| / V(join-attr, S)
09 · Sluttquiz

Test deg selv på hele kapittelet

Blandet vanskelighet. Klikk «Se svar» når du har tenkt.

1 · Lett · Helhetsbilde
Hvilken komponent velger plan, og hvilken kjører den?
Optimizeren velger plan basert på statistikk og kostnadsestimater. Executoren kjører plan-treet rotnoden ned, kaller aksessmetoder mot lagring.
2 · Lett · Aksessmetoder
Hvilke aksessveier nevner pensum?
Filscan (tabellscan), indeksscan, range-scan, og indeks-lookup. Optimizeren velger den billigste basert på statistikk.
3 · Lett · Lagring
Hvor mange blokker har en heap-fil med 100 000 rader à 80 bytes ved 4 KiB blokker?
Records pr. blokk = ⌊4096/80⌋ = 51. Antall blokker = ⌈100 000/51⌉ = 1961.
4 · Middels · Range-spørring
På samme tabell er det en unclustered B+-indeks på empno. Hvorfor velger optimizeren full table scan for WHERE empno > 80000 (20 % treff)?
Unclustered betyr at radene ligger spredt i heap. 20 % av 100 000 = 20 000 RID-pekere som hver kan kreve ett blokkoppslag. Det blir > 20 000 blokker, mot ~1961 ved full scan. Full scan vinner stort.
5 · Vanskelig · External merge sort
Hvorfor er total I/O for external merge sort 2b · (1 + #merge-passes) og ikke b · #passes?
Hvert pass både leser og skriver hele datasettet — derav 2b. Initialfasen er ett 2b-pass i seg selv. Mergefasen har k passer, hver 2b. Sum = 2b·(1+k).
6 · Middels · Sort-formel
b = 500 blokker, n_B = 6 buffer. Hva blir n_R, d_M og antall passer?
n_R = ⌈500/6⌉ = 84. d_M = 5. Antall mergepasser = ⌈log₅(84)⌉ = 3 (siden 5²=25 < 84 ≤ 5³=125). Total I/O = 2·500·(1+3) = 4000 blokker.
7 · Lett · Nested loop
Hvilken tabell skal være ytre i en block nested loop join?
Den minste. Da scanner vi indre tabell færrest ganger, fordi antall passer over indre er ⌈|ytre| / (n_B−2)⌉.
8 · Vanskelig · NL-kost
R = 200 blokker, S = 5000 blokker, buffer = 12. Block nested loop med R ytre — hva blir kostnaden? Og hvis vi snur?
R ytre: 200 + ⌈200/(12−2)⌉ · 5000 = 200 + 20·5000 = 100 200.
S ytre: 5000 + ⌈5000/10⌉·200 = 5000 + 500·200 = 105 000. R som ytre vinner.
9 · Lett · Join-algoritmer
Hvilke fire join-algoritmer nevner pensum? (J1–J4)
J1: Nested-loop join. J2: Single-loop / index nested loop. J3: Sort-merge join. J4: Partition-hash join. Pensum behandler J1 i detalj; resten kun konseptuelt.
10 · Middels · Index nested loop
I hvilken situasjon er index nested loop typisk raskere enn vanlig block nested loop?
Når indre tabell har en indeks på join-attributtet og ytre tabell er liten. Da slipper vi å scanne indre — vi gjør ett indeks-lookup per rad i ytre, hver til kostnad ~3–4 blokker (B+-tre-høyde + radblokk).
11 · Middels · Statistikk
Hvilken type statistikk lagres for et B+-tre i SQL-dictionaryet?
Trehøyde, LowKey og HighKey (laveste/høyeste nøkkel), antall blokker. Optimizeren bruker dette til å estimere kostnaden for indeks-lookup, range-scan og indeksscan.
12 · Lett · EXPLAIN
I MySQL EXPLAIN står det type = ALL. Hva betyr det?
Full table scan — MySQL leser hele tabellen blokk for blokk fordi ingen brukbar indeks finnes for spørringen.
13 · Vanskelig · J2-kostnad med formel
R = 80 blokker (1500 rader), S = 600 blokker. Det finnes en clustered B+-tre på S.pk med høyde h_S = 2. n_B = 5. Hva koster J2 med R som ytre, og hvordan sammenlignes det med J1?

J2: cost = b_R + |R|·(h_S + 1) = 80 + 1500·3 = 4580 blokker.

J1 (block NL): 80 + ⌈80/3⌉·600 = 80 + 27·600 = 16 280 blokker.

J2 vinner med en faktor 3.5 — fordi 1500 indeks-oppslag à 3 blokker er mye billigere enn 27 fulle scans av en 600-blokkers tabell. Men hadde |R| vært 100 000 ville J2 kostet 80 + 300 000 — verre enn J1. Kryssingspunktet ligger der antall ytre rader (i tusener) møter antall passes over indre.

14 · Middels · Selektivitet
|R| = 1 000 000, V(land, R) = 200, low(alder) = 0, high(alder) = 100. Estimer antall rader som matcher WHERE land = 'Norge' AND alder > 60.

sf_land = 1/V(land, R) = 1/200 = 0.005.

sf_alder = (high − c)/(high − low) = (100 − 60)/(100 − 0) = 0.4.

AND ⇒ sf_total = sf_land · sf_alder = 0.005 · 0.4 = 0.002.

Estimat: 0.002 · 1 000 000 = 2000 rader. Forutsetter at de to predikatene er uavhengige — i praksis kan korrelasjon mellom land og alder gjøre estimatet feil.

15 · Vanskelig · J3 vs J4
R = 300 blokker, S = 1500 blokker, n_B = 6. Hvilken vinner: J3 eller J4? Forklar hvorfor.

J3 (sort-merge): d_M = 5. sortkost(R): n_R = ⌈300/6⌉ = 50, passes = ⌈log₅(50)⌉ = 3, cost = 2·300·4 = 2400. sortkost(S): n_R = 250, passes = ⌈log₅(250)⌉ = 4, cost = 2·1500·5 = 15 000. Flettetrinn: 1800. Totalt: 19 200 blokker.

J4 (hash): 3·(300+1500) = 5400 blokker.

J4 vinner stort — sortering på 1500 blokker krever 4 merge-passes med så lite buffer, og hver pass leser+skriver hele datasettet. Hash join er robust mot lite buffer fordi det alltid er fast 3 passes.

16 · Middels · Range på unclustered
|R| = 200 000 rader, b = 4000 blokker. Unclustered B+ på alder med h = 2, b_indeks = 500 bladblokker. Range-spørringen treffer 30 % av radene. Bør optimizeren bruke indeksen?

Indeks-kost: h + ⌈sf·b_indeks⌉ + sf·|R| = 2 + 150 + 60 000 = 60 152 blokker.

Filscan: b = 4000 blokker.

Filscan vinner med en faktor 15 — optimizeren ignorerer indeksen. Klassisk «brede range + unclustered = drop indeksen»-fellen, og en hovedgrunn til at unclustered indekser ofte må pares med filscan-fallback i query-planen.

17 · Vanskelig · Plan-sammenligning (filter pushdown)
Spørring: SELECT * FROM Order o JOIN Customer c ON o.cid = c.cid WHERE c.country = 'NO'. Customer = 100 blokker (10 000 rader, V(country)=50), Order = 5000 blokker. n_B = 10. Skal vi joine før eller etter filtreringen — og hvorfor er forskjellen så stor?

sf for country='NO' = 1/50 = 0.02 ⇒ 200 Customer-rader = 2 blokker filtrert mellomresultat.

Plan A (filtrer først, så join): filscan Customer (100) + materialiser 2 blokker. J1 med 2 blokker ytre, Order indre: 2 + ⌈2/8⌉·5000 = 5002. Totalt: 5102 blokker.

Plan B (join hele Customer, så filtrer): J1 med Customer (100) ytre, Order indre: 100 + ⌈100/8⌉·5000 = 100 + 13·5000 = 65 100 blokker. Faktor 13 forskjell.

Vinner: Plan A. «Filter pushdown» (skyv filtreringen så tidlig som mulig) er en av de viktigste og enkleste optimaliseringsteknikkene — fordi antall passes over Order er ⌈|ytre|/(n_B−2)⌉, og en mindre ytre nesten alltid betyr drastisk færre passes.

Videre lesning

Nå er du klar for kapittel 8 — Transaksjoner. Spørringer er bare halve historien; den andre halvparten er hvordan vi kjører flere spørringer samtidig uten å trampe på hverandre.