Query metrics¶
A metric query names metrics and the dimensions to break them down by; Dosi
writes the aggregation, the joins and the GROUP BY. For row-level records
instead of aggregates, see Query detail rows.
$ 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
You never write a JOIN: revenue lives on orders, region on
customers, and Dosi follows the relationship between them. The examples on
this page use the bundled orders model and the orders.db file loaded in
the tutorial.
Pick metrics and dimensions¶
--metrics takes metric names; --group-by takes dataset.field items.
Ask the model what it has before guessing:
$ 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 lists the datasets a metric can be broken down by, and time the
grains it accepts. A misspelled name is refused with the names that do exist:
$ 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
Time grain and metric_time¶
Append :grain to a time dimension to bucket it at that
time grain: day, week, month,
quarter or year. The output column is named {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 stands for each metric's primary time dimension, so you can
ask for "revenue by month" without knowing which column holds the date. The
column is then metric_time__month. A metric's primary time comes from the
D-TIME extension, or from its dataset's only
time field.
Time ranges¶
--start-time is inclusive and --end-time exclusive, so a range is
half-open [start, end). February 2024 is:
$ 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
With no time dimension in the group-by, the range applies to each metric's primary time. When the group-by holds several time dimensions, Dosi won't pick one for you:
$ 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)
Add --time-dimension orders.order_date and it runs.
Filters¶
--where takes SQL, and Dosi places each AND conjunct by what it reads:
- a condition on fields filters input rows, before aggregation;
- a condition on a selected metric filters result rows, after
aggregation (the
HAVINGyou would otherwise write).
One exception: in a query that selects a window metric,
a condition on group-by fields only also filters result rows, so the rows
kept keep the ranks and partition counts the whole population gave them. To
rank within the narrower set instead, put the condition in --context-filter.
One --where can do both:
$ 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
Add --pretty without --execute to see the placement: orders.status
lands in the inner WHERE, revenue > 100 in a WHERE over the finished
rows. A condition some part of the query cannot read is not dropped silently
— it comes back as a warning (on stderr, or under warnings with --format
json).
--context-filter is the other filter slot: it always scopes the
calculation population, including the inputs of level-of-detail fields. See
LOD filters for when you need it. The full
placement rules are in Semantics contract §8.
Order and limit¶
--order takes comma-separated keys; a - prefix sorts descending. A key
names a metric or a group-by item — by its output column (order_date__month)
or by the spelling you used in --group-by (orders.order_date:month).
Anything else is refused with the keys you can use:
$ 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
NULLs sort last on every warehouse, ascending and descending alike;
--order "-revenue nulls first" puts them first. --limit caps the rows
after ordering, so --order -revenue --limit 10 is a top 10.
Compile or execute¶
Without --execute, dosi query only prints the SQL. That is the way to
check a query, or to hand the SQL to another tool:
| Flag | Effect |
|---|---|
--pretty |
format the SQL |
--dialect <name> |
compile for another warehouse — the model doesn't change |
--explain |
print the logical plan above the SQL: which joins and aggregations were chosen |
--execute |
run it and print the rows |
--connection <name> |
run it on a profile from your connections file (see Connect a warehouse) |
--format json |
machine-readable output: sql, outputs, warnings and, when executed, 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)
Without --dialect, Dosi compiles for your default connection (DuckDB when
there is none), so the SQL you check is the SQL --execute will run.
When Dosi refuses¶
Dosi never returns a number it cannot vouch for. The refusal that matters
most is fan_out_risk: the metric would be summed across a join that
repeats its rows. In the bundled tpcds model, store_productivity sums a
per-store value, and a store has no single date:
$ 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)
Written by hand, that join returns a number, and it is wrong. Group by something the metric's dataset reaches, or query the two metrics separately.
Every refusal carries a machine-readable code, the names involved,
candidates where a name was wrong, and a suggested_retry — run with
--format json to get them as JSON. The common ones:
| Code | Means |
|---|---|
unknown_metric, unknown_dimension, unknown_field |
a name the model doesn't define; candidates lists the ones it does |
unknown_order_key |
the order key is not a metric or group-by item |
time_range_needs_dimension |
several time dimensions in scope; pass --time-dimension |
fan_out_risk |
the result would double-count; regroup or split the query |
The full list is in Errors.
Beyond the CLI¶
The same metric query — same fields, same semantics, same errors — runs on every surface:
- REST API:
POST /v1/query/compile,/v1/query/explain,/v1/query/execute - MCP server: tools
compile_sql,explain_query,run_query - Python:
Engine.compile()andEngine.execute()in thedosi-enginewheel
See also¶
- CLI reference — every
dosi queryflag - Semantics contract — joins (§5), fan-out protection (§6), time (§7), filters (§8), ordering (§13)
- Parameterized metrics —
--parambindings - Understand LOD — calculate at one grain, display at another