Advanced semantic modeling
Calculated dimensions, filtered measures, rolling windows, derived metrics, period-over-period offsets, and multi-stage calculations.
Updated
You put the business logic in the semantic model. You do not put it in BI tools or in agent prompts.
Calculated dimensions
When an attribute needs SQL, not a direct column, use expr::
dimensions:
- name: order_size_tier
type: string
expr: "CASE WHEN amount >= 1000 THEN 'enterprise' WHEN amount >= 100 THEN 'standard' ELSE 'micro' END"
values: [enterprise, standard, micro]
Calculated dimensions compile inline in the generated query. You can group or filter on them like standard columns.
Filtered measures
A filtered measure applies conditions inside the aggregation with CASE WHEN ...:
measures:
- name: completed_revenue
expr: amount
agg: sum
filters:
- dimension: status
op: eq
value: completed
- name: refunded_amount
expr: amount
agg: sum
filters:
- dimension: status
op: in
value: [refunded, chargeback]
Consumers can query these slices next to other metrics in one query.
Rolling window measures
To compute trailing or cumulative windows, declare a rolling_window::
measures:
- name: revenue_trailing_30d
expr: amount
agg: sum
rolling_window:
trailing: 30 day
The compiler builds window frames on the selected time dimension.
Derived metrics
A metric can name another metric. The engine inlines the reference. It detects cycles:
metrics:
- name: gross_profit
expr: revenue - cost_of_goods_sold
- name: gross_margin_pct
expr: (gross_profit / revenue) * 100
description: Gross profit margin percentage.
Period-over-period and YoY
Use offset(expr, interval) to read one measure at an earlier period. After inlining, expr must be one bare measure.
metrics:
- name: revenue_prior_year
expr: offset(revenue, 1 year)
- name: revenue_yoy_growth
expr: (revenue - offset(revenue, 1 year)) / offset(revenue, 1 year)
- name: order_count_prior_month
expr: offset(order_count, 1 month)
Supported offset units: day, week, month, year.
Multi-stage metrics (avg_per, min_per, max_per)
Some calculations need two passes. First, aggregate per entity. Then aggregate across the cohort. Example: average customer revenue.
metrics:
- name: avg_revenue_per_customer
expr: avg_per(revenue, customer_id)
description: Calculates customer revenue totals first, then averages across all customers.
- name: max_revenue_per_status
expr: max_per(revenue, status)
The second name is a plain dimension on that measure’s own entity. Write it as a bare name. Do not qualify it with an entity. The engine isolates the first pass in a subquery. Then it computes the outer metric.
Percentiles and non-additive aggregations
For latency or distribution, use percentile aggregations:
measures:
- name: median_order_amount
expr: amount
agg: median
- name: p95_latency
expr: latency_ms
agg: p95
- name: p99_latency
expr: latency_ms
agg: p99
Supported aggregations: median, p90, p95, p99. Supported on PostgreSQL, Snowflake, BigQuery, and DuckDB.
Fiscal calendars
To align analytics with a fiscal year, set fiscal_year_start_month (1–12) on a time dimension:
dimensions:
- name: order_date
column: order_date
type: timestamp
fiscal_year_start_month: 2 # Fiscal year begins in February
grains: [day, month, quarter, year, fiscal_quarter, fiscal_year]
When a query asks for fiscal_quarter or fiscal_year, the compiler shifts calendar quarters and years by that offset.
Custom SQL entities (sql:)
If an entity cannot map to one table, back it with a SQL subquery:
entities:
- name: ActiveSubscription
primary_key: id
sql: |
SELECT id, user_id, plan_id, started_at, mrr
FROM billing.subscriptions
WHERE status = 'active' AND deleted_at IS NULL
dimensions:
- name: id
type: string
- name: started_at
type: timestamp
grains: [day, month, year]
measures:
- name: total_mrr
expr: mrr
agg: sum
Entity inheritance (extends:)
An entity can inherit dimensions and measures from a parent:
entities:
- name: BaseItem
primary_key: id
dimensions:
- name: id
type: string
- name: created_at
type: timestamp
grains: [day, month]
- name: PhysicalProduct
extends: BaseItem
table: warehouse.products
dimensions:
- name: weight_kg
type: decimal
Model drift resilience
When you rename or deprecate a field, keep stored queries working:
dimensions:
- name: region_code
column: region
renamed_from: [region, country_region] # Resolves legacy query names automatically
deprecated: false
metrics:
- name: old_gmv
expr: revenue
deprecated: true
deprecated_note: "Superseded by net_revenue. Scheduled for removal 2027-01-01."
renamed_from. Legacy queries that use the old name are rewritten to the canonical name.deprecated: true. Catalog discovery flags the item. Active queries still run.
Next: Query the REST API · Connect an LLM agent.