Interaktiv reise · Hele kurset i én SELECT

Spørringens reise gjennom DBMS-en

Vi følger én SELECT helt fra TCP-pakken kommer inn til siste rad er sendt og commit-recorden er på disk. Hver stasjon kobler kursets begreper til hva systemet faktisk gjør.

Demo-spørring vi følger gjennom hele reisen
SELECT name, salary
FROM   Employee
WHERE  depno = 4 AND age > 50;

Tabellen Employee har 100 000 rader, hver post er 80 byte, blokker er 4 KiB, og det finnes en clustered B+-treeempno samt en uclustered B+-tree(depno, age). Eksempelet er hentet fra Bratsbergs notater kap. 12.

Stasjon 01 · Forelesning 5 + Bratsberg kap. 1

Klient sender SQL-tekst til serveren

Reisen starter et helt annet sted enn der du tror at databasen «lever». Klienten — kanskje en JDBC-driver, kanskje psql, kanskje webserveren din — pakker SQL-strengen inn i en TCP-pakke og sender den til DBMS-prosessens session manager. Før noen som helst spørringsbehandling kan skje, må forbindelsen autentiseres og en sesjonskontekst etableres: hvem du er, hvilke roller du har, hvilken default-database og hvilket isolasjonsnivå du opererer på.

Den faktiske spørringen er bare en byte-streng på dette tidspunktet. DBMS-en har ingen anelse om hva Employee betyr, eller om name er gyldig. Det blir avklart i de neste stasjonene.

Klient JDBC / psql TCP · port 5432 · TLS DBMS Session mgr SQL
SQL-streng ferdes som bytes. Først bestemmes hvem som spør, deretter hva.
Hvorfor session-state betyr noe

Default schema, isolasjonsnivå, og SET-variabler er sesjons-bundet. To brukere som sender nøyaktig samme SQL kan derfor få ulike planer og ulik concurrency — fordi det implisitt er forskjellig.

Sjekkpunkt 1.1
Hva har DBMS-en lov til å anta om SQL-teksten i det den ankommer session manager?
Riktig: B. Parser sjekker syntaks (stasjon 02), katalogen sjekker navn og typer (stasjon 03), og autorisasjonen verifiseres når katalogen slås opp. På bytes-nivå har vi bare en streng som kan være SQL.
Stasjon 02 · SQL-kompilator

Parser tokeniserer og bygger et AST

SQL-kompilatoren har to oppgaver: tokenisere strengen og parse token-strømmen til et abstrakt syntakstre (AST). Tokenizeren skiller nøkkelord fra identifikatorer fra litteraler. Hver token vet sin posisjon — nyttig for feilmeldinger.

Parseren forsøker å matche tokens mot SQL-grammatikken. Lykkes det, har vi et tre der hvert nivå representerer en grammatikkregel: SelectStmtSelectList + FromClause + WhereClause. Mislykkes det, kastes en syntaksfeil — og reisen er over allerede her.

Token-strømmen

SELECT name , salary FROM Employee WHERE depno = 4 AND age > 50

AST

Select Proj From Where name salary Employee depno=4 age>50
Token-strømmen pakkes inn i et tre der hver node er en grammatikkregel. Treet er fortsatt ren syntaks — ingen semantikk.
Forskjell på AST og algebra-tre

AST-et følger SQL-syntaksen tett. Algebra-treet (stasjon 03) bruker relasjonsoperatorer fra kapittel 2: σ (select/filter), π (project), ⨝ (join). Det er algebra-treet optimizeren omformer.

Sjekkpunkt 2.1
Hvilken type feil oppdages typisk i parser-stadiet?
Riktig: C. Parseren vet bare hva grammatikken krever. Eksistens av tabeller (A) og type-sjekk (B) gjøres mot katalogen i neste stasjon. Manglende indeks (D) er optimizerens beslutning, ikke en feil.
Stasjon 03 · SQL-katalog

Katalog gir AST-et mening

Et AST har formen riktig — men er Employee en tabell? Finnes name som kolonne? Hva er typen til depno? Slike spørsmål besvares av SQL-katalogen: en samling systemtabeller som beskriver alle tabeller, kolonner, indekser, constraints og rettigheter. Katalogen er selv en database, og slås opp på samme måte som hvilken som helst annen tabell.

Tre ting skjer her parallelt:

  1. Navneoppløsning — kvalifiser name til Employee.name, slå opp tabell-OID-er.
  2. Typesjekk — er depno = 4 meningsfullt? Er 4 en INT som kan sammenliknes med depno?
  3. Privilegiekontroll — har brukeren SELECT-rett på Employee?

Resultatet er at AST-et omformes til et algebra-tre der nodene er relasjonsoperatorer fra kap. 2.

Et lite utdrag av katalogen

TabellKolonneTypeIndeks
EmployeeempnoINTPK · clustered B+-tree
EmployeenameVARCHAR(60)
EmployeedepnoINTunclustered B+-tree (depno, age)
EmployeeageINT(del av sammensatt)
EmployeesalaryNUMERIC(10,2)
DepartmentdnoINTPK · clustered
Statistikk-felter ligger også her: antall rader, antall blokker, treet sin høyde, distinkt-count per kolonne. Disse er gull verdt i neste stasjon.
Algebra-treet etter katalog-oppslag
πname, salary ( σdepno=4 ∧ age>50 ( Employee ) )
Sammensatt nøkkel
Indeksen (depno, age) sorterer leksikografisk. Først på depno, deretter age. Selektivitetsregelen: mest selektive felt først.
Statistikk
Katalogen lagrer høyde på B+-tre, antall blokker, min/maks-nøkkel. Optimizer trenger dette for å estimere kostnad.
Privilegier
GRANT/REVOKE legges også i katalogen og sjekkes her — før spørringen rør data.
Sjekkpunkt 3.1
Den sammensatte indeksen er definert som (depno, age). Vår filtrering er depno = 4 AND age > 50. Hvorfor er rekkefølgen i indeksen viktig?
Riktig: B. Sammensatte nøkler er sortert leksikografisk. Med (age, depno) hadde vi måttet scanne alle aldre >50 og filtrert depno separat — mye mer arbeid.
Stasjon 04 · Optimizer

Optimizer enumererer planer og velger den billigste

Algebra-treet er logisk — det sier hva, ikke hvordan. Optimizeren genererer mange likeverdige varianter (algebraiske ekvivalenser: dytt σ ned forbi join, omorganiser join-rekkefølge, …) og knytter fysiske operatorer til løvnodene: full table scan, index scan, hash join, sort-merge join. For hver kandidat estimerer den I/O-kostnaden ved hjelp av statistikk fra katalogen. Plan-treet med lavest forventet kostnad vinner.

To kandidatplaner for vår spørring

Plan A · Full heap scan

πname,salary
└─ σdepno=4 ∧ age>50
   └─ TableScan(Employee)

Les alle 1 961 blokker, filtrer i minnet.

~1 961 blokker

Plan B · Index scan på (depno, age)

πname,salary
└─ Fetch(RID)
   └─ IndexScan
       depno=4 ∧ age>50

3 nivåer ned i B+-tre + ~12 blads-blokker + RID-fetches. Vinner ved lav selektivitet.

~120 blokker (~6 % av A)

Lek deg med kostnadsmodellen

Anta heap-fil + uclustered B+-tre. Plan A skanner hele heap-filen (1 961 blokker). Plan B traverserer 2 indeks-blokker ned, scanner sideveis i bladet, og følger så en RID-peker per matchende rad. Når blir Plan B billigst?

0.5 %
…regner…
Hvorfor heter det «black art»?

Plan-rommet vokser eksponentielt med antall joins. Optimizeren bruker heuristikker (push σ ned, vurder bare left-deep trær, …) og dynamisk programmering (System R-stil) for å finne en god — ikke alltid optimal — plan på rimelig tid.

Sjekkpunkt 4.1
Hvorfor blir Plan B (index scan) plutselig dyrere enn Plan A (heap scan) når selektiviteten er høy?
Riktig: D. Bratsbergs eksempel: 20 % treff av 100 000 = 20 000 RID-pekere → potensielt 20 000+ blokk-lesinger, mot 1 961 ved full scan. Dette er hvorfor optimizere noen ganger «velger» full scan.
Stasjon 05 · Lock manager + log writer

Executor begynner å kjøre — men ber først om låser

Når den fysiske planen er valgt, tar executor over. Før den faktisk leser eller skriver, må to subsystemer engasjeres: lock manager (eller MVCC-snapshot) for isolasjon, og log writer for atomisitet og durability.

Akkurat hvilke låser som tas avhenger av isolasjonsnivå. Ved Strict 2PL og READ COMMITTED tas typisk en shared (S) lås per leste rad eller blokk, som slippes når raden er konsumert. Ved REPEATABLE READ holdes S-låsene til commit. Ved en UPDATE/INSERT/DELETE tas exclusive (X) lås, og den holdes alltid til commit (Strict 2PL).

Lock-tabellen oppdaterer seg

T17 · Employee.depno=4S-lock
T17 · idx(depno,age) page 42S-lock
T17 · log: BEGINwritten
Hver transaksjon har en peker fra trans-tabellen til en kjede av låser. Konflikter holder etterspørrere i kø til frigjøring.
WAL — write-ahead logging

Log-recordene må treffe disk før tilhørende dataside flushes. På commit force-skrives logbufferet (forced write). Dette er hva som gjør «D» i ACID til mer enn et håp.

Strict 2PL
X-låser holdes til commit/abort. Garanterer recoverable og cascadeless schedules. les mer
MVCC
Lesere får en snapshot av forrige committede versjon, skrivere lager ny versjon. Lesere blokkerer aldri skrivere.
Begin-record
Markerer transaksjonens start i loggen. Får et LSN — nummeret til alle senere log-recordene for samme transaksjon kjedes til denne via PrevLSN.
Sjekkpunkt 5.1
En SELECT (vår demo-spørring) kjører under READ COMMITTED. Hvilken lås- og log-aktivitet er ikke påkrevd?
Riktig: A. Ren lesing skriver ingen log-records. Force-log-at-commit gjelder skrivere, fordi det er der vi trenger durability. Lesere tar S-låser men logger ingenting.
Stasjon 06 · Database-buffer

Buffer-oppslag: hit eller miss?

For å lese en blokk spør executor først bufferet: «har du blokk N?». Bufferet er en stor del av RAM, organisert som frames som hver kan holde én blokk. Et hash-indeks på BlockId gjør oppslaget O(1) på snittet.

  • Hit — frame-pekeren returneres umiddelbart. Ingen disk-I/O.
  • Miss — frame må allokeres. Hvis bufferet er fullt, må en frame eviktes — og hvis den er skitten (modifisert), må den først flushes til disk.

Bufferet vårt — 16 frames

F0
B42
B11
F3
B93
B217?
B3
F7
B58
F9
B71
B120
B6
F13
B44
F15
Lyseblå = okkupert. Pulsende = blokken vi leter etter (B217). Tom = ledig frame klar til å fylles. * markerer skitne frames.
Pinning

Mens en operator bruker en frame må den «pinnes» slik at bufferet ikke evikter den under bruk. Pinned frames hopper over ved eviction-valg.

Erstatningsstrategier

  • LRU — least recently used. Klassiker, men tabell-scans kan «forgifte» hele bufferet.
  • MRU — most recently used. Bedre for repetisjons-mønstre.
  • Clock — billig approksimasjon av LRU. En referanse-bit roterer.
Sjekkpunkt 6.1
Hvorfor sier vi at bufferet er DBMS-ets «hemmelige våpen»?
Riktig: C. Disk er størrelser tregere enn RAM. Et godt buffer hit-rate (90 %+) er forskjellen på en database som svarer på millisekunder og en som svarer på sekunder.
Stasjon 07 · Disk-I/O

Når bufferet bommer går vi til disk

Buffer miss → eviction-policy plukker en kandidat-frame. Hvis frame er skitten flushes den til disk først (skrive-I/O). Deretter slås BlockId opp: deviceId + offset gir disken et fysisk eller logisk sted å lese fra. Blokken leses inn i frame, hash-indeksen oppdateres, og alle som ventet på den får signal.

Anatomi av en blokk-lesing

Disk · deviceId=1 B217 Buffer-frame B217 · pinned PageLSN · header · slots
Først evict en eldre frame (kanskje skitten — flush!), så seek/les blokken. Verifiser flip/flop-stempel: matcher start- og sluttall? Hvis ikke har vi en halvskrevet blokk.

Hva sjekker DBMS-en på blokken den nettopp leste?

  • Flip/flop-stempel ved start og slutt — atomisitet.
  • Checksum — bit rot fra disken?
  • PageLSN — hvilket log-record sist endret denne blokken? Avgjørende ved recovery.
Hvorfor 16 KiB?

Større blokker amortiserer seek-time bedre, men gir høyere kontensjon (flere rader låses sammen). 4–32 KiB er typisk. SSD-er endrer disse trade-offene fordi det ikke finnes seek-time, men write amplification dukker opp i stedet.

Sjekkpunkt 7.1
En frame som skal eviktes har PageLSN = 8421. Log-bufferet sin FlushedLSN = 8200. Hva må DBMS-en gjøre før blokken kan skrives til disk?
Riktig: B. WAL-regelen: log skrives før dataside. Hadde vi skrevet blokken først og krasjet, ville vi ikke kunne undo dens uncommitted endringer fordi undo-loggen ennå ikke var på disk.
Stasjon 08 · B+-tre-traversering

Topp-ned søk + sideveis range scan

Med blokken i buffer kan vi endelig traversere. Optimizeren valgte index scan på (depno, age). Vi starter ved roten, binærsøker lokalt etter «første nøkkel der depno=4», følger barn-pekeren ned, gjentar til vi treffer bladet. Der scanner vi sideveis (blader er lenket sammen) til depno > 4.

2 | 14 | 27 2 | 5 14 | 22 27 | 33 2,3,5 7,11,13 14,16,21 22,24 27,33
Søk: rot → midt-internnode → blad. Range scan følger leaf-link videre. Lysebrun = besøkt, grønn = treff.
Hvorfor sideveis lenker?

Range-spørringer som WHERE age > 50 er hovedgrunnen til at B+-trær slo B-trær. Når du står på første blad som matcher, koster sekvensiell scan svært lite — du følger en peker.

Fanout
Antall barnpekere per node. Med 4 KiB blokker og 12-byte (key, BlockId)-poster: ≈ 227 ved 2/3 fyllgrad.
Høyde
For 100 mill rader, fanout 200, fyllgrad 2/3: ⌈log200(100M)⌉ ≈ 4 nivåer. Det er 4 disk-aksesser i verste fall.
Clustered
Bladene er radene. Ekstremrask single-key og range. PRIMARY KEY i InnoDB.
Unclustered
Bladene har (key, RID). Ekstra hopp til heap-fil for selve raden — derav «for mange treff» kostnaden i stasjon 4.
Sjekkpunkt 8.1
Hva «vokser» når et B+-tre vokser i høyde?
Riktig: C. Splittinger propagerer oppover. Bare når roten selv må splittes vokser treet i høyde — derfor er B+-trær alltid balanserte. Se mer i kap. 6B.
Stasjon 09 · Resultat & commit

Tupler streames tilbake — og loggen sluttføres

Executor er en pipeline. Iteratoren returnerer ett tuple om gangen («Volcano-modellen»). Klienten ser de første radene før spørringen er ferdig kjørt. På toppen sitter projeksjonen π og kaster bort kolonnene klienten ikke spurte om.

For en SELECT er det ikke noe egentlig commit å gjøre — vi har ikke endret noe. Men sesjonens auto-commit eller eksplisitte COMMIT resulterer i:

  1. Et commit-record skrives til log-bufferet.
  2. Log-bufferet force-flushes til disk (durability!).
  3. Lock manager slipper alle låser (Strict 2PL).
  4. Tuples som ennå er igjen i prosjeksjons-pipelinen sendes ut.

Resultatet streamer tilbake

name salary
─────────────────────────
Olav Hansen 742 000.00
Kari Nygaard 815 500.00
Per Berge 690 000.00
Volcano-iteratoren produserer rad nr. n mens klienten leser rad nr. n − 1.
Force-log-at-commit

Et fsync-kall på log-devicen er det dyreste øyeblikket i en SQL-transaksjon. Group commit (samle commit-records og fsync sjeldnere) er en standardteknikk for å øke gjennomstrømning.

Sjekkpunkt 9.1
Hvorfor er det at commit-recordet er på disk — ikke at data-blokkene er det — som gir durability?
Riktig: D. Dette er kjernen i ARIES: data-blokker kan ligge i buffer lenge, så lenge logg-redo-recordene er på disk og refererer dem (PageLSN). Recovery roller frem fra siste checkpoint og bruker DPT for å minimere arbeidet.
Stasjon 10 · Sammenheng

Hele bildet på én side

Du har nå sett hva hvert eneste subsystem i kurset gjør på den samme spørringen. Tabellen oppsummerer hvor du finner mer.

SQL-kompilator + katalog (stasjon 02–03) Optimizer (stasjon 04) Executor + iteratorer (stasjon 09) Lock manager (05) Buffer pool (06) Log buffer (05/09) Disk · heap-filer · B+-trær · log device (07/08)
Lagene over leverer ned, lagene under leverer opp. Hver pil i en arkitekturtegning er én av stasjonene du nettopp har gått gjennom.

Avsluttende sjekk

Helhets-quiz
Hvilken sekvens er riktig for en typisk lese-spørring?
Riktig: B. Det er denne sekvensen vi har fulgt gjennom alle 9 stasjonene. Lock og log engasjeres etter at executor har valgt plan, men før data faktisk leses.
Når du leser om en feature i framtida …

… spør deg selv: hvor i denne reisen påvirker den? «Snapshot isolation» endrer stasjon 05 og 06. «Materialiserte views» endrer stasjon 04. «Column store» endrer stasjon 06–07. Det meste handler om hvordan stasjonene henger sammen.

Quiz: 0 / 0