SQLMesh Python 模型入门(三):前后置语句、蓝图建模与避坑指南

本系列基于 SQLMesh 官方文档(https://sqlmesh.readthedocs.io/en/stable/concepts/models/python_models/)整理,共 3 篇,面向初学者。本篇是完结篇,覆盖工程化进阶能力,并给出全系列的避坑总表。

  • 第(一)篇:基础语法与核心概念
  • 第(二)篇:取数与依赖管理、四种引擎的 DataFrame 实战

1. 前后置语句(pre/post-statements)

前置/后置语句让你在模型运行前后执行 SQL。典型用途:修改会话设置、创建索引。

⚠️ 并发提醒:不要写会与其他并发模型冲突的语句(例如创建物理表),并发执行时行为不可预测。

1.1 在装饰器里声明

pre_statements / post_statements 接收一个列表,元素可以是 SQL 字符串、SQLGlot 表达式或宏调用:

python 复制代码
@model(
    "db.test_model",
    kind="full",
    columns={
        "id": "int",
        "name": "text",
    },
    pre_statements=[
        "SET GLOBAL parameter = 'value';",
        exp.Cache(this=exp.table_("x"), expression=exp.select("1")),
    ],
    post_statements=["@CREATE_INDEX(@this_model, id)"],
)
def execute(context, start, end, execution_time, **kwargs) -> pd.DataFrame:
    return pd.DataFrame([{"id": 1, "name": "name"}])

其中 @CREATE_INDEX 是自定义宏,在项目的 macros 目录里这样定义------仅在 creating(建表)阶段执行:

python 复制代码
@macro()
def create_index(
    evaluator: MacroEvaluator,
    model_name: str,
    column: str,
):
    if evaluator.runtime_stage == "creating":
        return f"CREATE INDEX idx ON {model_name}({column});"
    return None

项目级默认值 :也可以在配置的 model_defaults 里为整个项目定义 pre/post 语句,所有模型自动继承,并与模型级语句合并(默认语句先执行)。

1.2 在函数体内声明

规则很简单:

  • 前置语句 :写在 return / yield 之前任意位置即可;
  • 后置语句 :必须把 return 改成 yield,然后写在 yield 之后(因为后置语句要在函数产出数据之后才执行)。
python 复制代码
def execute(
    context: ExecutionContext,
    start: datetime,
    end: datetime,
    execution_time: datetime,
    **kwargs: t.Any,
) -> pd.DataFrame:
    # pre-statement
    context.engine_adapter.execute("SET GLOBAL parameter = 'value';")

    # post-statement 要求用 yield 而不是 return
    yield pd.DataFrame([
        {"id": 1, "name": "name"}
    ])

    # post-statement
    context.engine_adapter.execute("CREATE INDEX idx ON example.pre_post_statements (id);")

2. on_virtual_update:虚拟层更新后执行

on_virtual_update 在 Virtual Update 完成后执行 SQL,典型用途是给虚拟层的视图授权:

python 复制代码
@model(
    "db.test_model",
    kind="full",
    columns={
        "id": "int",
        "name": "text",
    },
    on_virtual_update=["GRANT SELECT ON VIEW @this_model TO ROLE dev_role"],
)
def execute(context, start, end, execution_time, **kwargs) -> pd.DataFrame:
    return pd.DataFrame([{"id": 1, "name": "name"}])

注意:这些语句的表名解析发生在虚拟层 。在名为 dev 的环境中跑 plan 时,db.test_model 和 @this_model 都会解析成 db__dev.test_model,而不是物理表名。同样支持在 model_defaults 中配置项目级默认语句。


3. 蓝图(Blueprinting):一个模板批量生成多个模型

当多个模型逻辑相同、只是参数不同(比如每个客户一张表),不必复制粘贴 N 份代码------用 blueprints 属性传一个键值字典列表,一个文件就能"打印"出多个模型。

规则:模型名必须用蓝图里的变量做参数化 ,语法是 @{变量名}。

python 复制代码
import typing as t
from datetime import datetime

import pandas as pd
from sqlmesh import ExecutionContext, model


@model(
    "@{customer}.some_table",        # 用蓝图变量 customer 参数化模型名
    kind="FULL",
    blueprints=[
        {"customer": "customer1", "field_a": "x", "field_b": "y"},
        {"customer": "customer2", "field_a": "z", "field_b": "w"},
    ],
    columns={
        "field_a": "text",
        "field_b": "text",
        "customer": "text",
    },
)
def entrypoint(
    context: ExecutionContext,
    start: datetime,
    end: datetime,
    execution_time: datetime,
    **kwargs: t.Any,
) -> pd.DataFrame:
    return pd.DataFrame(
        {
            "field_a": [context.blueprint_var("field_a")],
            "field_b": [context.blueprint_var("field_b")],
            "customer": [context.blueprint_var("customer")],
        }
    )

上面的定义会产生两个独立模型:customer1.some_table 和 customer2.some_table,各自使用对应的参数映射,变量通过 context.blueprint_var 读取。

3.1 动态生成蓝图列表

蓝图映射也可以由宏动态构造(适合从 CSV 等外部数据源读取清单):

python 复制代码
@model(
    "@{customer}.some_table",
    blueprints="@gen_blueprints()",
    ...
)
python 复制代码
from sqlmesh import macro

@macro()
def gen_blueprints(evaluator):
    return (
        "((customer := customer1, field_a := x, field_b := y),"
        " (customer := customer2, field_a := z, field_b := w))"
    )

还可以配合 @EACH 宏和全局列表变量(@values):

python 复制代码
@model(
    "@{customer}.some_table",
    blueprints="@EACH(@values, x -> (customer := schema_@x))",
    ...
)

4. 模型属性里使用宏变量(小心 cron 的坑)

Python 模型的属性支持宏变量,但当宏变量出现在字符串内部时要特别小心。典型场景是把调度时间做成参数化 cron:

python 复制代码
# 正确写法:整个表达式用引号包住,并加 @ 前缀
@model(
    "my_model",
    cron="@'*/@{mins} * * * *'",  # 注意 @'...' 语法
    ...
)

# 配合蓝图变量同样适用
@model(
    "@{customer}.scheduled_model",
    cron="@'0 @{hour} * * *'",
    blueprints=[
        {"customer": "customer_1", "hour": 2},  # 凌晨 2 点跑
        {"customer": "customer_2", "hour": 8},  # 早上 8 点跑
    ],
    ...
)

为什么要这么麻烦?因为 cron 表达式本身常用 @ 表示别名(@daily、@hourly),会和 SQLMesh 的宏语法冲突,@'...' 的写法能确保正确解析。


5. 全系列避坑清单(Best Practices)

# 规则 原因
1 columns 声明必须与实际返回的 DataFrame 完全一致 SQLMesh 先建表再跑代码,schema 不符会引发意外行为
2 保持模型幂等 同一区间重跑结果必须一致,否则回刷数据时会产生脏数据
3 永远不要 return 空 DataFrame 可能为空时改用条件 yield(yield from ())
4 用 Spark/Snowflake/BigQuery 时优先返回对应原生 DataFrame context.spark / snowpark / bigframe 让计算分布式执行,避免本地内存瓶颈
5 读上游模型必先 resolve_table 直接硬编码表名会在 dev/prod 环境切换时拿错数据
6 depends_on 显式声明会覆盖函数体内的动态引用 避免依赖图与预期不符
7 pre/post 语句避免创建物理表 多模型并发执行时会产生冲突
8 后置语句必须配合 yield(不能 return) 后置语句要在函数产出数据之后执行
9 变量走函数参数时必须带默认值,且不藏在 kwargs 里 变量缺失时保证模型可加载
10 输出太大就用生成器分批 yield 降低单批内存占用
11 含宏变量的 cron 字符串用 @'...' 包裹 避免 @ 符号解析冲突
12 Python 模型不能用 VIEW / SEED / MANAGED / EMBEDDED kind 需要这些 kind 时改用 SQL 模型

6. 汇总:一个串联全系列知识点的完整示例

把增量 kind、依赖解析、区间过滤、幂等产出写在一起,作为出师检验:

python 复制代码
import typing as t
from datetime import datetime

import pandas as pd
from sqlmesh import ExecutionContext, model
from sqlmesh.core.model.kind import ModelKindName


@model(
    "docs_example.final_demo",
    kind=dict(
        name=ModelKindName.INCREMENTAL_BY_TIME_RANGE,
        time_column="event_date",
    ),
    columns={
        "id": "int",
        "name": "text",
        "event_date": "date",
    },
    depends_on=["docs_example.upstream_model"],
    cron="@daily",
)
def execute(
    context: ExecutionContext,
    start: datetime,
    end: datetime,
    execution_time: datetime,
    **kwargs: t.Any,
) -> pd.DataFrame:
    # 1. 解析上游表名(自动登记依赖)
    table = context.resolve_table("docs_example.upstream_model")

    # 2. 只取本次时间区间内的数据 ------ 保证增量与幂等
    df = context.fetchdf(
        f"SELECT id, name, event_date FROM {table} "
        f"WHERE event_date >= '{start}' AND event_date < '{end}'"
    )

    # 3. 用 pandas 做业务逻辑
    df["name"] = df["name"].str.strip().str.lower()

    # 4. 可能为空的结果:用 yield 而不是 return
    if df.empty:
        yield from ()
    else:
        yield df

结语

三篇文章读完后,SQLMesh Python 模型的心法浓缩成三句话:

  1. 一个 @model 装饰器 + 一个 execute 函数 = 一个模型,元数据字段与 SQL 模型一一对应;
  2. schema 先于代码 ------columns 必填且必须与返回的 DataFrame 严格一致;
  3. 让数据待在引擎里 ------能返回 Spark/Snowpark/Bigframe DataFrame 就不要落到 Pandas,输出太大就用生成器分批 yield。

你已经跨过了初学者到工程实践的门槛。下一步建议阅读官方文档的 model kinds 与 宏系统 章节,把增量策略和参数化能力用得更深。


参考资料:SQLMesh 官方文档 --- Python models(https://sqlmesh.readthedocs.io/en/stable/concepts/models/python_models/)

相关推荐
迅猛龙办公室1 小时前
Python实现绘制同切圆
开发语言·python
和裕2 小时前
年度框架直供 vs 零散按需采购:定制纸箱采购成本、交付与服务核心区别全对比
大数据·运维·网络·人工智能·算法
言乐62 小时前
Python语音检索
开发语言·python·django·virtualenv·pygame
泡茶喝茶写代码2 小时前
A股量化数据工程:从 REST 接口到策略信号(第 1 篇):指数列表与实时行情接入
java·python·股票数据api·股票数据·股票数据api接口·股票api数据接口·股票量化数据接口
具身AGI3 小时前
ARR还是交付量,物理AI 国产 的三种收入口径
大数据·人工智能
计算机源码社3 小时前
基于大数据技术的城市空气质量时序趋势与站点特征分析系统 面向监测站点的城市空气污染时空异质性分析与可视化系统
大数据·机器学习·数据挖掘·数据分析·毕业设计·课程设计·数据可视化
happylifetree3 小时前
Python34-35:核心语法-流程控制语句-循环-综合案例
python
AC赳赳老秦3 小时前
OpenClaw 数据引用规范自动生成:为公开数据构建可信来源标注与标准引用体系
大数据·开发语言·汇编·数据库·人工智能·deepseek·openclaw
吃饱了得干活3 小时前
Agent 的记忆与工具:从上下文窗口到 MCP
python·agent·mcp