SQLMesh 사용법 : dbt의 경쟁자가 될 수 있을까?


  • 이 글은 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 폴더를 클릭하면 실행된 모델과 모델의 메타데이터, 리니지를 볼 수 있음

  • UI에서도 plan을 실행하고 합칠 수 있음
  • 그 외에 기능은 아직 확인이 어려움. Oauth 등


SQLMesh 사용 후기

  • SQLMesh를 사용해보면 dbt보다 러닝커브가 낮음
  • 그러나 지금 SQLMesh롤 바로 도입해야 할까? 하면 고민되는 부분이 있음
    • 생태계가 아직 dbt에 비해 작고, 1점대도 아니라 계속 변경될 것으로 예상
    • Airflow와 Prefect의 관계를 보는 것 같기도 함
  • 그럼에도 불구하고 T 영역에서 거의 독점했던 dbt의 경쟁자가 있다는 점에서 SQLMesh를 응원하고 싶음. 사용성은 괜찮았음
  • dbt가 SDF labs를 인수한 후, 여러 통합 작업을 진행할 것으로 예상되는데 그 과정에서 SQLMesh가 제시하는 SQLMesh의 장점을 무력화할 수 있을 것 같음
  • 레딧에서 논의되는 내용을 보면 도움이 됨



레퍼런스 및 읽으면 좋은 자료



  • 글 작성하는데 걸린 시간 : 3시간 30분
    • 하고자 하는 이야기, 개요 정리 : 13분
    • 초안 글 작성 : 3시간 7분
    • 클로드와 셀프 글 피드백 : 10분

카일스쿨 유튜브 채널을 만들었습니다. 데이터 사이언스, 성장, 리더십, BigQuery 등을 이야기할 예정이니, 관심 있으시면 구독 부탁드립니다 :)

PM을 위한 데이터 리터러시 강의를 만들었습니다. 문제 정의, 지표, 실험 설계, 문화 만들기, 로그 설계, 회고 등을 담은 강의입니다

이 글이 도움이 되셨거나 다양한 의견이 있다면 댓글 부탁드립니다 :)

Buy me a coffeeBuy me a coffee





© 2017. by Seongyun Byeon

Powered by zzsza