dbt를 처음 쓰기 시작하면 대부분 materialized='table'로 시작한다. 간단하고, 예측 가능하고, 디버깅이 쉽다. 문제는 데이터가 커졌을 때다. 수억 행짜리 테이블을 매번 full refresh하면 파이프라인이 몇 시간짜리가 된다.
그래서 incremental 모델을 쓴다. 이미 처리된 데이터는 건너뛰고, 새로 들어온 데이터만 처리한다. 개념은 단순하다. 구현은 복잡하다.
기본 구조
{{ config(materialized='incremental') }}
SELECT
event_id,
user_id,
event_type,
created_at
FROM {{ source('raw', 'events') }}
{% if is_incremental() %}
WHERE created_at > (SELECT MAX(created_at) FROM {{ this }})
{% endif %}
is_incremental() 블록은 처음 실행(full refresh)에서는 False, 이후 incremental 실행에서는 True를 반환한다. {{ this }}는 현재 모델의 기존 테이블을 가리킨다.
unique_key와 merge 전략
단순 append로는 부족한 경우가 많다. 이벤트가 수정될 수 있는 경우, 중복 데이터가 들어올 수 있는 경우. 이때 unique_key를 설정하면 dbt가 upsert(merge) 전략을 사용한다.
{{ config(
materialized='incremental',
unique_key='event_id',
incremental_strategy='merge'
) }}
BigQuery, Snowflake, Databricks에서 각각 지원하는 merge 문법이 다르다. dbt가 어댑터별로 처리해주지만, 동작 방식을 이해하고 있어야 예상치 못한 중복을 막을 수 있다.
증분 처리의 핵심은 “무엇이 새 데이터인가”를 정확히 정의하는 것이다.
언제 쓰면 안 되는가
작은 테이블에는 그냥 table이 낫다. Incremental 모델은 복잡성을 도입한다. 필터 조건이 틀리면 데이터 누락이 생기고, 발견하기 어렵다. 또한 스키마가 자주 바뀌는 초기 단계에서는 full refresh를 자주 해야 하므로 장점이 없다.
데이터 볼륨이 크고, 스키마가 안정적이고, SLA가 빡빡할 때 incremental을 도입해야 한다.