From 42c8515b6262d364ed64ffc328f41f7d7fdc9aca Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Fri, 7 Nov 2025 12:25:41 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B8=D0=B4=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dwh-modeling/README.md | 90 ++++++++++++++++++++++++---------- dwh-modeling/sql/04_ddl_dm.sql | 34 +++++++++++++ dwh-modeling/sql/05_dml_dm.sql | 68 +++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 27 deletions(-) create mode 100644 dwh-modeling/sql/04_ddl_dm.sql create mode 100644 dwh-modeling/sql/05_dml_dm.sql diff --git a/dwh-modeling/README.md b/dwh-modeling/README.md index 007c737..634323c 100644 --- a/dwh-modeling/README.md +++ b/dwh-modeling/README.md @@ -433,27 +433,71 @@ GROUP BY d.date_actual, p.product_name, c.customer_segment; --- -## **8. Как выбрать модель? Дерево решений** +## **8. Как выбрать модель данных? Советы от практиков** -Вот простой алгоритм: +Выбор модели — **не техническая задача, а стратегическая**. +Это как решать: строить дом из кирпича, дерева или SIP-панелей. У каждой технологии — свои плюсы, но **главное — подходит ли она *вам* сегодня**. -``` -Нужна ли история изменений? ── Нет → Звезда (просто и быстро) - │ - Да - │ -Много ли источников (>5)? ── Нет → 3NF (надёжно, понятно) - │ - Да - │ -Нужна ли аудиторская трассировка? ── Нет → 3NF + SCD - │ - Да → Data Vault -``` +### 🔹 Главное, что нужно понять новичку: -> 🛑 Не делайте «гибрид» без причины: -> — Звезда в DDS — плохо (потеряете гибкость); -> — DV в DM — плохо (BI не потянет 50 JOIN’ов). +> **Не существует «самой правильной» модели.** +> Есть **самая подходящая под ваш контекст** — и он у всех разный. + +--- + +### 🛑 Что делать **не стоит** (если опыта < 2 лет в DWH): + +| Что делать не стоит | Почему | +|---------------------|--------| +| **Брать Data Vault «потому что модно»** | DV требует глубокого понимания интеграции, CDC, идемпотентности. Без этого — получите «историю», где невозможно найти актуальные данные. | +| **Строить сложную 3NF «как в книжках» под 10 таблиц** | Если у вас 2 источника — вы потратите 2 недели на нормализацию, чтобы потом делать 5 JOIN’ов ради простого отчёта. | +| **Пытаться «сделать сразу гибко на 5 лет вперёд»** | Гибкость = сложность. А сложность = баги, задержки, выгорание команды. | + +--- + +### ✅ **Базовые советы — с чего начать, если вы учитесь или делаете первый DWH** + +1. **Начните с витрины в формате Звезды (Star Schema).** + — Это просто: одна таблица фактов + несколько «плоских» справочников. + — Это быстро: отчёт в Power BI — за 10 минут. + — Это понятно: даже менеджер поймёт структуру. + +2. **Стройте DDS только когда это *действительно нужно*.** + — Если источников ≤ 3 и они стабильны — можно `ods → dm` напрямую. + — Если появятся расхождения («email в CRM и в заказах — разные»), *тогда* заводите `dds.dim_customer`. + +3. **Историю (SCD) включайте *постепенно*.** + — Сначала — без истории (Type 1: просто обновляете строку). + — Потом — только для ключевых сущностей (клиент, товар). + — Только потом — думайте про DV или полную историзацию. + +4. **Если сомневаетесь — спросите: «А кто будет этим пользоваться?»** + — Аналитик в Metabase → Звезда. + — BI-разработчик в Power BI → Звезда. + — Инженер, который строит ML-фичи → 3NF или даже сырые ODS-таблицы. + — Аудитор или регулятор → DV (но только если вы *готовы* к его сложности). + +--- + +### 💡 И ещё один совет от практиков: + +> **Лучше сделать простую модель — и вовремя переделать,** +> чем сделать «идеальную» — и застрять на этапе проектирования. + +Переделать Звезду → Звезда с SCD Type 2 — легко. +Переделать «недоделанный DV» → что-то рабочее — в 10 раз сложнее. + +--- + +### 📌 Кратко — что выбрать *сегодня*, если вы только учитесь: + +| У вас… | Делайте… | +|--------|----------| +| Учебный проект, 1–2 CSV | `ods → dm` по модели **Звезда** (без DDS, без истории) | +| Первый рабочий DWH, 3–5 источников | `stg → ods → dds (3NF/простая Звезда) → dm (Звезда)` | +| Команда из 1 инженера + 1 аналитика | **Не трогайте DV и Anchor** — они «съедят» ваше время без отдачи | + +А когда наберётесь опыта — приходите в DV. Он того стоит. Но *не раньше времени*. --- @@ -621,7 +665,7 @@ product_id,valid_from,valid_to,price 9001,2024-02-01,2999-12-31,110 ``` -> 📂 Примеры DDL, SQL-загрузки, SCD-скрипты — в [GitHub-репозитории к статье](https://github.com/yourname/dwh-basics) (реальный репо — по вашему усмотрению). +> 📂 Примеры DDL, SQL-загрузки, SCD-скрипты — в [GitHub-репозитории к статье](https://github.com/dementev-dev/de-roadmap/). --- @@ -653,11 +697,3 @@ CREATE TABLE dds.fact_sales ( > 💡 `date_key` — это `20240110`, а не `DATE`, чтобы не делать JOIN по диапазону в `fact → dim_date`. ---- - -Если нужно — могу: -- добавить **интерактивные схемы** (например, кликабельные Mermaid → SVG); -- подготовить **полный SQL-скрипт загрузки** (STG→ODS→DDS); -- сделать **вариант статьи в формате Jupyter Notebook** (с исполняемыми ячейками). - -Готов дорабатывать под ваш стиль и аудиторию. \ No newline at end of file diff --git a/dwh-modeling/sql/04_ddl_dm.sql b/dwh-modeling/sql/04_ddl_dm.sql new file mode 100644 index 0000000..642feba --- /dev/null +++ b/dwh-modeling/sql/04_ddl_dm.sql @@ -0,0 +1,34 @@ +-- =============================================== +-- DDL: Data Marts (DM) +-- Слой "готовых решений" — для BI, отчётов, API +-- =============================================== + +DROP SCHEMA IF EXISTS dm CASCADE; +CREATE SCHEMA dm; + +-- Витрина: ежедневные продажи по товарам и клиентам +-- Гранулярность: 1 строка = 1 день × 1 товар × 1 сегмент клиента +CREATE TABLE dm.mart_daily_sales ( + date_actual DATE NOT NULL, + product_name VARCHAR(100) NOT NULL, + customer_segment VARCHAR(20) NOT NULL, -- напр. 'Premium', 'Basic' + total_qty INT NOT NULL, + total_revenue NUMERIC(18,2) NOT NULL +); + +-- Витрина: 360°-портрет клиента (lifetime value) +CREATE TABLE dm.mart_customer_360 ( + customer_bk INT NOT NULL, + first_order_date DATE, + last_order_date DATE, + total_orders INT NOT NULL, + total_items INT NOT NULL, + lifetime_value NUMERIC(18,2) NOT NULL, + last_email VARCHAR(100), + last_city VARCHAR(50) +); + +-- Индексы для ускорения BI (опционально, но рекомендовано) +CREATE INDEX ON dm.mart_daily_sales (date_actual); +CREATE INDEX ON dm.mart_daily_sales (product_name); +CREATE INDEX ON dm.mart_customer_360 (customer_bk); diff --git a/dwh-modeling/sql/05_dml_dm.sql b/dwh-modeling/sql/05_dml_dm.sql new file mode 100644 index 0000000..bd8acfa --- /dev/null +++ b/dwh-modeling/sql/05_dml_dm.sql @@ -0,0 +1,68 @@ +-- =============================================== +-- DML: построение Data Marts из DDS +-- Идемпотентно: можно пересобирать в любое время +-- =============================================== + +-- 1. Очистка (full refresh — для простоты; в продакшене — incremental) +TRUNCATE dm.mart_daily_sales, dm.mart_customer_360; + +-- 2. mart_daily_sales: агрегация по дням +-- Используем актуальные версии измерений (is_current = true) +-- Для кого: Маркетолог, продакт-аналитик +-- Пример использования: «Как менялись продажи Phone в сегменте Premium по дням?» +INSERT INTO dm.mart_daily_sales ( + date_actual, product_name, customer_segment, total_qty, total_revenue +) +SELECT + d.date_actual, + p.product_name, + -- Простая сегментация по выручке за заказ + CASE + WHEN f.amount >= 200 THEN 'Premium' + ELSE 'Basic' + END AS customer_segment, + 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 c.is_current +GROUP BY d.date_actual, p.product_name, + CASE WHEN f.amount >= 200 THEN 'Premium' ELSE 'Basic' END; + +-- 3. mart_customer_360: lifetime-портрет +-- Здесь — НЕ используем is_current! Нам нужна вся история для расчёта LTV +-- Для кого: CRM-менеджер, retention-спец +-- Пример использования: «Найти клиентов с LTV > 250 ₽ и email из Москвы для email-рассылки» +«Найти клиентов с LTV > 250 ₽ и email из Москвы для email-рассылки» +INSERT INTO dm.mart_customer_360 ( + customer_bk, first_order_date, last_order_date, + total_orders, total_items, lifetime_value, + last_email, last_city +) +SELECT + c.customer_bk, + MIN(d.date_actual) AS first_order_date, + MAX(d.date_actual) AS last_order_date, + COUNT(DISTINCT f.sale_id) AS total_orders, + SUM(f.quantity) AS total_items, + SUM(f.amount) AS lifetime_value, + -- Берём email и город из самой свежей версии клиента + (SELECT email FROM dds.dim_customer c2 + WHERE c2.customer_bk = c.customer_bk + ORDER BY c2.valid_from DESC + LIMIT 1) AS last_email, + (SELECT city FROM dds.dim_customer c2 + WHERE c2.customer_bk = c.customer_bk + ORDER BY c2.valid_from DESC + LIMIT 1) AS last_city +FROM dds.fact_sales f +JOIN dds.dim_date d ON f.date_key = d.date_key +JOIN dds.dim_customer c ON f.customer_sk = c.customer_sk +-- ⚠️ Важно: здесь НЕ фильтруем по is_current — нужна вся история! +GROUP BY c.customer_bk; + +-- 4. Дополнительно: материализованное представление (альтернатива таблице) +-- В PG 12+ можно использовать MATERIALIZED VIEW — обновляется по команде REFRESH +-- CREATE MATERIALIZED VIEW dm.mv_monthly_sales AS +-- SELECT ... (аналогично)