Ebook · Hoofdstuk 5 van 10
Data Modelling Technieken
Star schema, fact types, SCD's en de geavanceerde patronen die je in elke serieuze warehouse-build tegenkomt.
Het hart van het warehouse
Modellering is de plek waar slechte beslissingen jaren doorwerken. Een goede sterrenstructuur is leesbaar voor BI-developers, snel voor de query-engine en flexibel genoeg om mee te groeien met de business. Dit hoofdstuk gaat dieper dan de basis: niet alleen wat een fact en dimensie is, maar welke varianten er zijn en wanneer je ze inzet.
Star schema versus snowflake
In een star schema is elke dimensie één gedenormaliseerde tabel. In een snowflake schema worden dimensies verder genormaliseerd in subtabellen. Het verschil:
-- Star: één dim_product
CREATE TABLE dim_product (
product_key INT PRIMARY KEY,
product_id VARCHAR(50),
product_name VARCHAR(200),
category VARCHAR(100),
subcategory VARCHAR(100),
brand VARCHAR(100),
brand_country VARCHAR(50)
);
-- Snowflake: dim_product genormaliseerd in subtabellen
CREATE TABLE dim_product (
product_key INT PRIMARY KEY,
product_id VARCHAR(50),
product_name VARCHAR(200),
brand_key INT,
category_key INT
);
CREATE TABLE dim_brand (
brand_key INT PRIMARY KEY,
brand_name VARCHAR(100),
country_key INT
);
CREATE TABLE dim_category (
category_key INT PRIMARY KEY,
category_name VARCHAR(100),
subcategory_name VARCHAR(100)
);
De praktische regel: kies star, tenzij je een hele goede reden hebt om te snowflaken. Star is sneller voor query engines (minder joins), simpeler voor BI-tools en intuïtiever voor gebruikers. Snowflake bespaart marginaal opslag op enorme dimensies — maar in cloud-warehouses met columnstore-compressie is dat geen issue meer.
Wanneer is snowflake wel verdedigbaar?
Drie scenario's: (1) een dimensie heeft attributen die afzonderlijk een eigen analytische rol spelen — een productcategorie die ook gebruikers- of regelgevings-attributen heeft. (2) Outrigger-dimensies in SCD Type 5: snelwisselende attributen los van langzaam wisselende. (3) Een Inmon-stijl 3NF-laag onder een Kimball-goldlaag (zie hoofdstuk 2). Buiten deze gevallen creëer je vooral extra joins zonder waarde.
Fact tables: drie hoofdtypen
Niet elke fact-tabel is hetzelfde. Kimball onderscheidt drie typen, elk met eigen toepassingen:
1. Transaction fact
Eén rij per gebeurtenis (verkoop, klik, login). Meest voorkomende type. Granular en flexibel — je kunt altijd nog aggregeren, maar nooit detail terughalen dat je niet hebt opgeslagen.
CREATE TABLE fact_sales (
sales_key BIGINT IDENTITY,
date_key INT,
customer_key INT,
product_key INT,
store_key INT,
order_id VARCHAR(50), -- degenerate dimension
quantity INT,
unit_price DECIMAL(10,2),
discount DECIMAL(10,2),
net_amount DECIMAL(12,2),
load_ts TIMESTAMP
);
2. Periodic snapshot fact
Eén rij per entiteit per periode (dagelijks saldo, maandelijkse voorraad). Geweldig voor trends en KPI's, maar grof.
CREATE TABLE fact_inventory_daily (
date_key INT,
product_key INT,
store_key INT,
units_on_hand INT,
units_sold INT,
units_received INT,
inventory_value DECIMAL(12,2),
PRIMARY KEY (date_key, product_key, store_key)
);
3. Accumulating snapshot fact
Eén rij per business proces dat door fasen gaat (een order van geplaatst naar geleverd). Datums voor elke mijlpaal — rij wordt geüpdatet als er fasen voorbij gaan.
CREATE TABLE fact_order_lifecycle (
order_id VARCHAR(50) PRIMARY KEY,
customer_key INT,
date_placed_key INT,
date_paid_key INT,
date_shipped_key INT,
date_delivered_key INT,
hours_to_pay INT,
hours_to_ship INT,
hours_to_deliver INT,
total_amount DECIMAL(12,2)
);
Elke fase update dezelfde rij. Maakt cycle-time-analyses (gemiddeld 36 uur tot levering?) triviaal.
Additieve, semi-additieve en niet-additieve metingen
Niet elke meting in een fact-tabel kan op elke dimensie gesommeerd worden. Drie categorieën:
- Additief — werkt op alle dimensies.
quantity,net_amount,units_sold. Som over tijd, klant, product, alles. - Semi-additief — werkt op de meeste, maar niet op tijd.
units_on_hand(voorraad),account_balance(saldo). Sommatie over klant of regio is correct, sommatie over tijd niet — daar moet je gemiddelde of laatste waarde nemen. - Niet-additief — werkt op geen enkele dimensie.
unit_price,conversion_rate,profit_margin_pct. Berekend vanuit additieve componenten op queryniveau.
Documenteer dit per fact-kolom. BI-developers die niet-additieve metingen optellen leveren rapportages die er goed uitzien én structureel fout zijn.
Slowly Changing Dimensions: 0 t/m 7
Klantgegevens veranderen. Hoe ga je daarmee om? Kimball benoemt zeven SCD-types:
| Type | Strategie | Wanneer |
|---|---|---|
| 0 | Nooit wijzigen — bevries originele waarde | Onveranderlijke attributen (geboortedatum) |
| 1 | Overschrijven — geen historie | Correcties van fouten |
| 2 | Nieuwe rij per wijziging — volledige historie | Standaard voor business-relevante wijzigingen |
| 3 | Extra kolommen voor "previous" waarde | Beperkte historie (alleen vorige stand) |
| 4 | Mini-dimensie met snelwisselende attributen | Klanten met regelmatig wisselende segmenten |
| 5 | Combinatie van 1 en 4 met outrigger | Beperkte type-2-overhead nodig |
| 6 | Combinatie van 1, 2 en 3 | Zowel huidige als historische analyses |
| 7 | Dual surrogate keys: huidig + historisch | "As-is" én "as-was" rapporteren |
In de praktijk gebruik je 95% van de tijd Type 1 of Type 2. De overige varianten zijn voor specifieke edge cases.
SCD Type 2 in detail
De volledige implementatie met effective dating:
CREATE TABLE dim_customer (
customer_key INT IDENTITY PRIMARY KEY,
customer_id VARCHAR(50),
name VARCHAR(200),
email VARCHAR(200),
segment VARCHAR(50),
country VARCHAR(50),
valid_from DATE,
valid_to DATE,
is_current BOOLEAN,
hash_diff CHAR(64) -- voor change-detection
);
-- Trick: gebruik een hash om te detecteren of er iets is veranderd
-- in plaats van per kolom te vergelijken
WITH src AS (
SELECT customer_id, name, email, segment, country,
SHA2(CONCAT_WS('|', name, email, segment, country), 256) AS hash_diff
FROM stg_customers
),
to_change AS (
SELECT s.*
FROM src s
JOIN dim_customer d ON d.customer_id = s.customer_id AND d.is_current
WHERE d.hash_diff <> s.hash_diff
)
-- 1. Verlopen sluiten
UPDATE dim_customer
SET valid_to = CURRENT_DATE - 1, is_current = FALSE
WHERE customer_id IN (SELECT customer_id FROM to_change)
AND is_current = TRUE;
-- 2. Nieuwe rijen
INSERT INTO dim_customer (customer_id, name, email, segment, country,
valid_from, valid_to, is_current, hash_diff)
SELECT customer_id, name, email, segment, country,
CURRENT_DATE, '9999-12-31', TRUE, hash_diff
FROM to_change
UNION ALL
-- 3. Volledig nieuwe klanten
SELECT s.customer_id, s.name, s.email, s.segment, s.country,
CURRENT_DATE, '9999-12-31', TRUE, s.hash_diff
FROM src s
LEFT JOIN dim_customer d ON d.customer_id = s.customer_id
WHERE d.customer_id IS NULL;
Hash-based change detection
In plaats van elke kolom expliciet te vergelijken, hash je alle SCD-2-attributen samen tot één hash_diff-kolom. Eén vergelijking en je weet of er iets veranderd is. Sneller, leesbaarder, minder bug-gevoelig.
Geavanceerde dimension-patronen
Junk dimension
Heb je een dozijn vlaggetjes en codes (is_promo, payment_method, channel) die niet de moeite waard zijn voor aparte dimensies? Stop ze in één junk dimension met alle mogelijke combinaties.
Role-playing dimension
Eén dim_date, maar meerdere keren joinen via aliassen (order_date, ship_date, delivery_date). Geen aparte tabellen nodig.
SELECT
o.order_id,
od.full_date AS order_date,
sd.full_date AS ship_date,
dd.full_date AS delivery_date
FROM fact_order_lifecycle o
JOIN dim_date od ON od.date_key = o.date_placed_key
JOIN dim_date sd ON sd.date_key = o.date_shipped_key
JOIN dim_date dd ON dd.date_key = o.date_delivered_key;
Bridge table (multi-valued)
Een patiënt heeft meerdere diagnoses, een product heeft meerdere kleuren. Een bridge-tabel met weight factors lost dit op zonder rijen te dubbeltellen.
CREATE TABLE bridge_product_color (
product_key INT,
color_key INT,
weight_factor DECIMAL(5,4) -- som per product = 1.0
);
Mini-dimensie
Snel-wisselende attributen (klant-credit-rating, marketing-segment) trekken SCD-2 explosies aan. Zet ze in een aparte mini-dimensie en koppel die direct aan de fact, niet aan de hoofd-dimensie.
Conformed dimensions
Een dimensie is conformed als hij identiek wordt gebruikt over meerdere fact tables. dim_date is altijd conformed. dim_customer hoort dat te zijn, maar wordt het vaak niet als verschillende teams hun eigen versie bouwen. Conformed dimensions zijn de enige manier om consistent te aggregeren over business processen heen.
Factless fact tables
Soms is "het feit dat iets gebeurde" zelf het feit, zonder meetwaarde. Een student die zich inschrijft voor een cursus, een klant die een nieuwsbrief opent. Een factless fact bevat alleen foreign keys, en wordt geteld in plaats van gesommeerd.
Surrogate keys versus natural keys
Bron-IDs (customer_id = 'C-12345') lijken een goede primary key voor je dimensie. Tot een SAP-migratie de IDs hernummert, een fusie twee klantenbestanden samenvoegt, of een leverancier zijn productcodes uitbreidt van 6 naar 8 tekens. Surrogate keys — een synthetische integer of hash, alleen zinvol binnen het warehouse — beschermen je tegen al deze pijn.
Conventies die zich in de praktijk uitbetalen:
- Elke dimensie heeft een
{entity}_key(surrogate, integer of hash) én een{entity}_id(de business key uit de bron, bewaard voor traceerbaarheid). - Fact tables refereren altijd naar
_key, nooit naar_id. - Reserveer
-1of0voor "Unknown" — een rij in elke dimensie met dummy-waarden zodat fact-rijen die nog geen match hebben toch aansluiting houden. Dit voorkomt dat outer joins data weggooien.
Late-arriving dimensies en facts
De werkelijkheid is wreed: soms komt een verkoop binnen voordat de bijbehorende klant in de klantdimensie staat. Of een SCD2-update arriveert dagen na de fact die ernaar verwijst. Twee patronen lossen dit op:
- Late-arriving dimensies — wanneer een fact-rij binnenkomt zonder dimensie-match, voeg je een placeholder-rij toe aan de dimensie met
customer_iden NULL voor alle andere attributen. Wanneer de echte klantgegevens later arriveren, update je de placeholder (Type 1) of maak je de placeholder als eerste SCD2-versie en open je een tweede. - Late-arriving facts — een verkoop die drie dagen later wordt gemeld moet gekoppeld worden aan de juiste SCD2-versie van de klant op het moment van verkoop, niet aan de huidige. Join op
customer_idén op deorder_datebinnenvalid_fromenvalid_to.
De grain — de heiligste regel
Voor je een fact-tabel ontwerpt: bepaal de grain — wat één rij voorstelt. "Eén rij per orderregel per dag." Schrijf het op. Mix nooit grains. Een fact die soms één rij per order is en soms één per orderregel is een tijdbom voor aggregaties.
Key takeaways
- Star > snowflake voor leesbaarheid en performance.
- Drie fact-types: transaction, periodic snapshot, accumulating snapshot — elk voor een ander doel.
- SCD Type 2 met hash-based change detection is de moderne standaard.
- Junk, role-playing, bridge en mini-dimensies lossen specifieke problemen op.
- Conformed dimensions maken cross-process analyses mogelijk.
- Definieer altijd je grain — en wijk er nooit van af.
- Documenteer of metingen additief, semi-additief of niet-additief zijn.
Veelgestelde vragen
Wat is het verschil tussen een star schema en een snowflake schema?
In een star schema is elke dimensie één gedenormaliseerde tabel die direct aan de fact-tabel hangt. In een snowflake schema worden dimensies verder genormaliseerd in subtabellen. Star is sneller voor query engines, simpeler voor BI-tools en de praktische standaard in moderne cloud-warehouses.
Wat is een Slowly Changing Dimension?
Een SCD is een dimensie waarvan de attributen in de loop van de tijd wijzigen. Kimball definieert zeven types die elk een andere strategie hebben: Type 1 overschrijft, Type 2 maakt een nieuwe rij per wijziging met valid_from/valid_to, Type 3 bewaart de vorige waarde in een extra kolom. In de praktijk gebruik je vooral Type 1 en Type 2.
Wat is een fact table?
Een fact table bevat metingen of gebeurtenissen — verkopen, klikken, voorraadstanden — met foreign keys naar dimensietabellen voor context. Er zijn drie hoofdtypen: transaction facts (één rij per gebeurtenis), periodic snapshot facts (één rij per entiteit per periode) en accumulating snapshot facts (één rij per business proces dat door fasen gaat).
Wat is een conformed dimension?
Een conformed dimension is een dimensie die identiek wordt gebruikt over meerdere fact tables — dim_date of dim_customer met dezelfde definities en surrogate keys, ongeacht of je hem joint met fact_sales of fact_inventory. Conformed dimensions zijn de enige manier om consistent te aggregeren over business processen heen.
Wat is de grain van een fact table?
De grain is wat één rij in de fact table voorstelt — bijvoorbeeld 'één rij per orderregel per dag'. Bepaal de grain vooraf, schrijf hem op en wijk er nooit van af. Een fact-tabel met gemixte grains maakt elke aggregatie onbetrouwbaar.
Wanneer gebruik je een bridge table?
Een bridge table los je multi-valued dimensions mee op — situaties waar één entiteit meerdere waarden in een andere kan hebben (een patiënt met meerdere diagnoses, een product met meerdere kleuren). De bridge bevat de relatie plus een weight_factor zodat aggregaties niet dubbeltellen.