Lagring — records, blokker, buffer
Bratsbergs notater kap 1–8. Hvordan en rad faktisk er bytes på en disk — og hvordan DBMS-en ber om dem.
Notatmapping → §01 = Bratsberg-kap 1 · §02 = kap 2 · §03 = kap 3 · §04 = kap 4 · §05 = kap 5 · §06 = kap 6 · §07 = kap 7 · §08 = kap 8. Full mapping →
Komponentene i en SQL-DBMS
Et DBMS (databasesystem) er ikke ett program — det er et lag-på-lag-system hvor hver komponent har én jobb. Når du kjører en SQL-spørring vandrer den gjennom hele stakken før den når disken og tilbake. Du møter alle komponentene i tur og orden senere i del 2 — for nå er det selve laginndelingen som er poenget.
- SQL-kompilatoren parser teksten og slår opp i katalogen for å oversette til et internt algebra-tre med konkrete tabell- og kolonnereferanser.
- Optimizeren genererer mange mulige kjøreplaner (med ulike join-rekkefølger og indekser) og velger den med lavest forventet diskaksess-kostnad (kalt I/O — Input/Output: lese/skrive blokker) basert på statistikk fra katalogen.
- Executoren kjører planen — den ber om blokker fra buffer pool, tar låser via lock manager, og skriver til logfilen hvis den endrer noe.
- Buffer pool er det eneste laget som faktisk leser/skriver disk. Alt over snakker kun med buffer pool.
Hvor disken slutter og blokkene begynner
Et device er enten en hel disk («raw partition» — disk uten filsystem oppå) eller en stor fil i filsystemet. DBMS-en deler denne lange byte-strømmen inn i blokker (også kalt sider/pages) — sammenhengende biter, typisk 4–32 KiB store. Blokken er DBMS-ens grunnleggende lese- og skrive-enhet mot disk: når DBMS-en trenger én rad, henter den hele blokken raden ligger i (en diskaksess). Hver blokk får et nummer kalt en BlockId.
HDD — rotating disk
Mekaniske armer søker fysisk til riktig sektor. Random seek: ~10 ms. Sekvensiell I/O kan være 100× raskere enn random.
SSD — flash
Ingen mekaniske deler. Random read: ~100 µs. Skriving krever at flash-cellen først nullstilles, og en liten endring kan tvinge ny-skrivning av en større enhet (kalt write amplification). Begrenset levetid pr. celle.
Hvordan en BlockId er bygget opp
BlockId-en kombinerer hvilken disk og hvor i disken. Med 4 byte: 1 byte for deviceId + 3 byte for offset (posisjonen — hvor i disken, talt i blokker fra start) → 224 blokker per device. Med 16 KiB blokker blir det 256 GiB per device. 8-byte BlockIds spenner over alt vi trenger.
(deviceId, offset). Hvorfor er det smart å adressere blokker logisk slik — i stedet for å bruke en absolutt byte-posisjon i en stor fil?Tre måter å pakke en rad
En record (rad) er én rad i tabellen, lagret som en sekvens bytes inne i en blokk. Hver record består av flere felt (kolonneverdier som empno, name, age). Hvordan disse bytene legges ved siden av hverandre avhenger av datatypene.
Når en tabell har kun fixed-length attributter (INT, CHAR(N), DATE — alle med kjent og fast lengde), ligger hvert felt på fast offset (samme byte-posisjon i hver record). Da er det enkelt å plukke ut feltet — bare hopp til riktig posisjon. Men så kommer VARCHAR (variabel-lengde tekst)…
Format 1 — Fixed length
Format 2 — Record vector (variable length)
Format 3 — Delimiter
Row store vs column store
Tradisjonell DBMS bruker row store: hele raden som én record. Bra for OLTP-arbeidslast (Online Transaction Processing — typiske webapper med mange små inserts/updates på enkeltrader). Column store lagrer hver kolonne for seg — bra for analytiske queries (SUM(salary), AVG(age)) der man kun trenger få kolonner men mange rader. Komprimerer også bedre fordi like verdier ligger sammen.
Person(name VARCHAR(50), age INT). Hvorfor kan ikke fixed-length-formatet brukes direkte?SELECT AVG(salary) FROM Employee. I row store må DBMS-en lese alle kolonner i hver rad, selv om bare salary brukes. I column store er hele salary-kolonnen sammenhengende → mindre I/O og bedre kompresjon. OLTP-arbeidslast (SELECT * på enkelt-rader) favoriserer fortsatt row store.Slot directory: anatomi av en blokk
For variable-length records trenger vi en mer fleksibel layout. Standardmønsteret er two-pointer-layout: records vokser fra starten, slot directory fra slutten. De møtes i midten — når det blir trangt, må man kompaktere eller splitte blokken.
Hvorfor er denne layouten genial?
Liten ordliste først: Hver slot er et lite par (pointer, lengde) som peker til startposisjonen til en record inne i blokken.
- RID forblir stabil ved kompaktering. Hvis vi sletter Record B og senere flytter C nedover for å fjerne hullet, oppdaterer vi bare slot[2].pointer. Indekser ute som peker til (BlockId, slot=2) virker fortsatt.
- Slot directory kan sorteres etter nøkkel. Da kan vi binærsøke i directoriet — selv om de fysiske recordene ligger i innsettingsorden.
- Flip/flop-stempel ved start og slutt: ved en torn write («revet skriving» — for eksempel hvis strømmen forsvinner midt i blokk-skrivingen, slik at halve blokken har gammel data og halve ny), vil de to stemplene være ulike. Da kan vi forkaste blokken som korrupt og hente fra log.
Interaktivt — sett inn og slett records
Slot directory simulator
Trykk på knappene for å se hvordan blokken endrer seg.
Den eneste broen mellom RAM og disk
Buffer pool-en er typisk svært stor — flere GB i serverdatabaser. Den fungerer som en cache (mellomlager) i RAM: én gang en blokk er lest inn fra disk, kan flere transaksjoner og spørringer dele den. For å finne en gitt blokk i bufferet brukes en hashtabell på BlockId — så lookup er konstant tid (O(1) — like raskt uansett hvor stort bufferet er).
Liten ordliste: Et frame er én plass i bufferet, akkurat stor nok til én blokk. pin-count = hvor mange aktive brukere holder blokken låst i bufferet (kan ikke kastes ut når > 0). Dirty = endret siden den ble lest fra disk; må skrives tilbake før den kastes. Eviction = å kaste en blokk ut for å gjøre plass til en ny.
Hva skjer når en blokk forespørres?
- Slå opp BlockId i hash-indeksen. Hit → returner pekeren til frame, øk pin-count, ferdig.
- Miss → må finne et ledig frame. Hvis ingen er tomme, velg en blokk å kaste ut («offer» eller eviction-kandidat) via erstatningsstrategien (LRU = kast eldste, MRU = kast nyeste, clock = billig tilnærming til LRU). Offerets pin-count må være 0.
- Hvis offeret er dirty, må de endrede dataene først skrives ut til disk. Regelen som styrer dette heter WAL (Write-Ahead Logging) og forklares i kap. 8.
- Les den ønskede blokken inn i frame. Oppdater hash-indeksen. Returner.
Erstatningsstrategier
| Strategi | Velger ut | Bra for | Dårlig for |
|---|---|---|---|
| LRU | Lengst siden bruk | Hot data, lokalitet | Skanninger forurenser bufferet |
| MRU | Sist brukte | Repeterte scans | Hot working set |
| Clock | Tilnærmet LRU, billigere | Praktisk default | Skanninger (samme som LRU) |
Interaktivt — LRU i aksjon
Tre buffer frames, LRU-eviction
Skriv en BlockId og trykk «Hent». Den eldste blokken kastes ut når bufferet er fullt.
Operativsystemet har sin egen blokk-cache (page cache), men den vet ikke at en stor table scan «forurenser» cachen ved å fylle den med blokker som aldri leses igjen, eller at en mye-brukt rot-blokk i en indeks bør låses fast (pinnes) i bufferet. DBMS-en kjenner semantikken og kan prefetche (lese fram blokker den vet kommer), pinne, og tvinge skrivinger ved behov.
Tabell uten orden
Heapfilen er den enkleste lagringsformen — bare en lenkeliste av blokker, og records legges inn der det er plass. Ingen sortering, ingen indeks-i-seg-selv.
Fordeler
- Trivielt enkelt å implementere
- Insert er konstant tid
- Ingen indeks-vedlikehold ved oppdatering
- Velegnet for tabeller som likevel skal fullskannes
Ulemper
- Søk er lineært i tabellstørrelsen
- Range-spørringer er like dårlige
- Blir spredt etter mange sletter — krever reorganisering
- Brukes nesten alltid sammen med indekser
Konstant-tid likhetsoppslag
Hashing leverer det heapfilen ikke kan: O(1) likhetssøk. Men den har en akilleshæl — du må bestemme antall blokker på forhånd.
Tre overflow-strategier
- Open addressing (åpen adressering): prøv neste blokk (eller fast hopp). Enkelt, men gjør sletting komplisert.
- Separat overflow: dedikerte overflow-blokker som bøtte-blokker peker til. Kan deles eller være per-bøtte.
- Multiple hashing: bruk en alternativ hash-funksjon ved kollisjon. Distribuert overflow.
Statisk hashing antar at antall records er kjent på forhånd. I praksis varierer datamengden, og lange overflow-kjeder gjør at oppslag som skulle vært O(1) blir O(N).
WHERE empno BETWEEN 100 AND 200, selv før overflow-problemet kommer inn i bildet?mod N. For å finne alle i intervallet må vi enten hashe hver mulig verdi (101 oppslag) eller skanne hele filen. Hashing er kun for likhetssøk — range krever en sortert struktur som B+-tre.Hashing som vokser elegant
Extendible hashing introduserer et lag mellom hash-verdien og blokken — en directory (oppslagstabell). Når en blokk fylles, dobles directory-en (eller bare blokken splittes), uten å rehasher hele filen.
Hovedideen — global vs. local depth
Tenk på directoriet som et oppslagsbord der adressen er d siste bits av hashen.
- Global depth (d): hvor mange bits av hashen som directory-en bruker for å indeksere — altså hvor mange rader oppslagsbordet har (2d).
- Local depth (d'): hvor mange bits den enkelte blokken faktisk skiller på (siden flere directory-slots kan dele samme blokk).
- Når d' < d for en blokk: flere directory-slots peker til samme blokk. Splitting skjer uten directory-doubling.
- Når d' = d og blokken splittes: directory må først dobles.
Det er ingen tilfeldighet — det er det som gjør doublingen elegant. Når d vokser fra 2 til 3, splittes hver gamle slot i to nye: slot 01 blir 001 og 101. Begge ender fortsatt på 01, så records som før hashet til slot 01 kan nå fordeles mellom de to nye slottene basert på den nye mest-signifikante biten — uten at vi må røre records i andre blokker. Direktoriet kan vokse uten at filen rehashes.
Med "siste d bit"-konvensjonen betyr en directory-fordobling rett og slett: kopier hele tabellen og lim den under seg selv. Så bare blokken som overflyttet trenger å splittes.
Bygg din egen — interaktiv simulator
Sett inn nøkler én og én, og se hvordan directoriet vokser, hvilke slots som deler samme blokk, og når blokk-splitting utløser directory-doubling. Hash-funksjonen er h(K)=K — vi tar de siste d bitene direkte fra nøkkelen. Prøv eksempelet, eller dine egne nøkler.
Extendible hash-simulator
Søk i extendible hashing — alltid 2 blokk-aksesser
- Beregn h(K), ta de siste d bitene → finn directory-slot.
- Les den datablokken slot-en peker til.
- Søk i blokken (lite, kan være lineært eller binær).
Sammenliknet med statisk hashing er ekstrakostnaden én directory-aksess. Til gjengjeld blir ytelsen ikke kvalt av lange overflow-kjeder.
Steg-for-steg trase — fra tom til 2 doublings
Vi setter inn nøklene 4, 5, 6, 12, 7, 11 i en extendible hash med kapasitet 2 records per blokk. Vi indekserer på de siste d bitene av nøkkelens binære representasjon. Start: d = 0, én slot som peker til én tom blokk A (d'=0).
| # | Op | Tilstand etter | Hva skjedde |
|---|---|---|---|
| 1 | insert 4 | d=0; A=[4] (d'=0) | Plass — sett inn. |
| 2 | insert 5 | d=0; A=[4,5] (d'=0) | A blir full. |
| 3 | insert 6 | d=1; slot 0→A=[4,6], slot 1→B=[5]; d'(A)=d'(B)=1 | Doubling #1. A var full og d'=d=0 → doble katalog (d: 0→1), splitt A på siste bit. 4 (...0), 6 (...0) → A; 5 (...1) → ny B. |
| 4 | insert 12 | d=2; slot 00→A=[4,12], slot 10→C=[6], slots 01,11→B=[5]; d'(A)=d'(C)=2, d'(B)=1 | Doubling #2. 12 (...0) → A (full), d'(A)=d=1 → doble (d: 1→2), splitt A på siste 2 bit. 4 (...00), 12 (...00) → A; 6 (...10) → ny C. B uendret — d'(B)=1, så slot 01 og 11 deler B. |
| 5 | insert 7 | d=2; B=[5,7] | 7 (...11) → B. Plass. |
| 6 | insert 11 | d=2; slot 01→B=[5], slot 11→D=[7,11]; d'(B)=d'(D)=2 | Splitt uten doubling. 11 (...11) → B (full). d'(B)=1 < d=2 → splitt B på siste 2 bit, men ingen ny doubling. 5 (...01) → B; 7 (...11), 11 (...11) → ny D. |
Resultat: 2 directory-doublings, 4 datablokker (A, B, C, D), endelig d = 2.
Full blokk og d'(blokk) = d: doble katalogen først (d → d+1), deretter splitt blokken. Full blokk og d'(blokk) < d: splitt bare blokken — flere katalogslots pekte før til denne ene blokken, nå deler de seg på to.
000 og 100 har samme 2 siste bit (00) og peker derfor til samme blokk. Når denne blokken senere splittes (uten å fylle opp directoriet) blir det to blokker, hver med local depth 3.Det du må huske
| Konsept | Hva | Når |
|---|---|---|
| Blokk | 4–32 KiB enhet for I/O | Alltid — ikke noe mindre |
| RID | (BlockId, slot) | Indeks → rad-peker |
| Slot directory | Pekere på slutten av blokk | Variable-length, sletting/kompaktering |
| Buffer pool | RAM-cache + hash-indeks | Minimere disk-I/O |
| Heapfil | Usortert lenkeliste av blokker | Sammen med en eller flere indekser |
| Statisk hash | h(K) mod N → blokk | Faste datamengder, kun likhet |
| Extendible hash | directory + lokal/global depth | Voksende datamengder, kun likhet |
Nå vet du hvordan en rad ligger i en blokk og hvordan blokker går mellom RAM og disk. Neste skritt: B+-trær — den indeksstrukturen som gjør raske søk og range-spørringer mulige på tabeller med milliarder rader.
Det studenter blander sammen i lagringskapittelet
Disse er ikke konseptfeil — de er presisjonsfeil. Les listen etter du har lest 01–09 over; den viser hvor presisjonen må sitte når du leser et MCQ-alternativ.
- BlockId vs RID. BlockId = (deviceId, offset) peker på en blokk. RID = (BlockId, slot) peker på én rad inne i blokken. Indekser bruker RID; buffer pool bruker BlockId. Bratsberg-eksamene oppgir alltid begge byte-størrelser separat — sjekk hvilken som hører hjemme i utregningen din.
- Slot directory ≠ indeks. Slot directory er internt i én blokk og oversetter slot-nummer til byte-offset i samme blokk. En B+-tre-indeks er en separat struktur som peker på rader på tvers av blokker. Begge bruker «directory»-ordet, men gjør helt forskjellige jobber.
- Pinning er ikke låsing. Pinning hindrer at en blokk kastes ut av buffer pool, men hindrer ikke at andre transaksjoner endrer dataen i blokken. Lock manager er en helt separat komponent (kap 8). En blokk kan være pinnet uten å være låst, og omvendt.
- WAL-regelen: logg ut før datablokk. Når en dirty blokk evictes, må alle tilhørende loggposter (opp til blokkens høyeste LSN) være tvunget til disk først. Hvis du svarer at «buffer pool kan skrive datablokken når som helst» — feil. Loggens skriving må komme først.
- LRU er ikke alltid best. Standardvalget er LRU/Clock, men ved store table scans «forurenser» de bufferet med blokker som aldri leses igjen. MRU er bedre der. Bratsberg-eksamen tester gjerne nettopp dette mønsteret: «hvilken strategi er optimal når DBMS-en gjør en full scan av en stor tabell?» — svar: MRU.
- Statisk hashing skalerer ikke. Når du vurderer tradeoffs mellom statisk og extendible hashing: statisk har fast antall bøtter ved opprettelse. Vokser dataene, fylles bøttene og overflow-kjeder oppstår — søketiden går fra O(1) til O(N/N₀). Extendible løser dette ved å la directory-en vokse.
- Hashing støtter kun likhet. Hash-funksjonen sprer nøkler tilfeldig — naboer i nøkkelrommet ender på vidt forskjellige bøtter.
WHERE empno BETWEEN 100 AND 200krever full scan i hash-fila. Hvis spørringene har ranges, må du velge B+-tre. - Heapfil + indeks ≠ clustered. En heap med en B+-tre-indeks ved siden av er unclustered. Bare når radene er fysisk sortert etter indeksnøkkelen og bladene er data-blokkene, har du en clustered indeks. Maks én clustered per tabell.
- Flip/flop-stempel beskytter mot torn write, ikke krasj. Stempelet sjekker at hele blokken ble skrevet ferdig fysisk. Det gjenoppretter ikke innholdet — det varsler bare at blokken er korrupt. Det er recovery via WAL (kap 8) som faktisk gjenoppretter dataene.
- Local depth ≤ global depth, alltid. Et klassisk extendible-hashing-spørsmål: hva skjer ved en split? Hvis local depth (på blokken) < global depth (på directory), splitter vi bare blokken og oppdaterer to directory-pekere — ingen directory-vekst. Hvis local depth = global depth, må directory dobles før split. Bytt om disse to og du svarer feil.
Klarer du å forklare alle ti på 30 sekunder hver? Da er du i god form på 6A. Hvis ikke — gå tilbake til seksjonen ved siden av nummeret og les den én gang til.
MCQ — i samme stil som eksamen i år
Eksamen i 2026 er kun flervalg. Disse spørsmålene er konstruert med distraktorer som matcher de vanligste feilene fra Bratsberg-eksamenene. Klikk det alternativet du tror er riktig — du får forklaring etter første feil.
SELECT * FROM Employee WHERE empno = ? hvis empno ikke er indeksert og er unik?Klarte du flesteparten? Bra. Hvis flere enn to bommet — gå tilbake til den tilsvarende seksjonen før du går videre til 6B (B+-trær).