Learning Platform
Глоссарий Troubleshooting
Урок 15.03 · 30 мин
Средний
MetricFlowSimple metricRatio metricCumulative metric

Metric types: simple, ratio, cumulative

В предыдущем уроке мы видели simple metric (direct alias на measure) и derived metric (formula). В этом уроке детально разберём остальные типы: simple (revisited), ratio (для conversion rate, CTR), cumulative (для running totals, MTD/YTD, retention windows).

Каждый тип решает свой класс задач. Понимая их, вы можете описать любую бизнес-метрику.

Fact Tables: мера как основа для MetricFlow simple metric

Simple metric — direct measure

Самый простой тип. Прямое отображение measure -> metric.

# В semantic_model:
measures:
  - name: order_count
    agg: count
    expr: order_id

# В metrics:
metrics:
  - name: total_orders
    description: "Total number of orders"
    type: simple
    type_params:
      measure: order_count

Когда total_orders запрашивается:

client.query(metrics=['total_orders'], group_by=['metric_time__month'])

MetricFlow генерирует:

SELECT
  DATE_TRUNC('month', order_date) AS metric_time__month,
  COUNT(order_id) AS total_orders
FROM analytics.fct_orders
GROUP BY 1

Простая агрегация. Применяется когда метрика = одно измерение без сложных вычислений.

Use cases:

  • total_revenue = SUM(amount)
  • customer_count = COUNT(DISTINCT customer_id)
  • avg_order_value = AVG(amount)

Ratio metric — numerator / denominator

Когда метрика — это отношение. Conversion rate, click-through rate, refund rate.

# В semantic_model orders:
measures:
  - name: order_count
    agg: count
    expr: order_id
  - name: refunded_order_count
    agg: count
    expr: order_id
    agg_params:
      filter: "refund_amount > 0"     # boolean filter

# В metrics:
metrics:
  - name: refund_rate
    description: "Percent of orders that were refunded"
    type: ratio
    type_params:
      numerator: refunded_order_count
      denominator: order_count

Когда refund_rate запрашивается by month:

SELECT
  DATE_TRUNC('month', order_date) AS month,
  COUNT(CASE WHEN refund_amount > 0 THEN order_id END) * 1.0 / COUNT(order_id) AS refund_rate
FROM analytics.fct_orders
GROUP BY 1

* 1.0 важно — без него на integer denominator получим integer division (0 вместо 0.05).

Ratio с фильтрами

metrics:
  - name: paid_customer_conversion
    description: "% of trial users who became paid customers"
    type: ratio
    type_params:
      numerator:
        name: paid_customer_count
        filter: "subscription_status = 'paid'"
      denominator:
        name: customer_count
        filter: "signed_up_at IS NOT NULL"

Можно фильтровать numerator и denominator независимо. Это для сложных conversion funnels.

Ratio с different semantic_models

metrics:
  - name: revenue_per_customer
    description: "Average revenue per customer (LTV proxy)"
    type: ratio
    type_params:
      numerator: order_amount        # measure on `orders` semantic_model
      denominator: customer_count    # measure on `customers` semantic_model

MetricFlow знает что измерения на разных таблицах. Делает JOIN через entities -> агрегирует.

SELECT
  SUM(o.amount) AS numerator,
  COUNT(DISTINCT c.customer_id) AS denominator,
  SUM(o.amount) * 1.0 / COUNT(DISTINCT c.customer_id) AS revenue_per_customer
FROM analytics.fct_orders o
RIGHT JOIN analytics.dim_customers c
  ON o.customer_id = c.customer_id

Cumulative metric — running aggregate

Когда метрика — это накопительная сумма за окно. MTD revenue, YTD revenue, rolling 7-day active users, 30-day cohort retention.

# В metrics:
metrics:
  - name: revenue_mtd
    description: "Month-to-date revenue (cumulative within month)"
    type: cumulative
    type_params:
      measure: order_amount
      window: 1 month       # окно
      grain_to_date: month  # restart accumulation each month

При запросе:

client.query(
    metrics=['revenue_mtd'],
    group_by=['metric_time__day']
)

MetricFlow генерирует:

SELECT
  d.day,
  SUM(o.amount) OVER (
    PARTITION BY DATE_TRUNC('month', d.day)
    ORDER BY d.day
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS revenue_mtd
FROM (
  SELECT DISTINCT DATE_TRUNC('day', order_date) AS day
  FROM analytics.fct_orders
) d
LEFT JOIN analytics.fct_orders o
  ON DATE_TRUNC('day', o.order_date) <= d.day
  AND DATE_TRUNC('month', o.order_date) = DATE_TRUNC('month', d.day)
ORDER BY 1

Window function с restart по началу месяца.

Cumulative без grain_to_date — всё время

metrics:
  - name: total_lifetime_revenue
    description: "Lifetime revenue (cumulative from start)"
    type: cumulative
    type_params:
      measure: order_amount
      # no window, no grain_to_date — cumulate over all history

SQL:

SELECT
  day,
  SUM(amount) OVER (ORDER BY day ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS total_lifetime_revenue
FROM ...

Cumulative с window — rolling

metrics:
  - name: revenue_rolling_30_day
    description: "Rolling 30-day revenue"
    type: cumulative
    type_params:
      measure: order_amount
      window: 30 day       # окно 30 дней

SQL:

SELECT
  day,
  SUM(amount) OVER (
    ORDER BY day
    RANGE BETWEEN INTERVAL '30 day' PRECEDING AND CURRENT ROW
  ) AS revenue_rolling_30_day
FROM ...

Каждый день — sum последних 30 дней. Полезно для tendency analysis (тренды без шума weekend dips).


Derived metric — формула из других metrics

Уже встречали. Композиция из других metrics через expr.

metrics:
  - name: profit_margin
    description: "Profit / Revenue percentage"
    type: derived
    type_params:
      expr: "(revenue - costs) / revenue * 100"
      metrics:
        - name: revenue
        - name: costs

Compiled SQL aggregates revenue и costs, потом считает (revenue - costs) / revenue * 100.

Derived с filter

metrics:
  - name: revenue_paid_tier
    type: derived
    type_params:
      expr: "revenue"
      metrics:
        - name: revenue
          filter: "{'{{ Dimension(\'customer__tier\') }}'} = 'paid'"

{'{{ Dimension(...) }}'} — Jinja-ссылка на dimension в filter context. Применит фильтр внутри метрики.


Conversion metric — funnel’ы

Специальный тип для conversion funnels:

metrics:
  - name: trial_to_paid_conversion
    description: "% of trial users who converted to paid within 30 days"
    type: conversion
    type_params:
      base_measure:
        name: trial_signups
      conversion_measure:
        name: paid_signups
      entity: customer_id
      window: 30 day
      conversion_type_params:
        calculation: conversion_rate

Compiled SQL — два аггрегата с window-based JOIN:

WITH base AS (
  SELECT customer_id, signup_at FROM ... WHERE type = 'trial'
),
conversion AS (
  SELECT customer_id, conversion_at FROM ... WHERE type = 'paid'
),
joined AS (
  SELECT
    b.customer_id,
    b.signup_at,
    c.conversion_at,
    CASE WHEN c.conversion_at IS NOT NULL AND
              c.conversion_at <= b.signup_at + INTERVAL '30 days'
         THEN 1 ELSE 0 END AS converted
  FROM base b
  LEFT JOIN conversion c USING (customer_id)
)
SELECT
  COUNT(CASE WHEN converted = 1 THEN customer_id END) * 1.0 / COUNT(customer_id) AS conversion_rate
FROM joined

Conversion metric — мощный для analytics. Available в MetricFlow 1.10+.


Полный пример: e-commerce SaaS

# models/semantic_models/mt_metrics.yml

metrics:
  # === Simple ===

  - name: total_revenue
    description: "Sum of order amounts"
    type: simple
    type_params:
      measure: order_amount

  - name: total_orders
    description: "Count of orders"
    type: simple
    type_params:
      measure: order_count

  - name: active_customer_count
    description: "Distinct customers in period"
    type: simple
    type_params:
      measure: customer_count_distinct

  # === Ratio ===

  - name: avg_order_value
    description: "Revenue / orders"
    type: ratio
    type_params:
      numerator: order_amount
      denominator: order_count

  - name: refund_rate
    description: "Refunded orders / total orders"
    type: ratio
    type_params:
      numerator: refunded_order_count
      denominator: order_count

  - name: revenue_per_customer
    description: "Revenue / unique customers"
    type: ratio
    type_params:
      numerator: order_amount
      denominator: customer_count_distinct

  # === Cumulative ===

  - name: revenue_mtd
    description: "Month-to-date revenue"
    type: cumulative
    type_params:
      measure: order_amount
      grain_to_date: month

  - name: revenue_ytd
    description: "Year-to-date revenue"
    type: cumulative
    type_params:
      measure: order_amount
      grain_to_date: year

  - name: revenue_rolling_30d
    description: "Rolling 30-day revenue"
    type: cumulative
    type_params:
      measure: order_amount
      window: 30 day

  # === Derived ===

  - name: net_revenue
    description: "Revenue minus refunds"
    type: derived
    type_params:
      expr: "total_revenue - refund_amount"
      metrics:
        - name: total_revenue
        - name: refund_amount

  - name: gross_margin_pct
    description: "(Revenue - Cost) / Revenue * 100"
    type: derived
    type_params:
      expr: "(total_revenue - total_costs) / total_revenue * 100"
      metrics:
        - name: total_revenue
        - name: total_costs

Этот набор покрывает ~80% типичных e-commerce SaaS метрик. На основе одного semantic_model orders + связанный customers.


Сравнение типов метрик

TypeЧто делаетSQL patternUse case
simpleDirect measureSUM(x) FROM t GROUP BYtotal revenue, total orders
rationum / denomSUM(num) / SUM(denom)conversion rate, refund rate, AOV
cumulativeRunning sum в windowSUM() OVER (...)MTD/YTD revenue, rolling 30d
derivedFormula из metrics(a - b) / a * 100margin, NRR, growth rate
conversionFunnel rate с windowwindow-based JOINtrial->paid, signup->purchase

В реальной project’е используются все 5 типов в comparable пропорциях. Понимая каждый, можно описать почти любую бизнес-метрику.


Performance considerations

TypePerformance characteristic
simpleПростой SUM/COUNT. Быстро.
ratioДва aggregate’а. Чуть медленнее.
cumulativeWindow function. Может быть тяжёлым на большом периоде.
derivedЗависит от underlying metrics. Если они быстрые — derived быстрый.
conversionСамый тяжёлый. Window JOIN на entity + time -> большой query.

Оптимизация:

  • cumulative: если запрашивается MTD каждый день для dashboard — материализуй через saved query.
  • conversion: всегда материализуй для production usage. Чистый ad-hoc — слишком медленно.
  • Pre-aggregations — топовая оптимизация. dbt Cloud saved queries или Cube pre-aggregations.

Попробуй сам

Расширьте семантические модели из прошлого урока. Добавьте все 4 типа метрик:

# models/semantic_models/mt_metrics.yml
metrics:
  - name: total_orders
    type: simple
    type_params:
      measure: order_count

  - name: aov   # average order value
    type: ratio
    type_params:
      numerator: order_amount
      denominator: order_count

  - name: revenue_mtd
    type: cumulative
    type_params:
      measure: order_amount
      grain_to_date: month

  - name: net_orders
    type: derived
    type_params:
      expr: "total_orders - refunded_orders"
      metrics:
        - name: total_orders
        - name: refunded_orders

Запустите:

dbt parse
dbt sl validate
dbt sl query --metrics aov --group_by metric_time__month
dbt sl query --metrics revenue_mtd --group_by metric_time__day

Получите SQL и результаты. Посмотрите как MetricFlow генерирует разный SQL для каждого типа.

Бонус: создайте conversion metric customer_to_repeat_purchase — % новых покупателей которые сделали второй заказ в 90 дней. Это типичная customer retention метрика.


Ключевые выводы

  1. 5 типов метрик в MetricFlow: simple, ratio, cumulative, derived, conversion.
  2. simple — direct alias на measure. SUM/COUNT/AVG в GROUP BY.
  3. ratio — numerator/denominator. Для conversion rate, refund rate, AOV. * 1.0 важно для float division.
  4. cumulative — running sum в window. grain_to_date restartит accumulation (MTD, YTD). window для rolling (30d).
  5. derived — формула из других metrics через expr. Composition без duplicating logic.
  6. conversion (1.10+) — funnel rate с time window. Самый сложный, тяжёлый на performance.
  7. Performance: simple -> ratio -> cumulative -> conversion в порядке возрастания cost. Для production cumulative/conversion — материализуйте через saved queries.
  8. На реальном проекте используются все типы, mix зависит от бизнеса (E-commerce — много ratio, SaaS — много cumulative и conversion).
Проверка знанийKnowledge check
CFO хочет metric 'Net Revenue Retention' (NRR) — стандартная SaaS метрика: (revenue at end of period from same cohort customers) / (revenue at start of period). Это derived? ratio? cumulative? Как декларировать?
ОтветAnswer
**NRR — это сложная metric, требующая custom modeling. Несколько подходов:**\n\n**1. Через intermediate dbt model + simple metric (рекомендуется):**\n\nNRR логика — это cohort analysis, не одна формула. Её сложно (и неэффективно) считать каждый раз в SL query. Лучший паттерн — **материализовать в dbt mart**:\n\n```sql\n-- models/marts/fct_nrr.sql\nWITH cohort_revenue AS (\n -- Revenue per customer per month\n SELECT customer_id, DATE_TRUNC('month', order_date) AS month, SUM(amount) AS revenue\n FROM {{ ref('fct_orders') }}\n GROUP BY 1, 2\n),\nperiod_pairs AS (\n SELECT\n customer_id,\n month AS current_month,\n DATE_TRUNC('month', month + INTERVAL '12 months') AS following_year,\n revenue AS start_revenue\n FROM cohort_revenue\n),\nfollowing_revenue AS (\n SELECT pp.customer_id, pp.current_month, pp.start_revenue, COALESCE(cr.revenue, 0) AS end_revenue\n FROM period_pairs pp\n LEFT JOIN cohort_revenue cr\n ON pp.customer_id = cr.customer_id\n AND cr.month = pp.following_year\n)\nSELECT\n current_month AS cohort_month,\n SUM(end_revenue) AS retained_revenue,\n SUM(start_revenue) AS base_revenue\nFROM following_revenue\nGROUP BY 1\n```\n\nЗатем simple semantic_model:\n\n```yaml\nsemantic_models:\n - name: nrr\n model: ref('fct_nrr')\n dimensions:\n - name: cohort_month\n type: time\n measures:\n - name: retained_revenue\n agg: sum\n - name: base_revenue\n agg: sum\n\nmetrics:\n - name: nrr\n type: ratio\n type_params:\n numerator: retained_revenue\n denominator: base_revenue\n```\n\nNRR теперь — простой ratio metric. Heavy lifting сделан в dbt mart.\n\n**Зачем this approach:**\n\n- **Performance** — materialized vs on-demand window calculations.\n- **Testability** — `fct_nrr` can have dbt tests (not_null, unique by cohort_month).\n- **Reusability** — multiple metrics на top of fct_nrr (NRR by tier, by region).\n- **Audit** — SQL очевиден, легко review.\n\n**2. Через derived metric (не рекомендуется для complex):**\n\nТеоретически можно:\n\n```yaml\nmetrics:\n - name: nrr_attempt\n type: derived\n type_params:\n expr: "retained_revenue / base_revenue"\n metrics:\n - name: retained_revenue\n - name: base_revenue\n```\n\nНо чтобы это работало, нужно derived metrics с **window-aware** measures (current period vs +12 months later). MetricFlow на 2026 не поддерживает такую сложную window логику в derived metrics. Можно через cumulative + offset, но это hacky.\n\n**3. Conversion metric (specific case):**\n\nЕсли NRR упрощено как "% customers retained" (binary: still paying yes/no), можно через conversion metric с 12-month window. Но это **customer retention rate**, не NRR (который про revenue, не headcount).\n\n**Главный урок и design pattern**:\n\nДля complex business metrics:\n\n1. **Heavy logic в dbt mart** — SQL очевиден, testable, performant.\n2. **Semantic Layer для exposing metric** — simple/ratio metric на top of mart.\n3. **NOT** пытайтесь cram complex logic в SL formulas — SL не Excel, есть border between data modeling и metric definition.\n\nГраница: **'если без сложной CTE не выразить — это data modeling, не metric definition'**. Mart решает, SL exposes.\n\n**Bonus**: документация. Description metric должна объяснять: 'NRR cohort-based, 12-month window, рассчитан в fct_nrr mart'. Consumers should know что-куда смотреть.
Проверка знанийKnowledge check
Команда настроила cumulative metric \`revenue_mtd\` для dashboard. На каждый запрос (refresh каждые 5 минут) генерируется window function SQL на fct_orders с 5M строк. Запрос занимает 8 секунд, плохо для real-time dashboard. Как оптимизировать?
ОтветAnswer
**Cumulative metrics на больших таблицах — типичная performance проблема.** Несколько уровней оптимизации:\n\n**1. Saved query — материализация (best fix):**\n\n```yaml\n# models/semantic_models/sq_revenue_dashboard.yml\nsaved_queries:\n - name: revenue_mtd_daily\n description: "Pre-computed daily MTD revenue"\n query_params:\n metrics: [revenue_mtd]\n group_by:\n - metric_time__day\n exports:\n - name: mart_revenue_mtd_daily\n config:\n export_as: table\n```\n\n`dbt build --select mart_revenue_mtd_daily` создаёт таблицу:\n\n```\nday | revenue_mtd\n2026-05-01 | 10000\n2026-05-02 | 25000\n2026-05-03 | 38000\n...\n```\n\nDashboard читает 30 строк (текущий месяц) из mart — миллисекунды. Refresh раз в час обновляет mart через cron-triggered dbt run. Это **классический паттерн pre-aggregation**.\n\nTrade-off: данные не real-time (отстают на час). Для dashboard это обычно OK — MTD не нужен на минутной точности.\n\n**2. Incremental rebuild mart (дальше fix):**\n\nЕсли pre-aggregated mart обновляется каждый час, можно сделать incremental:\n\n```sql\n-- models/marts/fct_revenue_mtd_daily.sql\n{{ config(\n materialized='incremental',\n unique_key='day'\n) }}\n\nSELECT day, SUM(amount) OVER (PARTITION BY DATE_TRUNC('month', day) ORDER BY day) AS revenue_mtd\nFROM ...\n{% if is_incremental() %}\nWHERE day не меньше DATE_TRUNC('day', CURRENT_DATE - 7) -- recompute last 7 days\n{% endif %}\n```\n\nIncremental rebuild — секунды вместо минут. Подходит для high-frequency refresh.\n\n**3. Better windowing — без full table scan:**\n\nOriginal cumulative metric на 5M строк делает window function на всю историю. Лимит до текущего месяца:\n\n```yaml\nmetrics:\n - name: revenue_mtd\n type: cumulative\n type_params:\n measure: order_amount\n grain_to_date: month\n window: 1 month # explicit window — only current month\n```\n\nMetricFlow добавляет `WHERE order_date не меньше DATE_TRUNC('month', CURRENT_DATE)` — читает только текущий месяц (200K строк вместо 5M). Window function на маленьком dataset быстра.\n\n**4. Warehouse partition + clustering:**\n\nЕсли fct_orders partitioned by order_date (BigQuery) или clustered (Snowflake), Snowflake/BQ автоматически prunes partitions для current month query. Скорость от 8s до 0.5s.\n\n**Setup**:\n\n```sql\n-- fct_orders.sql\n{{ config(\n materialized='table',\n cluster_by=['order_date', 'customer_id']\n) }}\n```\n\n**5. BI tool client-side caching:**\n\nTableau / Looker / Power BI — все имеют result caching. Dashboard refresh каждые 5 минут — но в client cache сидит результат от последнего refresh. Если query идентична — отдаём из cache. Real query — реже.\n\n**6. Cube + pre-aggregations (если outside dbt SL):**\n\nCube — full-featured caching layer. Можно pre-aggregate в Cube Store (Redis-based), serving queries из memory. Drastically faster но сложнее setup.\n\n**Recommended approach** для конкретного случая:\n\n1. **Primary fix**: saved query материализующая daily MTD в mart. Hourly refresh через dbt cron. Dashboard читает mart. From 8s to 50ms.\n2. **Secondary**: добавить window: 1 month если real-time важен (5-10 min staleness OK).\n3. **Tertiary**: cluster fct_orders по order_date для general future queries.\n\n**Главный урок**: Semantic Layer не магически быстрый. Cumulative metrics — особенно sensitive. Pre-aggregation via saved queries — стандартный pattern для production dashboards. SL даёт **API** для metrics, не automatic performance — это работа data engineering team.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Какие 5 типов metrics в MetricFlow и в чем разница?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5