Introductie
dbt is in vrijwel elk voorgaand hoofdstuk als transformatietool genoemd — voor medallion architecture, voor SCD Type 2 via snapshots, als het ELT-gereedschap bij uitstek. Dit hoofdstuk behandelt dbt als tool zelf, in de diepte die eerdere hoofdstukken bewust oversloegen: wat dbt precies wel en niet doet, hoe Jinja en macro's SQL herbruikbaar maken, hoe de dependency graph exact wordt opgebouwd, hoe incrementele modellen technisch werken, en het verschil tussen de twee soorten tests die dbt aanbiedt.
Wat dbt Precies Is (en Wat het Niet Is)
De meest voorkomende misvatting bij mensen die net met dbt beginnen: dbt is geen database, geen orchestrator, en verplaatst zelf geen data tussen systemen. dbt is een compiler. Je schrijft SQL met Jinja-templating erin; dbt compileert dat naar pure, uitvoerbare SQL en stuurt die naar je warehouse om uit te voeren. Het warehouse doet al het rekenwerk — dbt orkestreert alleen welke SQL-statements in welke volgorde worden uitgevoerd, gebaseerd op de afhankelijkheden die jij declareert.
Dit verklaart meteen waarom dbt zo goed past bij het ELT-patroon uit ETL vs ELT: omdat dbt zelf geen compute levert, profiteert het volledig van de compute die het warehouse toch al biedt, zonder een aparte verwerkingslaag te introduceren. Het verklaart ook waarom dbt niets doet aan extractie of laden — dat is het domein van Fivetran, Airbyte, of custom scripts; dbt begint pas zodra data al, ruw, in je warehouse staat.
De Kern-Bouwstenen
Models zijn .sql-bestanden met een SELECT-statement — de basiseenheid van transformatie. dbt compileert elk model naar een CREATE TABLE of CREATE VIEW-statement, afhankelijk van de materialization.
Sources zijn declaraties van externe, ruwe tabellen die dbt niet zelf beheert — meestal je bronze-laag, geladen door een los ingestion-proces. Door bronnen expliciet te declareren in sources.yml in plaats van tabelnamen hard te coderen, kan dbt ook freshness-checks op die bronnen uitvoeren.
Seeds zijn kleine, statische CSV-bestanden die je als tabel wilt laden — een mapping van landcodes naar regio's, een lijst met feestdagen. Niet bedoeld voor operationele data, wel handig voor kleine referentietabellen die zelden wijzigen en onder versiebeheer horen.
Snapshots — al aangehaald in Data Vault 2.0 als dbt's ingebouwde mechanisme voor SCD Type 2 — leggen periodiek de staat van een brontabel vast en bouwen daar automatisch valid_from/valid_to-historie van op, zonder dat je zelf MERGE-logica hoeft te schrijven.
-- models/staging/stg_orders.sql — een model verwijst naar een source, niet naar een tabelnaam
SELECT
id AS order_id,
customer_id,
CAST(order_date AS DATE) AS order_date,
amount
FROM {{ source('raw', 'orders') }}
# models/staging/sources.yml — de source expliciet gedeclareerd, met freshness-check
sources:
- name: raw
tables:
- name: orders
loaded_at_field: _ingested_at
freshness:
warn_after: {count: 12, period: hour}
error_after: {count: 24, period: hour}
Jinja & Macro's: Waarom SQL Alleen Niet Genoeg Is
Puur SQL heeft geen manier om logica te herhalen zonder te copy-pasten. dbt lost dit op door elk model door een Jinja-templating-engine te halen vóórdat het naar het warehouse gaat — dezelfde templating-taal die achter veel Python-webframeworks zit. Dit opent de deur naar macro's: herbruikbare, parametriseerbare stukken SQL-logica, functioneel vergelijkbaar met een functie in een programmeertaal.
-- macros/cents_to_euros.sql — een herbruikbare macro
{% macro cents_to_euros(column_name) %}
ROUND({{ column_name }} / 100.0, 2)
{% endmacro %}
-- gebruik in een model — wordt bij compile-time uitgeschreven naar pure SQL
SELECT
order_id,
{{ cents_to_euros('amount_cents') }} AS amount_euros
FROM {{ ref('stg_orders') }}
Dit compileert naar exact ROUND(amount_cents / 100.0, 2) AS amount_euros — de macro voegt geen runtime-overhead toe, het is puur een manier om dezelfde SQL-uitdrukking op meerdere plekken consistent te hergebruiken, zodat een wijziging in de rekenregel maar op één plek hoeft te gebeuren. Grotere macro's kunnen loops ({% for %}), conditionele logica ({% if %}) en zelfs dynamisch kolommen genereren bevatten — krachtig, maar met de waarschuwing dat te veel Jinja-magie een model net zo onleesbaar maakt als de GUI-tools die dbt juist probeerde te vervangen.
Laat de dbt Generator een model met sources, tests en schema.yml voor je opzetten.
De Dependency Graph in Detail: ref() en source()
ref() en source() zijn de twee functies die de dependency graph (DAG) van een dbt-project vormen, en het is de moeite waard precies te begrijpen wat ze doen. {{ ref('stg_orders') }} verwijst niet naar een letterlijke tabelnaam — het verwijst naar het model stg_orders, en dbt vertaalt dat op compile-time naar de daadwerkelijke, volledig gekwalificeerde tabelnaam in de doelomgeving (dev_jan.stg_orders in een developer-sandbox, analytics.stg_orders in productie). Dit is precies wat een volgende sectie over environments mogelijk maakt: hetzelfde model-bestand, andere gecompileerde tabelnaam, zonder één regel code te wijzigen.
Belangrijker: omdat elk model expliciet declareert welke andere modellen en sources het gebruikt via ref()/source(), kan dbt bij elke run automatisch de juiste uitvoeringsvolgorde afleiden — een model dat {{ ref('stg_orders') }} bevat, wordt gegarandeerd pas gebouwd nadat stg_orders zelf gebouwd is, zonder dat jij ooit een expliciete volgorde hoeft te specificeren zoals in een handmatig onderhouden Airflow-DAG. Dit is het mechanisme achter de "dbt leidt de dependency graph automatisch af"-uitspraak uit Modern Data Platform Architecture — nu concreet: het is puur statische analyse van welke ref()/source()-aanroepen in de gecompileerde SQL van elk model voorkomen.
Incrementele Modellen in Diepte
Een materialized='table'-model herberekent bij elke run de volledige tabel vanaf nul — correct, maar onbetaalbaar zodra een tabel miljarden rijen bevat. Een materialized='incremental'-model verwerkt bij elke run alleen nieuwe of gewijzigde rijen, via het is_incremental()-macro dat dbt automatisch beschikbaar stelt:
-- models/marts/fct_orders.sql
{{ config(
materialized='incremental',
unique_key='order_id',
incremental_strategy='merge'
) }}
SELECT order_id, customer_id, order_date, amount
FROM {{ ref('stg_orders') }}
{% if is_incremental() %}
-- deze WHERE-clausule bestaat alleen bij incrementele runs, niet bij een full-refresh
WHERE order_date > (SELECT MAX(order_date) FROM {{ this }})
{% endif %}
{{ this }} verwijst naar de tabel die het model zelf produceert — hier gebruikt om het watermark-patroon uit Introduction to Data Engineering direct in dbt te implementeren. De incremental_strategy bepaalt wat er met de nieuwe rijen gebeurt zodra ze zijn geselecteerd, en dit verschilt fundamenteel per strategie:
merge— het standaardgedrag op de meeste moderne warehouses (Snowflake, Databricks, BigQuery): nieuwe rijen met een bestaandeunique_keyworden bijgewerkt, nieuwe worden ingevoegd — functioneel identiek aan deMERGE-patronen uit SQL for Data Engineers, maar door dbt gegenereerd.delete+insert— verwijdert eerst alle rijen die matchen opunique_key, voegt dan de nieuwe rijen in. Nuttig op platformen zonder nativeMERGE-ondersteuning, of wanneer een enkeleunique_key-waarde meerdere rijen in de nieuwe batch vertegenwoordigt (mergestaat dat niet toe).append— voegt simpelweg alle nieuwe rijen toe, zonder ooit te controleren op duplicaten. Alleen veilig wanneer je zeker weet dat de bron nooit dezelfde rij twee keer aanlevert — in de praktijk zeldzamer dan beginners verwachten.
Een incrementeel model kan altijd geforceerd volledig herbouwd worden met dbt run --full-refresh — essentieel na een wijziging in de transformatielogica zelf, omdat de incrementele WHERE-clausule anders alleen nieuwe rijen met de nieuwe logica zou verwerken, terwijl bestaande rijen de oude, inmiddels onjuiste berekening zouden behouden.
Testing: Generic versus Singular Tests
dbt onderscheidt twee soorten tests, met een net iets andere toepassing dan de laag-specifieke teststrategie uit Medallion Architecture — dit gaat over de vorm van de test, niet over welke laag je test.
Generic tests zijn herbruikbare, parametriseerbare tests die je in schema.yml aanroept zonder zelf SQL te schrijven — not_null, unique, relationships, accepted_values zijn ingebouwd, en packages als dbt_utils en dbt_expectations voegen tientallen extra toe (bijvoorbeeld: controleer of een kolom altijd binnen een bepaalde range valt).
Singular tests zijn custom .sql-bestanden in de tests/-map die een specifieke, vaak business-specifieke bewering controleren — het bestand moet nul rijen teruggeven om te slagen; elke teruggegeven rij is een gefaalde test-case:
-- tests/assert_positive_revenue.sql — een singular test
-- Slaagt als deze query GEEN rijen teruggeeft
SELECT order_id, revenue
FROM {{ ref('fct_revenue') }}
WHERE revenue < 0
De vuistregel: gebruik generic tests voor structurele garanties (bestaat de rij, is de sleutel uniek, valt de waarde binnen een verwachte set) en singular tests voor businessregels die specifiek genoeg zijn dat geen generieke test ze kan uitdrukken — zoals "omzet mag nooit negatief zijn" of "het totaal van orderregels moet gelijk zijn aan het ordertotaal".
Environments: Dev, Staging, Prod
Omdat ref() op compile-time wordt opgelost naar de juiste omgeving, kan hetzelfde dbt-project zonder wijziging tegen verschillende doelen draaien, geconfigureerd via profiles.yml:
# profiles.yml — dezelfde modellen, andere schema's per target
my_project:
target: dev
outputs:
dev:
type: snowflake
schema: dev_jan
threads: 4
prod:
type: snowflake
schema: analytics
threads: 8
Een engineer die lokaal ontwikkelt, draait tegen dev — een persoonlijk schema waar experimenteren geen risico vormt voor productiedata. CI/CD (zie CI/CD for Data Engineering) draait dezelfde modellen tegen prod. Geen enkel model-bestand hoeft te weten in welke omgeving het draait — dat is precies het punt.
dbt Core versus dbt Cloud
dbt Core is de open-source command-line tool: je schrijft modellen, draait dbt run/dbt test lokaal of via je eigen orchestrator (Airflow, Dagster), en beheert alles zelf. dbt Cloud is de gehoste variant van dbt Labs: een web-IDE, ingebouwde scheduling, hosted documentatie, en een job-orchestrator specifiek voor dbt-runs — praktisch een managed laag bovenop dezelfde dbt Core-engine. De keuze is vergelijkbaar met de "beheer het zelf versus laat het managen"-afweging die overal in dit handbook terugkomt: dbt Core geeft volledige controle en geen licentiekosten, dbt Cloud bespaart operationele last in ruil voor een abonnement.
Packages: Niet Alles Zelf Bouwen
dbt heeft een eigen packagesysteem (packages.yml), vergelijkbaar met pip voor Python of npm voor JavaScript, waarmee je herbruikbare macro's en tests van de community importeert in plaats van zelf te herschrijven. dbt_utils is het meest gebruikte pakket: datum-manipulatie, surrogate key-generatie (dbt_utils.generate_surrogate_key, relevant voor de hash keys uit Data Vault 2.0), en generieke tests zoals dbt_utils.recency die we eerder in Medallion Architecture gebruikten voor freshness-checks. dbt_expectations breidt dit uit met tientallen statistische en distributie-tests, geïnspireerd op het Python-framework Great Expectations. Voordat je een macro zelf schrijft voor iets generieks (surrogate keys, datumreeksen genereren, snelle string-manipulatie), is de kans groot dat dbt_utils het al oplost — zelf het wiel opnieuw uitvinden is hier zelden de moeite waard.
# packages.yml
packages:
- package: dbt-labs/dbt_utils
version: [">=1.0.0", "<2.0.0"]
Best Practices
- Behandel elk model als een pure functie van zijn
ref()/source()-inputs — geen verborgen afhankelijkheden buiten wat expliciet gedeclareerd is, anders breekt de automatisch afgeleide uitvoeringsvolgorde. - Gebruik
mergeals standaard incremental_strategy, en wijk daar alleen bewust van af (delete+insert, append) met een concrete reden. - Test structurele garanties generiek, businessregels singulier — probeer geen complexe businesslogica in een generic test te proppen die daar niet voor bedoeld is.
- Documenteer wanneer een
--full-refreshverplicht is (na wijziging van incrementele logica) in de PR-conventie van je team, zodat dit niet wordt vergeten. - Houd macro's leesbaar — een macro met drie geneste
{% if %}-blokken is vaak minder onderhoudbaar dan drie aparte, iets minder DRY modellen.
Veelgemaakte Fouten
- Tabelnamen hardcoderen in plaats van
ref()/source()te gebruiken — werkt in dev, breekt zodra hetzelfde model in een andere omgeving met andere schema-namen draait. appendgebruiken als incremental_strategy zonder te verifiëren dat de bron nooit duplicaten aanlevert — leidt tot langzaam groeiende, onopgemerkte duplicatie.- Vergeten een
--full-refreshte draaien na een wijziging in incrementele logica, waardoor oude en nieuwe rijen met inconsistente berekeningen naast elkaar blijven bestaan. - Te veel logica in Jinja-macro's stoppen in plaats van in leesbare SQL, wat modellen onbegrijpelijk maakt voor iemand die geen Jinja-expert is.
- Singular tests gebruiken voor iets dat een generic test met een package als dbt_utils al had kunnen afdekken — onnodig maatwerk voor een opgelost probleem.
Performance Tips
- Gebruik incrementele modellen zodra een tabel merkbaar traag wordt om volledig te herbouwen — er is geen vaste rijgrens, meet de daadwerkelijke build-tijd.
- Beperk
threadsinprofiles.ymlniet onnodig laag — dbt bouwt onafhankelijke modellen parallel; te weinig threads laat warehouse-compute onbenut. - Vermijd macro's die per rij worden geëvalueerd in plaats van eenmalig bij compile-time — Jinja draait vóór de SQL naar het warehouse gaat, dus complexe macro-logica kost geen runtime-overhead, maar een macro die per ongeluk N losse queries genereert in plaats van één wel.
- Gebruik
dbt buildin plaats van losdbt run+dbt testin CI/CD — het bouwt en test elk model direct na elkaar in dependency-volgorde, zodat een gefaalde test een downstream-model kan blokkeren vóór het draait, in plaats van pas achteraf te ontdekken dat het op foute data is gebouwd.
Interviewvragen
"Wat doet dbt precies, en wat doet het niet?" Let op: dbt compileert SQL met Jinja-templating en orkestreert de uitvoeringsvolgorde; het levert zelf geen compute en doet niets aan extractie of laden.
"Wat is het verschil tussen ref() en source()?"
Let op: ref() verwijst naar een ander dbt-model (intern beheerd), source() verwijst naar een externe, ruwe tabel die dbt niet zelf bouwt — beide dragen bij aan de automatisch afgeleide dependency graph.
"Leg de drie incremental_strategy-opties uit: merge, delete+insert, append." Let op: merge voor update-of-insert op een unique key (het standaardgeval), delete+insert wanneer een unique key meerdere rijen per batch vertegenwoordigt, append alleen wanneer duplicatie gegarandeerd niet kan optreden.
"Wanneer zou je een --full-refresh moeten draaien op een incrementeel model?"
Let op: na elke wijziging in de transformatielogica van het model zelf, omdat de incrementele WHERE-clausule anders alleen nieuwe rijen met de bijgewerkte logica zou verwerken.
"Wat is het verschil tussen een generic en een singular test in dbt?" Let op: generic tests zijn herbruikbaar en parametriseerbaar via schema.yml (not_null, unique, relationships); singular tests zijn custom SQL-bestanden voor specifieke businessregels, die slagen als ze nul rijen teruggeven.
"Hoe zorgt dbt ervoor dat hetzelfde project tegen zowel een dev- als een productieomgeving kan draaien zonder codewijziging?"
Let op: ref() wordt op compile-time opgelost naar de juiste, volledig gekwalificeerde tabelnaam op basis van het actieve target in profiles.yml — modellen zelf bevatten nooit een hardcoded schema.
Relevante Documentatie
- dbt Labs: Incremental Models — officiële uitleg van alle incremental_strategy-opties per adapter.
- dbt Labs: Jinja and Macros — de volledige Jinja-syntax die dbt ondersteunt.
- dbt Labs: Testing — generic versus singular tests in detail.
- dbt Labs: About dbt Core setup — het verschil tussen dbt Core en dbt Cloud.
Samenvatting
dbt is een SQL-compiler met een dependency-resolutiemechanisme, geen database en geen orchestrator — het genereert en ordent SQL, het warehouse voert het uit. Jinja en macro's maken SQL herbruikbaar op een manier die pure SQL niet kan; ref() en source() bouwen automatisch de dependency graph op basis van statische analyse van elk model. Incrementele modellen, met de juiste incremental_strategy, zijn de sleutel tot betaalbare verwerking naarmate tabellen groeien, en de twee testsoorten (generic voor structuur, singular voor businesslogica) dekken samen de teststrategie die in het vorige hoofdstuk per laag werd uitgewerkt. Met dbt als gereedschap in de vingers, is het tijd om de platforms te bekijken waar dit gereedschap tegen draait — te beginnen met Snowflake in het volgende hoofdstuk.
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