Зачем слои
DWH — это не одна таблица, не одна стадия. Это набор слоёв, каждый из которых решает свою задачу. Понимание слоёв — это фундамент проектирования любого современного data pipeline.
Простая аналогия: представь завод. Сырьё поступает на склад (raw). Потом проходит обработку — отделяется мусор, проверяется качество (staging). Потом идёт по производственной цепочке (transformations). На выходе — готовая продукция в магазин (marts). Если завод проектировать как “сырьё сразу в магазин” — получится хаос.
В DWH то же самое. Без слоёв любая ошибка в источнике катит по всей системе, любой refactor требует переписывать всё с нуля, никто не понимает, откуда взялись цифры в финальном отчёте.
Классическая четырёхслойная архитектура
От источников до BI через staging, core, marts
Разберём каждый слой подробно.
Layer 0: Source / Raw
Это сырые данные из источников, как они пришли. JSON-дампы из API, CSV экспорты, change-data-capture stream из Postgres.
-- В Snowflake: raw layer хранит JSON как VARIANT
CREATE TABLE raw.stripe_charges (
loaded_at TIMESTAMP,
source TEXT,
data VARIANT -- весь JSON payload
);
Зачем хранить сырые данные:
- Debug. Если что-то сломалось в downstream — есть к чему откатиться.
- History. Источник может изменить схему — у нас остаётся история до изменения.
- Recovery. Можно пересобрать все downstream слои из raw, если найдена ошибка в логике.
- Регуляторика. Часто требуется хранить исходные данные за N лет.
В современной архитектуре raw часто живёт в data lake (Parquet на S3), а не в самом DWH. Это дешевле — long-term storage в S3 стоит в десятки раз меньше, чем в Snowflake. Подробно — модуль 14.
Layer 1: Staging
В staging данные очищаются и типизируются, но структура остаётся близкой к source. То есть в staging-таблицах те же сущности, что в source, но:
- Парсятся JSON-поля в колонки.
- Типы приводятся к нормальным (string -> date, string -> number).
- Применяется deduplication.
- Фильтруется явный мусор (NULL keys, тестовые записи).
- Унифицируются форматы (timestamps в UTC, currency codes в ISO).
-- Staging: парсинг JSON в structured колонки
CREATE TABLE staging.stripe_charges AS
SELECT
data:id::TEXT AS charge_id,
data:amount::INTEGER / 100 AS amount,
data:currency::TEXT AS currency,
data:customer::TEXT AS customer_id,
TO_TIMESTAMP(data:created::INTEGER) AS created_at,
data:status::TEXT AS status,
loaded_at
FROM raw.stripe_charges
WHERE data:id IS NOT NULL;
Важно: staging не делает business logic — никаких JOIN-ов между источниками, никаких aggregations. Только чистка одной сущности. Это позволяет staging моделям быть простыми и независимыми.
Layer 2: Core / Integration
Здесь живёт бизнес-логика и интеграция источников. Происходят:
- JOIN-ы между источниками (Postgres + Stripe + Salesforce).
- Создание conformed dimensions (унифицированный
dim_customerиз всех источников). - Surrogate keys для будущих fact-таблиц.
- SCD Type 2 для исторических dimensions (модуль 8).
- Бизнес-правила: “клиент с >$1000 покупок = premium”, “категория продукта = по первому слову названия”.
-- Core: интеграция customer из Postgres и Salesforce
CREATE TABLE core.dim_customer AS
WITH unified AS (
SELECT
pg.customer_id AS source_id,
'postgres' AS source_system,
pg.email,
pg.name,
pg.country,
pg.signup_date
FROM staging.pg_customers pg
UNION ALL
SELECT
sf.account_id AS source_id,
'salesforce' AS source_system,
sf.email,
sf.account_name AS name,
sf.billing_country AS country,
sf.created_date AS signup_date
FROM staging.sf_accounts sf
),
dedup AS (
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY signup_date) AS rn
FROM unified
)
SELECT
ROW_NUMBER() OVER () AS customer_key, -- surrogate
email,
name,
country,
signup_date,
source_system,
source_id
FROM dedup
WHERE rn = 1;
Это single source of truth. Все downstream marts ссылаются на core.dim_customer, а не на staging-таблицы.
Layer 3: Marts
В marts строятся dimensional models для конкретных бизнес-доменов. Один mart — один домен (sales, marketing, finance, product analytics).
-- Marts: sales fact table
CREATE TABLE marts.sales.fact_orders AS
SELECT
o.order_id AS order_key,
c.customer_key,
p.product_key,
d.date_key,
o.qty,
o.unit_price,
o.qty * o.unit_price AS total_amount,
o.discount_amount,
o.qty * o.unit_price - o.discount_amount AS net_amount
FROM staging.orders o
JOIN core.dim_customer c ON o.customer_id = c.source_id
JOIN core.dim_product p ON o.product_id = p.source_id
JOIN core.dim_date d ON o.order_date = d.full_date;
Каждый mart — это star schema или OBT, оптимизированный под reads. Аналитики подключают BI tools к marts.
Layer 4: Presentation (опционально)
Для критичных дашбордов с tight latency requirements может строиться дополнительный слой aggregations:
-- Presentation: ежедневная агрегация для дашборда
CREATE TABLE presentation.daily_sales_by_country AS
SELECT
d.full_date,
c.country,
SUM(f.net_amount) AS revenue,
COUNT(DISTINCT f.customer_key) AS unique_customers,
COUNT(*) AS orders_count
FROM marts.sales.fact_orders f
JOIN marts.sales.dim_customer c ON f.customer_key = c.customer_key
JOIN marts.sales.dim_date d ON f.date_key = d.date_key
GROUP BY 1, 2;
Это даёт sub-second отклик для дашбордов. Цена — дублирование данных и риск рассинхронизации с marts. Используется только при чётком latency SLA.
Medallion архитектура в lakehouse
В мире lakehouse (Databricks, Iceberg, Delta Lake) появилась терминология bronze/silver/gold. Это та же концепция слоёв, но с другим neimingом и lake-flavored.
Lake-native terminology для тех же слоёв
| Classical DWH layer | Medallion |
|---|---|
| Raw / Source | Bronze |
| Staging | Silver (lower) |
| Core / Integration | Silver (upper) |
| Marts | Gold |
| Presentation | Gold (aggregated) |
Это те же концепции, переименованные для lake/lakehouse контекста. Если ты увидел “bronze/silver/gold” в Databricks-документации — знай, что это про layering. Используется в любом современном data pipeline.
Орchestrация слоёв
Слои нужно строить в правильном порядке: staging -> core -> marts. И поддерживать: при изменении источника пересчитать staging, потом core, потом marts.
В современной архитектуре это делает dbt (модуль 13). Каждый слой — это set of SQL models, dbt автоматически вычисляет зависимости и запускает в правильном порядке.
dbt: как ref() строит граф зависимостей между слоями# dbt project: models/
staging/
stg_pg_customers.sql # SELECT FROM raw.pg_customers
stg_stripe_charges.sql # SELECT FROM raw.stripe_charges
core/
dim_customer.sql # uses staging tables
dim_product.sql
dim_date.sql
marts/
sales/
fact_orders.sql # uses core tables
dbt run запустит staging -> core -> marts -> presentation в правильной последовательности.
Реальный пример: e-commerce полный pipeline
Полная картина для нашего e-commerce примера:
Sources:
Postgres (orders, customers, products)
Stripe (payments)
Mailchimp (campaigns)
Salesforce (B2B accounts)
↓
Raw (Bronze):
raw.pg_orders # JSON dumps from Debezium CDC
raw.stripe_charges
raw.mailchimp_campaigns
raw.sf_accounts
↓
Staging (Silver lower):
staging.orders # parsed, typed, deduplicated
staging.charges
staging.campaigns
staging.sf_accounts
↓
Core (Silver upper):
core.dim_customer # unified Postgres + Salesforce
core.dim_product # с category hierarchy inline
core.dim_date # календарь компании
core.fact_orders # combined orders + payments + items
↓
Marts (Gold):
marts.sales.fact_orders # для sales analytics
marts.sales.dim_*
marts.marketing.fact_campaigns # для marketing
marts.marketing.dim_*
↓
Presentation (Gold aggregated):
daily_sales_by_country
monthly_cohort_retention
campaign_attribution
↓
BI tools (Tableau, Looker, Metabase)
Это стандартная архитектура Modern Data Stack 2026. Каждый слой — material тable в Snowflake/BigQuery/Databricks. dbt управляет.
Для junior data engineer: запоминать четыре слоя как мнемонику. Любой data pipeline проходит через них. Понимание этой архитектуры — это 50% работы DE.
Антипаттерны
Антипаттерн 1: один слой “transformed”. Команда пишет один большой SQL: source -> final mart. При изменении логики нужно переписать всё. Невозможно понять, где ошибка. Решение — выделить слои.
Антипаттерн 2: business logic в staging. Staging выполняет JOIN-ы и aggregations. Когда нужно создать новый mart с другой логикой — приходится переписывать staging. Решение — staging не делает business logic.
Антипаттерн 3: дублирование между marts. Sales mart и marketing mart строят свой dim_customer отдельно. Со временем расходятся. Решение — conformed dimensions в core layer.
Антипаттерн 4: нет raw layer. Данные сразу loadятся в staging с трансформацией. При ошибке некуда откатиться. Решение — всегда сохранять source как есть.
Попробуй сам
- Возьми любой публичный pipeline (например, dbt-jaffle-shop на GitHub). Посмотри models/ — найди staging/, marts/. Запусти dbt run, посмотри dependency graph.
- Спроектируй слои для гипотетической задачи: блог-платформа (posts, authors, comments, page views). Какие raw, staging, core, marts таблицы будут?
- Реализуй мини-pipeline на DuckDB: возьми 2-3 CSV-источника, напиши SQL для каждого слоя (staging -> core -> mart). Сохраняй в Parquet каждый слой.
- Изучи Medallion в Databricks documentation. Сравни с классическими слоями — увидишь, это то же.