Growth compares a period with an earlier one: (this period − earlier period) / earlier period. As a derived
metric it uses the same input metric twice, once as it is and once shifted with offset_window.
Copy and paste
metrics:
- name: revenue_mom_growth
label: Revenue growth vs last month
type: derived
expr: (revenue - revenue_lm) / revenue_lm
input_metrics:
- name: revenue
- name: revenue
alias: revenue_lm
offset_window: 1 month
- name: revenue_yoy_growth
label: Revenue growth vs same month last year
type: derived
expr: (revenue - revenue_ly) / revenue_ly
input_metrics:
- name: revenue
- name: revenue
alias: revenue_ly
offset_window: 1 year
mf query --metrics revenue,revenue_mom_growth,revenue_yoy_growth --group-by metric_time__month --start-time 2025-12-01
Example
| Month | revenue | revenue_mom_growth | revenue_yoy_growth |
|---|---|---|---|
| 2025-12 | 200 | 0.333 | |
| 2026-01 | 110 | −0.450 | 0.100 |
| 2026-02 | 130 | 0.182 | 0.083 |
| 2026-03 | 100 | −0.231 | 0.111 |
| 2026-04 | 80 | −0.200 | −0.273 |
| 2026-05 | 470 | 4.875 | 2.615 |
The results are ratios: 0.1 is 10%. Format them as percentages in the BI tool rather than multiplying by
100 in expr, so that later calculations keep the precision.
December 2025 has no year-over-year value because the data starts in January 2025. May 2026 is partial in real life; month-over-month and year-over-year growth for the current month compare a few days with a full month, so show them for complete months only, or use YTD growth for the current period.
When the earlier period is empty or zero
- No data in the earlier period. September 2025 has no month-over-month value: August had no orders,
so
revenue_lmis NULL and the division returns NULL. That is the honest answer: growth from nothing is not a percentage. - A zero base. If the earlier value can be exactly 0 (for example after
fill_nulls_with: 0), some warehouses raise a division-by-zero error and others return NULL or infinity. Guard it inexpr:
expr: (revenue - revenue_lm) / nullif(revenue_lm, 0)
The rules of offsets
- The aliased input is the one that is shifted;
exprrefers to aliases, so the same metric can appear twice. - The query must group by
metric_timeat some grain; without it MetricFlow refuses offset metrics. - The shift happens on days, then the result is aggregated to the query’s grain. A 1-month offset of daily data grouped by month compares calendar months, whatever their length.
For the absolute value of last year alongside this year’s, see how to compare a metric with the prior year.
Tested with dbt Core 1.12 and MetricFlow 0.213 on DuckDB.