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까지 쓰려니까 내용이 너무 많아져서 두번째 글을 작성할 예정
카일스쿨 유튜브 채널을 만들었습니다. 데이터 사이언스, 성장, 리더십, BigQuery 등을 이야기할 예정이니, 관심 있으시면 구독 부탁드립니다 :)
PM을 위한 데이터 리터러시 강의를 만들었습니다. 문제 정의, 지표, 실험 설계, 문화 만들기, 로그 설계, 회고 등을 담은 강의입니다
이 글이 도움이 되셨거나 다양한 의견이 있다면 댓글 부탁드립니다 :)