SQLMesh 的配置文件有两种:config.yaml 与 config.py。官方的说法很直接:YAML 更简单,推荐大多数项目使用;Python 更复杂,但提供了 YAML 不支持的能力------config.py 会被 Python 解释器真正执行,因此配置里可以有判断、函数、第三方 SDK,以及"当前在什么环境、由谁运行"的信息。
下面用五个实战案例说明这份"可执行"的含金量。

案例一:变更自动分类------同一份配置,CI 与本地行为不同
执行 plan 时,SQLMesh 要判断模型变更是 breaking 还是 non-breaking。自动分类有三种模式:full(全部自动,无法判断时保守归为 breaking)、semi(无法判断时提示用户)、off(关闭),并可按模型来源分别设置(sql / python / seed / external)。
YAML 只能写静态值:
yaml
auto_categorize_changes:
sql: full
python: semi
Python 则能让策略随运行环境变化:
python
from sqlmesh import is_cicd_environment # 识别 CI / GitHub Actions / GitLab CI
from sqlmesh.core.config import Config, ModelDefaultsConfig, CategorizerConfig
config = Config(
model_defaults=ModelDefaultsConfig(dialect="duckdb"),
# CI 里不能等人确认;本地开发反而希望人工把关
auto_categorize_changes=(
CategorizerConfig.all_full() if is_cicd_environment()
else CategorizerConfig.all_semi()
),
)
收益:一个文件覆盖两条流水线,也不必担心有人在 CI 里被交互提示卡住。
案例二:密钥不进仓库,连接按环境自动组装
密码不能进 Git;dev 与 prod 要用不同的库、角色;单元测试绝不能碰生产数据。在 YAML 里,这通常意味着复制多份配置,或者把密文塞进环境变量。
python
import os
from sqlmesh.core.config import (
Config, ModelDefaultsConfig, GatewayConfig,
SnowflakeConnectionConfig, DuckDBConnectionConfig,
)
from my_org.secrets import get # 例:boto3 / Vault / Azure Key Vault
env = os.environ.get("SQLMESH_ENV", "dev") # dev | prod
db = {"dev": "DEV_ANALYTICS", "prod": "ANALYTICS"}[env]
config = Config(
model_defaults=ModelDefaultsConfig(dialect="snowflake"),
gateways={
env: GatewayConfig(
connection=SnowflakeConnectionConfig(
account="acme",
user=f"svc_{env}",
password=get(f"sqlmesh/{env}/snowflake"), # 明文不进仓库
database=db,
warehouse=f"WH_{env.upper()}",
),
test_connection=DuckDBConnectionConfig(), # 单测只跑内存 DuckDB
),
},
default_gateway=env,
)
收益:
- 密钥只在运行时出现在内存里,仓库与 CI 日志中没有明文;
- 一条
SQLMESH_ENV=prod就切换整套环境,无需第二份配置文件; test_connection把单测隔离在内存 DuckDB 上,"测试污染生产"从流程约定变成物理隔离;- 还需要个人开发库时,再加一个
GatewayConfig,用--gateway指定即可。
案例三:before_all / after_all------把权限治理挂到 plan / run 生命周期
模型跑完通常要授权:视图要给分析师角色 SELECT,生产环境还要给 schema 的 USAGE。逐个模型写 on_virtual_update 会散落各处,也感知不到"当前是不是生产"。
before_all / after_all 会在 sqlmesh plan 与 sqlmesh run 的开始与结束执行 SQL 语句或 SQLMesh 宏,这两个位置还能取到 @this_env、@schemas、@views。把 Python 宏与 Python 配置放在一起,权限规则就有了唯一出处:
python
from sqlmesh.core.macros import macro
from sqlmesh.core.config import Config, ModelDefaultsConfig
@macro()
def grant_select(evaluator):
return [f"GRANT SELECT ON VIEW {v} TO ROLE analyst;" for v in evaluator.views]
@macro()
def grant_schema_usage(evaluator):
if evaluator.this_env != "prod": # 只在生产授权
return []
return [f"GRANT USAGE ON SCHEMA {s} TO ROLE analyst;" for s in evaluator.schemas]
config = Config(
model_defaults=ModelDefaultsConfig(dialect="duckdb"),
after_all=["@grant_select()", "@grant_schema_usage()"],
)
收益:授权只写一次、随每次 plan / run 自动生效,并自带环境判断;宏就是普通 Python 函数,可以单测,也能 import 你自己的权限矩阵。
案例四:一人一环境------默认目标环境按使用者生成
多人共用一个项目时,每个人都要手敲 sqlmesh plan dev_tony;稍不留神漏了环境名,plan 就落在 prod 上。开发环境还会不断堆积,没人清理。
python
import os
import getpass
from sqlmesh.core.config import (
Config, ModelDefaultsConfig, GatewayConfig, DuckDBConnectionConfig,
)
raw = os.environ.get("GITHUB_ACTOR") or getpass.getuser()
owner = raw.split("@")[0].lower().replace(".", "_") # 用户名需要先规范化
config = Config(
model_defaults=ModelDefaultsConfig(dialect="duckdb"),
gateways={"local": GatewayConfig(connection=DuckDBConnectionConfig())},
default_target_environment=f"dev_{owner}", # sqlmesh plan == sqlmesh plan dev_tony
environment_ttl="in 3 days", # 开发环境自动过期回收
pinned_environments=["prod"], # prod 不参与清理
)
收益:少打一个参数,也顺手消除了"误指 prod"这一整类事故------想动生产必须显式写出环境名;一人一环境加 TTL,让环境自动回收。
公平地说,YAML 里的 default_target_environment: dev_{``{ user() }} 也能覆盖一半场景。Python 的增量价值在于"计算":名字可能来自 CI 变量或工号,还得去掉域名、把 . 换成 _(否则不是合法标识符),甚至按角色决定命名空间与保留策略。
案例五:一个项目、多个引擎与区域------网关级默认值和变量
同一批模型,本地想用 DuckDB 快速全链路自测,生产跑在 Snowflake 与 Redshift 上;不同引擎的标识符大小写规则不同;多区域部署还要换参数。用 YAML,通常要为每个引擎复制一遍项目设置。
python
from sqlmesh.core.config import (
Config, ModelDefaultsConfig, GatewayConfig,
DuckDBConnectionConfig, SnowflakeConnectionConfig, RedshiftConnectionConfig,
)
config = Config(
# 全局默认:模型按 Snowflake 方言书写
model_defaults=ModelDefaultsConfig(dialect="snowflake"),
variables={"region": "us"},
gateways={
"local": GatewayConfig( # 本地换引擎,全链路自测
connection=DuckDBConnectionConfig(),
model_defaults=ModelDefaultsConfig(dialect="duckdb"),
),
"prod": GatewayConfig( # 模型仍是 Snowflake 方言
connection=RedshiftConnectionConfig(host="...", user="...", password="..."),
# 但按 Redshift 大小写不敏感的规则处理标识符
model_defaults=ModelDefaultsConfig(
dialect="snowflake,normalization_strategy=case_insensitive"),
),
"eu": GatewayConfig( # 同一套模型,换区域参数
connection=SnowflakeConnectionConfig(account="acme", user="svc", password="..."),
variables={"region": "eu"},
),
},
default_gateway="local",
)
收益:模型只写一份,方言与标识符规范化策略交给网关决定(gateway-specific model_defaults 的用途);变量按网关覆盖,多区域、多租户不必 fork 项目(模型里用 @region 或 context.var("region") 读取);默认网关指向本地 DuckDB,只有显式 --gateway prod 才连生产。
五个案例的共同点
- 配置是对象,不是文本 :
Config、GatewayConfig、CategorizerConfig、ModelDefaultsConfig在加载时校验,参数或枚举写错立刻报错,IDE 还能补全;YAML 把semi敲成seml,常要等到 plan 才暴露。 - 逻辑可复用:工厂方法、函数与常量让"环境矩阵""网关矩阵"只写一次,而不是复制多份 YAML 再逐份改。
- 随环境与使用者变化 :
os.environ、getpass.getuser()、SQLMesh 自带的is_cicd_environment()都能参与决策,配置因此可以随人、随时、随流水线而变化。 - 能与外部系统对话:密钥管理器、CMDB、权限系统都能在"生成配置"这一步接入,而不是等运行时报错。
- 一个文件多套配置 :用
sqlmesh --config my_second_config plan在 dev / prod / CI 之间切换,多仓库项目还能用project区分。
什么时候仍然该用 YAML
纯声明、单环境、没有动态逻辑的项目,YAML 更短更直观,也支持 {``{ env_var('...') }}、{``{ user() }} 与环境变量覆盖(SQLMESH__ 开头的变量按层级拼接,优先级最高)。务实的做法是默认 YAML,只在需要动态行为时引入 config.py;两者也可以并存,用 --config 选择。
风险也要承认:Python 配置是可执行代码,应像代码一样 review、避免副作用(别在 import 时写库或发网络请求),并保证运行 SQLMesh 的环境依赖齐全。
结语
YAML 描述"配置是什么",Python 描述"配置如何产生"。变更分类的按需自动、密钥与连接的环境化组装、权限治理的生命周期挂载、一人一环境的命名与回收、多引擎多区域的矩阵展开------五个案例背后是同一件事:当项目长到需要多环境、多网关、CI/CD 与密钥治理时,Python 配置把配置从静态清单升级为可复用、可测试、可演进的策略层。
参考: