Learning Platform
Глоссарий Troubleshooting
Урок 22.03 · 22 мин
Средний
CapstoneSnapshotsDocumentationDoc blocksExposures

В прошлом уроке мы собрали 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 dashboard
  • application — приложение, использующее данные
  • notebook — Jupyter/notebook анализ
  • analysis — ad-hoc analysis
  • ml — ML model

Maturity:

  • low — экспериментальный, может сломаться
  • medium — стабильный, но не critical
  • high — 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 — открывается полная документация.
SCD Type 2: теория за customers_snapshot

В следующем уроке — финальный билд, чек-лист самопроверки и что отправлять как результат capstone.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 6. Snapshot customers_snapshot в capstone использует strategy='timestamp' с updated_at='signup_date'. Что это значит?

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

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

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

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