跳转至

指标如何组合

Dosi 里的指标不是一个数,也不是一段 SQL,而是查询的函数:给它分组、时间范围和条件,它为每个分组返回一个值。 派生指标由少数几种积木搭成,每种积木都能以任意指标为输入:过滤套在比率上、窗口套在两张事实表之和上、 占比放在累计值旁边。某个组合算不对时,Dosi 用错误码拒绝,而不是返回一个错的数。

本页先用一个小例子演示这些积木,再说明它们的作用顺序、各类条件作用在哪里,以及仍然拒绝的组合。

示例

两张表。下面每个结果都是在它们上面真跑出来的。

customers region
C1 East
C2 East
C3 West
orders customer order_date amount is_new
O1 C1 2024-01-05 100 true
O2 C1 2024-01-20 300 false
O3 C2 2024-01-12 200 true
O4 C3 2024-02-03 400 true
O5 C2 2024-02-15 150 false
O6 C3 2024-02-20 250 false

revenue 是 SUM(orders.amount),order_count 是 COUNT(orders.order_id);其余指标都在下文由这两个声明出来。

指标是查询的函数

模型只声明一次 revenue,参数由每条查询给:

$ dosi query --model model.yaml --metrics revenue \
    --group-by customers.region --execute --db shop.duckdb
region  revenue
East    750
West    650
2 rows

$ dosi query --model model.yaml --metrics revenue \
    --group-by metric_time:month --where "customers.region = 'East'" \
    --order metric_time__month --execute --db shop.duckdb
metric_time__month   revenue
2024-01-01 00:00:00  600
2024-02-01 00:00:00  150
2 rows

同一个指标,两条查询,两个结果。查询传进来的东西叫上下文:分组、时间范围,以及按来源分开存放的条件—— 因为不同的积木读不同的条件(见条件作用在哪里)。

积木

积木 对它所在的查询做什么 模型里怎么写
聚合 读查询选中的行,按分组聚合 指标表达式,SUM(orders.amount)
过滤 给聚合的输入加一个条件,任何分组都不会消失 derive: filter
算术 在同一个查询下计算每个输入,再按分组逐行组合 derive: compose、比率
平移 把查询的时间范围挪开,再把结果对回当前期间 window 偏移、pop
窗口 在结果行上运行:累计、移动平均、排名 window 帧、排名、取值
Exclude 去掉部分分组维度来计算输入,再把更粗的值挂到每一行上 exclude(...)
Fixed 恰好在它列出的维度上计算输入,在 where 之前 fixed(...)
Include 在更细的粒度上计算输入,再聚合回来 avg(include(...))

任何积木都能放在任何指标上。比率上的过滤,就是比率的两个聚合各自过滤:

$ dosi query --model model.yaml --metrics aov,new_aov \
    --group-by customers.region --execute --db shop.duckdb
region  aov    new_aov
East    187.5  150
West    325    400
2 rows

aov 是 compose revenue / order_count,new_aov 是 aov 上按 orders.is_new 的过滤。东区的新客订单是 O1 和 O3,(100 + 200) / 2。

占比是一个指标和它的 exclude 总计之间的算术:

$ dosi query --model model.yaml --metrics revenue,revenue_total,revenue_share \
    --group-by customers.region --execute --db shop.duckdb
region  revenue  revenue_total  revenue_share
East    750      1400           0.5357142857142857
West    650      1400           0.4642857142857143
2 rows

不能在同一批行上一遍算完的积木,仍然可以放在同一条查询里。环比变化和累计值是月度行上的窗口; 每客户平均值要先把订单按客户重新分组。放在一起时,Dosi 把它们分成几个部分各自计算,再按分组对齐:

$ dosi query --model model.yaml \
    --metrics revenue,revenue_mom,running_revenue,revenue_per_customer \
    --group-by metric_time:month --order metric_time__month \
    --execute --db shop.duckdb
metric_time__month   revenue  revenue_mom  running_revenue  revenue_per_customer
2024-01-01 00:00:00  600      NULL         600              300
2024-02-01 00:00:00  800      200          1400             400
2 rows

1 月没有上一个月,所以 revenue_mom 是 NULL,不是 0。

积木的顺序

有些积木可以交换,顺序无关:

  • 过滤和其他任何积木都可交换。compose 上的过滤等于各成员先过滤再 compose;两个过滤就是一个合并的条件。
  • 先 exclude a 再 exclude b,等于同时 exclude 两者。

另一些不能交换,Dosi 按模型写的顺序保留:

  • 在日粒度上,两次平移一个月不等于一次平移两个月:2024 年 3 月 31 日往前一个月是 2 月 29 日,再往前一个月是 1 月 29 日,而往前两个月是 1 月 31 日。
  • 比率上的窗口不等于两个窗口之比:比率的累计和,不是分子累计和除以分母累计和。

仍然拒绝的组合

有几种组合需要一个积木先读到输入的结果行才能开始,Dosi 目前还不计算:

  • base 是 LOD 指标的窗口;
  • base 是窗口的 LOD;
  • 套在这两者上的过滤。

它们在校验模型时就被拒绝,从不算错:

$ dosi validate --model model.yaml
✗ NotImplemented in metric 'running_revenue_per_customer': window.base "revenue_per_customer" is a level-of-detail metric; a window over a level of detail is not implemented
  hint: see docs/datus-extensions.md#d-lod-view-relative

其余限制见窗口和 LOD 参考。

条件作用在哪里

查询里的条件按来源分开存放,因为不同的积木读不同的条件:

条件 谁读它
context_filter 每个聚合,在任何 LOD 之前读——fixed 也读
作用于行的 where 条件 除 fixed 以外的每个聚合
filter 派生 只有它下面的聚合
针对所选指标的 where 条件——在窗口查询里也包括只涉及分组字段的条件 结果行

这张表解释了为什么同一个条件写在 where 里和写在 context_filter 里,会让 fixed 指标得到两个不同的值: 一个能到达它,另一个不能。

限制

  • 没有时间轴补全(time spine):没有行的期间就没有行,也不会被补出来。平移按日历对齐期间,所以缺失的上个月读到 NULL。
  • 窗口最多可以叠三层读另一个窗口的输出,第四层会被拒绝。
  • 分部计算目前在 DuckDB、PostgreSQL、Hologres、GaussDB、DWS、MySQL、TiDB、Trino、Athena、Snowflake、BigQuery、 Databricks 和 ClickHouse 上启用。在其他方言上,需要它的查询会被拒绝。

另见