증분 모델(Incremental Models)
왜 증분 모델인가
table Materialization은 dbt run을 실행할 때마다 테이블을 DROP + CREATE한다. 1억 행짜리 이벤트 로그를 매일 전체 재생성하면 시간과 비용이 선형으로 늘어난다. 어제까지 처리된 데이터는 바뀌지 않았는데 매번 다시 읽는 것은 낭비다.
Incremental Materialization은 이 문제를 해결한다. 처음 실행(Full Run)에서는 전체 데이터를 로드하고, 이후 실행(Incremental Run)에서는 신규·변경된 행만 대상 테이블에 반영한다. 실제로 500GB짜리 이벤트 테이블 기준으로 전체 재생성이 45분이라면 증분 실행은 2~3분으로 줄어드는 경우가 흔하다.
기본 구조: is_incremental() 매크로
증분 모델의 핵심은 is_incremental() 매크로다. 현재 실행이 증분 실행일 때만 true를 반환한다. 이를 이용해 처리할 신규 행만 필터링한다.
-- models/marts/fct_events.sql
{{
config(
materialized='incremental',
unique_key='event_id'
)
}}
select
event_id,
user_id,
event_type,
occurred_at
from {{ ref('stg_events') }}
{% if is_incremental() %}
where occurred_at > (select max(occurred_at) from {{ this }})
{% endif %}{{ this }}는 이 모델이 만드는 테이블 자신을 가리키는 dbt 내장 변수다. select max(occurred_at) from {{ this }}는 "지금까지 적재된 가장 최근 타임스탬프"를 구하는 관용 패턴이다.
처음 실행 시에는 대상 테이블이 없으므로 is_incremental()이 false를 반환해 필터 없이 전체를 로드한다.
unique_key: 중복 방지의 핵심
unique_key는 행을 식별하는 컬럼(또는 복합 컬럼 목록)을 지정한다. 신규 행이면 INSERT, 기존 행이면 UPDATE(또는 REPLACE) 처리를 결정하는 기준이다.
config(
materialized='incremental',
unique_key='order_id' # 단일 컬럼
# unique_key=['order_id', 'product_id'] # 복합 키
)unique_key를 지정하지 않으면 신규 행을 무조건 INSERT한다(append 동작). 이때 이미 있는 행을 다시 처리하면 중복이 발생하므로, 불변 데이터가 아닌 경우 반드시 지정해야 한다.
unique_key컬럼에 NULL이 있으면 매칭이 실패해 중복이 발생한다. NULL 없이 관리되는 컬럼을 사용해야 한다.
증분 전략 비교
dbt는 다섯 가지 증분 전략을 지원한다.
append
신규 행만 INSERT하고 기존 행을 건드리지 않는다. unique_key 없이도 동작한다. 이벤트 로그처럼 한번 기록되면 바뀌지 않는 불변 데이터에 적합하다. 단, 재처리 시 중복이 발생할 수 있다.
merge
unique_key를 기준으로 MERGE 문을 실행한다. 일치하는 행이 있으면 UPDATE, 없으면 INSERT한다. 직관적이고 중복이 없지만, 전체 테이블을 스캔해 매칭하므로 대용량에서는 비용이 높다.
delete+insert
unique_key와 일치하는 행을 먼저 DELETE하고 새 행을 INSERT한다. Snowflake·Databricks에서 MERGE보다 최대 3.4배 빠른 경우가 있다. DELETE와 INSERT가 분리되므로 트랜잭션을 지원하는 웨어하우스에서 사용해야 중간 실패 시 일관성이 유지된다.
insert_overwrite
BigQuery에서 가장 효율적인 전략이다. 파티션 컬럼을 기준으로 영향받는 파티션만 통째로 교체한다. MERGE 없이 파티션 단위 덮어쓰기라 비용이 낮다. 파티션 컬럼이 없으면 사용 불가.
microbatch (dbt 1.9+)
event_time 컬럼과 batch_size(hour/day/week 등)를 설정하면, dbt가 시간 구간을 자동으로 쪼개 배치 단위로 처리한다. is_incremental() 블록을 직접 작성할 필요가 없다. 장애 발생 시 실패한 배치만 재실행할 수 있어 복구가 쉽다.
{{
config(
materialized='incremental',
incremental_strategy='microbatch',
event_time='occurred_at',
batch_size='day',
begin='2024-01-01',
lookback=3
)
}}
select * from {{ ref('stg_events') }}incremental_predicates: 스캔 범위 최적화
merge 전략에서 unique_key 매칭을 위해 전체 테이블을 스캔하는 비용을 incremental_predicates로 줄일 수 있다. 대상 파티션을 제한하는 조건절을 추가한다.
config(
materialized='incremental',
unique_key='event_id',
incremental_strategy='merge',
incremental_predicates=[
"DBT_INTERNAL_DEST.occurred_at >= dateadd(day, -7, current_date)"
]
)DBT_INTERNAL_DEST는 기존 대상 테이블을 가리키는 dbt 내부 별칭이다. 이 설정 하나로 MERGE 대상을 최근 7일 파티션으로 제한해 스캔 비용을 대폭 줄일 수 있다.
늦게 도착하는 데이터 처리
실시간 파이프라인에서는 이벤트가 발생 시간보다 몇 시간~며칠 뒤에 도착하는 경우가 있다. max(updated_at) 커트오프를 그대로 쓰면 늦게 도착한 데이터가 누락된다.
{% if is_incremental() %}
-- 최근 3일을 다시 처리해 늦은 도착 데이터 포함
where occurred_at > (
select dateadd(day, -3, max(occurred_at)) from {{ this }}
)
{% endif %}룩백(Lookback) 창을 크게 할수록 늦은 데이터를 더 많이 잡지만, 처리 비용도 늘어난다. 실제 데이터의 지연 분포를 분석해 99퍼센타일 지연 기준으로 창 크기를 결정하는 것이 실용적이다.
Full Refresh: 전체 재생성 강제
증분 모델 로직을 변경하거나, 증분 실행이 누적되어 데이터 드리프트가 의심될 때는 전체 재생성이 필요하다.
dbt run --full-refresh --select fct_events--full-refresh 플래그를 주면 is_incremental()이 강제로 false로 평가되어 전체 테이블을 재생성한다. 많은 팀에서 주말마다 전체 재생성을 스케줄해 드리프트를 자동으로 교정한다.
증분 모델에서 dbt_project.yml에 +full_refresh: false를 설정하면, 실수로 --full-refresh를 전달해도 해당 모델은 재생성되지 않는다. 재생성하면 안 되는 대용량 테이블 보호에 유용하다.
증분 모델 적용 체크리스트
| 항목 | 설명 |
|---|---|
| Materialization 설정 | materialized='incremental' |
| unique_key 지정 | 중복 방지가 필요한 경우 반드시 지정 |
| is_incremental() 필터 | 신규 행 식별 컬럼(타임스탬프 등) 기준 |
| 증분 전략 선택 | 웨어하우스와 데이터 특성에 맞게 |
| 룩백 창 설정 | 늦은 도착 데이터가 있는 경우 |
| Full Refresh 주기 | 로직 변경 시 또는 주기적 교정 |
| incremental_predicates | 대용량 merge 비용 최적화 |
한 줄 정리
증분 모델은 is_incremental() 매크로로 신규 행만 필터링하고, append·merge·delete+insert·insert_overwrite·microbatch 전략 중 데이터 특성과 웨어하우스에 맞는 것을 선택해 비용과 성능을 최적화한다.
References
- https://docs.getdbt.com/docs/build/incremental-models
- https://docs.getdbt.com/docs/build/incremental-strategy
- https://docs.getdbt.com/docs/build/incremental-microbatch
- https://docs.getdbt.com/best-practices/materializations/4-incremental-models
- https://docs.getdbt.com/reference/resource-configs/unique_key
- https://www.ryankirsch.dev/blog/dbt-incremental-models-guide
- https://dataworkers.io/resources/dbt-incremental-models-guide/
- https://dataengineeracademy.com/blog/incremental-data-models-in-dbt-append-merge-and-snapshot-strategies/
- https://dataopsleadership.substack.com/p/dbt-incremental-models-cookbook-merge