В прошлом уроке мы собрали functional core проекта — 13 моделей с тестами. Теперь добавляем production-grade обвязку: snapshot для исторических изменений, документация, exposures.
Шаг 6: Snapshot — customers_snapshot
Snapshot фиксирует исторические версии записей. Тип SCD2 — каждое изменение сохраняется как новая строка с dbt_valid_from и dbt_valid_to интервалами.
Зачем нужно: если клиент сменил email или country, в обычной таблице customers это просто перепишется. С snapshot ты сохраняешь все версии — можешь сделать «какой был email клиента 1 января 2025?».
Создай snapshots/customers_snapshot.sql:
{% snapshot customers_snapshot %}
{{
config(
target_schema='snapshots',
unique_key='customer_id',
strategy='timestamp',
updated_at='signup_date',
invalidate_hard_deletes=False,
)
}}
select
customer_id,
customer_name,
email,
country_code,
signup_date
from {{ ref('stg_jaffle__customers') }}
{% endsnapshot %}
Объяснение:
target_schema='snapshots'— snapshot материализуется в отдельной схеме (для изоляции от моделей).unique_key='customer_id'— по чему идентифицируем строки между запусками.strategy='timestamp'— сравнение черезupdated_atколонку. Если значениеsignup_dateпоменялось — это новая версия.updated_at='signup_date'— какая колонка несёт timestamp обновления. В реальной системе это может бытьlast_updated_at, в нашем учебном —signup_date.invalidate_hard_deletes=False— не помечать строки, удалённые из source, как deleted. В DuckDB hard_deletes не поддерживается (см. модуль 13).
Запуск:
dbt snapshot
Должен создать таблицу snapshots.customers_snapshot в my_jaffle_shop.duckdb. Открой в DuckDB shell:
$ duckdb my_jaffle_shop.duckdb
D> select * from snapshots.customers_snapshot;
Увидишь все customers + 4 служебные колонки:
dbt_valid_from— когда версия стала актуальнойdbt_valid_to— когда устарела (NULL для текущей)dbt_scd_id— surrogate key версииdbt_updated_at— копияsignup_date
При следующем запуске dbt snapshot (если данные изменились в stg_jaffle__customers) появятся новые строки для изменённых клиентов.
Тесты на snapshot
В _models.yml:
snapshots:
- name: customers_snapshot
description: "SCD2 history of customers — tracks email, country, signup_date changes"
columns:
- name: customer_id
data_tests:
- not_null
- name: dbt_scd_id
data_tests:
- not_null
- unique
Шаг 7: Расширенные descriptions
Сейчас у нас минимум descriptions. Добавим больше — особенно для marts, которые видят BI-потребители.
Обнови models/marts/_models.yml:
version: 2
models:
- name: customers
description: |
Customer dimension table with lifetime aggregates.
One row per customer. Includes:
- Customer demographics (name, email, country)
- Activity dates (signup, first order, latest order)
- Lifetime spend in cents
- Tier classification (bronze/silver/gold) based on lifetime spend
Updated daily by production job 'jaffle-daily-build'.
columns:
- name: customer_id
description: "Unique customer identifier. Primary key. Renamed from raw_customers.id"
data_tests:
- not_null
- unique
- name: customer_name
description: "Full name as provided at signup"
- name: email
description: "Customer email, lowercased and trimmed"
- name: country_code
description: "ISO 3166-1 alpha-2 country code"
- name: country_name
description: "Full country name from country_codes seed"
- name: signup_date
description: "Date the customer registered"
- name: orders_count
description: "Total number of orders placed by this customer (lifetime)"
- name: lifetime_spend_cents
description: |
Lifetime spend in cents (integer to avoid floating-point errors).
Sum of total_paid_cents across all orders.
- name: first_order_at
description: "Timestamp of first order. NULL if customer has never ordered"
- name: latest_order_at
description: "Timestamp of most recent order. NULL if customer has never ordered"
- name: customer_tier
description: |
Tier classification:
- **gold**: lifetime spend > 500$ (50000 cents)
- **silver**: lifetime spend > 100$ (10000 cents)
- **bronze**: lifetime spend <= 100$ (or no orders)
Thresholds configured in dbt_project.yml vars.
data_tests:
- accepted_values:
values: ['bronze', 'silver', 'gold']
- name: orders
description: |
Order fact table with item and payment aggregates.
One row per order. Joins:
- stg_jaffle__orders (base data)
- int_payments_joined (payment aggregates)
- int_order_items_pivoted (item aggregates)
columns:
- name: order_id
description: "Unique order identifier"
data_tests:
- not_null
- unique
- name: customer_id
description: "FK to customers.customer_id"
data_tests:
- relationships:
to: ref('customers')
field: customer_id
- name: order_placed_at
description: "Timestamp when order was placed"
- name: order_status
description: "Order lifecycle status: delivered | returned | pending"
data_tests:
- accepted_values:
values: ['delivered', 'returned', 'pending']
- name: total_items_count
description: "Total quantity of items in the order"
- name: order_total_cents
description: "Total order amount in cents (sum of item line totals)"
- name: total_paid_cents
description: "Sum of payments received for this order in cents"
- name: payment_status
description: |
Payment status derived from total_paid_cents vs order_total_cents:
- **paid_full**: amounts match
- **paid_partial**: paid > 0 but less than order_total
- **unpaid**: no payments received
data_tests:
- accepted_values:
values: ['paid_full', 'paid_partial', 'unpaid']
- name: revenue_daily
description: |
Daily revenue aggregates for monthly_revenue_dashboard.
One row per day. Counts orders, unique customers, gross/paid/unpaid revenue.
Used by Looker dashboard "Monthly Revenue" (see exposures).
columns:
- name: order_date
description: "Date of order placement"
data_tests:
- not_null
- unique
- name: orders_count
description: "Count of distinct orders on this date"
- name: unique_customers
description: "Count of distinct customers who placed orders this date"
- name: gross_revenue_cents
description: "Total order amount placed (regardless of payment)"
- name: paid_revenue_cents
description: "Total payments received for orders this date"
- name: unpaid_revenue_cents
description: "gross - paid: unpaid amounts for this date"
Это production-уровень documentation. Каждая колонка описана, нетривиальные — подробно. Через 6 месяцев и для нового аналитика — golden source of truth.
Шаг 8: Doc blocks
Если у тебя одна и та же колонка повторяется в нескольких моделях (например, customer_id в 10 моделях), пиши description руками 10 раз — анти-паттерн. Лучше — doc block.
Создай models/docs.md:
{% docs customer_id %}
Unique customer identifier. Primary key in customers table, foreign key everywhere else.
Originally from raw_customers.id, renamed for consistency.
Type: INTEGER, sequential starting from 1.
{% enddocs %}
{% docs order_id %}
Unique order identifier. Primary key in orders, foreign key in order_items and payments.
Originally from raw_orders.id.
Type: INTEGER, sequential starting from 1001.
{% enddocs %}
{% docs cents_columns_note %}
All monetary values stored as integers in cents to avoid floating-point precision issues.
To convert to dollars: column / 100.0
{% enddocs %}
Теперь в YAML ссылайся через {{ doc('...') }}:
columns:
- name: customer_id
description: "{{ doc('customer_id') }}"
- name: lifetime_spend_cents
description: |
Lifetime spend.
{{ doc('cents_columns_note') }}
Преимущества:
- Изменение в одном месте — обновляет все ссылки.
- Длинные описания не загромождают YAML.
- В docs UI ссылка ведёт на developed view.
Шаг 9: Exposures
Exposure — это декларация downstream использования твоей модели. Помогает:
- Понимать, что куда идёт (lineage всего пайплайна, не только dbt)
- Уведомлять владельцев BI о изменениях
- В Cloud Explorer показывает зависимые dashboards
Создай models/marts/_exposures.yml:
version: 2
exposures:
- name: monthly_revenue_dashboard
type: dashboard
maturity: high
url: "https://looker.jaffle-shop.com/dashboards/42-monthly-revenue"
description: |
Monthly revenue dashboard for executive team.
Shows:
- Daily revenue trend (last 90 days)
- Top customers by lifetime value
- Tier distribution (bronze/silver/gold counts)
- Country revenue breakdown
Refreshed daily at 09:00 UTC.
depends_on:
- ref('customers')
- ref('revenue_daily')
owner:
name: Finance Team
email: [email protected]
После dbt docs generate exposure появится в Explorer как узел в lineage-графе, со стрелками от моделей customers и revenue_daily.
Это полезно для:
- Change management: если ты хочешь удалить
customers.customer_tier, видишь, что это поле используется вmonthly_revenue_dashboard— нужно сначала договориться с Finance. - Documentation: PM открывает Explorer и понимает, какие модели feed-ят какие дешборды.
- Governance: можно фильтровать по
tag:pii exposure:public— какие PII-данные идут в публичные дешборды.
Можно добавить несколько exposures:
exposures:
- name: monthly_revenue_dashboard
type: dashboard
...
- name: customer_lifecycle_email_campaign
type: application
maturity: medium
description: "Email campaign target list: identifies inactive gold customers"
depends_on:
- ref('customers')
owner:
name: Marketing Team
email: [email protected]
- name: monthly_finance_report
type: notebook
maturity: low
description: "Jupyter notebook for monthly P&L"
depends_on:
- ref('revenue_daily')
- ref('orders')
owner:
name: Finance Team
email: [email protected]
Типы exposure:
dashboard— BI dashboardapplication— приложение, использующее данныеnotebook— Jupyter/notebook анализanalysis— ad-hoc analysisml— ML model
Maturity:
low— экспериментальный, может сломатьсяmedium— стабильный, но не criticalhigh— production, breaking changes требуют коммуникации
Шаг 10: Generate и просмотр docs
dbt docs generate
dbt docs serve --port 8080
Открой http://localhost:8080:
- Найди модель
customersв дереве слева. - Прочитай description — должно быть всё, что мы написали в YAML.
- Открой column
customer_tier— увидишь подробное описание. - Кликни «View Lineage Graph» — увидишь весь DAG проекта от seeds до exposures.
- Найди exposure
monthly_revenue_dashboard— она должна быть в lineage с подключёнными моделями.
Это production-минимум documentation. Можно показать на собеседовании со словами «вот мой dbt-проект».
Чек-лист этого урока
snapshots/customers_snapshot.sql— SCD2 на customers через timestamp strategy.dbt snapshotсоздал таблицуsnapshots.customers_snapshotс dbt_valid_from/to.- Тесты на snapshot: not_null на customer_id, unique на dbt_scd_id.
- Расширенные descriptions в
_models.yml: model description + column description для каждой нетривиальной колонки. models/docs.mdс doc blocks для повторяющихся колонок (customer_id, order_id, cents_columns_note).models/marts/_exposures.yml— exposure для monthly_revenue_dashboard.dbt docs generate && dbt docs serve— открывается полная документация.
В следующем уроке — финальный билд, чек-лист самопроверки и что отправлять как результат capstone.