dbt 증분 모델 구현 시 흔히 발생하는 데이터 유실 및 비효율성 오류의 안티패턴을 분석하고, 실무에서 마주칠 수 있는 문제점과 해결 방안을 제시합니다. 주니어 데이터 엔지니어가 반드시 알아야 할 실수들을 정리했습니다.
데이터 엔지니어링 파이프라인에서 dbt(data build tool)는 데이터 변환 작업을 효율적으로 관리하는 데 필수적인 도구로 자리 잡았습니다. 특히, 대규모 데이터셋을 다룰 때 증분 모델(Incremental Model)은 전체 데이터를 매번 다시 처리하는 대신, 새롭거나 변경된 데이터만 처리하여 리소스 사용을 최적화하고 실행 시간을 단축하는 핵심적인 기능을 제공합니다.
하지만 dbt 증분 모델은 그 유용성만큼이나 복잡한 구현 과정을 가지고 있으며, 잘못된 접근 방식은 예상치 못한 데이터 유실, 중복, 그리고 심각한 성능 저하로 이어질 수 있습니다. "왜 분명 증분 모델을 적용했는데 파이프라인 실행 시간이 줄지 않지?", "어제까지 있던 데이터가 갑자기 사라졌는데 원인을 모르겠어"와 같은 문제는 주니어 데이터 엔지니어들이 실무에서 흔히 마주치는 어려움입니다.
이 글에서는 dbt 증분 모델 구현 시 흔히 저지르는 안티패턴과 실수들을 심도 있게 분석하고, 각 문제의 원인과 해결 방안을 구체적인 예시와 함께 제시하고자 합니다. 이 글을 통해 독자들은 견고하고 효율적인 dbt 증분 모델을 구축하는 데 필요한 실질적인 통찰력을 얻을 수 있을 것입니다.
📑 목차
- dbt 증분 모델, 왜 도입했는데도 비효율적인가요?
- 증분 모델에서 고유 키(Unique Key)를 놓치면 어떤 문제가 발생하나요?
- unique_key 부재 시 데이터 중복 문제
- unique_key 선택 기준
- incremental_strategy 선택, 무심코 기본값만 사용하면 안 되는 이유?
- 주요 incremental_strategy 종류 및 특징
- 잘못된 전략 선택의 예시
- 데이터 유실, is_incremental()과 where 절의 흔한 오용 때문인가요?
- where 절의 역할과 흔한 오용
- 견고한 where 절 설계 방안
- 잘못된 full_refresh 전략이 데이터 파이프라인에 미치는 영향은 무엇인가요?
- 불필요한 full_refresh의 문제점
- full_refresh가 필요한 경우
- 올바른 full_refresh 전략
- on_schema_change 설정, 무관심이 불러오는 치명적인 오류는?
- on_schema_change의 중요성
- 주요 on_schema_change 옵션
- ignore 옵션의 위험성 (가장 흔한 실수)
- 증분 모델 성능 저하, 어떻게 진단하고 개선할 수 있나요?
- 성능 저하의 주요 원인
- 성능 진단 방법
- 성능 개선 방안
- 마무리하며: 견고하고 효율적인 증분 모델 구축을 위한 핵심
Image by cliffsmith23 on Pixabay
dbt 증분 모델, 왜 도입했는데도 비효율적인가요?
dbt 증분 모델의 핵심 목표는 "전체 데이터 재처리 방지"입니다. 원본 데이터의 크기가 커질수록 전체 빌드(full refresh)는 더 많은 시간과 컴퓨팅 리소스를 소모하게 됩니다. 증분 모델은 이러한 비효율성을 해결하기 위해, 마지막 빌드 이후에 발생한 변경 사항만 식별하여 대상 테이블에 반영하는 방식으로 동작합니다.
그러나 많은 주니어 개발자들이 증분 모델을 도입하고도 기대했던 성능 향상을 체감하지 못하는 경우가 많습니다. 이는 대개 다음과 같은 이유로 발생합니다:
- 잘못된 전략 선택: 데이터의 변경 패턴을 고려하지 않은
incremental_strategy선택. - 부적절한
where절: 증분 처리를 위한where절 조건이 너무 광범위하거나, 누락된 데이터 처리 로직. unique_key누락 또는 오용: 데이터 중복이나 잘못된 업데이트를 야기.- 스키마 변경 관리 부재: 원본 소스의 스키마 변경 시 대상 테이블이 동기화되지 않아 발생하는 오류.
이러한 문제들은 단순히 기능 구현을 넘어, 데이터 품질 저하 및 파이프라인 안정성 문제를 야기할 수 있습니다. 다음 섹션부터 각 문제점을 상세히 살펴보겠습니다.
증분 모델에서 고유 키(Unique Key)를 놓치면 어떤 문제가 발생하나요?
dbt 증분 모델에서 unique_key는 대상 테이블에 이미 존재하는 레코드를 어떻게 처리할지 결정하는 데 핵심적인 역할을 합니다. 특히 merge 또는 delete+insert 전략을 사용할 때, unique_key가 없거나 부적절하게 정의되면 심각한 데이터 중복 또는 잘못된 업데이트가 발생할 수 있습니다.
unique_key 부재 시 데이터 중복 문제
unique_key가 정의되지 않은 상태에서 merge 전략을 사용하면, dbt는 기존 레코드를 업데이트하지 않고 단순히 새 레코드를 추가하려고 시도할 수 있습니다. 이는 특정 데이터 웨어하우스(예: BigQuery)에서 merge 문이 ON NOT MATCHED BY TARGET 조건만 만족시키기 때문입니다. 결과적으로 동일한 논리적 엔티티에 대한 레코드가 대상 테이블에 여러 번 삽입되어 데이터 중복을 초래합니다.
-- models/my_incremental_model.sql
{{ config(
materialized='incremental',
-- unique_key='order_id' (이 부분이 없거나 잘못 정의된 경우)
incremental_strategy='merge'
) }}
SELECT
order_id,
customer_id,
order_timestamp,
status
FROM {{ source('raw_data', 'orders') }}
{% if is_incremental() %}
WHERE order_timestamp > (SELECT MAX(order_timestamp) FROM {{ this }})
{% endif %}
위 코드에서 unique_key가 없으면, 이미 처리된 order_id를 가진 레코드가 order_timestamp가 업데이트될 때마다 새로운 레코드로 추가될 위험이 있습니다. 이는 분석 결과의 왜곡과 스토리지 비용 증가로 이어집니다.
unique_key 선택 기준
unique_key는 대상 테이블의 각 행을 고유하게 식별할 수 있는 하나 이상의 컬럼이어야 합니다. 일반적으로 원본 소스 시스템의 기본 키(Primary Key)를 사용하는 것이 가장 좋습니다.
- 단일 컬럼:
order_id,user_id와 같이 단일 컬럼으로 충분히 고유성을 보장할 수 있는 경우. - 복합 컬럼:
(user_id, event_timestamp)와 같이 여러 컬럼의 조합으로 고유성을 보장해야 하는 경우.
올바른 unique_key 설정 예시:
-- models/my_incremental_model.sql
{{ config(
materialized='incremental',
unique_key='order_id', -- 여기에 고유 키를 명시
incremental_strategy='merge'
) }}
SELECT
order_id,
customer_id,
order_timestamp,
status,
updated_at
FROM {{ source('raw_data', 'orders') }}
{% if is_incremental() %}
-- 변경된 데이터만 가져오기 위해 updated_at을 활용하는 것이 더 견고할 수 있음
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}
unique_key 정의 여부 |
결과 (merge 전략 시) |
권장 사항 |
|---|---|---|
| 정의 안 함 또는 부적절 | 기존 레코드에 대한 업데이트 누락, 데이터 중복 발생 | 반드시 고유 키 정의, 데이터 원천의 PK 활용 |
| 정의함 (정확) | 기존 레코드 정확히 업데이트, 새 레코드 추가 | 데이터 원천의 변경 패턴에 맞는 고유 키 선택 |
unique_key는 증분 모델의 데이터 무결성을 보장하는 가장 기본적인 요소입니다. 이를 신중하게 설계하고 구현하는 것이 데이터 유실 및 중복 방지의 첫걸음입니다.
incremental_strategy 선택, 무심코 기본값만 사용하면 안 되는 이유?
dbt는 다양한 데이터 웨어하우스 어댑터에 따라 여러 incremental_strategy를 제공합니다. 각 전략은 데이터를 증분적으로 처리하는 방식에 차이가 있으며, 데이터의 특성과 변경 패턴에 따라 최적의 전략을 선택하는 것이 중요합니다. 단순히 기본값을 사용하는 것은 비효율성이나 데이터 부정확성을 초래할 수 있습니다.
주요 incremental_strategy 종류 및 특징
append: 가장 간단한 전략으로, 단순히 새로운 레코드를 대상 테이블에 추가합니다. 기존 레코드의 변경이나 삭제는 처리하지 않습니다.- 장점: 가장 빠르고 리소스 소모가 적습니다.
- 단점: 기존 레코드 업데이트/삭제를 반영할 수 없으므로, 데이터 중복이나 오래된 데이터가 남을 수 있습니다.
- 적합한 경우: 로그 데이터, 이벤트 스트림 등 오직 새로운 데이터만 추가되고 기존 데이터는 절대 변경되지 않는 경우.
delete+insert: 새로운 데이터와 변경된 데이터를 임시 테이블에 로드한 후,unique_key를 기준으로 기존 테이블에서 해당 레코드를 삭제하고 새 레코드를 삽입합니다.- 장점: 데이터 중복 없이 정확하게 업데이트/삽입을 처리할 수 있습니다.
- 단점: 대규모 테이블의 경우 삭제 및 삽입 작업이 많은 리소스를 소모하고 느릴 수 있습니다.
- 적합한 경우: 업데이트/삭제가 빈번하지만, 대상 테이블의 크기가 아주 크지 않거나, 특정 데이터 웨어하우스에서
merge를 지원하지 않는 경우.
merge: 새로운 데이터와 변경된 데이터를 임시 테이블에 로드한 후,unique_key를 기준으로 기존 테이블과 병합(merge)합니다. 매치되는 레코드는 업데이트하고, 매치되지 않는 레코드는 삽입합니다.- 장점:
delete+insert보다 일반적으로 효율적입니다. 데이터 웨어하우스 엔진이 최적화된MERGE문을 사용합니다. - 단점:
unique_key가 필수적이며, 일부 데이터 웨어하우스에서는 지원하지 않을 수 있습니다. - 적합한 경우: 업데이트/삽입이 빈번하고, 대상 테이블의 크기가 크며, 데이터 웨어하우스가
MERGE문을 효율적으로 지원하는 경우 (예: BigQuery, Snowflake, Databricks).
- 장점:
잘못된 전략 선택의 예시
만약 사용자 프로필과 같이 기존 데이터가 자주 업데이트되는 테이블에 append 전략을 사용한다면, 동일한 사용자에 대한 레코드가 계속해서 추가되어 데이터 중복이 발생하고 최신 정보를 파악하기 어려워집니다. 반대로, 변경이 거의 없는 대규모 로그 데이터에 delete+insert 전략을 사용하면 불필요하게 많은 삭제/삽입 작업으로 인해 성능 저하를 겪을 수 있습니다.
전략 선택 예시:
-- models/user_profiles_incremental.sql (업데이트가 잦은 경우)
{{ config(
materialized='incremental',
unique_key='user_id',
incremental_strategy='merge' -- 또는 'delete+insert'
) }}
SELECT
user_id,
user_name,
email,
last_login_at,
updated_at
FROM {{ source('raw_data', 'users') }}
{% if is_incremental() %}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}
-- models/event_logs_incremental.sql (추가만 되는 경우)
{{ config(
materialized='incremental',
incremental_strategy='append'
) }}
SELECT
event_id,
user_id,
event_type,
event_timestamp
FROM {{ source('raw_data', 'events') }}
{% if is_incremental() %}
WHERE event_timestamp > (SELECT MAX(event_timestamp) FROM {{ this }})
{% endif %}
| 전략 | 주요 특징 | 장점 | 단점 | 적합한 시나리오 |
|---|---|---|---|---|
append |
새로운 데이터만 추가 | 빠르고 효율적 | 업데이트/삭제 미반영, 중복 가능 | 로그, 이벤트 데이터 |
delete+insert |
기존 삭제 후 새 데이터 삽입 | 데이터 정확성 | 대규모 테이블 시 비효율적 | 업데이트/삭제 빈번, MERGE 미지원 환경 |
merge |
기존 업데이트 또는 새 데이터 삽입 | 효율적인 업데이트/삽입 | unique_key 필수, DB 지원 여부 |
업데이트/삭제 빈번, 대규모 테이블, MERGE 지원 환경 |
데이터의 변경 패턴(추가만 되는지, 업데이트/삭제도 발생하는지)을 정확히 이해하고, 대상 데이터 웨어하우스의 특성을 고려하여 최적의 incremental_strategy를 선택해야 합니다. 이는 성능과 데이터 정확성 모두에 지대한 영향을 미칩니다.
데이터 유실, is_incremental()과 where 절의 흔한 오용 때문인가요?
dbt 증분 모델에서 is_incremental() 매크로와 함께 사용되는 where 절은 증분 처리의 핵심 로직을 담당합니다. 이 where 절이 잘못 설계될 경우, 중요한 데이터가 누락되거나 예상치 못한 유실로 이어질 수 있습니다.
where 절의 역할과 흔한 오용
where 절은 is_incremental()이 참일 때, 즉 증분 빌드가 실행될 때 어떤 데이터를 처리할지 정의합니다. 일반적으로 대상 테이블의 최대 타임스탬프(MAX(timestamp_column))를 기준으로 그 이후의 데이터를 가져오는 방식으로 구현됩니다.
흔한 오용 예시:
-- models/my_model_with_error.sql
{{ config(
materialized='incremental',
unique_key='id',
incremental_strategy='merge'
) }}
SELECT
id,
value,
created_at
FROM {{ source('raw_data', 'some_table') }}
{% if is_incremental() %}
-- 문제: created_at만 고려, 이미 생성된 레코드가 나중에 업데이트되는 경우를 놓침
WHERE created_at > (SELECT MAX(created_at) FROM {{ this }})
{% endif %}
위 예시의 문제점은 다음과 같습니다:
- 지연 도착 데이터(Late Arriving Data) 누락: 원본 시스템에서 데이터가 생성된 시점(
created_at)보다 늦게 도착하는 경우가 발생할 수 있습니다.created_at만을 기준으로 하면, 이미 증분 처리된 범위에 속하지만 실제로는 누락된 데이터가 발생할 수 있습니다. - 기존 레코드의 업데이트 누락:
created_at은 레코드가 처음 생성된 시점을 나타냅니다. 만약 기존 레코드의 다른 컬럼(예:status,value)이 업데이트되었지만created_at은 변경되지 않았다면, 이 업데이트는 증분 빌드에서 처리되지 않아 대상 테이블에 최신 정보가 반영되지 않습니다. 이는 데이터 유실(논리적 유실)에 해당합니다. - 삭제된 레코드 미반영: 원본 소스에서 특정 레코드가 삭제되었을 때,
where절만으로는 이 변경 사항을 감지하여 대상 테이블에서 삭제할 수 없습니다. 이는 대상 테이블에 유령 데이터(ghost data)를 남기게 됩니다.
견고한 where 절 설계 방안
데이터 유실과 비효율성을 방지하기 위해서는 where 절을 매우 신중하게 설계해야 합니다. 몇 가지 전략은 다음과 같습니다:
updated_at컬럼 활용: 원본 소스에 레코드가 마지막으로 업데이트된 시점을 나타내는updated_at또는last_modified_at과 같은 컬럼이 있다면, 이를where절에 활용하는 것이 가장 효과적입니다.
이 방식은 기존 레코드의 업데이트까지 감지하여 반영할 수 있으므로, 데이터 정확성을 크게 향상시킵니다.-- models/my_model_robust.sql {{ config( materialized='incremental', unique_key='id', incremental_strategy='merge' ) }} SELECT id, value, created_at, updated_at -- 업데이트 시각 컬럼 활용 FROM {{ source('raw_data', 'some_table') }} {% if is_incremental() %} -- updated_at을 기준으로 증분 처리 WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }}) {% endif %}- 더 넓은 시간 범위 포괄: 데이터 지연 도착 가능성이 높다면, 단순히
MAX(updated_at)이후가 아닌, 일정 기간(예: 1시간, 1일)을 더 포함하여 처리하는 것도 고려할 수 있습니다. 예를 들어,WHERE updated_at > (SELECT MAX(updated_at) - INTERVAL '1 hour' FROM {{ this }})와 같이 설정하여 약간의 오버랩을 두는 것입니다. 이는 중복 처리될 수 있지만, 유실 위험을 줄일 수 있습니다. - 삭제된 레코드 처리: dbt 증분 모델은 기본적으로 삭제를 감지하지 않습니다. 삭제된 레코드를 처리하려면 다음과 같은 추가 전략이 필요합니다.
- 소프트 삭제(Soft Delete): 원본 소스에
is_deleted또는deleted_at과 같은 플래그 컬럼이 있다면, 이를 활용하여 대상 테이블에서 레코드를 논리적으로 삭제 처리할 수 있습니다. - 데이터 원본과의 비교: 주기적으로
full_refresh를 수행하거나, 별도의 작업으로 원본과 대상 테이블의unique_key를 비교하여 대상 테이블에만 존재하는 레코드를 삭제하는 로직을 구현해야 합니다.
- 소프트 삭제(Soft Delete): 원본 소스에
where 절은 데이터 유실을 막고 증분 모델의 효율성을 결정하는 가장 중요한 부분입니다. 원본 데이터의 특성과 변경 패턴을 면밀히 분석하여 가장 적절하고 견고한 조건을 설정해야 합니다.
Image by blickpixel on Pixabay
잘못된 full_refresh 전략이 데이터 파이프라인에 미치는 영향은 무엇인가요?
dbt에서 --full-refresh 플래그는 증분 모델을 처음부터 다시 빌드하도록 지시합니다. 즉, 기존 대상 테이블을 삭제하고 전체 데이터를 다시 로드하여 새로운 테이블을 생성하는 과정입니다. 이는 특정 상황에서 필수적이지만, 남용하거나 적절히 사용하지 않을 경우 데이터 파이프라인에 심각한 비효율성과 문제를 야기합니다.
불필요한 full_refresh의 문제점
- 막대한 리소스 소모: 대규모 테이블의 전체 빌드는 상당한 컴퓨팅 리소스(CPU, 메모리, 스토리지 I/O)와 시간을 소모합니다. 이는 클라우드 데이터 웨어하우스 비용 증가의 주범이 될 수 있습니다.
- 파이프라인 지연: 전체 빌드 시간이 길어지면 데이터 파이프라인의 전체 실행 시간이 늘어나, 다운스트림 애플리케이션이나 분석 결과의 지연을 초래합니다.
- 데이터 정합성 문제:
full_refresh가 실행되는 동안 대상 테이블이 일시적으로 존재하지 않거나, 오래된 데이터가 노출될 수 있어 다운스트림 시스템에서 데이터 정합성 문제가 발생할 수 있습니다. - 잦은 스키마 변경 트리거:
full_refresh는 테이블을 재생성하므로, 스키마 변경이 자주 발생하는 환경에서는 불필요하게 많은 스키마 변경 이력을 남길 수 있습니다.
full_refresh가 필요한 경우
그럼에도 불구하고 full_refresh가 필요한 상황은 분명히 존재합니다.
- 초기 빌드: 증분 모델을 처음 배포할 때 대상 테이블이 존재하지 않으므로,
full_refresh로 초기 데이터를 로드해야 합니다. - 대규모 스키마 변경: 기존 컬럼의 데이터 타입 변경, 컬럼 이름 변경 등
on_schema_change설정만으로는 자동으로 처리하기 어려운 복잡한 스키마 변경이 발생했을 때. - 데이터 무결성 문제: 알 수 없는 원인으로 인해 대상 테이블의 데이터가 손상되거나, 증분 로직의 버그로 인해 데이터 유실/중복이 심각하게 발생하여 전체 재구축이 필요할 때.
- 증분 로직 변경:
unique_key나incremental_strategy와 같은 핵심 증분 로직을 변경했을 때, 기존 데이터에 대한 영향을 최소화하기 위해full_refresh를 고려할 수 있습니다.
올바른 full_refresh 전략
- 필요할 때만 사용:
full_refresh는 최후의 수단으로 간주하고, 자동화된 스케줄에 포함시키기보다는 수동으로 실행하거나 특정 이벤트에 따라 트리거되도록 설계하는 것이 좋습니다. on_schema_change활용: 대부분의 스키마 변경은on_schema_change설정을 통해 자동으로 처리할 수 있습니다. 다음 섹션에서 더 자세히 다루겠습니다.- 데이터 품질 검사: 주기적으로 데이터 품질 테스트를 실행하여 데이터 무결성 문제를 사전에 감지하고,
full_refresh없이 증분 로직을 수정하여 해결하는 방안을 우선적으로 모색해야 합니다.
full_refresh는 강력한 도구이지만, 그만큼 신중하게 사용해야 합니다. 남용은 파이프라인의 효율성을 떨어뜨리고 비용을 증가시키는 주요 원인이 됩니다.
on_schema_change 설정, 무관심이 불러오는 치명적인 오류는?
원본 데이터 소스의 스키마는 끊임없이 변할 수 있습니다. 새로운 컬럼이 추가되거나, 기존 컬럼의 데이터 타입이 변경되거나, 심지어 컬럼이 삭제될 수도 있습니다. dbt의 증분 모델에서 이러한 스키마 변경에 무관심하면, 런타임 오류, 데이터 누락, 또는 부정확한 데이터 타입 매핑과 같은 치명적인 문제가 발생할 수 있습니다.
on_schema_change의 중요성
on_schema_change 설정은 증분 모델의 대상 테이블 스키마가 원본 소스의 스키마와 다를 때 dbt가 어떻게 반응할지 정의합니다. 이 설정은 dbt_project.yml 파일이나 개별 모델 설정에서 지정할 수 있습니다.
주요 on_schema_change 옵션
ignore(기본값): 스키마 변경을 무시합니다. 새로운 컬럼이 추가되어도 대상 테이블에 반영되지 않으며, 기존 컬럼의 데이터 타입이 변경되어도 오류 없이 실행될 수 있으나, 이는 잠재적인 데이터 유실이나 타입 불일치를 초래합니다.warn: 스키마 변경이 감지되면 경고를 출력하고 빌드를 계속합니다.ignore와 동일하게 변경 사항을 반영하지는 않습니다.fail: 스키마 변경이 감지되면 빌드를 실패시킵니다. 가장 엄격한 옵션으로, 스키마 변경을 수동으로 검토하고 처리할 때 유용합니다.append_new_columns: 원본 소스에 새로운 컬럼이 추가되면 대상 테이블에 자동으로 추가합니다. 기존 컬럼의 데이터 타입 변경이나 삭제는 처리하지 않습니다.sync_all_columns: 원본 소스의 스키마에 맞춰 대상 테이블의 스키마를 동기화합니다. 새로운 컬럼은 추가하고, 기존 컬럼의 데이터 타입이 변경되면 대상 테이블의 타입도 변경합니다 (일부 데이터 웨어하우스에서만 지원). 컬럼 삭제는 일반적으로 처리하지 않습니다.
ignore 옵션의 위험성 (가장 흔한 실수)
대부분의 주니어 개발자는 on_schema_change 설정을 명시적으로 지정하지 않아 기본값인 ignore를 사용하게 됩니다. 이는 다음과 같은 문제를 야기합니다.
- 데이터 누락: 원본 테이블에 새로운 중요한 컬럼이 추가되었음에도 불구하고, 대상 증분 모델에는 해당 컬럼이 반영되지 않아 다운스트림 분석에서 중요한 정보가 누락됩니다.
- 데이터 타입 불일치 및 오류: 기존 컬럼의 데이터 타입이 변경되면, 증분 처리 시 데이터 타입 불일치 오류가 발생하거나, 잘못된 타입으로 데이터가 로드되어 데이터 손상을 일으킬 수 있습니다. 예를 들어,
INTEGER컬럼이VARCHAR로 변경될 경우 런타임 오류가 발생할 수 있습니다. - 예측 불가능한 동작: 스키마 변경이 조용히 무시되므로, 문제가 발생했을 때 원인을 파악하기 어렵습니다.
올바른 on_schema_change 설정 예시:
-- models/my_incremental_model.sql
{{ config(
materialized='incremental',
unique_key='id',
incremental_strategy='merge',
on_schema_change='sync_all_columns' -- 또는 'append_new_columns'
) }}
SELECT
id,
column_a,
column_b,
new_column_c, -- 원본에 추가된 새로운 컬럼
updated_at
FROM {{ source('raw_data', 'some_table') }}
{% if is_incremental() %}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}
on_schema_change 옵션 |
새 컬럼 | 기존 컬럼 타입 변경 | 결과 | 권장 시나리오 |
|---|---|---|---|---|
ignore (기본값) |
무시 | 무시 (에러 발생 가능) | 가장 위험, 잠재적 데이터 문제 | 피해야 함 |
warn |
무시 | 무시 (경고 출력) | 문제 감지에는 도움, 해결은 아님 | 개발/테스트 환경 |
fail |
실패 | 실패 | 가장 엄격, 수동 처리 강제 | 스키마 변경에 대한 엄격한 통제 필요 시 |
append_new_columns |
자동 추가 | 무시 (에러 발생 가능) | 새 컬럼 자동 반영 | 새 컬럼만 반영하면 되는 경우 |
sync_all_columns |
자동 추가 | 자동 변경 (DB 지원 시) | 가장 자동화된 스키마 관리 | 대부분의 프로덕션 환경 |
프로덕션 환경에서는 on_schema_change='sync_all_columns' 또는 'append_new_columns'를 사용하여 스키마 변경에 대한 자동화된 대응을 구축하는 것이 일반적입니다. 이를 통해 데이터 유실을 방지하고 파이프라인의 안정성을 확보할 수 있습니다.
Image by Pexels on Pixabay
증분 모델 성능 저하, 어떻게 진단하고 개선할 수 있나요?
dbt 증분 모델을 올바르게 구현했음에도 불구하고 성능 저하를 경험하는 경우가 있습니다. 이는 주로 데이터베이스 내부적인 요인이나 모델 설계의 미흡에서 비롯됩니다. 성능 저하는 곧 리소스 낭비와 파이프라인 지연으로 이어지므로, 적극적인 진단과 개선 노력이 필요합니다.
성능 저하의 주요 원인
- 비효율적인
where절: 증분 처리를 위한where절이 인덱스(Index)나 파티션(Partition)을 제대로 활용하지 못하고 전체 테이블 스캔을 유발하는 경우. 예를 들어,updated_at컬럼에 인덱스가 없거나 파티션 키가 아닐 때. - 복잡한 조인(JOIN) 및 서브쿼리: 증분 모델 내에서 너무 많은 테이블을 조인하거나, 최적화되지 않은 서브쿼리를 사용하여 증분 데이터 처리 시에도 불필요하게 많은 데이터를 스캔하는 경우.
- 데이터 웨어하우스 최적화 부족: 대상 테이블에 적절한 클러스터링(Clustering) 또는 파티셔닝 전략이 적용되지 않아 데이터 읽기/쓰기 성능이 저하되는 경우.
- 과도한 데이터 변경: 증분 모델임에도 불구하고, 증분 구간에 포함되는 데이터 변경량이 너무 많아 사실상 전체 테이블을 다시 처리하는 것과 유사한 비용이 발생하는 경우.
성능 진단 방법
- dbt 로그 분석:
dbt run실행 후 출력되는 로그를 통해 각 모델의 실행 시간과 스캔된 데이터 양을 확인할 수 있습니다. 특히 느린 모델을 식별하는 데 유용합니다. - 데이터 웨어하우스 쿼리 플랜 분석: 각 데이터 웨어하우스(BigQuery, Snowflake, Redshift 등)는 쿼리 실행 계획을 시각화하거나 텍스트로 제공하는 기능을 갖추고 있습니다. 이를 통해 어떤 부분이 병목 현상을 일으키는지 정확히 파악할 수 있습니다 (예: 풀 테이블 스캔, 비효율적인 조인).
- dbt 프로파일링 툴 활용: dbt는 직접적인 프로파일링 기능을 제공하지 않지만, 데이터 웨어하우스 벤더의 모니터링 툴(예: BigQuery Monitoring, Snowflake Query History)을 활용하여 쿼리 성능을 상세하게 분석할 수 있습니다.
dbt-audit-helper사용: dbt 패키지 중dbt-audit-helper는 데이터 중복, 누락 등을 감사하여 증분 모델의 데이터 품질 문제를 간접적으로 파악하는 데 도움을 줄 수 있습니다.
성능 개선 방안
where절 최적화:- 증분 조건으로 사용하는 컬럼(
updated_at,event_timestamp등)에 인덱스를 생성하거나, 해당 컬럼을 파티션 키로 설정하여 스캔 범위를 최소화합니다. - 단순히
MAX(timestamp)를 사용하는 대신,timestamp_column >= (SELECT MAX(timestamp_column) FROM {{ this }}) - INTERVAL 'X' unit과 같이 약간의 오버랩을 두어 지연 도착 데이터를 처리하면서도 과도한 스캔을 피할 수 있습니다.
- 증분 조건으로 사용하는 컬럼(
- 데이터 웨어하우스 기능 활용:
- 파티셔닝(Partitioning): 시간 기반 컬럼(예: 날짜, 타임스탬프)으로 테이블을 파티셔닝하여 쿼리 시 필요한 파티션만 스캔하도록 합니다.
- 클러스터링(Clustering): 자주 함께 쿼리되는 컬럼들을 클러스터링하여 데이터 액세스 효율을 높입니다.
- 적절한 데이터 타입 사용: 컬럼에 가장 적합한 데이터 타입을 사용하여 스토리지 및 처리 효율을 높입니다.
- 뷰(View) 대신 테이블(Table) 사용: 복잡한 변환이 필요한 중간 모델은
view대신table또는incremental모델로 구체화하여 불필요한 재계산을 방지합니다.
- SQL 쿼리 최적화:
- 불필요한 조인을 제거하거나, 조인 순서를 최적화합니다.
SELECT *대신 필요한 컬럼만 명시하여 데이터 전송량을 줄입니다.- 복잡한 서브쿼리는 CTE(Common Table Expression)를 사용하여 가독성과 최적화 가능성을 높입니다.
- 증분 전략 재검토: 데이터 변경 패턴이 예상보다 복잡하거나 대량으로 발생하는 경우,
incremental_strategy를 변경하거나, 아예 증분 모델 대신full_refresh를 주기적으로 실행하는 것을 고려할 수도 있습니다 (물론 이는 최후의 수단입니다).
성능 개선은 일회성 작업이 아닌 지속적인 모니터링과 튜닝이 필요한 과정입니다. 위에서 제시된 진단 및 개선 방안을 적극적으로 활용하여 dbt 증분 모델의 효율성을 극대화할 수 있습니다.
마무리하며: 견고하고 효율적인 증분 모델 구축을 위한 핵심
dbt 증분 모델은 데이터 파이프라인의 효율성과 확장성을 크게 향상시키는 강력한 도구입니다. 하지만 그 잠재력을 온전히 발휘하기 위해서는 단순히 materialized='incremental' 설정을 추가하는 것을 넘어, 데이터의 특성과 변경 패턴에 대한 깊은 이해를 바탕으로 신중하게 설계하고 구현해야 합니다.
이 글에서 다룬 unique_key의 중요성, incremental_strategy의 적절한 선택, is_incremental()과 where 절의 견고한 설계, full_refresh의 현명한 사용, 그리고 on_schema_change를 통한 스키마 변경 관리는 데이터 유실과 비효율성을 방지하는 핵심적인 요소들입니다. 또한, 지속적인 성능 진단과 개선은 장기적으로 안정적인 데이터 파이프라인을 유지하는 데 필수적입니다.
주니어 데이터 엔지니어 여러분들이 이 글을 통해 dbt 증분 모델 구현 시 발생할 수 있는 흔한 실수들을 미리 파악하고, 더욱 견고하고 효율적인 데이터 파이프라인을 구축하는 데 필요한 실질적인 지식을 얻으셨기를 바랍니다. 여러분의 경험과 질문을 댓글로 공유해 주시면, 함께 더 나은 해결책을 모색하는 데 큰 도움이 될 것입니다.
📌 함께 읽으면 좋은 글
- [기술 리뷰] JavaScript 비동기 처리, 콜백 지옥 탈출로 응답 속도 30% 개선한 실전 노하우
- [데이터 엔지니어링] Airflow DAG 실행 시간, 이렇게 줄였습니다: 실전 최적화 경험기
- [오픈소스] 오픈소스 첫 기여, 내 PR이 계속 반려되는 이유와 해결 전략은?
이 글이 도움이 되셨다면 공감(♥)과 댓글로 응원해 주세요!
궁금한 점이나 다루었으면 하는 주제가 있다면 댓글로 남겨주세요.
'데이터 엔지니어링' 카테고리의 다른 글
| Airflow DAG 실행 시간, 이렇게 줄였습니다: 실전 최적화 경험기 (0) | 2026.07.07 |
|---|