跳转至

查询指标

指标查询只需写明要哪些指标、按哪些维度拆分;聚合、连接和 GROUP BY 由 Dosi 生成。 要的是行级记录而不是聚合值时,见查询明细行。

$ dosi query --model $DOSI_EXAMPLES/orders/model.yaml \
    --metrics revenue,order_count \
    --group-by customers.region,orders.status \
    --order -revenue \
    --execute --db orders.db
region  status     revenue  order_count
east    completed  270      3
west    completed  80       1
east    cancelled  70       1
west    cancelled  30       1
4 rows

不用写 JOIN:revenue 在 orders 上,region 在 customers 上, Dosi 沿着两者之间的关系连接。本页的例子都用自带的 orders 模型, 以及教程里导入数据的 orders.db 文件。

选指标和维度

--metrics 写指标名,--group-by 写 dataset.field。不确定有哪些,先问模型:

$ dosi list metrics --model $DOSI_EXAMPLES/orders/model.yaml
$ dosi list dimensions --model $DOSI_EXAMPLES/orders/model.yaml
$ dosi show metric revenue --model $DOSI_EXAMPLES/orders/model.yaml
metric: revenue
description: Total order amount
kind: aggregate
expr: SUM(orders.amount)
datasets: orders
group_by: orders, customers, products
time: metric_time (day, week, month, quarter, year)

group_by 列出这个指标能按哪些数据集拆分,time 列出它接受的时间粒度。 名字写错时直接拒绝,并给出模型里实际有的名字:

$ dosi query --model $DOSI_EXAMPLES/orders/model.yaml --metrics revenu
error: UnknownMetric: unknown metric "revenu"
  candidates: revenue, order_count, unique_customers, avg_order_value, total_margin

时间粒度与 metric_time

在时间维度后面加 :粒度 即可按该时间粒度分桶,可选 day、week、month、quarter、year。 输出列名为 {field}__{grain}:

$ dosi query --model $DOSI_EXAMPLES/orders/model.yaml \
    --metrics revenue --group-by orders.order_date:month \
    --order order_date__month --execute --db orders.db
order_date__month    revenue
2024-01-01 00:00:00  150
2024-02-01 00:00:00  230
2024-03-01 00:00:00  70
3 rows

metric_time 代表每个指标的主(聚合)时间维度。问"按月看收入"时, 不必知道日期在哪一列,输出列名是 metric_time__month。 指标的主时间来自 D-TIME 扩展, 没有声明时取其数据集里唯一的时间字段。

时间范围

--start-time 包含该值,--end-time 不包含,即左闭右开 [start, end)。 2024 年 2 月这样写:

$ dosi query --model $DOSI_EXAMPLES/orders/model.yaml \
    --metrics revenue --start-time 2024-02-01 --end-time 2024-03-01 \
    --execute --db orders.db
revenue
230
1 row

group-by 里没有时间维度时,范围作用在每个指标的主时间上。 group-by 里有多个时间维度时,Dosi 不替你选:

$ dosi query --model $DOSI_EXAMPLES/orders/model.yaml \
    --metrics revenue \
    --group-by orders.order_date:month,customers.signup_date:month \
    --start-time 2024-02-01 --end-time 2024-03-01
error: TimeRangeNeedsDimension: the group-by contains 2 time dimensions; name the one the time range applies to
  candidates: orders.order_date, customers.signup_date
  hint: pass time_dimension (e.g. --time-dimension orders.order_date, or metric_time)

加上 --time-dimension orders.order_date 就能跑。

过滤

--where 写 SQL,Dosi 按 AND 拆开,每个条件根据它读的内容决定位置:

  • 条件读的是字段:过滤输入行,在聚合之前;
  • 条件读的是已选的指标:过滤结果行,在聚合之后(相当于手写的 HAVING)。

有一个例外:查询选了窗口指标时,只读 group-by 字段的条件也过滤结果行, 保留下来的行仍是在完整总体上算出的排名和分区计数。想在缩小后的范围内排名,把条件写进 --context-filter。

一个 --where 可以两种都有:

$ dosi query --model $DOSI_EXAMPLES/orders/model.yaml \
    --metrics revenue,order_count --group-by customers.region \
    --where "orders.status = 'completed' AND revenue > 100" \
    --execute --db orders.db
region  revenue  order_count
east    270      3
1 row

去掉 --execute、加上 --pretty 就能看到落点:orders.status 在内层 WHERE, revenue > 100 在外层对结果行的 WHERE。查询里某一部分读不到的条件不会悄悄丢掉, 而是作为警告返回(打在 stderr 上;用 --format json 时在 warnings 里)。

--context-filter 是另一个过滤槽位:它始终限定参与计算的总体, 包括固定粒度字段的输入。什么时候需要它,见 LOD 过滤。 完整的落点规则见语义契约 §8。

排序与限行

--order 写逗号分隔的排序键,前缀 - 表示降序。排序键只能是指标或 group-by 项, 可以写输出列名(order_date__month),也可以照抄 --group-by 里的写法 (orders.order_date:month)。其他名字会被拒绝,并列出可用的键:

$ dosi query --model $DOSI_EXAMPLES/orders/model.yaml \
    --metrics revenue --order -amount
error: UnknownOrderKey: order key "amount" is not a metric or group-by item
  candidates: revenue

NULL 在每个数仓上都排在最后,升序降序都一样; --order "-revenue nulls first" 让 NULL 排在最前。--limit 在排序之后截取行数, 所以 --order -revenue --limit 10 就是前 10 名。

编译还是执行

不加 --execute 时,dosi query 只打印 SQL。用来检查查询,或者把 SQL 交给别的工具:

参数 作用
--pretty 格式化 SQL
--dialect <name> 编译成另一个数仓的 SQL,模型不用改
--explain 在 SQL 上方打印逻辑计划:选了哪些连接、哪些聚合
--execute 执行并打印结果行
--connection <name> 在连接配置文件里的某个配置上执行(见连接数仓)
--format json 机器可读输出:sql、outputs、warnings,执行时还有 rows
$ dosi query --model $DOSI_EXAMPLES/orders/model.yaml \
    --metrics revenue --group-by metric_time:month \
    --start-time 2024-01-01 --end-time 2024-03-01 \
    --dialect snowflake --pretty
SELECT
  DATE_TRUNC('MONTH', orders.order_date) AS metric_time__month,
  SUM(orders.amount) AS revenue
FROM orders AS orders
WHERE
  orders.order_date >= CAST('2024-01-01' AS DATE)
  AND orders.order_date < CAST('2024-03-01' AS DATE)
GROUP BY
  DATE_TRUNC('MONTH', orders.order_date)

不写 --dialect 时,Dosi 按默认连接的方言编译(没有默认连接时用 DuckDB),所以检查过的 SQL 就是 --execute 实际执行的 SQL。

Dosi 何时拒绝

Dosi 不返回它无法担保的数字。最重要的拒绝是 fan_out_risk: 指标会跨一个让行重复的连接求和。在自带的 tpcds 模型里, store_productivity 汇总的是按门店的值,而一个门店并不对应唯一的日期:

$ dosi query --model $DOSI_EXAMPLES/tpcds/model.yaml \
    --metrics total_sales,store_productivity --group-by date_dim.d_date:month
error: FanOutRisk: metric "store_productivity" aggregates Sum over dataset "store", which has no many→one path to ["date_dim"]; evaluating a duplicate-sensitive aggregate (SUM/AVG/COUNT) across that join would double-count rows
  hint: group by dimensions reachable from the metric's dataset, or restate the metric with a duplicate-insensitive aggregate (COUNT DISTINCT/MIN/MAX)

手写这个连接也能得到一个数字,但它是错的。改成按指标所在数据集能到达的维度分组, 或者把两个指标分开查。

每次拒绝都带机器可读的 code、涉及的名字、名字写错时的 candidates, 以及 suggested_retry;加 --format json 就能拿到 JSON。常见的几种:

错误码 含义
unknown_metric、unknown_dimension、unknown_field 模型里没有这个名字;candidates 列出有的
unknown_order_key 排序键既不是指标,也不是 group-by 项
time_range_needs_dimension 有多个时间维度;用 --time-dimension 指明
fan_out_risk 结果会重复计数;换分组,或者拆开查

完整列表见错误码。

CLI 之外

同一个指标查询在各个接口上字段相同、语义相同、错误也相同:

  • REST API:POST /v1/query/compile、 /v1/query/explain、/v1/query/execute
  • MCP 服务:工具 compile_sql、explain_query、run_query
  • Python:dosi-engine wheel 里的 Engine.compile() 和 Engine.execute()

另见

  • CLI 参考:dosi query 的全部参数
  • 语义契约:连接(§5)、扇出保护(§6)、时间(§7)、过滤(§8)、排序(§13)
  • 参数化指标:--param 绑定
  • 理解 LOD:在一个粒度上计算,在另一个粒度上展示