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.
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.
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: SelectStmt → SelectList + FromClause + WhereClause. Mislykkes det, kastes en syntaksfeil — og reisen er over allerede her.
Token-strømmen
AST
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.
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:
- Navneoppløsning — kvalifiser
nametilEmployee.name, slå opp tabell-OID-er. - Typesjekk — er
depno = 4meningsfullt? Er4en INT som kan sammenliknes meddepno? - 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
| Tabell | Kolonne | Type | Indeks |
|---|---|---|---|
| Employee | empno | INT | PK · clustered B+-tree |
| Employee | name | VARCHAR(60) | — |
| Employee | depno | INT | unclustered B+-tree (depno, age) |
| Employee | age | INT | (del av sammensatt) |
| Employee | salary | NUMERIC(10,2) | — |
| Department | dno | INT | PK · clustered |
π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.
(depno, age). Vår filtrering er depno = 4 AND age > 50. Hvorfor er rekkefølgen i indeksen viktig?(age, depno) hadde vi måttet scanne alle aldre >50 og filtrert depno separat — mye mer arbeid.
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 blokkerPlan 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?
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.
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
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.
READ COMMITTED. Hvilken lås- og log-aktivitet er ikke påkrevd?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
* markerer skitne frames.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.
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
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.
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.
PageLSN = 8421. Log-bufferet sin FlushedLSN = 8200. Hva må DBMS-en gjøre før blokken kan skrives til disk?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.
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.
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:
- Et commit-record skrives til log-bufferet.
- Log-bufferet force-flushes til disk (durability!).
- Lock manager slipper alle låser (Strict 2PL).
- Tuples som ennå er igjen i prosjeksjons-pipelinen sendes ut.
Resultatet streamer tilbake
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.
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.
Algebra-treet
π, σ, ⨝ er språket optimizeren tenker i. Repetér de algebraiske ekvivalensene.
Buffer & blokker
Hash-indeksen, eviction, flip/flop. Hva som faktisk skjer i RAM.
B+-trær
Splitting, høyde, clustered vs. unclustered. Hjertet av indekseringen.
Query processing
Aksessmetoder, kostnadsestimering, sortering, join-algoritmer.
Samtidighet
2PL, MVCC, isolation levels og deadlock-håndtering.
Recovery
WAL, ARIES, CLR, DPT. Hvordan databasen reiser seg etter krasj.
Avsluttende sjekk
… 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.