Sundial
Semantic Layer

Measures

The numbers you query — aggregations and the calculations derived from them.

A measure is a queryable number defined on a semantic model. Measures unify simple aggregations (sums, counts, distinct counts) and derived KPIs (ratios, growth rates) into a single concept — there is no separate "metric". Measure names are unique per tenant, so you reference a measure by its bare name and the layer resolves which model backs it.

Two types

measure_typeWhat it is
aggregateApplies an aggregation function to a column or expression. Also carries cumulative windows and semi-additive behavior.
calculatedAn arithmetic expression over other measures (ratios, growth rates). Has no agg of its own.

Shared required fields: name, measure_type, description. label is optional.

Aggregate measures

measures:
  - name: order_total
    measure_type: aggregate
    agg: sum
    sql: order_amount
    description: Sum of order amounts.
  - name: distinct_buyers
    measure_type: aggregate
    agg: count_distinct
    sql: user_id
    description: Distinct users who placed an order.
FieldRequiredNotes
aggYes (aggregate)One of sum, count, count_distinct, count_distinct_approx, hll_sketch, avg, min, max, median, percentile, stddev, variance.
sqlNo*Column or SQL expression to aggregate. Defaults to the measure name when omitted. *Required for hll_sketch and count_distinct_approx.
filterNoA SQL predicate applied before aggregation, e.g. status = 'completed'. Plain SQL only (no Jinja, no subqueries). Use dotted model.dimension to filter on a joined model.
override_time_dimensionNoAggregate this measure over a different time dimension than the model's default_time_dimension.

Filtered variants

- name: completed_revenue
  measure_type: aggregate
  agg: sum
  sql: order_amount
  filter: status = 'completed'
  description: Revenue from completed orders only.

Cumulative measures

Still measure_type: aggregate — these are modifiers, not a separate type. grain_to_date and rolling_window are mutually exclusive.

FieldNotes
grain_to_dateAccumulates from the start of the named period. One of day, week, month, quarter, year (e.g. month → month-to-date).
rolling_windowTrailing window, format "<N> <unit>" where unit is days, weeks, months, quarters, or years (e.g. "7 days").
- name: mtd_revenue
  measure_type: aggregate
  agg: sum
  sql: order_amount
  grain_to_date: month
  description: Month-to-date revenue.

- name: rolling_7d_buyers
  measure_type: aggregate
  agg: count_distinct
  sql: user_id
  rolling_window: "7 days"
  description: Distinct buyers in the trailing 7-day window.

Semi-additive (snapshot) measures

A measure is semi-additive when the same entity appears in multiple rows over time (daily balances, subscriber snapshots, rolling active-user tables), so summing across time would double-count. Declare it with:

FieldNotes
non_additive_dimThe time dimension the measure must not be summed across. Must name a time dimension on the same model.
non_additive_windowmax keeps rows at the latest value of that dimension (period-end / as-of-now — the common case); min keeps the earliest. Only max and min are valid.
non_additive_groupingsOptional entity columns to partition by when collapsing (e.g. [account_id]).
- name: account_balance
  measure_type: aggregate
  agg: sum
  sql: balance
  non_additive_dim: snapshot_date
  non_additive_window: max
  non_additive_groupings: [account_id]
  description: Latest balance per account, summable across accounts.

Valid with sum / avg-style aggregations, not with count. If a count double-counts, switch it to count_distinct, which deduplicates natively and needs no non_additive_dim.

Calculated measures

A calculated measure computes a value from other measures via derived_sql, which is required for measure_type: calculated (and it takes no agg of its own). Reference other measures by their bare names — or dotted model.measure for an explicit cross-model reference — and guard divisions with NULLIF; there is no automatic null protection. When an operand needs to be named explicitly or shifted in time, declare it with inputs and operand_offsets.

- name: avg_order_value
  measure_type: calculated
  derived_sql: order_total / NULLIF(order_count, 0)
  description: Average revenue per order.

Cross-model calculations join via the shared-dimension mechanism described in Semantic model → Joins:

- name: margin
  measure_type: calculated
  derived_sql: orders.revenue / NULLIF(inventory.cost, 0)
  description: Revenue divided by cost of goods.

Inputs and time-shifted operands

When a calculated measure combines operands that are offset in time, name each operand with inputs and shift it with operand_offsets:

FieldNotes
inputs[].aliasName used in derived_sql for this operand.
inputs[].measureThe measure to bind — a bare name (no dots).
inputs[].filterOptional filter applied to that operand.
operand_offsets.<measure>.value / .unit / .directionShift an operand by value × unit (dayyear), direction lag (backward) or lead (forward). The three go together.
operand_offsets.<measure>.to_grainOptionally align the join key to a coarser grain first.
- name: completion_ratio
  measure_type: calculated
  derived_sql: completed / NULLIF(started, 0)
  operand_offsets:
    started:
      value: 7
      unit: day
      direction: lag
  description: Completed relative to started, with the started operand shifted back 7 days.

Pre-aggregation note

Aggregate measures can be accelerated with pre-aggregations. Calculated measures are not pre-agg-eligible — pre-aggregate their component measures and the calculation is derived from them. count_distinct and rolling-window measures are not re-aggregatable across grains, so each queried grain needs its own pre-aggregation.


Still have questions?

On this page