Measures
Standard, rolling, and derived measures, and why additivity is the key to correct aggregation.
A measure is a value a contract lets callers aggregate. Gaur has three
kinds, set by a behavior field: standard, rolling, and derived.
Picking the right kind, and declaring additivity honestly, is what keeps a
contract's numbers correct.
Standard measures
A standard measure is a single aggregate expression. It's the default kind;
behavior can be omitted or set to "standard".
"revenue_usd": {
"sql": "sum(p.price_usd)",
"type": "number",
"additivity": "additive"
}Fields:
sql: the aggregate expression, likesum(p.price_usd),count(*), oravg(o.order_total_usd).type: the value type, one ofnumber,boolean,timestamp,date.additivity: how the measure behaves when rolled up (see below).description: optional explanation.
A standard measure has to be a single aggregate. An expression that aggregates an already-aggregated value, like a ratio of two sums, is not a standard measure. Use a derived measure for that.
Additivity
Additivity tells Gaur whether a measure can be summed across the rows of a group. Declaring it correctly is essential. It's how Gaur knows which roll-ups are safe.
| Additivity | Meaning | Examples |
|---|---|---|
additive | Safe to sum across any dimension. | revenue_usd, units_sold, order_count |
semi_additive | Safe to sum across some dimensions but not time. It's a snapshot at a point in time. | account balance, inventory on hand |
non_additive | Can't be summed across groups at all. | ratios, averages, count(distinct customer) |
A semi_additive measure also declares a semi_additive_grain, the dimension
at which the snapshot is taken.
Additivity is a promise about your data. If you mark an average as additive,
Gaur will happily sum averages and hand back a meaningless number. Declare
each measure honestly.
Rolling measures
A rolling measure aggregates a base measure over a moving time window: a 7-day rolling total, a 4-week rolling average of an underlying additive measure.
"revenue_usd_trailing_7d": {
"behavior": "rolling",
"type": "number",
"base_measure": "revenue_usd",
"window": "7d",
"order_by": "ordered_at"
}Fields:
behavior:"rolling".type:number.base_measure: the name of another measure in the same contract to roll.window: the window size, a number followed byd(days),w(weeks), orm(months), like7d,4w,3m.order_by: the time dimension the window moves along.
Two rules apply:
- The base measure must be additive. Rolling a non-additive measure, like an average or a ratio, over a window produces a meaningless number, so it's rejected.
- The window must match the query's time grain. A
7drolling measure makes sense at adaygrain. Requesting it at amonthgrain is rejected, because a 7-day window doesn't map onto monthly buckets. See Rules and limitations.
Derived measures
A derived measure combines two other measures after they're aggregated. Its main job is ratios: average order value, conversion rate, cost per unit.
"cogs_ratio": {
"behavior": "derived",
"type": "number",
"numerator": "total_cogs_usd",
"denominator": "revenue_usd"
}Fields:
behavior:"derived".numerator: the name of the measure used as the numerator.denominator: the name of the measure used as the denominator.type:number(optional; derived measures are numeric).
In product_performance, cogs_ratio ends up between 0 and 1 (share of
revenue consumed by COGS). The caller can then derive gross margin as
1 - cogs_ratio on the client, or pull the two underlying measures and
subtract.
Gaur computes a derived measure by aggregating the numerator and denominator separately, then dividing, for every group in the result. That's the only correct way to express a ratio in a contract.
Don't write a ratio as a standard measure like
"sql": "sum(o.subtotal_usd) / count(o.id)". Aggregating an aggregate inside
a standard measure is rejected. Express ratios as derived measures so they're
computed correctly at every grain.
Why ratios can't be standard measures
A ratio of sums isn't the sum of ratios. If you compute cogs / revenue row
by row and then add up the results, you get a number that means nothing. A
derived measure avoids this: it sums total_cogs_usd, sums revenue_usd,
and divides once at the correct grain, so the ratio is right whether the
caller groups by day, by month, or not at all.
Filtering on measures
Callers can filter on measures the same way they filter on dimensions. Gaur
compiles measure filters as HAVING clauses, applied after aggregation:
{
"contract_name": "product_performance",
"query": {
"dimensions": [{ "name": "sku" }],
"measures": ["revenue_usd"],
"filters": [{ "field": "revenue_usd", "operator": "gt", "value": 1000 }]
}
}This returns only SKUs whose revenue_usd exceeds 1000 in the selected
group. No separate having field is needed. See the
query schema for the full filter
vocabulary.
Choosing a measure kind
Standard
A single aggregate of raw data: a sum, a count, an average over rows.
Rolling
An additive measure aggregated over a moving time window.
Derived
One measure divided by another, computed after aggregation. Ratios.
Next
Continue to Rules and limitations to see how measures interact with joins, and which combinations Gaur rejects.