Dimensjonsmodellering i praksis
Fakta, dimensjoner, granularitet, nøkler og historikk, vist med ett eksempel fra en norsk nettbutikk, og feilene som oftest gir feil tall.
En operasjonell database er laget for å registrere hendelser: en kunde legger inn en ordre, et lager trekker fra en vare. En analytisk modell skal svare på spørsmål om de samme hendelsene: hvor mye solgte vi i Bergen i mars, og hvilke produktkategorier vokser? Dimensjonsmodellering er en måte å bygge den analytiske modellen på, slik at slike spørsmål blir enkle å stille og vanskelige å besvare feil.
Begrepene i denne artikkelen kommer fra Ralph Kimball, som beskrev metoden i boken The Data Warehouse Toolkit. Eksempelet går gjennom hele artikkelen: en norsk nettbutikk som vil analysere salget sitt.
Fakta og dimensjoner
En dimensjonsmodell består av to slags tabeller.
- Faktatabeller inneholder målbare hendelser. Hver rad er noe som skjedde, med tallene som beskriver det: antall, beløp, rabatt.
- Dimensjonstabeller inneholder konteksten hendelsene beskrives med. Hvem kjøpte, hva ble kjøpt, og når. Det er kolonnene i dimensjonene folk filtrerer og grupperer på: fylke, produktkategori, måned.
Når faktatabellen står i midten og dimensjonene rundt, kalles mønsteret et stjerneskjema. For nettbutikken blir det fakta_ordrelinje i midten, med dim_kunde, dim_produkt og dim_dato rundt. Stjerneskjemaet ligger typisk i gullaget, bygget på den felles datamodellen i sølvlaget.
Et nyttig skille: tall du vil summere, hører hjemme i faktatabellen. Tekst og kategorier du vil filtrere på, hører hjemme i en dimensjon.
Bestem granulariteten først
Før du velger én eneste kolonne, må du bestemme hva én rad i faktatabellen betyr. Det kalles granulariteten, eller kornet. For nettbutikken er det naturlige valget:
Én rad per ordrelinje, altså ett produkt i én ordre.
Valget styrer alt annet. Med én rad per ordrelinje kan du knytte hver rad til ett produkt, og du kan regne omsetning per produkt. Hadde du valgt én rad per ordre, ville produktet ikke hatt noen plass i faktatabellen, fordi en ordre kan ha mange produkter.
Det lønner seg som regel å velge den laveste granulariteten kildedataene har. Fra ordrelinjer kan du alltid summere opp til ordre, dag eller måned. Fra månedstall kan du aldri gå tilbake til enkeltlinjene.
Skriv granulariteten ned i beskrivelsen av tabellen. Den er det første en ny analytiker trenger å vite.
Eksempelet: en stjerne for ordrelinjer
Her er faktatabellen. Den inneholder nøkler til dimensjonene, et par kolonner som identifiserer ordren, og målene.
fakta_ordrelinje: én rad per ordrelinje.
| Kolonne | Type | Beskrivelse |
|---|---|---|
kunde_nokkel |
bigint | Surrogatnøkkel til dim_kunde, den versjonen av kunden som gjaldt da ordren ble lagt inn. |
produkt_nokkel |
bigint | Surrogatnøkkel til dim_produkt. |
dato_nokkel |
integer | Nøkkel til dim_dato for ordredatoen, på formen ÅÅÅÅMMDD. |
ordre_nr |
varchar | Ordrenummeret fra nettbutikken. Ingen egen dimensjon, men nyttig for å finne igjen ordren. |
linje_nr |
integer | Linjens nummer i ordren. Sammen med ordre_nr identifiserer den raden. |
antall |
integer | Antall enheter av produktet på linjen. |
enhetspris |
numeric(12,2) | Pris per enhet i kroner, uten merverdiavgift, før rabatt. |
rabatt |
numeric(12,2) | Samlet rabatt på linjen i kroner, uten merverdiavgift. |
belop |
numeric(12,2) | antall × enhetspris − rabatt, i kroner uten merverdiavgift. |
Legg merke til at beskrivelsen av belop sier både hvordan det regnes ut og om merverdiavgift er med. Det er den typen avgjørelse som ellers blir tatt på nytt i hver rapport. Mer om hva beskrivelsene bør inneholde, står i Fra forretningsbegrep til kolonne.
ordre_nr står direkte i faktatabellen uten en egen dimensjon. Det kalles en degenerert dimensjon: en identifikator som er nyttig å filtrere på, men som ikke har noen egne egenskaper å beskrive.
dim_kunde: én rad per versjon av en kunde.
| Kolonne | Type | Beskrivelse |
|---|---|---|
kunde_nokkel |
bigint | Surrogatnøkkel, laget i dataplattformen. Ny verdi for hver versjon av kunden. |
kunde_id |
varchar | Kundenummeret fra nettbutikken (den naturlige nøkkelen). |
navn |
varchar | Kundens navn. |
postnummer |
char(4) | Postnummeret i leveringsadressen. |
kommune |
varchar | Kommunen postnummeret hører til. |
fylke |
varchar | Fylket kommunen hører til. |
gyldig_fra |
date | Første dag denne versjonen gjaldt. |
gyldig_til |
date | Siste dag denne versjonen gjaldt. 9999-12-31 for gjeldende versjon. |
er_gjeldende |
boolean | Sann for den versjonen som gjelder nå. |
dim_produkt: én rad per produkt.
| Kolonne | Type | Beskrivelse |
|---|---|---|
produkt_nokkel |
bigint | Surrogatnøkkel, laget i dataplattformen. |
produkt_id |
varchar | Varenummeret fra nettbutikken. |
navn |
varchar | Produktnavnet slik det vises i butikken. |
kategori |
varchar | Hovedkategori, for eksempel «Kjøkken». |
underkategori |
varchar | Underkategori, for eksempel «Kaffetraktere». |
merke |
varchar | Produsentens merkenavn. |
dim_dato: én rad per kalenderdag.
| Kolonne | Type | Beskrivelse |
|---|---|---|
dato_nokkel |
integer | Datoen på formen ÅÅÅÅMMDD, for eksempel 20261021. |
dato |
date | Selve datoen. |
ukedag |
varchar | Ukedagen på norsk, for eksempel «onsdag». |
uke |
integer | Ukenummer etter ISO 8601. |
maned |
integer | Månedsnummer, 1–12. |
kvartal |
integer | Kvartal, 1–4. |
ar |
integer | Årstall. |
er_helligdag |
boolean | Sann for norske offentlige helligdager. |
Med disse fire tabellene kan analytikeren svare på «omsetning per fylke og kvartal for kategorien Kjøkken» med én spørring: faktatabellen koblet til tre dimensjoner, filtrert og gruppert på kolonner i dimensjonene, og summert over belop.
Naturlige nøkler og surrogatnøkler
Kildesystemet har allerede nøkler: kundenummer, varenummer, ordrenummer. Det er naturlige nøkler. Dimensjonene tar vare på dem, i kunde_id og produkt_id, slik at det går an å spore en rad tilbake til kilden.
Likevel peker faktatabellen på dimensjonene med surrogatnøkler, tall som dataplattformen lager selv. Det er tre grunner til det:
- Historikk. Hvis en kunde har flere versjoner i
dim_kunde, er ikkekunde_idlenger unik. Surrogatnøkkelen skiller versjonene, og faktaraden peker på akkurat den versjonen som gjaldt da ordren ble lagt inn. - Uavhengighet fra kilden. Hvis nettbutikken bytter system og kundenumrene endres, eller to kilder har overlappende numre, påvirker det bare dimensjonen, ikke alle radene i faktatabellen.
- Ukjente verdier. En ordrelinje uten kjent kunde kan peke på en egen rad i
dim_kundefor «ukjent kunde», i stedet for å ha en tom nøkkel som forsvinner i en vanlig join.
Datodimensjonen er et vanlig unntak: der brukes ofte selve datoen som nøkkel, fordi en kalenderdag ikke endrer seg.
Historikk: når kunden flytter
Kunde 10427 bor i Bergen og har handlet der i to år. I august flytter kunden til Trondheim og oppdaterer adressen i nettbutikken. Hva skal skje med dim_kunde? Det er spørsmålet SCD, slowly changing dimensions, handler om. De tre vanligste typene svarer forskjellig.
Type 1 overskriver. Raden oppdateres med den nye adressen. Det er enkelt, men all historikk forsvinner: alle ordrene fra Bergen ser nå ut som om de kom fra Trondheim, og omsetningen per fylke for i fjor endrer seg i etterkant. Type 1 passer for rettelser, som et feilstavet navn, der den gamle verdien aldri var riktig.
Type 2 legger til en ny rad. Den gamle raden får gyldig_til satt til dagen før flyttingen og er_gjeldende satt til usann. En ny rad med ny surrogatnøkkel får den nye adressen:
| kunde_nokkel | kunde_id | kommune | gyldig_fra | gyldig_til | er_gjeldende |
|---|---|---|---|---|---|
| 5810 | 10427 | Bergen | 2024-06-03 | 2026-08-14 | usann |
| 9132 | 10427 | Trondheim | 2026-08-15 | 9999-12-31 | sann |
Ordrene fra før flyttingen peker på 5810, de nye på 9132. Omsetningen for Bergen i fjor står fast, og du kan fortsatt telle kunden én gang ved å gruppere på kunde_id. Det er derfor surrogatnøkkelen trengs.
Type 3 tar vare på forrige verdi i en egen kolonne. dim_kunde får en kolonne forrige_kommune ved siden av kommune. Det gir mulighet til å sammenligne «før» og «etter» for én endring, men ikke mer: flytter kunden igjen, er Bergen borte. Type 3 brukes mest der en endring skjer på én gang for mange rader, for eksempel når en salgsregion deles opp.
Du velger type per kolonne, ikke per tabell. I dim_kunde kan adressen være type 2 mens navnet er type 1. Skriv valget inn i beskrivelsen av kolonnen.
Vanlige feil
Blandet granularitet i én faktatabell. Noen legger fraktkostnaden, som gjelder hele ordren, inn i fakta_ordrelinje. Da står den på hver linje, og summen av frakt blir like mange ganger for stor som ordren har linjer. Mål på ordrenivå hører hjemme i en egen faktatabell med én rad per ordre, eller må fordeles på linjene etter en regel som står i beskrivelsen.
Fakta lagret i dimensjoner. En kolonne total_omsetning i dim_kunde ser praktisk ut, men den er et aggregat som er riktig bare på det tidspunktet den ble regnet ut, og den kan ikke filtreres på periode. Tall som skal summeres, hører hjemme i faktatabellen.
Ingen datodimensjon. Uten dim_dato må hver rapport regne ut uke, kvartal og helligdager fra en dato, og ulike rapporter gjør det ulikt. Ukenummer rundt nyttår er et kjent eksempel. En datodimensjon gjør det én gang.
Dobbelttelling når to faktatabeller kobles. Nettbutikken har også fakta_retur med én rad per returnert vare. Kobler du fakta_ordrelinje og fakta_retur direkte på kunde, blir hver ordrelinje gjentatt for hver retur kunden har, og begge summene blir for store. Summer hver faktatabell for seg på den felles granulariteten, for eksempel kunde og måned, og koble resultatene etterpå. Det fungerer fordi begge bruker de samme dimensjonene.
Unødvendig snøfnugging. Snøfnugging betyr å dele en dimensjon opp i flere normaliserte tabeller, for eksempel dim_produkt → dim_underkategori → dim_kategori. Det sparer litt lagringsplass, men hver spørring trenger flere joins, og modellen blir vanskeligere å lese. I de fleste tilfeller er det enklere å ha kategori og underkategori som kolonner i dim_produkt, slik eksempelet over gjør.
Hvor du kan begynne
Du trenger ikke modellere hele virksomheten på én gang. Kimball beskriver fire steg for hver stjerne:
- Velg forretningsprosessen. Ta en prosess som noen allerede stiller spørsmål om, for nettbutikken salg.
- Bestem granulariteten. Skriv den ned: én rad per ordrelinje.
- Finn dimensjonene. Velg de få dimensjonene spørsmålene faktisk bruker, typisk hvem, hva og når. Bestem for hver kolonne i dimensjonene om endringer skal overskrives eller tas vare på.
- Finn faktaene. Ta med målene som hører til granulariteten: antall, enhetspris, rabatt og beløp.
Når den første stjernen er i bruk, kan neste prosess, for eksempel returer, gjenbruke dim_kunde, dim_produkt og dim_dato som felles (konforme) dimensjoner. Slik vokser modellen én faktatabell om gangen, med dimensjoner som betyr det samme overalt. Beskriver du tabellene med eier, betydning og kvalitetskrav, er du også på vei mot en datakontrakt for dem.
Kilder
- Kimball Group, Dimensional Modeling Techniques (Ralph Kimball og The Data Warehouse Toolkit)
- Kimball Group, Four-Step Dimensional Design Process (de fire stegene)
- Kimball Group, Grain (granularitet, laveste nivå, ingen blandet granularitet)
- Kimball Group, Degenerate Dimensions (ordrenummeret i faktatabellen)
- Kimball Group, Dimension Surrogate Keys (surrogatnøkler og unntaket for datoer)
- Kimball Group, Nulls in Fact Tables (egen rad for ukjent verdi)
- Kimball Group, Calendar Date Dimensions (nøkkel på formen ÅÅÅÅMMDD, uke og helligdager)
- Kimball Group, Type 1: Overwrite
- Kimball Group, Type 2: Add New Row
- Kimball Group, Type 3: Add New Attribute
- Kimball Group, Drilling Across (summere hver faktatabell for seg på felles dimensjoner)
- Kimball Group, Conformed Dimensions (felles dimensjoner)
- Kimball Group, Snowflaked Dimension (hvorfor snøfnugging bør unngås)