Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 15.02 · 30 мин
Средний
MetricFlowsemantic_modelentitiesdimensionsmeasures

MetricFlow концепт: semantic_model, entities, dimensions, measures

В прошлом уроке мы поняли зачем Semantic Layer. Теперь — что внутри. MetricFlow (движок dbt Semantic Layer) построен на четырёх ключевых концептах: semantic_model, entities, dimensions, measures. Понимая их, вы понимаете как метрики компилируются в SQL.


Большая картина

MetricFlow концепты
semantic_model — обёртка над dbt modelsemantic_model — обёртка над dbt model (fct_orders, dim_customers). Это не таблица — это metadata, описывающая что в таблице есть для analytics queries.
entities — ключи (primary, foreign)entities — ключи. Primary (PK таблицы), Foreign (FK для join), Unique (alt key). MetricFlow использует entities для автоматического JOIN между semantic_models.
dimensions — по чему GROUP BYdimensions — по чему группировать. Time (с granularity day/week/month), categorical (region, status), Boolean (is_active).
measures — что агрегируемmeasures — что агрегировать. SUM, COUNT, AVG, MIN, MAX, DISTINCT. Это building blocks для метрик.
metric — формула из measuresmetric — это формула из measures. revenue = order_amount - refund_amount. Простой metric — прямой measure. Derived — формула. Cumulative — running aggregate.

Аналогия: если 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 measures

Entities — то, что соединяет 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Что значит
primaryPK таблицы. Уникален. Используется для full identity.
foreignFK на другую таблицу. Используется для JOIN.
uniqueAlt key (например, email в customers — non-PK но unique).
naturalNatural 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
aggSQL equivalent
sumSUM(expr)
countCOUNT(expr)
count_distinctCOUNT(DISTINCT expr)
averageAVG(expr)
minMIN(expr)
maxMAX(expr)
medianMEDIAN(expr) (если warehouse поддерживает)
percentilePERCENTILE_CONT(...) (с params)
sum_booleanSUM(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:

  1. Resolves metric net_revenue -> derived formula -> measures order_amount, refund_amount, discount_amount.
  2. Все measures на orders semantic_model -> нужна only fct_orders для measures.
  3. country — на customers semantic_model -> JOIN через entity customer_id.
  4. 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.

WARNING

Без 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:

  1. Установите MetricFlow (он включён в dbt-core 1.9+):
pip install "dbt-core==1.10.0" "dbt-duckdb==1.10.0"
pip install dbt-metricflow
  1. Создайте 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
  1. Запустите:
dbt parse
dbt sl validate
  1. 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.


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

  1. semantic_model — metadata-обёртка над dbt model. YAML в models/semantic_models/. Описывает что в модели есть для analytics: entities, dimensions, measures.
  2. entities — keys (primary, foreign, unique, natural). Используются для автоматического JOIN между semantic_models.
  3. dimensions — то по чему GROUP BY. Time (с granularity), categorical, derived через expr.
  4. measures — простые aggregations (sum, count, count_distinct, avg, etc.). Building blocks для метрик.
  5. metric — поверх measures. Simple (alias), derived (формула), cumulative/ratio (другие типы — следующий урок).
  6. MetricFlow генерирует SQL автоматически на основе semantic_models + metric requests. Consumer не пишет SQL.
  7. JOIN through entities — автоматический. Если customer_id есть в обоих semantic_models — JOIN происходит без явного указания.
  8. dbt sl validate обязательно в CI/pre-commit — без него ошибки проявятся только при queries.
Проверка знанийKnowledge check
Junior пишет semantic_model для fct_orders и не понимает: 'я уже описал колонки в models/_models.yml (description, tests). Почему дублировать в semantic_models?'
ОтветAnswer
Это distinct concepts с разными целями — не дублирование, а разные **слои абстракции**.\n\n**dbt _models.yml — DATA layer:**\n\n- **Цель**: документация **физической структуры** таблицы.\n- **Содержит**: имена колонок, типы, tests (not_null, unique), descriptions.\n- **Использование**: dbt build, dbt test, dbt docs.\n- **Consumer**: разработчики, code review, lineage tools.\n- **Пример**: `order_id: integer not null unique`.\n\n**semantic_models — METRICS layer:**\n\n- **Цель**: описание **бизнес-семантики** для analytics queries.\n- **Содержит**: entities (ключи для JOIN), dimensions (что group by), measures (как aggregate).\n- **Использование**: MetricFlow для генерации SQL на metric queries.\n- **Consumer**: BI tools, AI agents, Python notebooks через SL API.\n- **Пример**: `order_id: primary entity`, `order_date: time dimension day`, `amount: measure SUM`.\n\n**Разные slices информации:**\n\n_models.yml говорит `SELECT order_id FROM fct_orders WHERE order_id IS NOT NULL` валидно (тест). semantic_model говорит `для query 'orders by month' используй order_date как time dimension agg=COUNT(order_id)`. Разная информация про ту же колонку.\n\n**Что НЕ дублируется:**\n\n- Tests (not_null, unique) — только в _models.yml. SL их не использует.\n- Materialization, schema, tags — только в _models.yml/config. SL не интересуется.\n- Metric formulas — только в semantic_models. _models.yml не знает про metrics.\n\n**Что может дублироваться (и стоит избежать):**\n\n- Descriptions. Например `description: 'Total order amount'` в _models.yml column И `description: 'Total order amount'` в semantic_model measure. Это normal — оба нужны для своих use cases (dbt docs vs SL UI). Можно использовать **doc blocks**:\n\n```yaml\n# _models.yml\ncolumns:\n - name: amount\n description: "{{ doc('order_amount_description') }}"\n\n# semantic_models/sm_orders.yml\nmeasures:\n - name: order_amount\n description: "{{ doc('order_amount_description') }}"\n```\n\nОдин source of truth для описания.\n\n**Аналогия**: `_models.yml` = schema definition для DBA. `semantic_models` = API design для consumers. Разные audiences, разные viewpoints, обе нужны.\n\n**Главный урок**: semantic_models не дублирует _models.yml. Это **complementary** layer — metadata для metrics на top того что _models.yml даёт для физических tables.
Проверка знанийKnowledge check
Команда настроила semantic_models для fct_orders и dim_customers. На запросе \`revenue by country\` MetricFlow генерирует JOIN через entity customer_id. На 50M строках query занимает 30 секунд. Junior спрашивает: 'почему так медленно, это же простой JOIN?'
ОтветAnswer
**Производительность SL queries — реальная проблема при scale.** Возможные причины и решения:\n\n**1. JOIN на unindexed columns (Snowflake/Postgres).**\n\nMetricFlow генерирует `JOIN dim_customers c ON o.customer_id = c.customer_id`. Если customer_id не clustered/indexed в fct_orders — Snowflake делает full table scan. На 50M строках это десятки секунд.\n\n**Solution**: в fct_orders.sql добавить clustering:\n\n```sql\n{{ config(\n materialized='table',\n cluster_by=['customer_id', 'order_date']\n) }}\n```\n\nSnowflake авто-clusters, JOIN использует pruning. Может ускорить в 10x.\n\n**2. Wide aggregations без pre-aggregation.**\n\nQuery `revenue by country` = `SUM(amount) - SUM(refunds) GROUP BY country`. Для 50M строк -> 50M scans + aggregation. Если country имеет 100 уникальных значений и query запускается часто — пре-агрегация exits.\n\n**Solution**: использовать **saved queries** в MetricFlow (см. следующий урок), которые материализуют как rollup tables:\n\n```yaml\nsaved_queries:\n - name: monthly_revenue_by_country\n query_params:\n metrics: [net_revenue]\n group_by: [customers__country, metric_time__month]\n config:\n cache: true\n```\n\nDbt build материализует это в `mart_monthly_revenue_by_country`. Queries hit rollup table (1000 rows) instead of 50M.\n\n**3. Denormalize в fct_orders.**\n\nДобавить `country` колонку прямо в `fct_orders` (denormalized). Тогда JOIN не нужен — `SUM(amount) FROM fct_orders WHERE ... GROUP BY country`.\n\nTrade-off: storage overhead (country повторяется на каждой строке fact), но dramatic speed gain. На fact tables с frequent dimensional queries — стандартная оптимизация data warehousing (star schema).\n\n**4. Каскадная агрегация через intermediate model.**\n\nСоздать `int_orders_with_customer.sql`:\n\n```sql\nSELECT o.*, c.country, c.tier\nFROM {{ ref('fct_orders') }} o\nLEFT JOIN {{ ref('dim_customers') }} c\n ON o.customer_id = c.customer_id\n```\n\nsemantic_model указывает на эту enriched модель. JOIN происходит один раз при build, не на каждом query.\n\n**5. Time partition (если warehouse поддерживает).**\n\nBigQuery — partition by order_date. Snowflake — clustering. DuckDB — partitioned reading from parquet. Query `WHERE order_date не меньше '2025-10-01'` прунит partitions, читает только 3 месяца не 50M строк.\n\n**6. Result caching.**\n\nDbt Cloud SL имеет result cache (повторные queries не пересчитываются). Cube — robust caching layer с pre-aggregations. MetricFlow core — нет server-side caching. Если queries repeating — рассмотреть Cube.\n\n**Diagnostic process**:\n\n1. Get compiled SQL: `dbt sl query --metrics revenue --group_by customers__country --explain`. Посмотреть EXPLAIN PLAN в warehouse.\n2. Identify bottleneck: full scan? unindexed JOIN? large aggregation?\n3. Optimize relevant layer: clustering, pre-aggregations, denormalize, or saved_queries.\n\n**Главный урок**: Semantic Layer не магически быстрый. Performance — function of underlying data model. Star schema, clustering, pre-aggregations — те же классические DWH оптимизации. SL — это API над warehouse, не альтернатива хорошему data modeling.

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

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

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

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