Van architectuurkeuze tot productie: een complete handleiding voor het ontwerpen en bouwen van een schaalbaar, performant datawarehouse op moderne cloud platforms.
De keuze voor je datawarehouse platform bepaalt je kosten, schaalbaarheid en onderhoudslasten voor jaren. Cloud-native platforms zijn voor de meeste organisaties de beste keuze.
| Platform | Type | Sterk in | Kosten model |
|---|---|---|---|
| Snowflake | Cloud SaaS | Multi-cloud, eenvoud, data sharing | Pay-per-second compute |
| Azure Synapse | Cloud PaaS | Azure integratie, hybride | DWU of serverless |
| Databricks | Cloud PaaS | Big data, ML, streaming | DBU per cluster |
| BigQuery | Cloud SaaS | Google ecosystem, serverless | Pay-per-query of flat rate |
| SQL Server | On-prem/Cloud | Bestaande MS licenties | Licentie + hardware |
Het Medallion-architectuurpatroon organiseert data in drie lagen met toenemende kwaliteit en transformatiegraad. Dit is de industriestandaard voor moderne datawarehouses.
| Laag | Alias | Inhoud | Wie schrijft |
|---|---|---|---|
| Bronze | Raw / Landing | Ruwe brondata, exact zoals ontvangen | Ingest pipeline |
| Silver | Clean / Curated | Gecleand, gevalideerd, verrijkt | dbt / Spark |
| Gold | Serving / Mart | Business-ready, geaggregeerd | dbt / SQL |
_ingested_at, _source_file, _batch_idCREATE TABLE IF NOT EXISTS bronze.orders_raw ( order_id STRING, customer_id STRING, order_date STRING, -- ruwe data: altijd STRING in bronze amount STRING, status STRING, _ingested_at TIMESTAMP DEFAULT current_timestamp(), _source_file STRING, _batch_id STRING ) USING DELTA PARTITIONED BY (date_trunc('day', _ingested_at)) LOCATION 'abfss://bronze@storage.dfs.core.windows.net/orders/';
INSERT INTO silver.orders SELECT order_id, customer_id, CAST(order_date AS DATE) AS order_date, CAST(amount AS DECIMAL(18,2)) AS amount, LOWER(status) AS status, _ingested_at, current_timestamp() AS _transformed_at FROM bronze.orders_raw WHERE order_id IS NOT NULL -- validatie AND amount ~ '^[0-9]+(\.[0-9]+)?$' -- numeriek check AND _ingested_at > ( SELECT MAX(_ingested_at) FROM silver.orders ); -- incrementeel laden
Consistente naamgeving en juiste datatypes voorkomen verwarring en zorgen voor optimale query performance.
| Element | Conventie | Voorbeeld |
|---|---|---|
| Schema | snake_case, enkelvoud | bronze, silver, gold, mart_sales |
| Feitentabel | fct_ prefix | fct_orders, fct_revenue |
| Dimensietabel | dim_ prefix | dim_customer, dim_product |
| Staging | stg_ prefix | stg_salesforce_orders |
| Primary key | tabel_id | order_id, customer_id |
| Foreign key | dim_tabel_id | customer_id, product_id |
| Datum kolom | _at (timestamp), _date (date) | created_at, order_date |
| Boolean | is_ prefix | is_active, is_deleted |
CREATE TABLE dim_customer ( customer_sk INTEGER NOT NULL, -- surrogate key: integer customer_id VARCHAR(50) NOT NULL, -- business key: string customer_name VARCHAR(200) NOT NULL, email VARCHAR(254), -- max email lengte country_code CHAR(2), -- ISO 3166-1: altijd 2 tekens annual_revenue DECIMAL(18, 2), -- geld: nooit FLOAT customer_tier VARCHAR(20) CHECK (customer_tier IN ('bronze', 'silver', 'gold')), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP, valid_from DATE NOT NULL, -- SCD2 valid_to DATE, -- NULL = huidig actief record CONSTRAINT pk_dim_customer PRIMARY KEY (customer_sk) );
FLOAT of DOUBLE voor geldbedragen — floating point geeft afrondingsfouten. Gebruik altijd DECIMAL(p, s) of NUMERIC(p, s).
In een datawarehouse kies je tussen twee schema ontwerpen. Voor de meeste BI-toepassingen is het ster-schema de beste keuze.
| Eigenschap | Ster-schema | Sneeuwvlokschema |
|---|---|---|
| Structuur | Dimensies gedenormaliseerd | Dimensies genormaliseerd (3NF) |
| Joins voor query | Weinig (1 niveau) | Meer (meerdere niveaus) |
| Query performance | Sneller | Langzamer |
| Opslagruimte | Meer (redundantie) | Minder |
| Onderhoud | Eenvoudiger | Complexer |
| Gebruik | BI/rapportage | OLTP-achtige situaties |
CREATE TABLE fct_orders ( -- Surrogate keys (foreign keys naar dimensies) date_sk INTEGER NOT NULL REFERENCES dim_date(date_sk), customer_sk INTEGER NOT NULL REFERENCES dim_customer(customer_sk), product_sk INTEGER NOT NULL REFERENCES dim_product(product_sk), channel_sk INTEGER NOT NULL REFERENCES dim_channel(channel_sk), -- Business key (voor traceerbaarheid) order_id VARCHAR(50) NOT NULL, -- Feiten (meetbare waarden) quantity INTEGER NOT NULL, unit_price DECIMAL(18,2) NOT NULL, discount_amount DECIMAL(18,2) DEFAULT 0, revenue DECIMAL(18,2) NOT NULL, cost DECIMAL(18,2), CONSTRAINT pk_fct_orders PRIMARY KEY (order_id) );
De keuze tussen ETL en ELT bepaalt waar je transformatielogica plaatst en welke tools je gebruikt.
| Aspect | ETL | ELT |
|---|---|---|
| Transformatie | Buiten het warehouse (ETL tool) | Binnen het warehouse (SQL/dbt) |
| Tools | SSIS, Informatica, Talend | dbt, Spark, BigQuery scripting |
| Schaalbaarheid | Beperkt door ETL server | Schaalt mee met warehouse |
| Kosten | Licentiekosten ETL tool | Warehouse compute kosten |
| Wanneer | Complexe transformaties, legacy | Cloud DWH, moderne aanpak |
Partitionering en clustering zorgen ervoor dat queries alleen relevante data lezen (partition pruning / cluster pruning), wat enorm scheelt in scankosten en snelheid.
-- Partitioneer feitentabellen op datum CREATE TABLE fct_orders USING DELTA PARTITIONED BY (order_year INT, order_month INT) AS SELECT *, YEAR(order_date) AS order_year, MONTH(order_date) AS order_month FROM silver.orders; -- Z-ORDER clustering voor snellere point queries OPTIMIZE fct_orders ZORDER BY (customer_id, product_id); -- Vacuum (verwijder oude Delta bestanden) VACUUM fct_orders RETAIN 168 HOURS; -- 7 dagen history
-- Clustering key instellen ALTER TABLE fct_orders CLUSTER BY (order_date, customer_id); -- Clustering status controleren SELECT SYSTEM$CLUSTERING_INFORMATION('fct_orders'); -- Handmatige recluster (bij grote wijzigingen) ALTER TABLE fct_orders RECLUSTER;
-- Slecht: subquery die per rij wordt uitgevoerd SELECT customer_id, (SELECT COUNT(*) FROM fct_orders o2 WHERE o2.customer_id = o1.customer_id) AS order_count FROM dim_customer o1; -- Goed: window function of join met aggregatie SELECT c.customer_id, COALESCE(o.order_count, 0) AS order_count FROM dim_customer c LEFT JOIN ( SELECT customer_id, COUNT(*) AS order_count FROM fct_orders GROUP BY customer_id ) o ON c.customer_id = o.customer_id; -- Window function: running total per klant SELECT customer_id, order_date, amount, SUM(amount) OVER ( PARTITION BY customer_id ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cumulative_revenue FROM fct_orders;
-- Azure Synapse: statistieken handmatig updaten UPDATE STATISTICS fct_orders (order_date) WITH FULLSCAN; UPDATE STATISTICS fct_orders (customer_id) WITH SAMPLE 30 PERCENT; -- PostgreSQL: automatisch (maar handmatig forceren na bulk load) ANALYZE fct_orders; VACUUM ANALYZE fct_orders;
CACHE TABLE of Delta Live Tables.
Een goed datawarehouse heeft monitoring voor zowel technische performance als datakwaliteit. Slechte data die ongedetecteerd blijft schaadt vertrouwen in alle downstream rapportages.
-- 1. Volledigheid: controleer verwacht aantal rijen SELECT DATE_TRUNC('day', order_date) AS dag, COUNT(*) AS aantal_orders, SUM(amount) AS totaal_omzet FROM fct_orders WHERE order_date >= CURRENT_DATE - INTERVAL '7 days' GROUP BY 1 ORDER BY 1 DESC; -- 2. Uniciteit: geen dubbele primaire sleutels SELECT order_id, COUNT(*) AS duplicaten FROM fct_orders GROUP BY order_id HAVING COUNT(*) > 1; -- 3. Referentiële integriteit SELECT COUNT(*) AS orphaned_orders FROM fct_orders o LEFT JOIN dim_customer c ON o.customer_sk = c.customer_sk WHERE c.customer_sk IS NULL; -- 4. Freshness: data niet ouder dan 26 uur SELECT MAX(_ingested_at) AS laatste_ingest, DATEDIFF('hour', MAX(_ingested_at), CURRENT_TIMESTAMP()) AS uren_geleden, CASE WHEN DATEDIFF('hour', MAX(_ingested_at), CURRENT_TIMESTAMP()) > 26 THEN 'ALERT: data te oud' ELSE 'OK' END AS status FROM fct_orders;