287 lines
15 KiB
Markdown
287 lines
15 KiB
Markdown
# Домашка: статусы клиента от 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 (в таблицах STG/ODS эта колонка будет называться `_load_ts`, но по смыслу это то же самое время загрузки).
|
||
|
||
Файл содержит несколько клиентов и несколько смен статуса по каждому — этого достаточно, чтобы отработать 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
|
||
|
||
Есть два варианта — выберите любой. Для первого прохождения рекомендуем **вариант 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` — это время загрузки (в CSV оно называется `load_ts`).
|
||
|
||
#### Вариант B: загрузить CSV
|
||
|
||
Можно загрузить файл `dwh-modeling/data/customer_status_events.csv` в таблицу `stg.customer_status_raw`:
|
||
|
||
- **Через DBeaver**: Import Data → CSV → `stg.customer_status_raw` (колонку `load_ts` маппить в `_load_ts`).
|
||
- **Через `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: очистка и типизация
|
||
|
||
В файле `08_dml_hw_customer_status_template.sql` найдите заготовку блока ODS и допишите SQL:
|
||
|
||
- привести:
|
||
- `customer_id` → `INT`,
|
||
- `status` → `VARCHAR(20)` (можно оставить как есть),
|
||
- `event_ts` и `load_ts` → `TIMESTAMP` (в DWH-таблицах эта колонка будет лежать как `_load_ts`);
|
||
- аккуратно обработать возможные пустые значения (если бы они были);
|
||
- заполнить `_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. Добавьте в CSV ещё несколько событий смены статуса (например, переход части клиентов из `churned` обратно в `active`).
|
||
2. Загрузите новые строки только в `stg.customer_status_raw`.
|
||
3. Напишите логику инкрементального обновления `dds.dim_customer_status`:
|
||
|
||
- ориентируйтесь на пример из `03_demo_increment.sql` для `dds.dim_customer`;
|
||
- важно:
|
||
- корректно «закрыть» старую актуальную строку (заполнить `valid_to` датой начала новой версии);
|
||
- вставить новую строку с `valid_to = NULL`.
|
||
|
||
Эта часть особенно полезна, если вы хотите почувствовать, как SCD2 живёт в реальном DWH.
|
||
|
||
---
|
||
|
||
## 6. Часть 4 — витрина в DM (по желанию)
|
||
|
||
Опциональное задание для закрепления: собрать небольшую витрину с количеством клиентов по статусам на каждую дату.
|
||
|
||
Перед началом убедитесь, что слой DM создан (схема `dm` и таблицы):
|
||
|
||
- выполните `dwh-modeling/sql/05_ddl_dm.sql` (один раз);
|
||
- затем можно собирать витрину.
|
||
|
||
Пример целевой таблицы:
|
||
|
||
```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.
|