跳转至

派生指标(D-DERIVE)

位置:一个 Metric 的 custom_extensions。 载荷键:derive,一个带 type 判别式的 JSON 对象 —— 外加 window 键的 base 字段(M2,见下)。 默认(不存在时):该指标是自包含的,没有任何结构元数据。 支持的形式:M1 发布 filter + compose; M2 补上 window.base 和成员含窗口指标的 compose;1.10 补上 lod(D-LOD, 和 D-LOD 的另一半写在一起)。reagg 与跨数据集的 filter 谓词仍是保留形态,会以 not_implemented 拒绝,并在提示里点名接替它的家族或所属里程碑。 在更粗的粒度上重新聚合一个指标,比如占总量或占上级分组的比例,现在的写法是先算出分母, 再用 compose 相除:任何指标都可以用 fixed / exclude 细节层次算分母, 能在被去掉的分组上相加的指标也可以用整分区窗口。见占比。

声明一个指标如何从同一模型的其它指标派生 —— 这是 Ossie 表达式承载不了的结构信息。 降级分两类。在展平层(flatten tier:filter,以及成员为叶子指标或 filter 指标的 compose)上,该指标的 expression 仍然是一个合法且语义等价的 Ossie 聚合 —— 基础模式照样计算它、得到相同的数字,引擎也会在编译期强制这份等价性 (derive_expression_mismatch)。在阶段层(stage tier:window.base, 以及成员含窗口指标的 compose)上,没有任何扁平表达式能表达这份语义, 于是 expression 只是最接近的那个近似,基础模式给出不一样的结果是合理的 (并带上扩展被忽略的告警)。无论哪一档,都请把这个表达式当作工具维护的产物, 而不是可以手改的东西。对不上时,derive_expression_mismatch 报错会在 expected_expression 里给出展开式(validate 的 JSON 输出),工具可以直接写回原处, 手工粘贴也行。

metrics:
  - name: net_revenue
    expression:
      dialects: [{ dialect: ANSI_SQL, expression: "COALESCE(SUM(orders.amount), 0) - COALESCE(SUM(refunds.ref_amount), 0)" }]
    custom_extensions:
      - vendor_name: DATUS
        data: '{"v": "1.4", "derive": {"type": "compose", "expr": "revenue - total_refunds", "fill": 0}}'
  # 复用 net_revenue;`expression` 是展开式,所以 net_revenue 的声明一改,
  # 这个指标会当场报错。
  - name: net_revenue_per_order
    expression:
      dialects: [{ dialect: ANSI_SQL, expression: "(COALESCE(SUM(orders.amount), 0) - COALESCE(SUM(refunds.ref_amount), 0)) / COUNT(orders.order_id)" }]
    custom_extensions:
      - vendor_name: DATUS
        data: '{"v": "1.4", "derive": {"type": "compose", "expr": "net_revenue / order_count"}}'

filter、compose 与 lod

  • filter —— {"type":"filter","base":<metric>,"where":<predicate>}: base 的一个按维度过滤的变体。谓词会折进聚合的参数里 (CASE WHEN pred THEN arg END;COUNT(*) 和对主键列的计数折成 THEN 1,对其他列的计数 保留该列,因为 THEN 1 会把该列为 NULL 的行也算进去), 而绝不是折成 WHERE —— 不匹配的时间分桶依然存在(取值为 NULL), 正是这一点让该变体在 ROWS 窗框下也安全。谓词读基础指标自己的数据集;从 1.12 起 (S-ATTR-3′)也可以读该数据集经唯一多对一路径可达的一个维度 (在 orders 上的指标写 where: "customers.premier_customer = TRUE";这时对主键的计数 保留该列,聚合才认得出自己的数据集)。base 本身已带守卫时,谓词可以读同一个维度 (两层守卫嵌套),不能读另一个。其他跨数据集谓词在 M4 之前是 not_implemented,且 base 必须是一个非派生的朴素单聚合。引擎会把子集关系(subset_of)记进指标元数据。
  • compose —— {"type":"compose","expr":<arithmetic>,"fill":<n>}: 在指标名之上做标量算术("revenue - total_refunds")。 标识符严格解析为指标名 —— 带限定符或未知的名字都是错误并附候选项, 绝不会悄悄当成列来读。聚合调用会被拒绝(组合绝不重新聚合)。 fill 在分支对齐之后、算术之前替换某个成员的 NULL (用于修正跨事实表时"缺失分桶毒化整个求和"的问题)。带 fill 的两个成员相除时, 分子转成 DOUBLE,被补值的分母包上 NULLIF(…, 0),分母缺失时得到 NULL 而不是除零错误; 模型里不带这些保护的写法仍能通过等价检查,实际执行的是带保护的版本。 compose 自己没有 time_dimension 时,每个成员按自己的时间轴分桶:成员声明的 time_dimension,没有就用它所在数据集的主时间,绝不借用别的数据集的日期。 引擎会识别线性组合(Σ cᵢ·metricᵢ),记录成员、系数和叶子度量的分解, 并在编译期固定下一致维度集(在唯一连接路径规则下,每个成员分支都能到达的维度; 成员共用一条时间轴、或各自都能解析出自己的时间轴时,还包括 metric_time, 见 S-TIME-5) —— dosi list 会暴露它,而在该集合之外分组的查询会得到 unconformed_dimension, 并点名是哪个成员够不到这个维度。成员之间没有共同维度时, 编译期会给出 compose_no_conformed_dimensions 告警。 归因元数据只有在 SUM/COUNT 层级度量之上的线性组合中才标记为 exact (一个被过滤的 COUNT(DISTINCT) 与它的补集会重叠 —— 永远不可能 exact)。
  • lod:{"type":"lod","expr":<粒度表达式>} 声明 include、exclude 或 fixed 指标。 对这些指标做算术组合时使用 compose。核心表达式保留要求的嵌套聚合形式, basic 模式会明确失败。完整规则见 LOD 声明与表达式。

窗口基座与窗口成员

从 M2 起,派生指标还可以建在窗口阶段之上,或读取窗口阶段的输出:

  • window.base —— window 键(D-WINDOW)接受 "base": <metric>: 窗口作用在被点名的那个指标的序列上,而不是本指标自己的表达式上。 base 可以是叶子指标、filter 指标、比率(1.9)、compose (度量落在一个还是多个数据集都可以),或者建在另一个窗口输出上的 compose (1.9)。跨事实 base 的序列要到分支合并之后才成为一个值,所以阶段落在合并之上, 而不是落在单个聚合之上。 被拒绝的情形:直接点名一个窗口指标 —— 内层窗口没有属于自己的阶段可落脚, 包一层 compose 就有了。 元数据记录 derive_family: "window"、derive_base 链,以及继承来的 subset_of。 作用在 filter base 之上的窗口会继承 CASE-WHEN 的分桶保留特性: 没有匹配行的分桶依然存在,所以 ROWS 窗框在日历上保持正确, 累计求和则跳过这些 NULL。

能把比率或 compose 当 base,「按成交率给市场排名」「这个率比上月高多少」 「我在不在平均率之上」才写得出来 —— 这些都不是单聚合能表达的。 要注意窗口平均的是什么:全分区的 avg 建在比率 base 上, 得到的是各行率的等权平均,而不是把分子分母先合并再相除的合并率。 两者都说得通,而且数不一样;窗口给的是「各个率的平均」那一个。 - 成员含窗口指标的 compose —— 当 compose 的 expr 引用了窗口指标时, 算术作用在窗口输出之上("累计客单价" = revenue_cum / order_count_cum: 分子分母各自累计,然后相除)。成员可以把窗口指标和叶子指标混用; 所有成员共享同一个窗口阶段,因此它们必须共享同一条时间轴 (跨事实表的窗口成员在日期经 D-CONFORM 配对时共享)。这类指标的归因永远是 approximate, 也没有叶子度量分解 —— 窗口输出没法从物化聚合里取。 - compose 的成员可以是 filter 指标 —— 例如 "new_order_count / order_count"; 它在展平层上组合,和叶子指标完全一样。

compose 套 compose

成员本身是 compose 时,会在编译期内联展开:编译产物是平的, 和把内层算术直接抄出来写一遍完全一样。这就是一条公式只写一次、 到处复用的办法 —— 而且外层的 expression 是拿内层的声明展开后来核对的, 所以改了内层、消费者那份副本没跟着改,会报 derive_expression_mismatch, 不会变成一个没人怀疑的数。

嵌套之前有四件事要知道:

  • expression 写完整的展开式。 把整条展平后的算术写出来, 包括嵌套带来的括号,以及内层是裸 ratio 形状时编译出来的 CAST(… AS DOUBLE)。 它是工具维护的产物 —— 从声明重新生成,不要手改。
  • fill 会下推到叶子。 每个叶子引用取它那条路径上最内层声明的 fill, 所以外层的 fill: 0 依然能修好内层某个没声明 fill 的成员里缺掉的分支。 这个决定是按引用算的:同一个叶子从两个成员到达、两边填法不同,两种填法都保留。
  • 展平层上 derive_members 是展平后的叶子列表,系数逐层折叠 (quadrupled = doubled × 2、doubled = revenue × 2,报出来的是 revenue 系数 4)。 一致维度沿 DAG 求交,所以 unconformed_dimension 点名的是真正缺维度的叶子, 不是中间那层 compose。dosi lineage 画的仍然是你写下的那条成员边。
  • 分层档上,compose 成员仍然是成员。 树里任何位置出现窗口都会把这个指标推到 分层档,而在那里内联是错的:每个成员都是窗口阶段输出的一条独立序列, 把中间那层 compose 溶解成叶子,会丢掉阶段根本不投影的那段算术 (cum_aov / revenue 会退化成「两个窗口成员对一个叶子」,除法没了)。 所以分层档上 derive_members 是直接成员,规划器把每个嵌套 compose 算成一个隐藏的阶段输出,代入在下降时由内向外进行。 档次判断读的是整棵树,所以窗口长在中间那层 compose 上时, 它上面的每一层也一并进分层档。
  • 嵌套有一个上限:单次展开最多 256 个成员引用 —— 被内联的成员在每一处出现都会被代入一份,所以引用两次就会让展开量逐层翻倍。

成环与元数据

指标引用之间成环会是 metric_reference_cycle,并给出完整路径。 元数据在每个宿主上都会暴露(dosi list、REST、MCP、Python): derive_family、derive_base、derive_expr、subset_of、derive_members、 leaf_measures、attribution、conformed_dimensions,以及 required_dimensions —— 查询必须分组的维度,沿整条依赖链收集:窗口的 partition.exclude(window-extension.md#frozen-degrees), 以及带键 exclude、fixed 写到的维度(D-LOD)。 requires_time_axis 表示查询是否还要带粒度的时间轴。lod 指标的 derive_expr 是规范化后的声明。