dbt 사용법과 기본 개념
in Data Engineering on dbt
- 이 글은 dbt 사용법(Data Build Tool)을 작성한 글입니다
- 예상 독자
- dbt가 궁금하신 분
- 마트 모델링을 해야 하는 데이터 분석가, 데이터 엔지니어
- 키워드 : dbt 사용법, dbt(data build tool), dbt 사용법, dbt 설치, dbt core
dbt란?
dbt가 나오게 된 배경
- 과거엔 데이터 처리를 ETL 방식으로 진행(Extract - Trasnform - Load)
- 예 : 원본 데이터를 추출하고 Apache Spark를 사용해 바로 데이터를 변환해서 변환한 결과를 저장함
- 이 당시의 데이터 저장소 비용은 지금에 비하면 상대적으로 높았고, 컴퓨팅 파워도 제한적이였음
- 이렇게 처리할 때 원본 데이터를 그대로 저장하는 것은 비용 이슈 때문에 고민이 되던 시기
- 클라우드 발전
- 클라우드가 발전하면서 클라우드 베이스의 데이터 웨어하우스가 등장했음(BigQuery, Snowflake, Redshift 등)
- 데이터 웨어하우스에서 저장 / 컴퓨팅을 분리하기 시작
- 데이터 저장소 비용이 점점 저렴해짐(S3, GCS 비용)
- 위 발전 덕분에 ETL 방식에서 ELT 방식이 업계에서 알려지고, 채택되는 케이스가 많아짐
- 클라우드가 발전하면서 클라우드 베이스의 데이터 웨어하우스가 등장했음(BigQuery, Snowflake, Redshift 등)
- ELT
- Extract - Load - Transform
- S3, DB 등에서 데이터를 추출하고, 추출한 데이터를 바로 데이터 웨어하우스에 불러옴(Load) 그 후에 불러온 데이터를 데이터 웨어하우스에서 목적에 맞게 Transform을 해서 저장함
- 데이터 웨어하우스에서 Transform을 할 때는 주로 SQL을 사용함. 과거엔 Apache Spark로 전처리를 하는 것이 필수였으나(물론 Spark도 SQL 방식이 요즘은 더 많이 사용되고 있으나, 과거엔 RDD 기반을 주로 사용하던 시기가 있었음) 이젠 더 쉽게 진행할 수 있게 됨
- 일단 데이터를 모두 가져오기 때문에 모든 데이터를 목적에 맞게 활용할 수 있음
- Transform을 다시 실행할 때, 원본 데이터가 데이터 웨어하우스에 있어서 바로 다시 Transform을 실행하면 됨
- 같은 데이터여도 목적에 따라 다르게 전처리가 필요할 때도 유용함
- 그러나 데이터를 모두 저장하는 것은 장점이기도 하지만, 사용되지 않는 데이터도 일단 다 저장해서 단점도 존재함(저장 비용의 증가)
- 사람들이 더 쉽게 데이터를 사용할 수 있게 되면서, 같은 지표인데 다르게 뽑는 경우가 생김. 예를 들어 DAU를 뽑아야 하는데, 부서마다 Active의 정의를 다르게 해서 CEO에게 보고함. CEO 입장에선 데이터가 달라서 의문을 제기하고, 데이터 조직에게 데이터를 확인해보라고 함
- 그래서 확인해보면, 조직 간 지표 정의가 달랐기 때문을 발견함
- 위 문제를 해결하기 위해 데이터 조직에서 표준화 작업을 시작함. 이 때는 데이터 분석가가 하는 조직도 있고, 데이터 엔지니어가 하는 조직도 있음. 조직 상황에 따라 할 수 있는 사람들이 하기 시작함
- 처음엔 단순하게 Transform을 Spark나 Airflow 등으로 만들면서 관리함
- 그러나 이 방식은 코드나 쿼리가 길어서 가독성이 떨어져, 유지보수를 할 수 있는 사람이 적어지게 됨(만든 사람만 이해함)
- 또한 모델 정의가 다를 땐 전체적으로 다시 실행하는 과정이 필요해짐
- 이런 맥락에서 dbt가 등장함
- 데이터 웨어하우스에서 Transform 작업을 체계적으로 관리할 수 있게 해주는 도구
- 버전 관리나 테스팅도 지원하며, 문서도 만들 수 있어 많은 회사들에서 채택함(실리콘밸리에서 많이 채택했으며, 한국에서는 최근 1~2년부터 도입하는 중)
dbt 설명
- dbt : data build tool
- dbt는 데이터 변환(transformation)에 특화된 도구. 데이터 엔지니어링에서 T 레이어에서 사용된다고 표현함
- 오픈소스인 dbt core와 클라우드 버전인 dbt cloud가 존재함
- 보통 회사에선 dbt core를 사용하고, Airflow나 dbt Web 화면을 직접 만들어서 쓰는 경우가 많음
- 특징
- 모델을 구조화해서 관리할 수 있음
- 데이터 모델 : 데이터를 어떻게 정의할 것인지를 나타내는 것. 머신러닝이나 AI 모델과 다른 개념
- SQL 쿼리를 복사 붙여넣기하는 것이 아닌 dbt에서 모델을 만들어서 그 결과 테이블을 사용할 수 있음
- 모델링하는 과정에서 복잡한 비즈니스 로직을 추가할 수 있음
- 데이터 리니지 제공
- 집계된 데이터에서 어떤 데이터를 참고하는지를 알려주는 기능이 있는데, 이 기능이 리니지(lineage), 계보 기능
- Incremental 지원
- 회사에서 전체 데이터를 매일 가지고 오는 것은 DB에도 부담이 되고, 시간도 오래 걸림. 이럴 때 Incremental하게 증분 데이터만 가지고 오는 것이 필요. 이 부분을 지원하는 것도 장점
- 데이터 퀄리티 테스트
- 데이터의 품질 테스트를 지원. 어떤 값이 있는지, 최소/최대값 등
- 모델을 구조화해서 관리할 수 있음
dbt 도입을 고려하면 좋은 조직
- (1) 데이터 분석가, 데이터 엔지니어가 함께 일하는 조직
- dbt는 SQL 사용해서 데이터 분석가도 모델을 만들 때 기여할 수 있음
- 그러나 데이터 분석가가 SQL을 못하는 경우도 존재함. Pandas 등을 사용해서 전처리를 하는 경우엔 SQL에 익숙해진 후에 도입하는 것을 추천함
- (2) 클라우드 데이터 웨어하우스를 사용하고 있는 조직
- 클라우드 기반 데이터 웨어하우스를 사용할 때 특히 더 좋음
- 이런 데이터 웨어하우스는 SQL로 데이터 처리를 할 수 있고, 보통 UDF 기능을 지원함
- 클라우드 데이터 웨어하우스의 인프라를 관리할 필요가 없기 때문에 클러스터 관리 등을 하지 않아도 괜찮음
- 클라우드 기반 데이터 웨어하우스를 사용할 때 특히 더 좋음
- (3) 데이터를 잘 활용하는 조직
- 데이터를 잘 활용하지 못하는 경우에 dbt를 도입하는 것은 비추천
- 데이터 성숙도 관점에서 모델링의 복잡성과 규모가 존재할 때 도입하는 것이 좋음
- 여러 소스의 데이터와 결합할 때 좋고, 단순하게 집계만 하려고 한다면 굳이 dbt를 사용하지 않아도 괜찮음
- 데이터 파이프라인 관점
- 데이터 파이프라인이 복잡할 때, 리니지 기능이 빛을 낼 수 있기 때문에 복잡한 조직에서 추천함. 간단하다면 그냥 문서에 정리해도 괜찮을 수 있음
- dbt cloud를 사용하지 않으면 Airflow나 dbt Web UI를 구성하는 것이 필요할 수 있음. 추가적으로 리소스가 든다는 것을 인지해야 함
- 구현해야 하는 기능
- 스케줄링 : Airflow에서 dbt run, test
- 알림/모니터링 : Slack 메시지 전송
- 대시보드 : Superset 등을 사용해 모델 지표 관리
- CI/CD : Github Action 등을 사용해 배포
- 구현해야 하는 기능
dbt 사용법
dbt 설치
- 글 작성 기준 최신 버전 명시
pip3 install dbt-core==1.9.1
(1) 프로젝트 생성
- CLI에서 아래 명령어 실행
- jaffle_shop이란 프로젝트를 init
- 이미 프로젝트가 있다면 이 작업은 생략 가능
dbt init jaffle_shop
(2) 프로젝트 폴더 구조
- dbt init을 하면 기본적으로 폴더가 생성됨
dbt init 할 때 생기는 폴더
├── README.md ├── analyses ├── dbt_project.yml ├── macros ├── models │ └── example │ ├── my_first_dbt_model.sql │ ├── my_second_dbt_model.sql │ └── schema.yml : 모델 정의 ├── seeds ├── snapshots └── tests
- dbt_project.yml : dbt 프로젝트 설정
- analyses/ : 임시 또는 일회성 분석을 위한 쿼리를 저장하는 폴더. dbt run을 해도 실행되지 않음
- macros/ : 재사용 가능한 매크로를 저장
- models/ : SQL 모델 파일들을 저장
- 보통 staging, intermediate, mart와 같이 하위 폴더로 구분
- schema.yml : 모델에 대한 정의. model name, description, columns, data_tests 등이 존재
- seeds/ : CSV 파일 형태의 변하지 않는 데이터를 저장
- snapshots/ : 데이터의 스냅샷을 찍기 위한 SQL 파일을 저장
- dbt snapshot 명령어로 실행
- tests/ : 데이터 품질 테스트 정의
(3) 모델 작성하기
- 모델 : 하나의 SQL 쿼리
- 쿼리의 결과가 데이터 웨어하우스의 테이블, VIEW로 저장됨
- 모델은 .sql 파일로 작성됨
- ‘참조(ref)’ 기능
- 특정 모델에서 다른 모델을 참조할 때, 테이블 이름을 직접 쓰는 대신
ref('model_name')
함수를 사용함 - 이 기능을 사용해 모델 간의 의존성을 자동으로 추적함
- 특정 모델에서 다른 모델을 참조할 때, 테이블 이름을 직접 쓰는 대신
- models/staging/stg_orders.sql 예시
-- models/staging/stg_orders.sql
{{ config(materialized='table') }}
SELECT
order_id,
customer_id,
order_date,
status,
amount
FROM raw_data.orders
WHERE status IS NOT NULL
- 위 테이블을 사용해서 일별 주문 집계를 구하고 싶다면
-- models/intermediate/int_daily_orders.sql
{{ config(materialized='table') }}
SELECT
DATE_TRUNC('day', order_date) as date,
COUNT(DISTINCT order_id) as order_count,
COUNT(DISTINCT customer_id) as customer_count,
SUM(amount) as total_amount
FROM {{ ref('stg_orders') }}
GROUP BY date
- 고객별 구매 이력을 만들 때는
-- models/marts/mart_customer_orders.sql
{{ config(materialized='table') }}
WITH customer_orders AS (
SELECT
customer_id,
COUNT(*) as total_orders,
SUM(amount) as total_spent,
MIN(order_date) as first_order_date,
MAX(order_date) as last_order_date
FROM {{ ref('stg_orders') }}
GROUP BY customer_id
)
SELECT
*,
DATEDIFF('day', first_order_date, last_order_date) as customer_lifetime_days
FROM customer_orders
- config의 materialization은 SQL 쿼리의 결과를 데이터 웨어하우스에서 어떻게 저장할지를 의미함
- table : 실제 테이블로 저장. 데이터가 자주 변경되지 않고, 쿼리 성능이 중요할 경우 사용
- view : 쿼리를 뷰로 저장. 저장 공간을 절약할 수 있지만, 쿼리 성능이 떨어질 수 있음. 다만 원본 데이터가 바뀌면 바로 볼 수 있음(최신 데이터 확인 가능)
- incremental : 테이블에 새로운, 변경된 데이터만 추가함
- emphemeral : 실제로 물리화되지 않고, 다른 모델에서 참조될 때 서브 쿼리로 사용
Incremental Materilization
- 자주 사용하는 패턴. 데이터를 증분만 처리
-- models/mart/web_analytics/daily_pageviews.sql
{{ config(
materialized='incremental',
unique_key='page_date_key'
) }}
SELECT
date_trunc('day', timestamp) as visit_date,
page_url,
count(*) as pageviews,
count(distinct user_id) as unique_visitors,
concat(date_trunc('day', timestamp), '-', page_url) as page_date_key
FROM {{ ref('stg_page_visits') }}
{% if is_incremental() %}
WHERE timestamp >= (SELECT max(visit_date) FROM ]{{ this }})
{% endif %}
GROUP BY 1, 2, 5
- config에서 incremental을 명시하고, unique_key를 추가함
- 증분 업데이트를 할 때 사용할 id를 의미함
- unique_key는 고유한 값이어야 하며, 여러 컬럼을 조합해서 사용할 수도 있음
- NULL이 있으면 안됨
{{ config( materialized='incremental', unique_key=['date_key', 'product_id', 'store_id'] ) }}
- 아래 if is_incremental()은 증분이 있는지 확인하는 것
- 이미 처리된 테이블이 있는 경우 실행하며, this는 현재 모델의 테이블을 의미
- timestamp 기준으로 현재 테이블에 저장된 최근 값(max(visit_date))보다 이후 데이터만 가져옴
{% if is_incremental() %} -- 이미 처리된 테이블이 있는 경우에만 실행 WHERE timestamp >= (SELECT max(visit_date) FROM {{ this }}) -- {{ this }}는 현재 생성되는 테이블을 참조 {% endif %}
insert, update를 모두 고려하고 싶다면 아래와 같이 작성
{% if is_incremental() %} WHERE created_at >= (SELECT max(created_at) FROM {{ this }}) OR updated_at >= (SELECT max(updated_at) FROM {{ this }}) {% endif %}
(4) 모델을 저장하는 방식
- models 하위 폴더 구조를 고민하는 것이 필요함
- dbt는 여러 직무에서 같이 협업하며 사용할 수 있기 때문에 협업에 최적화된 패턴을 고려해야 함
- 일관되고 이해하기 쉬운 구조를 만들어야 하며, 정답이 있는 것보단 조직 상황에 따라 다름
- 다양한 방식이 있지만, 대표적인 2가지 공유
대표적인 방식 : 비즈니스 도메인 맞춤
- dbt에서 가이드로 주고 있는 형태
- staging : 초기 정제
- intermediate : 중간 집계나 조인
- mart : 최종 분석용 테이블
- 비즈니스 도메인 별로 구분
- 이런 구성을 사용하면 파일 이름만 봐도 흐름이 명확해짐
models/
├── staging/
│ ├── stg_orders.sql
│ └── stg_customers.sql
├── intermediate/
│ ├── int_order_payments.sql
│ └── int_customer_orders.sql
└── mart/
├── finance/
│ ├── daily_revenue.sql
│ └── monthly_profit.sql
├── marketing/
│ ├── customer_segments.sql
│ └── campaign_performance.sql
└── product/
├── product_metrics.sql
└── inventory_analysis.sql
다른 방식 : Fact / Dimension
- Kimball의 차원 모델링 방식
- Facts / Dimension 관점으로 처리
- staging, intermediate는 동일
- Fact(팩트) 테이블
- 비즈니스 프로세스의 측정값/메트릭을 저장
- 주로 숫자형 데이터(금액, 수량 등)
- 매우 자주 업데이트되고 크기가 계속 증가
- Dimension(디멘전, 차원) 테이블
- 비즈니스 엔티티의 속성 정보를 저장
- 주로 텍스트/카테고리형 데이터
- 상대적으로 천천히 변함
- 이 방식의 장점
- 명확한 분리 : 팩트, 디멘전을 나눠서 각각의 업데이트 주기와 처리 방식을 나눔
- 쿼리 최적화 : 차원 테이블은 작고 자주 JOIN되어서 캐싱, 팩트 테이블은 필요한 컬럼만 포함해 효율적
- 유지보수 : 속성 변경은 차원 테이블에서만 관리함. 새로운 값은 팩트 테이블에만 추가
models/
├── staging/ # 원본 데이터 정제
├── intermediate/ # 중간 처리
└── marts/
├── facts/ # 측정값이 있는 팩트 테이블
│ ├── fct_orders.sql
│ └── fct_page_views.sql
└── dimensions/ # 차원 테이블
├── dim_customers.sql
└── dim_products.sql
Fact, Dimension 결과를 사용하는 예시 쿼리
SELECT d.customer_segment, d.country, COUNT(DISTINCT f.order_id) as total_orders, SUM(f.total_amount) as total_revenue FROM {{ ref('fct_orders') }} f JOIN {{ ref('dim_customers') }} d ON f.customer_id = d.customer_id GROUP BY 1, 2
(5) 모델 실행 & Test
- 모델 파일을 작성한 후, 실행 및 테스트를 진행해야 함
- dbt run 명령어로 실행할 수 있음
- +을 붙이면 의존된 모델을 실행함
- 앞에 붙이면 상위 의존성, 뒤에 붙이면 하위 의존성
- 추가적인 내용은 dbt Graph Operator Docs
# 특정 모델 실행 dbt run --select my_model # 하위 의존성 모델 포함해 실행 dbt run --select my_model+ # 상위 의존성 모델 포함 dbt run --select +my_model # 상위와 하위 의존성 모델 모두 포함 dbt run --select +my_model+
- dbt run 관련 문법은 Doc 참고
- Argument는
--select
,--exclue
,--selector
,--defer
가 있음 - 특정 모델, 그룹만 실행하고 싶을 땐 –select 를 사용하면 됨
dbt run --select "my_dbt_project_name" # 프로젝트의 모든 모델 dbt run --select "my_dbt_model" # 특정 모델만 dbt run --select "path/to/my/models" # 특정 디렉토리의 모든 모델 dbt run --select "tag:nightly" # "nightly" 태그가 있는 모델들
- Argument는
- 만약 문법 검사를 하고 싶다면 dbt compile로 확인할 수 있음
- 모델 참조 확인, SQL 문법 등
증분 모델 전체 재실행
- 일반적인 증분 모델도 동일하게 dbt run으로 실행함
- 그러나 처음부터 다시 실행해야 할 경우엔 –full-refresh 인자와 실행
- Incremental 모델의 스키마가 변경되어 다시 만들어야 하는 경우
- 새로운 논리가 추가되어 모델 전체를 다시 처리
- 내부적으로 기존 테이븡를 DROP하고 모든 데이터를 새로 계산해 적재함(탐색하는 범위에 비용을 부과하는 데이터 웨어하우스에선 비용이 많이 드는 행위일 수 있음)
db run --full-refresh --select my_model
dbt Test
- dbt test는 2가지로 구성됨
- generic
models/{모델_파일}
이 있는 폴더에 저장된schema.yml
에 저장함- 특정 컬럼이 Unique인가? Not Null인가?
- relationship : 특정 테이블의 컬럼에 존재하는 값인지
- accepted_values : 지정된 값만 있는지
- dbt_expectations(추가 패키지 설치 필요) : dbt hub 참고. 여러 Test가 존재함
# models/intermediate/schema.yml version: 2 models: - name: int_daily_sales columns: - name: date tests: - not_null - unique: combination_of: [date, product_id] - name: daily_sales tests: - not_null - positive_value
- singular
tests
폴더에 .sql 파일로 정의함- 복잡한 비즈니스 로직을 주로 저장함
- 실패 케이스를 반환하는 SQL 쿼리 작성
- 아래 쿼리는 결과 row가 0건이면 성공, 1건 이상이면 실패를 의미
-- tests/assert_total_payment_amount_is_positive.sql SELECT order_id, total_amount FROM {{ ref('orders') }} WHERE total_amount < 0
- 매출 합계와 일별 매출의 합이 같은지 확인하고 싶은 경우
-- 매출 합계가 일별 매출의 합과 같은지 확인 SELECT CASE WHEN total_revenue != daily_sum THEN '정합성 확인 필요' END as check_result FROM ( SELECT SUM(revenue) as total_revenue FROM {{ ref('revenue') }} ) t1 CROSS JOIN ( SELECT SUM(daily_revenue) as daily_sum FROM {{ ref('daily_revenue') }} ) t2 WHERE total_revenue != daily_sum
- generic
- dbt test 명령어로 실행
dbt test # 모든 테스트
dbt test --select my_model # 특정 모델의 테스트만
dbt test --select test_type:generic # generic 테스트만
dbt test --select test_type:singular # singular 테스트만
dbt test --select result:fail #실패한 테스트만 재실행
(6) dbt build
- build = run + test + snapshot + seed 모두 한번에 실행
- 실행 순서
- Seedfile Load
- Model Run
- Snapshot Run
- Test run
- 모델을 만들고, 테스트를 실행하는 과정
dbt build --select my_model
언제 build를 하는가?
- 모델 개발, 테스트 작성할 때는 run, test를 각각 하는 것이 효율적
- build는 Airflow에서 주기적으로 실행하곤 함
- 하지만 조직에 따라선 build를 안하고 dbt run만 하는 경우도 존재함(Test까지 하는 것이 이상적이나 현실적으로 Test를 많이 챙기려면 시간이 더 들기 때문에)
- 빌드 시간을 줄이기 위해 수정된 것만 빌드하는 인자도 존재
dbt build --select state:modified+ --defer --state path/to/prod/artifacts
- state:modified+ : 변경된 모델과 의존된 다운스트림만 실행
- state path: 프로덕션 환경의 상태와 비교해 어떤 모델이 변경되었나 확인
- defer : 변경되지 않은 모델은 프로덕션 데이터 참조
추가로 고려할 것들
- 기존에 SQL 기반으로 마트를 만들었다면 마이그레이션이 필요함
- dbt docs 배포
- dbt model에 대한 내용을 문서로 배포해, 사내 구성원들이 볼 수 있게 해야 함
- 김진석님의 dbt Docs 사내 공유 방법 feat. 사이트 호스팅 가이드 추천
- 대시보드와 통합하기 : lightdash 추천
- dbt + Airflow(스케줄링 도구)
- 주기적으로 실행하기 위해 Airflow 같은 도구와 통합 필요
- 또는 GitHub Action에서 실행할 수도 있음
- dbt metrics을 활용해 지표 집계
- dbt utils 확인하기
- dbt utils에 유용한 함수들이 많이 존재함
- dbt + 데이터 웨어하우스
- 이번 글은 dbt에 대한 전반적인 소개를 목적으로 한 글이라 이 내용을 포함하진 않았으나, 다음 글은 dbt + 데이터 웨어하우스 글을 작성할 예정
- dbt와 유사한 역할을 하는 도구는 dbt 외에도 SQLMesh라는 것도 존재함. dbt가 초반 러닝커브가 높은데(Jinja 템플릿을 활용) 이 부분
- 궁금하다면 Is It Time To Move From dbt to SQLMesh? 추천. SQLMesh가 더 좋은 부분도 있으나 dbt가 생태계가 더 큼. 마치 Airflow가 배치 스케줄링에서 제일 점유율이 높고, Prefect나 다른 도구들이 계속 발전하는 것과 유사한 느낌
- 그러나 아직 dbt를 도입하지 않았다면 SQLMesh를 도입하는 것도 괜찮다고 생각. dbt를 쓰다가 옮기는 것은 고민됨
- 궁금하다면 Is It Time To Move From dbt to SQLMesh? 추천. SQLMesh가 더 좋은 부분도 있으나 dbt가 생태계가 더 큼. 마치 Airflow가 배치 스케줄링에서 제일 점유율이 높고, Prefect나 다른 도구들이 계속 발전하는 것과 유사한 느낌
추천 자료
- Seoul dbt Meetup : dbt 행사를 종종 여시는데, dbt 사용자라면 참석 추천
- (쏘카) 데이터에 신뢰성과 재사용성까지, Analytics Engineering with dbt
- (당근) DBT와 Airflow 도입하며 마주한 7가지 문제들
- dbt를 통한 데이터 웨어하우스 개발 후기
- 오토피디아 데이터 웨어하우스 구축하기
- 글 작성하는데 걸린 시간 : 3시간 33분
- 하고자 하는 이야기, 개요 정리 : 13분
- 초안 글 작성 : 2시간 50분
- 클로드와 셀프 글 피드백 : 30분
- 간단 회고
- dbt + BigQuery까지 쓰려니까 내용이 너무 많아져서 두번째 글을 작성할 예정
카일스쿨 유튜브 채널을 만들었습니다. 데이터 분석, 커리어에 대한 내용을 공유드릴 예정입니다.
PM을 위한 데이터 리터러시 강의를 만들었습니다. 문제 정의, 지표, 실험 설계, 문화 만들기, 로그 설계, 회고 등을 담은 강의입니다
이 글이 도움이 되셨거나 의견이 있으시면 댓글 남겨주셔요.