运行你的第一个指标查询¶
这篇教程拿一份现成的语义模型,浏览它的指标,把其中一个编译成 SQL, 再在真实数据库上跑一遍,看数字如期出来。走完就理解了 Dosi 的完整闭环: 模型 → 查询 → SQL → 结果。
约 10 分钟,不需要 OSI 基础。
前置条件
已经安装好 Dosi,且 dosi validate --model
fixtures/orders/model.yaml 能打印出 ✓ 1 semantic model(s) valid。
下面的命令都在仓库根目录执行。(dosi 不在 PATH 里的话,
请读作 ./target/release/dosi。)
用的是内置的 orders 模型:一个很小的电商示例,三张表
(orders、customers、products)、五个指标。它刻意做得小,方便手工核对每个数字。
第 1 步 —— 看看模型里有什么¶
先看这份模型定义了哪些指标:
$ dosi list metrics --model fixtures/orders/model.yaml
NAME KIND DATASETS DESCRIPTION
revenue aggregate orders Total order amount
order_count aggregate orders Number of orders
unique_customers aggregate orders Distinct purchasing customers
avg_order_value ratio orders Revenue per order (ratio)
total_margin expression orders, products Revenue minus cost (expression over two aggregates)
每个指标都带一个推断出来的种类(kind):
- aggregate(聚合型):单个聚合,比如
SUM(orders.amount)。 - ratio(比率型):一个聚合除以另一个聚合,如
avg_order_value。 - expression(表达式型):多个聚合参与的算术运算,可以跨表,
如
total_margin横跨orders和products。
种类不用声明,Dosi 从每个指标的 SQL 里推断出来。
第 2 步 —— 确认模型是有效的¶
validate 检查结构、名称唯一性、关系是否指向真实存在的列,以及每个指标表达式
能否编译。这就是该放进 CI 的那道关卡,能在坏模型上线前拦住它。
第 3 步 —— 看看能按什么分组¶
指标回答"多少",字段负责把它拆开看。列出模型暴露的所有字段, 时间字段会带标记:
$ dosi list dimensions --model fixtures/orders/model.yaml
NAME TIME DESCRIPTION
orders.order_id
orders.customer_id
orders.product_id
orders.order_date time
orders.status
orders.amount
customers.customer_id
customers.region
customers.signup_date time
products.product_id
products.category
products.unit_cost
带 time 标记的是时间维度,这里有 orders.order_date 和
customers.signup_date,查询时可以按天、周、月、季、年分桶。
下面先按 orders.status 分组,再按月看一次。
第 4 步 —— 把第一条查询编译成 SQL¶
取 revenue,按订单状态拆开。不加 --execute,Dosi 只打印将要执行的 SQL;
加 --pretty 可以格式化输出(默认方言 DuckDB):
$ dosi query --model fixtures/orders/model.yaml \
--metrics revenue --group-by orders.status --pretty
SELECT
orders.status AS status,
SUM(orders.amount) AS revenue
FROM main.orders AS orders
GROUP BY
orders.status
只写了一个指标名和一个维度,聚合和 GROUP BY 都由 Dosi 生成。
第 5 步 —— 换数仓,但不改模型¶
这里才是语义引擎真正体现价值的地方。取按月的营收,
也就是在时间维度上用 :month 粒度,先为 DuckDB 编译:
$ dosi query --model fixtures/orders/model.yaml \
--metrics revenue --group-by orders.order_date:month --pretty --dialect duckdb
SELECT
DATE_TRUNC('MONTH', orders.order_date) AS order_date__month,
SUM(orders.amount) AS revenue
FROM main.orders AS orders
GROUP BY
DATE_TRUNC('MONTH', orders.order_date)
再把完全相同的请求编译给 MySQL,只有 --dialect 变了:
$ dosi query --model fixtures/orders/model.yaml \
--metrics revenue --group-by orders.order_date:month --pretty --dialect mysql
SELECT
STR_TO_DATE(DATE_FORMAT(orders.order_date, '%Y-%m-01'), '%Y-%m-%d') AS order_date__month,
SUM(orders.amount) AS revenue
FROM main.orders AS orders
GROUP BY
STR_TO_DATE(DATE_FORMAT(orders.order_date, '%Y-%m-01'), '%Y-%m-%d')
MySQL 没有 DATE_TRUNC,Dosi 就换成 MySQL 确实支持的写法来分桶,
而切换数仓一个字都没动模型。一份定义,在每个数仓上都是正确的 SQL。
两种情况下输出列都叫 order_date__month,即 {field}__{grain}。
第 6 步 —— 真跑一次¶
这次真连数据库跑。先用内置的种子脚本把示例数据灌进一个本地 DuckDB 文件:
然后给第 4 步那条"按状态看营收"加上 --execute,指向这个文件:
$ dosi query --model fixtures/orders/model.yaml \
--metrics revenue --group-by orders.status \
--execute --db orders.db
status revenue
completed 350
cancelled 100
2 rows
手工核对:种子数据里已完成的订单是 100 + 50 + 80 + 120 = 350, 已取消的是 30 + 70 = 100。引擎给的数字完全对得上。
第 7 步 —— 按时间拆开看¶
把第 5 步那条按月的查询(:month 粒度)也真跑一次,
加上 --order order_date__month 按分桶排序:
$ dosi query --model fixtures/orders/model.yaml \
--metrics revenue --group-by orders.order_date:month \
--execute --db orders.db --order order_date__month
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
1 月 = 100 + 50 = 150,2 月 = 80 + 30 + 120 = 230,3 月 = 70。 又对上了,而且没写一行日期相关的 SQL 就拿到了按月分桶。
第 8 步 —— 试一个比率型指标¶
最后看一个聚合除以聚合的指标:全部六笔订单的平均订单金额。
$ dosi query --model fixtures/orders/model.yaml \
--metrics avg_order_value --execute --db orders.db
avg_order_value
75
1 row
总营收 450 ÷ 6 笔订单 = 75。注意 Dosi 是按真正的比率算的, 还防住了整数除法,而不是对每行的值取平均。
你学到了什么¶
Dosi 的完整闭环已经端到端跑通:
- 浏览模型的指标和维度(
list)。 - 校验模型(
validate)。 - 把指标编译成 SQL,再用一个参数重定向到另一个数仓
(
query、--dialect)。 - 执行并手工核对结果(
--execute)。
每个数字都和源数据对得上 —— 指标只定义一次,剩下的交给引擎。