LLM WikiAccess-protected knowledge portal
← 스터디 홈
4편 · 약 13분

증분 모델(Incremental Models)

왜 증분 모델인가

table Materialization은 dbt run을 실행할 때마다 테이블을 DROP + CREATE한다. 1억 행짜리 이벤트 로그를 매일 전체 재생성하면 시간과 비용이 선형으로 늘어난다. 어제까지 처리된 데이터는 바뀌지 않았는데 매번 다시 읽는 것은 낭비다.

Incremental Materialization은 이 문제를 해결한다. 처음 실행(Full Run)에서는 전체 데이터를 로드하고, 이후 실행(Incremental Run)에서는 신규·변경된 행만 대상 테이블에 반영한다. 실제로 500GB짜리 이벤트 테이블 기준으로 전체 재생성이 45분이라면 증분 실행은 2~3분으로 줄어드는 경우가 흔하다.

table Materialization (매번 전체 재생성) 1일차: 전체 100만 행 처리 2일차: 전체 120만 행 처리 3일차: 전체 145만 행 처리 비용·시간이 데이터 총량에 비례
incremental Materialization (신규만 처리) 1일차: 전체 100만 행 처리 (최초) 2일차: 신규 20만 행만 처리 3일차: 신규 25만 행만 처리 비용·시간이 신규 데이터량에 비례
전체 재생성 vs 증분 실행 비교

기본 구조: 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 UPDATE 없음 ✓ 가장 빠름 ✗ 중복 발생 가능 이벤트·로그 merge unique_key 기반 INSERT + UPDATE ✓ 중복 방지 ✗ 전체 스캔 비용 주문·고객 테이블 delete+insert 키 삭제 후 재삽입 파티션 단위 가능 ✓ merge보다 빠름 ✗ 원자성 주의 대용량 Snowflake insert_overwrite 파티션 통째 교체 파티션 컬럼 필수 ✓ BQ에서 저비용 ✗ 파티션 컬럼 필수 BigQuery 파티션 microbatch 시간 배치 자동화 event_time 기반 ✓ 장애 복구 쉬움 ✗ dbt 1.9+ 필요 대용량 시계열 전략 선택 가이드 • 이벤트·로그 (불변 데이터): append • 변경되는 레코드 (주문·고객, 100M행 이하): merge • 대용량 Snowflake / Databricks: delete+insert • BigQuery 파티션 테이블: insert_overwrite • 대용량 시계열 + 장애 복구 필요: microbatch (dbt 1.9+) • 전략을 모를 때 시작점: merge incremental_predicates로 merge 스캔 범위를 파티션 단위로 제한하면 대용량에서도 merge가 실용적이다.
5가지 증분 전략 비교

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