Back to Blog
dbtJinjaMacrosSQL Templatedbt Packages

0x04. dbt Jinja & Macros - SQL에 프로그래밍을 더하는 법

반복문, 조건문, 재사용 함수를 SQL에 추가하는 Jinja 템플릿 엔진과 매크로 시스템을 정리한다.

왜 Jinja인가

SQL은 강력하지만 DRY(Don't Repeat Yourself) 원칙을 지키기 어렵다. 결제 방식이 3개이면 CASE WHEN을 3번 작성하고, 10개이면 10번 작성해야 한다. 환경에 따라 스키마를 바꾸려면 SQL 파일을 여러 벌 관리해야 한다.

dbt는 Jinja 템플릿 엔진을 SQL 위에 얹어 이 문제를 해결한다. 변수, 반복문, 조건문, 함수를 SQL 안에서 사용할 수 있다. 결과적으로 컴파일 시점에 순수 SQL로 변환되므로 웨어하우스는 Jinja의 존재를 모른다.

Jinja 기본 문법

문법용도예시
{{ }}표현식 출력{{ ref('stg_orders') }}
{% %}제어문 (if, for){% if is_incremental() %}
{# #}주석 (컴파일 시 제거){# TODO: 리팩토링 #}

반복문으로 반복 SQL 제거

기본 패턴

{% set methods = ['credit_card', 'bank_transfer', 'coupon'] %}

SELECT
    order_id,
    {% for method in methods %}
        SUM(CASE WHEN payment_method = '{{ method }}' THEN amount ELSE 0 END)
            AS {{ method }}_amount
        {% if not loop.last %},{% endif %}
    {% endfor %}
FROM {{ ref('stg_payments') }}
GROUP BY order_id

컴파일 결과:

SELECT
    order_id,
    SUM(CASE WHEN payment_method = 'credit_card' THEN amount ELSE 0 END) AS credit_card_amount,
    SUM(CASE WHEN payment_method = 'bank_transfer' THEN amount ELSE 0 END) AS bank_transfer_amount,
    SUM(CASE WHEN payment_method = 'coupon' THEN amount ELSE 0 END) AS coupon_amount
FROM raw.jaffle_shop.stg_payments
GROUP BY order_id

결제 방식이 3개에서 10개로 늘어도 배열만 수정하면 된다.

loop 변수 활용

Jinja의 for 루프는 loop 객체를 제공한다:

속성설명
loop.index1부터 시작하는 인덱스
loop.index00부터 시작하는 인덱스
loop.first첫 번째 반복이면 True
loop.last마지막 반복이면 True
loop.length전체 반복 횟수

loop.last는 콤마 처리에 필수적이다. SQL에서 마지막 컬럼 뒤에 콤마가 있으면 구문 오류가 발생하기 때문이다.

조건문

환경 분기

SELECT *
FROM {{ ref('stg_orders') }}

{% if target.name == 'dev' %}
  WHERE order_date >= DATEADD(day, -30, CURRENT_DATE())
{% endif %}

개발 환경에서는 최근 30일 데이터만, 프로덕션에서는 전체 데이터를 사용한다. 개발 속도와 비용을 절약할 수 있다.

Incremental 분기

{{ config(materialized='incremental', unique_key='order_id') }}

SELECT * FROM {{ source('jaffle_shop', 'orders') }}

{% if is_incremental() %}
  WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}

is_incremental()은 dbt가 제공하는 내장 매크로이다. 첫 실행 시 false(전체 로드), 이후 true(증분 로드)를 반환한다.

매크로 — 재사용 함수

기본 매크로

-- macros/cents_to_dollars.sql
{% macro cents_to_dollars(column_name, precision=2) %}
    ROUND({{ column_name }} / 100, {{ precision }})
{% endmacro %}
-- 모델에서 사용
SELECT
    order_id,
    {{ cents_to_dollars('amount_cents') }} AS amount_dollars,
    {{ cents_to_dollars('tax_cents', 4) }} AS tax_dollars
FROM {{ ref('stg_payments') }}

고급 매크로 — SQL 생성기

-- macros/generate_schema_name.sql
{% macro generate_schema_name(custom_schema_name, node) %}
    {% if target.name == 'prod' %}
        {{ custom_schema_name | default(target.schema, true) }}
    {% else %}
        {{ target.schema }}_{{ custom_schema_name | default(target.schema, true) }}
    {% endif %}
{% endmacro %}

dbt의 기본 동작을 오버라이드하는 매크로이다. 이 예시는 프로덕션에서는 analytics.fct_orders, 개발에서는 dev_jjin_analytics.fct_orders처럼 스키마를 분리한다.

매크로 디버깅

# 컴파일된 SQL 확인 (실행하지 않음)
dbt compile -s my_model

# target/compiled/ 폴더에서 결과 확인
cat target/compiled/my_project/models/marts/fct_orders.sql

Jinja가 제대로 작동하는지 확인할 때 dbt compile을 사용한다. 실제 쿼리를 실행하지 않고 SQL만 생성한다.

dbt Packages — 커뮤니티 매크로

packages.yml에 선언하고 dbt deps로 설치한다.

# packages.yml
packages:
  - package: dbt-labs/dbt_utils
    version: [">=1.0.0", "<2.0.0"]
  - package: calogica/dbt_expectations
    version: [">=0.10.0", "<0.11.0"]
  - package: dbt-labs/codegen
    version: [">=0.12.0", "<0.13.0"]

dbt-utils 주요 매크로

매크로용도예시
surrogate_key복합 키로 해시 생성{{ dbt_utils.generate_surrogate_key(['order_id', 'item_id']) }}
date_spine날짜 범위 테이블 생성누락 날짜 탐지용
pivot행을 열로 피벗동적 피벗 테이블
star모든 컬럼 선택 (특정 컬럼 제외){{ dbt_utils.star(from=ref('stg_orders'), except=['_loaded_at']) }}
union_relations여러 테이블 UNION멀티 소스 통합

codegen — 보일러플레이트 자동 생성

# Source YAML 자동 생성
dbt run-operation generate_source --args '{"schema_name": "jaffle_shop", "database_name": "raw"}'

# Model YAML (컬럼 목록) 자동 생성
dbt run-operation generate_model_yaml --args '{"model_names": ["stg_orders"]}'

새 소스를 추가할 때 YAML을 수동으로 작성하는 대신 codegen으로 뼈대를 자동 생성하면 시간을 절약할 수 있다.

컴파일 과정 요약

Jinja + SQL (개발자 작성)
      dbt compile
순수 SQL (웨어하우스가 실행)

dbt는 Jinja를 처리하여 순수 SQL을 생성하고, 이를 웨어하우스에 전달한다. 웨어하우스는 Jinja의 존재를 전혀 모른다. 이 분리가 dbt가 어떤 SQL 웨어하우스와도 호환되는 이유이다.