GaurGaur docs
Contracts

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, like sum(p.price_usd), count(*), or avg(o.order_total_usd).
  • type: the value type, one of number, 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.

AdditivityMeaningExamples
additiveSafe to sum across any dimension.revenue_usd, units_sold, order_count
semi_additiveSafe to sum across some dimensions but not time. It's a snapshot at a point in time.account balance, inventory on hand
non_additiveCan'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 by d (days), w (weeks), or m (months), like 7d, 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 7d rolling measure makes sense at a day grain. Requesting it at a month grain 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.

On this page