Star Schema & Dimensioneel Modelleren

Fact tables, dimension tables, grain, de vier soorten slowly changing dimensions met SQL-voorbeelden, en star schema versus snowflake schema — de dimensionele modelleeraanpak die achter vrijwel elk BI-dashboard zit.

12 min leestijd Gemiddeld Bijgewerkt: 2026-08-13 Data Modeling

Introductie

In Data Modeling zagen we het kernprincipe: analytische systemen denormaliseren bewust, omdat leessnelheid belangrijker is dan schrijfconsistentie zodra een tabel elke nacht wordt herbouwd in plaats van rij voor rij bijgewerkt. Dimensional modeling — ontwikkeld door Ralph Kimball in de jaren negentig en nog steeds de meest gebruikte modelleeraanpak voor BI-rapportage — is de concrete, gestandaardiseerde implementatie van dat principe. Het star schema is het resulterende patroon: één centrale feitentabel omringd door dimensietabellen, in een vorm die zowel voor mensen intuïtief te bevragen is als voor query-engines efficiënt te verwerken.

Dit hoofdstuk behandelt het patroon in de diepte die je nodig hebt om het zelf toe te passen: hoe je een fact table en zijn dimensies ontwerpt, waarom "grain" de belangrijkste beslissing is die je maakt, en hoe je omgaat met veranderende data over tijd via slowly changing dimensions.

Fact Tables en Dimension Tables

Een star schema bestaat uit precies twee soorten tabellen, met een strikt verschillende rol.

Fact tables bevatten de meetbare gebeurtenissen van je business — een verkoop, een klik, een betaling — samen met numerieke, optelbare meetwaarden (measures) zoals amount of quantity, en foreign keys naar de dimensies die die gebeurtenis beschrijven. Een fact table is meestal de grootste tabel in het schema, met miljoenen tot miljarden rijen, en groeit continu doordat nieuwe gebeurtenissen worden toegevoegd.

Dimension tables bevatten de beschrijvende context: wie, wat, waar, wanneer. dim_customer beschrijft klanten (naam, segment, regio), dim_product beschrijft producten (naam, categorie, merk), dim_date beschrijft kalenderdagen (weekdag, kwartaal, is_feestdag). Dimensietabellen zijn relatief klein (honderden tot miljoenen rijen, zelden meer) en veranderen traag in vergelijking met de fact table — vandaar de naam "slowly changing dimensions", verderop in dit hoofdstuk.

De vuistregel om onderscheid te maken: als je het kunt optellen, is het een measure in een fact table; als je het gebruikt om te filteren of te groeperen, is het een attribuut in een dimension table. amount tel je op — measure. product_category gebruik je om te groeperen — dimensieattribuut.

Het Star Schema Patroon

Een fact table voor verkooptransacties, omringd door zijn dimensies, ziet er in DDL als volgt uit:

-- Dimensies: relatief klein, beschrijvend, traag veranderend
CREATE TABLE dim_customer (
    customer_key   INT PRIMARY KEY,   -- surrogate key
    customer_id    VARCHAR(20),        -- natural key uit het bronsysteem
    customer_name  VARCHAR(100),
    segment        VARCHAR(50),
    region         VARCHAR(50)
);

CREATE TABLE dim_product (
    product_key    INT PRIMARY KEY,
    product_id     VARCHAR(20),
    product_name   VARCHAR(100),
    category       VARCHAR(50),
    brand          VARCHAR(50)
);

CREATE TABLE dim_date (
    date_key       INT PRIMARY KEY,   -- vaak als YYYYMMDD, bv. 20260813
    full_date      DATE,
    day_of_week    VARCHAR(10),
    month_name     VARCHAR(10),
    quarter        INT,
    year           INT,
    is_weekend     BOOLEAN
);

-- Fact table: groot, numeriek, foreign keys naar elke dimensie
CREATE TABLE fact_sales (
    sale_id        BIGINT PRIMARY KEY,
    customer_key   INT REFERENCES dim_customer(customer_key),
    product_key    INT REFERENCES dim_product(product_key),
    date_key       INT REFERENCES dim_date(date_key),
    quantity       INT,
    unit_price     NUMERIC(10,2),
    revenue        NUMERIC(12,2)
);

De naam "star schema" komt letterlijk van hoe dit eruitziet getekend: fact_sales in het midden, met stralen naar dim_customer, dim_product en dim_date eromheen — vergelijk dit met het genormaliseerde vijf-tabellen-schema uit Data Modeling, waar dezelfde informatie over meerdere gekoppelde niveaus verspreid zat.

Bevragen wordt hierdoor merkbaar eenvoudiger — elke dimensie is precies één join verwijderd van de fact table, nooit meer:

SELECT
    c.segment,
    p.category,
    d.month_name,
    d.year,
    SUM(f.revenue) AS total_revenue
FROM fact_sales f
JOIN dim_customer c ON c.customer_key = f.customer_key
JOIN dim_product p ON p.product_key = f.product_key
JOIN dim_date d ON d.date_key = f.date_key
GROUP BY c.segment, p.category, d.month_name, d.year;

Grain: de Belangrijkste Beslissing die je Maakt

Voordat je één kolom definieert, moet je antwoord hebben op: wat is precies één rij in de fact table? Dit heet de grain (korrelgrootte), en het is de eerste en belangrijkste ontwerpbeslissing van elk star schema — alle andere keuzes volgen eruit.

Voor fact_sales zou de grain kunnen zijn: "één rij per orderregel" (elke productregel binnen een order is een eigen rij), of "één rij per order" (de hele order samengevat in één rij), of zelfs "één rij per klant per dag" (alle orders van een klant op een dag opgeteld). Deze drie keuzes klinken vergelijkbaar maar hebben compleet verschillende consequenties: bij "per orderregel" kun je exact zien welk product in welke hoeveelheid werd verkocht; bij "per klant per dag" kun je dat niet meer terugvinden, ook niet met de meest geraffineerde query, omdat die informatie simpelweg niet is vastgelegd.

De vuistregel van Kimball: kies de laagste (fijnste) grain die praktisch haalbaar is. Je kunt altijd omhoog aggregeren van fijn naar grof in een query (SUM over orderregels naar orderniveau), maar je kunt nooit omlaag van grof naar fijn — die detailinformatie is dan onherstelbaar verloren. Een veelgemaakte, kostbare fout is een fact table op een te grove grain bouwen om "het simpeler te houden", en zes maanden later een analist tegenkomen die precies de detailinformatie nodig heeft die is weggegooid.

Soorten Fact Tables

Niet elke fact table volgt hetzelfde patroon van "één rij per transactie" — drie varianten komen steeds terug.

Transaction fact tables — één rij per discrete gebeurtenis, zoals fact_sales hierboven. Dit is de meest voorkomende en meest intuïtieve vorm.

Periodic snapshot fact tables — één rij per entiteit per vaste tijdsperiode, ongeacht of er activiteit was. Denk aan fact_account_balance_daily: elke dag, voor elk account, één rij met het saldo op dat moment — ook als er die dag geen enkele transactie was. Dit is de juiste vorm voor "standen" (voorraad, saldo, aantal actieve abonnementen) in plaats van "gebeurtenissen".

Accumulating snapshot fact tables — één rij per proces dat meerdere stadia doorloopt, waarbij de rij wordt bijgewerkt naarmate het proces vordert. Denk aan fact_order_fulfillment: één rij per order, met kolommen als ordered_date, paid_date, shipped_date, delivered_date — telkens bijgewerkt zodra dat stadium bereikt wordt. Dit is de enige fact table-vorm waarbij je bestaande rijen daadwerkelijk update in plaats van alleen toevoegt, wat het ontwerp van de laadlogica fundamenteel anders maakt dan bij de andere twee varianten.

Soorten Dimensies

Naast de standaard dimension table komen een paar specifieke patronen vaak genoeg voor om apart te benoemen.

Conformed dimensions — dezelfde dimensietabel (bijvoorbeeld dim_date of dim_customer) hergebruikt over meerdere fact tables heen, met exact dezelfde surrogate keys en attributen. Dit is wat het mogelijk maakt om fact_sales en fact_support_tickets samen te analyseren via dezelfde dim_customer — zonder conformed dimensions eindig je met inconsistente, onvergelijkbare rapportages tussen afdelingen.

Degenerate dimensions — een dimensie-achtig attribuut (zoals een ordernummer) dat in de fact table zelf blijft staan omdat het geen eigen beschrijvende attributen heeft en dus geen aparte tabel rechtvaardigt.

Junk dimensions — een verzameltabel voor een aantal losse, laag-cardinale vlaggen (is_gift_wrapped, payment_method, is_promotional) die anders elk een eigen kleine dimensietabel zouden vereisen; ze worden gebundeld tot één tabel met alle combinaties, om het aantal foreign keys in de fact table beheersbaar te houden.

Role-playing dimensions — dezelfde dimensietabel meerdere keren gebruikt in dezelfde fact table met een andere betekenis, bijvoorbeeld dim_date die zowel als order_date_key als ship_date_key in fact_order_fulfillment voorkomt. Dit vereist in SQL meestal een aparte view of alias per rol, zodat je niet twee keer dezelfde tabel met dezelfde kolomnamen hoeft te joinen.

Slowly Changing Dimensions in Detail

In Data Warehousing Fundamentals beloofden we dit onderwerp volledig te behandelen — hier is het. De vraag is simpel: wat doe je als een klant verhuist en dim_customer.region verandert van "Noord" naar "Zuid"? Er zijn vier standaardantwoorden, genummerd naar Kimball's classificatie.

SCD Type 0 — nooit wijzigen. Het attribuut wordt na de eerste keer nooit meer bijgewerkt, ook niet als de brondata verandert. Zeldzaam, maar correct voor echt onveranderlijke feiten zoals een geboortedatum.

SCD Type 1 — overschrijven. De oude waarde wordt simpelweg vervangen door de nieuwe; er is geen historie. Elk historisch rapport dat ooit is gegenereerd, zou bij het opnieuw draaien nu de nieuwe waarde tonen alsof die altijd al zo was. Geschikt voor het corrigeren van fouten (een verkeerd gespelde naam), niet geschikt voor legitieme wijzigingen die je historisch wilt kunnen reconstrueren.

-- SCD Type 1: eenvoudige UPDATE, geen historie
UPDATE dim_customer
SET region = 'Zuid'
WHERE customer_id = 'CUST-4471';

SCD Type 2 — nieuwe rij toevoegen, historie bewaren. Dit is de meest gebruikte aanpak voor echte, betekenisvolle wijzigingen. In plaats van de bestaande rij te overschrijven, voeg je een nieuwe rij toe met een nieuwe surrogate key, en markeer je met valid_from/valid_to/is_current-kolommen welke rij op welk moment geldig was:

CREATE TABLE dim_customer_scd2 (
    customer_key   INT PRIMARY KEY,   -- nieuwe surrogate key per versie
    customer_id    VARCHAR(20),        -- natural key, herhaalt over versies
    customer_name  VARCHAR(100),
    region         VARCHAR(50),
    valid_from     DATE,
    valid_to       DATE,               -- NULL of '9999-12-31' voor de huidige versie
    is_current     BOOLEAN
);

-- Nieuwe versie toevoegen bij een wijziging: sluit de oude rij af, open een nieuwe
MERGE INTO dim_customer_scd2 AS tgt
USING staging.customer_updates AS src
ON tgt.customer_id = src.customer_id AND tgt.is_current = TRUE
WHEN MATCHED AND tgt.region <> src.region THEN
    UPDATE SET valid_to = CURRENT_DATE - 1, is_current = FALSE;

INSERT INTO dim_customer_scd2 (customer_key, customer_id, customer_name, region, valid_from, valid_to, is_current)
SELECT
    next_surrogate_key(),   -- sequence of vergelijkbaar mechanisme
    src.customer_id, src.customer_name, src.region, CURRENT_DATE, NULL, TRUE
FROM staging.customer_updates src
LEFT JOIN dim_customer_scd2 tgt
    ON tgt.customer_id = src.customer_id AND tgt.is_current = TRUE
WHERE tgt.customer_key IS NULL;   -- nieuwe klant, of net afgesloten voor een update

Met deze structuur beantwoordt SELECT * FROM fact_sales f JOIN dim_customer_scd2 c ON c.customer_key = f.customer_key altijd de regio zoals die gold op het moment van de verkoop, omdat de fact table verwijst naar de surrogate key die op dát moment actueel was — een historisch correcte join, ook nadat de klant allang is verhuisd.

SCD Type 3 — extra kolom voor de vorige waarde. Een tussenvorm: je voegt een previous_region-kolom toe naast region, zodat je precies één stap terug kunt kijken, zonder de volledige historie van Type 2. Zelden gebruikt omdat het beperkt is tot één wijziging terug, maar soms voldoende voor use cases als "vergelijk met vorig kwartaal".

Star Schema versus Snowflake Schema

Een naamgevingsverwarring die het vermelden waard is: een snowflake schema (geen relatie met het platform Snowflake) is een variant waarbij dimensietabellen zelf weer genormaliseerd worden — dim_product verwijst dan naar een aparte dim_category-tabel in plaats van category_name direct als attribuut te bevatten. Dit bespaart opslag (categorie-namen worden niet duizenden keren herhaald) maar voegt joins terug toe die het star schema nu juist probeerde te vermijden. De praktische vuistregel: houd dimensies plat (star schema) tenzij een dimensie zo groot en met zo veel herhaling is dat de opslagbesparing van normaliseren zwaarder weegt dan de extra join-kosten — in moderne kolomgeoriënteerde warehouses met sterke compressie is dat inmiddels zelden het geval.

Genereer je fact/dimension-model direct

Laat de dbt Generator een model met de juiste tests en schema.yml voor je opzetten.

Open dbt Generator

Best Practices

  • Bepaal de grain expliciet, in woorden, vóórdat je één kolom schrijft — en leg die beslissing vast in de tabel-documentatie, zodat niemand later een rij toevoegt op een andere grain dan bedoeld.
  • Kies de laagste praktisch haalbare grain. Je kunt altijd aggregeren naar boven; nooit detail terughalen dat niet is vastgelegd.
  • Gebruik conformed dimensions over fact tables heen zodra meerdere teams dezelfde entiteiten (klanten, producten) analyseren, om vergelijkbare rapportages te garanderen.
  • Reserveer SCD Type 2 voor attributen waar historische correctheid er daadwerkelijk toe doet (regio, segment, prijscategorie); gebruik Type 1 voor attributen waar alleen de actuele waarde relevant is (een telefoonnummer, een spelfout-correctie).
  • Automatiseer surrogate key-generatie consistent (sequences, hash-gebaseerde keys, of een dedicated dbt-macro) zodat SCD Type 2-logica niet per ongeluk dezelfde surrogate key hergebruikt voor twee verschillende versies van een entiteit.

Veelgemaakte Fouten

  • De grain niet expliciet vastleggen, waardoor verschillende ETL-runs of teams onbewust op verschillende granulariteit gaan laden — het klassieke recept voor de fan-out-bug uit SQL for Data Engineers.
  • SCD Type 1 gebruiken waar Type 2 nodig was, waardoor historische rapporten met terugwerkende kracht "veranderen" zodra iemand een dimensie-attribuut bijwerkt — vaak pas ontdekt als een directielid vraagt waarom het kwartaalcijfer van vorig kwartaal ineens anders is dan het destijds gepubliceerde rapport.
  • Measures in een dimensie stoppen, of beschrijvende attributen in een fact table. Als je een kolom aan het optellen bent in bijna elke query, hoort hij in de fact table; als je hem gebruikt om te filteren/groeperen, hoort hij in een dimensie.
  • Snowflaken zonder concrete reden — dimensies onnodig normaliseren "omdat het netter is", terwijl het enige resultaat extra joins is zonder meetbaar opslagvoordeel.
  • Natural keys direct in de fact table gebruiken in plaats van surrogate keys — zie Data Modeling voor waarom dat een analytisch model fragiel maakt zodra een bronsysteem verandert.

Performance Tips

  • Clusterer/partitioneer fact tables op date_key in de meeste gevallen — het is vrijwel altijd de meest gebruikte filterkolom in analytische queries, en zorgt voor effectieve partition pruning.
  • Houd dimensietabellen plat (star, niet snowflake) tenzij een specifieke dimensie zo groot is dat normaliseren een meetbaar verschil maakt — extra joins kosten bij elke query, opslagbesparing is eenmalig.
  • Beperk het aantal actieve SCD Type 2-rijen per entiteit. Een dimensie waar elke kleine wijziging (ook onbeduidende) een nieuwe versie triggert, groeit onnodig snel — bepaal per attribuut bewust of het een SCD Type 2-trigger moet zijn.
  • Materialiseer veelgebruikte fact-dimensie-combinaties als aparte, kleinere gold-tabellen (bijvoorbeeld gold.sales_by_month_category) in plaats van bij elke dashboard-load de volledige fact table te aggregeren.

Interviewvragen

"Wat is het verschil tussen een fact table en een dimension table?" Let op: fact tables bevatten meetbare, optelbare gebeurtenissen met foreign keys; dimension tables bevatten beschrijvende context — de "kun je het optellen"-vuistregel is een goed teken van begrip.

"Wat is 'grain', en waarom is het de belangrijkste beslissing bij het ontwerpen van een fact table?" Let op: het besef dat grain bepaalt welk detailniveau vastligt, dat je altijd kunt aggregeren naar boven maar nooit detail kunt terughalen dat niet is vastgelegd, en de vuistregel om de laagste praktisch haalbare grain te kiezen.

"Leg de vier soorten slowly changing dimensions uit, met een voorbeeld van wanneer je elk zou gebruiken." Let op: Type 0 (nooit wijzigen), Type 1 (overschrijven, geen historie), Type 2 (nieuwe rij, volledige historie via valid_from/valid_to), Type 3 (één extra kolom voor de vorige waarde) — met een concreet voorbeeld per type.

"Waarom zou een historisch rapport 'veranderen' als je SCD Type 1 gebruikt in plaats van Type 2?" Let op: begrip dat Type 1 de oude waarde overschrijft, waardoor een join tussen een fact table en die dimensie altijd de huidige waarde teruggeeft — ook voor transacties die plaatsvonden toen de waarde nog anders was.

"Wat is het verschil tussen een star schema en een snowflake schema?" Let op: snowflake normaliseert dimensies verder (extra joins, minder opslag), star houdt dimensies plat (minder joins, wat meer opslag) — en het besef dat "Snowflake" hier geen relatie heeft met het gelijknamige platform.

"Wat zijn conformed dimensions, en waarom zijn ze belangrijk?" Let op: dezelfde dimensietabel hergebruikt over meerdere fact tables, noodzakelijk om rapportages tussen afdelingen of processen onderling vergelijkbaar te houden.

"Hoe zou je een accumulating snapshot fact table ontwerpen voor een orderproces met meerdere stadia?" Let op: één rij per order die wordt bijgewerkt naarmate stadia (besteld, betaald, verzonden, geleverd) worden bereikt, met een kolom per stadium-datum — en het besef dat dit de enige fact table-variant is die bestaande rijen update in plaats van alleen toevoegt.

Relevante Documentatie

Samenvatting

Dimensional modeling met het star schema is de concrete uitwerking van het denormalisatie-principe uit het vorige hoofdstuk: één centrale fact table met meetbare, optelbare gebeurtenissen, omringd door beschrijvende dimension tables, elk precies één join verwijderd. De belangrijkste ontwerpbeslissing is de grain — wat precies één rij in de fact table betekent — omdat elke andere keuze daaruit volgt en detail dat niet op de juiste grain is vastgelegd nooit meer terug te halen is. Slowly changing dimensions, vooral Type 2 met zijn valid_from/valid_to-patroon, lossen het probleem op van historisch correcte rapportage wanneer beschrijvende attributen wijzigen. Waar het star schema optimaliseert voor leessnelheid en eenvoud, kiest de aanpak in het volgende hoofdstuk — Data Vault — een compleet andere afweging: geoptimaliseerd voor traceerbaarheid en flexibiliteit bij veranderende bronsystemen, ten koste van query-eenvoud.

Hulp nodig bij het bouwen van een modern dataplatform?

DataPartner365 helpt organisaties met Microsoft Fabric, Snowflake, Databricks, dbt, Azure, data-architectuur en CI/CD.

Neem contact op met DataPartner365