Waarom leveren je dbt-modellen in verschillende omgevingen uiteenlopende cijfers op?

cropped visuals
Tamara de Heij

Founder & Managing Director of i-spark.

Zelfde input, zelfde code, ander resultaat

Wanneer een dbt-pipeline verschillende uitkomsten geeft in verschillende omgevingen, ligt het aan een van vier dingen: andere inputdata, andere code, een ander moment van draaien, of niet-deterministische SQL. In dit artikel lopen we elke check langs, van de meest voorkomende en makkelijkst uit te sluiten tot de lastigste. De aanpak werkt voor elk warehouse: Snowflake, BigQuery, Databricks of Redshift.

Laten we erin duiken!

Stel, je hebt een dbt-project met een reeks modellen, dat zowel in TEST als in UAT draait. In TEST klopt alles. In UAT komt de klant met de vraag waarom er gisteren 1.204 orders waren en geen 1.207. Zelfde model, zelfde code, en toch een ander getal.

Dit komt vaker voor dan je denkt. Zeker in het begin is de verleiding groot om meteen in de SQL te duiken, of de pipeline gewoon nog een keer te draaien. Dat laatste lost soms zelfs iets op, maar dan weet je nog steeds niet waarom.

Stap 1: Controleer of beide omgevingen dezelfde bron lezen

De eerste vraag is altijd: krijgen beide omgevingen dezelfde data binnen? Verschilt de input, dan mag de output ook verschillen. Dan is er eigenlijk niets mis.

Meestal lezen alle omgevingen uit dezelfde bron en dezelfde raw-laag; pas de lagen daarna verschillen per omgeving. Dan kan het eigenlijk niet. Heeft een omgeving toch een eigen bron, dan is het zeker een kandidaat. En ook als de bron gelijk hoort te zijn: check of je pipeline daar écht uit leest.

Waar je uit leest, leg je vast in je configuratie. Dat TEST en UAT naar een ander doelschema schrijven is normaal; dat maakt ze juist TEST en UAT. Aan de leeskant wijzen alle omgevingen meestal naar dezelfde plek, dus daar zit zelden het probleem. Toch is het de moeite waard om dat even na te lopen, op deze drie plekken:

  • Connection string: waarmee en waarheen je verbinding maakt, zoals server, database en gebruiker. Check niet alleen de verbinding naar je doel, maar ook die naar de bron.
  • Omgevingsvariabelen: waardes die niet in je code staan, maar per omgeving worden ingesteld. In dbt haal je ze op met env_var(), bijvoorbeeld in je profiles.yml of in de definitie van je sources. De waarde zelf stel je in dbt Cloud per omgeving in, of in je CI of terminal. Zo draait dezelfde code met andere waardes.
  • Parameters: waardes die je per run of per model meegeeft, zoals een startdatum, een lookback-window of een filter. Zelfde bron, maar een ander stuk ervan.

Een voorbeeld in dbt:

sources:

  – name: erp

    database: “{{ env_var(‘SOURCE_DB’, ‘raw_prod’) }}”

Beide omgevingen horen uit raw_test te lezen. In TEST staat SOURCE_DB op raw_test. In UAT is de variabele vergeten, dus valt dbt terug op de default raw_prod. Zelfde code, zelfde operationele bron, en toch leest UAT andere data. Ook bij een klant waar “de bron altijd hetzelfde is”.

Dit voorkom je door env_var() geen default te geven voor alles wat bepaalt waar je leest. Schrijf dus env_var(‘SOURCE_DB’) in plaats van env_var(‘SOURCE_DB’, ‘raw_prod’). Ontbreekt de variabele, dan faalt dbt meteen met een foutmelding in plaats van stilletjes ergens anders te lezen. Wil je de waardes per omgeving naast elkaar zien? In dbt Cloud kan dat op de pagina met omgevingsvariabelen.

Tip: vergelijk eerst een paar simpele tellingen op de brontabellen, zoals het aantal rijen per dag. Dat is vaak al genoeg om deze stap af te vinken.

Stap 2: Controleer of beide omgevingen dezelfde codeversie draaien

Is de input gelijk, dan kijk ik naar de code. Draait in beide omgevingen dezelfde versie? Dat check je zo:

  1. Zoek per omgeving op welke commit er het laatst gedraaid heeft. Je deployment- of CI-logs laten dat zien, en in dbt Cloud staat de commit bij elke run.
  2. Vergelijk die twee commits met git diff <commit-test> <commit-uat> — models/ macros/. Is de uitkomst leeg, dan is je code gelijk.
  3. Twijfel je over één specifiek model? Vergelijk dan de gecompileerde SQL in target/compiled/ van beide omgevingen. Als het goed is verschillen alleen de schema’s.

Kijk ook even naar de dbt-versie en de versies van je packages per omgeving. Staat je package-lock.yml in Git, dan zit je daar meestal goed.

Stap 3: Controleer of beide omgevingen op hetzelfde moment hebben gedraaid

Bieden input en code geen verklaring, dan check ik de timing. Hebben beide omgevingen op een vergelijkbaar moment gedraaid? Zo niet, dan kijk je mogelijk gewoon tegen een timingverschil aan en is er niets mis.

Dit is verraderlijker dan het lijkt. Een paar voorbeelden:

  • Incrementele loads die in de ene omgeving een run verder zijn dan in de andere.
  • Filters op “gisteren”, zoals CURRENT_DATE – 1. Draai je net voor en net na middernacht, dan kijk je naar een andere dag.
  • Data die laat binnenkomt. Een order van 23:58 die pas om 00:15 in de bron staat, zit in de ene run wel en in de andere niet.

Een variant hierop: je draait in één omgeving maar een deel van je modellen, bijvoorbeeld alleen wat je net hebt aangepast. Of je incrementele modellen bouwen voort op een historie die in TEST anders is opgebouwd dan in UAT. Wil je de twee eerlijk vergelijken, draai dan in beide omgevingen een volledige dbt build –full-refresh. Dan begin je allebei vanaf hetzelfde punt. Bij grote tabellen duurt dat even, maar het scheelt je veel giswerk.

Ook dit is meestal redelijk eenvoudig te controleren. Kijk in de logs wanneer welke run gedraaid heeft, en wat de laadmomenten van de brondata waren.

Nog geen antwoord gevonden? Dit kan er aan de hand zijn:

Stap 4: Zoek naar niet-deterministische SQL, meestal in een deduplicatie

De meest voorkomende oorzaak van niet-deterministische resultaten in dbt: een ROW_NUMBER()-deduplicatie waarbij de ORDER BY niet alle gelijke waarden onderscheidt.

Zijn input, code en timing gelijk, dan zou de uitkomst ook gelijk moeten zijn. Is dat niet zo, dan is het aannemelijk dat er iets niet-deterministisch in je code zit. Oftewel: dezelfde query op dezelfde data kan een ander resultaat geven.

In de praktijk zit dat bijna altijd in een ontdubbeling. Let dan goed op de PARTITION BY en de ORDER BY.

Stel, je hebt een klantentabel waarin elke wijziging een nieuwe rij oplevert. Je wilt per klant alleen de meest recente rij:

SELECT *

FROM klanten

QUALIFY ROW_NUMBER() OVER (

    PARTITION BY klant_id

    ORDER BY gewijzigd_op DESC

) = 1

Dat ziet er goed uit. Maar kijk eens naar deze data:

record_id  klant_id  gewijzigd_op         segment

88412      1001      2026-10-12 09:14:00  Goud

88413      1001      2026-10-12 09:14:00  Zilver

87950      1002      2026-10-03 16:40:00  Goud

88104      1003      2026-10-08 11:05:00  Zilver

Klant 1001 heeft twee rijen met exact dezelfde gewijzigd_op. Welke krijgt nummer 1? Dat bepaal jij niet, dat bepaalt de database. En die kan in TEST de ene kiezen en in UAT de andere. Soms verschilt het zelfs per run, binnen dezelfde omgeving.

Kijk wat dat doet met een simpele vraag: hoeveel Goud-klanten hebben we?

OmgevingKlant 1001 wordtAantal Goud-klanten
TESTGoud2
UATZilver1

Zelfde data, zelfde code, en toch een ander antwoord. Hangt er een korting, een campagne of een omzetrapportage per segment aan dit veld, dan werkt dat verschil overal door.

De oplossing is een tiebreaker: een extra kolom in de ORDER BY die de volgorde altijd eenduidig maakt.

ORDER BY gewijzigd_op DESC, record_id DESC

Nu wint bij klant 1001 altijd record 88413, in elke omgeving en bij elke run. Of Zilver inhoudelijk de juiste keuze is, is een tweede vraag. Maar het antwoord is in ieder geval overal hetzelfde.

Belangrijk is om te realiseren dat de combinatie van PARTITION BY en ORDER BY uniek moet zijn. Is dat niet zo, dan hangt je uitkomst af van toeval.Hetzelfde geldt bij DISTINCT ON, FIRST_VALUE en LIMIT zonder eenduidige ORDER BY. 

QUALIFY werkt onder andere in Snowflake, BigQuery en Databricks. Elders gebruik je een subquery met hetzelfde idee.

Samengevat

Bij een verschil tussen omgevingen loop ik deze vragen af, in deze volgorde:

  1. Is de input gelijk? Brondata, maar ook connection strings, omgevingsvariabelen en parameters.
  2. Is de code gelijk?
  3. Is er op hetzelfde moment gedraaid?
  4. Zit er iets niet-deterministisch in, meestal een ontdubbeling?

De volgorde is geen wet. Ik begin met wat het meest waarschijnlijk is en het makkelijkst te checken. Weet je bij een project zeker dat de bron overal gelijk is, sla stap 1 dan gerust over.

Tip: schrijf een test die controleert of de combinatie van je PARTITION BY- en ORDER BY-kolommen uniek is. In dbt kan dat met dbt_utils.unique_combination_of_columns. Dan vang je het probleem uit stap 4 af voordat iemand er in UAT over struikelt.

Kom je er niet helemaal uit? Neem vrijblijvend contact met ons op. We kunnen altijd even meekijken!

Delen op LinkedIn
Email

Inhoud

Zijn jullie data klaar voor de toekomst?
Flexibele dataoplossingen die met je meegroeien
dbt models return different numbers 1

Waarom leveren je dbt-modellen in verschillende omgevingen uiteenlopende cijfers op?

Zelfde input, zelfde code, ander resultaat Wanneer een dbt-pipeline verschillende uitkomsten geeft in verschillende omgevingen, ligt het aan een van vier dingen: andere inputdata, andere...

Power BI x Claude 1

Claude Code en Power BI instellen: wat deze koppeling mogelijk maakt en hoe je deze correct instelt

Als je met Power BI werkt, weet je dat DAX al snel het probleem van één persoon kan worden. Juist omdat het zo lang duurt...

Power BI x dbt

Hoe je Claude Code aan dbt koppelt en hoe een correcte configuratie eruitziet

Als je met dbt werkt, weet je wel welke onderdelen je vertragen. Het schrijven van de YAML, het toevoegen van de tests en het up-to-date...