Minwoo writings

DBT (Data Build Tool) - 101

SQL for Product Level

들어가며,

  • 데이터 분석가로 일하며 겪은 문제 중 하나는
    • 온전한 제품을 제공하고 운영하는 과정은 일반적인 개발자와 다름없음에도 불구하고 기반한 문서나 코드의 퀄리티가 개발을 업으로 하는 사람들과 큰 차이를 보인다는 점
    • 분석의 과정이 정형화되어 있지 않고 / 다양한 모습의 산출물로 요구되기 때문이기도 하고 / 기반한 기술들이 아직 성숙하지 않은 문제도 있기 때문
    • 개인적으로 SQL 작성 시에 이러한 문제와 한계를 많이 체감했다.
  • SQL이 업무의 산출물인 경우
    • 주로 성능과 안정성을 고려해 작성되는 SQL, 가독성이 떨어지며 유지보수 또한 어려움
    • 하나의 코드 뭉치에 복잡한 로직이 엉켜있어 일부의 로직 수정이 코드 전반에 걸친 대대적인 수정을 필요로 하고, 전체를 재작성하는 것이 더 빠른 상황이 자주 나옴.
  • 문제 해결을 위해
    • 작성된 로직에 오류는 없는지 동료의 리뷰가 필요했고,
    • 변경된 로직에 따라 수정된 내역을 보관하고 기록할 수 있도록 버전 관리가 필요했고,
    • 이상 데이터가 포함되어 오류가 발생하는지 모니터링도 필요하며,
    • 여타 다른 개발 언어와 같은 수준의 편의 기능들이 필요했다.
    • 동시에 동료, 외부 조직의 접근성을 높일 수 있도록 출력 결과에 대한 안내, 문서화가 필요했다.
  • dbt는 이런 문제들을 모두 해결하기 위한 도구였고 큰 기대를 하고 보기 시작했다.

About dbt

  • ELT(Extract, Load, Transform) 중에서 T(Transform)를 위해 제작된 도구
    • ETL → ELT로의 변화, 혹은 그 이후의 개선을 위해 DBT와 같은 Transform만을 위한 도구가 대두된 것
    • 외부 데이터 소스로부터 데이터를 추출하거나 적재하는 기능 부재
    • 이미 적재되어 있는 데이터를 조회하고 수정하는 데에 최적화된 도구 large
      • DBT가 제안하는 Data Platform Stack은 [External Source → Extract & Loader → Warehouse → Consumer] 크게 4개 영역으로 나누고 Warehouse에서 일어나는 모든 Transform을 DBT가 담당하는 것
      • DBT는 이러한 데이터 가공 과정을 Modeling이라 표현
  • How it works large
    • SQL, YML로 구성
      • SQL 파일에 모델링 작성
      • YML 파일에 SQL 파일의 compile에 필요한 설정 값을 작성
    • Compiler와 Runner로 구성된 CLI Tool
      • Compiler
        • SQL, YML 파일을 Compile하여 또 다른 SQL 생성
        • Jinja Template Engine 사용
      • Runner
        • 컴파일된 SQL을 연결된 warehouse에 전달하여 테이블 생성
        • 실행 시 의존성을 가지는 모델을 파악해 자동적으로 DAG을 작성, 이에 따라 실행 계획을 만들고 순서대로 실행
    • 오픈소스, Pyhton으로 제작, PIP를 통해 설치 가능
  • Why & When
    • SQL을 코드처럼 작성해야할 때 필요함
    • 작성된 SQL의 변경사항을 공유하거나 충돌을 막기 힘듦 → 버전 관리가 필요
    • 추출할 지표에 대한 문서화와 공유는 SQL로 이뤄지기 힘듦 → 코드와 연동한 문서화 도구가 필요
    • 작성된 SQL을 재활용하기 힘듦 → 부분부분 독립적으로 모듈화하여 재활용
    • 작성된 SQL을 테스트하기 힘듦 → SQL에 대한 테스트 도구 필요
  • Avaliable Databases
    • [BigQuery, Snowflake, Postgres, Redshift, Apache Spark, Databricks, Presto] 연결 지원

기능 및 구성 요소

  • Project
    • dbt 프로젝트를 의미
    • 프로젝트의 설정은 dbt_project.yml에 기록
      • 프로젝트의 이름, 데이터세트의 이름 등의 설정
      • materialization 설정을 프로젝트/폴더 단위로 설정
      • 타겟 설정 (환경 설정의 일종)
    • 프로젝트의 생성은 아래와 같이 실행
      1
      
        $ dbt init foo 
      
    • 실행 결과로 아래와 같은 구조의 폴더/파일이 생성됨
  • Model
    • select 구문이 포함된 sql 파일
    • Jinja Template 사용하여 SQL로 표현이 어려운 코드 작성 가능하다.
    • 예시
      1
      2
      3
      4
      5
      6
      
        {{ set columns = ['a', 'b', 'c'] }}
        select
            {% for column_name in columns %}
            {{ column_name }},
            {% endfor %}
        from source_data
      
    • 작성한 모델의 실행은 아래와 같이 입력
      1
      
        $ dbt run --models foo
      
    • 최종적으로 생성될 테이블의 이름은 모델의 이름과 동일하게 설정됨
      • 작업 환경에서의 폴더 구조와 다르게 데이터세트 하나에 테이블이 저장되므로 모델의 네이밍이 중요.
      • 따라서, 모델 상위 디렉터리의 구조를 모델 이름에 중복해 기술해주는 것을 권장
        • 예를 들어,
          • bad : “foo”
          • good : “stg_payments__foo”
  • Materialization
    • Model을 저장 혹은 유지하는 방법
    • 기본적으로 [table, view, incremental, ephemeral] type이 제공됨
      • table : 테이블을 신규 생성하거나 기존 테이블이 존재하는 경우 재구축
      • view : view table 생성
      • incremental : 테이블을 신규 생성하거나 기존 테이블이 존재하는 경우 신규 데이터를 insert하거나 update
      • ephemeral : 실제 테이블을 생성하지 않고 다른 모델에서 dependency로 사용할 수 있도록 함
    • 예시1. table 생성, date 컬럼에 파티션 추가 설정
      1
      2
      3
      4
      5
      6
      7
      8
      9
      
        {{ config(
            materialized='table',
            partition_by={
              "field": "date",
              "data_type": "date"
            }
        )}}
        select * 
        from `project.dataset.table`
      
    • 예시2. incremental 테이블 생성 혹은 업데이트, date 컬럼에 파티션 추가 설정
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      
        {{ config(
            materialized='incremental',
            incremental_strategy='insert_overwrite',
            partition_by={
              "field": "date",
              "data_type": "date"
            }
        )}}
        select * 
        from `project.dataset.table`
        {% if is_incremental() %}
        where
            date >= (select max(date) from {{ this }})
        {% endif %}
      
      • incremental model은 이미 생성된 테이블이 존재하는 경우, is_incremental()의 반환값이 true가 되어 하위의 구문이 실행됨. 예시에서는 where 구문에 일자를 조건으로 건 것.
      • 최초 실행 시에는 fully refresh, 이후 실행 시에는 조회 범위의 제한을 두어 일부만 가져와 업데이트하는 방식.
      • incremental_strategy의 설정에 따라 병합하거나 덮어씌울 수 있음
    • 기본적으로 제공되는 네 가지 materialization 외에도 custom materialization을 작성할 수 있음
  • Test
    • Model이 의도에 맞게 작성되었는지 테스트하기 위한 도구
    • 아래와 같이 실행
      1
      
        $ dbt test
      
    • 크게 두가지 테스트 방법이 존재 - [Schema, Data]
      • Schema Test
        • unique, not_null, accepted_values, relationships 둥 지정한 컬럼의 값이 조건을 만족하는지 확인
        • Model이 소속되어 있는 폴더 내의 schema.yml에 설정
        • 기본적으로 제공되는 macro외에도 custom macro를 직접 만들어 테스트 가능
        • 예시
          1
          2
          3
          4
          5
          6
          7
          8
          9
          
            version: 2
              
            models:
              - name: orders
                columns:
                  - name: order_id
                    tests:
                      - unique
                      - not_null
          
      • Data Test
        • tests directory의 sql을 실행 후 실행 결과의 row 유무에 따라 테스트 실패를 판단
        • 예시
          1
          2
          3
          4
          5
          6
          
            select
                order_id,
                sum(amount) as total_amount
            from {{ ref('stg_payments') }}
            group by 1
            having not(total_amount >= 0)
          
  • Documentation
    • 제작한 모델에 대한 문서를 생성한다.
    • 생성된 문서는 웹 페이지 형태로 제공됨
    • 아래 명령을 실행해 문서를 생성함
      1
      
        $ dbt docs 
      
    • 생성된 문서의 예시 생성한 모델에 대한 설명, 컬럼 안내, SQL을 조회할 수 있다. 생성한 모델의 의존성을 파악할 수 있도록 시각화하여 제공한다.
  • Macro
    • 다수 Model에서 재사용할 수 있도록 작성된 Jinja 코드, 함수의 일종
    • 예시
      • cents_to_dollars.sql
        1
        2
        3
        
          {% macro cents_to_dollars(column_name, precision=2) %}
              ({{ column_name }} / 100)::numeric(16, {{ precision }})
          {% endmacro %}
        
        • macro의 구현 또한 sql 파일로 작성한다.
      • my_model.sql
        1
        2
        3
        4
        
          select
              id as payment_id,
              {{ cents_to_dollars('amount') }} as amount_usd
          from app_data.payments
        
        • 작성해둔 cents_to_dollars macro를 모델에서 사용하기 위해서는 jinja template으로 감싸 사용한다.
      • 위 모델을 컴파일하면 아래와 같은 결과
        1
        2
        3
        4
        
          select
              id as payment_id,
              (amount / 100)::numeric(16, 2) as amount_usd
          from app_data.payments
        
    • Macro를 위한 Package Manager가 제공됨
      • dbt_utils와 같이 타 사용자들이 제작한 macro가 dbt Hub을 통해 공유됨
      • 프로젝트 내의 package.yml 파일에 아래와 같이 설정한 뒤,
        1
        2
        3
        
          packages:
            - package: dbt-labs/dbt_utils
              version: 0.7.0
        
      • 아래 명령어를 통해 설치 진행
        1
        
          $ dbt deps 
        

실행

  • 모델을 테이블로 만드는 모든 과정은 Run을 통해서 진행
  • Arguments
    • profile : 연결할 profile의 설정, dbt_project.yml의 값을 overriding
    • vars : macro 등에서 사용될 변수를 전달
    • models
      • 특정 모델을 선택하여 실행한다.
      • + 기호를 사용하여 지정한 모델의 전/후 DAG을 일괄 실행하도록 설정할 수 있다.
      • 예시1. my_first_dbt_model model 실행
        1
        
          $ dbt run --models my_first_dbt_model
        
        • 결과로 my_first_dbt_model 테이블이 생성된다.
      • 예시2. my_first_dbt_model model과 my_first_dbt_model에 의존성을 가지는 모든 모델 실행
        1
        
          $ dbt run --models my_first_dbt_model+
        
        • 결과로 my_first_dbt_model, my_second_dbt_model 모델이 생성된다.
      • 예시3. my_second_dbt_model model과 my_second_dbt_model이 의존성을 가지는 모든 모델 실행
        1
        
          $ dbt run --models +my_second_dbt_model
        
        • 결과로 my_first_dbt_model, my_second_dbt_model 모델이 생성된다.
    • full-refresh
      • incremental model인 경우 drop 후 rebuild 하도록 설정한다.
  • Scheduling
    • dbt 자체적으로 Scheduling Run하는 방법이 없으므로 Airflow 등의 외부의 도구가 필요
    • dbt의 제작자인 dbt labs가 운영하는 dbt cloud는 이러한 scheduling tool을 제공

모델 구성

  • dbt의 모델링 과정에서 느슨하게 약속된 몇 가지 개념/용어들이 존재 (강제 사항이 아님)

  • Model / Modeling
    • Transform이 완료된 데이터 / Transform 과정을 의미함
  • Source Model
    • Raw Data를 의미함
  • Staging Model
    • Source와 1:1 대응하는 모델을 의미
    • 데이터를 깔끔하게 정리하거나 표준화하는 등의 단순한 작업을 거친 모델
  • Intermediate Model
    • Staging Model에서 Reporting 등의 Consumer가 직접 사용하는 최종 Model 사이의 모델을 의미
    • 이 부분부터 정의가 애매한 편 혹은 자유도가 높음. 프로젝트 혹은 용도에 따라 맞춰 사용
  • Fact Model
    • 지표 저장을 위한 모델
  • Dimension Model
    • Dimension 저장을 위한 모델
  • Source / Staging / Fact / Dimension 으로 구성된 모델 흐름의 예시 large
    • 예시와 같이 모델들을 구성하는 과정은 자유로운 편, 위와 같은 구성이 강제적이지 않다.
    • 공식 문서의 FAQs → Example Project를 확인해보면 Source, Staging Model 까지는 공통, 이후부터의 구현은 프로젝트에 따라 다름

dbt Cloud

  • Managed dbt service
  • 1인 사용 시 무료, 팀 협업 시 유료
  • 장점 : 간편하게 설정하고 실행할 수 있는 custom running environment, job scheduling, logging, documentation deploy
  • 단점 : 일반적인 IDE에 비해 떨어지는 편집기, 가끔 보이는 버그 / 불안정한 동작, 아쉬운 UI/UX, 터미널을 통한 환경 접근 불가능 → custom macro 작성시 상당히 불편함

    편집기 dbt가 실행될 환경에 대한 설정 실행된 job들의 목록 실행된 job의 상세 정보 설정, 프로젝트나 팀메이트 초대 등이 가능


사용 후 의견

  • 비용과 성능 걱정이 적다.
    • BigQuery, Snowflake, Redshift에서라면 비용/성능 고민없이 다수의 transform을 만들 수 있게 될 것
    • 단순히 컴파일/명령을 전달하는 도구이므로 dbt 동작이 위해 큰 퍼포먼스를 요구하지 않는다. → 큐잉, 밸런싱 등의 엔지니어링 부담 없이 운영 가능하다.
  • 데이터 조직의 기능을 개선할 도구
    • 모델링 작업을 통해 데이터를 개선한다.
      • 데이터 조직 내에서도 쿼리를 작성하고 공유하거나 유지보수하는 것이 상당히 원시적인 경우가 많다.
      • dbt는 버전 관리, 모듈화, 모델링의 레이어 구분 등. 운영에 필수적인 요소를 갖추고 있다. 일반적인 데이터 조직에서 이러한 필수적인 요소를 잘 갖추지 못한 경우가.. 적지 않을 것이다.
    • 테스트를 통해 신뢰를 구축한다.
      • 데이터 조직 내에서도 작성한 쿼리의 결과를 믿지 못하는 경우가 많다.
      • 잘못된 데이터의 유입이나 잘못된 쿼리 등등, 스스로를 믿는 좋은 방법이 될 것이다..
    • 문서화를 통해 공유를 용이하게 한다.
      • 작성한 마트로의 물리적인 접근은 쉽다해도 마트에 대한 안내 없이는 외부 조직에서 쉽게 사용할 수 없다.
      • 데이터 리니지, 테이블이나 컬럼 등에 대한 안내가 외부 조직의 접근성을 높이는데 도움이 될 것이다.
  • Transform에 진심인 편..
    • 기존 ETL, ELT 작업을 관리하던 Job이 있다 해도, 완전히 독립적인 T만을 지향한다.
    • 기존 데이터세트에 덮어씌우는 대신 dbt 전용의 데이터세트를 구성한 뒤, merge하는 방식을 권장 / merge는 dbt가 아닌 다른 도구로..
    • Transform 과정에서 stateless를 극단적으로 유지하는 것을 권장
    • 원론적으로는 동의하고 맞는 말이지만, 레거시와 병합하는 과정에서는 괴로울 것..
  • 다소 불편한 UX
    • SQL 작성 시 문법 오류 확인이 어렵다.
      • 컴파일 결과를 dry run하기 전까지는 작성한 쿼리의 오류 여부를 확인할 수 없다. 따라서 작성→컴파일→수정→컴파일 과정을 불필요하게 반복하게 된다.
      • materialization의 구현을 직접 확인하기 전까지는 실제 컴파일 결과를 예상하기 어렵다. 사용자는 select statement만 작성하지만 실제 컴파일 결과에는 drop / create 등의 구문이 추가된다.
    • 컴파일 결과를 볼 때 불편하다.
      • models 폴더에 작업하고 동일한 이름의 파일로 target 폴더에 결과가 저장된다. 오류를 수정하고 컴파일 결과 파일을 수정하는 경우가 허다하다.
    • 매크로 작업이 다소 불편하다.
      • 기존 사용 가능한 매크로에 대한 안내가 부족해 일일히 코드 저장소를 찾아봐야 한다.
      • Python으로 작성된 adapter 기능의 안내 또한 매우 부족하며, custom으로 만들기 어렵다. 따라서 다소 복잡한 요구사항의 macro를 구현하는데에 제한이 크다.
      • 매크로 작성 시의 오류를 디버깅하는 것이 어렵다.
      • Package Manager가 제공되나.. npm pip 등과 비교해 사용 방법이 난해한 편

References