SQLMesh 模型类型详解(一):EMBEDDED——不建表的共享逻辑

本系列基于 SQLMesh 官方文档整理,讲三种"不走常规计算路径"的模型类型,每篇一种:

  • 第(一)篇:EMBEDDED(本篇)------ 不产生任何表/视图,逻辑内联进下游查询
  • 第(二)篇:EXTERNAL ------ 给外部表登记元数据
  • 第(三)篇:MANAGED ------ 让引擎替你维护数据

主要参考:


一、EMBEDDED 是什么

官方定义只有一句话,但信息量很大:

Embedded models are a way to share common logic between different models of other kinds.

(嵌入式模型是在其他类型的多个模型之间共享公共逻辑的一种方式。)

关键在于它的实现机制:

There are no data assets (tables or views) associated with EMBEDDED models in the data warehouse. Instead, an EMBEDDED model's query is injected directly into the query of each downstream model that references it, as a subquery.

拆成两个要点:

  1. 数仓里没有对应实体------不会为 EMBEDDED 模型建表,也不会建视图;
  2. 查询被内联 ------它的 SQL 会作为子查询直接注入到每一个引用它的下游模型的查询里。

一句话记忆:EMBEDDED = 可复用的 SQL 片段,编译期内联,运行时不落地。


二、最小示例

官方示例非常简单,正好能看清它的结构:

sql 复制代码
MODEL (
  name db.unique_employees,
  kind EMBEDDED
);

SELECT DISTINCT
  name
FROM db.employees;

这个模型定义了"去重后的员工名单"这段逻辑。当别的模型引用 db.unique_employees 时,SQLMesh 不会去查一张真实存在的表,而是把上面这段 SELECT DISTINCT ... 当作子查询嵌进下游模型的 SQL 里。

下游模型大致相当于:

sql 复制代码
MODEL (
  name db.employee_report,
  kind FULL
);

SELECT
  name
FROM (
  SELECT DISTINCT name FROM db.employees   -- ← EMBEDDED 逻辑在此内联
) AS unique_employees;

注:上面这段下游 SQL 是为帮助理解而改写的等价形态,官方文档描述的是"注入为子查询"这一机制,具体渲染结果以 sqlmesh render / sqlmesh create_update 的实际输出为准。


三、它解决什么问题

3.1 场景:同一段逻辑被多个模型重复使用

数据开发中最常见的重复来源:

  • 统一的清洗/去重规则:比如所有下游都要用同一套去重口径的员工名单、同一份"有效订单"过滤条件;
  • 多处复用的中间投影 :比如同一个 JSON 解析逻辑、同一套状态映射(CASE WHEN);
  • 多个 FULL / INCREMENTAL_BY_TIME_RANGE 模型共享的上游准备步骤。

如果直接复制 SQL 片段,改一次要改 N 处,极易口径漂移;如果做成 VIEW 或 FULL 模型,又会在数仓里多出一个真实对象。EMBEDDED 是第三条路:既复用代码,又不增加数仓对象。

3.2 与 VIEW 的对照(最容易混淆的点)

两者都"不主动写数据",但性质完全不同:

VIEW EMBEDDED
数仓里有对象吗 有------物理层建版本化视图,虚拟层再建一个视图 没有------表或视图都不建
何时执行查询 下游每次引用该视图时,由引擎执行视图内的查询 逻辑已内联在下游查询中,随下游查询一起执行
定义位置 models/*.sql models/*.sql
官方默认 kind 是(不指定 kind 时即为 VIEW) 否,必须显式写 kind EMBEDDED
Python 模型支持吗 ❌ 不支持 ❌ 不支持

官方对 VIEW 有一段值得对照的提醒:模型查询会在下游每次引用时被求值,如果查询本身计算量大或被很多下游引用,会带来不划算的计算成本与耗时。 EMBEDDED 同样不落地数据,思路与它相似,但对象数量更少------数仓里连视图都不出现。

3.3 可移植性上的一点提示

在 Tobiko 官方博客《SQLMesh for dbt Users - Part 1》中提到:SQLMesh 的 embedded 模型类型等价于 dbt 的 ephemeral 模型(来源见文末)。如果你来自 dbt 生态,可以直接把它对应起来理解;但要注意,这是跨工具的概念类比,不代表两者渲染细节完全一致。


四、能配置什么

EMBEDDED 没有任何 kind 专属参数 。查官方模型配置参考页可以看到,其中为 VIEW(materialized)、FULL、各类增量模型、SEED 都单独列了 kind 专属选项,但没有 EMBEDDED 小节------也就是说:

  • kind 里只写 EMBEDDED 即可,不带参数;
  • 其余可用配置全部来自通用模型属性 (name、audits、dialect、owner、tags、description、column_descriptions、grains、references、depends_on、enabled、optimize_query、formatting 等)。

需要注意的取舍:像 cron、partitioned_by、clustered_by 这些通用属性虽然写在语法上合法,但对一个"不产生任何数仓对象、也不被独立调度"的模型而言实际不生效------它没有需要刷新的物理表,也没有可分区/聚类的落表对象。


五、适用与不适用

适合:

  • 多个模型共享同一段逻辑,想避免复制粘贴;
  • 不希望这段逻辑作为独立表/视图出现在数仓里(减少对象数量、避免授权与元数据噪音);
  • 逻辑较轻、复用面广的清洗/去重/映射片段。

不适合 / 需要谨慎:

  1. 需要 Python 实现时不能用 ------官方明确:Python 模型不支持 EMBEDDED,请改用 SQL 模型;
  2. 需要作为独立资产被查询/授权/被 BI 直接使用时------它根本不存在于数仓,外部工具查不到它;
  3. 逻辑重、且被大量下游引用时 :内联意味着这段逻辑在每个下游查询里各写一遍,计算成本随引用数增长;此时应把这段逻辑做成物化的模型(FULL 或增量类型),把结果落表共享。
  4. 调试习惯要改 :不能在数仓里 SELECT * FROM db.unique_employees 直接验证------它的存在形式是"渲染进下游的 SQL",要看真实产物得用 SQLMesh 的渲染/血缘工具。

六、一个更贴近实战的写法

把"有效订单"的口径统一下沉成 EMBEDDED 模型,让多个下游共享:

sql 复制代码
-- models/valid_orders.sql :共享口径,不落地
MODEL (
  name stg.valid_orders,
  kind EMBEDDED,
  column_descriptions (
    order_id = '去重后的有效订单主键'
  )
);

SELECT
  order_id::TEXT AS order_id,
  customer_id::TEXT AS customer_id,
  amount::DOUBLE AS amount,
  event_date::DATE AS event_date
FROM raw.orders
WHERE status <> 'cancelled'
  AND amount > 0;
sql 复制代码
-- models/daily_revenue.sql :引用共享逻辑,自己落表
MODEL (
  name mart.daily_revenue,
  kind INCREMENTAL_BY_TIME_RANGE (
    time_column event_date
  )
);

SELECT
  event_date,
  SUM(amount)::DOUBLE AS revenue
FROM stg.valid_orders        -- ← 被内联为子查询
WHERE event_date BETWEEN @start_ds AND @end_ds
GROUP BY event_date;

这样"什么算有效订单"只有一处定义,mart.daily_revenue 与其它需要该口径的模型各自物化,EMBEDDED 本身不占数仓对象。

补充:SQLMesh 的查询优化器默认作用于所有 SQL 模型(optimize_query 默认 true)。EMBEDDED 逻辑被内联后,它就是下游查询的一部分,因此会连同下游查询一起被规范化与简化。这一点是从"所有 SQL 模型默认会被优化"推出的合理结论,而非文档对 EMBEDDED 的直接表述。


七、EMBEDDED 与 WITH(CTE)的区别:问得最多的一组对比

既然 EMBEDDED 最终是"内联进下游的子查询",很多人第一反应是:这和我在 SQL 里写 WITH(CTE)有什么区别? 这是全系列最容易被问住的问题,值得单独一节讲透。

核心结论一句话:WITH 是 SQL 语句内部的临时结构,EMBEDDED 是项目级的治理对象。

7.1 最本质的一条:作用域

WITH(CTE) EMBEDDED 模型
可见范围 仅在当前这条 SQL 语句内,语句结束就消失 整个 SQLMesh 项目 ,任何下游模型都能 FROM db.unique_employees 引用
有名字吗 有,但只是语句内的别名 有,且是全局唯一的模型名(schema.model)
复用方式 想复用只能复制粘贴那段 SQL 写一次,处处引用

这正是 EMBEDDED 存在的理由:官方定义它共享的是跨模型逻辑,而 CTE 天生无法跨模型共享。

7.2 CTE 复制粘贴会带来什么,EMBEDDED 就解决什么

假设"有效订单"的口径要出现在 5 个模型里,用 CTE 的写法就是 5 份重复 SQL:

  • 口径漂移:某次只改了 3 个,剩下 2 个悄悄不一致,报表对不上;
  • 血缘看不到:SQLMesh 能解析 CTE,但那 5 段 CTE 在它眼里是 5 个互不相干的片段,血缘图上没有"一个公共上游节点";
  • 改一次要 plan 五次:无法从一个源头触发下游更新。

换成 EMBEDDED 后,它是依赖图(DAG)里的一个真实节点:改一次定义,SQLMesh 自动识别所有下游并纳入同一次 plan;列级血缘图上有一个清晰的中转节点。

7.3 EMBEDDED 额外拥有的治理能力(CTE 完全做不到)

EMBEDDED 可以用 MODEL DDL 的通用属性挂元数据(官方模型配置页写明适用于除 SEED 外的所有 kind):

sql 复制代码
MODEL (
  name stg.valid_orders,
  kind EMBEDDED,
  owner data-platform,              -- 归属人
  description '统一的有效订单口径',   -- 说明
  column_descriptions (
    order_id = '去重后的有效订单主键'
  ),
  grains order_id,                  -- 粒度声明,让 table_diff 等工具可用
  references customer_id,           -- 关联声明
  audits (not_null(columns = (order_id))),  -- 质量校验
  tags staging shared               -- 标签便于归类
);

CTE 什么都挂不上:没有 owner、没有审计、没有粒度声明、不在血缘图里、UI 上搜不到。

一点需要诚实说明的边界(推断,非文档原文):description / column_descriptions 会被 SQLMesh 注册到引擎的表/列注释------但 EMBEDDED 在数仓里没有可注册的对象,所以这两个属性对它更多是项目内的文档与元数据 ,不会像 FULL 模型那样变成引擎里的 COMMENT。

7.4 运行期性能:其实是一样的

这是最容易误解的地方。EMBEDDED 不会 因为"是个模型"就获得特殊执行待遇------官方描述的机制就是渲染期把 SQL 作为子查询注入下游 ,之后与下游查询一起被优化器处理。所以从引擎视角看,EMBEDDED ≈ 手写的内联 SQL/CTE,不改变执行计划------它省的是代码维护成本,不是计算成本。

由此推出两个实操结论:

  1. 引用越多,成本越高 :同一段逻辑会被写进 N 个下游查询各算一遍。逻辑重、下游多时,应改成 FULL 或增量模型物化落表,让结果共享一次;
  2. 想真正控制执行方式,CTE 反而有额外手段 :部分引擎支持 MATERIALIZED / NOT MATERIALIZED 之类提示来影响 CTE 是否被内联下推,而 EMBEDDED 的渲染由 SQLMesh 决定,你无法在这一层做控制。

7.5 共同的限制

两者都不是可查询的资产:

  • 你无法 SELECT * FROM stg.valid_orders 去验证 EMBEDDED------数仓里它根本不存在(CTE 同理,离开那条语句就没了);要看真实产物得用 SQLMesh 的渲染结果或血缘工具;
  • BI 工具、外部系统都看不到 EMBEDDED;
  • Python 模型不支持 EMBEDDED。

7.6 一组场景对号入座

场景 该用什么
逻辑只在这一个模型里用一次 CTE,别为了复用而复用
同一段逻辑被两个及以上模型需要,要保证口径统一 EMBEDDED
逻辑计算很重,或结果本身有独立使用价值 物化模型 (FULL / 增量类型)
结果需要被 BI / 外部系统直接查询 物化模型 (VIEW / FULL / 增量)

八、小结

  1. EMBEDDED 的定位是跨模型共享逻辑,数仓里不建任何表或视图;
  2. 实现方式是把查询作为子查询注入每个引用它的下游模型;
  3. 它没有 kind 专属参数,配置全靠通用模型属性,但调度/分区类属性对它无实际意义;
  4. Python 模型不支持它;逻辑很重或引用极多时应改为物化模型;
  5. 与 WITH(CTE)的分界线是作用域:CTE 只在单条语句内有效,EMBEDDED 是项目级、可引用、可治理的 DAG 节点;但两者运行期都是内联子查询,性能上没有差别;
  6. 想不起名字的时候记这句:"不建表的共享逻辑,编译期内联"。

参考资料:

相关推荐
ao-weilai1 小时前
MySQL数据库:数据类型
android·数据库·mysql
IT毕设梦工厂1 小时前
计算机毕业设计选题推荐:基于大数据的青少年体质健康数据可视化与分析|毕业设计选题|计算机毕设|选题推荐|毕设指导|项目定制|源码|高质量项目
大数据·hadoop·信息可视化·数据挖掘·数据分析·spark·课程设计
梦想画家1 小时前
SQLMesh 模型类型详解(三):MANAGED——把数据刷新外包给引擎
数据库·数据开发·sqlmesh
DingYuan1011 小时前
Django模板继承
数据库·sqlite
Mortalbreeze1 小时前
MySQL 基础篇(四):表的约束
linux·服务器·数据库·mysql
阳光九叶草LXGZXJ2 小时前
达梦数据库-学习-71-存过执行过程中重建存过影响验证
linux·运维·数据库·sql·学习
傲世仙尊2 小时前
MySQL上手-库与表的操作编码校验集与备份还原
数据库·mysql
志栋智能3 小时前
安全超自动化如何支持快速安全扩容?
运维·服务器·数据库·架构·自动化
gb421528710 小时前
向量数据库多模态特性
数据库