参数化指标¶
revenue_1m_avg、revenue_3m_avg、revenue_6m_avg 其实是同一个指标问了三遍。
按宽度各建一个,数量会不断膨胀:每加一种宽度就多一个指标要定义、要评审、要
和兄弟指标保持一致;看板上写着"移动平均",读的人还得先弄清是哪一个。
参数化指标把宽度本身声明成带类型的查询时参数。指标只有一个 moving_avg,
宽度由查询决定:
$ dosi --model model.yaml query --metrics amount_total,moving_avg \
--group-by metric_time:month --where "sales.region = 'east'" \
--order metric_time__month --param n=1,3,6 --execute --db sales.duckdb
metric_time__month amount_total moving_avg__n_1 moving_avg__n_3 moving_avg__n_6
2024-11-01 00:00:00 10 10 10 10
2024-12-01 00:00:00 20 20 15 15
2025-01-01 00:00:00 30 30 20 20
2025-02-01 00:00:00 40 40 30 25
2025-03-01 00:00:00 50 50 40 30
2025-04-01 00:00:00 60 60 50 35
6 rows
参数填的是指标声明里一个带类型的洞,不是 SQL 文本。参数带类型和取值域, 所以模型对该参数允许的每一个取值都成立;绑定既跨不出取值域,也无从往编译出的 SQL 里注入任何东西。取值域就是治理边界:它规定这个指标允许是哪些口径。
本页是该功能的参考文档——哪些位置可以参数化、怎么声明、怎么绑定、边界在哪。 线上协议层面的 schema 见 datus-extensions.md。
参数可以改变什么¶
参数只能填下面这六个洞,此外没有别的位置:
| 槽位 | 控制什么 | 声明位置 | 类型 |
|---|---|---|---|
rolling.periods |
滚动窗口的宽度 | type: rolling 下的 window.periods |
int,取值域 ≥ 1 |
frame.preceding |
通用窗框向前看多少个桶 | window.frame.preceding |
int,取值域 ≥ 0 |
offset.count |
对比向前推多少个周期 | window.offset.count(对象写法) |
int,取值域 ≥ 1 |
rank.buckets |
ntile 的分桶数 |
window.rank.buckets |
int,取值域 ≥ 1 |
value.n |
nth_value 读第几个位置 |
window.value.n |
int,取值域 ≥ 1 |
filter.literal |
派生过滤指标谓词里的字面量 | type: filter 的 derive 的 where |
int、float 或 string |
五个窗口槽位都是计数,只接受整数。filter.literal 三种类型都可以
(region = :r、amount > :t、datediff_pay < :n)。
其余内容都在建模时定死。在指标 expression、查询的 where_sql 或任何其他扩展键
里引用参数一律报错,不会悄悄渲染成字面量。日期、粒度、维度、聚合函数、方言都
不能参数化。
声明参数¶
参数声明在指标上,写进 DATUS 扩展条目的 params 列表,和它要填的 window
或 derive 载荷并列。窗口槽位用 {"param": "<名字>"} 引用,过滤谓词里用
:<名字>。
metrics:
# A rolling width as a parameter: `moving_avg` at n=3 is the classic
# 3-month moving average; n=[1,3,6] is three columns in one query.
- name: moving_avg
description: n-month rolling average (n is a query-time parameter)
expression:
dialects: [{ dialect: ANSI_SQL, expression: "SUM(sales.amount)" }]
custom_extensions:
- vendor_name: DATUS
data: |
{"v": "1.5",
"params": [{"name": "n", "type": "int", "default": 3, "min": 1, "max": 12,
"description": "trailing window width in buckets of the queried grain"}],
"window": {"type": "rolling", "function": "avg", "periods": {"param": "n"}}}
# A filter literal as a parameter. The authored expression is the default
# (n = 7) instantiation — the two must agree.
- name: paid_within_n
description: Payments within the first n days after registration
expression:
dialects:
- dialect: ANSI_SQL
expression: "SUM(CASE WHEN base.datediff_pay < 7 THEN base.sum_pay_amount END)"
custom_extensions:
- vendor_name: DATUS
data: |
{"v": "1.5",
"params": [{"name": "n", "type": "int", "default": 7,
"allowed": [1, 3, 7, 14, 30, 60, 90, 180],
"description": "days since registration, exclusive upper bound"}],
"derive": {"type": "filter", "base": "pay_total", "where": "datediff_pay < :n"}}
params 的每一项包含:
| 字段 | 必填 | 含义 |
|---|---|---|
name |
是 | [a-z][a-z0-9_]*,在指标内唯一;查询按这个名字绑定 |
type |
是 | int、float 或 string |
default |
是 | 查询没绑定时用的值,必须落在取值域内 |
allowed |
二选一 | 合法值的显式清单 |
min / max |
二选一 | 闭区间,两端各自可省 |
description |
否 | 会出现在目录里;写给将来读它的 Agent 看 |
allowed 和 min/max 互斥。string 参数必须声明 allowed——不设边界的字符串
参数等于一个任意维度值,而不是一项受管控的选择——并且不接受 min/max。
int 或 float 参数完全不写取值域仍能编译,但会给出 datus_param_unbounded
警告;声明了却没有任何槽位引用的参数给出 datus_param_unused 警告。
一个参数可以填多个槽位,一个谓词里也可以有多个参数
(datediff_pay < :n AND platid = :plat)。只由参数构成的谓词(:n < 7)会被
拒绝——过滤条件仍然必须作用在列上。
声明了参数但查询时没绑定的指标,行为和不带参数的指标完全一致,连生成的 SQL
都一样。派生指标继承成员的参数:若 ltv = paid_within_n / new_user_cnt,则
ltv 自己不声明也接受 n。
查询时绑定¶
绑定按查询生效,按参数名匹配,作用于本次查询中所有声明了该名字的指标—— 直接声明的,以及通过派生成员继承的。
--param 可重复(--param n=6 --param k=4)。用逗号分隔即为列表;字符串值
本身含逗号时加引号(--param city=NYC,"Paris, FR" 是两个值)。
没绑定的参数取声明的默认值。 什么都不绑定永远合法,含义就是"模型里定义的 那个指标"。
列表把指标展开成每个值一列。 两个列表取笛卡尔积,先声明的参数在外层。 展开后每次查询最多 64 个指标列。
类型严格。 int 参数拒绝 7.5 和 "7"。唯一的隐式转换是 float 参数接受
JSON 整数。
读取结果¶
绑定为默认值时,列名就是指标名 moving_avg;绑定为非默认值时,列名是
{metric}__{param}_{value}:
moving_avg__n_6 # n = 6
ltv__n_30 # inherited parameter
rate__t_1_5 # float 1.5
region_share__r_o_neil # strings are sanitized
多个参数按声明顺序依次追加;只要有一个参数绑定了列表,所有列都带后缀,包括 取默认值的那一列。这条规则的作用是:两个看板不可能用同一个指标名展示不同宽度 的结果。
响应里同时给出机器可读的 outputs——每个指标列一条,说明它来自哪个指标、由哪些
参数产生,默认值也一并列出:
$ dosi --model model.yaml query --metrics amount_total,moving_avg \
--group-by metric_time:month --param n=3,6 --format json --execute --db sales.duckdb
{
"dialect": "duckdb",
"sql": "WITH base AS (…) SELECT … AVG(sales_amount_sum) OVER (ORDER BY metric_time__month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg__n_3, … AS moving_avg__n_6 FROM base",
"outputs": [
{"column": "amount_total", "metric": "amount_total"},
{"column": "moving_avg__n_3", "metric": "moving_avg", "params": {"n": 3}},
{"column": "moving_avg__n_6", "metric": "moving_avg", "params": {"n": 6}}
],
…
}
CLI 的 JSON 输出、REST 与 MCP 的查询响应、Python 结果字典、explain 都带
outputs;走 Arrow 时同样的信息放在字段元数据里(dosi.metric、
dosi.params)。读它,不要去解析列名。
排序上有一个直接后果:order_by 里写裸指标名,只在该指标仅有一列时能解析。
绑定了列表就要写具体列名(moving_avg__n_6)。
查看指标接受哪些参数¶
dosi list metrics --format json、GET /v1/metrics 和 MCP 的 list_metrics
都带上每个指标的 params——声明内容加上它所填的槽位;参数继承自成员时还有
declared_on:
$ dosi --model model.yaml list metrics --format json
{
"name": "moving_avg",
"params": [
{"name": "n", "type": "int", "default": 3, "min": 1, "max": 12,
"description": "trailing window width in buckets of the queried grain",
"slots": ["rolling.periods"]}
],
…
}
面向 Agent,MCP 的 describe_metric 还多给一个 param_schema:params 这个
映射的 JSON Schema,allowed 渲染成 enum,min/max 渲染成 minimum/
maximum。Agent 先读 schema,再在其中绑定。
引擎本身支不支持参数,看 dosi info --format json 和 GET /v1/capabilities
里 DATUS 扩展注册表的 params 键("since": "1.5")。
限制与边界¶
- 只有上面六个槽位。 日期、粒度、维度、聚合函数、方言都不能参数化。
- 每次查询一套绑定。
params作用于本次查询中所有声明了该名字的指标,没有 按指标覆盖的写法;共用同一个参数名的两个指标只能问同一个值。 - 展开后每次查询最多 64 个指标列(
param_expansion_too_large)。 - 归因每个参数只接受单值。
dosi attribute --param n=30可以,列表会被拒绝 并给出改成单值的重试建议;解析后的绑定回填在comparison_metadata.params。 见 attribution.md。 - 基础模式忽略参数。
--osi-basic下 D-WINDOW 与 D-DERIVE 载体本身就被忽略, 所有槽位取声明的默认值,查询时的params按unknown_metric_param拒绝。 - 旧引擎。 低于 DATUS 1.5 的引擎在版本门禁处直接拒绝模型。但 1.4 的服务端
容忍请求里的未知字段:它会静默丢掉客户端的
params,按默认值作答。远程绑定前 先查GET /v1/capabilities(或dosi info)。
错误¶
每个拒绝都是结构化的,会指出涉及的指标,并带一个可以原样重发的
suggested_retry。完整清单见 errors.md。
| 错误码 | 触发条件 |
|---|---|
unknown_metric_param |
本次查询没有任何指标声明这个参数名;candidates 列出确实声明了的名字 |
param_out_of_domain |
值的类型不对或超出取值域——空列表、重复值列表,以及只允许单值处却给了列表,也归这一类 |
param_expansion_too_large |
列表展开后超过 64 个指标列 |
$ dosi --model model.yaml query --metrics moving_avg --group-by metric_time:month --param n=0 --format json
{
"code": "param_out_of_domain",
"message": "parameter \"n\" must be in [1, 12] (declared on metric \"moving_avg\")",
"metrics": ["moving_avg"],
"suggested_retry": "\"params\": {\"n\": 3}"
}
另请参阅¶
- datus-extensions.md——D-PARAM 的线上 schema、 版本策略与兼容性约定。
- window-extension.md——五个整数参数所填槽位背后的窗口族。
- extensions-guide.md ——窗口指标本身的使用指南。
- cli.md · rest-api.md · mcp.md
——
--param/params各个接口面。