Jouw partner voor datagedreven groei en inzichten
Data modelleren is een van de meest fundamentele vaardigheden in de datawereld. Of je nu een database ontwerpt, een data warehouse bouwt of een Power BI-rapport maakt — zonder een goed datamodel werkt niets naar behoren. In dit artikel leggen we data modelleren van de grond af aan uit: van de theorie (conceptueel, logisch, fysiek model) tot ER-diagrammen, normalisatie en praktijkvoorbeelden met echte SQL-code.
Data modelleren is het proces waarbij je de structuur van data vastlegt: welke gegevens worden opgeslagen, hoe ze met elkaar samenhangen en hoe ze georganiseerd worden in tabellen, kolommen en relaties. Een datamodel is als een plattegrond van je databasis: zonder goede plattegrond bouw je een chaotisch gebouw.
Een goed datamodel zorgt voor:
Data modelleren gebeurt op drie niveaus, elk met een ander detailniveau en een andere doelgroep:
Het conceptuele model is het meest abstracte niveau. Het beschrijft wat er opgeslagen wordt, zonder technische details. Dit model wordt gemaakt samen met de business, niet door de IT-afdeling alleen. Het antwoordt op de vraag: welke concepten bestaan er in ons domein en hoe hangen ze samen?
Voorbeeld voor een webshop:
In een conceptueel model teken je entiteiten (rechthoeken) en relaties (lijnen met beschrijvingen). De details — welke kolommen, welke datatypes, welke sleutels — komen later.
Het logische model vertaalt het conceptuele model naar een gedetailleerdere beschrijving, los van een specifiek databasesysteem. Hier definieer je:
Voorbeeld logisch model voor de Klant-entiteit:
KLANT
├── klant_id (PK, integer)
├── voornaam (string, verplicht)
├── achternaam (string, verplicht)
├── e-mailadres (string, uniek)
├── geboortedatum (datum)
└── aanmaakdatum (datetime)
Het fysieke model is de implementatie in een specifiek databasesysteem. Hier worden keuzes gemaakt die afhankelijk zijn van de gekozen technologie:
-- Fysiek model in PostgreSQL
CREATE TABLE klanten (
klant_id SERIAL PRIMARY KEY,
voornaam VARCHAR(100) NOT NULL,
achternaam VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
geboortedatum DATE,
aangemaakt_op TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT email_format CHECK (email LIKE '%@%.%')
);
-- Index voor snelle lookups op e-mail
CREATE INDEX idx_klanten_email ON klanten(email);
-- Index voor sortering op achternaam
CREATE INDEX idx_klanten_naam ON klanten(achternaam, voornaam);
In het fysieke model houd je rekening met indexen voor snelheid, datatypes die het databasesysteem ondersteunt, partitionering voor grote tabellen en opslaglimieten.
Een ER-diagram (Entity-Relationship diagram) visualiseert de structuur van een datamodel. Het is de standaard manier om datamodellen te communiceren. Er zijn verschillende notaties, maar de meest gebruikte is de crow's foot (kraaienpoot) notatie.
| Symbool | Betekenis | Voorbeeld |
|---|---|---|
| ||—|| | Exact één | Een bestelling heeft exact één klant |
| ||—o| | Nul of één | Een klant heeft optioneel één primair adres |
| ||—<| | Één of meer | Een bestelling heeft één of meer bestelregels |
| ||—o< | Nul of meer | Een klant heeft nul of meer bestellingen |
KLANTEN ||--o{ BESTELLINGEN : "plaatst"
BESTELLINGEN ||--|{ BESTELREGELS : "bevat"
PRODUCTEN ||--o{ BESTELREGELS : "staat in"
CATEGORIEËN ||--o{ PRODUCTEN : "groepeert"
ADRESSEN ||--o{ BESTELLINGEN : "verzendadres"
KLANTEN ||--o{ ADRESSEN : "heeft"
Dit eenvoudige diagram communiceert al de kernstructuur: een klant heeft meerdere adressen, plaatst meerdere bestellingen, elke bestelling heeft bestelregels en elke bestelregel verwijst naar een product.
Normalisatie is het proces om een datamodel te optimaliseren voor integriteit en minimale redundantie. Het is geformaliseerd in "Normale Vormen" (NF). In de praktijk werk je normaal tot en met de derde normale vorm (3NF).
Regel: elke cel bevat één waarde, er zijn geen herhalende groepen.
Slecht (niet in 1NF):
| bestelling_id | klant | producten |
|---|---|---|
| 1001 | Jan de Vries | Laptop, Muis, Toetsenbord |
| 1002 | Sara Jansen | Monitor |
Goed (in 1NF):
| bestelling_id | klant | product |
|---|---|---|
| 1001 | Jan de Vries | Laptop |
| 1001 | Jan de Vries | Muis |
| 1001 | Jan de Vries | Toetsenbord |
| 1002 | Sara Jansen | Monitor |
Regel: in 1NF én alle niet-sleutelattributen zijn volledig afhankelijk van de volledige primaire sleutel (geen partiële afhankelijkheden).
Probleem: in de 1NF-tabel hierboven staat "klant" bij elke bestelregel hetzelfde. Als Jan zijn naam verandert, moet je drie rijen aanpassen. Dit is een partiële afhankelijkheid: "klant" hangt af van "bestelling_id", niet van "product".
Oplossing — splits in twee tabellen:
-- Tabel 1: Bestellingen
CREATE TABLE bestellingen (
bestelling_id INT PRIMARY KEY,
klant_id INT REFERENCES klanten(klant_id)
);
-- Tabel 2: Bestelregels
CREATE TABLE bestelregels (
bestelregel_id INT PRIMARY KEY,
bestelling_id INT REFERENCES bestellingen(bestelling_id),
product_id INT REFERENCES producten(product_id),
aantal INT NOT NULL DEFAULT 1
);
Regel: in 2NF én geen transitieve afhankelijkheden (niet-sleutelattributen zijn niet afhankelijk van andere niet-sleutelattributen).
Voorbeeld van een transitieve afhankelijkheid:
-- Problematisch: postcode bepaalt stad, niet de primaire sleutel
KLANT: klant_id → postcode → stad
-- Oplossing: aparte postcodes-tabel
CREATE TABLE postcodes (
postcode CHAR(6) PRIMARY KEY,
stad VARCHAR(100) NOT NULL,
provincie VARCHAR(50)
);
CREATE TABLE klantadressen (
adres_id SERIAL PRIMARY KEY,
klant_id INT REFERENCES klanten(klant_id),
straat VARCHAR(200),
huisnummer VARCHAR(10),
postcode CHAR(6) REFERENCES postcodes(postcode)
);
Voor analytische omgevingen (data warehouses, rapportages) gebruik je een ander type model: het dimensionele model. Dit is geoptimaliseerd voor leesbaarheid en query-prestaties, niet voor schrijfoperaties.
| Eigenschap | Feitentabel | Dimensietabel |
|---|---|---|
| Doel | Meetwaarden opslaan | Context geven aan de meetwaarden |
| Grootte | Groot (miljoenen rijen) | Klein (duizenden rijen) |
| Bevat | Getallen en vreemde sleutels | Beschrijvende tekst en attributen |
| Voorbeelden | Verkooptransacties, websitebezoeken | Klant, Product, Datum, Locatie |
| Update-frequentie | Continu (append-only) | Zelden (SCD-logica) |
In een star schema staat de feitentabel centraal, met rondom de dimensietabellen. Dit lijkt op een ster — vandaar de naam. Het is eenvoudig te begrijpen en geeft uitstekende queryprestaties.
-- Feitentabel: verkopen
CREATE TABLE feit_verkopen (
verkoop_id BIGINT PRIMARY KEY,
datum_sleutel INT REFERENCES dim_datum(datum_sleutel),
klant_sleutel INT REFERENCES dim_klant(klant_sleutel),
product_sleutel INT REFERENCES dim_product(product_sleutel),
winkel_sleutel INT REFERENCES dim_winkel(winkel_sleutel),
verkoop_bedrag DECIMAL(10,2),
verkoop_aantal INT,
korting_pct DECIMAL(5,2)
);
-- Dimensietabel: datum (essentieel voor tijdanalyses)
CREATE TABLE dim_datum (
datum_sleutel INT PRIMARY KEY, -- bijv. 20250315 voor 15 maart 2025
datum DATE NOT NULL,
dag INT,
dag_naam VARCHAR(20),
week_nummer INT,
maand INT,
maand_naam VARCHAR(20),
kwartaal INT,
jaar INT,
is_weekend BOOLEAN,
is_feestdag BOOLEAN,
feestdag_naam VARCHAR(100)
);
Bij een snowflake schema worden de dimensietabellen verder genormaliseerd. Producten kunnen bijvoorbeeld een aparte categorietabel hebben. Dit bespaart opslagruimte maar maakt queries complexer door extra joins.
-- Snowflake: product → subcategorie → categorie
CREATE TABLE dim_categorie (
categorie_id INT PRIMARY KEY,
categorie_naam VARCHAR(100)
);
CREATE TABLE dim_subcategorie (
subcategorie_id INT PRIMARY KEY,
naam VARCHAR(100),
categorie_id INT REFERENCES dim_categorie(categorie_id)
);
CREATE TABLE dim_product (
product_sleutel INT PRIMARY KEY,
product_naam VARCHAR(200),
merk VARCHAR(100),
subcategorie_id INT REFERENCES dim_subcategorie(subcategorie_id),
prijs_huidig DECIMAL(10,2)
);
Een veelgestelde vraag bij data warehouses: wat doe je als de informatie in een dimensie verandert? Stel dat een klant verhuist. Wil je de historische verkopen koppelen aan het oude of nieuwe adres? Dit heet het Slowly Changing Dimension (SCD) probleem.
De nieuwe waarde vervangt de oude. Geen historiek bewaard. Eenvoudig maar verliest verleden.
UPDATE dim_klant SET stad = 'Rotterdam' WHERE klant_sleutel = 1001;
Elke wijziging leidt tot een nieuwe rij met geldigheidsperiode. De meest gebruikte aanpak in analytics.
-- SCD Type 2 structuur
CREATE TABLE dim_klant (
klant_sleutel SERIAL PRIMARY KEY, -- surrogate key
klant_id_bron INT NOT NULL, -- business key
voornaam VARCHAR(100),
achternaam VARCHAR(100),
stad VARCHAR(100),
geldig_van DATE NOT NULL DEFAULT CURRENT_DATE,
geldig_tot DATE DEFAULT '9999-12-31',
is_huidig BOOLEAN DEFAULT TRUE
);
-- Bij verhuizing: sluit oude rij af en maak nieuwe aan
UPDATE dim_klant
SET geldig_tot = CURRENT_DATE - 1, is_huidig = FALSE
WHERE klant_id_bron = 5001 AND is_huidig = TRUE;
INSERT INTO dim_klant (klant_id_bron, voornaam, achternaam, stad)
VALUES (5001, 'Jan', 'de Vries', 'Rotterdam');
In Power BI gelden dezelfde principes, maar de implementatie is anders. Power BI werkt intern met een kolomgeoriënteerd in-memory model (Vertipaq engine). Een goed Power BI-model volgt altijd het star schema.
-- Datumdimensie genereren in Power Query (M)
let
StartDatum = #date(2020, 1, 1),
EindDatum = Date.From(DateTime.LocalNow()),
AantalDagen = Duration.Days(EindDatum - StartDatum) + 1,
DatumLijst = List.Dates(StartDatum, AantalDagen, #duration(1, 0, 0, 0)),
TabelVanDatums = Table.FromList(DatumLijst, Splitter.SplitByNothing()),
HernoemKolom = Table.RenameColumns(TabelVanDatums, {{"Column1", "Datum"}}),
DatumType = Table.TransformColumnTypes(HernoemKolom, {{"Datum", type date}}),
VoegJaarToe = Table.AddColumn(DatumType, "Jaar", each Date.Year([Datum]), Int64.Type),
VoegMaandToe = Table.AddColumn(VoegJaarToe, "Maand", each Date.Month([Datum]), Int64.Type),
VoegKwartaalToe = Table.AddColumn(VoegMaandToe, "Kwartaal", each "Q" & Text.From(Date.QuarterOfYear([Datum])), type text),
VoegWeekdagToe = Table.AddColumn(VoegKwartaalToe, "Dag", each Date.DayOfWeekName([Datum], "nl-NL"), type text)
in
VoegWeekdagToe
Na het bespreken van de theorie is het nuttig om de meest voorkomende praktijkfouten te belichten:
Er zijn diverse tools beschikbaar voor het maken en documenteren van datamodellen:
| Tool | Type | Sterkste punt |
|---|---|---|
| dbdiagram.io | Gratis, online | Snel ER-diagrammen maken met code |
| Lucidchart | Betaald, online | Mooie diagrammen, goed voor samenwerking |
| dbt docs | Gratis, code-driven | Automatisch documentatie genereren vanuit dbt |
| DataGrip / DBeaver | Database IDE | ER-diagram genereren vanuit bestaande database |
| Erwin / PowerDesigner | Enterprise, betaald | Volledig beheer van enterprise datamodellen |
Data modelleren is geen optionele stap — het is de fundering waarop alle analyses, rapportages en datapipelines worden gebouwd. Begin altijd met een conceptueel model samen met de business, werk toe naar een logisch model en implementeer ten slotte het fysieke model in de gekozen technologie. Gebruik normalisatie om redundantie te vermijden voor transactionele databases, en gebruik het star schema voor analytische omgevingen. Met de juiste aanpak en tools bouw je datamodellen die jarenlang meekunnen.
← Terug naar het blogoverzicht