MCP Server 基于 MetricFlow 引擎,在数据库表之上构建语义模型层。语义模型通过 YAML 文件定义实体、维度和度量,将数据库表映射为业务概念,使 AI 客户端能够理解业务语义并自动生成正确的 SQL 查询。本文介绍语义模型的定义规范、高级指标、工作空间管理和常见错误排查。
语义模型概述
语义模型是对数据库表的业务描述。通过 YAML 文件定义以下信息:
-
表的主键是什么(实体)。
-
有哪些字段可以用来分组(维度)。
-
哪些字段需要聚合计算(度量)。
定义好语义模型后,用户可以使用自然语言查询(例如"给我看每月订单总额"),系统自动生成正确的 SQL 语句。
快速上手
步骤 1:编写 YAML 文件
每个 YAML 文件描述一张表,放到 models/ 目录下:
# models/orders.yaml
---
semantic_model:
name: orders
db_table: dw.orders
defaults:
agg_time_dimension: order_date
entities:
- name: order_id
type: primary
expr: order_id
dimensions:
- name: order_date
type: time
type_params:
time_granularity: day
- name: channel
type: categorical
measures:
- name: total_amount
agg: sum
expr: amount
- name: order_count
agg: count
expr: order_id
步骤 2:定义时间配置
创建 project.yaml 指定日历表,用于计算同比、环比等时间指标:
# models/project.yaml
---
time_config:
calendar:
- table: dw.dim_date
column: date_id
grain: day
步骤 3:验证并提交
-
在 WebUI 中单击验证,检查模型是否正确。系统自动检查表是否存在、列名是否正确、实体和维度是否完整。
-
确认无误后单击提交使模型生效。
-
使用
list_metrics查看可用指标,使用query_metric查询数据。
语义模型结构
一个完整的 semantic_model 包含以下字段:
|
字段 |
类型 |
必填 |
说明 |
|
|
字符串 |
是 |
全局唯一的模型名称。小写字母开头,可含数字和下划线。 |
|
|
字符串 |
是 |
物理表名, |
|
|
对象 |
是 |
默认配置,必须包含 |
|
|
列表 |
是 |
实体定义,至少包含一个 |
|
|
列表 |
是 |
可用于分组和筛选的维度。 |
|
|
列表 |
推荐 |
聚合计算定义,定义后自动成为可查询的指标。 |
命名规则:所有名称必须以小写字母开头,只能包含小写字母、数字和下划线,不能包含两个连续下划线,至少 2 个字符。
实体
实体定义了行与行之间的唯一性和关联关系。每张表都必须有一个主实体。
|
类型 |
说明 |
|
|
主键。每行唯一,覆盖全部记录。每张表必须有且仅有一个 primary 实体。 |
|
|
外键。可以有重复值和空值,用于关联其他表。 |
|
|
唯一键。每行唯一,可能不覆盖全部记录。 |
|
|
自然键。现实世界中的唯一标识,如商品条形码、员工工号。 |
示例
entities:
- name: order_id # 主键
type: primary
expr: order_id
- name: customer # 外键,关联用户表
type: foreign
expr: user_id
- name: order_ref # 外键 + SQL 表达式
type: foreign
expr: substring(trace_id FROM 1 FOR 8)
维度
维度定义了如何对数据进行分组和筛选,包括时间维度和分类维度两种类型。
时间粒度
|
粒度 |
含义 |
示例 |
|
|
按天 |
2025-01-15 |
|
|
按周 |
2025-W03 |
|
|
按月 |
2025-01 |
|
|
按季度 |
2025-Q1 |
|
|
按年 |
2025 |
示例
dimensions:
- name: order_date # 时间维度(按天)
type: time
type_params:
time_granularity: day
- 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)
维度支持使用 substring、concat、coalesce、cast 等标准 SQL 函数作为表达式。
度量
度量定义了对数据列的聚合运算。每个度量自动生成一个同名的查询指标。
|
聚合类型 |
含义 |
典型用法 |
|
|
求和 |
总金额、总次数 |
|
|
计数 |
订单数、用户数 |
|
|
去重计数 |
独立用户数、活跃设备数 |
|
|
平均值 |
平均金额、平均时长 |
|
|
最小值 |
最低价格 |
|
|
最大值 |
最高价格 |
|
|
中位数 |
订单金额中位数 |
|
|
百分位数 |
P99 延迟、P95 金额 |
示例
measures:
- name: total_amount
agg: sum
expr: amount
- name: unique_customers
agg: count_distinct
expr: user_id
- name: p99_amount
agg: percentile
expr: amount
agg_params:
percentile: 0.99
- name: internal_counter # 不暴露为指标
agg: sum
expr: raw_value
create_metric: false
完整示例
以下是一个电商场景的完整语义模型定义,包含订单事实表、用户维表和商品维表三个模型文件。
orders.yaml — 订单事实表
# models/orders.yaml — 订单事实表
---
semantic_model:
name: orders
description: "电商订单事实表,每行代表一笔订单"
db_table: dw.orders
defaults:
agg_time_dimension: order_date
# ── 实体 ──
entities:
- name: order_id
description: "订单主键"
type: primary
expr: order_id
- name: customer
description: "关联用户"
type: foreign
expr: user_id
- name: product
description: "关联商品"
type: foreign
expr: product_id
# ── 维度 ──
dimensions:
- name: order_date
description: "下单日期(按天)"
type: time
type_params:
time_granularity: day
expr: order_date
- name: order_month
description: "下单月份"
type: time
type_params:
time_granularity: month
expr: order_date
- name: channel
description: "下单渠道"
type: categorical
expr: channel
- name: status
description: "订单状态"
type: categorical
expr: status
# ── 度量 ──
measures:
- name: total_amount
description: "订单总金额"
label: "总金额"
agg: sum
expr: amount
- name: order_count
description: "订单总数"
label: "订单数"
agg: count
expr: order_id
- name: unique_customers
description: "下单的独立客户数"
label: "独立客户"
agg: count_distinct
expr: user_id
- name: avg_amount
description: "平均订单金额"
label: "客单价"
agg: average
expr: amount
users.yaml — 用户维表
# models/users.yaml — 用户维表
---
semantic_model:
name: users
description: "用户信息维表"
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
products.yaml — 商品维表
# models/products.yaml — 商品维表
---
semantic_model:
name: products
description: "商品信息维表"
db_table: dw.products
defaults:
agg_time_dimension: order_date
entities:
- name: product
type: primary
expr: product_id
dimensions:
- name: order_date
type: time
type_params:
time_granularity: day
expr: product_id
- name: product_name
type: categorical
measures:
- name: product_count
agg: count
expr: product_id
常见场景
同一列作为不同粒度的维度
一张日期列可以同时定义 day、week、month 三个维度,只需为每个粒度创建一个时间维度并指向同一列:
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 # 同一列
使用 SQL 表达式
当列名不够直观或需要计算字段时,可以使用 SQL 表达式。支持 substring、concat、coalesce、cast 等标准 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) # 计算字段
分区表
如果表有分区列,可以通过 is_partition: true 标记分区维度:
dimensions:
- name: ds
type: time
type_params:
time_granularity: day
is_partition: true # 标记为分区列
expr: ds
高级指标定义
除度量自动生成的简单指标外,还可以通过独立的 YAML 文件(顶层使用 metric:)定义高级指标。支持四种类型:
|
类型 |
说明 |
典型场景 |
|
|
分子 ÷ 分母 |
转化率、利润率 |
|
|
对已有指标做表达式计算 |
环比增长、同比变化 |
|
|
按时间窗口累加 |
近 7 日销量、月累计注册数 |
|
|
两个事件的转化分析 |
下单转化率、注册转化率 |
比率指标
# models/conversion_rate.yaml
---
metric:
name: order_conversion_rate
description: "订单转化率:下单数 / 访问数"
type: ratio
type_params:
numerator: order_count
denominator: visit_count
派生指标
# models/revenue_growth.yaml
---
metric:
name: revenue_growth
description: "收入环比增长率"
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
累积指标
# models/weekly_sales.yaml
---
metric:
name: weekly_sales
description: "近 7 天销售额"
type: cumulative
type_params:
measure:
name: total_amount
window: 7 days
转化指标
# models/order_conversion.yaml
---
metric:
name: visit_to_order_conversion
description: "从访问到下单的转化率"
type: conversion
type_params:
conversion_type_params:
base_measure:
name: visit_count
conversion_measure:
name: order_count
entity: user
calculation: conversion_rate
进阶功能
过滤器
可以在度量或指标定义中添加 SQL 过滤条件,在聚合计算前应用。
# 度量级过滤:只计算已完成状态的订单总额
measures:
- name: completed_amount
agg: sum
expr: amount
filter: {{ render_dimension_template('status') }} = 'completed'
过滤器语法:使用 {{ Dimension('name') }} 引用维度,使用 {{ Entity('name') }} 引用实体,后跟 SQL 条件。
预定义查询
将常用的指标、分组和筛选组合保存为查询模板,用户可直接调用。
# models/weekly_report.yaml
---
saved_query:
name: weekly_revenue_report
description: "周度收入报告"
query_params:
metrics:
- total_amount
- order_count
group_by:
- order_id__order_week
- order_id__channel
order_by:
- "-order_id__order_week"
limit: 52
维度引用格式为 实体名__维度名(双下划线连接)。
空值填充与时间轴对齐
metric:
name: daily_orders
type: simple
type_params:
measure:
name: order_count
fill_nulls_with: 0 # 无数据日期显示 0
join_to_timespine: true # 对齐到时间轴,补全无数据日期
非加性度量与缓慢变化维度
有些度量不能简单求和(如库存量、账户余额),需要在特定维度下取快照值。通过 non_additive_dimension 指定非加性维度和窗口选择方式:
# 非加性度量:库存量(取月末快照)
measures:
- name: monthly_inventory
description: "月末库存量"
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: "生效开始时间"
type: time
type_params:
time_granularity: day
validity_params:
is_start: true # 标记为开始时间
- name: valid_to
description: "生效结束时间"
type: time
type_params:
time_granularity: day
validity_params:
is_end: true # 标记为结束时间
原生表引用格式
除了 db_table 简写,也支持 MetricFlow 原生的 node_relation 格式,支持三段式目录引用:
# 两段式:库名.表名
node_relation:
schema_name: dw
alias: orders
# 三段式:目录.库名.表名
node_relation:
database: catalog
schema_name: dw
alias: orders
# db_table 同样支持三段式
db_table: catalog.dw.orders
累积指标聚合方式
累积指标支持三种 period_agg 模式,控制在时间窗口内的聚合行为:
metric:
name: end_of_week_inventory
type: cumulative
type_params:
cumulative_type_params:
measure:
name: inventory_count
window: 7 days
period_agg: last # 取窗口内最后一天的值(默认)
# period_agg: average # 窗口内每日均值
# period_agg: first # 窗口内第一天的值
|
period_agg |
说明 |
|
|
窗口内最后一天的值(默认) |
|
|
窗口内每日均值 |
|
|
窗口内第一天的值 |
转化指标固定属性
在转化指标中,可以用 constant_properties 指定在不同事件间保持不变的属性:
# 按"渠道"维度分析转化,要求渠道在两次事件间不变
metric:
name: visit_to_order_by_source
type: conversion
type_params:
conversion_type_params:
base_measure:
name: visit_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 既是"买家"也是"推荐人"。通过 role 区分:
entities:
- name: user
type: foreign
expr: buyer_id
role: buyer # 此 user 实体的角色是"买家"
- name: user
type: foreign
expr: referrer_id
role: referrer # 此 user 实体的角色是"推荐人"
不指定 role 时,默认角色等同于实体名。
工作空间管理
工作空间概述
工作空间是语义模型的隔离容器。每个工作空间拥有独立的模型文件、指标列表、查询引擎实例和数据库连接池。工作空间 A 的指标对工作空间 B 完全不可见。
工作空间有三种运行状态:
|
状态 |
图标 |
说明 |
|
healthy |
绿色 |
正常运行。语义模型已加载,指标可查询。 |
|
no_models |
白色 |
空工作区。尚未上传任何 YAML 文件。 |
|
not_ready |
红色 |
加载失败。YAML 语法错误、表不存在或缺少 project.yaml 等。 |
Active Store 与 Staging Store
每个工作空间内部有两层存储:
-
Active Store(已生效):已提交并加载的模型,查询引擎使用此版本,文件只读。
-
Staging Store(待提交):用户上传或编辑的待定变更,验证通过后提交进入 Active Store。
更新流程
-
上传或编辑 YAML:文件进入 Staging Store,不影响正在运行的查询。
-
验证:系统自动检查表存在性、列名正确性、YAML 语法、命名规范和跨模型重复名。
-
提交:将 Staging 内容移至 Active Store,系统自动触发引擎重载。
-
自动重载:后台线程解析新模型、编译清单、更新路由,通常 2~5 秒完成。
必须先验证再提交。未经过验证的 Staging 内容会被提交操作拒绝。
权限模型
|
操作 |
管理员 |
普通用户 |
|
查看语义模型、查询指标 |
✅ |
✅ |
|
上传、编辑、删除 YAML |
✅ |
❌ |
|
验证、提交、丢弃 |
✅ |
❌ |
|
创建、删除工作空间 |
✅ |
❌ |
默认示例工作区
首次启动时,系统会自动创建 example 工作区,包含完整的种子数据:
|
内容 |
说明 |
|
|
订单表(12 条模拟数据),含 order_id、user_id、product_id、amount、channel、status、order_date |
|
|
用户表(5 条模拟数据),含 user_id、name、city、level、register_date |
|
|
商品表(5 条模拟数据),含 product_id、name、category、brand、price |
|
|
日期维度表(日历表),用于时间轴对齐和累计计算 |
|
语义模型 YAML |
orders.yaml、users.yaml、products.yaml + project.yaml(自动注入 active_store_example) |
示例指标包括:total_amount(订单总额)、order_count(订单数)、avg_amount(客单价)、unique_users(独立用户数)来自 orders 模型;user_count(用户数)来自 users 模型。
可在配置中设置 seed_example: false 禁用自动种子。
通过 WebUI 管理模型
在浏览器中打开控制台显示的 WebUI 地址,使用数据库账号登录后,可进行以下操作:
|
按钮 |
说明 |
|
验证 |
验证 Staging 中的所有变更,包括物理校验、语义校验和重复检测。 |
|
提交 |
将验证通过的内容提交到 Active Store,触发引擎重载。 |
|
放弃 |
丢弃 Staging 中的所有变更,回到 Active Store 版本。 |
|
重载 |
手动触发引擎重载(通常提交后自动执行)。 |
|
新建 |
新建 YAML 模型文件。 |
最佳实践
-
一张表一个文件:文件命名使用
表名.yaml,结构清晰易维护。 -
按顺序定义:先定义实体,再写维度,最后加度量。实体是语义模型的骨架,维度是分组能力,度量是查询目标。
-
添加描述:给每个度量写
description,终端用户通过描述理解指标含义。 -
多粒度维度:同一张时间列可以定义多种粒度的维度,如
order_date列同时定义 day、week、month、quarter、year。 -
有意义的实体名称:外键实体使用业务名称,如
customer比user_id更易理解。 -
先验证再提交:每次修改 YAML 后,先执行验证确认无误再提交。
-
度量名全局唯一:不同模型的度量名重复会导致指标覆盖,建议全局唯一。
常见错误排查
|
错误信息 |
解决方法 |
|
|
检查 |
|
|
检查 |
|
|
重命名其中一个度量,确保全局唯一。 |
|
|
创建包含 |
|
|
使用小写字母开头,去除双下划线,加长到至少 2 个字符。 |
|
|
先上传或修改 YAML 文件,再执行验证。 |
快速排查步骤:
-
确认数据库表存在:执行
DESCRIBE dw.orders。 -
确认列名和大小写完全一致。
-
检查 YAML 缩进是否正确(必须用空格,不能用 Tab)。
-
确认所有名称符合小写字母开头的命名规范。