跳转至

明细查询

同一份语义模型,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 只沿一个方向走关系:从多的一侧走向一的一侧。 每一跳最多给已有的每一行附上目标表的一行,所以整条路径走完,要的粒度 依然不变。

字段的三种写法

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

第三种在模型能用多条路径抵达同一数据集时才用得上。假设 ordersregions 既能直连(发货地),也能经 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 —— 指标需要聚合、时间范围和分组,明细查询 这三样都没有。提示里直接给出该用哪个命令。

每个拒绝都带机器可读的 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 会要求点名 一个,而不是猜。

行数上限

--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/compilePOST /v1/select/explain, 见 REST API

工具 selectcompile_selectexplain_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 与空值填充

本版本的限制

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

另请参阅