语义模型概念
SelectDB MCP 服务通过 Model Context Protocol (MCP) 让 AI Agent 直接查询 SelectDB Cloud 中的数据。它提供两条查询路径:
- 有语义模型:语义查询。当 healthy workspace 中存在包含匹配指标的语义模型时,Agent 优先使用该定义,MetricFlow 生成 SQL。
- 没有语义模型:SQL 查询。当没有语义模型或匹配指标时,Agent 发现元数据并生成只读 SQL 查询。
语义路径的准确性取决于模型定义本身。正确的指标表达式、维度、关系和过滤条件才能产生一致、受治理的结果。定义不正确时,即使查询成功,也可能返回错误答案。
本页介绍语义模型的概念和 YAML 结构。要在 Semantic Web UI 中创建、校验、提交或排查模型问题,请看创建和管理语义模型。
什么是语义模型?
语义模型是对数据库表的业务描述。你告诉系统:
-
这张表的主键是什么(实体)
-
哪些字段可以用来分组(维度)
-
哪些字段需要聚合(度量)
定义完成后,用户就可以用自然语言查询(如"显示每月订单总金额"),系统会自动生成正确的 SQL。
提示:
YAML 是规范,MetricFlow 是翻译器:它把"每月订单总金额"翻译成
SELECT DATE_TRUNC('month', order_date), SUM(amount) FROM orders GROUP BY 1。
语义模型结构
一个完整的 semantic_model 包含以下字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | ✅ | 全局唯一的模型名称。以小写字母开头,可包含数字和下划线 |
db_table | string | ✅ | SelectDB 物理表,格式为 database.table,例如 dw.orders |
defaults | object | ✅ | 默认配置。当前必须包含 agg_time_dimension |
entities | list | ✅ | 表的实体定义。至少需要一个 type: primary 主实体 |
dimensions | list | ✅ | 用于分组和过滤的维度 |
measures | list | 推荐 | 聚合定义。定义后会自动成为可查询的指标 |
description | string | 可选 | 模型的文字描述 |
label | string | 可选 | 显示名称 |
primary_entity | string | 条件必填 | 当 entities 中没有 type: primary 实体时必填 |
警告:
所有名称(模型、实体、维度、度量)必须满足:
- 以小写字母开头
- 只包含小写字母、数字和下划线
- 不能包含连续两个下划线
__- 长度至少为 2 个字符
✅
order_id、total_amount、user_count❌
OrderID、order__id、a
实体与表身份
实体定义行之间的唯一性和关系。每张表必须有一个主实体。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | ✅ | 实体名称,在该模型内唯一 |
type | enum | ✅ | 实体类型(见下表) |
expr | string | 推荐 | 对应的数据库列。可以省略(默认为 name);也支持 SQL 表达式 |
description | string | 可选 | 文字描述 |
label | string | 可选 | 显示名称 |
实体类型
| 类型 | 含义 | 使用场景 |
|---|---|---|
primary | 主键。每行唯一,覆盖所有记录 | 表的 ID 列。每张表必须恰好有一个主实体 |
foreign | 外键。允许重复和空值 | 关联其他表的列,如 customer_id 或 product_id |
unique | 唯一键。每行唯一,可能不覆盖所有记录 | 如邮箱、身份证号 |
natural | 自然键。现实世界中的唯一标识 | 如商品条码、员工工号 |
示例:orders 表的实体定义
entities:
- name: order_id # 主键:每个订单的唯一 ID
type: primary
expr: order_id
- name: customer # 外键:关联 users 表
type: foreign
expr: user_id
- name: order_ref # 外键 + SQL 表达式
type: foreign
expr: substring(trace_id FROM 1 FOR 8)
注意:
如果表没有
type: primary实体,请在模型顶层使用primary_entity: entity_name。
用于分组数据的维度
维度定义数据如何分组和过滤。有两种类型:时间维度和分类维度。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | ✅ | 维度名称 |
type | enum | ✅ | time 或 categorical |
type_params | object | 时间维度必填 | 时间粒度配置(见下文) |
expr | string | 推荐 | 对应的列或 SQL 表达式 |
is_partition | bool | 可选 | 是否为分区列。默认 false |
description | string | 可选 | 文字描述 |
label | string | 可选 | 显示名称 |
时间粒度(time_granularity)
| 粒度 | 含义 | 示例 |
|---|---|---|
day | 按天 | 2025-01-15 |
week | 按周 | 2025-W03 |
month | 按月 | 2025-01 |
quarter | 按季度 | 2025-Q1 |
year | 按年 | 2025 |
hour | 按小时 | 2025-01-15 14:00 |
minute | 按分钟 | 2025-01-15 14:30 |
示例
dimensions:
- name: order_date # 下单日期(按天)
type: time
type_params:
time_granularity: day
expr: order_date
- name: order_month # 下单月份(按月)
type: time
type_params:
time_granularity: month
expr: order_date # 同一列,不同粒度
- name: channel # 渠道(分类)
type: categorical
expr: channel
- name: status_label # 使用 SQL 表达式
type: categorical
expr: concat(status, '_', channel)
用于聚合的度量
度量定义对数据列的聚合。每个度量会自动生成一个同名可查询指标。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | ✅ | 度量名称(同时成为指标名称) |
agg | enum | ✅ | 聚合类型(见下表) |
expr | string | 推荐 | 要聚合的列或 SQL 表达式 |
description | string | 可选 | 指标描述 |
label | string | 可选 | 显示名称 |
create_metric | bool | 可选 | 设为 false 时不自动生成指标。默认 true |
agg_time_dimension | string | 可选 | 覆盖模型默认的时间维度 |
聚合类型
| 类型 | 含义 | 典型用途 |
|---|---|---|
sum | 求和 | 总金额、总数量 |
count | 计数 | 订单数、用户数 |
count_distinct | 去重计数 | 去重用户数、活跃设备数 |
average | 平均 | 平均金额、平均时长 |
min | 最小 | 最低价格、最早时间 |
max | 最大 | 最高价格、最晚时间 |
median | 中位数 | 订单金额中位数 |
percentile | 百分位 | P99 延迟、P95 金额(需要 agg_params.percentile) |
sum_boolean | 布尔求和 | 转化数、通过数 |
示例
measures:
- name: total_amount # 订单总金额
description: "Sum of all order amounts"
agg: sum
expr: amount
label: "Total Amount"
- name: order_count # 订单数
agg: count
expr: order_id
- name: unique_customers # 去重客户数
description: "Number of distinct customers who placed an order"
agg: count_distinct
expr: user_id
- name: p99_amount # P99 订单金额
agg: percentile
expr: amount
agg_params:
percentile: 0.99
- name: internal_counter # 不暴露为指标
agg: sum
expr: raw_value
create_metric: false
完整示例
下面是一个电商场景的完整语义模型定义——orders 表:
# models/orders.yaml — orders 事实表
---
semantic_model:
name: orders
description: "E-commerce order fact table; each row is one order"
db_table: dw.orders
defaults:
agg_time_dimension: order_date
# ── 实体 ──
entities:
- name: order_id
description: "Order primary key"
type: primary
expr: order_id
- name: customer
description: "Associated user"
type: foreign
expr: user_id
- name: product
description: "Associated product"
type: foreign
expr: product_id
# ── 维度 ──
dimensions:
- name: order_date
description: "Order date (by day)"
type: time
type_params:
time_granularity: day
expr: order_date
- name: order_month
description: "Order month"
type: time
type_params:
time_granularity: month
expr: order_date
- name: channel
description: "Order channel"
type: categorical
expr: channel
- name: status
description: "Order status"
type: categorical
expr: status
# ── 度量 ──
measures:
- name: total_amount
description: "Total order amount"
label: "Total Amount"
agg: sum
expr: amount
- name: order_count
description: "Total order count"
label: "Order Count"
agg: count
expr: order_id
- name: unique_customers
description: "Distinct customers who placed an order"
label: "Distinct Customers"
agg: count_distinct
expr: user_id
- name: avg_amount
description: "Average order amount"
label: "Average Order Value"
agg: average
expr: amount
配套:users 和 products 表
# models/users.yaml — users 维度表
---
semantic_model:
name: users
description: "User dimension table"
db_table: dw.users
defaults:
agg_time_dimension: register_date
entities:
- name: user_id
type: primary
expr: user_id
dimensions:
- name: register_date
type: time
type_params:
time_granularity: day
- name: city
type: categorical
- name: level
type: categorical
measures:
- name: user_count
agg: count
expr: user_id
# models/products.yaml — products 维度表(纯维度表,没有度量,因此省略 defaults)
---
semantic_model:
name: products
description: "Product dimension table"
db_table: dw.products
entities:
- name: product
type: primary
expr: product_id
dimensions:
- name: product_name
type: categorical
expr: name
- name: category
type: categorical
expr: category
- name: brand
type: categorical
expr: brand
高级指标定义
除了由 measures 自动生成的简单指标,你还可以用 metric: 文档定义高级指标。高级指标组合已有的度量或指标,实现更复杂的逻辑。支持四种类型:
| 类型 | 含义 | 典型场景 |
|---|---|---|
ratio | 比率指标:分子 ÷ 分母 | 转化率、利润率、占比 |
derived | 派生指标:基于已有指标的表达式 | 环比增长、同比增长、加权计算 |
cumulative | 累计指标:在时间窗口内累加 | 最近 7 天销售额、月度累计注册数 |
conversion | 转化指标:两个事件之间的转化分析 | 下单转化率、注册转化率 |
比率指标
计算两个指标的比率,例如人均订单数 = 订单数 / 用户数。
# models/orders_per_user.yaml
---
metric:
name: orders_per_user
description: "Orders per user: order count / user count"
type: ratio
type_params:
numerator: order_count # 分子:引用已有指标
denominator: user_count # 分母:引用已有指标
分子和分母都必须是已经定义好的指标名称(简单指标或其他高级指标均可)。
派生指标
通过表达式基于一个或多个已有指标计算。最常用于环比和同比计算。
# models/revenue_growth.yaml — 环比增长
---
metric:
name: revenue_growth
description: "Revenue period-over-period growth rate"
type: derived
type_params:
expr: (current_revenue - prev_revenue) / prev_revenue
metrics:
- name: total_amount # 本期营收
alias: current_revenue
- name: total_amount # 上期营收(同一指标,带时间偏移)
alias: prev_revenue
offset_window: 1 month # 向前偏移一个时间窗口
| 参数 | 说明 |
|---|---|
expr | 计算表达式;通过 alias 引用每个输入指标 |
metrics | 输入指标列表 |
name | 被引用的指标名称 |
alias | 在 expr 中使用的别名 |
offset_window | 时间偏移,如 1 month、7 days、1 year |
offset_to_grain | 偏移的粒度,如 month、year |
累计指标
在时间窗口内累加某个指标,例如"最近 7 天销售额"。
# models/weekly_sales.yaml
---
metric:
name: weekly_sales
description: "Sales over the last 7 days"
type: cumulative
type_params:
measure:
name: total_amount
window: 7 days # 时间窗口:过去 7 天
| 参数 | 说明 |
|---|---|
measure | 被引用的度量名称(来自 semantic_model 的 measures) |
window | 时间窗口格式 数字 粒度,如 28 days、4 weeks、3 months |
grain_to_date | 可选。累计到指定粒度,如 month(月初至今)、year(年初至今) |
转化指标
衡量用户从一个事件(基础事件)转化到另一个事件(转化事件)的比率。常用于分析用户漏斗。
# models/order_conversion.yaml
---
metric:
name: register_to_order_conversion
description: "Registration-to-order conversion rate"
type: conversion
type_params:
conversion_type_params:
base_measure: # 基础事件(注册)
name: user_count
conversion_measure: # 转化事件(下单)
name: order_count
entity: user # 关联实体:按哪个维度计算转化
calculation: conversion_rate # 计算方法
| 参数 | 说明 |
|---|---|
base_measure | 基础事件的度量名称 |
conversion_measure | 转化事件的度量名称 |
entity | 计算转化所依据的关联实体(通常是 user 或 session) |
calculation | conversion_rate(转化率)或 conversions(绝对数量) |
window | 可选。转化窗口,如 7 days |
常见场景
场景 1:同一列定义不同粒度的维度
一个日期列可以同时定义天、周、月维度:
dimensions:
- name: order_date
type: time
type_params:
time_granularity: day
expr: order_date
- name: order_week
type: time
type_params:
time_granularity: week
expr: order_date # 同一列!
- name: order_month
type: time
type_params:
time_granularity: month
expr: order_date # 同一列!
场景 2:使用 SQL 表达式
当列名不够直观,或需要计算字段时,可以使用 SQL 表达式:
dimensions:
- name: user_label
type: categorical
expr: concat(level, '_', city) # 拼接字段
entities:
- name: user_short_id
type: foreign
expr: substring(trace_id FROM 1 FOR 8) # 子串
measures:
- name: net_amount
agg: sum
expr: coalesce(amount, 0) - coalesce(discount, 0) # 计算字段
提示:
支持
substring、concat、coalesce、cast等标准 SQL 函数。这些表达式在物理校验阶段会被识别,跳过列名校验。
场景 3:分区表
如果表有分区列,用 is_partition: true 标记:
dimensions:
- name: ds
type: time
type_params:
time_granularity: day
is_partition: true # 标记为分区列
expr: ds
场景 4:隐藏内部度量
有些度量只是中间计算,不应暴露给终端用户,可设置 create_metric: false:
measures:
- name: total_amount # ✅ 公开指标
agg: sum
expr: amount
- name: _raw_count # ❌ 不暴露
agg: count
expr: order_id
create_metric: false
高级功能
过滤条件
可以在度量定义或指标定义中添加 SQL 过滤条件。过滤在聚合之前生效。
注意:
可以在以下位置添加过滤条件:
measures条目的filter:字段:限定单个度量的数据范围。metric:的filter:字段:限定整个指标的数据范围。- 度量被高级指标引用时,
input_measures[].filter:限定被引用的度量。
# 示例 1:度量级过滤——只统计"已完成"订单的总金额
measures:
- name: completed_amount
description: "Sum of completed order amounts"
agg: sum
expr: amount
filter: {{ render_dimension_template('status') }} = 'completed'
# 示例 2:指标级过滤
---
metric:
name: premium_user_orders
description: "Order count for premium users"
type: simple
type_params:
measure:
name: order_count
filter: {{ render_dimension_template('user_level') }} = 'premium'
警告:
过滤条件语法要求:
- 在 YAML 中用
{{ Dimension('qualified_name') }}或{{ render_dimension_template('dimension_name') }}引用维度。- 用
{{ Entity('entity_name') }}或{{ render_entity_template('entity_name') }}引用实体。- 引用后跟正常的 SQL 条件,如
= 'value'或IN ('a', 'b')。query_metric的where参数直接接受原始 SQL,如"channel = 'APP'"。编译器会自动转换为 MetricFlow 模板语法。
保存的查询
把常用的指标 + 分组 + 过滤组合保存为查询模板,用户可以一键调用。
# models/weekly_report.yaml
---
saved_query:
name: weekly_revenue_report
description: "Weekly revenue report: total order amount and order count grouped by channel"
label: "Weekly Revenue Report"
query_params:
metrics:
- total_amount
- order_count
group_by:
- order_id__order_week # 按周分组
- order_id__channel # 按渠道分组
order_by:
- "-order_id__order_week" # 按周倒序
limit: 52
| 字段 | 说明 |
|---|---|
metrics | 要查询的指标名称列表 |
group_by | 分组维度,格式为 entity_name__dimension_name(双下划线连接) |
order_by | 排序;- 前缀表示倒序 |
where | 过滤条件(语法与过滤条件相同) |
limit | 最大返回行数 |
提示:
用双下划线
entity_name__dimension_name引用维度。例如order_id__order_date表示 orders 表的order_date维度。
不可加度量和缓慢变化维度(SCD Type II)
有些度量不能简单相加(如库存、账户余额),需要按特定维度取快照值。
# 不可加度量:库存(月末快照)
measures:
- name: monthly_inventory
description: "Month-end inventory"
agg: sum
expr: inventory_count
non_additive_dimension:
name: snapshot_date # 不可加维度
window_choice: max # 在时间窗口内取最大值
window_groupings:
- product # 按产品分组取快照
SCD Type II(缓慢变化维度):当维度表有生效时间范围时,标记开始和结束维度:
# 在维度表中标记 SCD Type II
dimensions:
- name: valid_from
description: "Validity start time"
type: time
type_params:
time_granularity: day
validity_params:
is_start: true # 标记为开始时间
- name: valid_to
description: "Validity end time"
type: time
type_params:
time_granularity: day
validity_params:
is_end: true # 标记为结束时间
空值填充和时间线对齐
# 把 NULL 替换为 0(让计数类指标在没有数据的日期显示 0)
metric:
name: daily_orders
type: simple
type_params:
measure:
name: order_count
fill_nulls_with: 0 # 在没有数据的日期显示 0
join_to_timespine: true # 对齐时间线(补齐缺失日期)
| 参数 | 说明 |
|---|---|
fill_nulls_with | 把聚合结果中的 NULL 替换为指定值(通常为 0) |
join_to_timespine | 把指标结果与时间线表关联,使每天/每月/每年都有行(缺失日期填充 NULL 或 0) |
原生表引用格式
除了 db_table 简写,还支持 MetricFlow 原生的 node_relation 格式,包括三段式 catalog 引用:
# 两段式:database.table
node_relation:
schema_name: dw
alias: orders
# 三段式:catalog.database.table
node_relation:
database: catalog
schema_name: dw
alias: orders
# db_table 也支持三段式
db_table: catalog.dw.orders
累计指标的聚合模式
累计指标支持三种 period_agg 模式,控制时间窗口内的聚合方式:
# last:取窗口内最后一天的值(默认行为)
metric:
name: end_of_week_inventory
type: cumulative
type_params:
cumulative_type_params:
measure:
name: inventory_count
window: 7 days
period_agg: last # 取最后一天的值
# average:窗口内的日均值
period_agg: average # 7 天平均值
# first:窗口内第一天的值
period_agg: first # 取第一天的值
period_agg | 说明 |
|---|---|
last | 窗口内最后一天的值(默认) |
average | 窗口内的日均值 |
first | 窗口内第一天的值 |
转化指标的常量属性
在转化指标中,可以用 constant_properties 指定两个事件之间必须保持不变的属性:
# 按"流量来源"分析转化,要求两个事件来源相同
metric:
name: register_to_order_by_source
type: conversion
type_params:
conversion_type_params:
base_measure:
name: user_count
conversion_measure:
name: order_count
entity: user
constant_properties:
- base_property: order_id__channel # 基础事件的属性
conversion_property: order_id__channel # 转化事件的属性(必须匹配)
指标时间粒度和偏移粒度
指标级 time_granularity:可以直接在指标定义中设置时间粒度(覆盖查询时的默认值):
metric:
name: monthly_revenue
type: simple
time_granularity: month # 该指标默认按月聚合
type_params:
measure:
name: total_amount
offset_to_grain:在派生指标中,把偏移对齐到指定粒度(而不是默认的天粒度):
metric:
name: yoy_growth
type: derived
type_params:
expr: (current - prev) / prev
metrics:
- name: total_amount
alias: current
- name: total_amount
alias: prev
offset_window: 1 year
offset_to_grain: month # 偏移到月粒度(而不是天)
实体角色
同一个实体(如 user_id)在表中可能扮演多个角色。例如 orders 表中的 user_id 既是"买家"也是"推荐人"。用 role 区分:
entities:
- name: user
type: foreign
expr: buyer_id
role: buyer # 该 user 实体的角色是"买家"
- name: user
type: foreign
expr: referrer_id
role: referrer # 该 user 实体的角色是"推荐人"
未指定 role 时,默认角色等于实体名称。
最佳实践
-
每张表一个文件。 文件命名为
table_name.yaml,结构清晰、易于维护。✅
orders.yaml、users.yaml、products.yaml -
先定义实体,再定义维度,最后定义度量。 实体是语义模型的骨架,维度提供分组能力,度量是查询目标。按这个顺序编写有助于避免遗漏。
-
为每个度量写
description。 终端用户看到的是指标名称和描述。好的描述让用户不用读 YAML 就能理解指标。 -
同一个时间列可以定义多个粒度维度。 例如
order_date列可以同时有天、周、月、季度、年五种粒度。 -
外键实体使用有业务含义的名称。 实体名
customer比user_id更容易理解——它代表"客户"这个概念,而不只是"user_id 列"。 -
提交前先校验。 每次 YAML 变更后,按照校验与提交工作流操作。服务会检查表是否存在、列名是否正确以及命名规则。校验通过后再提交。
-
度量名称在模型内必须唯一。 度量名称可以跨模型重复,但会造成指标覆盖,所以最好保持全局唯一。