派生指标(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 是规范化后的声明。