
Inkrementelle tabeller er et av de viktigste verktøyene vi har for å bygge skalerbare datamodeller. I stedet for å laste og prosessere hele datasettet på nytt hver gang, oppdaterer vi bare det som faktisk har endret seg siden forrige kjøring. Det gir raskere kjøretider, lavere kostnader i datavarehuset og modeller som tåler økende datamengder over tid.
Der inkrementell last ofte går galt
Inkrementell last i dbt virker enkelt i teorien, men går ofte galt i praksis. Mange modeller ser inkrementelle ut, men gjør enten full last hver gang, produserer duplikater eller blir unødvendig dyre å kjøre.
Årsaken er sjelden dbt i seg selv. Den ligger nesten alltid i hvordan inkrementell logikk er implementert. La oss starte med tre feller som går igjen i de fleste dbt-prosjekter.
Felle 1: Inkrementell materialisering uten inkrementell logikk
Den vanligste misforståelsen er å tro at materialiseringen alene gjør modellen inkrementell.
{{ config(materialized='incremental') }}
select *
from {{ ref('stg_timeforing') }}Her er det ingenting som forteller dbt hvilke rader som er nye eller oppdatert. Resultatet er at hele tabellen lastes på nytt hver gang. Modellen er teknisk sett inkrementell, men kjøres i praksis som en full last hver gang.
Inkrementell last forutsetter eksplisitt filtrering i SQL-en, vanligvis styrt av is_incremental() som evalueres før spørringen kjøres i datavarehuset:
...
from {{ ref('stg_timeforing') }}
{% if is_incremental() %}
where <endringsbetingelse>
{% endif %}Felle 2: Feil inkrementell strategi
Dette er en mer subtil feil: modellen kjører uten problemer, men dataene blir gradvis feil over tid. Mange velger append fordi det er enkelt og raskt, uten å vurdere om dataene faktisk er immutable.
{{ config(
materialized='incremental',
incremental_strategy='append'
) }}append legger kun til nye rader. Eksisterende rader oppdateres aldri. Hvis kilden endrer historiske data, for eksempel ved å korrigere timeføring bakover i tid, vil dbt legge inn nye rader i stedet for å oppdatere de gamle. Over tid fører dette til både inkonsistente data og duplikater i tabellen – uten at dbt gir noen advarsel.
Med mindre dataene er immutable, bør append unngås til fordel for strategier som merge (oppdaterer eksisterende rader) eller delete+insert (erstatter rader ved å slette og sette inn på nytt).
Felle 3: Inkrementell filtrering med faste tidsvinduer
En veldig vanlig tilnærming er å gjøre inkrementell filtrering med et fast tidsintervall, typisk “siste 7 dager”. Det ser fornuftig ut, men det er egentlig en antakelse om at alle relevante endringer alltid ligger innenfor vinduet.
{% if is_incremental() %}
where sist_oppdatert >= dateadd(day, -7, current_date)
{% endif %}I disse tilfellene skjer nesten alltid én av to ting:
- Enten prosesserer du for mye: hver kjøring leser og skriver om igjen et helt vindu, selv om svært få rader faktisk har endret seg. Det blir dyrt og tregt i datavarehuset uten at du får bedre datakvalitet.
2. ... eller så prosesserer du for lite: når data kommer forsinket, kan rader utenfor vinduet fortsatt være “nye” for mål-tabellen. Da filtreres de bort, og blir aldri lastet inn uten en eksplisitt backfill-mekanisme.
En mer robust tilnærming er å la inkrementell filtrering ta utgangspunkt i hva mål-tabellen faktisk inneholder. Her refererer {{ this }} til mål-tabellen som modellen skriver til i datavarehuset:
{% if is_incremental() %}
where sist_oppdatert > (select max(sist_oppdatert) from {{ this }})
{% endif %}Fellene over ser forskjellige ut, men har én ting til felles: de oppstår når man forventer at dbt skal forstå mer enn det den faktisk gjør. For å unngå dem, må man forstå hvordan dbt behandler inkrementelle modeller i praksis.
Hvordan dbt faktisk kjører inkrementelle modeller
Når du setter materialized='incremental', tar dbt ansvar for hvordan data skrives til mål-tabellen. Den tar ikke ansvar for hvilke rader som er nye. Det avgjør du i SQL-en.
Det starter alltid med konfigurasjonen:
{{ config(
materialized='incremental',
unique_key=['ansatt_id', 'dato'],
incremental_strategy='merge'
) }}Denne konfigurasjonen sier to viktige ting:
unique_keydefinerer hvordan rader matchesincremental_strategybestemmer hva som skjer når en rad allerede finnes
Første kjøring: alltid full last
På første kjøring finnes ikke mål-tabellen. Da ignoreres all inkrementell logikk, og dbt kjører en full last:
select
ansatt_id,
dato,
timer,
sist_oppdatert
from {{ ref('stg_timeforing') }}Datavarehuset ender opp med en spørring noe tilsvarende dette:
create or replace table fct_timeforing as (
select ...
);Senere kjøringer: strategi + filtrering
Først når tabellen finnes, blir modellen faktisk inkrementell. Da evalueres is_incremental() under kompilering, og dbt inkluderer WHERE-logikken før SQL-en sendes til datavarehuset.
Med denne filtreringen:
{% if is_incremental() %}
where sist_oppdatert >
(select max(sist_oppdatert) from {{ this }})
{% endif %}og denne strategien:
incremental_strategy='merge'vil dbt generere et MERGE-statement i datavarehuset som:
- matcher rader basert på
unique_key - oppdaterer eksisterende rader
- legger inn nye rader
Forenklet ser det slik ut:
merge into fct_timeforing T
using (select * from ...) S
on T.ansatt_id = S.ansatt_id
and T.dato = S.dato
when matched then update set ...
when not matched then insert ...Ved å sammenligne mot det modellen faktisk inneholder ({{ this }}), blir inkrementell last forutsigbar, repeterbar og robust – også når kjøringer feiler, data kommer sent eller historiske rader endres.
Derfor er inkrementell last robust først når filtreringen tar utgangspunkt i modellens faktiske tilstand, ikke i antakelser om når data burde ha kommet eller hvor stort et tidsvindu bør være.
Oppsummert under panseret
- dbt avgjør hvordan data skrives (via config)
- SQL-en avgjør hvilke rader som behandles
- Datavarehuset gjør selve jobben (
MERGE,INSERT, osv.)
Robust inkrementell last krever at:
- strategien støtter hvordan data faktisk endres
- filtreringen tar utgangspunkt i hva modellen allerede har lastet
Når begge deler stemmer, blir inkrementelle tabeller både riktige og forutsigbare – også når data er forsinket eller kjøringer feiler.