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.
Komponentene i en SQL-DBMS
Et DBMS 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.
- 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 I/O-kostnad basert på katalogstatistikk.
- 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) eller en stor fil i filsystemet. DBMS-en ser den som en lang vektor av blokker, indeksert med 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 blokk-erasure (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 → 224 blokker per device. Med 16 KiB blokker blir det 256 GiB per device. 8-byte BlockIds spenner over alt vi trenger.
Tre måter å pakke en rad
Når en tabell har kun fixed-length attributter (INT, CHAR(N), DATE), er hvert felt på fast offset. Da er decoding triviell. Men så kommer VARCHAR…
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 der man jobber på enkelt-rader. 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?
- 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 hvor halve blokken har gammel data og halve har 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: én gang en blokk er lest inn, kan flere transaksjoner og spørringer dele den. Hash-indeksert på BlockId for O(1) lookup.
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 et offer via erstatningsstrategi (LRU/MRU/clock). Offerets pin-count må være 0.
- Hvis offeret er dirty, skriv først ut til disk (gjerne via WAL).
- 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.
OS-en har sin egen page cache, men den vet ikke at en sekvensiell scan forurenser cachen, eller at en hot indeks-rotnode bør pinnes. DBMS-en kjenner semantikken og kan prefetche, pinne, og force-skrive 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: 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?mod N. For å finne alle i intervallet må vi enten hashe hver mulig verdi (101 oppslag) eller skanne hele filen. B+-trær bevarer sortering og er mye bedre her.Hashing som vokser elegant
Extendible hashing introduserer et lag mellom hash-verdien og blokken — en directory. Når en blokk fylles, dobles directory-en (eller bare blokken splittes), uten å rehasher hele filen.
Hovedideen — global vs. local depth
- Global depth (d): hvor mange bits av hashen som directory-en bruker for å indeksere.
- Local depth (d'): hvor mange bits hver enkelt blokk faktisk skiller på.
- 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.
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.
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.