指标如何组合¶
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再 excludeb,等于同时 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
条件作用在哪里¶
查询里的条件按来源分开存放,因为不同的积木读不同的条件:
| 条件 | 谁读它 |
|---|---|
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 上启用。在其他方言上,需要它的查询会被拒绝。
另见¶
- 粒度:一行代表什么——查询和 LOD 所在的粒度
- 派生指标——
filter与compose - 窗口指标——平移、帧、排名
- 理解并开始使用 LOD——
exclude、fixed、include - 查询指标——查询和它的响应