From 2d78fb4459bd718c436fcdb19b32c68809829fe6 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sun, 23 Nov 2025 18:31:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=BC=D0=B0=D1=88=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + .../Homework_Customer_Status_DDS_DM.md | 241 ++++++++++++++++++ dwh-modeling/README.md | 7 +- dwh-modeling/data/customer_status_events.csv | 10 + .../sql/07_ddl_hw_customer_status.sql | 43 ++++ .../08_dml_hw_customer_status_template.sql | 48 ++++ 6 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 dwh-modeling/Homework_Customer_Status_DDS_DM.md create mode 100644 dwh-modeling/data/customer_status_events.csv create mode 100644 dwh-modeling/sql/07_ddl_hw_customer_status.sql create mode 100644 dwh-modeling/sql/08_dml_hw_customer_status_template.sql diff --git a/README.md b/README.md index 2e2a34a..a240d9d 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ - [Базы данных. 1,2,3 нормальные формы. - Youtube](https://www.youtube.com/watch?v=zwQzL80U51c) - [Введение в структуру хранилища данных](dwh-modeling/README.md) - Теория про Slowly Changing Dimensions: [SCD](dwh-modeling/SCD.md) +- Практика по моделированию статусов клиента: [домашка STG → ODS → DDS → DM](dwh-modeling/Homework_Customer_Status_DDS_DM.md) - Еще про Data Vault: - [Введение в Data Vault - Хабр](https://habr.com/ru/articles/348188/) - [Основы Data Vault - Хабр](https://habr.com/ru/articles/502968/) diff --git a/dwh-modeling/Homework_Customer_Status_DDS_DM.md b/dwh-modeling/Homework_Customer_Status_DDS_DM.md new file mode 100644 index 0000000..dacb976 --- /dev/null +++ b/dwh-modeling/Homework_Customer_Status_DDS_DM.md @@ -0,0 +1,241 @@ +# Домашка: статусы клиента от STG до DDS (и немного DM) + +Небольшое практическое задание на 1–2 вечера: по данным о смене статусов клиента (CRM) построить цепочку слоёв `STG → ODS → DDS (SCD2)` и, по желанию, небольшую витрину в `dm`. + +Цель — потренировать **руками**: + +- работу со слоями DWH (stg / ods / dds / dm); +- проектирование и загрузку **измерения с историей (SCD Type 2)**; +- аккуратную работу со временем (`event_ts`, `valid_from`, `valid_to`). + +Исходим из того, что вы уже прошли основную статью `dwh-modeling/README.md` и познакомились с примером интернет‑магазина. + +--- + +## 1. Данные: события смены статуса клиента + +Представьте, что в CRM для каждого клиента хранится история статусов: + +- `new` — только что зарегистрировался; +- `active` — делал покупки недавно; +- `vip` — часто покупает и много тратит; +- `churned` — давно ничего не делал, считаем «отвалившимся». + +Эта информация приходит в DWH в виде **событий** (events): «у клиента X в момент времени Y статус стал Z». + +В репозитории в каталоге `dwh-modeling/data` лежит файл: + +- `customer_status_events.csv` + +Структура файла: + +```text +customer_id,status,event_ts,_load_id,load_ts +101,new,2024-01-01 09:00:00,batch_20250405_0800,2025-04-05 08:00:00 +... +``` + +Колонки: + +- `customer_id` — бизнес-ключ клиента (тот же, что и в основном примере — 101, 102, 103); +- `status` — статус клиента в CRM (`new`, `active`, `vip`, `churned`); +- `event_ts` — момент, когда статус сменился в CRM; +- `_load_id` — идентификатор батча загрузки; +- `load_ts` — момент, когда данные попали в DWH. + +Файл содержит несколько клиентов и несколько смен статуса по каждому — этого достаточно, чтобы отработать SCD2. + +--- + +## 2. Целевая схема: какие таблицы уже есть + +Чтобы не тратить время на DDL, структуры таблиц для домашки уже подготовлены в `dwh-modeling/sql`: + +- `07_ddl_hw_customer_status.sql` — создаёт дополнительные таблицы: + - `stg.customer_status_raw` — сырые события о статусе клиента; + - `ods.customer_status` — очищенные и типизированные события; + - `dds.dim_customer_status` — измерение статусов клиента в формате **SCD Type 2**. +- `08_dml_hw_customer_status_template.sql` — шаблон DML-скрипта с подсказками и заготовками блоков. + +Перед началом работы: + +1. Поднимите demo‑Postgres по инструкции из корневого `README.md`. +2. Выполните базовые скрипты DWH: + - `01_ddl_stg-dds.sql` + - `02_dml_stg-dds.sql` +3. Выполните DDL для домашки: + - `07_ddl_hw_customer_status.sql` + +После этого схемы `stg`, `ods`, `dds` уже существуют, а дополнительные таблицы для статусов созданы. + +--- + +## 3. Часть 1 — STG → ODS (обязательно) + +**Задача:** загрузить CSV в STG и переложить данные в ODS с приведением типов. + +### 3.1. STG: загрузка CSV + +1. Откройте `psql` в контейнере (см. инструкцию в `postgres-bookings/README.md`). +2. Загрузите файл `customer_status_events.csv` в таблицу `stg.customer_status_raw`: + - можно использовать `\copy` из `psql`; + - либо любой другой способ, к которому вы привыкли. +3. Убедитесь, что данные загрузились: + +```sql +SELECT * FROM stg.customer_status_raw LIMIT 10; +``` + +### 3.2. ODS: очистка и типизация + +В файле `08_dml_hw_customer_status_template.sql` найдите заготовку блока ODS и допишите SQL: + +- привести: + - `customer_id` → `INT`, + - `status` → `VARCHAR(20)` (можно оставить как есть), + - `event_ts` и `load_ts` → `TIMESTAMP`; +- аккуратно обработать возможные пустые значения (если бы они были); +- заполнить `_load_id` и `_load_ts` в `ods.customer_status`. + +Проверьте, что в `ods.customer_status` данные выглядят аккуратно: + +```sql +SELECT * +FROM ods.customer_status +ORDER BY customer_id, event_ts; +``` + +--- + +## 4. Часть 2 — ODS → DDS (SCD Type 2, обязательно) + +**Задача:** по событиям в `ods.customer_status` построить измерение `dds.dim_customer_status`, где каждая строка — период действия статуса. + +Целевая таблица уже создана (см. `07_ddl_hw_customer_status.sql`): + +- `customer_bk` — бизнес-ключ клиента (тот же, что `customer_id` в ODS); +- `status` — статус клиента; +- `hashdiff` — хэш от атрибутов (здесь достаточно самого `status`); +- `valid_from` / `valid_to` — период, когда статус был актуален; +- `is_current` — флаг актуальной строки; +- `created_at` / `updated_at` — технические поля. + +### 4.1. Начальная загрузка SCD2 + +В шаблоне `08_dml_hw_customer_status_template.sql` допишите блок начальной загрузки: + +1. Сформируйте промежуточный набор: + + - `customer_bk`, + - `status`, + - `event_ts` (как «время начала действия статуса»), + - `hashdiff` (например, `md5(status)` или `dds.customer_hash(status)` по аналогии с `dim_customer`). + +2. Для каждого клиента отсортируйте события по `event_ts` и с помощью `LEAD()` посчитайте: + + - `valid_from` — текущее `event_ts`, + - `valid_to` — следующее `event_ts - 1 second` (или `9999-12-31`, если следующего нет). + +3. Вставьте получившиеся строки в `dds.dim_customer_status`: + +```sql +INSERT INTO dds.dim_customer_status ( + customer_bk, status, hashdiff, + valid_from, valid_to, + is_current, created_at, updated_at +) +SELECT + ... +``` + +Проверьте результат: + +```sql +SELECT * +FROM dds.dim_customer_status +ORDER BY customer_bk, valid_from; +``` + +Ожидаемое поведение: + +- у клиента 101 несколько строк с разными статусами и непересекающимися периодами; +- `is_current = TRUE` только у самой свежей строки для каждого клиента. + +### 4.2. Проверка себя + +Примеры проверочных запросов (можно придумать свои): + +- «Какой статус был у клиента 101 на дату `2024-06-01`?» + → одна строка с нужным статусом. +- «Сколько клиентов были в статусе `active` на `2024-04-10`?» + → несколько строк, если статус *активен* для диапазона дат. + +--- + +## 5. Часть 3 — инкрементальная загрузка (по желанию) + +Если хочется потренироваться глубже: + +1. Добавьте в CSV ещё несколько событий смены статуса (например, переход части клиентов из `churned` обратно в `active`). +2. Загрузите новые строки только в `stg.customer_status_raw`. +3. Напишите логику инкрементального обновления `dds.dim_customer_status`: + +- ориентируйтесь на пример из `03_demo_increment.sql` для `dds.dim_customer`; +- важно: + - корректно «закрыть» старую актуальную строку (`valid_to`, `is_current = FALSE`); + - вставить новую строку с `is_current = TRUE`. + +Эта часть особенно полезна, если вы хотите почувствовать, как SCD2 живёт в реальном DWH. + +--- + +## 6. Часть 4 — витрина в DM (по желанию) + +Опциональное задание для закрепления: собрать небольшую витрину с количеством клиентов по статусам на каждую дату. + +Пример целевой таблицы: + +```sql +CREATE TABLE dm.mart_customer_status_daily ( + date_actual DATE NOT NULL, + status VARCHAR(20) NOT NULL, + customers_cnt INT NOT NULL +); +``` + +Идея: + +- использовать `dds.dim_date` как календарь; +- для каждой `date_actual` найти, какой статус был у клиента в этот день + (через `JOIN` на `dds.dim_customer_status` по диапазону `valid_from/valid_to`); +- агрегировать по `status`. + +Пример запроса к витрине: + +```sql +SELECT + date_actual, + status, + customers_cnt +FROM dm.mart_customer_status_daily +WHERE date_actual BETWEEN '2024-04-01' AND '2024-04-30' +ORDER BY date_actual, status; +``` + +--- + +## 7. Как вписать эту домашку в обучение + +Рекомендуемое место в дорожке: + +1. Пройти основную теорию по DWH и SCD: + - `dwh-modeling/README.md` + - `dwh-modeling/SCD.md` +2. Разобрать базовый пример интернет‑магазина (скрипты `01_`–`06_`). +3. Выполнить **эту домашку** как первую попытку «самостоятельного» моделирования и ETL: + - познакомиться с ещё одним измерением с историей (`dim_customer_status`); + - потренироваться аккуратно работать с датами и периодами; + - при желании — собрать простую витрину в `dm`. + +Если что‑то не получается — можно разбирать решения по шагам вместе с ментором: от простого `SELECT` из STG до полноценного SCD2 в DDS. + diff --git a/dwh-modeling/README.md b/dwh-modeling/README.md index 352b8dd..8fb6718 100644 --- a/dwh-modeling/README.md +++ b/dwh-modeling/README.md @@ -723,6 +723,12 @@ SELECT 'OK' WHERE EXISTS ( ## Приложения +### Дополнительные материалы и практика + +- [Домашка: статусы клиента от STG до DDS (и немного DM)](Homework_Customer_Status_DDS_DM.md) +- [SCD: как хранить историю изменений](SCD.md) +- [DataVault: как пережить бурную жизнь источников](DataVault.md) + ### 📚 Мини-глоссарий (RU / EN) | Термин | Пояснение | @@ -839,4 +845,3 @@ CREATE TABLE dds.fact_sales ( ``` > 💡 `date_key` — это `20240110`, а не `DATE`, чтобы не делать JOIN по диапазону в `fact → dim_date`. - diff --git a/dwh-modeling/data/customer_status_events.csv b/dwh-modeling/data/customer_status_events.csv new file mode 100644 index 0000000..493886a --- /dev/null +++ b/dwh-modeling/data/customer_status_events.csv @@ -0,0 +1,10 @@ +customer_id,status,event_ts,_load_id,load_ts +101,new,2024-01-01 09:00:00,batch_20250405_0800,2025-04-05 08:00:00 +101,active,2024-02-15 10:30:00,batch_20250405_0800,2025-04-05 08:00:00 +101,vip,2024-05-10 11:00:00,batch_20250405_1200,2025-04-05 12:00:00 +101,churned,2024-09-01 12:15:00,batch_20250405_1800,2025-04-05 18:00:00 +102,new,2024-03-05 14:00:00,batch_20250405_0800,2025-04-05 08:00:00 +102,active,2024-04-01 09:45:00,batch_20250405_1200,2025-04-05 12:00:00 +102,churned,2024-04-20 16:20:00,batch_20250405_1800,2025-04-05 18:00:00 +103,new,2024-03-10 10:10:00,batch_20250405_0800,2025-04-05 08:00:00 + diff --git a/dwh-modeling/sql/07_ddl_hw_customer_status.sql b/dwh-modeling/sql/07_ddl_hw_customer_status.sql new file mode 100644 index 0000000..c3c2946 --- /dev/null +++ b/dwh-modeling/sql/07_ddl_hw_customer_status.sql @@ -0,0 +1,43 @@ +-- =============================================== +-- DDL: дополнительные таблицы для домашки +-- Тема: статусы клиента (SCD2 поверх статуса) +-- Скрипт можно запускать после 01_ddl_stg-dds.sql +-- =============================================== + +-- 1. STG: сырые события о статусе клиента из CRM +DROP TABLE IF EXISTS stg.customer_status_raw; +CREATE TABLE stg.customer_status_raw ( + customer_id TEXT, + status TEXT, + event_ts TEXT, + _load_id TEXT, + _load_ts TIMESTAMP DEFAULT NOW() +); + +-- 2. ODS: очищенные и типизированные статусы +DROP TABLE IF EXISTS ods.customer_status; +CREATE TABLE ods.customer_status ( + customer_id INT NOT NULL, + status VARCHAR(20) NOT NULL, + event_ts TIMESTAMP NOT NULL, + _load_id TEXT NOT NULL, + _load_ts TIMESTAMP NOT NULL +); + +ALTER TABLE ods.customer_status + ADD PRIMARY KEY (customer_id, event_ts); + +-- 3. DDS: измерение статусов клиента с историей (SCD Type 2) +DROP TABLE IF EXISTS dds.dim_customer_status; +CREATE TABLE dds.dim_customer_status ( + customer_status_sk BIGSERIAL PRIMARY KEY, + customer_bk INT NOT NULL, + status VARCHAR(20) NOT NULL, + hashdiff TEXT NOT NULL, + valid_from TIMESTAMP NOT NULL, + valid_to TIMESTAMP NOT NULL, + is_current BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); + diff --git a/dwh-modeling/sql/08_dml_hw_customer_status_template.sql b/dwh-modeling/sql/08_dml_hw_customer_status_template.sql new file mode 100644 index 0000000..32f5f2a --- /dev/null +++ b/dwh-modeling/sql/08_dml_hw_customer_status_template.sql @@ -0,0 +1,48 @@ +-- =============================================== +-- DML-шаблон для домашки +-- Тема: статусы клиента (STG → ODS → DDS SCD2) +-- Цель: по customer_status_events.csv построить историю статусов +-- =============================================== + +-- Подсказка: +-- 1) Загрузите CSV в stg.customer_status_raw (через COPY или \copy в psql). +-- См. пример структуры файла в dwh-modeling/data/customer_status_events.csv +-- 2) Переложите данные в ods.customer_status с приведением типов. +-- customer_id → INT, status → VARCHAR(20), event_ts / load_ts → TIMESTAMP. +-- 3) Постройте из ods.customer_status измерение dds.dim_customer_status в стиле SCD2: +-- - одна строка на период действия статуса (valid_from / valid_to); +-- - is_current = TRUE только у актуальной строки для клиента; +-- - hashdiff можно считать, например, от одного поля status. +-- 4) При желании добавьте инкрементальную логику (как в 03_demo_increment.sql). +-- 5) Опционально: соберите витрину dm.mart_customer_status_daily +-- с количеством клиентов по статусам на каждую дату. + +-- Ниже — ЗАГОТОВКИ блоков, которые можно дописать. +-- Они намеренно оставлены пустыми, чтобы вы написали SQL сами. + +-- 1. ODS: очистка и типизация +-- TRUNCATE ods.customer_status; +-- INSERT INTO ods.customer_status (...) +-- SELECT ... +-- FROM stg.customer_status_raw; + +-- 2. DDS: начальная загрузка SCD2 +-- Примерный план: +-- - рассчитать hashdiff по (status); +-- - по каждому клиенту отсортировать события по времени; +-- - построить для каждой строки valid_from и valid_to (LEAD() OVER ...); +-- - вставить в dds.dim_customer_status. + +-- 3. DDS: инкрементальная загрузка (по желанию) +-- Можно ориентироваться на примеры в 03_demo_increment.sql. + +-- 4. DM: витрина статусов клиентов по датам (по желанию) +-- Пример целевой структуры: +-- CREATE TABLE dm.mart_customer_status_daily ( +-- date_actual DATE NOT NULL, +-- status VARCHAR(20) NOT NULL, +-- customers_cnt INT NOT NULL +-- ); +-- Идея: на каждую дату взять актуальный статус клиента +-- через JOIN dds.dim_customer_status + dds.dim_date. +