SQLMesh 사용법 : dbt의 경쟁자가 될 수 있을까?
in Data Engineering on SQLMesh
- 이 글은 SQLMesh 사용법에 대해 작성한 글입니다
- 예상 독자
- SQLMesh가 궁금하신 분
- SQLMesh가 dbt 대비 어떤 것이 다른지 궁금하신 분
- 이 글을 보시기 전에 dbt, DuckDB에 대해 알고 계시면 좋을 수 있는데, 과거에 작성한 글을 추천
- 키워드 : SQLMesh 사용법, SQLMesh vs dbt
SQLMesh란?
- SQLMesh GitHub
- Tobiko의 오픈소스
- SQLMesh는 Transformation, Testing, Collaboration을 위한 DataOps 솔루션
- 데이터 Transformation 영역에서 dbt가 제일 유명한데, dbt에서 개선하면 좋은 점들을 개선해서 나오는 도구
- SQLMesh 공식 문서에 보면 dbt와 다른 점에서도 상세히 공유하고 있음
- Terraform에서 영감을 받은 부분이 있다고 함. 그래서 plan 명령어가 있음
- 2025년 3월 15일 기준 v0.165.0
SQLMesh의 장점
- SQLMesh의 Concept은 dbt와 유사하며, dbt와 차이점으로 나오는 장점들을 정리
1) 간단한 SQL로 모델 생성 가능
- 데이터 분석가 직무에 계신 분들이 처음 dbt를 사용하면 Jinja, YAML에 어려움을 겪는데, SQLMesh는 간단한 SQL로 정의할 수 있음
2) 증분 로직 처리
- dbt에선 증분 로직을
is_incremental
등을 사용해서 만들어야 하는데, SQLMesh는 증분 로직을 쉽게 만들어줌 - dbt의 증분 로직
- 처음에 dbt를 학습할 때 제일 어려운 부분
- dbt는 state를 추적하지 않아서 이렇게 구현해야 함
- 날짜 경계를 찾기 위해 수동으로 매크로를 지정하는 작업에서 오류가 나기 쉬움
-- dbt incremental SELECT * FROM {{ ref(raw.events) }} e JOIN {{ ref(raw.event_dims) }} d ON e.id = d.id -- must specify the is_incremental flag because this predicate will fail if the model has never run before {% if is_incremental() %} -- this filter dynamically scans the current model to find the date boundary AND d.ds >= (SELECT MAX(ds) FROM {{ this }}) {% endif %} {% if is_incremental() %} WHERE e.ds >= (SELECT MAX(ds) FROM {{ this }}) {% endif %}
- SQLMesh는 날짜 범위를 알아서 추적해 간단하게 쓸 수 있음
-- SQLMesh incremental
SELECT *
FROM raw.events e
JOIN raw.event_dims d
-- date ranges are handled automatically by SQLMesh
ON e.id = d.id AND d.ds BETWEEN @start_ds AND @end_ds
WHERE d.ds BETWEEN @start_ds AND @end_ds
3) Data Gap 감지
- dbt에서 증분 모델을 구현할 때는 MAX(date)로 최근 데이터를 확인하고, 그 이후부터 적재하도록 함
- 이렇게 할 경우 과거에 누락된 데이터를 놓치게 되며, 데이터 파이프라인 관점에서 유지보수를 더 많이 해야 함
SQLMesh는 모델이 실행된 날짜 간격을 저장해서 누락된 날짜를 정확히 알 수 있음
Expected dates: 2022-01-01, 2022-01-02, 2022-01-03 Missing past data: ?, 2022-01-02, 2022-01-03 Data gap: 2022-01-01, ?, 2022-01-03
4) 환경 격리
- 데이터 파이프라인도 Prod/Dev 환경을 격리해야 할 수 있음. 규모가 커질수록 환경의 필요성이 생기지만, 환경을 구축하는 것은 고민이 많이 됨
- 환경을 구축하기 위해선 실제 데이터를 Dev에 그대로 제공해야 하는데, 이는 데이터 저장 비용이 드는 일이라 고민이 됨(데이터 저장 비용이 2배가 되는 상황)
- dbt는 환경마다 별도의 테이블에 데이터를 물리적으로 복제함
- SQLMesh는 VIEW를 사용해서 데이터를 물리적으로 저장하지 않아 비용이 들지 않음
- 환경마다 테이블 이름에 접두사를 붙여서 환경을 구분함
5) 스키마 변경 자동화
- 스키마가 변경되면 자동으로 감지하고 필요한 마이그레이션을 생성함
- dbt는 개발자가 직접 스키마 변경을 관리해야 함
6) 오픈소스 UI 제공
- dbt는 Paid UI가 제공되며, 오픈소스 중에 찾으려면 lightdash를 사용해야 함
- SQLMesh는 오픈소스 UI가 존재해서 쉽게 사용할 수 있음(그러나 디자인은 약간 아쉬움)
SQLMesh 실습
작업 흐름
- 파이썬 가상 환경 / 라이브러리 설치
- 파이썬 가상환경 설정
- SQLMesh 설치
- 프로젝트 생성
- Prod 환경 설정
- Model 수정
- Dev에서 작업
- Prod로 통합
파이썬 가상 환경 / 라이브러리 설치
- 가상 환경 설정
python -m venv .venv
source .venv/bin/activate
- SQLMesh 설치
- Web UI도 사용할 예정이라, 같이 설치
pip3 install "sqlmesh[web]"
- Duckdb 설치 : Local에서 Duckdb를 사용할 예정
pip3 install duckdb
프로젝트 생성
- 폴더 생성(sqlmesh-example)
mkdir sqlmesh-example
cd sqlmesh-example
DuckDB 환경에서 init
sqlmesh init duckdb
- 다음과 같은 폴더, 파일들이 생성됨
├── audits
│ └── assert_positive_order_ids.sql
├── config.yaml
├── macros
│ └── __init__.py
├── models
│ ├── full_model.sql
│ ├── incremental_model.sql
│ └── seed_model.sql
├── seeds
│ └── seed_data.csv
└── tests
└── test_full_model.yaml
6 directories, 8 files
config.yaml
- 프로젝트 디렉토리에 생성됨
- 어떤 엔진을 사용할지, 게이트웨이 커넥션 등이 저장됨
gateways:
duckdb:
connection:
# For more information on configuring the connection to your execution engine, visit:
# https://sqlmesh.readthedocs.io/en/stable/reference/configuration/#connections
# https://sqlmesh.readthedocs.io/en/stable/integrations/engines/duckdb/#connection-options
type: duckdb
database: db.db
# concurrent_tasks: 1
# register_comments: True
# pre_ping: False
# pretty_sql: False
# catalogs:
# extensions:
# connector_config:
# token:
default_gateway: duckdb
model_defaults:
dialect: duckdb
start: 2025-03-14
생성된 폴더의 특징
- config.yaml : 프로젝트 설정 파일
- models : SQL/Python 모델 파일
- 현재 full_model.sql, incremental_model.sql, seed_model.sql이 저장됨
- seeds : CSV 같은 고정된 데이터셋
- audits : 모델의 출력을 검증할 때 사용
- 목적: 모델 출력의 데이터 품질을 검증
- 실행 시점: 모델 실행 후 매번 자동으로 실행. 계획(plan)을 적용할 때마다 SQLMesh는 자동으로 실행
- 기본 동작: 감사가 실패하면 sqlmesh plan을 중단해 잠재적으로 유효하지 않은 데이터가 다운스트림으로 전파되는 것을 방지
- tests : 모델의 로직의 정확성 검증
- 목적: 모델의 로직을 검증하고 예상된 출력과 일치하는지 확인
- 실행 시점: 개발 중, 또는 CI/CD 과정
- 작동 방식: 사전 정의된 입력으로 모델의 로직을 평가하고 출력을 테스트에서 제공된 예상 결과와 비교
- 소프트웨어 개발의 단위 테스트(unit testing)와 유사
- macros : 매크로 저장. SQLMesh 매크로와 Jinja 매크로로 나뉨
Model 생성
- Model Doc
- 위에 init하면서 생긴 Model 살펴보기
Full Model
- 최상단에 MODEL 명령어를 사용해 모델을 정의
- name은 프로젝트 이름에서 Unique해야 함
- kind : 모델의 종류를 의미
- Model Kind properties에서 확인할 수 있음
- VIEW, FULL, Incremental Model이 존재
- Incremental Model은 INCREMENTAL_BY_TIME_RANGE, INCREMENTAL_BY_UNIQUE_KEY, INCREMENTAL_BY_PARTITION로 나뉨
- cron : 모델이 데이터를 처리하거나 새로고침을 할 때 사용할 일정. 자체 스케쥴러를 쓸 수도 있고, Airflow를 사용할 수도 있음
- grain : 모델이 반환한 결과에서 행을 고유하게 식별할 수 있는 컬럼, 여러 컬럼의 조합도 가능함
- audits : 모델이 평가된 후 실행될 오딧(여기선 audits/assert_positive_order_ids.sql 파일을 실행한다는 의미)
- 그 후 모델의 쿼리
- 만약 컬럼의 데이터를 형변환하고 싶다면 다음과 같이 정의하면 됨
item_id::TEXT
- 만약 컬럼의 데이터를 형변환하고 싶다면 다음과 같이 정의하면 됨
MODEL (
name sqlmesh_example.full_model,
kind FULL,
cron '@daily',
grain item_id,
audits (assert_positive_order_ids),
);
SELECT
item_id,
COUNT(DISTINCT id) AS num_orders,
FROM
sqlmesh_example.incremental_model
GROUP BY item_id
- 비교를 위해 dbt 모델 파일도 추가. 모델 정의 자체는 비슷하다고 볼 수 있지만, 명시적으로 MODEL을 정의하는 점이 차이
-- 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
Incremental Model
- INCREMENTAL_BY_TIME_RANGE을 사용했으며, 시간 컬럼을 event_date로 지정
- grain을 id, event_date로 설정함
- SELECT 쿼리 하단을 보면 WHERE에 event_date BETWEEN @start_date AND @end_date가 있는데, 이렇게 증분 처리가 끝남
MODEL (
name sqlmesh_example.incremental_model,
kind INCREMENTAL_BY_TIME_RANGE (
time_column event_date
),
start '2020-01-01',
cron '@daily',
grain (id, event_date)
);
SELECT
id,
item_id,
event_date,
FROM
sqlmesh_example.seed_model
WHERE
event_date BETWEEN @start_date AND @end_date
Seed Model
- Seed Model Doc
- Seed Model을 불러오고 싶은 경우 사용
- 컬럼의 이름과 타입을 지정할 수 있음
MODEL (
name sqlmesh_example.seed_model,
kind SEED (
path '../seeds/seed_data.csv'
),
columns (
id INTEGER,
item_id INTEGER,
event_date DATE
),
grain (id, event_date)
);
Audit
- Audit Document
- assert_positive_order_ids.sql
- 최상단에 AUDIT을 명시하고, 이름 지정
- FROM 절에 @this_model을 사용하는데, 이는 SQLMesh의 빌트인 매크로임. Runtime Variable에서 확인할 수 있음
AUDIT (
name assert_positive_order_ids,
);
SELECT *
FROM @this_model
WHERE
item_id < 0
- 파라미터를 주입할 수도 있음. 특정 값이 초과되지 않는 경우를 확인하고 싶은 Audit
AUDIT (
name does_not_exceed_threshold
);
SELECT * FROM @this_model
WHERE @column >= @threshold;
- 위 Audit을 실행하려면 MODEL에서 아래와 같이 정의해야 함
MODEL (
name sushi.items,
audits (
does_not_exceed_threshold(column := id, threshold := 1000),
does_not_exceed_threshold(column := price, threshold := 100)
)
);
SQLMesh Plan
- SQLMesh에선 환경에 Plan을 만들고 적용하는 것이 핵심
- 모델의 새 버전을 생성하고 저장함
- 모델을 평가하고 실행
- Plan의 Target 환경에 업데이트
sqlmesh plan
- Backfill Tables에 y를 입력하면 Backfill 작업을 수행함
- 폴더를 보면,
db.db
와 logs가 추가된 것을 확인할 수 있음
.
├── audits
│ └── assert_positive_order_ids.sql
├── config.yaml
├── db.db
├── logs
│ ├── sqlmesh_2025_03_15_14_04_40.log
│ └── sqlmesh_2025_03_15_14_31_00.log
├── macros
│ ├── __init__.py
│ └── __pycache__
│ └── __init__.cpython-310.pyc
├── models
│ ├── full_model.sql
│ ├── incremental_model.sql
│ └── seed_model.sql
├── seeds
│ └── seed_data.csv
└── tests
└── test_full_model.yaml
8 directories, 12 files
DuckDB에서 데이터 확인
- CLI에서 아래 명령어 실행
duckdb db.db
- DuckDB를 실행한 후,
.tables
를 실행하면 테이블을 확인할 수 있음- full_model, incremental_model, seed_model : 위 MODEL에서 생성한 결과. 사용자가 사용할 땐 이걸 사용
sqlmesh_example__full_model__2278521865
: 이 형태도 물리적 테이블이나, 버전 관리를 위해 사용하는 내부적인 테이블
D .tables
_auto_restatements
_environment_statements
_environments
_intervals
_plan_dags
_snapshots
_versions
full_model
incremental_model
seed_model
sqlmesh_example__full_model__2278521865
sqlmesh_example__incremental_model__1880815781
sqlmesh_example__seed_model__2185867172
- 아래 쿼리를 실행하면 데이터를 확인할 수 있음
- sqlmesh_example는 모델의 스키마로 위에 모델 생성할 때 사용함
SELECT * FROM sqlmesh_example.full_model;
모델 수정 및 Dev에서 작업하기
모델 수정
- 이제 Prod 환경을 채웠으니, SQL 모델 하나를 수정
models/incremental_model.sql
에'z' AS new_column
을 추가함
MODEL (
name sqlmesh_example.incremental_model,
kind INCREMENTAL_BY_TIME_RANGE (
time_column event_date
),
start '2020-01-01',
cron '@daily',
grain (id, event_date)
);
SELECT
id,
item_id,
'z' AS new_column, -- Added column
event_date,
FROM
sqlmesh_example.seed_model
WHERE
event_date BETWEEN @start_date AND @end_date
Dev 환경 생성 및 Backfill
dev 환경을 생성
sqlmesh plan dev
생성하면 변경된 모델이 incremental_model이라고 나오며, 변경된 부분이 나옴
- backfill을 해주면, dev 환경에 테이블이 생성됨
데이터 확인
- 위에서 했던 것처럼
duckdb db.db
로 DuckDB에 접근한 후,.tables
로 테이블 확인
D .tables
_auto_restatements
_environment_statements
_environments
_intervals
_plan_dags
_snapshots
_versions
full_model
full_model
incremental_model
incremental_model
seed_model
sqlmesh_example__full_model__2278521865
sqlmesh_example__full_model__2278521865__dev # 새로 추가됨
sqlmesh_example__incremental_model__1880815781
sqlmesh_example__incremental_model__772980299 # 새로 추가됨
sqlmesh_example__seed_model__2185867172
- full_model은 변경된 것이 없어서 기존 모델에 __dev 추가(새 환경)
- 반면 incremental_model은 변경된 것이 있어서 해시가 변경됨
- 새롭게 추가된 incremental_model 데이터 확인
select * from sqlmesh_example__dev.incremental_model;
Dev에서 작업한 내용 Prod에 적용
- sqlmesh plan 명령어를 사용해서 Dev -> Prod로 적용
- DuckDB에 접속하지 않고, sqlmesh 명령어로 데이터를 가지고 올 수도 있음
sqlmesh fetchdf "select * from sqlmesh_example.incremental_model"
SQLMesh Web UI
- 아래 명령어를 통해 Web UI를 실행
sqlmesh ui
- 위에서 작업한 incremental_model.sql을 클릭하니 리니지를 보여줌
- 데이터를 쿼리해서 확인할 수도 있음
- Models 폴더를 클릭하면 실행된 모델과 모델의 메타데이터, 리니지를 볼 수 있음
SQLMesh 사용 후기
- SQLMesh를 사용해보면 dbt보다 러닝커브가 낮음
- 그러나 지금 SQLMesh롤 바로 도입해야 할까? 하면 고민되는 부분이 있음
- 생태계가 아직 dbt에 비해 작고, 1점대도 아니라 계속 변경될 것으로 예상
- Airflow와 Prefect의 관계를 보는 것 같기도 함
- 그럼에도 불구하고 T 영역에서 거의 독점했던 dbt의 경쟁자가 있다는 점에서 SQLMesh를 응원하고 싶음. 사용성은 괜찮았음
- dbt가 SDF labs를 인수한 후, 여러 통합 작업을 진행할 것으로 예상되는데 그 과정에서 SQLMesh가 제시하는 SQLMesh의 장점을 무력화할 수 있을 것 같음
- 레딧에서 논의되는 내용을 보면 도움이 됨
레퍼런스 및 읽으면 좋은 자료
- SQL 홈페이지
- SQLMesh Document
- Sung Won Chung님의 링크드인 : Tobiko의 Solution Architect로 근무하고 계신데 튜토리얼 내용을 링크드인 글로 공유해주심
- Tobiko 블로그
- dbt와 SQLMesh의 기능 비교 : 각 라이브러리가 제공하는 범위를 비교해줌
- FAQ 문서도 보는 것을 추천함. dbt와 SQLMesh의 용어가 살짝 다름
- dbt materialization : SQLMesh의 model kinds에 정의
- dbt seeds : SQLMesh model kind의 seed
- dbt test : SQLMesh audits
- dbt build : sqlmesh run
- SQLMesh + GitHub Action 통합
- SQLMesh를 사용한 데이터 파이프라인
- 글 작성하는데 걸린 시간 : 3시간 30분
- 하고자 하는 이야기, 개요 정리 : 13분
- 초안 글 작성 : 3시간 7분
- 클로드와 셀프 글 피드백 : 10분
카일스쿨 유튜브 채널을 만들었습니다. 데이터 사이언스, 성장, 리더십, BigQuery 등을 이야기할 예정이니, 관심 있으시면 구독 부탁드립니다 :)
PM을 위한 데이터 리터러시 강의를 만들었습니다. 문제 정의, 지표, 실험 설계, 문화 만들기, 로그 설계, 회고 등을 담은 강의입니다
이 글이 도움이 되셨거나 다양한 의견이 있다면 댓글 부탁드립니다 :)