49 KiB
Хранилище данных: как устроена аналитика «под капотом»
Для тех, кто знает SQL, но хочет понять, как хранить данные не в Excel, а по-взрослому
Что вы уже умеете — и что узнаете здесь
✅ Уже знаете:
SELECT,JOIN,GROUP BY;- как посчитать сумму/среднее/количество по таблице.
🆕 Узнаете в этой статье:
- слои хранилища (STG → ODS → DDS → DM) и зачем они нужны;
- факты и измерения — основные кирпичики аналитики;
- SCD Type 2 — как хранить историю изменений клиента (например, смену email или города);
- суррогатные ключи (SK) и чем они отличаются от обычных
id; - четыре модели данных: 3NF, Звезда (Star), Data Vault, Anchor Modeling — и когда какую использовать.
⛔ Не будем говорить здесь о:
- физическом хранении (партиции, индексы, ClickHouse-движки);
- распределённых кластерах (Kafka, Spark, Airflow — это отдельный курс);
- настройке производительности (
EXPLAIN, кэши и т.п.).
Это — про логику, структуру и здравый смысл.
1. Введение: почему нельзя просто SELECT из базы заказов?
Представьте: вы — аналитик в интернет-магазине. Вам нужно ответить на вопрос:
«Сколько заказов сделал клиент с email
a@ex.comза 2023 год, и сколько он потратил?»
Вы идёте в базу заказов — и… не находите email. Он в CRM. Идёте в CRM — там нет сумм заказов. Возвращаетесь в заказы — сумма есть, но только текущая цена товара. А в 2023 году цена была другой!
Знакомо? Это — проблема OLTP-систем (оперативного учёта):
- CRM, склад, платёжка — это разные базы;
- каждая оптимизирована под быструю запись операций («добавить заказ», «списать товар»);
- историю там не хранят — email меняется «в лоб»: старое значение перезаписывается.
Такие системы называют OLTP (Online Transaction Processing — обработка транзакций в реальном времени).
А для аналитики нужна OLAP (Online Analytical Processing — обработка запросов на анализ).
➡️ Хранилище данных (Data Warehouse, DWH) — это как «единая карта сокровищ», куда собирают данные из всех источников, сохраняя историю, выравнивая термины и готовя их к анализу.
И вот главный секрет его успеха: слоистая архитектура.
2. Учебный пример: интернет-магазин
Чтобы всё было на пальцах — разберём простой, но живой пример.
У нас есть 6 таблиц из трёх источников:
| Таблица | Источник | Что содержит |
|---|---|---|
customers |
CRM | Клиенты: customer_id, email, phone, city |
orders, order_items |
Заказы | Заказы и позиции в них |
products |
Склад | Товары: product_id, name |
prices |
Склад | История цен: product_id, valid_from, valid_to, price |
promos |
Маркетинг | Акции: promo_id, code |
⚠️ Обратите внимание:
customer_id = 101в одном месяце —a@ex.com, в другом —b@ex.com;- цена на товар
9001(Phone) в январе — 100 ₽, в феврале — 110 ₽; order_itemsсодержитprice_at_sale— цену в момент покупки, а не текущую.
Это уже намёк: чтобы посчитать выручку 2023 года, нам нужна не текущая цена, а та, что была в день заказа.
(ER-диаграмма и DDL-примеры — в конце статьи, в разделе «Для практики».)
3. Зачем делить DWH на слои?
Представьте, что вы строите дом. Вы же не будете сразу вбивать гвозди в стены — сначала:
- Привезли стройматериалы (песок, доски, кирпич) — сырьё;
- Очистили, просеяли, нарезали — обработка;
- Собрали каркас, провели коммуникации — интеграция;
- Сделали отделку под конкретную квартиру — готовое решение.
В DWH — то же самое. Каждый слой отвечает за одну задачу:
flowchart TD
subgraph Sources["Источники"]
A["CRM"]
B["Заказы"]
C["Склад"]
end
subgraph STG["STG — «Сырьё»"]
D["Таблицы-дубликаты</br>в формате источника"]
end
subgraph ODS["ODS — «Очистка»"]
E["Типы:</br>даты → DATE,</br>числа → INT/DECIMAL</br>Валидация: email, phone"]
end
subgraph DDS["DDS — «Интеграция»"]
F["Общие сущности:</br>клиент, товар, дата</br>История (SCD),</br>суррогатные ключи"]
end
subgraph DM["DM — «Готовые решения»"]
G["Витрина продаж: дата, товар, клиент, сумма</br>+ агрегаты (выручка/день)"]
end
A --> STG
B --> STG
C --> STG
STG --> ODS
ODS --> DDS
DDS --> DM
DM --> BI["BI-системы</br>(Power BI, Tableau,</br>Metabase)"]
👉 Почему так лучше, чем «одна большая таблица»?
- Управляемость: если в
customersпришёл битыйemail— ошибка локализована в STG/ODS, DDS не пострадает. - Прозрачность: можно посмотреть: «а как выглядел исходник?», «а как мы его почистили?».
- Производительность: в DDS и DM — только то, что нужно для анализа. Никаких
JSON-полей,TEXTбез причины.
4. Путешествие данных: от STG до DM
Давайте проследим, как превращается строка заказа.
STG (Staging / Bronze) — «как пришло»
- Таблицы:
stg.orders_raw,stg.customers_raw; - Структура — точно как в источнике (может быть
VARCHARдаже у дат); - Добавлены технические поля:
_load_id— идентификатор загрузки;_load_ts— время получения данных;
- Главное правило: неизменяемость. Если пришла новая порция — либо добавляем новые строки, либо полностью перезагружаем слой (идемпотентность).
💡 Пример:
stg.orders_rawсодержит"2024-01-10"как строку — это нормально. Главное — не потерять оригинал.
ODS (Operational Data Store / Silver) — «почистили, но не трогали смысл»
- Таблицы:
ods.orders,ods.customers; - Здесь:
- привели
order_dateк типуDATE; - убрали заказы без клиента (
customer_id IS NULL→ ошибка или флаг); - привели телефоны к формату
79991112233; - проверили email на валидность (регуляркой или простой проверкой).
- привели
- Но! Не объединяем клиента из CRM и клиента из заказов — это будет позже.
- Пока — никакой бизнес-логики. Только техническая очистка.
- Дедупликация: если два раза пришёл один и тот же заказ — оставляем один (по
order_id + _load_ts).
🎯 Цель ODS — дать «надёжную платформу» для следующего слоя. Как сухое, чистое бревно перед сборкой дома.
DDS (Data Delivery Store / Core / Conformed) — «интеграция + история»
Здесь рождается единая бизнес-модель.
Появляются понятия: измерения, факты, суррогатные ключи, SCD.
Например:
| Таблица | Назначение |
|---|---|
dds.dim_customer |
Измерение «Клиент» с историей (SCD Type 2) |
dds.dim_product |
Измерение «Товар» |
dds.dim_date |
Готовый календарь на 10 лет вперёд (день/неделя/месяц/квартал) |
dds.fact_sales |
Факт «Продажа» — строка заказа с суммой и количеством |
💡 Суррогатный ключ (Surrogate Key, SK) — это BIGINT, который мы генерируем сами (например, customer_sk = 1001).
Бизнес-ключ (Business Key, BK) — это customer_id = 101 из источника.
Мы храним и то, и другое — чтобы можно было и джойнить, и понимать, откуда строка.
✅ Почему не использовать
customer_idнапрямую?
— Потому что в одном источникеcustomer_id— целое число, в другом — строкаCUST-101.
— Потому что ID могут повторяться (например, в тестовой и продовой базах).
— Потому что нам нужна связь с историей: у клиента с BK =101может быть 3 версии вdim_customer.
DM (Data Mart / Gold/ «Витрины») — «готово к употреблению»
Здесь — таблицы и представления для конкретных задач:
dm.mart_daily_sales— ежедневные продажи по товарам и сегментам;dm.mart_customer_360— полный портрет клиента: сколько потратил, когда заходил, какие товары любит.
Они часто построены по модели Звезда (Star Schema) — потому что BI-инструментам так удобнее всего.
5. Базовые понятия: факты, измерения, SCD
Представьте отчёт:
«10 января 2024 года клиент из Москвы (сегмент Premium) купил Phone за 100 ₽».
В DWH это разложится на:
- Факт (Fact) — событие, которое можно измерить: покупка.
Хранится вfact_sales:quantity = 1,amount = 100. - Измерения (Dimensions) — контекст факта:
dim_date→ 10 января 2024;dim_customer→ Москва, Premium;dim_product→ Phone.
erDiagram
dim_date ||--o{ fact_sales : "дата"
dim_customer ||--o{ fact_sales : "клиент"
dim_product ||--o{ fact_sales : "товар"
dim_date {
int date_key PK "YYYYMMDD"
date calendar_date "сама дата"
int year
int month
int day
varchar dow "день недели"
}
dim_customer {
bigint customer_sk PK "суррогатный ключ"
varchar customer_bk "бизнес-ключ, напр. '101'"
varchar customer_name
varchar email
varchar city
date valid_from "SCD2: с какой даты запись актуальна"
date valid_to "SCD2: по какую дату актуальна"
boolean is_current "SCD2: текущая версия?"
}
dim_product {
bigint product_sk PK
varchar product_bk "код товара / артикул"
varchar product_name
varchar category
}
fact_sales {
bigint sale_id PK
int date_key FK "ссылка на dim_date.date_key"
bigint customer_sk FK
bigint product_sk FK
int quantity
decimal amount
}
SCD Type 2 — как хранить историю
Клиент №101:
- с 1 янв по 15 мая —
email = a@ex.com,city = Москва; - с 16 мая —
email = b@ex.com,city = Москва; - с 1 окт —
email = b@ex.com,city = Санкт-Петербург.
В dim_customer это будет три строки:
| customer_sk | customer_bk | city | valid_from | valid_to | is_current | |
|---|---|---|---|---|---|---|
| 1001 | 101 | a@ex.com | Москва | 2023-01-01 | 2023-05-15 | false |
| 1002 | 101 | b@ex.com | Москва | 2023-05-16 | 2023-09-30 | false |
| 1003 | 101 | b@ex.com | СПб | 2023-10-01 | 9999-12-31 | true |
Когда мы считаем продажи за 12 января — джойним fact_sales к той строке dim_customer, где:
fact_sales.order_date BETWEEN dim_customer.valid_from AND dim_customer.valid_to
и получаем актуальный на тот день email и город.
🔍 Подробнее про SCD — в отдельной статье Slow Changing Dimensions (сравнение Type 1/2/3, паттерны обновления).
Теперь, когда мы разобрались, что такое факты, измерения и SCD, давайте посмотрим, как именно можно устроить слой DDS внутри — есть несколько вариантов.
6. Модели данных для слоя DDS: 4 подхода — и когда какой выбрать
В DDS мы можем хранить данные по-разному. Это не «правильно/неправильно», а выбор под задачу.
1. 3NF (третья нормальная форма)
Источник: Билл Инмон (Bill Inmon)
✅ Плюсы:
- Минимум избыточности при строгих ключах и правилах дедупликации.
- Проще поддерживать единую терминологию и НСИ (reference data).
- Атрибуты и справочники легко расширять.
❌ Минусы:
- Много JOIN даже для простых отчётов.
- Историчность (SCD2) усложняет таблицы.
- Новые источники дороже гармонизировать (привести к канону).
📌 Когда выбирать:
→ Корпоративные DWH, где важна единая терминология и долгосрочная поддержка.
→ Стабильные домены (финансы, НСИ, договоры) и умеренная динамика изменений.
2. Звезда (Star Schema)
Источник: Ральф Кимболл (Ralph Kimball)
✅ Плюсы:
- Простота: факт + несколько «плоских» измерений;
- Скорость: BI-системы любят звезду — запросы пишутся за 5 минут;
- Понятно бизнесу: «продажи по товарам и клиентам» — это ровно то, что в таблицах.
❌ Минусы:
- Дублирование: город будет повторяться в каждой строке клиента;
- Изменение структуры измерения — дорого (перестроить всю витрину).
📌 Когда выбирать:
→ Витрины (DM), а не ядро (DDS);
→ Начинающим командам и MVP;
→ Когда отчёты — главная цель.
3. Data Vault 2.0 — «конструктор Lego» для больших DWH
Идея: Дэн Линстедт (Dan Linstedt). Цель — собирать DWH из повторно используемых блоков, не боясь, что новый источник «сломает» всё, что было до него.
С чего начать? Представьте конструктор
У вас есть коробка Lego. В ней:
- Красные кирпичи (Хабы) — это «что или кто»: клиент, заказ, товар.
У них нет цвета, надписей — только форма (бизнес-ключ). Главное — узнать, что это «один и тот же клиент №101», даже если его email менялся трижды. - Серые соединители (Линки) — это «как связаны»: «заказ №5001 создан клиентом №101».
Не сами объекты, а связь между ними. И даже эту связь можно «отвязать» и привязать по-другому — без пересборки кирпичей. - Жёлтые наклейки (Сателлиты) — это «какие у них свойства»: имя клиента, статус заказа, цена товара.
И самое важное: каждый раз, когда что-то изменилось — наклеиваем новую новую наклейку, не стирая старую. Так у нас остаётся полная история.
💡 Главный принцип DV:
«Идентичность — навсегда. Свойства — меняются. Связи — тоже могут меняться. И всё это — отдельно.»
erDiagram
%% Хабы - красные кирпичи (идентичность)
%% Связи между компонентами
HUB_CUSTOMER ||--o{ SAT_CUSTOMER_INFO : "имеет"
HUB_CUSTOMER ||--o{ LINK_ORDER_CUSTOMER : "участвует_в"
HUB_ORDER ||--o{ LINK_ORDER_CUSTOMER : "создан"
LINK_ORDER_CUSTOMER ||--o{ SAT_ORDER_STATUS : "имеет_статус"
HUB_CUSTOMER {
varchar customer_bk PK "Бизнес-ключ: '101'"
varchar record_source
timestamp load_dttm
}
HUB_ORDER {
varchar order_id PK "Бизнес-ключ: '5001'"
varchar record_source
timestamp load_dttm
}
%% Линки - серые соединители (связи)
LINK_ORDER_CUSTOMER {
varchar link_key PK "Хэш от (order_id+customer_bk)"
varchar order_id FK "→ HUB_ORDER"
varchar customer_bk FK "→ HUB_CUSTOMER"
varchar record_source
timestamp load_dttm
}
%% Сателлиты - жёлтые наклейки (атрибуты + история)
SAT_CUSTOMER_INFO {
varchar customer_bk FK "→ HUB_CUSTOMER"
varchar hashdiff
timestamp valid_from
timestamp valid_to
varchar name
varchar email
varchar city
varchar record_source
timestamp load_dttm
}
SAT_ORDER_STATUS {
varchar link_key FK "→ LINK_ORDER_CUSTOMER"
varchar hashdiff
timestamp valid_from
timestamp valid_to
varchar status
varchar record_source
timestamp load_dttm
}
Как читать схему:
🔴 Красные блоки (Хабы) = «Кто/что это?» — только идентичность, никаких атрибутов.
⚪ Серые блоки (Линки) = «Как связаны?» — соединяют хабы, фиксируют отношения.
🟡 Жёлтые блоки (Сателлиты) = «Какие свойства?» — хранят атрибуты + историю изменений.💡 Правило конструктора: Чтобы добавить новый источник (например, мобильное приложение), мы просто «приклеиваем» новый сателлит к существующему хабу — не перестраивая всю модель.
Почему это не просто SCD Type 2 «по-другому»?
В SCD Type 2 (в dim_customer) мы смешиваем:
- идентичность (
customer_bk = 101), - и атрибуты (
email,city), - и связь с бизнес-событиями (
valid_from,is_current).
В Data Vault эти три вещи разнесены по разным таблицам:
hub_customer— толькоcustomer_bkи технические поля (откуда пришёл, когда загрузили);sat_customer_info— только атрибуты + их история (как в SCD, но отдельно!);link_order_customer— только «кто сделал заказ» (и даже тут — отдельный сателлит может хранить статус связи: «активный», «отменённый» и т.д.).
➡️ Это даёт огромный бонус:
Если завтра пришёл новый источник — например, мобильное приложение — и там у клиента есть device_id,
вы не перестраиваете dim_customer (как в Kimball/3NF),
а просто добавляете новый сателлит — sat_customer_device.
Старые отчёты продолжают работать. Новые — используют новую наклейку.
Как устроен «сырой» Data Vault (упрощённо)
| Таблица | Внутри | Пример |
|---|---|---|
hub_customer |
customer_bk (например, '101') + load_dttm + source |
Клиент «101» появился в CRM 10 января |
sat_customer_info |
customer_bk, email, city, valid_from, valid_to, load_dttm |
10 янв–15 мая: a@ex.com, Москва; 16 мая–…: b@ex.com, Москва |
hub_order |
order_id + load_dttm + source |
Заказ «5001» из системы заказов |
link_order_customer |
order_id, customer_bk + load_dttm + source |
Заказ 5001 → клиент 101 |
sat_order_status (опционально) |
order_id, status, valid_from, valid_to |
10 янв: «оплачен», 12 янв: «доставлен» |
⚠️ *На практике вместо
customer_bkчасто используют хэш-ключ (hk_customer) — чтобы сравнивать быстрее и избежать проблем с типами (число vs строка).
А как же отчёты? Где витрины?
Хороший вопрос. Сырой DV — не для BI. Это «склад запчастей».
Чтобы собрать «машину», нужен второй шаг: Business Vault (в DV 2.0 это — обязательная часть!).
Там уже:
- PIT-таблицы (Point-in-Time) — «какой клиент был 12 января 2024?» за 1 JOIN, а не за 5 оконных функций;
- Bridge-таблицы — готовые пути: «заказ → клиент → последняя версия профиля»;
- Derived-таблицы — после бизнес-логики (скидки, сегментация, флаги).
А уже из них строят витрины в формате Звезды — те самые mart_daily_sales, которые вы подключаете в Power BI.
🔁 То есть цепочка:
Источник → Hub/Link/Sat (Raw DV) → PIT/Bridge (Business Vault) → Star Schema (DM)
Можно строить и напрямую, без PIT/Bridge таблиц. Но это значительно "тяжелее" с точки зрения затрат вычислительных ресурсов.
Плюсы и минусы — честно и без прикрас
| ✅ Плюсы | ❌ Минусы |
|---|---|
| История «из коробки» — каждое изменение видно, ничего не перезаписывается | Сырой слой тяжел для чтения — нужен Business Vault (доп. работа) |
| Добавить источник — легко (новый сателлит, не ломая старое) | Новых таблиц много (Hub/Link/Sat — уже 3 на простую сущность) |
| Полная трассировка: кто, откуда, когда привёз каждую строчку | Требует дисциплины: если record_source забыли — аудит сломан |
| Параллельная работа: одна команда — клиенты, другая — заказы | Не для MVP: окупается на 10+ источниках, не на двух CSV |
Когда Data Vault — ваш выбор?
| Выберите DV 2.0, если… | Не начинайте с DV, если… |
|---|---|
| У вас 5+ разнородных систем (CRM, ERP, мобильные приложения, внешние API) | У вас 1–2 источника и нужно быстро дать отчёт |
| Схемы часто меняются (новые атрибуты, новые связи) | Схема стабильна (например, учёт договоров в банке) |
| Важен аудит: «кто и когда изменил email?» (финтех, госсектор) | Отчёты — главная цель, а не соответствие регуляторам |
| В будущем — масштабирование: новые домены, новые команды | Вы один или вдвоём, и хотите простоты |
🎯 Совет для новичка:
Пока учитесь — не пытайтесь собрать DV вручную. Но понимать его логику — обязательно.
Почему? Потому что многие современные enterprise-DWH сегодня — либо DV, либо гибриды (DV + Star).
Даже если вы будете работать с витринами — вы будете понимать, откуда берутся данные и почему история «не обновляется».
Важно: DV 1.0 vs DV 2.0 — в чём разница?
-
DV 1.0 — только Hub/Link/Sat + историзация через
load_dttm.
Проблема: нет стандартного способа хранить периоды действия (как в SCD Type 2) —valid_from/valid_toприходилось изобретать самим. -
DV 2.0 — стандартизирует:
- Effectivity Satellites (с
valid_from/valid_to) — для явной историчности, - Multi-Active Satellites — когда у объекта несколько одновременных значений (например, три активных телефона),
- Business Vault — как обязательный слой перед витринами.
- Effectivity Satellites (с
➡️ Сегодня, когда говорят «Data Vault», почти всегда имеют в виду DV 2.0.
4. Anchor Modeling (анкерное моделирование)
Источник: Ларс Рёне (Lars Rönnbäck)
Ещё более атомарный подход:
- Anchor — сущность (клиент, товар);
- Attribute — атрибут (email, имя);
- Tie — связь (как Link в DV);
- Все таблицы — 2–3 столбца.
✅ Плюсы:
- Максимальная гибкость: поменяли модель — не трогали старые таблицы;
- Бесконечная эволюция: можно добавлять атрибуты «задним числом».
❌ Минусы:
- Очень сложные запросы (JOIN’ов — десятки);
- Почти не используется «в чистом виде» — чаще как концепция.
📌 Когда выбирать:
→ Экспериментальные проекты;
→ Когда схема данных каждый месяц радикально меняется.
Сравнение моделей — наглядно
quadrantChart
title Где какая модель? (интуитивно)
x-axis "Низкая сложность → Высокая сло́жность"
y-axis "Низкая гибкость → Высокая гибкость"
"Звезда": [0.2, 0.3]
"3NF": [0.6, 0.5]
"Data Vault": [0.8, 0.8]
"Anchor": [0.95, 0.95]
🎯 Вывод: нет «лучшей» модели. Есть подходящая под контекст.
— Для обучения — Звезда (просто, наглядно).
— Для корпоративного DWH — 3NF + Звезда на выходе.
— Для масштабируемой интеграции — Data Vault.
7. Практикум: как собрать первую витрину
Покажем на примере mart_daily_sales — таблицу, которую можно сразу подключить к BI.
Этапы сборки
- Из STG → ODS:
stg.orders_raw→ods.orders(привелиorder_dateкDATE);
- Из ODS → DDS:
ods.customers→dds.dim_customer(SCD Type 2);ods.products→dds.dim_product;ods.orders+ods.order_items→dds.fact_sales;
- Из DDS → DM:
fact_sales+dim_*→mart_daily_sales.
flowchart LR
STG[stg.orders_raw] --> ODS[ods.orders]
ODS --> DDS[dds.fact_sales]
dds.dim_customer --> DDS
dds.dim_product --> DDS
dds.dim_date --> DDS
DDS --> DM[mart_daily_sales]
DM --> BI[Power BI]
Готовые SQL-скрипты
Все необходимые скрипты для построения хранилища находятся в папке sql/:
01_ddl_stg-dds.sql— создание схем и таблиц (STG, ODS, DDS)02_dml_stg-dds.sql— загрузка данных и трансформация03_demo_increment.sql— инкрементальная загрузка и SCD204_validation.sql— проверки качества данных05_ddl_dm.sql— создание витрин (Data Marts)06_dml_dm.sql— наполнение витрин данными
Пример SQL-запроса для витрины
-- mart_daily_sales: ежедневные продажи с сегментацией
CREATE MATERIALIZED VIEW dm.mart_daily_sales AS
SELECT
d.date_actual AS order_date,
p.product_name,
c.customer_segment, -- например: 'Premium', 'Basic'
SUM(f.quantity) AS total_qty,
SUM(f.amount) AS total_revenue
FROM dds.fact_sales f
JOIN dds.dim_date d
ON f.date_key = d.date_key
JOIN dds.dim_product p
ON f.product_sk = p.product_sk
JOIN dds.dim_customer c
ON f.customer_sk = c.customer_sk
AND f.order_date BETWEEN c.valid_from AND c.valid_to -- SCD!
WHERE c.is_current = true -- или не фильтровать — тогда будет история
GROUP BY d.date_actual, p.product_name, c.customer_segment;
💡 Материализованное представление (MATERIALIZED VIEW) — это «кэш» результата. Обновляется по расписанию (например, ночью).
8. Как выбрать модель данных? Советы от практиков
Выбор модели — не техническая задача, а стратегическая.
Это как решать: строить дом из кирпича, дерева или SIP-панелей. У каждой технологии — свои плюсы, но главное — подходит ли она вам сегодня.
🔹 Главное, что нужно понять новичку:
Не существует «самой правильной» модели.
Есть самая подходящая под ваш контекст — и он у всех разный.
🛑 Что делать не стоит (если опыта ещё мало):
| Что делать не стоит | Почему |
|---|---|
| Брать Data Vault «потому что модно» | DV требует глубокого понимания интеграции, CDC, идемпотентности. Без этого легко получить «историю», в которой невозможно найти актуальные данные. |
| Строить сложную 3NF «как в книжках» под 10 таблиц | Если у вас 2–3 источника — вы потратите недели на нормализацию, чтобы потом делать 5 JOIN’ов ради простого отчёта. |
| Пытаться «сделать сразу гибко на 5 лет вперёд» | Гибкость = сложность. А сложность = баги, задержки, выгорание команды. |
✅ Базовые советы — с чего начать, если вы учитесь или делаете первый DWH
-
Начните с витрины в формате Звезды (Star Schema).
— Это просто: одна таблица фактов + несколько «плоских» измерений.
— Это быстро: отчёт в BI — за 10 минут.
— Это понятно: даже менеджер поймёт структуру. -
Стройте DDS только когда это действительно нужно.
— Если источников ≤ 3 и они стабильны — можно идтиods → dmнапрямую.
— Если появляются расхождения («email в CRM и в заказах — разные») — тогда заводитеdds.dim_customerи другие общие сущности. -
Историю (SCD) включайте постепенно.
— Сначала — без истории (Type 1: просто обновляете строку).
— Потом — только для ключевых сущностей (клиент, товар, договор).
— Только потом — думайте про DV или полную историзацию всего. -
Если сомневаетесь — спросите: «А кто будет этим пользоваться?»
Выбор модели начинается не с технологий, а с вопроса: кто будет работать с результатом?
Это как выбрать инструмент в мастерской: для гвоздей — молоток, для саморезов — отвёртка.Вот как это выглядит на практике:
— Аналитик в Metabase / Looker Studio → Звезда (Star Schema)
Почему: ему нужны готовые метрики без сложных JOIN’ов. Звезда даёт понятные таблицы: «продажи по дням и товарам» — без углубления в атомарные сущности.— BI-разработчик в Power BI / Tableau → Звезда
Почему: все инструменты визуализации оптимизированы под star schema. Один факт + несколько измерений = быстрые отчёты и простою модель.— Инженер ML (Data Scientist / ML-инженер) → 3NF или сырые ODS-таблицы
Почему: для фичей нужны атомарные события и детальные атрибуты. Машинное обучение ценит полноту и детализацию данных больше, чем удобство отчётов.— Юрист, финансовый контролёр, аудитор / регулятор → 3NF с SCD Type 2, иногда + DV в ядре
Почему: им нужна доказуемая история изменений, но не инфраструктурная сложность DV на каждый чих.
Обычно достаточно хранить аудит-историю по ключевым сущностям (клиенты, договоры, счета) в формате 3NF + SCD Type 2.
Data Vault имеет смысл только если у вас 10+ разнородных источников и жёсткие требования по аудиту и трассировке.
💡 Золотое правило:
«Собирай данные как DV (максимально детально), показывай как Звезду (максимально просто).»
На начальных этапах вам почти всегда хватит SCD Type 2 в рамках 3NF/Звезды.
Data Vault нужен тогда, когда основная боль — интеграция множества систем и аудит, а не «первый отчёт для маркетинга».
💡 Ещё один совет от практиков
Лучше сделать простую модель — и вовремя переделать,
чем сделать «идеальную» — и застрять на этапе проектирования.
Переделать Звезду → Звезду с SCD Type 2 — относительно легко.
Переделать «недоделанный DV» → что-то рабочее — в разы сложнее.
📌 Кратко — что выбрать сегодня, если вы только учитесь
| У вас… | Делайте… |
|---|---|
| Учебный проект, 1–2 CSV | ods → dm по модели Звезда (без DDS, без истории) |
| Первый рабочий DWH, 3–5 источников | stg → ods → dds (3NF или простая Звезда) → dm (Звезда) |
| Команда из 1 инженера + 1 аналитика | Не трогайте DV и Anchor — они «съедят» ваше время без отдачи |
А когда наберётесь опыта — приходите в DV. Он того стоит. Но не раньше времени.
9. Эксплуатация: качество данных — это не «опция»
Самая красивая архитектура бессмысленна, если в mart_daily_sales — нули.
Поэтому в каждом слое — контроль качества (DQ, Data Quality).
graph TB
A[Данные поступили] --> B{Проверка качества}
B --> C["Уникальность: order_id — уникален?"]
B --> D["Полнота: email не NULL?"]
B --> E["Валидность: order_date — дата?"]
B --> F["Свежесть: данные за сегодня?"]
C --> G{OK?}
D --> G
E --> G
F --> G
G -->|Да| H[Загрузить в следующий слой]
G -->|Нет| I[Оповещение + остановка пайплайна]
Примеры проверок (на SQL):
-- Проверка уникальности order_id в ODS
SELECT order_id, COUNT(*)
FROM ods.orders
GROUP BY order_id
HAVING COUNT(*) > 1;
-- Проверка свежести: есть ли данные за вчера?
SELECT 'OK' WHERE EXISTS (
SELECT 1 FROM ods.orders
WHERE order_date = CURRENT_DATE - INTERVAL '1 day'
);
🔔 Совет: делайте DQ-тесты частью CI/CD — как unit-тесты в коде.
10. Заключение: главное — понимать «почему»
Хранилище данных — это не про «крутые технологии», а про мышление:
-
Слои (STG→ODS→DDS→DM) — это про разделение ответственности.
Не смешивайте сырые данные и аналитические — иначе не найдёте, где ошибка. -
Факты и измерения — это про структуру мышления.
События (факты) и контекст (измерения) — две стороны одного процесса. -
SCD Type 2 — это про уважение к истории.
Бизнес меняется — и данные должны это отражать. -
Модели (Star/3NF/DV) — это про выбор под задачу.
Нет «серебряной пули» — есть компромиссы.
🎁 Финальный подарок:
Запомните 5 золотых правил DWH:
- Всегда храните BK (бизнес-ключ) — иначе потеряете связь с источником.
- В DDS — только интегрированные, «чистые» сущности.
- В DM — только то, что нужно для отчёта.
- Проверяйте качество на каждом слое.
- Собирайте витрины итеративно: MVP → доработка → новые метрики.
Приложения
📚 Мини-глоссарий (RU / EN)
| Термин | Пояснение |
|---|---|
| Слой (Layer) | Логический уровень в DWH: STG/ODS/DDS/DM |
| Витрина (Data Mart) | Готовый набор таблиц для конкретной аналитики (например, финансы или маркетинг) |
| Факт (Fact) | Таблица событий или измерений: продажи, клики, звонки |
| Измерение (Dimension) | Справочник контекста: клиенты, товары, дата |
| Суррогатный ключ (SK) | Искусственный BIGINT, генерируемый в DWH |
| Бизнес-ключ (BK) | Естественный идентификатор из источника (customer_id, order_number) |
| SCD (Slowly Changing Dimension) | Подход к хранению истории атрибутов измерения |
| CDC (Change Data Capture) | Техника инкрементальной загрузки «только изменений» |
| Conformed Dimension | Измерение, единое для нескольких витрин (например, dim_date) |
🧱 Синонимы слоёв в индустрии
| Название | Синонимы |
|---|---|
| STG | Staging, Raw, Bronze, Landing Zone |
| ODS | Cleaned, Integrated, Silver |
| DDS | Core, Conformed, Golden Layer, Enterprise Data Model |
| DM | Data Mart, Semantic Layer, Gold, Analytics Layer |
⚠️ Названия могут отличаться — смотрите на содержание, а не на ярлыки.
🚫 Антипаттерны (чего избегать)
| Антипаттерн | Почему плохо |
|---|---|
| «Одна огромная история заказов» | Запросы тормозят, нет истории атрибутов (клиент сменил email — и всё прошлое «перекрасилось») |
| STG и ODS в одной таблице | Невозможно понять: ошибка в источнике или при очистке? |
Факт с текстовыми атрибутами (customer_name в fact_sales) |
Дублирование, нарушение нормализации, «спрятанная» бизнес-логика |
| SCD без BK | История «отвязана» от бизнеса: удалили клиента — и вся его история исчезла |
ЧАСТЬ C. Мини-датасет (для практики)
Все данные для практики находятся в папке data/ — тренируйтесь:
customer_id,email,phone,city
101,a@ex.com,700,Москва
101,b@ex.com,700,Москва
102,c@ex.com,701,СПб
order_id,order_date,customer_id
5001,2024-01-10,101
5002,2024-02-05,102
order_item_id,order_id,product_id,qty,price_at_sale
1,5001,9001,2,100.00
2,5001,9002,1,50.00
3,5002,9001,1,100.00
product_id,name
9001,Phone
9002,Case
product_id,valid_from,valid_to,price
9001,2023-12-01,2024-01-31,100
9001,2024-02-01,2999-12-31,110
📂 Все SQL-скрипты для построения хранилища находятся в папке
sql/.
ЧАСТЬ D. DDL-скелеты (PostgreSQL)
Полные DDL-скрипты для всех слоёв хранилища находятся в файле 01_ddl_stg-dds.sql.
Пример структуры основных таблиц DDS:
-- DDS: измерение клиента (SCD Type 2)
CREATE TABLE dds.dim_customer (
customer_sk BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_bk VARCHAR(50) NOT NULL, -- напр. '101'
email VARCHAR(100),
phone VARCHAR(20),
city VARCHAR(50),
valid_from DATE NOT NULL,
valid_to DATE DEFAULT '9999-12-31',
is_current BOOLEAN NOT NULL DEFAULT TRUE
);
-- DDS: факт продаж (гранулярность: строка заказа)
CREATE TABLE dds.fact_sales (
sale_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_sk BIGINT NOT NULL REFERENCES dds.dim_customer(customer_sk),
product_sk BIGINT NOT NULL,
date_key INT NOT NULL, -- YYYYMMDD, ссылка на dim_date.date_key
quantity INT NOT NULL CHECK (quantity > 0),
amount DECIMAL(18,2) NOT NULL CHECK (amount >= 0)
);
💡
date_key— это20240110, а неDATE, чтобы не делать JOIN по диапазону вfact → dim_date.