Introductie
Data modeling wordt vaak verward met "een ERD tekenen" of "tabellen aanmaken", maar het is fundamenteler dan dat: het is de beslissing over hoe de werkelijkheid van een organisatie — klanten, orders, producten, hun onderlinge relaties — wordt vertaald naar een structuur die een database kan opslaan en een query-engine efficiënt kan bevragen. Een goed model maakt correcte antwoorden vanzelfsprekend; een slecht model maakt foute antwoorden onvermijdelijk, ongeacht hoe zorgvuldig de SQL erbovenop is geschreven.
Dit hoofdstuk behandelt de fundamenten die onder élk specifiek modelleerpatroon liggen — normalisatie, entity-relationship modeling, en de reden waarom analytische systemen daar bewust van afwijken. Het is de basis waarop Star Schema & Dimensional Modeling en Data Vault 2.0 voortbouwen; zonder dit fundament is het lastig te begrijpen waarom die twee aanpakken bestaan en wanneer je welke kiest.
De Drie Abstractieniveaus: Conceptueel, Logisch, Fysiek
Een veelgemaakte vergissing is meteen beginnen met tabellen en kolommen. Ervaren modelleerders werken doorgaans via drie niveaus, elk met een ander doel en publiek.
Conceptueel model — de businessentiteiten en hun relaties, zonder technische details. "Een klant plaatst orders. Een order bevat producten." Geen datatypes, geen sleutels, geen tabellen — dit niveau is bedoeld om met domeinexperts (sales, finance, product) te valideren of je de business correct begrijpt, vóórdat er ook maar iets gebouwd wordt.
Logisch model — hetzelfde, maar nu met attributen, sleutels en cardinaliteit, nog steeds onafhankelijk van een specifiek databasesysteem. Hier leg je vast dat een order een order_id, een order_date en een customer_id heeft, en dat de relatie tussen customer en order één-op-veel is (één klant kan meerdere orders hebben, een order hoort bij precies één klant).
Fysiek model — de daadwerkelijke implementatie: specifieke datatypes (VARCHAR(50) versus TEXT), indexen, partitioneringsstrategie, en platform-specifieke keuzes zoals clustering-sleutels in Snowflake of partition-kolommen in Databricks. Dit is het niveau waarop de meeste data engineers dagelijks werken, maar het is een vertaling van het logische model — niet het startpunt.
Het overslaan van de eerste twee niveaus is de meest voorkomende reden dat een fysiek model achteraf pijnlijk blijkt te zijn: technische beslissingen (welke kolom wordt de primary key, hoe worden many-to-many-relaties opgelost) zonder dat businessregels expliciet zijn vastgelegd, leiden tot een schema dat toevallig werkt voor de eerste use case en breekt bij de tweede.
Entity-Relationship Modeling: de Basis
Een entity-relationship model (ER-model) legt entiteiten (dingen die bestaan: klanten, producten, orders) en hun relaties vast, met drie kernbegrippen die in vrijwel elk schema terugkomen.
Entiteiten en attributen. Een entiteit is een op zichzelf staand ding met eigen attributen — Customer met customer_id, name, email. Een goede vuistregel: als iets een eigen levenscyclus heeft (aangemaakt, gewijzigd, mogelijk verwijderd, onafhankelijk van andere dingen), is het waarschijnlijk een aparte entiteit, geen attribuut van iets anders.
Relaties en cardinaliteit. Relaties tussen entiteiten hebben een cardinaliteit: één-op-één (zeldzaam — meestal een teken dat twee entiteiten eigenlijk één zouden moeten zijn), één-op-veel (de meest voorkomende — één klant, veel orders), en veel-op-veel (bijvoorbeeld producten en categorieën, als een product in meerdere categorieën kan vallen). Veel-op-veel-relaties kun je niet direct in twee tabellen met een foreign key vastleggen — daarvoor is een bridging table (ook wel junction table) nodig, een derde tabel die per combinatie één rij bevat.
Primary keys en foreign keys. De primary key identificeert een rij uniek binnen zijn eigen tabel; de foreign key verwijst naar de primary key van een andere tabel om de relatie fysiek vast te leggen. Dit mechanisme — niet de tabelnamen, niet de kolomvolgorde — is wat een verzameling losse tabellen tot een samenhangend model maakt.
-- Één-op-veel: customer -> orders
CREATE TABLE customer (
customer_id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100)
);
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT REFERENCES customer(customer_id),
order_date DATE
);
-- Veel-op-veel: product <-> category, via een bridging table
CREATE TABLE product_category (
product_id INT REFERENCES product(product_id),
category_id INT REFERENCES category(category_id),
PRIMARY KEY (product_id, category_id)
);
Normalisatie: 1NF, 2NF, 3NF
Normalisatie is het proces waarmee je een model stapsgewijs herstructureert om update-anomalieën te voorkomen — situaties waarin het wijzigen van één feit meerdere rijen zou moeten raken, met het risico dat je er een mist en de data inconsistent wordt.
Bekijk deze bewust slecht genormaliseerde tabel:
-- Ongenormaliseerd: klantgegevens herhaald op elke orderregel
CREATE TABLE orders_denormalized (
order_id INT,
customer_name VARCHAR(100),
customer_email VARCHAR(100),
product_name VARCHAR(100),
product_price NUMERIC(10,2),
quantity INT
);
Als een klant haar e-mailadres wijzigt, moet je hier elke rij met een order van die klant bijwerken. Vergeet je er één, dan heb je twee verschillende e-mailadressen voor dezelfde klant in je systeem — een update-anomalie. Normalisatie lost dit stapsgewijs op:
1NF (eerste normaalvorm) — elke kolom bevat atomaire waarden (geen kommagescheiden lijsten in één cel), en er is een duidelijke primary key. Een kolom product_names met "Laptop, Muis, Toetsenbord" erin schendt 1NF.
2NF (tweede normaalvorm) — bouwt voort op 1NF, en elimineert attributen die afhankelijk zijn van slechts een deel van een samengestelde primary key. Als de primary key (order_id, product_id) is, maar customer_email alleen van order_id afhangt (niet van product_id), hoort die kolom niet in deze tabel.
3NF (derde normaalvorm) — bouwt voort op 2NF, en elimineert attributen die afhankelijk zijn van een ander niet-sleutel-attribuut in plaats van rechtstreeks van de primary key. customer_email hangt af van customer_id, niet rechtstreeks van order_id — dus hoort het in een aparte customer-tabel, niet in orders.
Het resultaat van het toepassen van deze regels op het voorbeeld hierboven is precies het customer/orders/product-schema dat we eerder al zagen: drie aparte tabellen, gekoppeld via foreign keys, waarbij elk feit precies één keer wordt opgeslagen.
Waarom Analytische Modellen Denormaliseren
Hier wordt het interessant, en het is de brug naar de rest van dit deel van het handbook: 3NF is uitstekend voor OLTP, maar problematisch voor OLAP. Een volledig genormaliseerd schema met tien of twintig tabellen betekent dat een simpele analytische vraag — "omzet per productcategorie per maand" — al snel vier of vijf joins vereist. Elke join kost rekenwerk, en op grote datavolumes stapelt dat op tot merkbaar tragere queries, precies het probleem dat we in Data Warehousing Fundamentals bespraken bij columnar storage en MPP.
De oplossing in analytische systemen is bewust denormaliseren: sommige redundantie accepteren (dezelfde productcategorie-naam op duizenden rijen herhalen) in ruil voor minder joins en snellere, eenvoudigere queries. Dit is geen "slecht modelleren" — het is een andere afweging, omdat het probleem dat normalisatie oplost (voorkomen van update-anomalieën bij schrijfoperaties) simpelweg niet relevant is in een warehouse waar gold-tabellen elke nacht volledig herbouwd worden vanuit silver, in plaats van rij voor rij bijgewerkt. Dit exacte principe — bewuste, gecontroleerde denormalisatie voor leesprestaties — is de kern van dimensioneel modelleren, uitgewerkt in Star Schema & Dimensional Modeling. Data Vault, behandeld in Data Vault 2.0, kiest weer een andere afweging: een vorm die dichter bij normalisatie blijft, geoptimaliseerd voor traceerbaarheid en flexibiliteit bij schemawijzigingen in plaats van voor leessnelheid.
Genereer een dbt-model met de juiste tests en schema.yml voor je gemodelleerde tabellen.
Praktisch Voorbeeld: van Genormaliseerd naar Analytisch
Stel je hebt het genormaliseerde OLTP-schema van hierboven (customer, orders, product, product_category, category). Een analytische vraag als "omzet per categorie per maand, met klantsegment" vereist op dit schema:
-- Vijf joins voor één analytische vraag — werkt, maar kostbaar op schaal
SELECT
c.segment,
cat.category_name,
DATE_TRUNC('month', o.order_date) AS month,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN customer c ON c.customer_id = o.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
JOIN product p ON p.product_id = oi.product_id
JOIN product_category pc ON pc.product_id = p.product_id
JOIN category cat ON cat.category_id = pc.category_id
GROUP BY c.segment, cat.category_name, DATE_TRUNC('month', o.order_date);
Een gedenormaliseerd analytisch model (een voorloper van de star schema-aanpak uit het volgende hoofdstuk) legt de categorie- en klantgegevens al vooraf plat op de feiten-rij vast, zodat dezelfde vraag zonder joins te beantwoorden is:
-- Dezelfde vraag, tegen een al gedenormaliseerd analytisch model
SELECT
customer_segment,
category_name,
DATE_TRUNC('month', order_date) AS month,
SUM(revenue) AS revenue
FROM gold.sales_flat
GROUP BY customer_segment, category_name, DATE_TRUNC('month', order_date);
De tweede versie is niet alleen korter — hij is fundamenteel goedkoper om uit te voeren, omdat het denormaliseren (het samenvoegen van klant-, product- en categoriegegevens) al één keer gebeurde tijdens de nachtelijke transformatie, in plaats van bij elke query opnieuw.
Modelleerkeuzes die Blijven Terugkomen
Een paar ontwerpbeslissingen komen in vrijwel elk model terug, ongeacht of je normaliseert of denormaliseert.
Surrogate keys versus natural keys. Een natural key is een bestaand, betekenisvol attribuut dat toevallig uniek is (een e-mailadres, een BTW-nummer). Een surrogate key is een kunstmatig, betekenisloos, meestal automatisch oplopend identifier (customer_id: 1, 2, 3, ...) zonder relatie tot de data zelf. In analytische modellen is de vuistregel: gebruik bijna altijd surrogate keys, omdat natural keys kunnen veranderen (een e-mailadres wordt gewijzigd) of dubbel kunnen voorkomen tussen bronsystemen — een probleem dat je niet wilt hebben zodra die sleutel de basis is van duizenden foreign key-relaties.
Nullable-ontwerp is een bewuste keuze, geen toeval. Een kolom NOT NULL maken is een garantie die de rest van je pipeline op mag vertrouwen; een kolom nullable laten zonder erover na te denken schuift de vraag "wat betekent afwezigheid hier?" door naar iedereen die de tabel later gebruikt.
Veel-op-veel-relaties vereisen altijd een bridging table, nooit een kommagescheiden lijst in een enkele kolom (dat schendt 1NF, zoals hierboven) en meestal ook geen twee foreign keys direct in dezelfde rij (dat werkt alleen als de cardinaliteit gegarandeerd één-op-veel blijft, wat bij veel-op-veel per definitie niet zo is).
Zwakke entiteiten (weak entities) zijn entiteiten die niet zelfstandig kunnen bestaan — een order_item heeft geen betekenis zonder een bijbehorende order, en zijn identiteit is mede afhankelijk van die relatie (vaak via een samengestelde sleutel als (order_id, line_number) in plaats van een eigen surrogate key). Het herkennen van een zwakke entiteit is belangrijk omdat het bepaalt of verwijderen cascadeert: als een order wordt verwijderd, horen de bijbehorende orderregels normaliter mee te gaan, terwijl het verwijderen van een klant niet automatisch alle historische orders zou moeten wissen.
Associatieve entiteiten zijn een bridging table die zelf ook eigen attributen heeft, niet alleen twee foreign keys. De product_category-tabel hierboven zou bijvoorbeeld een attribuut is_primary_category kunnen krijgen om aan te geven welke van meerdere categorieën de hoofdcategorie is — op dat moment is het niet langer een pure koppeltabel, maar een entiteit met een eigen betekenis die toevallig een veel-op-veel-relatie modelleert.
Best Practices
- Werk via de drie abstractieniveaus, ook al voelt het conceptuele niveau als "te simpel om op te schrijven" — het is waar je met domeinexperts valideert vóórdat er code geschreven is.
- Gebruik surrogate keys als primary key in vrijwel elk analytisch model, en bewaar natural keys als een gewoon (geïndexeerd) attribuut ernaast.
- Normaliseer voor OLTP, denormaliseer bewust voor OLAP — beide zijn correct, toegepast op het juiste probleem.
- Leg cardinaliteit expliciet vast (één-op-veel, veel-op-veel) vóórdat je tabellen aanmaakt; dit bepaalt of je een foreign key of een bridging table nodig hebt.
- Documenteer wat NULL betekent in elke nullable kolom — "onbekend", "niet van toepassing" en "nog niet ingevuld" zijn drie verschillende dingen die dezelfde NULL-waarde verbergt.
Veelgemaakte Fouten
- Direct beginnen met tabellen bouwen zonder eerst een conceptueel of logisch model te valideren met de mensen die de business kennen.
- Kommagescheiden waarden in één kolom ("tags: sql,python,snowflake") in plaats van een aparte, genormaliseerde tabel — onmogelijk om efficiënt op te filteren of te aggregeren.
- Natural keys als primary key gebruiken in een analytisch model, waardoor een gewijzigd e-mailadres of een dubbele waarde uit een tweede bronsysteem het hele model breekt.
- Denormalisatie toepassen zonder te begrijpen waarom — data redundant opslaan "omdat het sneller is" zonder te weten welk specifiek queryprobleem dat oplost, wat later tot inconsistente kopieën leidt.
- Veel-op-veel-relaties negeren tot het probleem zich toont, met een haastige work-around (zoals een tweede foreign key-kolom) in plaats van een nette bridging table.
Performance Tips
- Indexeer foreign keys in OLTP-systemen — zonder index op de foreign key-kolom wordt elke join een volledige scan van de gerelateerde tabel.
- Vermijd over-normaliseren in analytische modellen. Elke extra join-laag kost rekenwerk bij elke query; in een warehouse waar je toch elke nacht herbouwt, is die kost vaak niet de moeite waard.
- Meet het aantal joins in je meest voorkomende dashboardqueries. Vijf of meer joins in een query die honderden keren per dag wordt uitgevoerd, is een sterk signaal dat een gedenormaliseerde gold-tabel de investering waard is.
- Gebruik samengestelde primary keys spaarzaam in kolomgeoriënteerde warehouses — een enkele surrogate key is vaak eenvoudiger te clusteren en te filteren op dan een combinatie van kolommen.
Interviewvragen
"Wat is het verschil tussen een conceptueel, logisch en fysiek datamodel?" Let op: begrip dat elk niveau een ander doel en publiek heeft, en dat het overslaan van de eerste twee vaak tot pijnlijke fysieke ontwerpfouten leidt.
"Leg 3NF uit met een voorbeeld, en welk probleem lost het op?" Let op: een concreet voorbeeld van een update-anomalie (zoals de herhaalde klantgegevens hierboven) en de drie stappen (1NF, 2NF, 3NF) die dat oplossen.
"Waarom denormaliseren analytische modellen bewust, terwijl OLTP-systemen normaliseren?" Let op: het inzicht dat normalisatie schrijfintegriteit optimaliseert en denormalisatie leessnelheid, en dat een warehouse dat elke nacht herbouwt de downsides van denormalisatie (update-anomalieën) niet heeft.
"Wat is het verschil tussen een surrogate key en een natural key, en wanneer gebruik je welke?" Let op: surrogate keys als standaardkeuze in analytische modellen vanwege stabiliteit, natural keys bewaard als attribuut voor herkenbaarheid en debugging.
"Hoe modelleer je een veel-op-veel-relatie?" Let op: een bridging/junction table met een samengestelde primary key die verwijst naar beide kanten van de relatie — niet een kommagescheiden kolom of een work-around met twee foreign keys.
"Wat betekent het als een kolom NULL toestaat, en waarom is dat een ontwerpbeslissing?" Let op: het besef dat NULL meerdere betekenissen kan hebben (onbekend, niet van toepassing, nog niet ingevuld) en dat dit expliciet gedocumenteerd moet worden, niet aan toeval overgelaten.
Relevante Documentatie
- PostgreSQL: Normalization — praktische uitleg van foreign keys en referentiële integriteit.
- dbt Labs: Data Modeling Techniques — hoe klassieke modelleertheorie zich vertaalt naar dbt-projecten.
- Snowflake: Data Modeling Best Practices — platformspecifieke aanbevelingen voor analytische modellen.
Samenvatting
Data modeling is het vertalen van de werkelijkheid naar een structuur die correct én efficiënt bevraagbaar is, via drie abstractieniveaus die van conceptueel naar fysiek gaan. Entity-relationship modeling met primary/foreign keys en expliciete cardinaliteit is de basisgrammatica; normalisatie (1NF-3NF) voorkomt update-anomalieën door elk feit precies één keer op te slaan. Analytische systemen wijken hier bewust van af: ze denormaliseren om leesprestaties te winnen, omdat het probleem dat normalisatie oplost (schrijfconsistentie) in een warehouse dat elke nacht herbouwt simpelweg niet speelt. Dat ene inzicht — normaliseren voor schrijven, denormaliseren voor lezen — is de sleutel tot de twee grote analytische modelleeraanpakken die in de volgende hoofdstukken aan bod komen: het snelheid-geoptimaliseerde star schema en het traceerbaarheid-geoptimaliseerde Data Vault.
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