# **Хранилище данных: как устроена аналитика «под капотом»** *Для тех, кто знает 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 на слои?** Представьте, что вы строите дом. Вы же не будете сразу вбивать гвозди в стены — сначала: 1. Привезли стройматериалы (песок, доски, кирпич) — **сырьё**; 2. Очистили, просеяли, нарезали — **обработка**; 3. Собрали каркас, провели коммуникации — **интеграция**; 4. Сделали отделку под конкретную квартиру — **готовое решение**. В DWH — то же самое. Каждый слой отвечает за *одну задачу*: ```mermaid flowchart TD subgraph Sources["Источники"] A["CRM"] B["Заказы"] C["Склад"] end subgraph STG["STG — «Сырьё»"] D["Таблицы-дубликаты
в формате источника"] end subgraph ODS["ODS — «Очистка»"] E["Типы:
даты → DATE,
числа → INT/DECIMAL
Валидация: email, phone"] end subgraph DDS["DDS — «Интеграция»"] F["Общие сущности:
клиент, товар, дата
История (SCD),
суррогатные ключи"] end subgraph DM["DM — «Готовые решения»"] G["Витрина продаж: дата, товар, клиент, сумма
+ агрегаты (выручка/день)"] end A --> STG B --> STG C --> STG STG --> ODS ODS --> DDS DDS --> DM DM --> BI["BI-системы
(Power BI, Tableau,
Metabase)"] ``` 👉 **Почему так лучше, чем «одна большая таблица»?** 1. **Управляемость**: если в `customers` пришёл битый `email` — ошибка локализована в STG/ODS, DDS не пострадает. 2. **Прозрачность**: можно посмотреть: «а как выглядел исходник?», «а как мы его почистили?». 3. **Производительность**: в DDS и DM — только то, что нужно для анализа. Никаких `JSON`-полей, `TEXT` без причины. --- ## **4. Путешествие данных: от STG до DM** Давайте проследим, как превращается строка заказа. ### **STG (Staging / Bronze)** — «как пришло» - Таблицы: `stg.orders_raw`, `stg.customers_raw`; - Структура — *точно как в источнике* (может быть `VARCHAR` даже у дат); - Добавлены технические поля: - `_load_id` — идентификатор загрузки; - `_load_ts` — время получения данных; - Главное правило: **неизменяемость**. Если пришла новая порция — либо добавляем новые строки, либо *полностью перезагружаем* слой (идемпотентность). - Дедупликация: если два раза пришёл один и тот же заказ — оставляем один (по `order_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 и клиента из заказов — это будет позже. - Пока — никакой бизнес-логики. Только *техническая* очистка. > 🎯 Цель ODS — дать «надёжную платформу» для следующего слоя. Как сухое, чистое бревно перед сборкой дома. --- ### **DDS (Data Delivery Store / Core / Gold)** — «интеграция + история» Здесь рождается *единая бизнес-модель*. Появляются понятия: **измерения**, **факты**, **суррогатные ключи**, **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 / «Витрины»)** — «готово к употреблению» Здесь — таблицы и представления для конкретных задач: - `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. ```mermaid erDiagram dim_date ||--o{ fact_sales : "дата" dim_customer ||--o{ fact_sales : "клиент" dim_product ||--o{ fact_sales : "товар" dim_customer { bigint customer_sk PK "суррогатный ключ" varchar customer_bk "бизнес-ключ, напр. '101'" varchar customer_name varchar email varchar city date valid_from date valid_to boolean is_current } fact_sales { bigint sale_id PK bigint customer_sk FK bigint product_sk FK int date_key FK "ссылка на dim_date.date_key" 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 | email | 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`, где: ```sql fact_sales.order_date BETWEEN dim_customer.valid_from AND dim_customer.valid_to ``` и получаем актуальный на тот день email и город. > 🔍 Подробнее про SCD — в отдельной статье [Slow Changing Dimensions](SCD.md) (сравнение Type 1/2/3, паттерны обновления). --- ## **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** *Источник: Дэн Линстедт (Dan Linstedt)* Модель основана на трёх типах таблиц: | Тип | Назначение | Пример | |-----|------------|--------| | **Hub (Хаб)** | Хранит бизнес-ключи (BK) | `hub_customer`: `customer_id`, `load_dttm` | | **Link (Связь)** | Фиксирует отношения | `link_order_customer`: `order_id + customer_id` | | **Satellite (Сателлит)** | Хранит атрибуты + историю | `sat_customer_info`: имя, email, дата начала/окончания | ```mermaid erDiagram hub_customer ||--o{ sat_customer_info : "хаб → сателлит" hub_order ||--o{ sat_order_details : "хаб → сателлит" hub_customer ||--o{ link_order_customer : "участвует в" hub_order ||--o{ link_order_customer : "создан клиентом" ``` ✅ **Плюсы**: - **История «из коробки»** — каждое изменение — новая строка в сателлите; - **Масштабируемость**: легко подключить новый источник — добавили хаб/линк/сателлит; - **Аудит**: откуда пришёл каждый факт — видно по `_load_dttm`. ❌ **Минусы**: - Сложно читать (20 таблиц вместо 3); - Нужно писать сложные запросы (или использовать автоматическую генерацию витрин). 📌 **Когда выбирать**: → Большие проекты с 10+ источниками; → Когда критична **трассировка** и **соответствие регуляторным требованиям** (например, финтех). > 📌 *DV 1.0* не поддерживал SCD Type 2 «из коробки» — в DV 2.0 это решено. --- ### **4. Anchor Modeling (анкерное моделирование)** *Источник: Ларс Рёне (Lars Rönnbäck)* Ещё более атомарный подход: - **Anchor** — сущность (клиент, товар); - **Attribute** — атрибут (email, имя); - **Tie** — связь (как Link в DV); - Все таблицы — 2–3 столбца. ✅ **Плюсы**: - **Максимальная гибкость**: поменяли модель — не трогали старые таблицы; - **Бесконечная эволюция**: можно добавлять атрибуты «задним числом». ❌ **Минусы**: - Очень сложные запросы (JOIN’ов — десятки); - Почти не используется «в чистом виде» — чаще как концепция. 📌 **Когда выбирать**: → Экспериментальные проекты; → Когда схема данных *каждый месяц* радикально меняется. --- ### **Сравнение моделей — наглядно** ```mermaid 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` — таблицу, которую можно сразу подключить к Power BI. ### **Этапы сборки** 1. Из STG → ODS: - `stg.orders_raw` → `ods.orders` (привели `order_date` к `DATE`); 2. Из ODS → DDS: - `ods.customers` → `dds.dim_customer` (SCD Type 2); - `ods.products` → `dds.dim_product`; - `ods.orders` + `ods.order_items` → `dds.fact_sales`; 3. Из DDS → DM: - `fact_sales` + `dim_*` → `mart_daily_sales`. ```mermaid 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 -- 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-панелей. У каждой технологии — свои плюсы, но **главное — подходит ли она *вам* сегодня**. ### 🔹 Главное, что нужно понять новичку: > **Не существует «самой правильной» модели.** > Есть **самая подходящая под ваш контекст** — и он у всех разный. --- ### 🛑 Что делать **не стоит** (если опыта < 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. Он того стоит. Но *не раньше времени*. --- ## **9. Эксплуатация: качество данных — это не «опция»** Самая красивая архитектура бессмысленна, если в `mart_daily_sales` — нули. Поэтому в каждом слое — **контроль качества (DQ, Data Quality)**. ```mermaid 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): ```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**: > 1. Всегда храните BK (бизнес-ключ) — иначе потеряете связь с источником. > 2. В DDS — только интегрированные, «чистые» сущности. > 3. В DM — только то, что нужно для отчёта. > 4. Проверяйте качество *на каждом слое*. > 5. Собирайте витрины *итеративно*: 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** | История «отвязана» от бизнеса: удалили клиента — и вся его история исчезла | --- ### 📖 Что почитать дальше? | Книга / Ресурс | Для кого | |----------------|----------| | **Кимболл, «Техника создания хранилищ данных»** | Начинающим: звезда, SCD, витрины | | **Инмон, «Построение хранилищ данных»** | Для понимания 3NF и корпоративного подхода | | **Linstedt & Olschimke, «Data Vault 2.0»** | Практическое руководство по DV | | **[anchor-modeling.com](https://www.anchormodeling.com/)** | Официальный сайт Anchor Modeling | | **Курс «Аналитика в Postgres» (Stepik / Postgres Pro)** | Практика SQL + DWH на реальных данных | --- ## **ЧАСТЬ C. Мини-датасет (для практики)** Положите эти файлы в папку `data/` — и тренируйтесь: **`customers.csv`** ```csv customer_id,email,phone,city 101,a@ex.com,700,Москва 101,b@ex.com,700,Москва 102,c@ex.com,701,СПб ``` **`orders.csv`** ```csv order_id,order_date,customer_id 5001,2024-01-10,101 5002,2024-02-05,102 ``` **`order_items.csv`** ```csv 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 ``` **`products.csv`** ```csv product_id,name 9001,Phone 9002,Case ``` **`prices.csv`** ```csv product_id,valid_from,valid_to,price 9001,2023-12-01,2024-01-31,100 9001,2024-02-01,2999-12-31,110 ``` > 📂 Примеры DDL, SQL-загрузки, SCD-скрипты — в [GitHub-репозитории к статье](https://github.com/dementev-dev/de-roadmap/). --- ## **ЧАСТЬ D. DDL-скелеты (PostgreSQL)** ```sql -- 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`.