跳转至

明细查询

同一份语义模型,Dosi 回答两类问题。

问题 命令 返回
上个月各地区的收入是多少? dosi query 聚合后的指标
列出 Enterprise 客户最大的 100 笔订单 dosi select 明细行

两者走同一个计划器、同一张关系图、同一套方言后端,只差一个算子: query 做聚合,select 做投影。

$ dosi select \
    --from orders \
    --fields orders.order_id,orders.amount,customers.name \
    --where "customers.tier = 'Enterprise'" \
    --order -amount \
    --limit 100

不用写 JOIN。只要点名想要哪些字段,Dosi 自己判断它们落在哪些数据集上、 哪些关系把它们连起来,以及这样连是否安全。

--from 定义粒度,不是 SQL 的 FROM

--from orders 的含义是一行结果对应一个订单,不是"SQL 里只能出现 orders"。

字段、过滤、排序、时间范围都可以引用别的数据集。Dosi 收集这次查询需要的 全部数据集,为每一个找到关系路径,然后生成连接:

$ dosi select --from orders --fields orders.order_id --where "customers.region = 'US'"

customers 一个字段都没投影,但过滤需要它,于是照样连上。结果仍是一行 一个订单。

这一点成立,是因为 Dosi 只沿一个方向走关系:从多的一侧走向一的一侧。 每一跳最多给已有的每一行附上目标表的一行,所以整条路径走完,要的粒度 依然不变。

说得再直白些:一笔订单只对应一个客户,所以把 customers.name 拉过来,是 给这一行加一列,不会给结果加一行。它是一次查找,就像表格里的 VLOOKUP 只填出一列、不改变表有多少行。

反方向才是手写 SQL 悄悄出错的地方。一个客户有多笔订单,从 customers 往 orders 连,一个客户会被拆成好几行,这时对任何客户级的列求 SUM, 这个客户就会按订单数被重复计入。这就是经典的重复计数,而且不报错:查询 照常成功,只是数字是错的。Dosi 不会生成这种 SQL:在 --from customers 下要订单上的字段,查询会被 detail_fanout 拒绝,并把真正能回答这个问题的粒度 递回来(--from orders),而不是给你一份重复的结果。

字段的三种写法

写法 例子 什么时候用
裸字段 amount 该名字在模型里唯一
数据集限定 orders.amount 常规写法
关系前缀 orders_to_buyer.name 需要自己指定连接路径

第三种在模型能用多条路径抵达同一数据集时才用得上。假设 orders 到 regions 既能直连(发货地),也能经 customers(买家所在地)。这时问 regions.region_name 确实有歧义,Dosi 会说出来,而不是替你挑一条:

$ dosi select --from orders --fields regions.region_name --format json
{
  "code": "ambiguous_join_path",
  "candidates": [
    "orders -[orders_to_customers]-> customers, customers -[customers_to_regions]-> regions",
    "orders -[orders_to_ship_region]-> regions"
  ],
  "suggested_retry": "name the path you mean by prefixing the field with its relationships, one of: orders_to_customers.customers_to_regions.<field> | orders_to_ship_region.<field>"
}

把其中一个前缀贴回去,查询就能跑:

$ dosi select --from orders --fields orders.order_id,orders_to_ship_region.region_name

关系前缀在 --where 里同样可用,路径多长都行;指标平面的 --group-by 也接受 —— 到处是同一种写法。

输出列名

输出列名 = 字段引用去掉根前缀,剩下的每个 . 换成 __:

引用(根为 orders) 列名
orders.order_id order_id
customers.name customers__name
orders_to_ship_region.region_name orders_to_ship_region__region_name
orders.created_at:month created_at__month

根字段保持裸名,因为问的就是它;其余都带上来源,这样两个数据集各有一个 name 时不会悄悄撞在一起。两个字段会产生同一个列名时报 duplicate_output_name。

什么情况下拒绝

引擎绝不返回自己无法担保的行。有三种拒绝值得认识。

detail_fanout —— 要的数据集只能逆着连接方向抵达。一个客户有多笔 订单,所以从 --from customers 投影订单字段会把一个客户变成好几行:

$ dosi select --from customers --fields customers.name,orders.order_id --format json
{
  "code": "detail_fanout",
  "suggested_retry": "query at the finer grain instead: --from orders and project the customers fields you need"
}

改法就写在提示里:换到更细的粒度去问。--from orders --fields orders.order_id,customers.name 给出同样的信息,一行一个订单,不重复。

ambiguous_join_path —— 多条路径都能抵达该字段。按上面的办法点名一条。

metric_in_detail_query —— 指标需要聚合、时间范围和分组,明细查询 这三样都没有。提示里直接给出该用哪个命令。

unknown_query_key —— 查询带了它没有定义的键,通常是拼写错误。直接 拒绝而不是忽略,因为被忽略的 distinct 或筛选条件会返回一份看起来合理、 回答的却是另一个问题的结果。candidates 列出查询定义的键。

每个拒绝都带机器可读的 code、涉及的名字,以及 suggested_retry。 参见错误码。

时间范围

--start-time / --end-time 给出半开区间 [start, end)。不写 --time-dimension 时,范围落到根数据集的主时间维度上:

$ dosi select \
    --from orders \
    --fields orders.order_id,orders.amount \
    --start-time 2026-01-01 --end-time 2026-02-01

根数据集没有主时间维度、投影里也没有唯一的时间字段时,Dosi 会要求点名 一个,而不是猜。

固定粒度字段

明细查询可以投影、筛选和排序根数据集上的固定 LOD 字段,包括基础指标读取其他数据集的字段。 用 --from 指定该承载数据集;当前不支持读取关联数据集上的 LOD 字段。 结果保留根记录的行数关系,--distinct 只对投影结果去重。

--param NAME=VALUE 绑定字段 base 的参数。字段只接受单值, 没有接收者的参数返回 unknown_metric_param。 上下文/时间范围与普通筛选对固定计算的影响不同。每次汇总都在自己的数据集上读 时间范围和上下文,读不到的条件在结果的 warnings 里说明。完整规则见 LOD 查询接口、过滤范围 和独立窗口。可运行的完整明细查询放在独立的 电商示例中。

重复行

明细查询的一行对应根数据集的一行,所以从事实表上读出的 ID 名单,每个 ID 会按事实行重复出现:登录表上是一次登录一行,而不是一个玩家一行。 --distinct(其他接口面写 "distinct": true)会去掉投影值完全相同的行, 即 SELECT DISTINCT:

$ dosi select --from logins --fields logins.player_id --distinct

它只合并行,不改变有哪些行,所以一对多的遍历仍然直接拒绝,而不是先放大再去重。

行数上限

--limit 默认 100,上限截到 10000。四个接口面都是这个规则, 所以 Agent 忘了写 limit 只会拿到一份样本,不会扫全表。

CLI 之外

同一个 DetailQuery 通到每个接口面,语义一致、错误一致。

$ curl -s localhost:8080/v1/select/execute \
    -H 'content-type: application/json' \
    -d '{"from":"orders","fields":["orders.order_id","customers.name"],"limit":10}'

还有 POST /v1/select/compile 与 POST /v1/select/explain, 见 REST API。

工具 select、compile_select、explain_select, 见 MCP 服务。

engine.select({"from": "orders", "fields": ["orders.order_id"], "limit": 10})

信任结果之前先看连接

--explain 打印逻辑计划:根数据集、选中的每一条连接,以及投影。

$ dosi select --from orders --fields orders.order_id,customers.name --explain
logical plan
Project[order_id, customers__name]
  JoinRelated[orders_to_customers -> customers, left]
    ReadDataset[orders]

连接默认 LEFT,所以匹配不上的行会带着 NULL 保留下来,不会凭空消失。 关系可以改成 INNER,见控制 JOIN 与空值填充。

本版本的限制

  • 一次查询抵达每个数据集只能走一条路径。两条命名路径指向同一数据集 需要两个表别名,目前还没有,所以引擎宁可拒绝,也不把同一个连接读两遍。
  • 排序键必须是已投影的列。想按哪个字段排序,就把它投影出来。
  • 指标和字段不能混在一次查询里。分别查两个平面,结果自己拼。按实体汇总的值是 例外:把它声明成固定粒度字段,就能在这里读。
  • 固定粒度字段必须在 --from 数据集上。

另请参阅