Materialization이란
Materialization은 dbt 모델이 웨어하우스에 어떤 형태로 저장되는지를 결정하는 전략이다. 같은 SELECT 문이라도 View로 만들 수도 있고, 물리적 Table로 만들 수도 있다. 이 선택이 빌드 시간, 쿼리 성능, 스토리지 비용을 좌우한다.
{{ config(materialized='incremental') }}
SELECT * FROM {{ ref('stg_orders') }}
모델 파일 상단에 config() 블록으로 지정하거나, dbt_project.yml에서 폴더 단위로 일괄 설정할 수 있다.
네 가지 전략
| 전략 | 저장 | 빌드 시간 | 쿼리 성능 | 사용처 |
|---|---|---|---|---|
| View | X | 즉시 | 느림 | Staging |
| Table | O | 매번 전체 | 빠름 | Marts (소규모) |
| Incremental | O | 변경분만 | 빠름 | Marts (대규모 팩트) |
| Ephemeral | X | 없음 | - | 중간 CTE |
View
{{ config(materialized='view') }}
SELECT
id AS order_id,
user_id AS customer_id,
order_date,
status
FROM {{ source('jaffle_shop', 'orders') }}
매 쿼리 시 SQL을 재실행한다. 저장 공간이 필요 없고 항상 최신 데이터를 보장한다. Staging 레이어에 적합하다.
장점: 스토리지 비용 0, 항상 최신 단점: 매번 소스를 스캔하므로 쿼리가 느릴 수 있음 사용처: 소스 데이터의 컬럼명 표준화, 타입 캐스팅 같은 가벼운 변환
Table
{{ config(materialized='table') }}
SELECT
customer_id,
COUNT(*) AS total_orders,
SUM(amount) AS lifetime_value
FROM {{ ref('stg_orders') }}
GROUP BY customer_id
결과를 물리적 테이블로 저장한다. dbt run 시 DROP + CREATE로 완전 재빌드한다. 자주 쿼리되는 집계 테이블에 적합하다.
장점: 쿼리 빠름, 인덱스 적용 가능 단점: 매 빌드마다 전체 재생성, 대용량이면 느림 사용처: Marts 레이어의 소규모 디멘션/팩트 테이블
Incremental
대용량 테이블의 핵심 전략이다. 신규/변경된 행만 INSERT 또는 MERGE한다.
{{ config(
materialized='incremental',
unique_key='order_id',
incremental_strategy='merge'
) }}
SELECT
order_id,
customer_id,
order_date,
status,
amount,
updated_at
FROM {{ source('jaffle_shop', 'orders') }}
{% if is_incremental() %}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}
동작 방식
- 첫 실행:
is_incremental()이 false → WHERE 절 무시 → 전체 데이터 로드 - 이후 실행:
is_incremental()이 true →{{ this }}(기존 테이블)의 MAX 타임스탬프 이후 데이터만 로드 unique_key를 지정하면 MERGE 전략으로 기존 행 UPDATE + 신규 행 INSERT
Incremental 전략 비교
| 전략 | 동작 | 지원 DW | 적합한 경우 |
|---|---|---|---|
append | INSERT만 | 모두 | 이벤트 로그 (업데이트 없음) |
merge | MERGE (upsert) | BigQuery, Snowflake, Databricks | 업데이트 있는 데이터 |
delete+insert | 기존 삭제 후 INSERT | Redshift, Postgres | merge 미지원 DW |
insert_overwrite | 파티션 단위 교체 | BigQuery, Spark | 파티션 기반 대용량 |
주의사항
- Late-arriving data: 타임스탬프 기반 필터링은 늦게 도착한 데이터를 놓칠 수 있다. lookback window를 추가하거나
--full-refresh주기적 실행을 권장 - 스키마 변경: 컬럼 추가 시
on_schema_change='append_new_columns'설정 필요 - 전체 재빌드:
dbt run --full-refresh -s model_name으로 언제든 전체 재빌드 가능
수억 행 팩트 테이블에서 빌드 시간을 몇 시간에서 몇 분으로 줄일 수 있다.
Ephemeral
{{ config(materialized='ephemeral') }}
SELECT
order_id,
CASE
WHEN amount >= 1000 THEN 'high'
WHEN amount >= 100 THEN 'medium'
ELSE 'low'
END AS order_tier
FROM {{ ref('stg_orders') }}
웨어하우스에 물리적 객체를 생성하지 않는다. 이 모델을 참조하는 다른 모델에 CTE(Common Table Expression)로 인라인된다.
장점: 오브젝트 생성 안 함, 깔끔한 모듈화 단점: 디버깅 어려움 (직접 쿼리 불가), dbt Docs에서 리니지 안 보임 사용처: 가벼운 중간 계산, 재사용 빈도 낮은 변환
선택 가이드
소스 정리 → View
├── 중간 변환 (재사용 낮음) → Ephemeral
├── 중간 변환 (재사용 높음) → View
└── 최종 테이블
├── 소규모 (< 수백만 행) → Table
└── 대규모 (수억+ 행) → Incremental
dbt_project.yml에서 폴더 단위로 기본값을 설정하면 편하다:
models:
my_project:
staging:
+materialized: view
intermediate:
+materialized: ephemeral
marts:
+materialized: table
개별 모델에서 config()로 오버라이드할 수 있다. 예를 들어 marts 폴더의 기본은 table이지만, 대용량 팩트 테이블만 incremental로 지정한다.