最終更新:2026年8月23日
SQLMesh は、Python と SQL のハイブリッド構文で書けるオープンソースのデータ変換フレームワークで、仮想データ環境 (Virtual Data Environments)・列レベル系譜・自動バックフィル・監査 (Audits) を標準搭載し、dbt の弱点だった「環境のコピーが遅い」「バックフィルを毎回手で書く」「SQL 方言差でテストがすり抜ける」といった運用上の痛点を根本から解決します。私が担当している深夜バッチも、dbt から SQLMesh に載せ替えてから backfill の手順書を書かなくなりました。本ガイドでは、SQLMesh 0.140+ を DuckDB でローカルに動かしながら、モデル定義・Python モデル・監査・CI 連携まで、本番運用に必要な要素を最短で押さえます。
SQLMesh とは何か
SQLMesh は Tobiko Data が中心となって開発している、Python と SQL のハイブリッドで書くデータ変換フレームワークです。dbt と同じく「SELECT 文でモデルを書き、依存関係を DAG として解決してテーブルを構築する」というコアコンセプトは共有していますが、SQLMesh は最初から データウェアハウス運用の痛点を解決する目的で設計されており、SQLGlot による SQL パースを土台に、コンパイル時に方言差を吸収し、列レベルの系譜を静的に構築します。私は 2024 年から本番で使っていますが、いちばんの価値は「危険な変更を sqlmesh plan が事前に警告してくれる」点にあります。破壊的変更・後方互換な変更・データ変更の 3 種類を SQLMesh が自動判定し、必要ならバックフィル計画まで提示してくれるため、Slack で「これ流していいですか?」と確認する回数が体感で 8 割減りました。
2026 年 8 月時点の最新安定版は 0.140.x 系で、Python 3.9 以上をサポートします。CLI・Python API・Web UI (browser IDE) の 3 種類のフロントを持ち、後述するように DuckDB を組み合わせればローカルで完結する開発サイクルを組めるのが特徴です。TobikoData/sqlmesh の GitHub リリースノートを追うと、Snowflake の dynamic table・BigQuery の materialized view・Iceberg テーブルのサポート追加など、モダンな OLAP スタックへの追従が速いことが分かります。
SQLMesh と dbt の違い
「dbt でよくない?」という質問には、機能表で答えるのが早いです。以下は 2026 年 8 月時点の dbt Core 1.9 系と SQLMesh 0.140 系を、データエンジニアが実運用で気にする軸で比較したものです。
| 観点 | dbt Core 1.9 | SQLMesh 0.140 |
|---|---|---|
| ライセンス | Apache 2.0 | Apache 2.0 |
| 環境分離の方式 | スキーマ複製 (物理コピー) | 仮想データ環境 (ビュー張り替え) |
| バックフィル | 手動 --full-refresh / マクロで自作 | 時間範囲を自動検出・冪等実行 |
| SQL パース | Jinja テンプレート展開のみ | SQLGlot による完全 AST 解析 |
| 列レベル系譜 | dbt Cloud のみ (有償) | OSS で標準搭載 |
| Python モデル | Snowpark / PySpark / Databricks のみ | DuckDB・Postgres 含む全エンジン |
| 単体テスト | YAML でのアサーションのみ | 入出力の完全比較テスト (tests/) が可能 |
| 変更影響の分類 | なし | Breaking / Non-breaking / Metadata を自動判定 |
実運用でいちばん効くのは 環境分離の方式とバックフィルの自動化です。dbt でステージング環境を作ろうとすると、対象スキーマ全体を CREATE TABLE ... AS SELECT でコピーする必要があり、TB 級のテーブルがあると数十分〜数時間かかります。SQLMesh は「本番テーブルはそのまま、開発版はビューで差分だけ指す」方式なので、ブランチを切り替えても数秒で環境が立ち上がります。dbt を長く使ってきた人ほど、この差の大きさに驚くはずです。
インストールと初期化:DuckDB で 3 分で動かす
SQLMesh は pip install sqlmesh だけで入ります。追加エンジンは extras で指定します。ローカル検証には DuckDB がベストで、私はチュートリアル講師をするときも必ず DuckDB から始めるようにしています。既に DuckDB × Python の実践ガイドで紹介したように、DuckDB はローカルで OLAP クエリを爆速で回せるので、SQLMesh の学習環境として理想的です。
# Python 3.9+ が必須。仮想環境を切ることを強く推奨。
python -m venv .venv
source .venv/bin/activate
# DuckDB extras と Web UI (browser IDE) をまとめて入れる
pip install "sqlmesh[duckdb,web]==0.140.*"
# サンプルプロジェクトを生成 (dialect は運用先に合わせる)
mkdir sales_warehouse && cd sales_warehouse
sqlmesh init duckdb
# 生成物を確認
ls -la
# config.yaml models/ audits/ tests/ seeds/ macros/
config.yaml にはゲートウェイ (接続先) とデフォルトエンジンが書かれます。DuckDB の場合はローカルファイル db.db が自動で作られ、以降のモデル実行はすべてそのファイルに書き込まれます。CI 用に test ゲートウェイをインメモリ DuckDB で追加しておくと、テストが数秒で終わるので便利です。
# config.yaml の例
gateways:
local:
connection:
type: duckdb
database: db.db
test:
connection:
type: duckdb # in-memory (database 未指定)
default_gateway: local
model_defaults:
dialect: duckdb
start: '2026-01-01' # インクリメンタルモデルの開始日
初期化直後に sqlmesh plan を叩くと、まだ何もモデルが無いにも関わらず「prod 環境を作成しますか?」と聞いてきます。この時点で SQLMesh の思想が体感できます — すべての変更は plan を通すということです。
SQL モデルとインクリメンタル戦略
モデルは models/ 配下に .sql ファイルとして置きます。冒頭に MODEL (...) ブロックを書くのが dbt との最大の違いで、ここに種別 (kind)・粒度・監査などをすべて宣言します。以下は日次で積み上がる売上ファクトテーブルの例です。
-- models/marts/fct_daily_sales.sql
MODEL (
name marts.fct_daily_sales,
kind INCREMENTAL_BY_TIME_RANGE (
time_column order_date,
batch_size 30 -- 30 日ずつバッチ実行
),
cron '@daily',
grain (order_date, store_id),
audits (
not_null(columns := (order_date, store_id, gross_amount)),
unique_values(columns := (order_date, store_id, order_id))
)
);
SELECT
o.order_date::DATE AS order_date,
o.store_id,
o.order_id,
SUM(li.qty * li.unit_price) AS gross_amount,
COUNT(*) AS line_count
FROM raw.orders AS o
JOIN raw.order_lines AS li USING (order_id)
WHERE o.order_date BETWEEN @start_ds AND @end_ds -- SQLMesh が自動注入
GROUP BY 1, 2, 3
ポイントは 3 つです。1 つ目は INCREMENTAL_BY_TIME_RANGE という kind の宣言。これを付けると SQLMesh は「このモデルは order_date 列で時間分割される」と理解し、後述する自動バックフィルの対象になります。2 つ目は @start_ds / @end_ds というマクロ変数。SQLMesh がバッチごとにこの範囲を注入するので、私たちは「1 日ぶんの SELECT」だけを書けば冪等になります。3 つ目は audits ブロック — 監査を SQL 内に閉じて書けるので、YAML と SQL を行き来する dbt の煩わしさがありません。
他にも FULL, VIEW, INCREMENTAL_BY_UNIQUE_KEY, SCD_TYPE_2, SEED などの kind があります。特に SCD Type 2 が組み込みで用意されているのは強力で、変更履歴を持つディメンションを YAML 20 行程度で宣言できます。SQLMesh 公式ドキュメントの Model kinds ページに一覧があるので、実装前に眺めておくと選定が楽になります。
Python モデルで pandas / Polars を返す
SQL では書きづらい特徴量生成や外部 API 呼び出しは、Python モデルで書けます。dbt でも Python モデルはありますが、対応エンジンが Snowpark / Databricks / PySpark に限定されるのに対し、SQLMesh はすべてのエンジンで Python モデルが動きます。返り値として pandas / Polars / PyArrow のいずれかを返せば、SQLMesh がテーブルに書き戻します。
# models/features/customer_ltv.py
from __future__ import annotations
import typing as t
from datetime import datetime
import pandas as pd
from sqlmesh import ExecutionContext, model
@model(
"features.customer_ltv",
kind="FULL",
columns={
"customer_id": "BIGINT",
"ltv_90d": "DOUBLE",
"predicted_churn_prob": "DOUBLE",
},
audits=[("not_null", {"columns": "(customer_id)"})],
)
def execute(
context: ExecutionContext,
start: datetime,
end: datetime,
execution_time: datetime,
**kwargs: t.Any,
) -> pd.DataFrame:
# 上流テーブルを DataFrame として取得 (エンジンに依らず動く)
orders = context.fetchdf(
"SELECT customer_id, order_date, gross_amount FROM marts.fct_daily_sales"
)
# 90 日 LTV を計算
ltv = (
orders[orders["order_date"] >= (pd.Timestamp(end) - pd.Timedelta(days=90))]
.groupby("customer_id", as_index=False)["gross_amount"]
.sum()
.rename(columns={"gross_amount": "ltv_90d"})
)
# 学習済みモデルをロードして churn 確率を推論 (省略)
ltv["predicted_churn_prob"] = _predict_churn(ltv)
return ltv
def _predict_churn(df: pd.DataFrame) -> pd.Series:
# 実際は joblib.load('models/churn.pkl').predict_proba(...) など
return (1.0 / (1.0 + df["ltv_90d"] / 1000)).clip(0, 1)
Python モデルはスキーマを明示宣言する必要があるのがポイントです。SQL モデルは SQLGlot が SELECT からスキーマを推論できますが、Python の戻り値は静的解析できないため、columns を書き忘れると sqlmesh plan で警告されます。データ品質を担保するには Pandera でのスキーマ検証 と組み合わせて、返す DataFrame を検証してから return するのがおすすめです。
仮想データ環境と blue-green デプロイ
SQLMesh の看板機能が仮想データ環境です。開発者ごとに「そのブランチが指すバージョンのテーブル群」をビュー越しに束ねる仕組みで、物理コピーは一切発生しません。sqlmesh plan dev_hannah と叩けば、私専用の dev_hannah 環境が数秒で出来上がり、変更したモデルだけがバックフィルされます。上流の 500GB のファクトテーブルはビューで本番を参照するだけなので、ディスクは 1MB も食いません。
本番反映は sqlmesh plan prod です。SQLMesh は「その変更が Breaking か Non-breaking か」を SQLGlot の AST 比較で自動判定します。列の追加・別名・コメント変更は Non-breaking として無停止で反映されますが、集計ロジックの変更や列削除は Breaking と判断され、影響を受ける下流モデルのバックフィル計画が提示されます。これは事実上の blue-green デプロイで、旧バージョンのビューは janitor が回収するまで残るため、切り戻しも sqlmesh plan --restate-model 一発です。
自動バックフィルは実際どう動くのか
私が SQLMesh を推す最大の理由がこれです。INCREMENTAL_BY_TIME_RANGE で宣言したモデルは、SQLMesh が内部で「実行済み時間範囲」をメタテーブルに記録します。何らかの理由で 3 日ぶんの実行が抜けていた場合、次の sqlmesh run で自動的にその 3 日ぶんだけが冪等に流されます。dbt では --full-refresh でテーブル全体を再計算するか、独自マクロで WHERE 句を書く運用が一般的ですが、SQLMesh はこれをフレームワーク層で解決しています。
もし過去のロジックにバグがあって特定期間だけ再計算したいときは、sqlmesh plan --restate-model marts.fct_daily_sales --start 2026-06-01 --end 2026-06-15 と叩くだけです。SQLMesh は該当期間のパーティションを削除して再計算し、その下流モデルも同じ期間だけ再実行します。深夜 3 時に「昨日のバッチが壊れた」と Slack に叩き起こされる回数が激減しました。
監査 (Audits) と単体テスト
SQLMesh は「本番データに対するアサーション (Audits)」と「入出力を固定した単体テスト (Tests)」の 2 段構えでデータ品質を守ります。前者は毎回のバッチで走り、後者は CI で走ります。パイプラインテストは非交渉なので、両方入れることを強く勧めます。
# audits/not_negative.sql
AUDIT (
name not_negative_amounts,
defaults (column := 'gross_amount')
);
SELECT *
FROM @this_model
WHERE @column < 0
# tests/test_fct_daily_sales.yaml
test_fct_daily_sales:
model: marts.fct_daily_sales
inputs:
raw.orders:
- {order_id: 1, order_date: '2026-06-01', store_id: 10}
raw.order_lines:
- {order_id: 1, qty: 2, unit_price: 100.0}
outputs:
query:
- {order_date: '2026-06-01', store_id: 10, order_id: 1,
gross_amount: 200.0, line_count: 1}
sqlmesh test でテストが走り、ミリ秒で完了します。DuckDB in-memory で回るので CI のマシンリソースも食いません。私のチームでは PR に単体テストが 1 件も追加されていないと GitHub Actions で fail させる運用にしています。データ品質を後付けで足すのはコストが跳ねるので、モデル追加とテスト追加をセットにする文化を早い段階で作るのが大事です。
CI/CD 連携とプルリクエスト運用
SQLMesh は GitHub Actions と Bitbucket 用の公式ワークフローを提供しており、PR ごとに以下を自動実行できます。pandas パイプライン記事で書いたのと同じ「変更前後の diff を機械的に確認する」原則が、そのまま SQL パイプラインに適用できます。
# .github/workflows/sqlmesh.yml
name: SQLMesh CI
on:
pull_request:
branches: [main]
jobs:
sqlmesh-cicd:
runs-on: ubuntu-latest
permissions: {contents: write, pull-requests: write, statuses: write}
steps:
- uses: actions/checkout@v4
with: {fetch-depth: 0}
- uses: actions/setup-python@v5
with: {python-version: '3.11'}
- run: pip install "sqlmesh[github,duckdb]==0.140.*"
- run: sqlmesh_cicd -p . github --token ${{ secrets.GITHUB_TOKEN }} run-all
sqlmesh_cicd ... run-all は、PR ブランチ用の仮想環境を作成 → 変更モデルのバックフィル → 単体テスト → 監査 → テーブル差分レポートを PR コメントとして自動投稿までを一気通貫で実行します。レビュアーは「この変更でどの列がどのくらい変わるか」を PR ページで即座に確認できるので、レビューの精度と速度が両方上がります。
dbt からの移行パス
既存の dbt プロジェクトからの移行は、SQLMesh が公式に提供する sqlmesh init -t dbt テンプレートを使うのがいちばん楽です。これは dbt_project.yml と models/ をそのまま読み込めるアダプタで、Jinja マクロ・sources・refs はそのまま動きます。段階的移行として、まず SQLMesh で dbt プロジェクトを実行できるようにし、そこから 1 モデルずつ SQLMesh 記法に書き換える戦略が現実的です。
# 既存 dbt プロジェクトのルートで
pip install "sqlmesh[dbt,duckdb]==0.140.*"
# dbt アダプタで初期化
sqlmesh init -t dbt
# dbt モデルをそのまま plan
sqlmesh plan dev
実運用の移行では、最初にリスクの低い staging 層 (単純な source → 型合わせ) を SQLMesh 記法に書き換え、次にファクト層、最後にマート層という順で進めるのが安全です。ディメンション表は SCD Type 2 の恩恵が大きいので、移行時にまとめて置き換えることをおすすめします。SQLMesh の dbt アダプタ公式ドキュメントに対応状況の詳細が載っているので、Jinja マクロ・custom materializations を多用している場合は事前に一読してください。
よくある質問
SQLMesh は無料ですか?
SQLMesh 本体は Apache 2.0 ライセンスの完全な OSS で、CLI・Python API・Web UI (browser IDE) すべてが無料で使えます。有償なのは Tobiko Cloud という SaaS 版で、こちらはスケジューリング・監視・SSO などのエンタープライズ機能を提供します。ローカル開発と OSS デプロイだけであれば費用は一切かかりません。
SQLMesh と dbt はどちらを選ぶべきですか?
新規プロジェクトで ステージング環境の作成が遅い、バックフィルを手で書くのがつらい、列レベル系譜が欲しいのいずれかに当てはまるなら SQLMesh がおすすめです。既存資産が dbt に大きく寄っていてマクロ・パッケージ (dbt-utils, dbt-expectations) を深く使っている場合は、dbt を続けるか、SQLMesh の dbt アダプタで段階移行するのが現実的です。
SQLMesh のインクリメンタルモデルとは何ですか?
時間軸で分割されたテーブルを差分だけで更新する仕組みです。INCREMENTAL_BY_TIME_RANGE で time_column を宣言すると、SQLMesh がバッチごとに @start_ds / @end_ds を注入し、未処理範囲だけを冪等に実行します。実行済み範囲はメタテーブルに記録され、欠損期間は次回実行時に自動でバックフィルされます。
SQLMesh はどのデータベースに対応していますか?
2026 年 8 月時点で BigQuery・Snowflake・Databricks・Redshift・Postgres・MySQL・DuckDB・Trino・Athena・ClickHouse・MotherDuck・Iceberg (Glue / REST) など主要 10 種以上のエンジンに対応します。SQLGlot が方言変換を担うため、開発は DuckDB で、本番は Snowflake という「dev-prod 異エンジン運用」も可能です。
SQLMesh でバックフィルが失敗したらどうなりますか?
失敗した範囲は「未実行」としてメタテーブルに記録されるだけで、テーブルは中途半端な状態にはなりません (トランザクション対応エンジンでは 1 バッチが 1 トランザクション)。再度 sqlmesh run を叩けば失敗した範囲だけが再実行されます。手動での再実行が必要な場合は sqlmesh plan --restate-model <model> --start ... --end ... で範囲を明示できます。



