MetricFlow концепт: semantic_model, entities, dimensions, measures
В прошлом уроке мы поняли зачем Semantic Layer. Теперь — что внутри. MetricFlow (движок dbt Semantic Layer) построен на четырёх ключевых концептах: semantic_model, entities, dimensions, measures. Понимая их, вы понимаете как метрики компилируются в SQL.
Большая картина
Аналогия: если dbt mart — это таблица в warehouse, semantic_model — это resume этой таблицы для аналитики.
semantic_model — обёртка над моделью
# models/semantic_models/sm_orders.yml
semantic_models:
- name: orders
description: "Order facts for revenue/customer analytics"
model: ref('fct_orders')
defaults:
agg_time_dimension: order_date # default time dimension для measures
entities:
- name: order_id
type: primary
- name: customer_id
type: foreign
- name: product_id
type: foreign
dimensions:
- name: order_date
type: time
type_params:
time_granularity: day
- name: status
type: categorical
- name: region
type: categorical
expr: shipping_region # mapping к колонке shipping_region
measures:
- name: order_amount
agg: sum
expr: amount
- name: order_count
agg: count
expr: order_id
- name: customer_count
agg: count_distinct
expr: customer_id
Что важно:
name: orders— идентификатор. На него ссылаются metrics.model: ref('fct_orders')— какая dbt-модель «обёрнута».defaults.agg_time_dimension: order_date— default time dim. Когда metric агрегируется по времени, используется этот.entities,dimensions,measures— три «секции» описания.
expr: — выражение SQL для derived колонок. Может быть простым (expr: shipping_region — alias) или сложным (expr: amount * 0.9 — calculation).
entities — ключи и отношения
Fact table и measures — Kimball-корень MetricFlow measuresEntities — то, что соединяет semantic_models. Если в fct_orders есть customer_id (FK на dim_customers), а в dim_customers есть customer_id как primary — MetricFlow может автоматически делать JOIN при запросах.
# sm_orders.yml
semantic_models:
- name: orders
model: ref('fct_orders')
entities:
- name: order_id
type: primary
- name: customer_id
type: foreign # ссылается на dim_customers.customer_id
# sm_customers.yml
semantic_models:
- name: customers
model: ref('dim_customers')
entities:
- name: customer_id
type: primary # primary key
dimensions:
- name: signup_date
type: time
- name: tier
type: categorical
- name: country
type: categorical
Теперь при запросе revenue by country:
client.query(
metrics=['revenue'],
group_by=['customers__country'] # ← из dim_customers
)
MetricFlow автоматически:
SELECT
c.country,
SUM(o.amount) - SUM(o.refund_amount) AS revenue
FROM fct_orders o
JOIN dim_customers c
ON o.customer_id = c.customer_id
GROUP BY 1
JOIN через entities. Без явного указания «JOIN ON o.customer_id = c.customer_id» в каждом запросе.
Типы entities
| Type | Что значит |
|---|---|
primary | PK таблицы. Уникален. Используется для full identity. |
foreign | FK на другую таблицу. Используется для JOIN. |
unique | Alt key (например, email в customers — non-PK но unique). |
natural | Natural key для SCD (например, customer_email в snapshot — может меняться, но идентифицирует логику). |
В простых проектах достаточно primary и foreign.
dimensions — по чему группировать
Dimensions — это колонки для GROUP BY. Два главных типа:
Time dimensions
dimensions:
- name: order_date
type: time
type_params:
time_granularity: day
time_granularity определяет минимальную гранулярность. Можно day, week, month, quarter, year, hour, minute. На queries MetricFlow умеет truncate от меньшей к большей: если data на day, можно запросить group by month — MetricFlow сделает DATE_TRUNC('month', order_date).
Categorical dimensions
dimensions:
- name: status
type: categorical
- name: region
type: categorical
expr: shipping_region # mapping
Простой dimension — name берётся из колонки. Можно переопределить через expr:.
Calculated dimensions
dimensions:
- name: order_size_bucket
type: categorical
expr: |
CASE
WHEN amount < 50 THEN 'small'
WHEN amount < 500 THEN 'medium'
ELSE 'large'
END
Полезно для derived buckets, не дублируя логику в каждом BI dashboard.
measures — что агрегируем
Measures — building blocks метрик. Это простые aggregations на одной таблице:
measures:
- name: order_amount
agg: sum
expr: amount
- name: order_count
agg: count
expr: order_id
- name: customer_count
agg: count_distinct
expr: customer_id
- name: avg_order_value
agg: average
expr: amount
- name: max_order_amount
agg: max
expr: amount
| agg | SQL equivalent |
|---|---|
sum | SUM(expr) |
count | COUNT(expr) |
count_distinct | COUNT(DISTINCT expr) |
average | AVG(expr) |
min | MIN(expr) |
max | MAX(expr) |
median | MEDIAN(expr) (если warehouse поддерживает) |
percentile | PERCENTILE_CONT(...) (с params) |
sum_boolean | SUM(CASE WHEN expr THEN 1 ELSE 0 END) |
Measure — это measurement of fact. Метрика — это business-meaningful expression поверх measure’ов.
metric — поверх measures
Метрика — это то что consumer запрашивает:
# models/semantic_models/mt_revenue.yml
metrics:
- name: revenue
description: "Net revenue: orders minus refunds"
type: derived
type_params:
expr: "order_amount - refund_amount"
metrics:
- name: order_amount
- name: refund_amount
- name: order_amount
description: "Total order amount"
type: simple
type_params:
measure: order_amount # ссылка на measure в semantic_model
- name: refund_amount
description: "Total refunds"
type: simple
type_params:
measure: refund_amount
Простая metric order_amount — прямой alias на measure. Derived revenue — формула из других metrics.
Подробнее про типы метрик — следующий урок.
Полный пример: e-commerce
# models/semantic_models/sm_orders.yml
semantic_models:
- name: orders
description: "Order facts"
model: ref('fct_orders')
defaults:
agg_time_dimension: order_date
entities:
- name: order_id
type: primary
- name: customer_id
type: foreign
dimensions:
- name: order_date
type: time
type_params:
time_granularity: day
- name: status
type: categorical
- name: discount_applied
type: categorical
expr: "CASE WHEN discount_amount > 0 THEN true ELSE false END"
measures:
- name: order_amount
agg: sum
expr: amount
- name: discount_amount
agg: sum
expr: discount_amount
- name: refund_amount
agg: sum
expr: refund_amount
- name: order_count
agg: count
expr: order_id
- name: customer_count_distinct
agg: count_distinct
expr: customer_id
# models/semantic_models/sm_customers.yml
semantic_models:
- name: customers
description: "Customer dimensions"
model: ref('dim_customers')
entities:
- name: customer_id
type: primary
dimensions:
- name: signup_date
type: time
type_params:
time_granularity: day
- name: tier
type: categorical
- name: country
type: categorical
measures:
- name: customer_count
agg: count
expr: customer_id
- name: lifetime_value
agg: average
expr: ltv_score
# models/semantic_models/mt_metrics.yml
metrics:
# Simple metric — direct measure
- name: total_orders
type: simple
type_params:
measure: order_count
# Derived metric — combination
- name: net_revenue
description: "Order amount minus refunds and discounts"
type: derived
type_params:
expr: "order_amount - refund_amount - discount_amount"
metrics:
- name: order_amount
- name: refund_amount
- name: discount_amount
# Simple proxies
- name: order_amount
type: simple
type_params:
measure: order_amount
- name: refund_amount
type: simple
type_params:
measure: refund_amount
- name: discount_amount
type: simple
type_params:
measure: discount_amount
Как MetricFlow генерирует SQL
При запросе net_revenue by country:
mf_client.query(
metrics=['net_revenue'],
group_by=['customers__country', 'metric_time__month']
)
MetricFlow:
- Resolves metric
net_revenue-> derived formula -> measuresorder_amount, refund_amount, discount_amount. - Все measures на
orderssemantic_model -> нужна onlyfct_ordersдля measures. country— наcustomerssemantic_model -> JOIN через entitycustomer_id.metric_time__month— time dim, агрегация по month ->DATE_TRUNC('month', order_date).
Скомпилированный SQL:
SELECT
c.country,
DATE_TRUNC('month', o.order_date) AS month,
SUM(o.amount) - SUM(o.refund_amount) - SUM(o.discount_amount) AS net_revenue
FROM analytics.fct_orders o
LEFT JOIN analytics.dim_customers c
ON o.customer_id = c.customer_id
WHERE o.order_date >= '2025-01-01'
GROUP BY 1, 2
ORDER BY 1, 2
Consumer не пишет этот SQL. Не знает имена таблиц. Просто: «net_revenue by country, monthly».
Где живут semantic_models
В models/semantic_models/:
models/
├── staging/
├── marts/
│ ├── fct_orders.sql
│ ├── dim_customers.sql
│ └── ...
└── semantic_models/
├── sm_orders.yml # semantic_model для fct_orders
├── sm_customers.yml # semantic_model для dim_customers
├── mt_revenue.yml # metrics
└── mt_customer_metrics.yml
Convention sm_ — semantic_model, mt_ — metrics. Не обязательно, но помогает organize.
Можно держать всё в одном файле, можно по entity / domain (orders, customers, products). Большие проекты — domain-based partition.
Запуск и валидация
# Parse — проверить YAML structure
dbt parse
# Validate semantic layer
dbt sl validate
# Query metric (через CLI)
dbt sl query --metrics net_revenue --group_by customers__country
# В Python через dbt-sl-sdk
from dbt_sl_client import SemanticLayerClient
client = SemanticLayerClient(...)
df = client.query(metrics=['net_revenue'], group_by=['customers__country'])
dbt sl validate проверяет: все measures referenced в metrics существуют, все entities между semantic_models совпадают (типы, имена), нет circular references.
Без dbt sl validate ошибки в semantic_model проявятся только при первом query. Лучше включить validate в pre-commit или CI.
Производительность
Compiled SQL генерируется при каждом query. MetricFlow has caching (на уровне query plan), но fundamentally — queries to warehouse.
Performance аспекты:
- Аггрегация в warehouse: Snowflake/BigQuery с большими таблицами эффективно агрегируют. DuckDB на средних — быстро.
- JOIN performance: entities должны быть indexed (PK или clustered). Без индекса JOIN на 100M строках — медленно.
- Pre-aggregations не в core MetricFlow. dbt Cloud имеет «saved queries» которые материализуют (см. урок 4).
- Caching результатов: на client-side (BI tool cache) или через прослойку (Cube умеет, MetricFlow — нет).
В среднем — query метрики 1-5 секунд на средних объёмах. Если медленнее — индексы или denormalize в марте.
Попробуй сам
В вашем dbt-проекте с DuckDB:
- Установите MetricFlow (он включён в dbt-core 1.9+):
pip install "dbt-core==1.10.0" "dbt-duckdb==1.10.0"
pip install dbt-metricflow
- Создайте
models/semantic_models/sm_orders.yml:
semantic_models:
- name: orders
model: ref('your_orders_model')
defaults:
agg_time_dimension: order_date
entities:
- name: order_id
type: primary
- name: customer_id
type: foreign
dimensions:
- name: order_date
type: time
type_params:
time_granularity: day
- name: status
type: categorical
measures:
- name: order_amount
agg: sum
expr: amount
- name: order_count
agg: count
expr: order_id
metrics:
- name: total_orders
type: simple
type_params:
measure: order_count
- Запустите:
dbt parse
dbt sl validate
- Query:
dbt sl query --metrics total_orders --group_by metric_time__month
Получите SQL и результат.
Бонус: добавьте sm_customers.yml для dim_customers с entity customer_id (primary). Сделайте derived metric revenue = order_amount - refund_amount. Query by customers__tier.
Ключевые выводы
- semantic_model — metadata-обёртка над dbt model. YAML в
models/semantic_models/. Описывает что в модели есть для analytics: entities, dimensions, measures. - entities — keys (primary, foreign, unique, natural). Используются для автоматического JOIN между semantic_models.
- dimensions — то по чему GROUP BY. Time (с granularity), categorical, derived через
expr. - measures — простые aggregations (sum, count, count_distinct, avg, etc.). Building blocks для метрик.
- metric — поверх measures. Simple (alias), derived (формула), cumulative/ratio (другие типы — следующий урок).
- MetricFlow генерирует SQL автоматически на основе semantic_models + metric requests. Consumer не пишет SQL.
- JOIN through entities — автоматический. Если customer_id есть в обоих semantic_models — JOIN происходит без явного указания.
- dbt sl validate обязательно в CI/pre-commit — без него ошибки проявятся только при queries.