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 방식이 업계에서 알려지고, 채택되는 케이스가 많아짐
  • 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" 태그가 있는 모델들
    
  • 만약 문법 검사를 하고 싶다면 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
        
    
  • 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 배포
  • 대시보드와 통합하기 : 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를 쓰다가 옮기는 것은 고민됨


추천 자료



  • 글 작성하는데 걸린 시간 : 3시간 33분
    • 하고자 하는 이야기, 개요 정리 : 13분
    • 초안 글 작성 : 2시간 50분
    • 클로드와 셀프 글 피드백 : 30분
  • 간단 회고
    • dbt + BigQuery까지 쓰려니까 내용이 너무 많아져서 두번째 글을 작성할 예정

카일스쿨 유튜브 채널을 만들었습니다. 데이터 사이언스, 성장, 리더십, BigQuery 등을 이야기할 예정이니, 관심 있으시면 구독 부탁드립니다 :)

PM을 위한 데이터 리터러시 강의를 만들었습니다. 문제 정의, 지표, 실험 설계, 문화 만들기, 로그 설계, 회고 등을 담은 강의입니다

이 글이 도움이 되셨거나 다양한 의견이 있다면 댓글 부탁드립니다 :)

Buy me a coffeeBuy me a coffee





© 2017. by Seongyun Byeon

Powered by zzsza