dbt 실습 repo

 

 

 

< dbt 란 >

 

SQL 기반 데이터 변환 도구.

ETL 의 Transformation 을 담당

 

dbt 로 만들어진 (데이터 처리를 위한) SQL은 재사용이 가능하고

dependency 관리가 수월하다는 것이 큰 장점임

또한 dbt 는 테스트 기능 역시 제공하며

문서화 및 배포까지 도와줌

 

dbt 는 데이터 모델링 작업을 위한 데이터 처리를 하는 데 많이 사용됨

 

 

 

 

 

 

< dbt 알아야 하는 개념들 >

 

  • model
  • ref()
  • incremental model, table, view
  • macro, jinja
  • snapshot
  • source(), source freshness
  • seed
  • materialization
  • dependency
  • materialized : table, view
  • test - schema test, singular test, custom test, whatnot
  • dbt core - 오픈소스, CLI 기반, 로컬 개발
  • dbt cloud - SaaS, UI 제공, 스케줄링, 협업 기능

 

 

 

< dbt project 디렉토리 구조 gi>

 

dbt_project/
├── models/
│   ├── staging/
|        ├── stg_****.sql
|        └── schema.yml  
│   ├── intermediate/
|        ├── int_****.sql
|        └── schema.yml  
│   └── marts/
|        ├── fact_****.sql
|        ├── dim_****.sql
|        └── schema.yml  

├── seeds/
├── snapshots/
├── macros/
├── tests/
├── analyses/
├── snapshots/

├── dbt_project.yml
├── packages.yml
└── profiles.yml

 

 

 

 

 

 

< models >

 

dbt 는 주로 raw 데이터를 staging 에 적재하고, intermediate, marts 과정을 거쳐 데이터를 처리하게 됨

이건 차원 모델링이나 메달리온 아키텍처와도 연관됨

 

models 내에 transformation 작업을 진행하는 SQL 이 들어있음

 

models 내에 SQL 파일들이 실행되면 나중에

table 이 되거나

view 가 되거나

incremental table 이 됨

그리고 dbt 가 알아서 dependency graph 를 그린다고 함

 

 

 

 

 

models/staging 단계에서는 최소한의 transformation 작업이 진행됨

- 컬럼명 정리

- 타입 정리(캐스팅)

- null 처리

원본 데이터를 최대한 파괴하지 않는 선에서 작업 진행. lake 로 사용할 수 있도록

 

 

 

 

models/intermediate 단계에서는 복잡한 transformation 을 분리하는 작업이 진행됨

말 그대로 중간 계산 과정을 만드는 단계

- join

- aggregation

- business logic

- reusable logic 

등의 처리를 진행하게 됨

 

예를 들어

select
    customer_id,
    count(*) as total_orders,
    sum(amount) as lifetime_value
from {{ ref('stg_orders') }}
group by 1

 

데이터 처리를 위해 엄청 복잡하고 방대한 SQL 이 실행되어야 하는 경우,

이걸 한 번에 다 처리하면 디버깅하기도 어렵고 재사용하기도 어렵고 이해하기도 어려움

따라서 intermediate 과정을 중간에 넣어서, 중간 생산물들을 처리하도록 하는 것

나중에 최종 테이블 만들기 위한 transformation 작업이 복잡해지지 않고

중간결과물 재사용도 가능하도록

 

 

 

 

 

models/marts 단계에서는 최종 비즈니스 테이블을 만드는 작업이 진행됨

여기 만들어진 테이블은 BI, analytics teams 가 보고 비즈니스 결정하는 데 사용함

 

차원모델링의 최종 결과 테이블들(fact, dim tables) 가 이 단계에서 생성됨

 

아래와 같이, 도메인별로 각기 다른 차원 테이블들을 만들어주면 도메인별로 분석하기가 편해짐

팀 단위의 ownership 을 구분하고 결정하기도 쉬워짐

 

models/
└── marts/
    ├── finance/
    │   ├── fct_payments.sql
    │   └── dim_customers.sql
    │
    └── marketing/
        ├── fct_campaigns.sql
        └── dim_channels.sql

 

 

그래서 models 의 전체적인 flow 를 보면 아래와 같게 됨

raw_orders
    ↓
stg_orders
    ↓
int_customer_orders
    ↓
fct_customer_revenue

 

dbt run 하면 dbt 가 위와 같은 dependency graph 를 자동으로 생성해서 처리한다고 함

(=stage 건너뛰고 marts 먼저 만드는 등 순서를 뒤죽박죽 섞어서 실행하지 않는다는 말)

 

 

 

 

 

그럼 models 마다 존재하는 schema.yml 은 뭐냐?

shecma.yml 은 

- test 를 정의하는 역할
- documentation 만드는 역할

- column 을 설명하는 역할

 

아래와 같은 schema.yml 이 있다고 해보자

version: 2

models:
  - name: stg_orders
    columns:
      - name: order_id
        tests:
          - not_null
          - unique

 

 

models.columns.tests 항목을 통해, 해당 컬럼의 test 를 설정할 수 있음

예를 들어 null 이 존재하는지 확인하는 not_null,

중복이 없는지 확인하는 unique 등등

 

 

 

 

 

< macros >

 

재사용 가능한 SQL 함수를 정의해 두는 곳

말 그대로, 진짜 함수(파라미터 받아서 결과 반환하는)를 만들어 두는 곳

 

예를 들어, 아래와 같은 macro(generate_surrogate_key.sql)가 존재하고 내용도 아래와 같다고 하자

macros/
└── generate_surrogate_key.sql
{% macro cents_to_dollars(column_name) %}
    {{ column_name }} / 100.0
{% endmacro %}

 

함수 이름은 cents_to_dollars

함수의 파라미터는 column_name

 

아래와 같이 models 내 SQL에서 불러 사용할 수 있음

{{ cents_to_dollars('amount') }}

 

 

왜 macro 를 사용해야 하나?

바로 코드 중복을 제거해줄 수 있기 때문에

동일한 코드를 다양한 곳에서 똑같이 쓸 필요가 없고

또 macro 하나만 수정하면 나머지에 전부 적용되기 때문에 관리하기도 편함

 

 

 

 

 

 

 

< dbt_project.yml >

 

이 파일 내부에는 프로젝트의 핵심 설정 파일이 들어있음

dbt 프로젝트 전체의 동작 방식을 정의하는 중앙 설정 파일임.

(Docker Compose의 docker-compose.yml 같은 거라고 보면 된대!)

 

만약 내가 아래와 같은 sql 을 만들고 dbt run 을 하면

models/staging/stg_orders.sql
select * from raw.orders

 

dbt 는 이 sql 을 실행하기 위해 갖가지 정보가 필요함

- 테이블 어디에 만들지?

- view 로 만들어야 하나? table 로 만들어야 하나? 아니면 incremental?

- 어떤 schema 에 만들지?

- 어떤 db 에 만들지? 등등

이런 정보들을 제공하는 곳이 바로 dbt_project.yml 임

 

 

dbt_project.yml 파트별로 예를 들어봄

name: my_project
version: '1.0'
config-version: 2

 

name 은 dbt 프로젝트의 이름임

dbt init my_project 명령어를 실행해서 만들어졌을 것임

version 은 프로젝트의 버전. CI/CD 에서 사용된다고 함

config-version 은 설정파일 포맷 버전인데, 2로 고정됨. 왜 넣는지는 모르겠음

 

model-paths: ["models"]
seed-paths: ["seeds"]
snapshot-paths: ["snapshots"]
macro-paths: ["macros"]
test-paths: ["tests"]

 

위와 같은 config 를 줬기 때문에 dbt 가 어느 디렉터리에서 정보를 가져가야 할 지 알게 됨

이를테면, model-path: ["models"] 로 설정된 부분으로 인해, dbt 는 "model 들은 'models' 디렉터리 안에 있구나" 라고 인식함

 

models:
  my_project:

    staging:
      +materialized: view
      +schema: staging

    intermediate:
      +materialized: ephemeral

    marts:
      +materialized: table
      +schema: marts

 

위 config 의 'models'는 'model 에 대한 설정'을 의미함

models 이하에 나오는 config 를 통해

각 models(SQL 파일들)을 어떻게 처리해야 할 지 알 수 있음

 

my_project 는 프로젝트 이름

위의 name: my_project 와 연결됨

(프로젝트 이름을 굳이 다시 넣었다는 말은.. dbt_project.yml 내에 여러 프로젝트의 config 가 들어갈 수 있다는 말인가?)

 

materialized 가 뭐하는거야?

SQL 결과를 warehouse 에 어떤 형태로 저장할지 결정하는 부분

SQL 을 실행한 후 결과를 table 로 만들 수 있고, view 로 만들 수 있고, 혹은 incremental table 로 만들 수 있음

잘 보면, 각각의 SQL 파일들마다 materialized 가 있는게 아니고, 각 단계(staging, intermediate, marts)마다 materialized 가 존재함

즉, materialized 는 하나의 단계 전체적으로 적용된다고 볼 수 있음. 내가 이해한 게 맞나?

 

+materialized : view

여기서 view 는 실제 데이터를 저장하지 않는 가상 테이블임.

create view 명령어 실행되어 생성되는 뷰 그거 맞음

query 실행되는 런타임마다 그때 그때 계산되어 생성 및 사용되기 때문에, 항상 최신 데이터를 갖고 있음

하지만 매 런타임마다 계산되기 때문에, select 퍼포먼스가 느림
실제 테이블을 만들지 않기 때문에 차지하는 storage 적음

심지어 view 는 BI tool 에서도 접근이 가능함. BI tool 이 view 테이블에 접근할 때마다 view 쿼리를 계속 재실행한다는 말이지.....

view 는 staging 처리할 때 자주 사용된다고 하는데.. 그 이유가, 굳이 물리적인 테이블을 만들 필요가 없어서..?

staging 을 위한 sql 이 비용이 적은 이유(타입 캐스팅 혹은 컬럼명 변경 등)는 알겠어

그래서 view 처럼 런타임에 실행해도 무리가 없다는 거지

근데 staging 은 ground truth 역할(lake 마냥)을 해야하기 때문에 무조건 물리적인 테이블로 만들어둬야 하지 않나?

 

+materialized : table

여기서 table 은 실제 데이터를 저장하는 물리적인 테이블을 의미함

따라서 storage 용량을 차지하며 생성될 때 갖고있는 데이터를 그대로 보관하게 되며

하지만 view 와 달리 select 실행시 재계산을 하지 않아도 되기 때문에 select 퍼포먼스가 빠름

 

+materialized : incremental

변경분만 table 에 업데이트 함. hudi 의 upsert 와 같은 느낌인거지

즉, 기존 테이블이 없으면 새로운 table 을 만들고

기존 테이블에 데이터가 없으면 새로 데이터를 추가하고

기존 테이블에 데이터가 있으면 변경된 부분만 업데이트 함

incremental 이 적용되는 model 쿼리를 한 번 보자

select *
from orders
where updated_at > (
    select max(updated_at)
    from {{ this }}
)

위와 같이 {{ this }} 를 통해 자기 자신 테이블에서 최신 날짜만 가져온 후

최신 날짜 이후로 들어온 데이터만 추출해서 incremental 로 업데이트

 

+materialized : ephemeral

실제 table이나 view 는 생성하지 않지만

SQL 내부에 inline 된다고....(????)
ephemeral 로 설정된 model SQL 을 실행해도

warehouse 내에 object 를 생성하지 않음(그건 view 도 마찬가지잖아?-> 개발자 입장에서는 view 도 object를 만들지 않는것 처럼 보이는데, 실제로 warehouse 입장에서는 object 처럼 보인다고 함. 그래서 select * from my_view 같이 from 에 view 를 넣은 쿼리가 가능함)

ephemeral 로 설정된 model SQL 은 downstream SQL 에서 CTE 처럼 삽입된다고 함

view 랑 다른게 뭐지...??

위의 예제에서 intermediate 부분에 ephemeral 이 설정되어있고,

marts 에 table 이 설정되어 있잖아

intermediate 및 marts SQL model 이 아래와 같다고 하자

-- int_orders.sql

select
    customer_id,
    sum(amount) as total_amount
from {{ ref('stg_orders') }}
group by 1
-- fct_customer_revenue.sql

select *
from {{ ref('int_orders') }}
where total_amount > 100

이걸 실행시키면, ephemeral 로 설정된 int_orders 의 쿼리가 fct_customer_revenue 쿼리 안으로 쏙 들어감

select *
from (

    select
        customer_id,
        sum(amount) as total_amount
    from stg_orders
    group by 1

) as int_orders
where total_amount > 100

 

이걸 보고 inline 이 된다고 한 거임....

 

view 랑 ephemeral 의 차이는 알았는데, 그럼 각각은 무슨 장단점이 있을까?

- view 는 warehouse 에서 조회가능한 object를 주르륵 만들기 때문에, 여러개의 view 를 만들면 지저분해질 수 있음

ephemeral 은 지저분하지 않음(....이게 장점인가?)

- ephemeral 은 object 생성이 어려우니 디버깅하기가 어려움.

즉, view 처럼 select 해서 직접 값을 볼 수 없으니 ephemeral 로직이 잘 실행되었는지 아닌지 몰라

- ephemeral 을 사용하면 하나의 큰 nested sql 이 생성됨. 이게 단점이 되나?

- downstream 에서 동일한 ephemeral 을 여러번 사용하면, 중복 계산이 되기 때문에 퍼포먼스가 좋지 않음

 

그럼 ephemeral 은 언제 쓰는거야?

warehouse object 를 만들 가치가 없는 경우에 쓴다고 함(....이게 뭔 소리임)

너무 작은 크기의 view 를 만들 바에야 ephemeral 을 사용한다는 말인 듯

 

 

 

 

 

 

< seeds >

 

이곳에 csv 파일을 넣어두면, dbt 가 warehouse 에 자동으로 upload 한다고 함

작은 reference table 을 만드는 용도로 사용한다고 함

 

예를 들어 아래와 같은 csv 를 파일을 넣어두면....

 

country_codes.csv
currency_codes.csv

 

무슨 일이 벌어지지? 어느 db 에 만드는거야? 함부러 table 만드는 권한은 어떻게 갖고있는거지?

 

 

 

 

 

 

< tests >

 

custom test 를 저장하는 곳

예를 들어 아래와 같이 내가 원하는 테스트를 만들어 넣음 

tests/
└── assert_positive_revenue.sql
select *
from {{ ref('fct_orders') }}
where revenue < 0

 

결과가 나타난다면, 실패하는 거임...

select * 의 결과가 나타나지 말아야 함

 

ref('fct_orders') 는 fact table 대상으로 테스트를 진행한다는 말이 될 것 같음

 

그럼 여기 tests 디렉터리 내에 있는 테스트와, schema.yml 내에 있는 테스트의 차이는?

 

 

 

 

 

 

< snapshots >

 

변경 이력 추적용이라는데 뭔 소린지 모르겠음

 

 

 


< analyses >

 

ad-hoc SQL 을 저장하는 곳이라는데 뭔 소린지 모르겠음

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'Coding Interview' 카테고리의 다른 글

[IT] Data Warehouse + Data Modeling 공부  (1) 2026.04.16
[Spark] interview questions  (0) 2026.01.22
[Data Engineer] interview questions  (0) 2026.01.16
[Airflow] interview questions  (0) 2025.12.14
[SQL] interview questions  (0) 2025.12.12

+ Recent posts