Incremental 모델

Incremental 모델 (증분 모델)

Incremental 모델은 데이터 웨어하우스에 테이블로 빌드되지만, 매번 전체 데이터가 아니라 원하는 행만 변환해 기존 테이블에 넣는 방식이에요. 첫 실행 때는 소스 데이터의 모든 행을 변환해 테이블을 만들고, 이후 실행에서는 필터링한 새 행만 insert해요. 보통 그 필터는 지난 실행 이후 생성·갱신된 행이죠.

이 방식은 변환할 데이터량을 크게 줄여 런타임을 줄이고, 웨어하우스 성능을 높이며 컴퓨팅 비용을 낮춰요. 다만 추가 설정이 필요하고, is_incremental() 매크로의 동작을 이해해야 해요.

출처: Configure incremental models

Incremental materialization 구성하기

다른 materialization처럼 incremental 모델도 select 문으로 정의하고, materialization을 config 블록에 지정해요.

{{
    config(
        materialized='incremental'
    )
}}

select ...

incremental 모델을 쓰려면 dbt에게 두 가지를 알려줘야 해요.

  • incremental 실행에서 행을 어떻게 필터링할지
  • 모델의 고유 키(unique key)가 있다면 그것

is_incremental() 매크로 이해하기

is_incremental() 매크로가 incremental materialization을 구동해요. 다음 모든 조건이 충족되면 True를 반환해요.

  • 모델이 이미 데이터베이스에 테이블로 존재한다
  • full-refresh 플래그가 전달되지 않았다
  • 실행 중인 모델이 materialized='incremental'로 구성돼 있다

모델의 SQL은 is_incremental()TrueFalse든 유효해야 한다는 점을 기억하세요.

incremental 실행에서 행 필터링하기

incremental 실행에서 변환할 행을 지정하려면 그 행을 필터링하는 유효한 SQL을 is_incremental() 매크로 안에 감싸면 돼요. 보통 '새로운' 행, 즉 지난 실행 이후 생성된 행을 필터링하고 싶을 거예요. 가장 최근 실행 시각을 찾는 가장 좋은 방법은 대상 테이블의 최신 타임스탬프를 확인하는 것이고, {{ this }} 변수로 대상 테이블을 쉽게 조회할 수 있어요.

새 레코드와 갱신된 레코드를 모두 잡고 싶다면 unique_key를 정의해서 수정된 레코드가 중복으로 들어오지 않게 해야 해요. 예를 들어 컬럼에 계산 비용이 큰 변환이 있는 모델을 incremental로 만들 수 있어요.

{{
    config(
        materialized='incremental'
    )
}}

select
    *,
    my_slow_function(my_column)

from {{ ref('app_data_events') }}

{% if is_incremental() %}

  -- this filter will only be applied on an incremental run
  -- (uses >= to include records whose timestamp occurred since the last run of this model)
  -- (If event_time is NULL or the table is truncated, the condition will always be true and load all records)
where event_time >= (select coalesce(max(event_time),'1900-01-01') from {{ this }} )

{% endif %}

CTE를 쓰는 더 복잡한 incremental 모델은 is_incremental() 매크로의 위치가 쿼리 성능에 영향을 줄 수 있어요. 어떤 웨어하우스에서는 레코드를 일찍 필터링하는 게 런타임을 크게 줄여줘요.

unique_key 정의하기

선택적인 unique_key 파라미터를 정의하면 새 행을 추가만 하는 대신 기존 행을 업데이트할 수 있어요. 기존 unique_key에 새 정보가 오면 기존 정보를 대체하고, 중복 행이 오면 무시할 수 있어요.

unique_key를 지정하지 않으면 대부분의 어댑터는 append-only 동작을 해요. 즉 모델 SQL이 반환하는 모든 행을 중복 여부와 상관없이 기존 테이블에 insert해요.

unique_key는 모델의 grain(단일 고유 행을 식별하는 필드)을 정의해요. config 블록에서 단일 컬럼명 또는 컬럼명 리스트로 지정할 수 있어요.

{{
    config(
        materialized='incremental',
        unique_key='date_day'
    )
}}

select
    date_trunc('day', event_at) as date_day,
    count(distinct user_id) as daily_active_users

from {{ ref('app_data_events') }}


{% if is_incremental() %}

  -- this filter will only be applied on an incremental run
  -- (uses >= to include records arriving later on the same day as the last run of this model)
  where date_day >= (select coalesce(max(date_day), '1900-01-01') from {{ this }})

{% endif %}

group by 1

unique_key가 있으면 새 데이터 행마다 이렇게 동작해요.

  • 같은 unique_key가 '새'·'옛' 데이터 모두에 있으면 dbt가 옛 행을 새 행으로 update/replace해요. 정확한 메커니즘은 데이터베이스·incremental 전략·전략별 설정에 따라 달라져요.
  • unique_key가 '옛' 데이터에 없으면 dbt가 행 전체를 테이블에 insert해요.

여러 컬럼을 조합해 각 행을 고유하게 식별해야 한다면 리스트(unique_key = ['user_id', 'session_number'])로 전달하는 걸 권장해요. 문자열 표현식보다 더 보편적이고, dbt가 컬럼을 데이터베이스에 맞게 템플릿할 수 있기 때문이에요. 이렇게 쓸 때 각 컬럼에 null이 없어야 해요. null이 있으면 행 매칭에 실패해 중복 행이 생길 수 있어요. coalesce로 null을 처리하거나, dbt_utils.generate_surrogate_key 같은 서로게이트 키로 단일 컬럼을 정의하는 게 좋아요.

unique_key 컬럼에 기존 테이블이나 새 incremental 행 어느 쪽에서든 여러 행이 있으면, 데이터베이스와 전략에 따라 모델이 실패할 수 있어요. 문제가 생기면 unique key가 양쪽 모두에서 진짜로 유일한지 확인해 보세요. 참고로 delete+insert·merge 같은 일반 전략은 unique_key를 쓰지만, insert_overwrite는 파티션 단위로 동작해 unique_key를 쓰지 않아요.

스키마 변경 처리 (on_schema_change)

incremental 모델은 선택적인 on_schema_change 파라미터로 컬럼 변경 시 제어를 강화할 수 있어요. 이를 통해 스키마 변경이 있어도 incremental 모델을 계속 실행할 수 있고, --full-refresh를 덜 써서 쿼리 비용을 아낄 수 있어요.

on_schema_change의 가능한 값은 다음과 같아요.

  • ignore: 기본 동작.
  • fail: 소스와 대상 스키마가 어긋나면 에러 메시지를 띄운다.
  • append_new_columns: 새 컬럼을 기존 테이블에 추가한다(더 이상 없는 컬럼은 제거하지 않음).
  • sync_all_columns: 새 컬럼을 추가하고 사라진 컬럼도 제거한다(타입 변경 포함).

모든 on_schema_change 동작은 새로 추가된 컬럼에 대해 옛 레코드의 값을 채워주지 않아요. 그 값을 채워야 한다면 수동 업데이트를 실행하거나 --full-refresh를 트리거하는 게 좋아요. 그리고 on_schema_change는 최상위 컬럼 변경만 추적하고, 중첩 컬럼 변경(BigQuery 등)은 추적하지 않는다는 점도 기억하세요.

모델 재구축하기

incremental 모델 로직이 바뀌면 새 행의 변환이 대상 테이블에 저장된 과거 변환과 어긋날 수 있어요. 이때는 모델을 재구축해야 해요. --full-refresh 플래그로 전체 모델을 처음부터 다시 빌드하게 할 수 있어요. 이 플래그는 기존 대상 테이블을 드롭하고 전체 기간에 대해 다시 빌드해요.

$ dbt run --full-refresh --select my_incremental_model+

명령의 끝 +my_incremental_model에 의존하는 모든 하위 모델도 함께 실행하게 해요. 하위 의존성 중 incremental 모델도 있다면 그것들도 full-refresh돼요. full_refresh config를 쓰면 프로젝트나 리소스 수준에서 항상 또는 절대 full-refresh하지 않도록 지정할 수도 있어요.

더 알아보기 (Learn more)