왜 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.index | 1부터 시작하는 인덱스 |
loop.index0 | 0부터 시작하는 인덱스 |
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 웨어하우스와도 호환되는 이유이다.