Files
de-roadmap/dwh-modeling/Homework_Customer_Status_DDS_DM.md
T
ddadmin a1f2643841 feat(site): добавлен MkDocs Material сайт с CI/CD на GitHub Pages
- Зачем:
  - роадмап нуждается в презентабельном виде с навигацией и поиском, а не только GitHub README.
- Что:
  - создан mkdocs.yml (Material, docs_dir: ., поиск на русском, тёмная/светлая тема).
  - создан .github/workflows/deploy-site.yml (push в main → сборка → GitHub Pages).
  - адаптирован Markdown для dual compatibility (GitHub + MkDocs): пустые строки перед списками, отступы 2sp→4sp, заголовки README #→## для корректного TOC.
  - заменены em-dash на запятые в 4 заголовках dwh-modeling/README.md (фикс расхождения якорей).
  - добавлены правила Markdown Style и команды MkDocs/Playwright в AGENTS.md.
- Проверка:
  - uv run --with 'mkdocs-material==9.6.14' --with 'mkdocs-same-dir==0.1.3' mkdocs build --strict
2026-03-27 22:57:15 +03:00

293 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Домашка: статусы клиента от 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_20240101_1000,2024-01-01 10: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` (нужен как минимум для `dds.dim_date`)
- `05_ddl_dm.sql` (создаёт схему `dm` для витрин)
3. Выполните DDL для домашки:
- `07_ddl_hw_customer_status.sql`
После этого схемы `stg`, `ods`, `dds`, `dm` уже существуют, а дополнительные таблицы для статусов созданы.
---
## 3. Часть 1 — STG → ODS (обязательно)
**Задача:** загрузить CSV в STG и переложить данные в ODS с приведением типов.
### 3.1. STG: загрузка CSV
Есть два варианта — выберите любой. Для первого прохождения рекомендуем **вариант A** (самый простой).
#### Вариант A (рекомендуемый): вставить данные в STG через `INSERT`
Откройте SQL‑клиент (DBeaver или `psql`) и вставьте данные текстом:
> 💡 Если вы уже загружали данные в `stg.customer_status_raw` и делаете повторный запуск — начните с `TRUNCATE stg.customer_status_raw;`.
>
> 💡 В примере ниже показан минимальный набор для клиента 101. Чтобы получить несколько клиентов и больше событий — используйте вариант B (CSV) или добавьте строки из файла `dwh-modeling/data/customer_status_events.csv`.
```sql
INSERT INTO stg.customer_status_raw (customer_id, status, event_ts, _load_id, _load_ts) VALUES
('101','new','2024-01-01 09:00:00','batch_20240101_1000','2024-01-01 10:00:00'),
('101','active','2024-02-15 10:30:00','batch_20240215_1100','2024-02-15 11:00:00'),
('101','vip','2024-05-10 11:00:00','batch_20240510_1200','2024-05-10 12:00:00'),
('101','churned','2024-09-01 12:15:00','batch_20240901_1300','2024-09-01 13:00:00');
```
> 💡 Здесь `_load_ts` — это время загрузки.
#### Вариант B: загрузить CSV
Можно загрузить файл `dwh-modeling/data/customer_status_events.csv` в таблицу `stg.customer_status_raw`:
- **Через DBeaver**: Import Data → CSV → `stg.customer_status_raw`.
- **Через `psql` в контейнере (`./psql_sh`)**: без установки `psql` на хост.
Способ: передайте CSV в `psql` через STDIN и выполните `\copy ... FROM STDIN`:
```bash
./postgres-bookings/psql_sh -c "TRUNCATE stg.customer_status_raw;"
cat dwh-modeling/data/customer_status_events.csv | ./postgres-bookings/psql_sh -c \
"\\copy stg.customer_status_raw(customer_id,status,event_ts,_load_id,_load_ts) FROM STDIN WITH (FORMAT csv, HEADER true)"
```
> 💡 Для части 5 (инкрементальная загрузка) `TRUNCATE stg.customer_status_raw` делать не нужно — загружайте только новые строки.
После загрузки убедитесь, что данные на месте:
```sql
SELECT * FROM stg.customer_status_raw LIMIT 10;
```
### 3.2. ODS: очистка и типизация
> 💡 Обратите внимание: в основном примере `ods.customers` хранит **снимок** (одна строка на клиента, PK = `customer_id`), а здесь `ods.customer_status` хранит **все события** (PK = `customer_id + event_ts`). Это не ошибка, а сознательный выбор: источник данных о статусах - поток событий, и 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`.
> 💡 Для простоты сделайте загрузку идемпотентной через `TRUNCATE ods.customer_status;` перед `INSERT` (таблица в ODS имеет первичный ключ `(customer_id, event_ts)`).
Проверьте, что в `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` — период, когда статус был актуален;
- `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` для клиентов).
2. Для каждого клиента отсортируйте события по `event_ts` и с помощью `LEAD()` посчитайте (в учебном варианте считаем, что обновление DWH идёт раз в день, поэтому используем `DATE`):
- `valid_from``event_ts::date`,
- `valid_to` — следующий `event_ts::date` (а у последней версии `valid_to = NULL`).
> 💡 Упрощение для домашки: считаем, что у клиента не бывает двух разных смен статуса в один и тот же день.
> Если такое бывает — удобнее строить периоды в `TIMESTAMP` (или вводить дополнительное правило сортировки), но это уже усложнение.
> 💡 Если в событиях встречаются повторы одного и того же статуса подряд, можно отфильтровать “не-изменения” через `LAG(status)` (или `LAG(hashdiff)`) перед расчётом `LEAD()`.
3. Вставьте получившиеся строки в `dds.dim_customer_status`. Актуальная строка для клиента — та, где `valid_to IS NULL`.
> 💡 Для первой версии решения можно сделать full refresh: перед вставкой очистить таблицу (`TRUNCATE dds.dim_customer_status;`), как в основном примере с `dds.dim_customer`.
```sql
INSERT INTO dds.dim_customer_status (
customer_bk, status, hashdiff,
valid_from, valid_to,
created_at, updated_at
)
SELECT
...
```
Проверьте результат:
```sql
SELECT *
FROM dds.dim_customer_status
ORDER BY customer_bk, valid_from;
```
Ожидаемое поведение:
- у клиента 101 несколько строк с разными статусами и непересекающимися периодами;
- `valid_to IS NULL` только у самой свежей строки для каждого клиента.
### 4.2. Проверка себя
Примеры проверочных запросов (можно придумать свои):
- «Какой статус был у клиента 101 на дату `2024-06-01`
→ одна строка с нужным статусом.
- «Сколько клиентов были в статусе `active` на `2024-04-10`
→ несколько строк, если статус *активен* для диапазона дат.
---
## 5. Часть 3 — инкрементальная загрузка (по желанию)
Если хочется потренироваться глубже:
1. Добавьте ещё несколько событий смены статуса (например, переход части клиентов из `churned` обратно в `active`).
- Можно дописать в исходный CSV самостоятельно.
- Либо взять готовую порцию “для инкремента” из файла `dwh-modeling/data/customer_status_events_increment.csv`.
2. Загрузите **только новые строки** в `stg.customer_status_raw` (не делайте `TRUNCATE`):
```bash
cat dwh-modeling/data/customer_status_events_increment.csv | ./postgres-bookings/psql_sh -c \
"\\copy stg.customer_status_raw(customer_id,status,event_ts,_load_id,_load_ts) FROM STDIN WITH (FORMAT csv, HEADER true)"
```
3. Напишите логику инкрементального обновления `dds.dim_customer_status`:
- ориентируйтесь на пример из `03_demo_increment.sql` для `dds.dim_customer`;
- важно:
- корректно «закрыть» старую актуальную строку (заполнить `valid_to` датой начала новой версии);
- вставить новую строку с `valid_to = NULL`.
Эта часть особенно полезна, если вы хотите почувствовать, как SCD2 живёт в реальном DWH.
---
## 6. Часть 4 — витрина в DM (по желанию)
Опциональное задание для закрепления: собрать небольшую витрину с количеством клиентов по статусам на каждую дату.
DDL витрины уже создан в `07_ddl_hw_customer_status.sql` (таблица `dm.mart_customer_status_daily`).
Идея:
- использовать `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.
---
## 8. Эталонное решение
Когда выполните домашку и захотите сверить результат — готовое решение лежит в файле [`09_dml_hw_customer_status_solution.sql`](sql/09_dml_hw_customer_status_solution.sql).
Постарайтесь не подглядывать до того, как напишете свой вариант — основная ценность задания именно в самостоятельном разборе.