跳转至

参数化指标

revenue_1m_avgrevenue_3m_avgrevenue_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: filterderivewhere intfloatstring

五个窗口槽位都是计数,只接受整数。filter.literal 三种类型都可以 (region = :ramount > :tdatediff_pay < :n)。

其余内容都在建模时定死。在指标 expression、查询的 where_sql 或任何其他扩展键 里引用参数一律报错,不会悄悄渲染成字面量。日期、粒度、维度、聚合函数、方言都 不能参数化。


声明参数

参数声明在指标上,写进 DATUS 扩展条目的 params 列表,和它要填的 windowderive 载荷并列。窗口槽位用 {"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 intfloatstring
default 查询没绑定时用的值,必须落在取值域内
allowed 二选一 合法值的显式清单
min / max 二选一 闭区间,两端各自可省
description 会出现在目录里;写给将来读它的 Agent 看

allowedmin/max 互斥。string 参数必须声明 allowed——不设边界的字符串 参数等于一个任意维度值,而不是一项受管控的选择——并且不接受 min/maxintfloat 参数完全不写取值域仍能编译,但会给出 datus_param_unbounded 警告;声明了却没有任何槽位引用的参数给出 datus_param_unused 警告。

一个参数可以填多个槽位,一个谓词里也可以有多个参数 (datediff_pay < :n AND platid = :plat)。只由参数构成的谓词(:n < 7)会被 拒绝——过滤条件仍然必须作用在列上。

声明了参数但查询时没绑定的指标,行为和不带参数的指标完全一致,连生成的 SQL 都一样。派生指标继承成员的参数:若 ltv = paid_within_n / new_user_cnt,则 ltv 自己不声明也接受 n


查询时绑定

绑定按查询生效,按参数名匹配,作用于本次查询中所有声明了该名字的指标—— 直接声明的,以及通过派生成员继承的。

dosi --model model.yaml query --metrics moving_avg \
  --group-by metric_time:month --param n=6

--param 可重复(--param n=6 --param k=4)。用逗号分隔即为列表;字符串值 本身含逗号时加引号(--param city=NYC,"Paris, FR" 是两个值)。

{"metrics": ["amount_total", "moving_avg"],
 "group_by": [{"field": "metric_time", "grain": "month"}],
 "params": {"n": 6}}              // one binding
{"metrics": ["moving_avg"],
 "group_by": [{"field": "metric_time", "grain": "month"}],
 "params": {"n": [1, 3, 6]}}      // a list: one column per value

没绑定的参数取声明的默认值。 什么都不绑定永远合法,含义就是"模型里定义的 那个指标"。

列表把指标展开成每个值一列。 两个列表取笛卡尔积,先声明的参数在外层。 展开后每次查询最多 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.metricdosi.params)。读它,不要去解析列名。

排序上有一个直接后果:order_by 里写裸指标名,只在该指标仅有一列时能解析。 绑定了列表就要写具体列名(moving_avg__n_6)。


查看指标接受哪些参数

dosi list metrics --format jsonGET /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_schemaparams 这个 映射的 JSON Schema,allowed 渲染成 enummin/max 渲染成 minimum/ maximum。Agent 先读 schema,再在其中绑定。

引擎本身支不支持参数,看 dosi info --format jsonGET /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 载体本身就被忽略, 所有槽位取声明的默认值,查询时的 paramsunknown_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}"
}

另请参阅