Мартс: Определяемые бизнесом сущности
Наши рекомендации здесь расходятся в зависимости от того, используете ли вы Semantic Layer. В проекте без Semantic Layer мы рекомендуем активно денормализовать данные, следуя лучшим практикам, описанным ниже. С другой стороны, если вы используете Semantic Layer, мы стремимся сохранять модели максимально нормализованными, чтобы предоставить MetricFlow наибольшую гибкость. Подробнее см. в разделе The dbt Semantic Layer and marts.
Это слой, где все объединяется, и мы начинаем организовывать все наши атомы (модели подготовки) и молекулы (промежуточные модели) в полноценные клетки, которые имеют идентичность и цель. Мы иногда называем это слоем сущностей или слоем концепций, чтобы подчеркнуть, что все наши мартсы предназначены для представления конкретной сущности или концепции на ее уникальном уровне детализации. Например, заказ, клиент, территория, событие клика, платеж — каждая из этих сущностей будет представлена отдельным мартсом, и каждая строка будет представлять отдельный экземпляр этих концепций. В отличие от традиционной звездообразной схемы Кимбалла, в современном хранении данных — где хранение дешево, а вычисления дороги — мы с удовольствием заимствуем и добавляем любые данные из других концепций, которые имеют отношение к ответам на вопросы о основной сущности мартса. Создание одних и тех же данных в нескольких местах, как мы делаем с orders в нашем примере мартса customers ниже, более эффективно в этой парадигме, чем повторное соединение этих концепций (это базовое определение денормализации в этом контексте). Давайте посмотрим, как мы подходим к этому первому слою, предназначенному специально для предоставления конечным пользователям.
Мартсы: Файлы и папки
Последний слой наших основных преобразований представлен ниже, предоставляя модели для отделов finance и marketing.
models/marts
├── finance
│ ├── _finance__models.yml
│ ├── orders.sql
│ └── payments.sql
└── marketing
├── _marketing__models.yml
└── customers.sql
✅ Группировка по отделу или области интересов. Если у вас меньше 10 или около того мартсов, вам может не понадобиться много подпапок, поэтому, как и с промежуточным слоем, не переоптимизируйте слишком рано. Если вы все же обнаружите, что вам нужно добавить больше структуры и группировки, используйте здесь полезные бизнес-концепции. В нашем слое мартсов мы больше не беспокоимся о данных, согласованных с источником, поэтому группировка по отделам (маркетинг, финансы и т.д.) является наиболее распространенной структурой на этом этапе.
✅ Назовите по сущности. Используйте простой английский язык для названия файла на основе концепции, которая формирует зерно мартса, например customers, orders. Мартсы, которые не включают временные свертки (чистые мартсы), не должны иметь временного измерения (orders_per_day) здесь, обычно лучше всего фиксируемого через метрики.
❌ Создавайте одну и ту же концепцию по-разному для разных команд. finance_orders и marketing_orders обычно считаются антипаттерном. Как всегда, есть исключения — распространенный шаблон, который мы видим, заключается в том, что финансы могут иметь специфические потребности, например, отчетность о доходах для правительства, которая отличается от того, как компания в целом измеряет доходы ежедневно. Просто убедитесь, что они четко спроектированы и понятны как отдельные концепции, а не как ведомственные представления одной и той же концепции: tax_revenue и revenue, а не finance_revenue и marketing_revenue.
Мартсы: Модели
Наконец, мы рассмотрим лучшие практики для моделей в каталоге мартсов, изучив две из наших моделей мартсов. Это бизнес-согласованные — то есть созданные в соответствии с нашим видением и потребностями — сущности, которые мы объединяем из этих преобразованных компонентов.
-- orders.sql
with
orders as (
select * from {{ ref('stg_jaffle_shop__orders' )}}
),
order_payments as (
select * from {{ ref('int_payments_pivoted_to_orders') }}
),
orders_and_order_payments_joined as (
select
orders.order_id,
orders.customer_id,
orders.order_date,
coalesce(order_payments.total_amount, 0) as amount,
coalesce(order_payments.gift_card_amount, 0) as gift_card_amount
from orders
left join order_payments on orders.order_id = order_payments.order_id
)
select * from orders_and_order_payments_joined
-- customers.sql
with
customers as (
select * from {{ ref('stg_jaffle_shop__customers')}}
),
orders as (
select * from {{ ref('orders')}}
),
customer_orders as (
select
customer_id,
min(order_date) as first_order_date,
max(order_date) as most_recent_order_date,
count(order_id) as number_of_orders,
sum(amount) as lifetime_value
from orders
group by 1
),
customers_and_customer_orders_joined as (
select
customers.customer_id,
customers.first_name,
customers.last_name,
customer_orders.first_order_date,
customer_orders.most_recent_order_date,
coalesce(customer_orders.number_of_orders, 0) as number_of_orders,
customer_orders.lifetime_value
from customers
left join customer_orders on customers.customer_id = customer_orders.customer_id
)
select * from customers_and_customer_orders_joined
- ✅ Материализация в виде таблиц или инкрементальных моделей. Когда мы доходим до слоя marts, пора начинать строить в хранилище не только логику, но и сами данные. Это обеспечивает значительно более высокую производительность для конечных пользователей при работе с этими поздними моделями, которые как раз и предназначены для их использования, а также снижает затраты за счёт того, что нам не нужно пересчитывать целые цепочки моделей каждый раз, когда кто‑то обновляет дашборд или запускает регрессию в python. Хорошее общее практическое правило для выбора материализации — всегда начинать с view (она практически не занимает места в хранилище и всегда возвращает актуальные результаты), затем, когда такую view становится слишком долго запрашивать, превратить её в таблицу, и, наконец, когда уже сама таблица становится слишком долгой в построении и замедляет ваши прогоны, сконфигурировать её как инкрементальную модель. Как и всегда, начинайте с простого и добавляйте сложность только по мере необходимости. Модели с наибольшими объёмами данных и самыми ресурсоёмкими трансформациями однозначно должны использовать отличные возможности инкрементальной материализации в dbt, однако попытка по умолчанию сделать все модели marts инкрементальными лишь добавит ненужной сложности. Мы рекомендуем прочитать этот классический пост Тристана об ограничениях инкрементального моделирования.
- ✅ Широкие и денормализованные. В отличие от классических подходов к хранилищам данных, в современном data stack хранение дешёвое, а вычисления — дорогие, и именно их нужно экономить в первую очередь. Поэтому имеет смысл упаковывать данные в очень широкие денормализованные структуры, которые могут предоставить всё необходимое о некотором бизнес‑понятии в одном месте.
- ❌ Слишком много join’ов в одном mart. Одно из полезных практических правил при построении трансформаций в dbt — избегать объединения слишком большого количества понятий в одном mart. Что именно считать «слишком большим количеством», может различаться. Если вам нужно объединить 8 staging‑моделей, используя только простые join’ы, это может быть вполне нормально. С другой стороны, если вы связываете 4 понятия с помощью сложных и вычислительно тяжёлых оконных функций, это уже может быть чрезмерно. Необходимо соотносить количество моделей, которые вы объединяете, со сложностью логики внутри mart: если код становится трудно читать и сложно сформировать чёткую ментальную модель происходящего, стоит подумать о модульности. Это не жёсткое правило, но если для создания mart вы объединяете более 4–5 понятий, вам может быть полезно добавить промежуточные модели для повышения наглядности. Две промежуточные модели, каждая из которых объединяет по три понятия, и mart, который затем объединяет эти две промежуточные модели, как правило, дают гораздо более читаемую цепочку логики, чем один mart с шестью join’ами.
- ✅ Осознанно используйте другие marts при построении. Хотя мы стараемся сохранять сужающийся DAG вплоть до слоя marts, на этом этапе правила могут стать менее строгими. Типичный пример — передача информации между marts с разной зернистостью, как мы видели выше, когда мы используем mart
ordersв martcustomers, чтобы агрегировать критически важные данные о заказах до зернаcustomer. Теперь, когда мы действительно «тратим» вычислительные ресурсы и хранилище, физически создавая данные в выходных моделях, разумно использовать уже построенные ресурсы, чтобы ускорить расчёты и снизить затраты для выходов, которым требуются схожие данные, вместо того чтобы каждый раз пересчитывать одни и те же view и CTE с нуля. Правильный подход здесь сильно зависит от вашего конкретного DAG, набора моделей и целей — важно лишь помнить, что использование одного mart при построении другого, более позднего mart допустимо, но требует внимательного подхода, чтобы избежать напрасного расхода ресурсов или циклических зависимостей.
Наиболее важный аспект мартсов заключается в том, что они содержат все полезные данные о конкретной сущности на детальном уровне. Это не значит, что мы не привлекаем множество других сущностей и концепций, таких как множество данных user в наш мартс orders, мы это делаем! Это просто означает, что отдельные orders остаются основным зерном нашей таблицы. Если мы начинаем группировать users и orders вдоль временной оси, в нечто вроде user_orders_per_day, мы выходим за рамки мартсов в метрики.
Мартсы: Другие соображения
- Устранение неполадок через таблицы. Хотя наложение представлений и эфемерных моделей до наших мартсов — построение данных в хранилище только в конце цепочки, когда у нас есть модели, с которыми мы действительно хотим, чтобы конечные пользователи работали — идеально в производстве, это может представлять некоторые трудности в разработке. В частности, определенные ошибки могут казаться возникающими в наших более поздних моделях, которые на самом деле происходят из гораздо более ранних зависимостей в нашей цепочке моделей (предшествующих моделей в нашей DAG, которые строятся до того, как модель выдает ошибки). Если у вас возникают трудности с определением, где или что говорит вам ошибка базы данных, может быть полезно временно построить конкретную цепочку моделей как таблицы, чтобы хранилище выдало ошибку там, где она действительно происходит.
Семантический слой dbt и мартсы
Наши рекомендации по структуре в значительной степени зависят от того, используете ли вы Semantic Layer. Если вы используете Semantic Layer, мы рекомендуем более нормализованный подход к построению marts. Если же вы не используете Semantic Layer, мы рекомендуем более денормализованный подход, который стал типичным для dbt‑проектов.
Полный список рекомендаций по структуре, неймингу и организации в рамках Semantic Layer можно найти в руководстве How we build our metrics, в частности в разделе Refactoring an existing rollup.