Author SHA1 Message Date
ddadminandClaude Opus 4.6 f1a3ec00dd feat(site): добавлен раздел «Разработка с ИИ» с двумя статьями-переводами
- Зачем:
  - расширить роадмап практическим разделом о работе с ИИ-ассистентами.
- Что:
  - добавлена вводная страница ai-dev/README.md.
  - добавлен перевод «Лучшие практики работы с coding agents» (ai-dev/best-practice.md).
  - добавлен перевод «Механизм памяти coding agents» (ai-dev/memory-mechanism.md).
  - добавлен раздел «Разработка с ИИ» в навигацию mkdocs.yml.
- Проверка:
  - mkdocs build --strict проходит без ошибок.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:05:12 +03:00
ddadminandClaude Opus 4.6 f1d118b81e docs(project): обновлён статус Спринта 2, добавлен TODO
- Зачем:
  - зафиксировать прогресс Спринта 2 и запланированные задачи.
- Что:
  - PRD: отмечены выполненные пункты (Telegram, Метрика, collapsible).
  - добавлен project/TODO.md с задачами: перенос учебника Airflow, переработка раздела «Сложность алгоритмов», бейджи статуса.
  - домашка DWH: эталонное решение обёрнуто в <details> (dual-compatible).
- Проверка:
  - mkdocs build --strict без ошибок.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:57:05 +03:00
ddadminandClaude Opus 4.6 66267e8b8c feat(site): подключена Яндекс.Метрика (счётчик 108294923)
- Зачем:
  - нужна базовая аналитика посещаемости сайта.
- Что:
  - добавлен скрипт Яндекс.Метрики в блок extrahead шаблона overrides/main.html.
  - включены вебвизор, карта кликов и точный показатель отказов.
- Проверка:
  - mkdocs build --strict без ошибок.
  - счётчик присутствует на всех страницах собранного сайта.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:23:41 +03:00
ddadminandClaude Opus 4.6 33a95cc3e4 feat(site): добавлена кнопка «Написать в Telegram» и соцсети в футер
- Зачем:
  - менти и посетителям сайта нужен быстрый способ связаться с ментором.
- Что:
  - добавлена floating-кнопка Telegram (левый нижний угол, всегда видна при скролле).
  - добавлены иконки Telegram и GitHub в футер через extra.social.
  - создан template override (overrides/main.html) с inline-стилями.
- Проверка:
  - mkdocs build --strict проходит без ошибок.
  - кнопка видна на всех страницах, ведёт на t.me/dementev_dev.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:04:50 +03:00
ddadmin f62c6f761b docs(project): обновлён статус PRD — Спринт 1 завершён
- Зачем:
  - зафиксировать прогресс и актуальное состояние проекта.
- Что:
  - отмечены все чекбоксы MVP (Спринт 1) как выполненные.
  - отмечены кастомный домен и тёмная тема в Спринте 2.
  - обновлён URL сайта на de.dementev.space.
- Проверка:
  - project/PRD.md читаем на GitHub.
2026-03-27 23:28:27 +03:00
ddadmin 7dc5a2ae59 feat(site): подключён кастомный домен de.dementev.space
- Зачем:
  - презентабельный URL вместо dementev-dev.github.io/de-roadmap.
- Что:
  - добавлен файл CNAME с записью de.dementev.space.
  - обновлён site_url в mkdocs.yml.
- Проверка:
  - после распространения DNS: https://de.dementev.space/
2026-03-27 23:21:45 +03:00
ddadminandGitHub 537e77eba6 Merge pull request #1 from dementev-dev/feature/mkdocs-site
feat(site): MkDocs Material сайт с деплоем на GitHub Pages
2026-03-27 23:07:09 +03:00
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
ddadmin 721ed4163b docs(project): добавлены PRD и ADR для сайта de-roadmap
- Зачем:
  - зафиксировать проектные решения до начала реализации.
- Что:
  - добавлен project/PRD.md — требования, цели, итерации и риски.
  - добавлен project/ADR.md — выбор MkDocs Material, структура файлов, стратегия dual-compatible ссылок, конфиг CI/CD.
- Проверка:
  - файлы читаемы на GitHub: project/PRD.md и project/ADR.md.
2026-03-25 23:38:34 +03:00
ddadmin 2967664e43 Merge branch 'roadmap' 2026-03-15 16:56:27 +03:00
ddadmin e66ed85af2 docs(readme): переработана секция «Карьера и менторство»
- Зачем:
  - плейсхолдеры и внутренние заметки ментора были видны студентам.
- Что:
  - заменён плейсхолдер про резюме на описание совместной практики с ментором.
  - секция «Навыки поиска работы с HH и Habr карьера» переименована в «Поиск работы и собеседования».
  - убран дубль ссылки на видео про испытательный срок.
  - плейсхолдеры про собесы заменены на пункт «мок-собеседования с ментором» и отметку «в подготовке».
- Проверка:
  - визуальная проверка рендеринга README.md.
2026-03-14 22:28:29 +03:00
ddadmin deed1a4bcc merge(roadmap): доработки README — новые секции, исправлены ссылки, упрощено оглавление 2026-03-14 22:09:50 +03:00
ddadminandClaude Opus 4.6 515fa6a5de docs(readme): доработан роадмап — новые секции, исправлены ссылки, упрощено оглавление
- Зачем:
  - убраны мёртвые и архивные ссылки на чужой форк (HalltapeRoadmapDE), наполнены пустые секции, добавлены недостающие темы
- Что:
  - оглавление упрощено до H1/H2, убран self-link
  - исправлен casing GreenPlum → Greenplum
  - убраны 4 архивные ссылки на HalltapeRoadmapDE (Greenplum, DWH, Spark, dbt)
  - добавлена подсекция venv перед Jupyter Lab в формате буллет-группы
  - NiFi и Kafka объединены в секцию «Streaming (NiFi + Kafka)» с единым стендом nifi-kafka-postgres-lab
  - добавлена preview-секция «Lakehouse (Spark, Iceberg, Trino)» с русскоязычным видео и стендом mini-lakehouse-lab
  - секция dbt наполнена: quickstart, перевод документации, практика с jaffle-shop
  - обновлены вводный текст и критерии прохождения блока «Расширенные навыки»
- Проверка:
  - визуально проверить рендеринг якорей оглавления в GitHub

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 22:04:45 +03:00
ddadmin e78d5a1a35 docs(readme): реорганизована секция Greenplum и курсовой работы
- Зачем:
  - упрощение роадмапа: курсовая и практика Greenplum теперь используют один стенд.
- Что:
  - курс Yandex по Greenplum выделен как основной.
  - практика по Greenplum привязана к стенду airflow-dwh-gp-lab.
  - курсовая работа переписана под использование того же стенда.
  - добавлено описание эталонного DWH и автоматической проверки.
  - удалена секция "Стенд в Docker Compose" из оглавления.
- Проверка:
  - git diff HEAD~1 README.md.
2026-03-14 19:55:03 +03:00
ddadmin fd3f591802 Merge branch 'main' into roadmap 2026-03-14 19:31:28 +03:00
ddadmin c6c67d1fc6 merge(dwh-modeling): влита ветка dwh-modeling-basics с материалами по DWH
- Зачем:
  - объединить улучшенные учебные материалы по DWH-моделированию в основную ветку.
- Что:
  - добавлен COMMIT_RULES.md с правилами оформления коммитов.
  - добавлено решение домашнего задания (09_dml_hw_customer_status_solution.sql).
  - улучшена документация: расширены разделы 3NF и Звезда, добавлены пояснения по ODS.
  - исправлены опечатки и SQL-скрипты по результатам ревью.
  - обновлены CSV-данные для корректной работы примеров.
- Проверка:
  - git log --oneline -5.
  - просмотр изменённых файлов в dwh-modeling/.
2026-02-21 21:42:49 +03:00
ddadminandClaude Opus 4.6 b3826f47cb fix(modeling): пример витрины в README приведён в соответствие со скриптами
- customer_segment: убрана несуществующая колонка dim_customer, заменена
  на CASE по сумме (как в 06_dml_dm.sql)
- MATERIALIZED VIEW заменён на TRUNCATE + INSERT INTO (как в скриптах),
  упоминание MV оставлено в ремарке
- добавлен комментарий о недетерминированности CURRENT_DATE в bounds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 19:21:39 +03:00
ddadminandClaude Opus 4.6 c6a20f3cf7 fix(modeling): исправлены ошибки по результатам перекрёстного ревью
- Зачем:
  - в статье и скриптах обнаружены фактические ошибки и несостыковки
- Что:
  - README: f.order_date -> d.date_actual в примере витрины (колонки order_date нет в fact_sales)
  - README: «календарь на 10 лет» -> «5 лет» (соответствует генерации 2023-2027 в скрипте)
  - 09_solution: bounds CTE теперь использует CURRENT_DATE для открытых интервалов (valid_to IS NULL)
  - 09_solution: комментарий про дубли в STG при повторном запуске блока 3
  - 05_ddl_dm: выравнивание total_line_items
  - AGENTS.md: обновлён диапазон скриптов 01-09
- Проверка:
  - визуальная проверка diff

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:13:38 +03:00
ddadminandClaude Opus 4.6 14a61a3b46 docs(modeling): пояснена разница между снимковым и событийным ODS
- Зачем:
  - менти видит два разных паттерна ODS (снимок vs event log) и не понимает почему
- Что:
  - в решении домашки (блок 1 ODS): комментарий, почему customer_status хранит все события
  - в домашке (раздел 3.2): пометка о сознательном выборе модели ODS
- Проверка:
  - визуальная проверка diff

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:55:38 +03:00
ddadminandClaude Opus 4.6 f830d54cd7 refactor(sql): улучшено решение домашки как учебный материал
- Зачем:
  - решение домашки должно быть самодостаточным и наглядным для самопроверки
- Что:
  - добавлены контрольные SELECT после каждого блока (ODS, DDS, инкремент, DM)
  - блок 3 (инкремент) стал самодостаточным: загрузка в STG + UPSERT в ODS + SCD2
  - DDL витрины вынесен из решения/шаблона/домашки в 07_ddl_hw_customer_status.sql
  - предусловия в домашке дополнены (05_ddl_dm.sql, пояснение про dim_date)
  - в шапку решения добавлено напоминание сначала попробовать самостоятельно
- Проверка:
  - визуальная проверка diff

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:49:49 +03:00
ddadminandClaude Opus 4.6 1d99539052 docs(modeling): расширены разделы 3NF и Звезда практическими примерами
- Зачем:
  - разделы 3NF и Звезда были слишком краткими для учебного материала,
    менти не видел разницу между моделями на практике
- Что:
  - 3NF: добавлена mermaid-диаграмма с dim_city и SQL-запрос (3 JOIN)
  - Звезда: добавлена явная связь с разделом 5, SQL-запрос (2 JOIN) для контраста
  - оба примера отвечают на один вопрос: «сколько потратил клиент из Москвы?»
- Проверка:
  - визуальная проверка diff

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:41:42 +03:00
ddadminandClaude Opus 4.6 45f8f74378 refactor(modeling): улучшены учебные материалы DWH по результатам ревью
- Зачем:
  - убрать путаницу, дублирование и неточности в демо-скриптах и домашке
- Что:
  - CSV: заголовок load_ts → _load_ts во всех файлах (совпадает с именем в таблицах)
  - домашка: убраны оговорки о расхождении load_ts/_load_ts, добавлена ссылка на эталонное решение
  - 09_dml_hw_customer_status_solution.sql: эталонное решение скопировано из ветки solution/hw_customer_status в основную
  - 02_dml: добавлена карта загрузки в шапку (что откуда строится)
  - 05_ddl_dm + 06_dml_dm: total_orders → total_line_items (название точнее отражает содержимое)
- Проверка:
  - визуальная проверка diff, скрипты не запускались (демо-стенд не поднят)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 14:31:10 +03:00
ddadmin 8973eea064 feat(docs): добавлены правила коммитов COMMIT_RULES.md
- Зачем:
  - нужна единая спецификация для сообщений коммитов
  - облегчение code review и читаемости истории
- Что:
  - создан COMMIT_RULES.md с правилами Conventional Commits
  - адаптированы scopes под репозиторий: sql, modeling, bookings, docs, data
  - обновлена секция в AGENTS.md с ссылкой на полные правила
- Проверка:
  - git log --oneline -1
  - cat COMMIT_RULES.md | head -20
2026-02-21 14:29:23 +03:00
ddadmin 973883385b Обновлена статья DWH-моделирование: переработан раздел Data Vault, исправлены опечатки, добавлена ссылка на домашку 2026-02-21 14:05:16 +03:00
ddadmin ebeb6bbf68 мелкая правка 2026-02-01 21:22:00 +03:00
ddadmin f31e265454 Merge branch 'roadmap' 2026-02-01 21:18:45 +03:00
ddadmin a7742257f8 Расширение оглавления 2026-02-01 21:18:13 +03:00
ddadmin 39fb62fc32 Критерии усвоения методологий разработки 2026-02-01 21:07:30 +03:00
ddadmin 7b0418ccc5 Перенос подраздела в раздел 2026-02-01 21:03:47 +03:00
ddadmin cd3aed30d9 Методологии разработки 2026-02-01 18:55:40 +03:00
ddadmin d284d13046 Теория про OLAP БД 2026-01-22 21:35:11 +03:00
ddadmin 6871172da3 Главы из 2х книжек 2026-01-22 21:29:58 +03:00
ddadmin 1b73649d40 Подробнее про звезду + 3NF + иллюстрации 2026-01-20 21:15:01 +03:00
ddadmin bb52fe3de4 Уточнение названия блока 2026-01-09 21:07:53 +03:00
ddadmin 6fa530dab5 Данные для инкремента статусов 2025-12-21 21:40:43 +03:00
ddadmin 20ea175e3a Улучшения домашки 2025-12-21 21:09:52 +03:00
ddadmin a347988af2 возможные планы 2025-12-21 20:23:20 +03:00
ddadmin 855530bee5 Более понятная реализация scd2 2025-12-21 20:23:07 +03:00
ddadmin d342519bb5 одна из целей - анализ плана запросов 2025-12-21 19:56:26 +03:00
ddadmin ae00475df5 переписана реализация scd2 2025-12-21 18:50:50 +03:00
ddadmin 87301921a9 Merge branch 'roadmap' 2025-11-30 12:06:23 +03:00
ddadmin 4bec6217cf Правки по ходу перечитывания 2025-11-30 12:02:30 +03:00
ddadmin 5e1c80f870 Merge branch 'main' into roadmap 2025-11-30 11:18:36 +03:00
ddadmin ba9cdff421 Причесывание комментариев 2025-11-29 21:37:03 +03:00
ddadmin eb9215c4f1 Уулчшения витрин 2025-11-29 21:24:08 +03:00
ddadmin e659fbb731 Исправление неточностей 2025-11-29 20:46:03 +03:00
ddadmin 218c6b9fe7 Исправлиение примера с scd2 2025-11-29 20:16:16 +03:00
ddadmin 2d78fb4459 Домашка по моделированию 2025-11-23 18:31:43 +03:00
ddadmin 04573efda1 Раздел про курсовую работу 2025-11-23 18:16:18 +03:00
ddadmin e53fa0b9d4 Очередная чистка мелких шероховатостей 2025-11-23 17:58:17 +03:00
ddadmin 26a35b0560 DoD по разделам 2025-11-23 17:47:05 +03:00
ddadmin e37c1ed1cc Критерии пройденности разделов 2025-11-23 17:36:06 +03:00
ddadmin 805141a0b9 Смена тональности обращений 2025-11-23 13:30:15 +03:00
ddadmin e5615c9419 Расширен раздел с GreenPlum 2025-11-23 13:18:34 +03:00
ddadmin 64fe665cdc Ссылки на ментора 2025-11-23 12:46:11 +03:00
ddadmin d46d9848f4 Полировка формулировок 2025-11-23 12:20:35 +03:00
ddadmin d5cee7ae6d Про аудит 2025-11-23 12:15:35 +03:00
ddadmin d8c98c73c5 Добавлена шпаргалка 2025-11-23 12:13:53 +03:00
ddadmin add67223b8 Мелкие правки 2025-11-23 12:07:59 +03:00
ddadmin 0521b6cc9a Правка реализации истории 2025-11-23 11:56:51 +03:00
ddadmin ee4b6ae5a4 AI guideline 2025-11-23 11:37:04 +03:00
ddadmin 58fe605116 Добавлено оглавление 2025-11-15 21:38:59 +03:00
ddadmin 71d885a9db Чистка заголовков 2025-11-15 21:20:52 +03:00
ddadmin b2ae216b07 Merge branch 'main' into dwh-modeling-basics 2025-11-15 21:01:04 +03:00
ddadmin f1338ccfc5 Детальнее про pit и bridge таблицы 2025-11-15 20:59:53 +03:00
ddadmin aa3ebbfd7c Переработка структуры 2025-11-15 20:47:08 +03:00
ddadmin 32ffcfb4c5 Фикс диаграммы в практикуме 2025-11-15 20:44:59 +03:00
ddadmin 5f16476c81 Первый вариант статьи про DV 2025-11-15 20:00:58 +03:00
ddadmin bb9902a448 Упрощение раздела про DV 2025-11-15 19:08:20 +03:00
ddadmin c66ba776c2 Пересобран раздел 8 2025-11-15 18:43:43 +03:00
ddadmin 51c33a543d Маленькая склейка 2025-11-15 18:36:25 +03:00
ddadmin e48fa424e2 Еще битые ссылки 2025-11-15 18:33:46 +03:00
ddadmin 9175f84cf9 Битые ссылки 2025-11-15 18:29:23 +03:00
ddadmin ecb41941fc Улучшения диаграммы в "базовые понятия" 2025-11-15 18:13:06 +03:00
ddadmin a8a122bdfa Переделка раздела советов для сомневающихся 2025-11-15 17:57:35 +03:00
ddadmin e7ba8cebd0 Еще материал по DV 2025-11-15 13:03:52 +03:00
ddadmin 0d9bb047a4 Раздел про Data Vault 2025-11-15 12:53:02 +03:00
ddadmin ff6086ff21 Стилистическая правка 2025-11-15 12:29:31 +03:00
ddadmin dabcadfc19 Правка ошибок 2025-11-10 11:22:47 +03:00
ddadmin fde77d33ce Интеграция структуры хранилища данных в общий план обучения 2025-11-09 13:30:44 +03:00
ddadmin d5220adcf0 В статью добавлены ссылки на скрипты с учебным примером 2025-11-08 22:16:11 +03:00
ddadmin 3cbaeb5fc2 Переименования скриптов 2025-11-08 20:42:35 +03:00
ddadmin a234c2954d Вычитка статьи 2025-11-08 20:40:04 +03:00
ddadmin c52d0d8b31 Стилистические правки 2025-11-08 19:21:43 +03:00
ddadmin be1557716f Забыли заполнить dds.orders 2025-11-07 16:31:23 +03:00
ddadmin cefe9971b8 Проверки 2025-11-07 16:31:09 +03:00
ddadmin 3cb77d9cf6 SCD2 теперь строится 2025-11-07 16:15:25 +03:00
ddadmin 57d1dd3186 Уточнения по SCD 2 - бех юэкфилл 2025-11-07 15:46:52 +03:00
ddadmin b29de24c31 SCD2 для dim_customers 2025-11-07 15:10:30 +03:00
ddadmin 42c8515b62 Доработки идей 2025-11-07 12:25:41 +03:00
ddadmin dfc5748358 Чистовик статьи + материалы 2025-11-07 11:05:44 +03:00
ddadmin fa42a4508f Дальнейшая доработка 2025-11-07 10:22:11 +03:00
ddadmin ee55515b54 Развитие идей 2025-11-07 09:58:37 +03:00
ddadmin acfbc4d037 Правка плани 2025-11-04 23:15:14 +03:00
ddadmin a17c775c1d Вариант от deepseek 2025-11-04 21:26:13 +03:00
ddadmin 1879eb341f Чистка 2025-11-04 21:22:03 +03:00
ddadmin fc6282a4d4 Чистка заголовков 2025-11-04 21:12:15 +03:00
ddadmin 960bfa3ce0 Фикс графиков 2025-11-04 21:00:31 +03:00
ddadmin e16ea6e0cf Первоначальный план статьи 2025-11-04 20:54:14 +03:00
ddadmin e9b50406e3 Пререработка структуры заголовков 2025-11-04 18:00:40 +03:00
ddadmin 197ef1b0d2 Упрощение 2025-11-04 17:45:03 +03:00
ddadmin f18bd7ea39 Раскрыт детальнее update для trino 2025-11-04 17:39:17 +03:00
ddadmin f789839643 Расширен раздел про SCD2 для append-only систем 2025-11-04 17:20:35 +03:00
ddadmin 1730f6801b Документация по dbc 2025-11-03 17:36:49 +03:00
ddadmin 20acb883a9 Merge branch 'main' of github.com-dementev:dementev-dev/de-roadmap 2025-10-26 22:24:18 +03:00
ddadmin 4979a1dc57 Больше материалов по Python 2025-10-26 22:24:14 +03:00
ddadmin 63b8811776 Ссылка на статью про Parquet и Iceberg 2025-10-24 10:35:10 +03:00
ddadmin 43fb42cfb0 Ссылка на учебник 2025-10-19 22:27:23 +03:00
ddadmin 9793ab209e Ссылка на Jupyter Lab 2025-10-19 16:13:00 +03:00
ddadmin d25db5b780 Развитие тематики Python 2025-10-19 13:17:29 +03:00
ddadmin 15f4140ef8 Фикс оформления 2025-10-19 13:13:29 +03:00
ddadmin 69faae7f25 Материал про Jupyter Lab 2025-10-19 13:12:59 +03:00
ddadmin 42754739cf Раздел про Python - ООП, Pandas 2025-10-19 13:07:23 +03:00
ddadmin 6fd8b69d1c Добавлена ссылка на SCD 2025-10-19 12:35:24 +03:00
ddadmin ed5a18d655 Статья про SCD 2025-10-19 12:33:27 +03:00
ddadmin 4693147750 Фикс опечатки 2025-10-18 20:23:50 +03:00
ddadmin f636d74e02 Раздел про моделирование данных 2025-10-18 19:09:44 +03:00
ddadmin f374047387 Улучшение структуры 2025-10-18 18:44:18 +03:00
ddadmin b43630cf49 Материалы по Docker 2025-10-16 23:28:06 +03:00
ddadmin d699696869 Фикс оформления 2025-10-16 23:19:32 +03:00
ddadmin 459cb5ab2b Приоретизация материалов 2025-10-16 23:18:51 +03:00
ddadmin 253268bd60 Главы книги, которые стоит прочесть 2025-10-16 23:17:44 +03:00
ddadmin 07f9e0c5d2 Доп видео по git 2025-10-16 23:10:45 +03:00
41 changed files with 5519 additions and 87 deletions
+43
View File
@@ -0,0 +1,43 @@
name: Deploy MkDocs to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- run: pip install mkdocs-material==9.6.14 mkdocs-same-dir==0.1.3
- run: mkdocs build --strict
- uses: actions/upload-pages-artifact@v3
with:
path: site/
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
+4
View File
@@ -1 +1,5 @@
.env .env
.idea
.internal/
site/
tmp
+79
View File
@@ -0,0 +1,79 @@
# Repository Guidelines
## Project Structure & Module Organization
- Root `README.md` describes the learning roadmap (RU) and serves as the main page of the MkDocs site.
- `dwh-modeling/` contains the article and demo DWH model; SQL lives in `dwh-modeling/sql` as ordered scripts `01_...sql``09_...sql` (0709 are homework DDL, template and solution).
- `postgres-bookings/` is a Dockerized PostgreSQL + demo “bookings” DB; start it first, then apply DWH scripts against the `demo` database.
- `mkdocs.yml` — MkDocs Material config; `docs_dir: .` (repo root = site root). Excluded dirs: `project/`, `postgres-bookings/`, `.github/`, `.claude/`.
- `.github/workflows/deploy-site.yml` — CI/CD: push to `main` → build → deploy to GitHub Pages.
- `project/` — PRD and ADR (excluded from site).
## Build, Test, and Development Commands
- Start demo Postgres:
`cd postgres-bookings && bash download_db.sh && docker compose up -d`
- Open `psql` inside the container:
`cd postgres-bookings && ./psql_sh`
- Apply DWH schema from the repo root (after Postgres is up):
`psql -h 127.0.0.1 -p 5432 -U postgres -d demo -f dwh-modeling/sql/01_ddl_stg-dds.sql`
- Stop and reset the cluster when needed:
`cd postgres-bookings && docker compose down -v`
## Coding Style & Naming Conventions
- SQL: PostgreSQL dialect, uppercase keywords, `snake_case` identifiers, 4-space indentation, and concise comments (`-- ...`).
- SQL files: keep numeric prefixes (`01_`, `02_`, …) to reflect execution order and use descriptive suffixes like `ddl_*` / `dml_*`.
- Shell: target `bash`, prefer simple, POSIX-friendly constructs; mirror the style of existing scripts in `postgres-bookings/`.
## Markdown Style (dual-compatible: GitHub + MkDocs Material)
All `.md` files MUST render correctly on both GitHub and the MkDocs Material site. Follow these rules:
**Headings:**
- `README.md` (root): start from `##` (H2). Do NOT use `#` (H1) — MkDocs uses H1 as page title, multiple H1s break the right-sidebar TOC.
- `dwh-modeling/*.md`: may use `#` (H1) since each file is a separate page on the site.
**Lists:**
- Always leave a **blank line before the first list item** after a paragraph, heading, or any non-list text. Python-Markdown (MkDocs) requires this; GitHub tolerates its absence but blank lines don't hurt.
- Use **4-space indentation** for nested lists (not 2-space). GitHub supports both, Python-Markdown requires 4.
- Example:
```markdown
Some introductory text:
- Item one
- Item two
- Nested item (4 spaces)
```
**Links:**
- Internal links: always use **relative paths to `.md` files**: `[text](dwh-modeling/SCD.md)`. MkDocs resolves them automatically.
- Anchor links: use lowercase slugs with single hyphens. Avoid em-dash `` in headings (it produces `--` in MkDocs slugs vs `-` on GitHub). Use commas or colons instead.
**Special characters in headings:**
- OK: colons `:`, commas `,`, parentheses `()`, guillemets `«»` — stripped equally by both platforms.
- Avoid: em-dash ``, en-dash `` — slug behavior differs between GitHub and MkDocs.
## MkDocs Site Commands
- Local preview (user starts, ask user to run via `!`):
`uv run --with 'mkdocs-material==9.6.14' --with 'mkdocs-same-dir==0.1.3' mkdocs serve`
- Build with strict validation (catches broken links/anchors):
`uv run --with 'mkdocs-material==9.6.14' --with 'mkdocs-same-dir==0.1.3' mkdocs build --strict`
- Visual check via Playwright (when `mkdocs serve` is running on port 8000):
`npx playwright screenshot --viewport-size='1280,800' 'http://127.0.0.1:8000/#anchor' /path/to/screenshot.png`
Then read the screenshot with the Read tool to inspect rendering. Use `--viewport-size='1280,2000'` for tall pages.
- Kill stuck dev server: `lsof -ti :8000 | xargs kill`
- Site URL: `https://dementev-dev.github.io/de-roadmap/`
## Testing Guidelines
- There is no dedicated test framework; treat SQL scripts as executable documentation.
- For `dwh-modeling/sql`, run scripts sequentially and rerun `04_validation.sql` after changes to ensure the demo model still loads and basic checks pass.
- For `postgres-bookings`, after modifications run `docker compose up -d && ./psql_sh` and verify simple queries such as `SELECT COUNT(*) FROM bookings.flights;`.
## Commit & Pull Request Guidelines
**Required:** Read [COMMIT_RULES.md](COMMIT_RULES.md) before making commits.
Pull requests should focus on one topic, include a brief context, list of changes, and manual steps to reproduce or validate (commands you ran, expected results).
## Security & Configuration Tips
- Do not commit personal `.env` files or credentials; use local overrides only.
- Demo credentials and ports in `postgres-bookings` are for local training only—never reuse them in shared or production environments.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+1
View File
@@ -0,0 +1 @@
de.dementev.space
+220
View File
@@ -0,0 +1,220 @@
# Commit Rules
Unified commit style for all project contributors. Follows [Conventional Commits](https://www.conventionalcommits.org/) specification.
## Language
- **Primary language**: Russian
- If language is not specified, use Russian
- For AI-generated commits, Russian is mandatory unless task explicitly sets `lang:en`
- English is allowed only by explicit instruction (`lang:en`) or external collaboration requirements
- Do not mix languages in free-text parts of one commit message (subject + body + footer)
- Conventional Commit `type(scope)` stays in English
- Technical terms (PostgreSQL, SQL, DWH, DDL, DML) keep as-is
## Header Format
```
<type>(<scope>): <short description>
```
- Maximum header length: 72 characters
- For Russian subject, use result form (e.g. "добавлено", "исправлено", "обновлено")
- For English subject, use imperative present form (e.g. "add", "fix", "update")
- For English subject, do not use past forms (e.g. "added", "fixed", "updated")
- No trailing period
- Keep subject specific; avoid vague messages like "update", "fix bug", "changes"
### Allowed `type`
| Type | Description |
|------|-------------|
| `feat` | New feature |
| `fix` | Bug fix |
| `refactor` | Code restructuring without behavior change |
| `docs` | Documentation only |
| `test` | Tests, checks, validations |
| `chore` | Maintenance (configs, scripts, hooks) |
| `ci` | CI/CD changes |
| `perf` | Performance optimization |
| `revert` | Revert previous commit |
### Recommended `scope` for this repo
| Scope | Used for |
|-------|----------|
| `sql` | SQL scripts in `dwh-modeling/sql/` |
| `modeling` | DWH modeling docs, articles, schemas in `dwh-modeling/` |
| `bookings` | Docker PostgreSQL demo in `postgres-bookings/` |
| `docs` | Documentation, README, guides |
| `data` | Data files (CSV, fixtures) |
## Body Structure
For non-trivial changes, body is required. Use bullet points for readability.
Body is considered required when at least one condition is true:
- behavior or API/contract changed
- migration, rollback risk, or compatibility impact exists
- more than one meaningful file/module changed
- fix is non-obvious from header alone
### Multiline body in CLI (important)
- Do not pass body as one quoted string with `\n` (it will be stored literally).
- Use multiple `-m` flags, or `-F` with heredoc.
Correct:
```bash
git commit \
-m "feat(sql): добавлена валидация данных для DWH" \
-m "- Зачем:
- нужна проверка целостности перед загрузкой
- Что:
- добавлен скрипт 04_validation.sql
- добавлены проверки на NULL и уникальность
- Проверка:
- psql -f dwh-modeling/sql/04_validation.sql"
```
Also correct:
```bash
git commit -F- <<'MSG'
feat(sql): добавлена валидация данных для DWH
- Зачем:
- нужна проверка целостности перед загрузкой
- Что:
- добавлен скрипт 04_validation.sql
- добавлены проверки на NULL и уникальность
- Проверка:
- psql -f dwh-modeling/sql/04_validation.sql
MSG
```
### Template (Russian - default)
```
<type>(<scope>): <краткое описание результата>
- Зачем:
- причина изменения
- Что:
- ключевое изменение 1
- ключевое изменение 2
- Проверка:
- как проверено
```
### Template (English - only with `lang:en`)
```
<type>(<scope>): <short action description>
- Why:
- reason for change
- What:
- key change 1
- key change 2
- Check:
- how verified (command/test/smoke-check)
```
## Commit Scope Rules
- One commit = one logical task
- Don't mix feature changes with large refactoring
- Update docs in the same commit where behavior changes
## Breaking Changes
Use `!` in header for breaking changes:
```
feat(sql)!: rename stg_orders column contract
```
Add footer:
```
BREAKING CHANGE: column order_date renamed to created_at
```
## Examples
### Good examples
```
feat(sql): добавлен скрипт загрузки DM-слоя
- Зачем:
- нужны витрины для аналитики
- Что:
- добавлен 06_dml_dm.sql с загрузкой фактов и измерений
- добавлены индексы для оптимизации запросов
- Проверка:
- psql -f dwh-modeling/sql/06_dml_dm.sql
- SELECT COUNT(*) FROM dm.fact_orders;
```
```
fix(bookings): исправлен порт в docker-compose.yml
- Зачем:
- конфликт с локальным PostgreSQL на 5432
- Что:
- порт хоста изменен на 5433
- Проверка:
- docker compose up -d
- psql -h localhost -p 5433 -U postgres
```
```
docs(modeling): обновлена схема Data Vault после ревью
```
```
chore(docs): синхронизировано оглавление README
```
### Bad examples (don't do this)
```
❌ added sql script # no type, past tense
❌ feat: добавлен скрипт # no scope
❌ fix: исправлен баг # no scope, vague and non-actionable
❌ feat(sql): added new table # past tense in English subject
❌ feat(sql): add script and fix validation and update docs # multiple concerns
❌ feat(docs): add README и почини SQL # mixed languages in one message
```
## Quick Reference
```bash
# Feature
feat(scope): добавлена новая возможность
# Bug fix
fix(scope): исправлена проблема
# Documentation
docs(scope): обновлена документация
# Refactoring
refactor(scope): упрощена структура без изменения поведения
# Performance
perf(scope): ускорено выполнение
# Maintenance
chore(scope): обновлены служебные настройки
# Feature (lang:en)
feat(scope): add new capability
# Bug fix (lang:en)
fix(scope): correct response parsing
# Documentation (lang:en)
docs(scope): update setup guide
```
+411 -83
View File
@@ -1,29 +1,77 @@
# Основные знания ## О роадмапе
## База по Git Этот роадмап — конспект моих подходов к обучению Data Engineering.
Он подойдёт тем, кто хочет:
- системно войти в профессию с нуля или близкого к нулю уровня;
- закрыть пробелы в базе (SQL, Git, Python, DWH, Airflow, Greenplum);
- подготовиться к собеседованиям и первым рабочим задачам.
Роадмап можно проходить самостоятельно или вместе со мной в формате менторства.
Если хотите идти с поддержкой ментора — напишите в Telegram: [@dementev_dev](https://t.me/dementev_dev).
### Оглавление
- [Основные знания](#основные-знания) — Git, SQL, Python, методологии, Docker
- [Практика и инструменты](#практика-и-инструменты) — Airflow, Greenplum, курсовая работа
- [Карьера и менторство](#карьера-и-менторство) — резюме, собеседования, испытательный срок
- [Расширенные навыки](#расширенные-навыки) — ClickHouse, Streaming, Lakehouse, dbt
- [Софт скиллы](#софт-скиллы)
- [Дополнительные материалы](#дополнительные-материалы)
Рекомендуемый способ использования:
- двигаться по разделам последовательно, не перепрыгивая через базу;
- выполнять практику и домашки, а не только смотреть материалы;
- возвращаться к разделам по мере появления реальных задач.
---
## Основные знания
[[к оглавлению]](#оглавление)
### Git и базовые инструменты
#### База по Git
Что такое контроль версий, когда используется, ПОЧЕМУ и как мы в обучении будем использовать. Что такое контроль версий, когда используется, ПОЧЕМУ и как мы в обучении будем использовать.
Как создать репозиторий на GitHub, сохранять в нем изменения. Как создать репозиторий на GitHub, сохранять в нем изменения.
- [Что такое Git для Начинающих / GitHub за 30 минут / Git Уроки - Youtube](https://www.youtube.com/watch?v=VJm_AjiTEEc) - [Что такое Git для Начинающих / GitHub за 30 минут / Git Уроки - Youtube](https://www.youtube.com/watch?v=VJm_AjiTEEc)
- [Git: Конфликты для Начинающих // Git Cherry Pick, Git Revert, Git Reset - Youtube](https://www.youtube.com/watch?v=F7FnnfnB9YY)
- Книга [Pro Git](https://git-scm.com/book/ru/v2) - читать главу 1 - Книга [Pro Git](https://git-scm.com/book/ru/v2) - читать главу 1
Основы Markdown #### Основы Markdown
- [Язык Markdown и файл README | Git и GitHub для начинающих - Youtube](https://www.youtube.com/watch?v=8lEDTrr-G4U) - [Язык Markdown и файл README | Git и GitHub для начинающих - Youtube](https://www.youtube.com/watch?v=8lEDTrr-G4U)
- [Markdown и его возможности: простой способ оформления текста](https://kurshub.ru/journal/blog/markdown-chto-eto/) - [Markdown и его возможности: простой способ оформления текста](https://kurshub.ru/journal/blog/markdown-chto-eto/)
- [Синтаксис Markdown: подробная шпаргалка для веб-разработчиков / Skillbox Media](https://skillbox.ru/media/code/yazyk-razmetki-markdown-shpargalka-po-sintaksisu-s-primerami/) - [Синтаксис Markdown: подробная шпаргалка для веб-разработчиков / Skillbox Media](https://skillbox.ru/media/code/yazyk-razmetki-markdown-shpargalka-po-sintaksisu-s-primerami/)
Домашки по остальным темам тренируемся делать в Git, там же пишем документацию. Домашки по остальным темам тренируемся делать в Git, там же пишем документацию.
## SQL
### База по SQL **Когда блок Git и базовые инструменты считаем пройденным:**
- вы уверенно создаёте репозиторий, коммитите изменения и отправляете их на GitHub;
- имеете представление о работе с ветками: создание, переключение, что такое merge/PR и разруливание простых конфликтов;
- оформляете базовую документацию в Markdown (README, заголовки, списки, ссылки, кодовые блоки).
### Базы данных: SQL и моделирование данных
SQL и моделирование данных специально идут рядом: сначала учимся уверенно извлекать данные запросами, затем — понимать и проектировать структуру данных, чтобы ETL/витрины были осмысленными.
#### База по SQL
Книга: [PostgreSQL. Основы языка SQL](https://postgrespro.ru/education/books/sqlprimer) - Глава 1 "Введение в базы данных и SQL" + ДЗ Книга: [PostgreSQL. Основы языка SQL](https://postgrespro.ru/education/books/sqlprimer) - Глава 1 "Введение в базы данных и SQL" + ДЗ
Бесплатный тренажер: [Интерактивный тренажер по SQL – Stepik](https://stepik.org/course/63054/promo) Бесплатный тренажер: [Интерактивный тренажер по SQL – Stepik](https://stepik.org/course/63054/promo)
Целевой уровень знания SQL - Live кодинг на собесе. Проверяем на первом мок-интервью Целевой уровень знания SQL - Live кодинг на собесе. Проверяем на первом мок-интервью
**СТЕ**
**CTE**
- Зачем нам CTE: [Getting started with CTEs | dbt Labs](https://www.getdbt.com/blog/getting-started-with-cte) - Зачем нам CTE: [Getting started with CTEs | dbt Labs](https://www.getdbt.com/blog/getting-started-with-cte)
- Подробнее про синтаксис: [PostgreSQL : Документация: 17: 7.8. Запросы WITH (Общие табличные выражения) : Компания Postgres Professional](https://postgrespro.ru/docs/postgresql/17/queries-with) - Подробнее про синтаксис: [PostgreSQL : Документация: 17: 7.8. Запросы WITH (Общие табличные выражения) : Компания Postgres Professional](https://postgrespro.ru/docs/postgresql/17/queries-with)
Для дальнейшей тренировки и поддержания уровня можно использовать [Database - LeetCode](https://leetcode.com/problem-list/database/). Хорошая подборка задачек: [SQL 50 - Study Plan - LeetCode](https://leetcode.com/studyplan/top-sql-50/) Для дальнейшей тренировки и поддержания уровня можно использовать [Database - LeetCode](https://leetcode.com/problem-list/database/). Хорошая подборка задачек: [SQL 50 - Study Plan - LeetCode](https://leetcode.com/studyplan/top-sql-50/)
### Повышение знаний SQL
#### Повышение знаний SQL
Смотрим курс от Postgres Pro [DEV1](https://postgrespro.ru/education/courses/DEV1) Смотрим курс от Postgres Pro [DEV1](https://postgrespro.ru/education/courses/DEV1)
Темы - от "Введение" до "SQL" включительно, "Управление доступом", "Резервное копирование". Для лучшего усваивания материала проделываем все примеры и домашние задания из конспектов лекция. Темы - от "Введение" до "SQL" включительно, "Управление доступом", "Резервное копирование". Для лучшего усваивания материала проделываем все примеры и домашние задания из конспектов лекция.
С темой "PL/pgSQL" можно ознакомиться обзорно. С темой "PL/pgSQL" можно ознакомиться обзорно.
@@ -31,127 +79,407 @@
Для развития навыков инженера будет полезно лабораторные работы делать не в виртуальной машине, а в docker контейнере. Предложенный (не обязательный) вариант - в каталоге `postgres-bookings` репозитория. Для развития навыков инженера будет полезно лабораторные работы делать не в виртуальной машине, а в docker контейнере. Предложенный (не обязательный) вариант - в каталоге `postgres-bookings` репозитория.
Для дальнейшего закрепления материала - читаем книгу [PostgreSQL. Основы языка SQL](https://postgrespro.ru/education/books/sqlprimer) Для дальнейшего закрепления материала - читаем книгу [PostgreSQL. Основы языка SQL](https://postgrespro.ru/education/books/sqlprimer)
- Глава 8 - Индексы + ДЗ - Глава 8 - Индексы + ДЗ
- Глава 9 - Транзакции - Глава 9 - Транзакции
- Глава 10 - Повышение производительности + ДЗ - Глава 10 - Повышение производительности + ДЗ
Вопросы оптимизации запросов хорошо описаны в курсе [QPT](https://postgrespro.ru/education/courses/QPT) от Postgres Pro. Полученные навыки применимы для работы в том числе с GreenPlum, и частично, другими БД. Вопросы оптимизации запросов хорошо описаны в курсе [QPT](https://postgrespro.ru/education/courses/QPT) от Postgres Pro. Полученные навыки применимы для работы в том числе с Greenplum, и частично, другими БД.
На момент написания, видеолекции были доступны только для старой версии Postgres 13, но ее вполне достаточно. На момент написания, видеолекции были доступны только для старой версии Postgres 13, но ее вполне достаточно.
## Python #### Моделирование данных
2 курса по Python - простой и расширенный Понимание того, **как устроены данные и зачем они нужны**, — ключ к качественным ETL-процессам.
- ["Поколение Python": курс для начинающих – Stepik](https://stepik.org/course/58852/info) Мы кратко разбираем:
- ["Поколение Python": курс для продвинутых – Stepik](https://stepik.org/course/68343/info)
- Кратко(???) про ООП, менеджеры контекста - Основные подходы: нормализованные (3NF) vs денормализованные (звезда, снежинка)
- Что такое staging, marts, слои raw / clean / business
- Как проектировать таблицы под конкретные сценарии использования
Цель — не стать архитектором, а **уметь читать и объяснять структуру данных**, чтобы писать осмысленные запросы и трансформации.
Материалы (включая демо DWH-модель из этого репозитория):
- Мартин Клеппман — «Высоконагруженные приложения» - Глава 2: Модели данных и языки запросов. - Для понимания, чем реляционная модель (SQL) отличается от документной (NoSQL) и графовой, и почему для аналитики мы всё ещё любим таблицы
(Важно: не перепутайте главу 2 с Частью 2 про распределенные данные!).
- [Яндекс Практикум: что такое нормализация, простыми словами (для самых начинающих)](https://practicum.yandex.ru/blog/chto-takoe-normalizaciya-dannyh/)
- [Базы данных. 1,2,3 нормальные формы. - Youtube](https://www.youtube.com/watch?v=zwQzL80U51c)
- [Введение в структуру хранилища данных](dwh-modeling/README.md)
- Теория про Slowly Changing Dimensions: [SCD](dwh-modeling/SCD.md)
- (Опционально) Ральф Кимбалл — «Инструментарий хранения и анализа данных» (The Data Warehouse Toolkit) - первые 3 главы
- Практика по моделированию статусов клиента: [домашка STG → ODS → DDS → DM](dwh-modeling/Homework_Customer_Status_DDS_DM.md)
- Еще про Data Vault:
- Конспект и примеры из этого репозитория: [DataVault.md](dwh-modeling/DataVault.md)
- [DataVault за 10 минут - Youtube](https://www.youtube.com/watch?v=9oQs_wJ045I)
- Статья [«Что такое Data Vault: моделирование КХД для архитектора Big Data»](https://bigdataschool.ru/blog/what-is-data-vault/) — обзор, плюсы/минусы, контекст применения.
- [Гибкие методологии проектирования Data Vault и Anchor Modeling | Евгений Ермаков | karpov.courses](https://www.youtube.com/watch?v=fNGIOb8SJvU)
- Лекция в рамках курса по DWH: именно [«Основы Data Vault, создаем первую модель»](https://www.youtube.com/watch?v=65b99XCuiR4) — хороший формат: теория + пример.
- Доклад - практический пример: [Денис Лукьянов — Data Vault 2.0. Когда внедрять, проблемы применения при построении DWH на Greenplum](https://www.youtube.com/watch?v=oGwQbeP5iss)
- Хорошее общее введение в модели данных дано в статье и докладе от Yandex: [Как мы внедрили свою модель хранения данных — highly Normalized hybrid Model. Доклад Яндекса](https://habr.com/ru/companies/yandex/articles/557140/)
**Когда блок «Базы данных» считаем пройденным:**
- вы уверенно пишете запросы с JOIN, агрегатами, подзапросами и CTE;
- можете подробно объяснить план запроса в Postgres, понимаете где планировщик отработал корректно, а где - есть возможность улучшить;
- можете объяснить простую модель данных (3NF/звезда) и прочитать схему DWH;
- решаете типовые задачи уровня SQL live-coding без долгих пауз.
### Python
- Если совсем не знакомы с Python, начинаем с курса ["Поколение Python": курс для начинающих – Stepik](https://stepik.org/course/58852/info)
- Изучаем глубже и "оттачиваем" live coding: ["Поколение Python": курс для продвинутых – Stepik](https://stepik.org/course/68343/info)
- Продолжение "базы", спрашиваемой на собеседованиях, по Python: ["Поколение Python": курс для профессионалов](https://stepik.org/course/82541/promo). Курс очень полезный, но платный. Вместо него можно почитать "продвинутые" темы дальше.
- "Продвинутые" темы:
- [Полезные функции](https://pyneng.readthedocs.io/ru/latest/book/10_useful_functions/index.html)
- [Работа с файлами в формате CSV, JSON, YAML](https://pyneng.readthedocs.io/ru/latest/book/17_serialization/index.html)
- [Итераторы, итерируемые объекты и генераторы](https://pyneng.readthedocs.io/ru/latest/book/13_iterator_generator/index.html)
- [Декораторы Python: пошаговое руководство](https://habr.com/ru/companies/otus/articles/727590/)
- Работа с датой/временем: https://django.fun/docs/python/3.10/library/datetime/
- ООП
- [Tproger: «ООП простыми словами»](https://tproger.ru/experts/oop-in-simple-words)
- Введение в [ООП](https://metanit.com/python/tutorial/7.1.php)
- [Яндекс Учебник: «Объектная модель Python: классы, поля и методы»](https://education.yandex.ru/handbook/python/article/obuektnaya-model-python-klassy-polya-i-metody)
- [Real Python: OOP in Python (tutorial)](https://realpython.com/python3-object-oriented-programming/)
- Виртуальные окружения (venv) — изолируют зависимости проекта; настройте перед установкой Jupyter и библиотек
- [Habr: как настроить виртуальное окружение](https://habr.com/ru/articles/889670/)
- [SkillFactory: Виртуальные окружения в Python](https://blog.skillfactory.ru/venv-virtualnoe-okruzhenie-v-python/)
- Jupyter Lab
- [Блог Практикума: «Что такое Jupyter Notebook: как установить и открыть»](https://practicum.yandex.ru/blog/chto-takoe-jupyter-notebook/)
- Готовая реализация Jupyter Lab, включающая в себя Spark, в Docker: https://github.com/dementev-dev/jupyter-spark-docker
- Pandas - Pandas
- Jupyter Lab - кратко - [GeeksforGeeks: “Why Pandas is Used in Python”](https://www.geeksforgeeks.org/pandas/why-pandas-is-used-in-python/)
- [Skillbox: «Для чего нужна библиотека Pandas»](https://skillbox.ru/media/code/rabotaem-s-pandas-osnovnye-ponyatiya-i-realnye-dannye/)
- [Official: “10 minutes to pandas”](https://pandas.pydata.org/docs/user_guide/10min.html)
- [Хабр (RUVDS): «Моя шпаргалка по pandas»](https://habr.com/ru/companies/ruvds/articles/494720/)
- [Tproger: «Наглядная шпаргалка по операциям с DataFrame»](https://tproger.ru/articles/pandas-data-wrangling-cheatsheet)
Полезно, но дороговато и не обязательно: хорошее комбо SQL + Python: ["Поколение Python": профи + ООП + SQL Stepik](https://stepik.org/course/233341/promo?search=7181036958) Полезно, но дороговато и не обязательно: хорошее комбо SQL + Python ["Поколение Python": профи + ООП + SQL Stepik](https://stepik.org/course/233341/promo?search=7181036958)
Цель - LiveCoding простых задач Python, далее нужно будет для создания DAG Airflow Цель — уверенно решать простые задачи на Python в формате live-coding; дальше эти навыки пригодятся для создания DAG Airflow.
## Технические навыки **Когда блок Python считаем пройденным:**
### Запись встреч
OBS Studio - вы без подсказок пишете небольшие скрипты с циклами, функциями, обработкой ошибок и работой с коллекциями;
[Настройка записи экрана](https://docs.google.com/document/d/1qd8uRYlAaZp9c5zpvCVBOvYQCEukGHI9PEPjnjahI1k/) - умеете читать и модифицировать чужой код, в том числе с использованием pandas и DataFrame;
- уверенно проходите простой live-coding по Python для DE: прочитать CSV/JSON, отфильтровать, сгруппировать данные и посчитать агрегаты.
- можете отвечать как на простые вопросы собеседований (циклы, списки, словари), так и продвинутые (итераторы, декораторы, управление памятью, базовые понятия ООП)
### Методологии разработки
> **Зачем это разработчику?**
> 1. **Работа в команде.** Вам нужно понимать «правила игры». Почему задачи двигаются именно так? Зачем мы встречаемся каждое утро на 15 минут? Почему нельзя просто взять задачу из середины списка?
> 2. **Собеседование и «легенда».** Когда вас спросят: «Как строилась работа в вашей прошлой команде?», вы должны ответить грамотно. Использование правильной терминологии (спринты, груминг, ретроспектива, WIP-лимиты) — это маркер профессионализма. Это показывает, что вы не просто писали код в вакууме, а были частью налаженного процесса.
#### 1. Основы и сравнение подходов
Для начала нужно понять глобальную разницу между жестким планированием (Waterfall) и гибкой разработкой (Agile).
- [Waterfall или Agile, Scrum или Kanban: что выбрать](https://habr.com/ru/companies/kaiten/articles/906006/) — *Базовая статья. Читать внимательно, закрывает вопросы и по Waterfall, и по выбору пути.*
- [Agile, Scrum, Kanban - обзор, отличия, мифы](https://www.youtube.com/watch?v=LhA5aWjHTJw) — *Видео для закрепления. Помогает разложить кашу в голове по полочкам.*
#### 2. Agile: Философия гибкости
Agile — это не метод, а философия. Scrum и Kanban — это инструменты этой философии.
- [Scrum vs Kanban: отличия и разница Agile методов](https://kaiten.ru/blog/kanban-vs-scrum/) — *Сравнение двух главных фреймворков. Важно понять, где заканчивается один и начинается другой.*
#### 3. Scrum (Скрам)
Используется, когда мы создаем продукт и работаем спринтами (циклами).
*Часто встречается в продуктовых командах, где DE работает в связке с Backend/Frontend.*
- [Методология Scrum: принципы, ценности, этапы](https://kaiten.ru/blog/chto-takoie-scrum-i-kak-ispolzovat/)
#### 4. Kanban (Канбан)
Используется для управления потоком задач и поддержки.
*Наиболее популярен в Data Engineering и DevOps, так как данные поступают непрерывно, и их сложно «запереть» в двухнедельный спринт.*
- [Канбан: метод, инструменты и принципы](https://kaiten.ru/blog/cto-takoe-kanban/)
#### Практика: Как это выглядит в жизни
Теория — это хорошо, но на работе вы увидите конкретный интерфейс (Jira или Yandex Tracker). Важно понимать, куда нажимать и как двигать задачи.
##### 1. Jira (Мировой стандарт)
Самый популярный инструмент. Скорее всего, вы столкнетесь именно с ним.
* [Как работать с Jira на реальных проектах](https://www.youtube.com/watch?v=oPgm-fsHVfM) (15 мин) — *Отличное видео, где показывают базу: как создать задачу, как перетащить её по доске (Kanban) и что писать в комментариях. Смотреть с 04:00, где начинается практика.*
* [Создание и настройка Scrum-досок в JIRA](https://www.youtube.com/watch?v=u-u8NRyApUs) — *Если хотите увидеть, как выглядит Спринт и Бэклог изнутри.*
##### 2. Yandex Tracker (Российский стандарт)
Активно внедряется в крупных компаниях РФ. Логика та же, но интерфейс другой.
* [Начало работы в Яндекс.Трекере](https://www.youtube.com/watch?v=pdlYiijjn70) (3 мин) — *Супер-короткий официальный гайд. За 3 минуты показывают всё: очереди, доски, карточки.*
* [Настройка процесса разработки в Tracker](https://www.youtube.com/watch?v=EdKlYJR2ph0&t=397s)** (c 06:37) — *Более глубокий разбор: как выглядит очередь задач разработчика и жизненный цикл тикета.*
> **💡 Совет:**
> Не бойтесь кнопок. Главное правило любого трекера: **«Взял задачу в работу — переведи статус в In Progress»**. Это сигнал команде, что вы заняты и вас лучше не отвлекать.
**Когда блок «Методологии разработки» считаем пройденным:**
- вы в общих чертах можете объяснить разницу Waterfall vs Agile и Scrum vs Kanban;
- знаете основные мероприятия Scrum (planning / daily / review / retro) и что от вас ожидается на каждом;
- умеете работать с трекером (Jira/Tracker): создать/уточнить задачу, взять в работу, корректно двигать статусы и оставлять понятные комментарии;
- умеете своевременно сообщать о блокерах и уточнять требования, если задача «не бьётся» или в ней не хватает входных данных.
### Технические навыки
#### Продвинутый Git
- Сжатый, но емкий видеогайд: [GIT, GitHub, GitLab. Полный АКТУАЛЬНЫЙ гайд ЗА ПОЛТОРА ЧАСА. Без этого выгонят с работы - Youtube](https://www.youtube.com/watch?v=0Y-fneoUIO8)
- Книга: [Pro Git](https://git-scm.com/book/ru/v2) - главы
- 2 Основы Git
- 3 Ветвление в Git
- 5 Распределённый Git
- 6 GitHub
- [Курс работы с Git и GitLab - ЭФКО ЦПР | YouTube плейлист](https://www.youtube.com/playlist?list=PLbf8m52BvqlFlblJqQKPuEU26pwgqe7zK). Настоятельно рекомендую проделать за лектором все те действия что он показывает.
### Git
Книга: [Pro Git](https://git-scm.com/book/ru/v2) - указать главы для чтения
[Курс работы с Git и GitLab - ЭФКО ЦПР | YouTube плейлист](https://www.youtube.com/playlist?list=PLbf8m52BvqlFlblJqQKPuEU26pwgqe7zK) - указать номера лекций для просмотра и повторения за лектором.
Целевой уровень знания - понимание процесса GitFlow. Как создать ветку, влить изменения в другие ветки. Понимание, зачем. Целевой уровень знания - понимание процесса GitFlow. Как создать ветку, влить изменения в другие ветки. Понимание, зачем.
На собесах обычно не спрашивают, но нужно в работе. На собесах обычно не спрашивают, но нужно в работе.
### Docker #### Docker
- Postgres (развертывание, допиливание, запекание в него учебной БД, выгрузка на docker hub)
- Основа для будущих домашних работ - сборка стендов. Хранение в Git и проверка ментором.
### Методы разработки (водопад, scrum, kanban) - Курс https://karpov.courses/docker
Найти краткие обзоры методов разработки. Потом проговорить на занятии, когда что используется
## Airflow Основное предназначение для нас - учебные стенды, где мы разбираем и тренируемся с разными технологиями. На работе - иногда пригождается. На собесах спрашивают редко.
Один из основных инструментов.
Упрощенный docker compose: https://github.com/LexxaRRioo/rzv_de_shared_folder/tree/main/docker_compose
Найти объяснение для менти...
## Курсовая работа #### Запись встреч
### Стенд в Docker Compose OBS Studio
- Apache Airflow
- Источник данных - TelecomX - Руководство по OBS: [OBS Studio - Настройка ОБС для Записи Игр и Стрима | Настройка Микрофона в Обс и т.д - Youtube](https://www.youtube.com/watch?v=bj8VEphZ65U)
- Postgres - [Как записывать собеседования](https://docs.google.com/document/d/1qd8uRYlAaZp9c5zpvCVBOvYQCEukGHI9PEPjnjahI1k/)
- ETL
- Исходные коды всего - в Git **Когда блок технических навыков считаем пройденным:**
- вы понимаете базовый GitFlow: как организована работа с ветками в команде и как ваши коммиты попадают в прод;
- используете Docker для учебных стендов: запускаете контейнеры, смотрите логи и при необходимости перезапускаете сервисы;
- при необходимости умеете настроить запись экрана/созвонов, чтобы сохранять материалы обучения.
---
## Практика и инструменты
[[к оглавлению]](#оглавление)
### Airflow
Apache Airflow — инструмент для оркестрации ETL-процессов.
Мы используем его для:
- планирования задач,
- отслеживания зависимостей между шагами,
- визуализации статуса выполнения.
Материалы:
- [Учебник по Airflow](https://github.com/dementev-dev/airflow-manual)
**Когда блок Airflow считаем пройденным:**
- вы можете объяснить, что такое DAG, задачи, операторы и сенсоры, и как между ними задаются зависимости;
- на базе учебного стенда подготавливаете, отлаживаете и запускаете свои DAG'и с расписанием и несколькими шагами (например, загрузка данных и последующие трансформации);
- уверенно смотрите логи, находите место падения и понимаете, как перезапустить задачу.
### Greenplum
Разбираем, чем Greenplum отличается от PostgreSQL и зачем нужны MPP-хранилища.
#### Фундаментальная теория
Прежде чем нажимать кнопки, нужно понять "физику" больших данных. Почему обычный Postgres начинает тормозить?
- Мартин Клеппман, "Высоконагруженные приложения":
- Глава 1. Надежность, масштабируемость. (Разбираемся, чем вертикальное масштабирование отличается от горизонтального).
- Глава 3 (только конец главы). Читаем разделы:
- «Обработка транзакций или аналитика?» (OLTP or OLAP?) — ключевое различие нагрузок.
- «Хранение по столбцам» — почему аналитика требует другого способа записи данных на диск.
- Зачем: Это объясняет, почему Greenplum устроен именно так. Без этого вы будете пытаться работать с ним как с обычным Postgres.
#### Знакомство с Greenplum
Теперь, понимая теорию, смотрим, как это реализовано в конкретном инструменте.
- Простое введение: [Greenplum | Что это такое и как оно работает? - Youtube](https://www.youtube.com/watch?v=rLG9Z_HcKPY)
- [Визуализатор распределения Greenplum](https://gpskew.rzvde.pro/)
**Курс Yandex по Greenplum** — основной учебный курс, рекомендуется пройти целиком:
- [Бесплатный курс Yandex Cloud по Greenplum](https://yandex.cloud/ru/training/greenplum)
- Практику по курсу удобно делать на стенде [airflow-dwh-gp-lab](https://github.com/dementev-dev/airflow-greenplum) — `make up` поднимает рабочий Greenplum с PXF, не нужен облачный кластер.
- Стенд покрывает основные темы курса: типы таблиц (heap / appendonly), политики дистрибуции, сжатие, PXF, анализ планов выполнения (`EXPLAIN`).
- Единственное ограничение: cloud-специфичные темы (тема 2 курса — развёртывание в Yandex Cloud) на локальном стенде не покрыты.
Дополнительно:
- [Учебный курс по Greenplum от datafinder](https://datafinder.ru/products/uchebnyy-kurs-po-greenplum) — отдельные главы для углубления.
**Когда блок Greenplum считаем пройденным:**
- вы понимаете, как данные распределяются по сегментам, что такое skew и как его увидеть;
- на базе стенда `airflow-dwh-gp-lab` можете загружать и выгружать данные в Greenplum, выполнять запросы и разбирать планы выполнения (`EXPLAIN`);
- можете объяснить, в чём практическая разница между MPP-хранилищем и одиночным Postgres на уровне типичных задач DE и собеседований.
### Курсовая работа
Курсовая работа — важный майлстоун роадмапа: ваш первый end-to-end data-проект. После неё у вас есть ключевые технические навыки для старта карьеры в Data Engineering.
Курсовая выполняется на том же стенде [airflow-dwh-gp-lab](https://github.com/dementev-dev/airflow-greenplum), который вы уже использовали для практики по Greenplum.
**Что внутри:**
- Стенд содержит DWH с реализованным эталонным срезом (STG → ODS → DDS → DM) — это ваш образец для подражания.
- Задача — довести DWH до полного, реализовав недостающие загрузки по аналогии с эталоном.
- Есть готовый план от аналитика (ТЗ с маппингами и бизнес-правилами) — не нужно придумывать, что делать.
- Встроенная автоматическая проверка реализации поможет убедиться в корректности до проверки ментором.
- Ветка `main` — рабочая (с заготовками для реализации), ветка `solution` — эталон для сверки.
**Когда блок курсовой работы считаем пройденным:**
- все загрузки реализованы, DWH заполняется полностью (STG → ODS → DDS → DM);
- автоматическая проверка (валидационный DAG) проходит без ошибок;
- вы можете на собеседовании за 5–10 минут рассказать архитектуру проекта, его цели и показать ключевые части кода.
### Понятие сложности алгоритмов
В Data Engineering редко требуется писать сложные алгоритмы, но важно понимать, как оценивать эффективность кода:
- в SQL — через объём сканируемых данных, типы JOIN’ов, использование индексов;
- в Python — через асимптотику операций с pandas/списками (например, O(n) vs O(n²)).
Это помогает избегать «тормозящих» решений на собеседованиях и в реальных пайплайнах.
---
## Карьера и менторство
[[к оглавлению]](#оглавление)
### Менторство по этому роадмапу
Если вы нашли этот роадмап в интернете и хотите пройти его не в одиночку, а с поддержкой ментора, можно присоединиться ко мне.
**Что даёт менторство:**
- структурный план прохождения роадмапа под вашу ситуацию;
- разбор вопросов по SQL / DWH / Airflow и другим темам из этого документа;
- разбор домашних заданий и код-ревью;
- помощь с подготовкой к собеседованиям (резюме, мок-интервью).
**Как записаться**
Просто напишите мне в Telegram: [@dementev_dev](https://t.me/dementev_dev)
со словами «Хочу пройти роадмап с ментором» — дальше всё обсудим.
### Подготовка к собеседованиям
Цель блока — сформировать «опыт от 2 лет» и уметь корректно его показать в резюме и на собеседовании.
#### Помощь в подготовке резюме
## Понятие сложности алгоритмов
### SQL
### Python
## Подготовка к собеседованиям
Думаем, как "сделать" опыт, от 2 лет
### Помощь в подготовке резюме
- Видео от ОМ по составлению резюме - Видео от ОМ по составлению резюме
- [Как накрутить опыт в резюме | «Ультимативный гайд» @digital_ninja](https://www.youtube.com/watch?v=EPuogJuYsvY) - [Как накрутить опыт в резюме | «Ультимативный гайд» @digital_ninja](https://www.youtube.com/watch?v=EPuogJuYsvY)
- [Как писать резюме, чтобы его читали - доклад - Boosty](https://boosty.to/m0rtymerr/posts/71b02a6b-8116-466a-b945-b2ed793abd8f) - [Как писать резюме, чтобы его читали - доклад - Boosty](https://boosty.to/m0rtymerr/posts/71b02a6b-8116-466a-b945-b2ed793abd8f)
- [Как грамотно продать себя на собеседовании / Созвон сообщества - Boosty](https://boosty.to/m0rtymerr/posts/7289cd23-60c6-4010-bb1c-a5b28dac399a) - [Как грамотно продать себя на собеседовании / Созвон сообщества - Boosty](https://boosty.to/m0rtymerr/posts/7289cd23-60c6-4010-bb1c-a5b28dac399a)
- Попытки менти написать, моя обратная связь - итеративно - Практика: совместная работа над резюме — ментор помогает переработать опыт, сформировать убедительную карьерную историю и подготовиться к вопросам по ней.
### Помощь с прохождением испытательного срока
#### Поиск работы и собеседования
- [Как подтвердить опыт без трудовой / Хабр против работяг](https://www.youtube.com/watch?v=GHqABzA1zi8)
- Видео по прохождению собеседований из сообщества ОМ — *в подготовке*
- Практика: мок-собеседования с ментором — тренировка ответов, разбор слабых мест, психологическая подготовка к реальным интервью.
#### Помощь с прохождением испытательного срока
- [Как успешно пройти испытательный срок в IT | «Ультимативный гайд» c @digital_ninja - Youtube](https://www.youtube.com/watch?v=r1lWP5rYVdk) - [Как успешно пройти испытательный срок в IT | «Ультимативный гайд» c @digital_ninja - Youtube](https://www.youtube.com/watch?v=r1lWP5rYVdk)
- [Испытательный срок - доклад - Boosty](https://boosty.to/m0rtymerr/posts/40e7f17e-022b-495c-8d03-dabbe4383b8e) - [Испытательный срок - доклад - Boosty](https://boosty.to/m0rtymerr/posts/40e7f17e-022b-495c-8d03-dabbe4383b8e)
Видео по прохождению собесов от ОМ. Мои комментарии к нему, мой опыт
Первые тренировки мок собесы, обратная связь
Навыки поиска работы с HH и Habr карьера **Когда блок подготовки к собеседованиям считаем пройденным:**
[Как накрутить опыт в резюме | «Ультимативный гайд» ‪@digital_ninja](https://www.youtube.com/watch?v=EPuogJuYsvY) - у вас есть актуальное резюме под DE с понятными примерами проектов вместо «пустого» опыта;
[Как подтвердить опыт без трудовой / Хабр против работяг](https://www.youtube.com/watch?v=GHqABzA1zi8) - вы умеете искать и отбирать вакансии на HH и Habr Карьера, адаптируя отклики под конкретную позицию;
[Как успешно пройти испытательный срок в IT | «Ультимативный гайд» c @digital_ninja - Youtube](https://www.youtube.com/watch?v=r1lWP5rYVdk) - вы прошли хотя бы пару мок-собеседований, получили обратную связь и по результатам доработали резюме и стратегию поиска.
# Расширенные навыки ---
Поясняю, что главное - уметь пользоваться и отвечать на вопросы собесов. Уметь самому разворачивать сложные конфигурации - излишне, для этого в компаниях обычно есть DevOps и DBA. Достаточно прочувствовать на простом docker стенде.
## Greenplum
Дать теоретический материал - разница с Postgres.
Предварительно: [Учебный курс по Greenplum](https://datafinder.ru/products/uchebnyy-kurs-po-greenplum) - дать только отдельные главы
Контейнер с GreenPlum, несколько домашек по нему, чтобы прочувствовать работу распределенных запросов.
- [sergeyosechkin/greenplum Tags | Docker Hub](https://hub.docker.com/r/sergeyosechkin/greenplum/tags)
- [Как собрать Docker-образ Greengage DB | Greengage DB Docs](https://greengagedb.org/ru/docs-gg/current/use_docker.html)
В сложности с виртуалками - только если менти сильно захочет. Не буду рекомендовать.
## ClickHouse ## Расширенные навыки
Эти темы выходят за рамки базового минимума для старта в Data Engineering, но дают более полное представление об экосистеме.
Их цель — понимать, зачем и когда используется тот или иной инструмент, а не осваивать его на уровне администратора или DevOps-инженера.
[[к оглавлению]](#оглавление)
Мы кратко знакомимся с:
- **Streaming** (NiFi + Kafka) — инструментами для построения потоковых и интеграционных пайплайнов;
- **ClickHouse** — колоночной СУБД для высоконагруженной аналитики;
- **Lakehouse** (Spark, Iceberg, Trino) — архитектурой, построенной на разделении compute и storage;
- **dbt** — подходом к трансформации данных как кода.
Практика ограничивается минимальным рабочим примером (запуск в Docker, простой пайплайн или SQL-модель).
Этого достаточно, чтобы уверенно говорить об инструменте на собеседовании и понимать его место в архитектуре — а всё остальное при необходимости осваивается уже на проекте.
### ClickHouse
Бесплатный курс https://yandex.cloud/ru/training/clickhouse Бесплатный курс https://yandex.cloud/ru/training/clickhouse
Платный курс [ClickHouse для аналитика – Stepik](https://stepik.org/course/100210/promo?search=6551441002) Платный курс [ClickHouse для аналитика – Stepik](https://stepik.org/course/100210/promo?search=6551441002)
## NiFi ### Streaming (NiFi + Kafka)
Плейлист [Apache NiFi с нуля за 3 часа. Конструктор вместо кода - Youtube](https://youtube.com/playlist?list=PL4MpKy3QjNp_rOEEibc4Ro8UK4g8vLX6_&si=W_hidjHmBOZ_aUfS) - первые 4 видео. Дальше - по желанию. NiFi — визуальный конструктор потоков данных, Kafka — распределённая очередь сообщений. Вместе они закрывают типичный сценарий: принять данные, буферизовать, доставить в хранилище.
Делаем отдельный docker compose Postgres + Nifi
В NiFi собираем генератор данных
## Kafka Материалы:
[Лучший Гайд по Kafka для Начинающих За 1 Час - Youtube](https://www.youtube.com/watch?v=hbseyn-CfXY)
Добавляем к предыдущему docker compose Kafka.
Строим поток данных NiFi->Kafka
Kafka->NiFi->Postgres
## dbt - [Apache NiFi с нуля за 3 часа (Youtube-плейлист)](https://youtube.com/playlist?list=PL4MpKy3QjNp_rOEEibc4Ro8UK4g8vLX6_&si=W_hidjHmBOZ_aUfS) — первые 4 видео, дальше — по желанию
Обзорная лекция, для понимания смысла - [Лучший Гайд по Kafka для Начинающих За 1 Час (Youtube)](https://www.youtube.com/watch?v=hbseyn-CfXY)
Практика — на стенде [nifi-kafka-postgres-lab](https://github.com/dementev-dev/nifi-kafka-postgres-lab) (Docker Compose с NiFi, Kafka и Postgres):
- настраиваем в NiFi простой генератор данных и поток в Postgres;
- строим поток NiFi → Kafka → NiFi → Postgres.
### Lakehouse (Spark, Iceberg, Trino)
> Секция в разработке. Lakehouse — отдельное направление в DE, построенное на разделении compute и storage, открытых табличных форматах (Iceberg, Delta) и движках распределённой обработки (Spark, Trino). Этот роадмап фокусируется на классическом DWH-стеке, поэтому полноценный блок пока не готов — ниже только отправные точки для самостоятельного изучения.
- [DataLearn: «Что такое Apache Spark»](https://youtu.be/Tl9YzC-dQLI) — введение в Spark с нуля, ~40 минут
- Стенд для экспериментов: [mini-lakehouse-lab](https://github.com/dementev-dev/mini-lakehouse-lab) (Spark + Iceberg + Trino + MinIO)
### dbt
dbt (data build tool) — инструмент для трансформации данных в хранилище.
Мы рассматриваем его как альтернативу «ручному» написанию сложных CTE и для понимания современного подхода к моделированию данных как кода.
Материалы:
- [dbt quickstart (официальный гайд)](https://docs.getdbt.com/guides/manual-install?step=1)
- [Перевод документации dbt на русский](https://docs.getdbt.tech/)
Практика:
- Клонировать [jaffle-shop](https://github.com/dbt-labs/jaffle-shop), установить dbt-duckdb, прогнать `dbt build`, `dbt test`, `dbt docs generate && dbt docs serve` — этого достаточно, чтобы увидеть весь цикл.
**Когда блок расширенных навыков считаем пройденным:**
- вы можете на собеседовании кратко объяснить, когда уместны Streaming (NiFi/Kafka), ClickHouse, Lakehouse-стек и dbt, и чем они дополняют базовый стек (Postgres, Airflow, Greenplum);
- понимаете типичные сценарии: потоковые интеграции и очереди (NiFi + Kafka), аналитические витрины и отчёты на ClickHouse, Lakehouse-архитектура (Spark/Iceberg/Trino), трансформации данных как код (dbt);
- не боитесь увидеть эти инструменты в описании вакансии и можете поддержать содержательный разговор об их месте в архитектуре.
---
## Софт скиллы
[[к оглавлению]](#оглавление)
# Софт скиллы
- [Все ветви дохода в IT / Полный гайд по деньгам](https://youtube.com/live/JHClTWwK1EM) - [Все ветви дохода в IT / Полный гайд по деньгам](https://youtube.com/live/JHClTWwK1EM)
- [Гайд как писать отзывы](https://boosty.to/m0rtymerr/posts/b04040ec-0f46-4524-9c75-188a513140ad?share=post_link) - [Гайд как писать отзывы](https://boosty.to/m0rtymerr/posts/b04040ec-0f46-4524-9c75-188a513140ad?share=post_link)
- [Гайд по Антистрессу](https://youtu.be/bu0YiXOKaoU) - [Гайд по Антистрессу](https://youtu.be/bu0YiXOKaoU)
# Тех. материалы несортировано ---
## Дополнительные материалы
[[к оглавлению]](#оглавление)
- [ananevsyu/SandBox_DB_public: Песочница для изучения различных технологий связанных с инженерией данных](https://gitflic.ru/project/ananevsyu/sandbox_db_public) - [ananevsyu/SandBox_DB_public: Песочница для изучения различных технологий связанных с инженерией данных](https://gitflic.ru/project/ananevsyu/sandbox_db_public)
- Клон проекта [dementev_dev/sandbox_db_public-форк](https://gitflic.ru/project/dementev_dev/sandbox_db_public-fork) - Клон проекта [dementev_dev/sandbox_db_public-форк](https://gitflic.ru/project/dementev_dev/sandbox_db_public-fork)
- [System Design. Разбор книги "Высоконагруженные приложения". Глава 1 - Youtube](https://www.youtube.com/watch?v=owjrIB_5go8) — отличный видео-конспект первой главы Клеппмана на русском.
- [Индексы в БД - Youtube](https://www.youtube.com/watch?v=DyqtBiDrz3g) - [Индексы в БД - Youtube](https://www.youtube.com/watch?v=DyqtBiDrz3g)
- [Spark + Iceberg in 1 Hour - Memory Tuning, Joins, Partition - Youtube](https://www.youtube.com/watch?v=3R-SLYK-P_0) - [Spark + Iceberg in 1 Hour - Memory Tuning, Joins, Partition - Youtube](https://www.youtube.com/watch?v=3R-SLYK-P_0)
- [Введение в устройство Parquet и Iceberg - habr](https://habr.com/ru/companies/vktech/articles/959398/)
- [Введение в Apache Iceberg. Основы, архитектура, как работает?](https://ivan-shamaev.ru/apache-iceberg-tutorial-architecture-how-to-work/#__Apache_Iceberg-2) - [Введение в Apache Iceberg. Основы, архитектура, как работает?](https://ivan-shamaev.ru/apache-iceberg-tutorial-architecture-how-to-work/#__Apache_Iceberg-2)
- [Алгоритмы: теория и практика. Методы – Stepik](https://stepik.org/course/217/info) - [Алгоритмы: теория и практика. Методы – Stepik](https://stepik.org/course/217/info)
- [Алгоритмы: теория и практика. Структуры данных – Stepik](https://stepik.org/course/1547/promo) - [Алгоритмы: теория и практика. Структуры данных – Stepik](https://stepik.org/course/1547/promo)
- [Apache Hadoop для самых маленьких: HDFS, RACK-AWARENESS, репликация и Data Locality - Youtube](https://youtu.be/0fsY5bW2l84) - [Apache Hadoop для самых маленьких: HDFS, RACK-AWARENESS, репликация и Data Locality - Youtube](https://youtu.be/0fsY5bW2l84)
- [Книга. Введение в Apache Kafka для системных аналитиков и проектировщиков интеграций](https://systems.education/kafka) - [Книга. Введение в Apache Kafka для системных аналитиков и проектировщиков интеграций](https://systems.education/kafka)
- - [Перевод документации dbt на русский язык](https://docs.getdbt.tech/)
### Записи ОМ
## Записи ОМ
- [Как пройти собеседование на программиста | Ультимативный гайд с ‪@om_nazarov - Youtube](https://www.youtube.com/watch?v=tzSdiYZ52kI) - [Как пройти собеседование на программиста | Ультимативный гайд с ‪@om_nazarov - Youtube](https://www.youtube.com/watch?v=tzSdiYZ52kI)
- [Как стать программистом в 2025 | «Ультимативный гайд» с ‪@om_nazarov](https://www.youtube.com/watch?v=6151ekTOl38) - [Как стать программистом в 2025 | «Ультимативный гайд» с ‪@om_nazarov](https://www.youtube.com/watch?v=6151ekTOl38)
-
+14
View File
@@ -0,0 +1,14 @@
# Разработка с ИИ
Практические материалы о работе с ИИ-ассистентами для разработчиков: как эффективно взаимодействовать с coding agents, организовать контекст и память, выстроить рабочий процесс.
Раздел не привязан к основному роадмапу по Data Engineering и может использоваться независимо.
## Материалы
- [Лучшие практики работы с coding agents](best-practice.md): 10 принципов эффективной работы с ИИ-ассистентами, от структурирования задач до автоматизации
- [Механизм памяти coding agents](memory-mechanism.md): как устроена память агентов, типы памяти, многоуровневая организация и практические рекомендации
## Источники
Материалы раздела подготовлены на основе документации [Z.AI DevPack](https://docs.z.ai/devpack/resources/best-practice).
+234
View File
@@ -0,0 +1,234 @@
# Лучшие практики работы с coding agents
> По мотивам [Best Practice](https://docs.z.ai/devpack/resources/best-practice) (Z.AI DevPack)
По мере развития фундаментальных моделей ИИ-инструменты для разработки эволюционируют от простых ассистентов автодополнения кода в **coding agents**, способных участвовать в полном цикле разработки ПО. В отличие от традиционных copilot-инструментов, coding agents умеют читать и навигировать по кодовой базе, модифицировать файлы, выполнять команды, вызывать внешние инструменты и решать сложные задачи через многошаговое взаимодействие.
С этим сдвигом разработчикам нужно больше, чем техники написания промптов. Нужен надёжный подход к работе с coding agents на практике. Среди ведущих инструментов формируется общий паттерн использования: предоставить чёткий контекст задачи, спланировать шаги выполнения, зафиксировать проектные правила, подключить внешние инструменты и системы, автоматизировать повторяющиеся процессы.
Опираясь на официальные рекомендации этих инструментов, статья описывает **общий фреймворк лучших практик для coding agents**.
## 1. Относитесь к агенту как к коллеге, а не к одноразовому инструменту
Типичная ошибка при работе с coding agent — использовать его как одноразовый вопрос-ответ:
> Задать вопрос, получить код, завершить взаимодействие.
На практике этот подход не раскрывает возможности агента.
Coding agent лучше воспринимать как настраиваемого коллегу, которого можно совершенствовать со временем. Через конфигурационные файлы проекта, интеграции с инструментами и переиспользуемые навыки (skills) разработчик может постоянно формировать поведение агента так, чтобы оно соответствовало рабочему процессу команды.
!!! tip "Ключевая мысль"
Ценность coding agent определяется не только возможностями модели. Она складывается из возможностей модели **и** рабочего процесса вокруг неё.
## 2. Структурируйте входные данные задачи: контекст важнее промпт-инженерии
При работе с coding agent многие разработчики слишком фокусируются на технике написания промптов и недостаточно на том, что важнее: **контексте задачи**.
В сложной кодовой базе эффективное описание задачи обычно включает четыре элемента:
- **Цель.** Чётко опишите, что нужно сделать: исправить баг, реализовать эндпоинт, отрефакторить модуль
- **Контекст.** Укажите релевантные файлы, сообщения об ошибках, документацию или примеры. Назовите конкретные файлы, функции или модули
- **Ограничения.** Перечислите инженерные требования: стандарты кодирования, архитектурные правила, требования безопасности, ограничения зависимостей
- **Критерии завершения.** Определите, как оценивать готовность: тесты проходят, поведение изменилось ожидаемым образом, баг больше не воспроизводится
Такой структурированный ввод снижает лишние догадки и делает изменения агента более последовательными и легко проверяемыми.
В большинстве coding agents контекст можно предоставить, указав на файлы, приложив фрагменты кода или явно описав детали в промпте. Когда контекст задан, следующий шаг для сложной работы: планирование перед внесением изменений.
## 3. Планируйте перед выполнением сложных задач
Когда задача имеет чёткий контекст, следующая проблема: выполнение. Для сложных запросов coding agents наиболее эффективны, когда они **планируют перед действием**.
Если попросить агента сразу писать код при сложном запросе, это часто приводит к логическим ошибкам, ненужной переработке или повторным правкам. Более эффективный подход: **сначала план, потом реализация**.
Фаза планирования обычно включает:
- Анализ кодовой базы
- Определение объёма изменений
- Подтверждение подхода к реализации до начала правок
Например, Claude Code поощряет шаг анализа и планирования для сложных задач. Некоторые coding agents также предоставляют выделенный режим планирования, который генерирует полный план выполнения перед реализацией.
Это сдвигает агента от простой генерации кода по запросу к выполнению работы пошагово по явному плану.
## 4. Фиксируйте повторяющиеся правила в конфигурационных файлах проекта
На практике многие промпты повторяют одни и те же проектные правила:
- структура директорий проекта
- команды сборки
- процесс тестирования
- стандарты кодирования
- процесс подачи PR
Если эти правила повторяются в каждом промпте, рабочий процесс становится неэффективным, а инструкции со временем начинают расходиться.
Поэтому большинство coding agents позволяют хранить **долгоживущие проектные правила** в конфигурационных файлах проекта, чтобы агент автоматически загружал нужный контекст при выполнении задач.
В одних инструментах это файлы-инструкции для агента, описывающие структуру репозитория, способ запуска проекта и принятые конвенции. В других та же информация фиксируется через конфигурационные файлы, скрипты или настройки проекта.
Независимо от реализации, цель одна: перенести информацию, которую иначе пришлось бы повторять в диалоге, в **стабильный проектный контекст**.
!!! success "Практическое правило"
**Временные инструкции пишите в промпте, а долгоживущие правила фиксируйте в конфигурационных файлах проекта.**
## 5. Среда выполнения определяет возможности агента
Работая с coding agents, разработчики часто объясняют непоследовательные результаты возможностями модели. На практике многие из этих проблем вызваны неполной или неправильно настроенной **средой выполнения**.
В отличие от традиционных инструментов автодополнения, coding agents обычно работают в реальной среде разработки и выполняют задачи:
- чтение и модификация исходных файлов
- запуск команд сборки или тестирования
- вызов внешних инструментов или API
- взаимодействие с системами контроля версий
Поведение агента зависит не только от возможностей модели, но и от того, **насколько среда выполнения полна, стабильна и доступна**. При неправильной конфигурации агент может столкнуться с проблемами:
!!! warning "Типичные проблемы среды"
- Невозможность найти нужную директорию проекта
- Отсутствие прав на чтение или модификацию критичных файлов
- Невозможность запустить команды сборки или тестирования
- Отсутствие доступа к внешним инструментам или сервисам
Эти проблемы часто выглядят как непонимание со стороны модели или низкое качество кода, но реальная причина обычно в том, что у агента недостаточно прав выполнения или доступа к нужному контексту.
Большинство ведущих coding agents предоставляют настройки среды:
- выбор модели или уровня рассуждений
- управление правами доступа к файлам и политиками песочницы
- определение разрешённых команд
- настройка подключений к внешним инструментам или сервисам
!!! success "Три типа контекста"
Coding agent зависит от трёх типов контекста:
- **Контекст задачи**: промпт и входные данные текущей задачи
- **Контекст проекта**: структура репозитория и инженерные правила
- **Контекст среды**: инструменты, права доступа и среда выполнения
Из них контекст среды определяет **что агент может делать и как далеко зайти**.
## 6. Вовлекайте агента в полный цикл разработки
Когда у coding agent есть правильная среда выполнения, следующий шаг: вовлечь его в полный цикл разработки, а не использовать только для генерации кода. В реальной разработке изменение кода оценивается не только по генерации. Оно должно пройти тесты, соответствовать инженерным стандартам и пройти ревью.
Типичный цикл разработки с агентом включает шаги:
1. **Реализация изменений.** Модификация существующего кода или добавление нового по требованиям задачи
2. **Написание или обновление тестов.** Добавление тестового покрытия для новой функциональности или исправляемого бага
3. **Запуск тестов.** Выполнение модульных или интеграционных тестов для проверки ожидаемого поведения
4. **Проверка кода.** Запуск линтеров, форматирования или проверки типов для соответствия стандартам
5. **Ревью изменений.** Инспекция диффа для выявления потенциальных проблем, рисков регрессии или нежелательных модификаций
В этом рабочем процессе coding agent перестаёт быть просто генератором кода. Он становится активным участником **реализации, валидации и ревью**.
!!! success "Смена роли"
С точки зрения рабочего процесса, coding agent трансформируется из традиционного **генератора кода** в **узел выполнения внутри цикла разработки**.
## 7. Расширяйте контекст агента через MCP
В реальных рабочих процессах информация, необходимая coding agent, не всегда находится в репозитории. Многие данные, влияющие на решения при реализации, распределены по внешним системам:
- системы трекинга задач и требований
- статус и результаты CI/CD
- схемы баз данных или продуктовые данные
- документация API и ссылки на внешние сервисы
Если эту информацию приходится копировать и вставлять вручную каждый раз, процесс становится неэффективным, а контекст, передаваемый агенту, фрагментирован и ненадёжен.
Поэтому многие coding agents поддерживают **Model Context Protocol (MCP)**, который предоставляет стандартный способ подключения внешних инструментов и систем. Через MCP coding agent может получать доступ к:
- платформам хостинга и совместной работы с кодом
- базам данных и интерфейсам запросов
- API-сервисам и технической документации
- внутренним инструментам и системам автоматизации
!!! success "Расширение границ"
Когда агент может работать только с информацией из промпта, он обычно ограничен локальными задачами. Подключение к внешним системам позволяет ему участвовать в более полных рабочих процессах: читать контекст задач, исследовать упавшие CI-запуски, проверять определения API, анализировать проблемы по схемам баз данных.
Агент эволюционирует из **исполнителя уровня репозитория** в **узел взаимодействия внутри реальной инженерной среды**.
## 8. Оформляйте повторяющиеся процессы как Skills
Со временем команды обнаруживают, что определённые задачи возникают снова и снова:
- ревью PR
- анализ логов
- генерация release notes
- стандартные отладочные процессы
Если описывать эти задачи вручную в промпте каждый раз, результат: ненужное повторение и менее стабильные результаты.
Поэтому многие системы coding agents предоставляют механизм **Skills**: упаковку типовых процессов в переиспользуемые шаблоны.
На высоком уровне Skill можно понимать как **структурированный шаблон рабочего процесса**. Он абстрагирует логику выполнения, которая иначе была бы разбросана по промптам, и позволяет агенту применять один и тот же процесс последовательно при обработке похожих задач.
Разные инструменты реализуют Skills по-разному: через выделенные файлы, конфигурацию или скрипты. Но цель одна: **превратить разовые промпты в переиспользуемые рабочие процессы**.
На практике работает простое правило:
> **Если паттерн промпта или поток задач используется повторно, он, вероятно, должен быть оформлен как Skill.**
## 9. Автоматизируйте стабильные процессы
Когда Skill можно выполнить надёжно, следующий шаг: автоматизация.
В долгоживущих рабочих процессах разработки многие задачи повторяются или привязаны ко времени:
- генерация резюме коммитов по расписанию
- автоматическое расследование упавших CI-запусков
- сканирование на потенциальные баги или аномальные логи
- подготовка ежедневных или еженедельных инженерных отчётов
Даже если эти задачи уже оформлены как Skills, они по-прежнему создают ручную работу, если разработчикам приходится запускать их каждый раз.
!!! info "Автоматизация как следующий слой"
Автоматизация находится уровнем выше Skills. Skill определяет **как** выполняется рабочий процесс, а автоматизация определяет **когда** он запускается и **как** продолжает работать со временем.
Например, навык генерации release notes можно настроить на запуск:
- при каждой новой публикации релиза
- раз в неделю для подготовки сводки релизов
- автоматически после завершения CI
Это сдвигает coding agent из **интерактивного инструмента** в **непрерывного ассистента разработки**.
## 10. Управляйте сессиями осознанно
При работе с coding agents сессия — это больше, чем история чата. На практике она функционирует как **рабочий контекст**, который накапливает контекст, промежуточные рассуждения и результаты выполнения.
По мере продвижения задачи агент постепенно наращивает информацию в рамках той же сессии:
- цель задачи
- релевантный контекст кода
- уже внесённые изменения
- промежуточные рассуждения и решения
Если сессиями не управлять, несвязанные задачи накапливаются в одной сессии, делая контекст излишне сложным. Это часто снижает качество рассуждений и выполнения агента.
Общие практики:
- **Используйте отдельную сессию для каждой задачи.** Не смешивайте несвязанные задачи, чтобы рабочий контекст оставался ясным
- **Избегайте слишком длинных сессий.** Когда сессия накапливает слишком много истории, используйте резюме или сжатие для снижения нагрузки на контекст
- **Начинайте новую сессию для ответвлений.** Если задача открывает новое направление исследования, продолжайте его в отдельной сессии
- **Периодически сжимайте исторический контекст.** Резюмируйте старые части разговора для снижения давления на контекстное окно
В более сложных сценариях команды могут использовать **модель многоагентного сотрудничества**: подзадачи (исследование кодовой базы, запуск тестов, расследование сбоев) делегируются отдельным агентам, а главный агент координирует общую задачу. Это сохраняет ясность основной сессии и повышает эффективность выполнения.
## Заключение
Эффективность coding agent определяется не только моделью. Она зависит от того, как разработчики выстраивают рабочий процесс вокруг неё.
Зрелый рабочий процесс с coding agent обычно включает следующие этапы:
1. Структурированный ввод задачи с контекстом
2. Планирование перед выполнением
3. Проектные правила в конфигурационных файлах
4. Настроенная среда выполнения
5. Участие в полном цикле разработки
6. Расширение контекста через MCP
7. Повторяющиеся процессы как Skills
8. Автоматизация стабильных процессов
9. Осознанное управление сессиями
+362
View File
@@ -0,0 +1,362 @@
# Механизм памяти coding agents
> По мотивам [Memory Mechanism](https://docs.z.ai/devpack/resources/memory-mechanism) (Z.AI DevPack)
Память позволяет coding agent сохранять контекст между задачами и сессиями, сокращая повторный ввод и повышая эффективность выполнения. С хорошо продуманной системой памяти агент может постоянно учитывать структуру проекта, инженерные конвенции и предпочтения пользователя, автоматически переиспользуя эту информацию в будущей работе.
В системах coding agents память обычно организована в несколько слоёв: **автоматическая память, проектная память** и **сессионная память**.
## Зачем coding agents нужна память?
Традиционные большие языковые модели не сохраняют состояние между вызовами. Они не могут запомнить контекст проекта между сессиями, накапливать опыт решения проблем или последовательно адаптироваться к предпочтениям пользователя.
Агентные системы решают это ограничение через **внешнюю память**.
Типичная архитектура выглядит так:
```
Ввод пользователя
Извлечение памяти
Сборка контекста
Рассуждение LLM
Действие / вызов инструмента
Обновление памяти
```
Агент извлекает релевантную память перед началом задачи и обновляет память после завершения.
Эта архитектура является общим паттерном в современных агентных системах, таких как LangGraph, AutoGPT и Devin.
## Полная архитектура памяти
На высоком уровне полная архитектура памяти агента выглядит так:
```
Краткосрочная память
Контекст сессии
Долгосрочная память
├ семантическая память
├ эпизодическая память
└ процедурная память
```
## Основные типы памяти
### Сессионная память
Сессионная память — это контекстная информация текущей задачи. Включает текущую историю разговора, последние результаты инструментов, текущий план выполнения и содержимое файлов в области видимости. Эта информация обычно находится в контекстном окне модели.
Пример:
```
Пользователь: Исправь этот баг в Python
Агент: Анализирует ошибку
Агент: Модифицирует код
Агент: Запускает тесты
```
Все эти шаги выполнения относятся к сессионной памяти.
### Проектная память
Проектная память хранит **долгоживущую информацию о всей кодовой базе**: архитектуру проекта, стандарты кодирования, процессы сборки, часто используемые команды. Такая память обычно записывается в `.md`-файлы и загружается в начале сессии.
Пример структуры:
```
your-project/
├── .claude/
│ ├── CLAUDE.md # Основные инструкции проекта
│ └── rules/
│ ├── code-style.md # Стиль кода
│ ├── testing.md # Конвенции тестирования
│ └── security.md # Требования безопасности
```
При такой структуре агент автоматически следует этим правилам при модификации кода.
### Семантическая память
Семантическая память хранит фактические знания и справочную информацию: документацию API, правила языков программирования, базы знаний проекта. На практике часто реализуется через RAG (Retrieval-Augmented Generation).
Типичный поток:
```
запрос
эмбеддинг
векторный поиск
извлечение документов
рассуждение LLM
```
Это один из наиболее распространённых методов запоминания в coding agents.
### Эпизодическая память
Эпизодическая память записывает прошлый опыт агента: шаги исправления предыдущего бага, корневую причину прошлого сбоя сборки, стратегию отладки, которая сработала. Этот тип памяти помогает агенту учиться на предыдущем опыте.
Пример:
```
Эпизод:
Сбой CI из-за отсутствующей зависимости
Решение: обновить pip-пакет
```
### Процедурная память
Процедурная память хранит стратегии или пошаговые процессы выполнения задач.
Пример:
```
Debug_Workflow.md
1. прочитать лог ошибок
2. найти файл
3. написать патч
4. запустить тесты
```
Такая память обычно используется в системных промптах, шаблонах рабочих процессов и политиках агента.
## Стандартный паттерн использования памяти
В реальных системах агенты обычно следуют единообразному процессу работы с памятью.
**Шаг 1: Извлечение памяти**
Перед началом задачи агент извлекает релевантную проектную память, записи из базы знаний и предыдущий опыт, затем внедряет их в рабочий контекст.
**Шаг 2: Сборка контекста**
Извлечённые воспоминания собираются в полный контекст и передаются модели.
**Шаг 3: Обновление памяти**
После завершения задачи агент решает, нужно ли записать новые воспоминания: обнаруженные проектные правила, опыт отладки или предпочтения пользователя.
## Как правильно использовать память
В основных агентных системах память проектируется как **многослойная, управляемая, извлекаемая и обновляемая**.
Обычно память делится на **краткосрочную** и **долгосрочную**. Краткосрочная используется для сохранения состояния в текущем потоке или сессии. Долгосрочная поддерживается через явные файлы, конфигурации правил, векторное извлечение или другие механизмы постоянного хранения.
Например, в **Claude Code** каждая сессия начинается с чистого контекстного окна. Знания переносятся между сессиями через файлы инструкций (CLAUDE.md) и **автоматическую память**. В **LangChain / LangGraph** память также делится на **краткосрочную в рамках потока** и **долгосрочную между сессиями**.
На практике наиболее эффективный подход: не полагаться на модель в автоматическом «запоминании всего», а установить чёткий паттерн управления памятью. Определить: что записывать в проектные файлы памяти, что извлекать из базы знаний или векторного хранилища, что оставить только в текущей сессии, а что продвинуть в долгосрочную память после завершения задачи.
### Разделяйте инструкционную и обучающую память
Один из наиболее практичных принципов: различать два фундаментально разных вида памяти.
- **Инструкционная память**: написана людьми, чтобы указать агенту, как он должен работать. Обычно включает стандарты кодирования, конвенции директорий, команды сборки, процедуры тестирования, требования к именованию, правила коммитов и правила безопасности на уровне команды. В Claude Code это файлы инструкций вроде `CLAUDE.md`
- **Обучающая память**: не определена заранее, а накоплена агентом из ваших поправок, предпочтений, неудачных попыток, частых команд и привычек проекта. В Claude Code это называется автоматическая память (auto memory)
Если эти два типа памяти смешиваются, поведение системы со временем дрейфует. Лучший подход: чётко разделить их роли.
- **Правила, политики и поведенческие ограничения** записывайте в **инструкционную память**, чтобы поведение агента оставалось стабильным и предсказуемым
- **Опыт, предпочтения пользователя, временные открытия и ретроспективные выводы** записывайте в **обучающую память**, чтобы решения улучшались в будущих задачах
Это разделение предотвращает постепенное загрязнение основных правил системы заметками из опыта.
### Многоуровневое управление памятью
#### Уровень организации
Правила, определённые и распространяемые на уровне команды или компании, применимые ко всем разработчикам и проектам:
- требования безопасности и соответствия
- базовые стандарты код-ревью
- запрещённые директории для чтения/записи
- ограничения зависимостей и лицензий
- инженерные стандарты организации
На организационном уровне общий файл правил развёртывается по системному пути и не должен легко отключаться пользователями. **Организационная память — это высокоприоритетный управленческий слой, который не должен обходиться.**
#### Уровень проекта
Командный контекст проекта, версионируемый и общий для всех участников. **Это самый важный слой памяти для coding agent.**
- документация архитектуры проекта
- конвенции структуры директорий
- команды сборки и тестирования
- где должны располагаться API
- конвенции именования
- типовые процессы разработки
Claude Code рекомендует хранить эту информацию в проектном файле, а команда `/init` может автоматически сгенерировать первоначальный черновик. Ключевое свойство этого слоя: **общий для проекта, под контролем версий, стабильный во времени**.
#### Уровень пользователя
Персональные предпочтения разработчика, применимые ко всем проектам. Лучше хранить в домашней директории пользователя как переиспользуемый личный контекст для всех рабочих пространств:
- предпочитаемый стиль кодирования
- привычная последовательность отладки
- предпочитаемый формат вывода
- персональные быстрые команды
Должен дополнять проектные конвенции, а не переопределять их.
#### Локальный уровень
Специфичен для вашей локальной копии проекта, **не должен попадать в Git**:
- персональные тестовые аккаунты
- локальные порты разработки
- временные адреса тестовых заглушек
- заметки по среде выполнения на конкретной машине
- экспериментальные рабочие процессы, не готовые к распространению
Ценность этого слоя: **позволяет индивидуальную эффективную работу без загрязнения общей памяти**.
#### Уровень субагента / роли
Разные субагенты могут поддерживать собственные области памяти вместо использования единой глобальной. Это особенно важно в многоагентных системах, где одна из самых частых проблем: загрязнение контекста между ролями.
Лучший паттерн: каждый субагент хранит только память, релевантную его роли:
- **агент тестирования** помнит команды тестирования, поведение CI, стиль утверждений
- **агент рефакторинга** помнит границы модулей, запрещённые зависимости, стратегии миграции
- **агент документации** помнит глоссарий терминов, шаблоны документации, стиль для целевой аудитории
Это делает память короче, точнее и стабильнее.
### Загрузка `.md`-файлов по пути
Для крупных репозиториев рекомендуется разделять инструкции на несколько Markdown-файлов в `.claude/rules/`, каждый посвящён одной теме: `testing.md`, `api-design.md`, `security.md`.
Claude Code также поддерживает **привязку правил к определённым поддиректориям или типам файлов**: правила загружаются только когда агент работает с подходящими файлами. Это снижает шум и экономит контекстное окно.
Три принципа организации:
- **Основной файл памяти ограничен глобальным общим контекстом**: фон проекта, высокоуровневая архитектура, кросс-проектные конвенции
- **Специализированные правила модульны**: один файл правил на тему
- **Если правило можно загрузить по пути, не загружайте его глобально**: включайте в контекст только при необходимости
Пример структуры:
```
agent-memory/
├── project.md # Обзор проекта
├── rules/
│ ├── code-style.md # Стиль кода
│ ├── testing.md # Конвенции тестирования
│ ├── api-design.md # Правила дизайна API
│ ├── security.md # Требования безопасности
│ └── frontend/
│ └── react.md # Правила фронтенда
└── local/
└── developer.local.md
```
Три преимущества такой структуры:
1. **Проще поддерживать.** Каждый файл правил фокусируется на одной теме, набор правил менее склонен к разрастанию
2. **Проще загружать по запросу.** Когда агент работает над тестами, ему не нужно загружать конвенции фронтенда или правила баз данных
3. **Лучше для командной работы.** Разные команды могут поддерживать собственные директории правил вместо редактирования единого монолитного файла
### Пишите правила памяти как конкретные инструкции
При написании памяти агента используйте **конкретные, проверяемые правила**, а не абстрактные принципы. Чем яснее инструкции, тем стабильнее поведение агента.
Общие рекомендации:
- инструкции должны быть **лаконичными и явными**
- правила должны быть **согласованы** друг с другом
- основной файл памяти **не более 200 строк** по возможности
- используйте **Markdown-заголовки и списки** для читаемости
- формулируйте требования как правила, которые можно **проверить и выполнить**
Избегайте расплывчатых формулировок:
- ~~Держите код чистым~~
- ~~Пишите хорошие тесты~~
- ~~Следите за дизайном API~~
- ~~Разделяйте модули при необходимости~~
Предпочитайте конкретные правила:
- Используйте **2-пробельный отступ** во всех новых TypeScript-файлах
- **Запускайте `pnpm test`** после модификации бизнес-логики
- Размещайте **все обработчики API в `src/api/handlers/`**
- Держите React-компоненты страниц **менее 300 строк**; разбивайте большие на хуки или дочерние компоненты
Конкретные правила значительно сокращают пространство для интерпретации агентом, что повышает стабильность поведения.
### Переиспользование памяти через импорт
В реальных проектах многие правила — это **общие инженерные конвенции между репозиториями**. Переписывание их в каждом репозитории увеличивает накладные расходы на поддержку и повышает вероятность рассогласования.
В Claude Code:
- `CLAUDE.md` может импортировать другие файлы правил через `@path/to/import`
- `.claude/rules/` может делить правила через **символические ссылки** (symlinks)
- импортируемый контент раскрывается **рекурсивно**, символические ссылки разрешаются нормально
Это позволяет командам создавать **переиспользуемые пакеты правил**:
- `company-security-rules`
- `frontend-react-rules`
- `backend-api-rules`
- `python-testing-rules`
Каждый проект ссылается только на нужные модули правил, а не поддерживает полную копию всего набора.
Два прямых преимущества:
1. **Правила поддерживаются централизованно и обновляются единообразно**
2. **Разные проекты разделяют один инженерный язык**, делая поведение агента согласованным между репозиториями
## Устранение проблем с памятью
### Агент не следует `.md`-файлам памяти
`.md`-файлы памяти предоставляются агенту как контекстные инструкции, а не как принудительная конфигурация. Агент прочитает их и попытается следовать, но не гарантирует строгое соблюдение при расплывчатых, неясных или конфликтующих правилах.
Если агент не следует правилам, проверьте:
- Подтвердите загрузку `.md`-файлов памяти (команда `/memory` или аналог)
- Проверьте, находятся ли файлы в пути, разрешённом для загрузки в текущей сессии
- Проверьте конфликты правил между файлами. Если разные файлы дают разные инструкции для одного поведения, агент может выбрать произвольно
### Непонятно, что сохранила автоматическая память
Большинство coding agents поддерживают авто-память в фоне для захвата контекста проекта, предпочтений пользователя или частых действий.
Способы проверки:
- Выполните `/memory` (или аналогичную команду) для просмотра текущей директории авто-памяти
- Авто-память обычно хранится в Markdown-файлах, которые можно читать, редактировать или удалять напрямую
### Файлы памяти слишком большие
Раздутые файлы памяти потребляют больше контекстного окна, снижают следование инструкциям и увеличивают вероятность конфликтов.
Рекомендуется:
- разделить детальный контент на несколько Markdown-файлов
- использовать ссылки на файлы или импорты (`@path/to/file`)
- перенести правила в выделенную директорию правил (`rules/`)
### Инструкции исчезают после сжатия контекста
Многие coding agents **сжимают или резюмируют контекст** в длинных разговорах для уменьшения длины контекста.
В большинстве случаев файлы памяти **перезагружаются с диска** после сжатия, поэтому сохраняется только контент, записанный в файлы памяти. Если правила исчезают после сжатия, значит они **существовали только в разговоре** и не были записаны в файл.
Решение:
- записывайте долгосрочные инструкции в `.md`-файлы памяти
- не полагайтесь только на разговор для сохранения правил
+650
View File
@@ -0,0 +1,650 @@
# **Data Vault 2.0: как собрать хранилище из конструктора**
## 📚 Оглавление
1. [Зачем вообще нужен Data Vault?](#1-зачем-вообще-нужен-data-vault)
2. [Интуиция: DV как конструктор Lego](#2-интуиция-dv-как-конструктор-lego)
3. [Три типа таблиц в Data Vault 2.0](#3-три-типа-таблиц-в-data-vault-20)
4. [Типы сателлитов в DV 2.0](#4-типы-сателлитов-в-dv-20)
5. [Raw Vault и Business Vault](#5-raw-vault-и-business-vault)
6. [Пример: клиент и заказы в DV-стиле](#6-пример-клиент-и-заказы-в-dv-стиле)
7. [Как это живёт в пайплайне загрузки](#7-как-это-живёт-в-пайплайне-загрузки)
8. [Плюсы и минусы Data Vault](#8-плюсы-и-минусы-data-vault)
9. [Когда DV стоит использовать, а когда нет](#9-когда-dv-стоит-использовать-а-когда-нет)
10. [Шпаргалка для собеседования](#10-шпаргалка-для-собеседования)
---
## 1. Зачем вообще нужен Data Vault?
Большинство знакомятся с хранилищами через две модели:
* **3NF** (Инмон) — нормализованное ядро: много таблиц, строгие связи, минимум дублирования.
* **Звезда (Star Schema)** (Кимбалл) — витрины под отчёты: факт + несколько «плоских» измерений.
Этого хватает для:
* 25 источников,
* относительно стабильных схем,
* задач типа «сделать отчёт для маркетинга/финансов».
Проблемы начинаются, когда:
* источников **становится десяток и больше** (CRM, биллинг, ERP, сайт, мобильное приложение, партнёры, скоринги…);
* схемы **постоянно меняются**: добавляются поля, сущности, новые связи;
* появляются требования по **аудиту и трассировке** (то есть умению по шагам показать, откуда взялись данные и как они менялись): «покажите, откуда взялся вот этот показатель, по шагам».
Вот тут обычная 3NF/Звезда начинает скрипеть:
* любое изменение источника → больно по ядру и витринам;
* история размазана по разным местам (где-то SCD, где-то лог-таблицы, где-то вообще нет истории);
* добавление нового источника превращается в мини-проект на месяц.
**Data Vault 2.0** отвечает именно на эту боль:
> Как сделать так, чтобы **новый источник** → это не «ремонт всего дома», а просто «докрутить ещё один модуль»?
---
## 2. Интуиция: DV как конструктор Lego
Классическая метафора DV — это **конструктор из трёх типов деталей**:
* 🔴 **Hub (Хаб)***«кто/что это»*
Сущности: клиент, заказ, договор, счёт.
Внутри: бизнес‑ключ (например, customer_id или contract_number) и техполя.
***Link (Линк)***«как они связаны»*
«Клиент сделал заказ», «договор относится к счёту».
Внутри: ссылки на хабы и техполя.
* 🟡 **Satellite (Сателлит)***«какие у них свойства и как они менялись»*
Атрибуты сущности (имя, email, статус, тариф) плюс история изменений.
Главная идея:
> **Идентичность, связи и атрибуты живут отдельно.**
> Тогда изменения в одном не ломают другое.
На картинке это можно показать так:
```mermaid
erDiagram
HUB_CUSTOMER ||--o{ SAT_CUSTOMER_INFO : имеет_атрибуты
HUB_CUSTOMER ||--o{ LINK_ORDER_CUSTOMER : участвует_в
HUB_ORDER ||--o{ LINK_ORDER_CUSTOMER : создан
LINK_ORDER_CUSTOMER ||--o{ SAT_ORDER_STATUS : имеет_историю_статусов
HUB_CUSTOMER {
varchar customer_bk PK
varchar record_source
timestamp load_dttm
}
HUB_ORDER {
varchar order_id PK
varchar record_source
timestamp load_dttm
}
LINK_ORDER_CUSTOMER {
varchar link_key PK
varchar order_id FK
varchar customer_bk FK
varchar record_source
timestamp load_dttm
}
SAT_CUSTOMER_INFO {
varchar customer_bk FK
varchar hashdiff
varchar name
varchar email
varchar city
varchar record_source
timestamp load_dttm
}
SAT_ORDER_STATUS {
varchar link_key FK
varchar hashdiff
varchar status
varchar record_source
timestamp load_dttm
}
```
Как это читать:
* 🔴 HUB_CUSTOMER / HUB_ORDER — «этот клиент существует», «этот заказ существует»;
* ⚪ LINK_ORDER_CUSTOMER — «именно этот заказ сделал именно этот клиент»;
* 🟡 SAT_… — как менялись атрибуты (email, статус и т.п.) во времени.
На схеме выше для наглядности показаны бизнес-ключи (`customer_bk`, `order_id`, `link_key`), а в DDL-примерах дальше используются уже хэш-ключи (`hk_*`) — это два уровня детализации одной и той же модели.
## 3. Три типа таблиц в Data Vault 2.0
Чуть менее «сказочно», чуть более технично.
### 3.1. Hub — сущность и её бизнес-ключ
**Hub** содержит:
* бизнес‑ключ (customer_bk, order_id, contract_number);
* техническую информацию:
* record_source — из какой системы пришла первая запись;
* load_dttm — когда запись попала в DV;
* иногда — хэш бизнес‑ключа (hk_customer).
Главные правила:
* один бизнес‑ключ — один хаб (одна строка на сущность, без истории);
* хаб не знает про атрибуты (имя, email) — только идентичность.
Простейший DDL‑скелет:
```sql
CREATE TABLE hub_customer (
hk_customer BYTEA PRIMARY KEY, -- хэш от BK
customer_bk VARCHAR(50) NOT NULL, -- business key
record_source VARCHAR(50) NOT NULL,
load_dttm TIMESTAMP NOT NULL
);
```
Конкретные типы данных (`BYTEA`, длины `VARCHAR`, детали `hashdiff`) и реализации хэш‑ключей можно не запоминать: на старте важнее понять саму идею — у сущностей есть стабильные ключи, а все изменения атрибутов мы записываем отдельными версиями в сателлитах.
### 3.2. Link — связи между сущностями
**Link** описывает факт связи, например:
* заказ ↔ клиент;
* договор ↔ счёт;
* карта ↔ клиент.
Примеры бизнес‑смыслов:
* link_order_customer — «этот заказ принадлежит этому клиенту»;
* link_contract_account — «этот договор привязан к этому счёту».
DDL‑эскиз:
```sql
CREATE TABLE link_order_customer (
hk_order_customer BYTEA PRIMARY KEY,
hk_order BYTEA NOT NULL,
hk_customer BYTEA NOT NULL,
record_source VARCHAR(50) NOT NULL,
load_dttm TIMESTAMP NOT NULL
);
```
### 3.3. Satellite — атрибуты и история
**Satellite** хранит:
* атрибуты хаба или линка;
* историю изменений этих атрибутов.
Примеры:
* sat_customer_info — имя, email, город клиента;
* sat_customer_segment — сегмент, категория, риск‑профиль;
* sat_order_status — статус заказа.
Типичные поля:
* ссылка на HUB или LINK (hk_customer, hk_order_customer);
* атрибуты (email, city, status и т.п.);
* hashdiff — хэш от всех атрибутов, чтобы понять, изменилась ли строка;
* record_source, load_dttm — источник и момент загрузки версии.
```sql
CREATE TABLE sat_customer_info (
hk_customer BYTEA NOT NULL,
hashdiff BYTEA NOT NULL,
name VARCHAR(100),
email VARCHAR(100),
city VARCHAR(50),
record_source VARCHAR(50) NOT NULL,
load_dttm TIMESTAMP NOT NULL
);
```
Главная мысль: DV заставляет явно разделять идентичность, связи и атрибуты с историей. Это делает модель сложнее на вид, но гораздо устойчивее к изменениям источников.
Когда нужны именно бизнес-периоды действия («с/по»), их удобнее моделировать не в базовом сателлите, а отдельными effectivity-сателлитами или через PIT-таблицы в Business Vault.
## 4. Типы сателлитов в DV 2.0
> Если вы только знакомитесь с DV, этот раздел можно прочитать по диагонали: для собеседования важно скорее знать, что такие роли бывают, чем разбираться во всех нюансах.
В DV 2.0 появилось разделение по «ролям» сателлитов. Главное, что стоит знать:
* **Descriptive Satellites** — обычные атрибуты (имя, адрес, тариф) с историей.
* **Effectivity Satellites** — фокус на периодах действия (`valid_from` / `valid_to`), очень похоже на SCD2.
* **Multi-Active Satellites** — когда у сущности несколько одновременных значений (например, три активных телефона клиента).
* **Transactional Satellites** — события, привязанные к одному хабу/линку (например, журнал изменений статуса).
На практике это разные DDL-«шаблоны» поверх одной и той же идеи:
**атрибуты + время → отдельная табличка.**
---
## 5. Raw Vault и Business Vault
Обычно под «Data Vault» люди смешивают два слоя:
```mermaid
flowchart LR
SRC[Источники] --> STG[STG / ODS]
STG --> RAW[Raw Vault
Hubs, Links, Sats]
RAW --> BV[Business Vault
PIT, Bridge, Derived]
BV --> DM[Data Marts
Star Schema]
DM --> BI[BI / ML]
```
* **Raw Vault** — это про приём и хранение данных «как есть», но уже в форме Hub / Link / Satellite.
* **Business Vault** — это про приведение этих данных в более «деловой» вид: с бизнес-правилами, PIT/Bridge и подготовленными представлениями.
### 5.1. Raw Vault — «всё прилетевшее, аккуратно разложенное по ящичкам»
Raw DV — первый слой поверх STG / ODS:
* выравниваем ключи;
* разбираем сущности по Hub / Link / Sat;
* сохраняем всю историю изменений, не решая ещё, что такое «активный клиент» или «успешный заказ».
Характерные черты Raw Vault:
* минимум бизнес-логики:
* никаких правил вроде «клиент активен, если была хотя бы одна покупка за 90 дней»;
* все источники показываются «как есть», только приведены к общим ключам;
* структура стабильна: добавился новый источник → появился новый Satellite к тому же Hub.
### 5.2. Business Vault — «там, где из Lego собирают модули»
Business Vault (BV) — следующий слой над Raw DV:
* здесь применяются бизнес-правила (что считать активным клиентом, как трактовать статусы);
* здесь строятся вспомогательные структуры:
* PIT-таблицы,
* Bridge-таблицы,
* агрегаты и derived-таблицы.
Именно из BV чаще всего строятся витрины в формате Звезды, к которым подключаются BI и отчётность.
Если сильно упростить:
* Raw DV → «мы всё собрали»;
* Business Vault → «мы это привели в вид, с которым удобно жить»;
* DM → «мы вынесли это на витрину в понятной форме».
#### 5.2.1. PIT-таблицы (Point-in-Time)
> Раздел для любопытных: PIT-таблицы — уже продвинутая тема, на первом знакомстве с DV её можно смело пропустить и вернуться позже.
PIT (Point-in-Time) решает очень конкретную боль:
> «Покажи, как объект выглядел **на дату X**, но так, чтобы запрос был простым».
Если у нас есть несколько сателлитов с историей (например, `sat_customer_info`, `sat_customer_segment`, `sat_customer_risk`), то без PIT любой запрос превращается в пачку условий
`as_of_date >= valid_from AND (valid_to IS NULL OR as_of_date < valid_to)` (в effectivity-сателлитах или любых таблицах с периодами действия) или оконных функций.
**Идея PIT:**
Мы заранее считаем «словарь» вида:
> для каждой пары (объект, дата) — какие версии сателлитов на эту дату актуальны.
Упрощённый пример структуры:
```sql
CREATE TABLE pit_customer_daily (
hk_customer BYTEA,
as_of_date DATE,
hk_sat_info BYTEA, -- sat_customer_info на эту дату
hk_sat_segment BYTEA, -- sat_customer_segment на эту дату
load_dttm TIMESTAMP
);
```
Внутри логически это выглядит так:
| hk_customer | as_of_date | hk_sat_info | hk_sat_segment |
| ----------- | ---------- | ------------ | -------------- |
| 101 | 2023-06-10 | hash_info_v1 | hash_seg_v1 |
| 101 | 2023-06-11 | hash_info_v2 | hash_seg_v1 |
| 101 | 2023-06-12 | hash_info_v2 | hash_seg_v2 |
Дальше витрина продаж вместо сложных `BETWEEN` делает простой JOIN:
```sql
SELECT
s.sale_date,
s.amount,
seg.segment,
info.city
FROM fact_sales s
JOIN pit_customer_daily pit
ON pit.hk_customer = s.hk_customer
AND pit.as_of_date = s.sale_date
JOIN sat_customer_segment seg
ON seg.hk_customer = pit.hk_customer
AND seg.hashdiff = pit.hk_sat_segment
JOIN sat_customer_info info
ON info.hk_customer = pit.hk_customer
AND info.hashdiff = pit.hk_sat_info;
```
Мы **один раз** дорого посчитали PIT (по расписанию),
и дальше все витрины просто используют эту «справочную таблицу».
Коротко: **PIT — это таблица, где заранее записано, какая версия данных была актуальна на дату X.**
#### 5.2.2. Bridge-таблицы
> Тоже продвинутый приём: Bridge-таблицы чаще нужны в боевых хранилищах, чем на первых собеседованиях.
Bridge-таблицы отвечают на другой вопрос:
> «Какие объекты **в итоге** связаны между собой через длинную цепочку Links?»
В Data Vault связь между двумя сущностями редко бывает «одним JOIN’ом». Чаще это цепочка:
* клиент → договор → счёт → карта → транзакция;
* подразделение → филиал → торговая точка → чек;
* компания → дочерняя компания → проект → контракт → платёж.
Если каждый раз в витринах писать все эти JOIN’ы, запросы становятся:
* длинными и хрупкими;
* плохо читаемыми;
* дублируются во множестве отчётов.
**Идея Bridge:**
Сделать отдельную таблицу, где заранее развернуть «финальные» пары:
```sql
CREATE TABLE bridge_customer_trx (
hk_customer BYTEA,
hk_trx BYTEA
-- опционально: даты действия связи, тип связи и т.п.
);
```
Такую таблицу мы считаем **в ETL**, один раз по расписанию, а не в каждом запросе.
Условный псевдокод построения:
```sql
INSERT INTO bridge_customer_trx (hk_customer, hk_trx)
SELECT DISTINCT
c.hk_customer,
t.hk_trx
FROM hub_customer c
JOIN link_customer_contract lcc ON lcc.hk_customer = c.hk_customer
JOIN hub_contract ct ON ct.hk_contract = lcc.hk_contract
JOIN link_contract_account lca ON lca.hk_contract = ct.hk_contract
JOIN hub_account acc ON acc.hk_account = lca.hk_account
JOIN link_account_card lac ON lac.hk_account = acc.hk_account
JOIN hub_card card ON card.hk_card = lac.hk_card
JOIN link_card_trx lct ON lct.hk_card = card.hk_card
JOIN hub_trx t ON t.hk_trx = lct.hk_trx;
```
После этого витрина может использовать уже готовый Bridge:
```sql
SELECT
c.customer_bk,
SUM(t.amount) AS total_amount
FROM hub_customer c
JOIN bridge_customer_trx bct
ON bct.hk_customer = c.hk_customer
JOIN hub_trx t
ON t.hk_trx = bct.hk_trx
WHERE t.trx_date >= CURRENT_DATE - INTERVAL '30 day'
GROUP BY c.customer_bk;
```
Сложный путь по Links мы спрятали внутрь bridge‑таблицы,
а витрины работают с простой связкой «клиент ↔ транзакция».
Коротко:
* **PIT** — про «какая версия атрибутов была на дату X»;
* **Bridge** — про «какие объекты в итоге связаны друг с другом по длинному маршруту».
Bridge — это **не обязательный элемент DV**, а инструмент оптимизации.
Он нужен тогда, когда цепочки Links становятся длинными и повторяются во многих отчётах.
#### 5.2.3. Business-правила и derived-таблицы
> Этот раздел полезен, чтобы увидеть, как DV помогает «прятать» повторяющуюся бизнес-логику, но для базового понимания Data Vault его можно оставить «на потом».
В BV логично размещать бизнес-логику, которая:
* повторяется во многих отчётах;
* достаточно стабильна.
Примеры:
* флаг «активный клиент», который вычисляется на основе истории покупок и логинов;
* «основной тариф», выбранный по набору правил из нескольких источников;
* «чистый статус заказа», свёрнутый из цепочки статусов (created → paid → shipped → delivered / cancelled).
Это могут быть как отдельные Satellites / Links, так и логические таблицы BV с уже посчитанными флагами и агрегатами.
### 5.3. Граница между Business Vault и витринами (DM)
Важно проговорить границу:
* Business Vault — ещё про данные и историю;
* DM (Data Marts) — уже про конкретные бизнес-сценарии и удобство BI.
Витрины можно переделывать, не трогая BV, пока вы не меняете фундаментальные бизнес-правила.
### 5.4. Как выглядит связка Raw DV → BV → DM на примере
Мини-пример интернет-магазина:
1. Raw Vault:
* `hub_customer`, `hub_order`;
* `link_order_customer`;
* `sat_customer_info`, `sat_order_status`, `sat_customer_segment`.
2. Business Vault:
* `pit_customer_daily` — срез клиента по дням;
* `bv_customer_flags` — активность, VIP-статусы, сегменты;
* `bridge_customer_order` — связи клиент ↔ заказ с удобными ключами.
3. DM / Star Schema:
* `dm.fact_sales` — факт продаж;
* `dm.dim_customer` — уже плоское измерение с нужными полями (email_current, segment, is_active_90d и т.п.);
* `dm.dim_date`, `dm.dim_product` и другие измерения.
В результате:
* Raw DV — технически корректный, историчный и некрасивый слой;
* BV — рабочий слой для инженеров и продвинутых аналитиков;
* DM — привычная Звезда для всех остальных.
## 6. Пример: клиент и заказы в DV-стиле
Возьмём мини‑пример всё того же интернет‑магазина.
У нас есть:
* клиент с `customer_id = 101`, у которого иногда меняется email и город;
* два заказа: `order_id = 5001` и `order_id = 5002`;
* статусы заказов: `created → paid → shipped → delivered`.
### Что появляется в Raw Vault
В Data Vault это раскладывается на несколько таблиц:
* `hub_customer` — по одной строке на каждого клиента (BK = `customer_id`).
* `hub_order` — по одной строке на каждый заказ (BK = `order_id`).
* `link_order_customer` — связь «какой заказ сделал какой клиент».
* `sat_customer_info` — история атрибутов клиента (имя, email, город).
* `sat_order_status` — история статусов заказа.
Примерно так это выглядит логически:
```text
HUB_CUSTOMER
--------------------------------------
customer_bk | record_source | load_dttm
--------------------------------------
101 | CRM | 2023-01-10 10:00
SAT_CUSTOMER_INFO
customer_bk | load_dttm | hashdiff | email | city
---------------------------------------------------------------------------
101 | 2023-01-10 10:00 | ... | a@example.com | Moscow
101 | 2023-06-01 09:00 | ... | alice@newmail.com | Moscow
HUB_ORDER
--------------------------------------
order_id | record_source | load_dttm
--------------------------------------
5001 | SHOP | 2023-06-10 12:00
5002 | SHOP | 2023-06-11 09:30
SAT_ORDER_STATUS
order_id | load_dttm | hashdiff | status
--------------------------------------------------------------
5001 | 2023-06-10 12:00 | ... | created
5001 | 2023-06-10 12:05 | ... | paid
5001 | 2023-06-11 09:00 | ... | shipped
... | ... | ...
```
Ключевая идея: **любое изменение** (email, статус) — это **новая строка** в соответствующем Satellite.
### Как из этого получить витрину продаж
Дальше нам нужно привычное измерение `dim_customer` и факт `fact_sales` в формате Звезды.
Обычно цепочка выглядит так:
1. В Business Vault строим `PIT`‑таблицу по клиентам:
* для каждой даты (или дня, или часа) знаем, какая строка из `sat_customer_info` была актуальна.
2. Строим витрину `dm.dim_customer`:
* на выбранную дату берём нужную версию из `sat_customer_info`;
* добавляем флаги/сегменты из других Satellites/BV‑таблиц.
3. Строим факт `dm.fact_sales`, где каждая строка — заказ или позиция заказа.
Итог: на витрине мы видим «плоского» клиента (одна строка → текущее имя/город/email на момент заказа),
хотя внутри DV лежит полная, аккуратно разложенная история.
## 7. Как это живёт в пайплайне загрузки
Посмотрим теперь, как DV вписывается в обычный ETL/ELT‑пайплайн.
Типичный поток выглядит так:
1. **STG / ODS — приём и первичная обработка**
* Подключаемся к источникам (CRM, биллинг, сайт, партнёрские выгрузки).
* Забираем инкременты (CDC, выгрузки по расписанию, API).
* Приводим типы данных, чистим очевидный мусор, нормализуем форматы дат и т.п.
2. **Raw Vault — приземление в Hub / Link / Satellite**
* Из STG/ODS считаем хэш‑ключи для бизнес‑ключей (HK для Hubs).
* Создаём/обновляем **Hubs** — если бизнес‑ключ новый, заводим запись.
* Создаём/обновляем **Links** — фиксируем связи между сущностями.
* Для **Satellites** считаем `hashdiff` по атрибутам и добавляем новые строки
только если что‑то реально изменилось.
3. **Business Vault — бизнес‑правила и ускорители**
* Строим PIT‑таблицы, чтобы быстро получать срез «на дату X».
* Строим Bridge‑таблицы для сложных цепочек связей.
* Вычисляем стабильные бизнес‑флаги и derived‑атрибуты (активность, сегменты, статусы).
4. **DM / Star Schema — витрины для отчётов и аналитики**
* На основе BV собираем факты и измерения в формате Звезды.
* Под это уже настраиваются BI‑инструменты, отчётность, дашборды.
Чем это отличается от классического «STG → ODS → DDS → DM»:
* слой DDS в DV‑подходе часто фактически превращается в **Raw+Business Vault**;
* вместо одной «большой» нормализованной схемы ядра у нас набор Lego‑модулей (Hubs/Links/Sats), которые за счёт хэш‑ключей и независимой загрузки доменов проще масштабировать и развивать/грузить параллельно.
DV не отменяет STG/ODS/DM — он скорее **раскладывает слой DDS на более мелкие и управляемые детали**.
## 8. Плюсы и минусы Data Vault
### 8.1. Плюсы
* 📜 **История «из коробки»**
Каждое изменение — отдельная запись в сателлите. Ничего не перезатирается.
* 🧩 **Лёгкое добавление источников**
Новый источник с теми же сущностями = новые сателлиты к тем же хабам.
* 🔎 **Трассировка и аудит**
Видно, из какого источника, когда и с какими атрибутами прилетела каждая строка.
* 👥 **Параллельная работа команд**
Разные домены в DV практически не блокируют друг друга.
### 8.2. Минусы
* 🧠 **Высокий порог входа**
Нужно понимать SCD, хэш‑ключи, нагрузку на JOIN, паттерны загрузки.
* 📈 **Больше таблиц и JOIN’ов**
Даже простой запрос превращается в «HUB + LINK + 23 SAT + PIT».
* 🛠 **Нужна дисциплина**
Забыли заполнить `record_source`/`load_dttm` — потеряли часть аудита.
***Плохо подходит для MVP**
Для 2–3 источников DV обычно дороже, чем классическая 3NF/Звезда.
## 9. Когда DV стоит использовать, а когда нет
### Подходит, если:
* у вас **зоопарк источников** (5+ систем, которые ещё и меняются);
* важна **полная история и аудит** (финтех, гос, телеком, крупный банк);
* команда нацелена на долгую жизнь DWH, а не одноразовый отчёт;
* есть люди, готовые жить в этой модели (архитектор, data engineer’ы).
### Лучше не начинать с DV, если:
* это **первый DWH в компании**;
* 1–3 источника и нет жёстких требований по аудиту;
* команда малая (1–2 инженера + аналитик) и сроки жмут;
* задача звучит как «дайте отчёты к кварталу», а не «построим платформу на 5 лет».
В таких случаях честнее (и дешевле) начать с:
> `stg → ods → dds (3NF/простая Звезда с SCD2) → dm (Звезда)`
А DV оставить как следующий шаг, когда появятся реальные боли, которые он решает.
## 10. Шпаргалка для собеседования
Если нужно быстро объяснить, что такое Data Vault:
- Data Vault — это модель хранилища, которая разделяет идентичность (Hubs), связи (Links) и атрибуты с историей (Satellites), чтобы проще переживать изменения источников.
- В DV есть три типа таблиц: `Hub` (бизнес-ключи сущностей), `Link` (связи между сущностями) и `Satellite` (атрибуты и их история).
- Raw Vault — слой, где данные складываются «как есть» в виде Hub/Link/Sat, Business Vault — слой с бизнес-правилами, PIT/Bridge и подготовленными представлениями для витрин.
- История в DV хранится «из коробки»: каждое изменение атрибутов — новая строка в сателлите, прошлые значения не затираются.
- DV хорошо подходит, когда много источников, они часто меняются и важен аудит; для маленького, простого DWH обычно хватает 3NF/Звезды.
- Для собеседования важно уметь связать всё вместе: объяснить Hub/Link/Satellite, отличия Raw и Business Vault и то, что хэш-ключи и независимые сателлиты позволяют параллельно загружать разные сущности, не ломая историю и аудит.
@@ -0,0 +1,297 @@
# Домашка: статусы клиента от 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. Эталонное решение
<details>
<summary>Показать ссылку на решение</summary>
Когда выполните домашку и захотите сверить результат — готовое решение лежит в файле [`09_dml_hw_customer_status_solution.sql`](sql/09_dml_hw_customer_status_solution.sql).
Постарайтесь не подглядывать до того, как напишете свой вариант — основная ценность задания именно в самостоятельном разборе.
</details>
+868
View File
@@ -0,0 +1,868 @@
# Хранилище данных: как устроена аналитика «под капотом»
*Для тех, кто знает SQL, но хочет понять, как хранить данные не в Excel, а по-взрослому*
## Оглавление
- [Что вы уже умеете, и что узнаете здесь](#что-вы-уже-умеете-и-что-узнаете-здесь)
- [1. Введение: почему нельзя просто SELECT из базы заказов?](#1-введение-почему-нельзя-просто-select-из-базы-заказов)
- [2. Учебный пример: интернет-магазин](#2-учебный-пример-интернет-магазин)
- [3. Зачем делить DWH на слои?](#3-зачем-делить-dwh-на-слои)
- [4. Путешествие данных: от STG до DM](#4-путешествие-данных-от-stg-до-dm)
- [5. Базовые понятия: факты, измерения, SCD](#5-базовые-понятия-факты-измерения-scd)
- [6. Модели данных для слоя DDS: 4 подхода, и когда какой выбрать](#6-модели-данных-для-слоя-dds-4-подхода-и-когда-какой-выбрать)
- [7. Практикум: как собрать первую витрину](#7-практикум-как-собрать-первую-витрину)
- [8. Как выбрать модель данных? Советы от практиков](#8-как-выбрать-модель-данных-советы-от-практиков)
- [9. Эксплуатация: качество данных, это не «опция»](#9-эксплуатация-качество-данных-это-не-опция)
- [10. Заключение: главное, понимать «почему»](#10-заключение-главное-понимать-почему)
- [Приложения](#приложения)
---
## Что вы уже умеете, и что узнаете здесь
✅ Уже знаете:
- `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["Таблицы-дубликаты</br>в формате источника"]
end
subgraph ODS["ODS — «Очистка»"]
E["Типы:</br>даты → DATE,</br>числа → INT/DECIMAL</br>Валидация: email, phone"]
end
subgraph DDS["DDS — «Интеграция»"]
F["Общие сущности:</br>клиент, товар, дата</br>История (SCD),</br>суррогатные ключи"]
end
subgraph DM["DM — «Готовые решения»"]
G["Витрина продаж: дата, товар, клиент, сумма</br>+ агрегаты (выручка/день)"]
end
A --> STG
B --> STG
C --> STG
STG --> ODS
ODS --> DDS
DDS --> DM
DM --> BI["BI-системы</br>(Power BI, Tableau,</br>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` — время получения данных;
- Главное правило: **неизменяемость**. Если пришла новая порция — либо добавляем новые строки, либо *полностью перезагружаем* слой (идемпотентность).
> 💡 *Пример:* `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 и клиента из заказов — это будет позже.
- Пока — никакой бизнес-логики. Только *техническая* очистка.
- Дедупликация: если два раза пришёл один и тот же заказ — оставляем один (по `order_id + _load_ts`).
> 🎯 Цель ODS — дать «надёжную платформу» для следующего слоя. Как сухое, чистое бревно перед сборкой дома.
---
### **DDS (Data Delivery Store / Core / Conformed)** — «интеграция + история»
Здесь рождается *единая бизнес-модель*.
Появляются понятия: **измерения**, **факты**, **суррогатные ключи**, **SCD**.
Например:
| Таблица | Назначение |
|---------|------------|
| `dds.dim_customer` | Измерение «Клиент» с историей (SCD Type 2) |
| `dds.dim_product` | Измерение «Товар» |
| `dds.dim_date` | Готовый календарь на 5 лет вперёд (день/неделя/месяц/квартал) |
| `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 / Gold/ «Витрины»)** — «готово к употреблению»
Здесь — таблицы и представления для конкретных задач:
- `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_date {
int date_key PK "YYYYMMDD"
date calendar_date "сама дата"
int year
int month
int day
varchar dow "день недели"
}
dim_customer {
bigint customer_sk PK "суррогатный ключ"
int customer_bk "бизнес-ключ, напр. 101"
varchar customer_name
varchar email
varchar city
date valid_from "SCD2: с какой даты запись актуальна"
date valid_to "SCD2: по какую дату актуальна (NULL = сейчас)"
}
dim_product {
bigint product_sk PK
varchar product_bk "код товара / артикул"
varchar product_name
varchar category
}
fact_sales {
bigint sale_id PK
int date_key FK "ссылка на dim_date.date_key"
bigint customer_sk FK
bigint product_sk FK
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 |
|-------------|-------------|-------|------|------------|----------|
| 1001 | 101 | a@ex.com | Москва | 2023-01-01 | 2023-05-16 |
| 1002 | 101 | b@ex.com | Москва | 2023-05-16 | 2023-10-01 |
| 1003 | 101 | b@ex.com | СПб | 2023-10-01 | NULL |
Когда мы считаем продажи за **12 января** — джойним `fact_sales` к той строке `dim_customer`, где:
```sql
fact_sales.order_date >= dim_customer.valid_from
AND (dim_customer.valid_to IS NULL OR fact_sales.order_date < dim_customer.valid_to)
```
и получаем актуальный на тот день email и город.
> 🔍 Подробнее про SCD — в отдельной статье [Slowly Changing Dimensions](SCD.md) (сравнение Type 1/2/3, паттерны обновления).
Теперь, когда мы разобрались, что такое факты, измерения и SCD, давайте посмотрим, как именно можно устроить слой DDS внутри — есть несколько вариантов.
---
## 6. Модели данных для слоя DDS: 4 подхода, и когда какой выбрать
В DDS мы можем хранить данные по-разному. Это не «правильно/неправильно», а **выбор под задачу**.
### 1. 3NF (третья нормальная форма)
*Источник: Билл Инмон (Bill Inmon)*
Если упростить, 3NF - это когда данные о разных бизнес-сущностях хранятся в отдельных таблицах и связываются ключами: клиент, заказ, город, регион, страна и т.д. Вместо одной большой таблицы с большим числом дублирующихся данных мы получаем цепочку таблиц, связанных ключами: `Заказ → Клиент → Город → Регион → Страна`. JOIN-ов становится больше, зато одно и то же свойство (например, название города) хранится в одном месте, а не дублируется в каждой строке заказа.
Исторически подход с ядром в 3NF чаще связывают с Биллом Инмоном (Bill Inmon): сначала проектируют корпоративную модель данных ядра в 3NF (сущности, атрибуты, связи), а уже поверх неё строят витрины.
![Пример цепочки в 3NF](images/3NF-small.jpg)
* Данные о сущностях разнесены по отдельным таблицам: клиент, заказ, продукт.
* Минимум дублирования: общие атрибуты хранятся в одном месте, таблицы связаны ключами.
#### Как выглядел бы наш магазин в 3NF
В нашем примере `city` лежит прямо в `dim_customer`. В 3NF город стал бы отдельной таблицей, чтобы название хранилось в одном месте:
```mermaid
erDiagram
dim_city ||--o{ dim_customer : "город"
dim_customer ||--o{ fact_sales : "клиент"
dim_city {
int city_id PK
varchar city_name "Москва, СПб, ..."
}
dim_customer {
bigint customer_sk PK
int customer_bk
varchar email
int city_id FK "ссылка на dim_city"
date valid_from
date valid_to
}
fact_sales {
bigint sale_id PK
bigint customer_sk FK
int date_key FK
int quantity
decimal amount
}
```
Теперь, чтобы узнать **«сколько потратил клиент из Москвы за январь 2024?»**, нужно пройти по цепочке:
```sql
-- 3NF: три JOIN, чтобы добраться до города
SELECT SUM(f.amount)
FROM dds.fact_sales f
JOIN dds.dim_customer c ON f.customer_sk = c.customer_sk
JOIN dds.dim_city ct ON c.city_id = ct.city_id
JOIN dds.dim_date d ON f.date_key = d.date_key
WHERE ct.city_name = 'Москва'
AND d.year = 2024 AND d.month = 1;
```
Запрос читаемый, но JOIN-ов уже три - и это для простого вопроса. В реальном ядре цепочка может быть длиннее: `Клиент → Город → Регион → Страна`.
**Плюсы:**
* Удобно поддерживать **единую «карту бизнеса»**: где живут «клиент», «заказ», «договор» и как они связаны;
* Меньше дублирования: одно и то же свойство хранится в одном месте, проще исправлять ошибки и контролировать качество;
* Проще собирать разные витрины поверх ядра: внизу держим детальные данные и связи, наверху показываем «как удобно».
**Минусы:**
* Если строить отчёты прямо по ядру, запросы часто получаются тяжёлыми: много `JOIN`-ов и условий;
* История (SCD Type 2) увеличивает объём данных и добавляет временной контекст в соединения - запросы становятся сложнее и менее удобными для чтения;
* Изменения в бизнес-процессах приходится аккуратно встраивать в существующую модель: чем старше ядро, тем дороже большие переделки.
Частый паттерн: **ядро в 3NF** (подход ближе к Инмону), витрины - в Звезде.
---
### 2. Звезда (Star Schema)
*Источник: Ральф Кимболл (Ralph Kimball)*
Самый распространённый способ построения таблиц для слоя витрин. Именно эту модель мы использовали в [разделе 5](#5-базовые-понятия-факты-измерения-scd): схема `fact_sales` + `dim_date` / `dim_customer` / `dim_product` - это и есть Звезда.
Структура:
* в центре - таблица фактов (события и метрики);
* вокруг - измерения, обычно денормализованные («плоские») - широкие таблицы со всеми атрибутами сущности. Мы сознательно избегаем цепочек справочников ради простоты запросов.
![Звёздная схема вокруг fact_sales](images/star-model-small.jpg)
Методология Ральфа Кимбалла (Ralph Kimball) как раз делает упор на такие звёздные схемы: витрины, которые максимально просты для чтения и понятны аналитикам и BI-инструментам.
#### Тот же вопрос - в Звезде
В Звезде `city` лежит прямо в `dim_customer` (денормализовано). Тот же отчёт выглядит проще:
```sql
-- Звезда: два JOIN, город - прямо в измерении
SELECT SUM(f.amount)
FROM dds.fact_sales f
JOIN dds.dim_customer c ON f.customer_sk = c.customer_sk
JOIN dds.dim_date d ON f.date_key = d.date_key
WHERE c.city = 'Москва'
AND d.year = 2024 AND d.month = 1;
```
На один JOIN меньше, и не нужно знать, где именно хранится город: он лежит прямо в карточке клиента. Для аналитика или BI-инструмента это большая разница.
**Плюсы:**
* проста для понимания: аналитикам и BI-инструментам удобно работать с такой моделью;
* меньше `JOIN`-ов - запросы обычно проще и быстрее;
* хорошо подходит для витрин под конкретные задачи.
**Минусы:**
* измерения денормализованы, поэтому атрибуты дублируются (например, название города повторяется у всех клиентов из этого города);
* изменения атрибутов могут требовать обновлять много строк в измерении.
---
### 3. Data Vault 2.0 — «конструктор Lego» для больших DWH
*Идея: Дэн Линстедт (Dan Linstedt). Цель — так организовать хранилище, чтобы можно было спокойно добавлять новые источники и хранить историю, не ломая старую модель.*
#### В чём идея, по-человечески
В Data Vault (DV) все сущности разлетаются по трём типам таблиц:
- **Hub (Хаб)***«кто/что это»*.
Только бизнес-ключ и технические поля: клиент, заказ, договор.
- **Link (Линк)***«как они связаны»*.
«Клиент сделал заказ», «договор относится к счёту».
- **Satellite (Сателлит)***«какие у них свойства и как они менялись»*.
Имя клиента, email, статус заказа, цены — всё с историей изменений.
![Пример модели Data Vault](images/data-vault-small.jpg)
💡 **Главная мысль:**
идентичность, связи и атрибуты живут **в разных таблицах**, поэтому:
- историю проще хранить;
- новые источники проще прикручивать;
- меньше шансов «сломать» старые отчёты.
Data Vault хорошо подходит там, где много разнородных источников, нужна полная история изменений и прозрачный аудит. За гибкость приходится платить сложностью модели и количеством таблиц — поэтому для небольших проектов (2–5 источников, маленькая команда) DV почти наверняка избыточен.
> 🔍 Подробнее про Data Vault — сравнение с 3NF/Звездой, Raw и Business Vault, когда внедрять — в отдельной статье [DataVault: как пережить бурную жизнь источников](DataVault.md).
---
### 4. Anchor Modeling (анкерное моделирование)
*Источник: Ларс Рёне (Lars Rönnbäck)*
Anchor Modeling - ещё более атомарный подход к моделированию ядра, чем Data Vault. Если упростить, он «режет» модель на очень мелкие части, чтобы изменения в атрибутах и связях можно было добавлять почти без переделок схемы.
Основные типы таблиц:
* **Anchor** - сущности (например, «клиент» или «заказ»).
* **Attribute** - отдельный атрибут сущности, обычно с историей (например, email, город, статус - каждый в своей таблице).
* **Tie** - связь между сущностями (например, «клиент ↔ заказ»).
![Пример Anchor Modeling](images/anchor-model-small.jpg)
Плюс подхода - высокая гибкость: проще добавлять новые атрибуты и варианты связей. Минус - цена этой гибкости: получается очень много таблиц, и запросы (и поддержка модели) обычно заметно сложнее, огромное кол-во `JOIN`. Для обычного DWH-проекта это точно не первый выбор - скорее вариант для очень динамичных предметных областей, где структура данных часто меняется.
---
### Сравнение моделей — наглядно
```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` — таблицу, которую можно сразу подключить к 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 TD
%% STG
STG_PROD[stg.products_raw]
STG_CUST[stg.customers_raw]
STG_ORD[stg.orders_raw]
STG_ITEMS[stg.order_items_raw]
%% ODS
ODS_PROD[ods.products]
ODS_CUST[ods.customers]
ODS_ORD[ods.orders]
ODS_ITEMS[ods.order_items]
%% DDS
DIM_PROD[dds.dim_product]
DIM_CUST[dds.dim_customer]
DIM_DATE[dds.dim_date]
FACT_SALES[dds.fact_sales]
%% DM / BI
DM_SALES[dm.mart_daily_sales]
BI[BI / Power BI]
%% Потоки данных
STG_PROD --> ODS_PROD --> DIM_PROD --> DM_SALES
STG_CUST --> ODS_CUST --> DIM_CUST --> DM_SALES
STG_ORD --> ODS_ORD --> FACT_SALES --> DM_SALES
STG_ITEMS --> ODS_ITEMS --> FACT_SALES
DIM_DATE --> FACT_SALES
DM_SALES --> BI
```
### Готовые SQL-скрипты
Все необходимые скрипты для построения хранилища находятся в папке [`sql/`](sql/):
- [`01_ddl_stg-dds.sql`](sql/01_ddl_stg-dds.sql) — создание схем и таблиц (STG, ODS, DDS);
- [`02_dml_stg-dds.sql`](sql/02_dml_stg-dds.sql) — первичная загрузка данных и демонстрация SCD2 через полный пересчёт (`full backfill`) из STG;
- [`03_demo_increment.sql`](sql/03_demo_increment.sql) — пример инкрементальной загрузки и SCD2 по последнему снимку в ODS;
- [`04_validation.sql`](sql/04_validation.sql) — проверки качества данных;
- [`05_ddl_dm.sql`](sql/05_ddl_dm.sql) — создание витрин (Data Marts);
- [`06_dml_dm.sql`](sql/06_dml_dm.sql) — наполнение витрин данными.
### Пример SQL-запроса для витрины
```sql
-- mart_daily_sales: ежедневные продажи с сегментацией
-- Полная пересборка (full refresh) - для простоты; в продакшене бывает incremental.
TRUNCATE dm.mart_daily_sales;
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 -- факт ссылается на нужную версию SK
GROUP BY d.date_actual, p.product_name,
CASE WHEN f.amount >= 200 THEN 'Premium' ELSE 'Basic' END;
```
> 💡 В продакшене витрину иногда оформляют как **MATERIALIZED VIEW** - «кэш» результата запроса, который обновляется по расписанию. В нашем примере используем обычную таблицу с `TRUNCATE` + `INSERT` - для учебных целей это нагляднее.
✏️ **Попробуйте сами:** [Домашка: статусы клиента от STG до DDS (и немного DM)](Homework_Customer_Status_DDS_DM.md) — пройдёте тот же путь, но самостоятельно.
---
## 8. Как выбрать модель данных? Советы от практиков
Выбор модели — **не техническая задача, а стратегическая**.
Это как решать: строить дом из кирпича, дерева или SIP-панелей. У каждой технологии — свои плюсы, но **главное — подходит ли она *вам* сегодня**.
### 🔹 Главное, что нужно понять новичку:
> **Не существует «самой правильной» модели.**
> Есть **самая подходящая под ваш контекст** — и он у всех разный.
---
### 🛑 Что делать **не стоит** (если опыта ещё мало):
| Что делать не стоит | Почему |
|---------------------|--------|
| **Брать Data Vault «потому что модно»** | DV требует глубокого понимания интеграции, CDC, идемпотентности. Без этого легко получить «историю», в которой невозможно найти актуальные данные. |
| **Строить сложную 3NF «как в книжках» под 10 таблиц** | Если у вас 2–3 источника — вы потратите недели на нормализацию, чтобы потом делать 5 JOIN’ов ради простого отчёта. |
| **Пытаться «сделать сразу гибко на 5 лет вперёд»** | Гибкость = сложность. А сложность = баги, задержки, выгорание команды. |
---
### ✅ Базовые советы — с чего начать, если вы учитесь или делаете первый DWH
1. **Начните с витрины в формате Звезды (Star Schema).**
— Это просто: одна таблица фактов + несколько «плоских» измерений.
— Это быстро: отчёт в BI — за 10 минут.
— Это понятно: даже менеджер поймёт структуру.
2. **Стройте DDS только когда это *действительно нужно*.**
— Если источников ≤ 3 и они стабильны — можно идти `ods → dm` напрямую.
— Если появляются расхождения («email в CRM и в заказах — разные») — тогда заводите `dds.dim_customer` и другие общие сущности.
3. **Историю (SCD) включайте *постепенно*.**
— Сначала — без истории (Type 1: просто обновляете строку).
— Потом — только для ключевых сущностей (клиент, товар, договор).
— Только потом — думайте про DV или полную историзацию всего.
4. **Если сомневаетесь — спросите: «А кто будет этим пользоваться?»**
Выбор модели начинается не с технологий, а с вопроса: **кто будет работать с результатом?**
Это как выбрать инструмент в мастерской: для гвоздей — молоток, для саморезов — отвёртка.
Вот как это выглядит на практике:
**Аналитик в Metabase / Looker Studio****Звезда (Star Schema)**
Почему: ему нужны готовые метрики без сложных JOIN’ов. Звезда даёт понятные таблицы: «продажи по дням и товарам» — без углубления в атомарные сущности.
**BI-разработчик в Power BI / Tableau****Звезда**
Почему: все инструменты визуализации оптимизированы под star schema. Один факт + несколько измерений = быстрые отчёты и простую модель.
**Инженер ML (Data Scientist / ML-инженер)****3NF или сырые ODS-таблицы**
Почему: для фичей нужны атомарные события и детальные атрибуты. Машинное обучение ценит полноту и детализацию данных больше, чем удобство отчётов.
**Юрист, финансовый контролёр, аудитор / регулятор****3NF с SCD Type 2, иногда + DV в ядре**
Почему: им нужна доказуемая история изменений, но не инфраструктурная сложность DV на каждый чих.
Обычно достаточно хранить аудит-историю по ключевым сущностям (клиенты, договоры, счета) в формате 3NF + SCD Type 2.
**Data Vault имеет смысл только если** у вас 10+ разнородных источников и жёсткие требования по аудиту и трассировке.
💡 **Золотое правило:**
«**Собирай данные как DV (максимально детально), показывай как Звезду (максимально просто).**»
На начальных этапах вам почти всегда хватит **SCD Type 2 в рамках 3NF/Звезды**.
**Data Vault** нужен тогда, когда основная боль — интеграция множества систем и аудит, а не «первый отчёт для маркетинга».
---
### 💡 Ещё один совет от практиков
> **Лучше сделать простую модель — и вовремя переделать,**
> чем сделать «идеальную» — и застрять на этапе проектирования.
Переделать Звезду → Звезду с SCD Type 2 — относительно легко.
Переделать «недоделанный DV» → что-то рабочее — в разы сложнее.
---
### 📌 Кратко — что выбрать *сегодня*, если вы только учитесь
| У вас… | Делайте… |
|--------|----------|
| Учебный проект, 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 → доработка → новые метрики.
---
## Приложения
### Дополнительные материалы и практика
- [Домашка: статусы клиента от STG до DDS (и немного DM)](Homework_Customer_Status_DDS_DM.md)
- [SCD: как хранить историю изменений](SCD.md)
- [DataVault: как пережить бурную жизнь источников](DataVault.md)
### 📚 Мини-глоссарий (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** | История «отвязана» от бизнеса: удалили клиента — и вся его история исчезла |
---
### Мини-датасет (для практики)
Все данные для практики находятся в папке [`data/`](data/) — тренируйтесь:
[`customers.csv`](data/customers.csv):
```csv
customer_id,email,phone,city,event_ts,_load_id,_load_ts
101,a@ex.com,700,Москва,2024-01-01,batch_20240101_0800,2024-01-01 08:00
102,c@ex.com,701,СПб,2024-01-01,batch_20240101_0800,2024-01-01 08:00
101,b@ex.com,700,Москва,2024-05-16,batch_20240516_0800,2024-05-16 08:00
101,b@ex.com,700,Санкт-Петербург,2024-10-01,batch_20241001_0800,2024-10-01 08:00
```
[`orders.csv`](data/orders.csv):
```csv
order_id,order_date,customer_id
5001,2024-01-10,101
5002,2024-02-05,102
```
[`order_items.csv`](data/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`](data/products.csv):
```csv
product_id,name
9001,Phone
9002,Case
```
[`prices.csv`](data/prices.csv):
```csv
product_id,valid_from,valid_to,price
9001,2023-12-01,2024-01-31,100
9001,2024-02-01,,110
9002,2023-01-01,,50
```
> 📂 Все SQL-скрипты для построения хранилища находятся в папке [`sql/`](sql/).
---
### DDL-скелеты (PostgreSQL)
Полные DDL-скрипты для всех слоёв хранилища находятся в файле [`01_ddl_stg-dds.sql`](sql/01_ddl_stg-dds.sql).
Пример структуры основных таблиц DDS:
```sql
-- DDS: измерение клиента (SCD Type 2)
CREATE TABLE dds.dim_customer (
customer_sk BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_bk INT NOT NULL, -- напр. 101
email VARCHAR(100),
phone VARCHAR(20),
city VARCHAR(50),
valid_from DATE NOT NULL,
valid_to DATE
);
-- 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`.
+426
View File
@@ -0,0 +1,426 @@
# Медленно меняющиеся измерения (SCD): как хранить историю в аналитических базах данных
> **Для кого эта статья?**
> Для тех, кто уже умеет писать базовые SQL-запросы в PostgreSQL, знаком с понятиями таблиц, строк и колонок, и теперь делает первые шаги в аналитике и проектировании хранилищ данных.
---
## 1. Введение: зачем вообще нужны измерения и почему они «медленно меняются»?
Представьте, что вы строите отчёт по продажам. У вас есть таблица с фактами — например, «продано 10 единиц товара X клиенту Y 15 марта». Но чтобы понять, *кто такой клиент Y* или *что за товар X*, вам нужны **справочники** — таблицы с описанием клиентов, товаров, регионов и т.п.
В мире аналитики такие справочники называют **измерениями** (*dimensions*), а таблицу с продажами — **фактами** (*facts*).
А теперь представьте: клиент сменил адрес или перешёл в другую категорию (например, из «обычного» в «VIP»). Если вы просто обновите строку в таблице клиентов, то потеряете информацию о том, **какой статус у клиента был на момент продажи**. А это критично: отчёт «продажи VIP-клиентам в марте» окажется неверным!
Такие атрибуты — которые **меняются со временем, но не каждый день** — и называются **медленно меняющимися измерениями** (*Slowly Changing Dimensions*, **SCD**).
---
## 2. Что такое Slowly Changing Dimensions (SCD)?
SCD — это подход к хранению изменений в измерениях **с учётом времени**. Он позволяет отвечать на вопросы вроде:
- Какой адрес у клиента был **на дату заказа**?
- Сколько продаж пришлось на товары категории «Электроника» **до того, как её переименовали в «Гаджеты»**?
Без SCD вы видите только **текущее состояние**, а с ним — **всю историю**.
---
## 3. Типы SCD — простыми словами
Существует несколько стандартных стратегий обработки изменений. Рассмотрим самые важные.
### **Type 0 — Никогда не меняется**
Атрибут фиксирован навсегда. Например, дата рождения клиента.
Такие поля не требуют специальной обработки — они просто не обновляются.
### **Type 1 — Просто перезаписать**
Вы просто делаете `UPDATE`, и старое значение исчезает.
✅ Просто.
❌ История теряется.
> Подходит, если изменение — это исправление ошибки (например, опечатка в имени).
### **Type 2 — Новая строка для новой версии**
Каждое изменение порождает **новую строку** в таблице. Старая строка остаётся, но помечается как «устаревшая».
✅ Полная история.
✅ Можно восстановить состояние на любую дату.
❌ Больше данных, сложнее запросы.
> Это **самый распространённый** подход в аналитике.
### **Type 3 — Добавить колонку «предыдущее значение»**
В таблице появляются поля вроде `previous_category`, `category_change_date`.
✅ Простая история «до/после».
❌ Хранит только **одно** предыдущее значение. Не масштабируется.
> Используется редко, чаще как компромисс в очень простых системах.
### **Type 4, 5, 6 — Продвинутые гибриды**
Эти типы существуют, но **встречаются редко** и почти не используются новичками:
- **Type 4**: история выносится в отдельную таблицу («мини-хранилище» для одного измерения).
- **Type 5**: комбинация Type 4 и Type 1 — текущее значение в основной таблице, а история — отдельно.
- **Type 6**: объединяет Type 1, 2 и 3 в одной таблице — очень гибко, но сложно.
> Вам **не нужно запоминать** эти типы сейчас. Достаточно знать, что они бывают — на случай, если встретите их в документации.
---
## 4. Практический пример на PostgreSQL: Type 1 vs Type 2
Допустим, у нас есть таблица клиентов:
```sql
-- Исходная таблица (до изменений)
CREATE TABLE customers (
customer_id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL -- например: 'Regular', 'VIP'
);
```
### Type 1: просто обновляем
Клиент №1 стал VIP:
```sql
UPDATE customers
SET category = 'VIP'
WHERE customer_id = 1;
```
Теперь в таблице только новое значение. Если продажа была сделана **до** этого UPDATE, вы не узнаете, что клиент тогда был «Regular».
---
### Type 2: сохраняем историю — подробнее
Чтобы хранить историю, мы меняем структуру таблицы. Вот ключевые поля:
- **`customer_key`** — искусственный (surrogate) первичный ключ. Он **уникален для каждой версии** клиента.
- **`customer_id`** — бизнес-идентификатор (например, из CRM). Он **не меняется** и связывает все версии одного клиента.
- **`valid_from`** — дата, **с которой** эта версия стала актуальной.
- **`valid_to`** — дата, **по которую** эта версия была актуальной. Если `NULL` — значит, версия **актуальна сейчас**.
Пример структуры:
```sql
CREATE TABLE customers_scd2 (
customer_key SERIAL PRIMARY KEY, -- уникальный ID каждой версии
customer_id INT NOT NULL, -- неизменный бизнес-ID клиента
name TEXT NOT NULL,
category TEXT NOT NULL,
valid_from DATE NOT NULL, -- с какой даты действует
valid_to DATE -- по какую дату действовала (NULL = сейчас)
);
```
**Шаг 1.** Добавляем начальную запись (клиент зарегистрировался 1 января 2024):
```sql
INSERT INTO customers_scd2 (customer_id, name, category, valid_from, valid_to)
VALUES (1, 'Иван Петров', 'Regular', DATE '2024-01-01', NULL);
```
**Шаг 2.** 15 апреля 2025 клиент становится VIP. Мы делаем **два действия**:
1. **Закрываем старую запись**: указываем дату начала новой версии (интервал `[valid_from, valid_to)`).
2. **Добавляем новую запись**: она начинает действовать **с 15 апреля** и пока актуальна.
```sql
-- 1. Завершаем предыдущую версию
UPDATE customers_scd2
SET valid_to = DATE '2025-04-15'
WHERE customer_id = 1 AND valid_to IS NULL;
-- 2. Вставляем новую версию
INSERT INTO customers_scd2 (customer_id, name, category, valid_from, valid_to)
VALUES (1, 'Иван Петров', 'VIP', DATE '2025-04-15', NULL);
```
Теперь в таблице две строки для одного клиента. И мы можем спросить:
> Какой была категория клиента **на 10 апреля 2025**?
```sql
SELECT category
FROM customers_scd2
WHERE customer_id = 1
AND DATE '2025-04-10' >= valid_from
AND (valid_to IS NULL OR DATE '2025-04-10' < valid_to);
```
Результат: `'Regular'` — правильно!
> 💡 Почему не `BETWEEN valid_from AND valid_to`?
> Потому что у актуальной записи `valid_to IS NULL`. В SQL сравнения с `NULL` не дают `TRUE`, поэтому для “текущей” версии обычно пишут `valid_to IS NULL OR ...`.
### Type 2 через логику, похожую на UPSERT
В реальных ETL-процессах часто используют **идемпотентные** операции: запуск скрипта дважды не должен ломать данные. Для этого удобно применять подход, похожий на *upsert* (update + insert), но адаптированный под логику SCD Type 2.
В PostgreSQL классический `ON CONFLICT` не подходит напрямую, потому что мы **не обновляем существующую строку**, а **добавляем новую при изменении**.
Поэтому логика выглядит так:
1. Сравнить входящие данные с последней версией в таблице.
2. Если атрибуты изменились — закрыть старую запись и вставить новую.
3. Если не изменились — ничего не делать.
Пример (часто реализуется в Python, dbt, Airflow и т.п.):
```sql
-- Предположим, новая версия: customer_id=1, category='VIP', effective_date='2025-04-15'
-- Шаг 1: вставляем новую версию, только если есть изменения
WITH last_version AS (
SELECT * FROM customers_scd2
WHERE customer_id = 1 AND valid_to IS NULL
)
INSERT INTO customers_scd2 (customer_id, name, category, valid_from, valid_to)
SELECT 1, 'Иван Петров', 'VIP', DATE '2025-04-15', NULL
WHERE EXISTS (
SELECT 1 FROM last_version WHERE category != 'VIP'
);
-- Шаг 2: если вставка произошла — закрываем старую запись
UPDATE customers_scd2
SET valid_to = DATE '2025-04-15'
WHERE customer_id = 1 AND valid_to IS NULL
AND EXISTS (
SELECT 1 FROM customers_scd2
WHERE customer_id = 1 AND category = 'VIP' AND valid_from = DATE '2025-04-15'
);
```
На практике такие логики чаще выносят в **ETL-инструменты** (например, dbt с пакетом `dbt-scd`), потому что чистый SQL быстро становится громоздким.
> 💡 Главное: **SCD Type 2 — это не одна операция, а процесс**: сравнить → закрыть старое → добавить новое.
---
В учебном проекте из папки `dwh-modeling/sql/` эти идеи можно увидеть «вживую»:
- в [`02_dml_stg-dds.sql`](sql/02_dml_stg-dds.sql) собирается полная история клиентов (SCD2) из всех событий в `stg.customers_raw` — это пример **первичной загрузки** / `full backfill`;
- в [`03_demo_increment.sql`](sql/03_demo_increment.sql) реализован **инкрементальный SCD2**: в одной транзакции добавляются новые версии клиентов из снимка `ods.customers` и закрываются предыдущие актуальные строки в `dds.dim_customer`.
---
#### А что, если СУБД не позволяет UPDATE? (Trino, Hive, ClickHouse в режиме append-only)
Некоторые аналитические системы (например, **Hive в формате ORC/Parquet**, **Trino**, **ClickHouse в режиме только вставки**) **не поддерживают UPDATE старых строк**. Как тогда реализовать SCD Type 2?
Ответ: **никаких UPDATE не нужно** — ведь в Type 2 мы и так **не меняем старые данные**, а только **добавляем новые**!
Алгоритм загрузки (ETL):
1. Сравнить входящие данные с последней версией в таблице.
2. Если есть изменения — **пишем новую строку** с новыми `valid_from`.
3. Старые строки остаются нетронутыми.
Пример в Trino/Hive-стиле (только INSERT):
Давайте разберём этот запрос по шагам, чтобы понять его логику:
```sql
-- new_customers — staging-таблица с обновлёнными данными
-- dim_customers_scd2 — основная таблица (append-only)
INSERT INTO dim_customers_scd2
WITH current_customers AS (
SELECT *
FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY valid_from DESC) AS rn
FROM dim_customers_scd2
)
WHERE rn = 1 -- отбираем первую, самую свежую, запись
)
SELECT
uuid() AS customer_key, -- уникальный ID каждой версии
n.customer_id, -- неизменный бизнес-ID клиента
n.name,
n.category,
COALESCE(n.effective_date, current_date) AS valid_from -- дата начала действия новой версии
FROM new_customers n
LEFT JOIN current_customers c ON n.customer_id = c.customer_id
WHERE c.customer_id IS NULL OR c.category != n.category; -- условие верно, если какие-то из полей справочника поменялись
```
##### 🎯 Как работает этот запрос: пошаговое объяснение
###### Шаг 1: Подготовка данных (CTE current_customers)
CTE `current_customers` находит **последнюю версию** каждого клиента из таблицы `dim_customers_scd2`:
```sql
SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY valid_from DESC) AS rn
FROM dim_customers_scd2
```
- `PARTITION BY customer_id` — группируем по клиентам
- `ORDER BY valid_from DESC` — сортируем версии от самой новой к самой старой
- `WHERE rn = 1` — выбираем только самую свежую версию
###### Шаг 2: Сравнение данных (LEFT JOIN + WHERE)
Теперь сравниваем новые данные с текущими:
```sql
FROM new_customers n
LEFT JOIN current_customers c ON n.customer_id = c.customer_id
```
**Возможные сценарии после JOIN:**
| Сценарий | n.customer_id | c.customer_id | Условие WHERE | Результат |
|----------|---------------|---------------|---------------|-----------|
| Новый клиент | 2 | NULL | ✅ `c.customer_id IS NULL` | Вставляется |
| Категория изменилась | 1 | 1 | ✅ `c.category != n.category` | Вставляется |
| Без изменений | 3 | 3 | ❌ оба условия ложны | Пропускается |
###### Шаг 3: Вставка новых версий
Для подходящих записей создаём новую версию:
- `uuid()` — генерируем уникальный ключ для новой версии
- `current_date` - функция, возвращающая текущую даты
- `COALESCE(n.effective_date, current_date)` — устанавливаем дату начала действия новой версии
> 💡 **Правильный подход к датам**: В реальных ETL-процессах важно использовать дату из исходных данных, когда она доступна. Мы используем `COALESCE(n.effective_date, current_date)`, что означает:
> - Если в `new_customers` есть поле `effective_date` — используем его
> - Если нет — используем текущую дату (`current_date`)
>
> **Почему это важно:**
> - `effective_date` отражает реальную дату изменения (например, когда клиент стал VIP)
> - `current_date` — это дата загрузки данных, которая может не совпадать с датой изменения
> - Использование правильной даты критично для точного исторического анализа
>
> **Пример правильной структуры исходных данных:**
> ```sql
> new_customers:
> customer_id | name | category | effective_date
> 1 | Иван Петров | VIP | 2025-04-15 ← дата реального изменения
> ```
##### Практический пример
**До выполнения запроса:**
```
dim_customers_scd2:
customer_id | name | category | valid_from
1 | Иван Петров | Regular | 2024-01-01
```
**Новые данные:**
```
new_customers:
customer_id | name | category
1 | Иван Петров | VIP ← изменилась категория
2 | Мария Иванова| Regular ← новый клиент
```
**После выполнения запроса:**
```
dim_customers_scd2:
customer_id | name | category | valid_from
1 | Иван Петров | Regular | 2024-01-01 ← старая версия
1 | Иван Петров | VIP | 2025-11-04 ← новая версия
2 | Мария Иванова| Regular | 2025-11-04 ← новый клиент
```
> 💡 **Ключевой момент**: В append-only системах мы **не обновляем** старые записи, а только **добавляем новые**. История сохраняется автоматически!
##### Главный вопрос после загрузки: как же читать эти данные?
Поскольку мы не можем обновлять `valid_to` у предыдущей версии, стандартный подход с `BETWEEN` не сработает. Вместо этого, для поиска нужной версии мы полагаемся на **оконные функции** или на логику «ближайшей даты, но не позже».
###### Паттерн 1: Найти последнюю (актуальную) версию на сегодня
Это самый частый запрос. Мы хотим видеть самую свежую информацию о клиенте.
```sql
-- Вариант с оконной функцией (универсальный и надежный)
WITH ranked AS (
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY valid_from DESC) AS rn
FROM dim_customers_scd2
)
SELECT
customer_id, name, category, valid_from
FROM ranked
WHERE rn = 1;
```
Этот запрос берёт строку с самой поздней датой начала действия для каждого клиента.
###### Паттерн 2: Найти версию, которая была актуальна на конкретную дату
Это основная цель SCD2. Например, «какой статус клиента был на дату заказа `2025-03-15`?».
В append-only мире у нас нет `valid_to`, поэтому мы ищем **последнюю версию, у которой `valid_from` ≤ целевой даты**.
```sql
-- Запрос для получения состояния на '2025-03-15'
WITH as_of_date AS (
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY valid_from DESC) AS rn
FROM dim_customers_scd2
WHERE valid_from <= DATE '2025-03-15'
)
SELECT
customer_id, name, category, valid_from
FROM as_of_date
WHERE rn = 1;
```
**Как это работает:**
1. `WHERE valid_from <= DATE '2025-03-15'` отфильтровывает все версии, которые появились **после** нашей целевой даты.
2. `ORDER BY valid_from DESC` сортирует оставшиеся версии от самой свежей к самой старой.
3. `ROW_NUMBER() ... WHERE rn = 1` выбирает самую свежую из **актуальных на ту дату** версий.
Это и есть «путешествие во времени» (time travel) в системах без встроенной поддержки этой функции.
> **Почему не использовать флаг «текущая версия»?**
> В append-only системах любой флаг актуальности становится устаревшим сразу после новой вставки. Без `UPDATE` его сложно поддерживать, поэтому в таких архитектурах чаще полагаются на даты и оконные функции — так модель остаётся идемпотентной.
Таким образом, SCD Type 2 не только совместим с append-only системами, но и является для них **естественным выбором**, так как его логика основана исключительно на добавлении данных, а не на их изменении.
---
## 5. Когда что использовать?
| Сценарий | Рекомендуемый тип |
|--------|------------------|
| Исправление опечатки | Type 1 |
| Юридически значимые изменения (статус, тариф, регион) | Type 2 |
| Очень простая аналитика без требований к истории | Type 1 |
| Нужна только «последняя смена» и ничего больше | Type 3 (осторожно!) |
**Совет новичку**: если сомневаетесь — выбирайте **Type 2**. Лучше иметь историю и не использовать её, чем не иметь и не суметь ответить на важный вопрос.
---
## 6. Подводные камни и советы
- **Не используйте `customer_id` как первичный ключ в Type 2**. Он повторяется! Вместо этого — `customer_key` (surrogate key).
- Всегда задавайте `valid_to` как `NULL` для актуальной записи, если это допустимо в вашей СУБД — это упрощает модель.
- Для условий “актуально на дату” используйте паттерн: `d >= valid_from AND (valid_to IS NULL OR d < valid_to)`.
- Type 2 увеличивает объём данных — но для аналитики это нормально.
- В связке с фактами: в таблице фактов храните **`customer_key`**, а не `customer_id` — иначе не получится соединить с нужной версией.
---
## 7. Заключение
Медленно меняющиеся измерения — это не «магия», а **практический инструмент** для честной и точной аналитики во времени.
Начните с понимания разницы между Type 1 и Type 2. Попробуйте реализовать оба подхода в своей БД. Задайте себе вопрос:
> «Если бы я построил отчёт по данным на прошлый месяц — дал бы он правильный ответ после сегодняшнего изменения?»
Если нет — вам нужен SCD Type 2.
И помните: даже в системах без `UPDATE` вы можете хранить полную историю — достаточно понимать, как правильно читать данные с помощью оконных функций и временных границ.
+28
View File
@@ -0,0 +1,28 @@
# TODO: dwh-modeling
## SCD2 (dim_customer / dim_customer_status)
- Добавить в `02_dml_stg-dds.sql` (блок SCD2 backfill) и `03_demo_increment.sql` (incremental) явные допущения:
- гранулярность `DATE` (daily-grain), интервалы `[valid_from, valid_to)`, current = `valid_to IS NULL`;
- предполагаем **не более одного изменения в день** на BK (иначе нужен `TIMESTAMP`/sequence);
- `valid_from` берём как **effective date**: `COALESCE(event_ts, _load_ts)::date` (и почему так);
- late-arriving/backdated события в демо **не обрабатываются** (что будет “в проде”).
- Коротко документировать “effective time vs load time”:
- `event_ts` = когда изменение произошло в источнике;
- `_load_ts` = когда событие попало в DWH;
- `valid_from/valid_to` строим по effective time, а `_load_id/_load_ts` используем для трассировки/аудита.
- (Опционально) Добавить микросекцию “как читать CTE” в SCD2-блоках: что делает `src → ordered → changes → framed`.
## Вариант с TIMESTAMP (advanced, под вопросом)
- Подумать над отдельным примером SCD2 с `valid_from_ts/valid_to_ts TIMESTAMP`:
- кейс “несколько изменений в один день”;
- корректная обработка одинаковых `event_ts` (tie-breaker: `_load_ts`/`_load_id`);
- влияние на join фактов (условие по `[from,to)`).
- Зафиксировать: показываем как “опционально/advanced”, чтобы не пугать на базовом треке.
## Greenplum (после Postgres-трека)
- Отдельно проговорить практику для больших объёмов:
- обновления SCD2 в GP могут быть дорогими; обсудить паттерны (partitioning/append-only/минимизация UPDATE);
- какие поля выбирать для распределения и сортировки таблиц измерений/фактов (на уровне рекомендаций).
@@ -0,0 +1,9 @@
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
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
102,new,2024-03-05 14:00:00,batch_20240305_1500,2024-03-05 15:00:00
102,active,2024-04-01 09:45:00,batch_20240401_1000,2024-04-01 10:00:00
102,churned,2024-04-20 16:20:00,batch_20240420_1700,2024-04-20 17:00:00
103,new,2024-03-10 10:10:00,batch_20240310_1100,2024-03-10 11:00:00
1 customer_id status event_ts _load_id _load_ts
2 101 new 2024-01-01 09:00:00 batch_20240101_1000 2024-01-01 10:00:00
3 101 active 2024-02-15 10:30:00 batch_20240215_1100 2024-02-15 11:00:00
4 101 vip 2024-05-10 11:00:00 batch_20240510_1200 2024-05-10 12:00:00
5 101 churned 2024-09-01 12:15:00 batch_20240901_1300 2024-09-01 13:00:00
6 102 new 2024-03-05 14:00:00 batch_20240305_1500 2024-03-05 15:00:00
7 102 active 2024-04-01 09:45:00 batch_20240401_1000 2024-04-01 10:00:00
8 102 churned 2024-04-20 16:20:00 batch_20240420_1700 2024-04-20 17:00:00
9 103 new 2024-03-10 10:10:00 batch_20240310_1100 2024-03-10 11:00:00
@@ -0,0 +1,5 @@
customer_id,status,event_ts,_load_id,_load_ts
101,active,2024-11-15 09:00:00,batch_20241115_1000,2024-11-15 10:00:00
102,active,2024-05-05 09:30:00,batch_20240505_1000,2024-05-05 10:00:00
103,active,2024-03-20 12:00:00,batch_20240320_1300,2024-03-20 13:00:00
104,new,2024-06-01 08:00:00,batch_20240601_0900,2024-06-01 09:00:00
1 customer_id status event_ts _load_id _load_ts
2 101 active 2024-11-15 09:00:00 batch_20241115_1000 2024-11-15 10:00:00
3 102 active 2024-05-05 09:30:00 batch_20240505_1000 2024-05-05 10:00:00
4 103 active 2024-03-20 12:00:00 batch_20240320_1300 2024-03-20 13:00:00
5 104 new 2024-06-01 08:00:00 batch_20240601_0900 2024-06-01 09:00:00
+5
View File
@@ -0,0 +1,5 @@
customer_id,email,phone,city,event_ts,_load_id,_load_ts
101,a@ex.com,700,Москва,2024-01-01,batch_20240101_0800,2024-01-01 08:00
102,c@ex.com,701,СПб,2024-01-01,batch_20240101_0800,2024-01-01 08:00
101,b@ex.com,700,Москва,2024-05-16,batch_20240516_0800,2024-05-16 08:00
101,b@ex.com,700,Санкт-Петербург,2024-10-01,batch_20241001_0800,2024-10-01 08:00
1 customer_id email phone city event_ts _load_id _load_ts
2 101 a@ex.com 700 Москва 2024-01-01 batch_20240101_0800 2024-01-01 08:00
3 102 c@ex.com 701 СПб 2024-01-01 batch_20240101_0800 2024-01-01 08:00
4 101 b@ex.com 700 Москва 2024-05-16 batch_20240516_0800 2024-05-16 08:00
5 101 b@ex.com 700 Санкт-Петербург 2024-10-01 batch_20241001_0800 2024-10-01 08:00
+4
View File
@@ -0,0 +1,4 @@
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
1 order_item_id order_id product_id qty price_at_sale
2 1 5001 9001 2 100.00
3 2 5001 9002 1 50.00
4 3 5002 9001 1 100.00
+3
View File
@@ -0,0 +1,3 @@
order_id,order_date,customer_id
5001,2024-01-10,101
5002,2024-02-05,102
1 order_id order_date customer_id
2 5001 2024-01-10 101
3 5002 2024-02-05 102
+4
View File
@@ -0,0 +1,4 @@
product_id,valid_from,valid_to,price
9001,2023-12-01,2024-01-31,100.00
9001,2024-02-01,,110.00
9002,2023-01-01,,50.00
1 product_id valid_from valid_to price
2 9001 2023-12-01 2024-01-31 100.00
3 9001 2024-02-01 110.00
4 9002 2023-01-01 50.00
+3
View File
@@ -0,0 +1,3 @@
product_id,name
9001,Phone
9002,Case
1 product_id name
2 9001 Phone
3 9002 Case
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

+155
View File
@@ -0,0 +1,155 @@
-- ===============================================
-- DDL-скрипт: определение структуры хранилища
-- Запускается ОДИН РАЗ при инициализации БД
-- или при изменении схемы (миграции)
-- ===============================================
-- 1. Схемы
DROP SCHEMA IF EXISTS stg CASCADE;
DROP SCHEMA IF EXISTS ods CASCADE;
DROP SCHEMA IF EXISTS dds CASCADE;
CREATE SCHEMA stg;
CREATE SCHEMA ods;
CREATE SCHEMA dds;
-- 2. STG: сырые данные (как пришли)
DROP TABLE IF EXISTS stg.customers_raw;
CREATE TABLE stg.customers_raw (
customer_id TEXT, -- может быть строкой или числом
email TEXT,
phone TEXT,
city TEXT,
event_ts TEXT,
_load_id TEXT, -- идентификатор загрузки (обязательно!)
_load_ts TIMESTAMP DEFAULT NOW() -- время получения в DWH
);
CREATE TABLE stg.orders_raw (
order_id TEXT,
order_date TEXT,
customer_id TEXT
);
CREATE TABLE stg.order_items_raw (
order_item_id TEXT,
order_id TEXT,
product_id TEXT,
qty TEXT,
price_at_sale TEXT
);
CREATE TABLE stg.products_raw (
product_id TEXT,
name TEXT
);
-- 3. ODS: очищенные данные
DROP TABLE IF EXISTS ods.customers;
CREATE TABLE ods.customers (
customer_id INT NOT NULL, -- привели к INT
email VARCHAR(100),
phone VARCHAR(20),
city VARCHAR(50),
event_ts TIMESTAMP,
_load_id TEXT NOT NULL, -- сохраняем для отладки и SCD
_load_ts TIMESTAMP NOT NULL -- время загрузки (копия из STG)
);
CREATE TABLE ods.orders (
order_id INT,
order_date DATE,
customer_id INT
);
CREATE TABLE ods.order_items (
order_item_id INT,
order_id INT,
product_id INT,
qty INT,
price_at_sale NUMERIC(10,2)
);
CREATE TABLE ods.products (
product_id INT,
name VARCHAR(100)
);
-- Первичные ключи в ODS (для ускорения и валидации)
ALTER TABLE ods.customers ADD PRIMARY KEY (customer_id);
ALTER TABLE ods.orders ADD PRIMARY KEY (order_id);
ALTER TABLE ods.order_items ADD PRIMARY KEY (order_item_id);
ALTER TABLE ods.products ADD PRIMARY KEY (product_id);
-- 4. DDS: интегрированная модель
-- dim_date: справочник дат (ключ — суррогатный date_key)
CREATE TABLE dds.dim_date (
date_key INT PRIMARY KEY,
date_actual DATE NOT NULL,
year SMALLINT,
quarter SMALLINT,
month SMALLINT,
day SMALLINT,
weekday_name VARCHAR(10),
weekday_num SMALLINT,
is_first_week BOOLEAN
);
-- dim_product: измерение "Товар"
CREATE TABLE dds.dim_product (
product_sk BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
product_bk INT NOT NULL,
product_name VARCHAR(100) NOT NULL
);
-- dim_customer: измерение "Клиент" с историей (SCD Type 2)
CREATE TABLE dds.dim_customer (
customer_sk BIGSERIAL PRIMARY KEY,
customer_bk INT NOT NULL, -- бизнес-ключ
email TEXT,
phone TEXT,
city TEXT,
hashdiff TEXT NOT NULL, -- md5 по нормализованным атрибутам
valid_from DATE NOT NULL,
valid_to DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- одна версия на момент времени (на одну пару BK+valid_from)
ALTER TABLE dds.dim_customer
ADD CONSTRAINT uq_dim_customer_bk_from UNIQUE (customer_bk, valid_from);
-- ускорители
CREATE INDEX ix_dim_customer_bk_current ON dds.dim_customer (customer_bk) WHERE valid_to IS NULL;
CREATE INDEX ix_dim_customer_bk_from_to ON dds.dim_customer (customer_bk, valid_from, valid_to);
-- fact_sales: факт "Продажи"
CREATE TABLE dds.fact_sales (
sale_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_sk BIGINT NOT NULL,
product_sk BIGINT NOT NULL,
date_key INT NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
amount NUMERIC(18,2) NOT NULL CHECK (amount >= 0)
);
-- Внешние ключи (опционально — в продакшене часто отключают ради скорости)
ALTER TABLE dds.fact_sales
ADD CONSTRAINT fk_fact_customer FOREIGN KEY (customer_sk) REFERENCES dds.dim_customer(customer_sk),
ADD CONSTRAINT fk_fact_product FOREIGN KEY (product_sk) REFERENCES dds.dim_product(product_sk),
ADD CONSTRAINT fk_fact_date FOREIGN KEY (date_key) REFERENCES dds.dim_date(date_key);
-- Через md5 по нормализованным атрибутам
CREATE OR REPLACE FUNCTION dds.customer_hash(email TEXT, phone TEXT, city TEXT)
RETURNS TEXT LANGUAGE sql IMMUTABLE AS $$
SELECT md5(
concat_ws('||',
lower(coalesce(trim(email), '')),
lower(coalesce(trim(phone), '')),
lower(coalesce(trim(city), ''))
)
);
$$;
+223
View File
@@ -0,0 +1,223 @@
-- ===============================================
-- DML-скрипт: загрузка и трансформация данных
-- Запускается ПОВТОРНО при каждой загрузке (идемпотентно!)
-- ===============================================
--
-- Карта загрузки в этом скрипте:
-- STG → ODS: orders, order_items, products, customers (снимок: одна строка на BK)
-- STG → DDS: dim_customer (SCD2, full backfill напрямую из STG — см. комментарий к п.5)
-- ODS → DDS: dim_product, fact_sales
-- отдельно: dim_date (генерация календаря)
--
-- 1. STG: имитация загрузки из источников (в реальности — COPY или INSERT из Kafka/NiFi)
-- ⚠️ В продакшене STG часто очищается перед загрузкой (TRUNCATE), либо используется партицирование по дате
DELETE FROM stg.customers_raw;
DELETE FROM stg.orders_raw;
DELETE FROM stg.order_items_raw;
DELETE FROM stg.products_raw;
-- STG (пример вставки с метками времени)
INSERT INTO stg.customers_raw (_load_id, _load_ts, event_ts, customer_id, email, phone, city) VALUES
('batch_20240101_0800', '2024-01-01 08:00', '2024-01-01', '101','a@ex.com','700','Москва'),
('batch_20240101_0800', '2024-01-01 08:00', '2024-01-01', '102','c@ex.com','701','СПб'),
('batch_20240516_0800', '2024-05-16 08:00', '2024-05-16', '101','b@ex.com','700','Москва'),
('batch_20241001_0800', '2024-10-01 08:00', '2024-10-01', '101','b@ex.com','700','Санкт-Петербург');
INSERT INTO stg.orders_raw (order_id, order_date, customer_id) VALUES
('5001', '2024-01-10', '101'),
('5002', '2024-02-05', '102');
INSERT INTO stg.order_items_raw (order_item_id, order_id, product_id, qty, price_at_sale) VALUES
('1', '5001', '9001', '2', '100.00'),
('2', '5001', '9002', '1', '50.00'),
('3', '5002', '9001', '1', '100.00');
INSERT INTO stg.products_raw (product_id, name) VALUES
('9001', 'Phone'),
('9002', 'Case');
-- 2. ODS: очистка и типизация
-- ⚠️ В продакшене используем UPSERT (INSERT ... ON CONFLICT DO UPDATE) или incremental load, не TRUNCATE+INSERT
TRUNCATE ods.customers, ods.orders, ods.order_items, ods.products;
INSERT INTO ods.orders (order_id, order_date, customer_id)
SELECT
order_id::INT,
TO_DATE(order_date, 'YYYY-MM-DD'),
customer_id::INT
FROM stg.orders_raw
WHERE order_date IS NOT NULL AND customer_id ~ '^\d+$';
-- берём по BK самую позднюю запись (по дате события, иначе по дате загрузки)
WITH src AS (
SELECT
s.customer_id::INT AS customer_id,
NULLIF(trim(s.email), '') AS email,
NULLIF(trim(s.phone), '') AS phone,
NULLIF(trim(s.city), '') AS city,
NULLIF(s.event_ts, '')::timestamp AS event_ts,
s._load_id,
s._load_ts,
COALESCE(NULLIF(s.event_ts, '')::date, s._load_ts::date) AS eff_date
FROM stg.customers_raw s
WHERE s.customer_id ~ '^\d+$'
),
ranked AS (
SELECT
customer_id, email, phone, city, event_ts, _load_id, _load_ts,
row_number() OVER (PARTITION BY customer_id ORDER BY eff_date DESC, _load_ts DESC) AS rn
FROM src
)
INSERT INTO ods.customers (customer_id, email, phone, city, event_ts, _load_id, _load_ts)
SELECT customer_id, email, phone, city, event_ts, _load_id, _load_ts
FROM ranked
WHERE rn = 1;
INSERT INTO ods.order_items (order_item_id, order_id, product_id, qty, price_at_sale)
SELECT
order_item_id::INT,
order_id::INT,
product_id::INT,
NULLIF(qty, '')::INT,
NULLIF(price_at_sale, '')::NUMERIC(10,2)
FROM stg.order_items_raw
WHERE qty ~ '^\d+$' AND price_at_sale ~ '^\d+(\.\d+)?$';
INSERT INTO ods.products (product_id, name)
SELECT
product_id::INT,
TRIM(name)
FROM stg.products_raw
WHERE product_id ~ '^\d+$';
-- 3. DDS: dim_date — генерация календаря (идемпотентно: можно пересоздавать)
-- В реальности — делается ОДИН РАЗ, либо дополняется по мере необходимости
DELETE FROM dds.dim_date;
WITH RECURSIVE dates AS (
SELECT DATE '2023-01-01' AS d
UNION ALL
SELECT (d + INTERVAL '1 day')::DATE -- ← приведение к DATE
FROM dates
WHERE d + INTERVAL '1 day' <= DATE '2027-12-31'
)
INSERT INTO dds.dim_date (
date_key, date_actual, year, quarter, month, day,
weekday_name, weekday_num, is_first_week
)
SELECT
CAST(TO_CHAR(d, 'YYYYMMDD') AS INT),
d,
EXTRACT(YEAR FROM d)::SMALLINT,
EXTRACT(QUARTER FROM d)::SMALLINT,
EXTRACT(MONTH FROM d)::SMALLINT,
EXTRACT(DAY FROM d)::SMALLINT,
TRIM(TO_CHAR(d, 'Day')), -- ← TRIM — убрать trailing space
EXTRACT(DOW FROM d)::SMALLINT,
d BETWEEN DATE_TRUNC('month', d)
AND DATE_TRUNC('month', d) + INTERVAL '1 month' - INTERVAL '1 day'
AND EXTRACT(DAY FROM d) <= 7
FROM dates;
-- 4. DDS: dim_product — полная перезагрузка (если товары редко меняются)
-- В реальности — инкрементальная загрузка по BK
DELETE FROM dds.dim_product;
INSERT INTO dds.dim_product (product_bk, product_name)
SELECT product_id, name
FROM ods.products;
-- 5. DDS: dim_customer — первичная загрузка SCD2 (full backfill из STG)
-- В ЭТОМ ДЕМО: dim_customer строится напрямую из stg.customers_raw, который играет роль
-- устойчивого event-лога (все события по клиенту в одном месте).
-- Это удобно для учебной первичной загрузки (full backfill), когда мы один раз
-- восстанавливаем всю историю клиента.
-- В РЕАЛЬНОМ DWH: так делают редко. Исторические измерения обычно строят
-- поверх очищенных и нормализованных слоёв (ODS / PSA / Data Vault).
-- Для примера инкрементальной заливки SCD2 по снимку из ODS см. 03_demo_increment.sql и SCD.md.
--
-- Идея SCD2 простыми словами:
-- - одна строка = один период, когда атрибуты клиента (email/phone/city) были одинаковыми;
-- - valid_from = дата, когда "стало так";
-- - valid_to = дата следующего изменения (NULL = текущая версия).
--
-- Откуда берём дату изменения:
-- - если в событии есть event_ts — считаем, что изменение произошло тогда;
-- - если event_ts пустой — берём дату загрузки (_load_ts), чтобы не терять историю.
--
-- Важно для демо: считаем, что у клиента не бывает двух разных изменений в один и тот же день.
TRUNCATE dds.dim_customer, dds.fact_sales;
WITH src AS ( -- 1) Приводим типы, готовим дату изменения (eff_date) и считаем hashdiff атрибутов
SELECT
s.customer_id::INT AS customer_bk,
NULLIF(trim(s.email), '') AS email,
NULLIF(trim(s.phone), '') AS phone,
NULLIF(trim(s.city), '') AS city,
COALESCE(NULLIF(s.event_ts, '')::date, s._load_ts::date) AS eff_date,
dds.customer_hash(s.email, s.phone, s.city) AS hashdiff
FROM stg.customers_raw s
WHERE s.customer_id ~ '^\d+$'
),
ordered AS ( -- 2) Сортируем по датам и смотрим "какой hashdiff был до этого" (LAG)
SELECT *,
lag(hashdiff) OVER (PARTITION BY customer_bk ORDER BY eff_date) AS prev_hash
FROM src
),
changes AS ( -- 3) Оставляем только первое состояние и реальные изменения (где hashdiff поменялся)
SELECT *
FROM ordered
WHERE prev_hash IS DISTINCT FROM hashdiff OR prev_hash IS NULL
),
framed AS ( -- 4) Превращаем изменения в периоды: valid_to = дата следующего изменения (LEAD)
SELECT
customer_bk, email, phone, city, hashdiff,
eff_date AS valid_from,
lead(eff_date) OVER (PARTITION BY customer_bk ORDER BY eff_date) AS valid_to
FROM changes
)
INSERT INTO dds.dim_customer (
customer_bk, email, phone, city, hashdiff,
valid_from, valid_to,
created_at, updated_at
)
SELECT
customer_bk, email, phone, city, hashdiff,
valid_from,
valid_to,
now(), now()
FROM framed
ORDER BY customer_bk, valid_from;
-- 6. DDS: fact_sales — загрузка фактов с учётом SCD
-- В продакшене — фильтруем по диапазону дат (инкрементально)
--TRUNCATE dds.fact_sales;
INSERT INTO dds.fact_sales (customer_sk, product_sk, date_key, quantity, amount)
SELECT
dc.customer_sk,
dp.product_sk,
CAST(TO_CHAR(o.order_date, 'YYYYMMDD') AS INT),
oi.qty,
oi.price_at_sale * oi.qty
FROM ods.orders o
JOIN ods.order_items oi ON o.order_id = oi.order_id
JOIN ods.products p ON oi.product_id = p.product_id
JOIN dds.dim_product dp ON p.product_id = dp.product_bk
JOIN dds.dim_customer dc
ON o.customer_id = dc.customer_bk
AND o.order_date >= dc.valid_from
AND (dc.valid_to IS NULL OR o.order_date < dc.valid_to);
-- 7. Проверка — вывод итогов (не часть ETL, но полезно для отладки)
-- В реальном пайплайне такие SELECT выносятся в отдельные скрипты или дашборды
SELECT 'dim_customer current = ' || COUNT(*) FROM dds.dim_customer WHERE valid_to IS NULL;
SELECT 'fact_sales count = ' || COUNT(*) FROM dds.fact_sales;
+149
View File
@@ -0,0 +1,149 @@
-- ===============================================
-- 03_demo_increment.sql
-- Имитация новых событий + инкрементальный SCD2
-- ===============================================
-- 0. Новые события в STG (пример)
INSERT INTO stg.customers_raw (_load_id, _load_ts, event_ts, customer_id, email, phone, city) VALUES
('batch_20241101_0800', '2024-11-01 08:00', '2024-11-01', '101','b@ex.com','700','Москва'), -- город вернулся
('batch_20240310_0800', '2024-03-10 08:00', '2024-03-10', '103','d@ex.com','702','Казань'); -- новый клиент
-- 1) UPSERT в ODS (вставка с обновлением по конфликту, INSERT ... ON CONFLICT DO UPDATE):
-- сохраняем в ods.customers последнюю версию клиента по BK (бизнес-ключ = customer_id)
WITH src AS (
SELECT
s.customer_id::INT AS customer_id,
NULLIF(trim(s.email), '') AS email,
NULLIF(trim(s.phone), '') AS phone,
NULLIF(trim(s.city), '') AS city,
NULLIF(s.event_ts,'')::timestamp AS event_ts,
s._load_id,
s._load_ts,
-- eff_ts нужен, чтобы выбрать "самое свежее" событие по клиенту:
-- если event_ts нет, используем время загрузки (_load_ts) как приближение.
COALESCE(NULLIF(s.event_ts,'')::timestamp, s._load_ts) AS eff_ts
FROM stg.customers_raw s
WHERE s.customer_id ~ '^\d+$'
),
ranked AS (
SELECT
customer_id, email, phone, city, event_ts, _load_id, _load_ts,
-- берём одну строку на клиента: с максимальным eff_ts (при равенстве — с максимальным _load_ts)
row_number() OVER (PARTITION BY customer_id ORDER BY eff_ts DESC, _load_ts DESC) AS rn
FROM src
)
INSERT INTO ods.customers (customer_id, email, phone, city, event_ts, _load_id, _load_ts)
SELECT customer_id, email, phone, city, event_ts, _load_id, _load_ts
FROM ranked
WHERE rn = 1
ON CONFLICT (customer_id) DO UPDATE
SET email = EXCLUDED.email,
phone = EXCLUDED.phone,
city = EXCLUDED.city,
event_ts= EXCLUDED.event_ts,
_load_id= EXCLUDED._load_id,
_load_ts= EXCLUDED._load_ts
-- апдейтим только если пришло более «свежее» событие
WHERE COALESCE(EXCLUDED.event_ts, EXCLUDED._load_ts) >
COALESCE(ods.customers.event_ts, ods.customers._load_ts);
-- 2) Инкрементальное SCD2 из ODS (в одной транзакции, по последнему снимку в ODS)
-- Канон для курса: считаем по дням (valid_from/valid_to — DATE), интервалы [valid_from, valid_to),
-- текущая версия = valid_to IS NULL
-- Предполагаем, что для клиента нет нескольких изменений в один день.
--
-- Идея (по шагам):
-- 1) Берём текущий "снимок" клиента из ODS (одна строка на BK = customer_id).
-- 2) Сравниваем его с текущей версией в dds.dim_customer (valid_to IS NULL) по hashdiff.
-- 3) Если изменилось — закрываем текущую версию (ставим valid_to) и вставляем новую (valid_to = NULL).
--
-- Про даты:
-- eff_date берём из event_ts, а если его нет — из _load_ts (как приближение).
BEGIN;
-- 2.1) Закрываем предыдущую актуальную версию (только если реально изменились атрибуты)
-- delta/current считаем прямо в запросе (без временных таблиц) специально для читабельности.
WITH delta AS (
SELECT
c.customer_id AS customer_bk,
c.email, c.phone, c.city,
COALESCE(c.event_ts::date, c._load_ts::date) AS eff_date,
dds.customer_hash(c.email, c.phone, c.city) AS hashdiff
FROM ods.customers c
),
current AS (
SELECT d.*
FROM dds.dim_customer d
WHERE d.valid_to IS NULL
)
UPDATE dds.dim_customer d
SET valid_to = x.eff_date,
updated_at = now()
FROM (
-- x = кандидаты на "закрытие" текущей версии:
-- клиент есть в DDS (current) и атрибуты изменились (hashdiff стал другим).
SELECT
t.customer_bk,
t.eff_date,
c.customer_sk
FROM delta t
JOIN current c
ON c.customer_bk = t.customer_bk
WHERE c.hashdiff <> t.hashdiff
AND t.eff_date > c.valid_from -- не создаём период нулевой/отрицательной длины
) x
WHERE d.customer_sk = x.customer_sk
AND d.valid_to IS NULL;
-- 2.2) Вставляем новую версию (только если новая или изменившаяся)
-- delta/current повторяем ещё раз отдельно, чтобы блок вставки читался независимо от блока UPDATE.
WITH delta AS (
SELECT
c.customer_id AS customer_bk,
c.email, c.phone, c.city,
COALESCE(c.event_ts::date, c._load_ts::date) AS eff_date,
dds.customer_hash(c.email, c.phone, c.city) AS hashdiff
FROM ods.customers c
),
current AS (
SELECT d.*
FROM dds.dim_customer d
WHERE d.valid_to IS NULL
),
to_insert AS (
-- to_insert = кандидаты на вставку:
-- 1) новый клиент (в current нет строки);
-- 2) изменившийся клиент (hashdiff поменялся).
-- если атрибуты не менялись — клиент сюда не попадёт, и ничего делать не нужно.
SELECT
t.customer_bk,
t.email,
t.phone,
t.city,
t.hashdiff,
t.eff_date
FROM delta t
LEFT JOIN current c
ON c.customer_bk = t.customer_bk
WHERE c.customer_sk IS NULL
OR (c.hashdiff <> t.hashdiff AND t.eff_date > c.valid_from)
)
INSERT INTO dds.dim_customer (
customer_bk, email, phone, city, hashdiff,
valid_from, valid_to,
created_at, updated_at
)
SELECT
t.customer_bk, t.email, t.phone, t.city, t.hashdiff,
t.eff_date, NULL,
now(), now()
FROM to_insert t
-- защита от повторного запуска: не вставляем одну и ту же версию (BK + valid_from) второй раз
WHERE NOT EXISTS (
SELECT 1
FROM dds.dim_customer d
WHERE d.customer_bk = t.customer_bk
AND d.valid_from = t.eff_date
);
COMMIT;
-- (факты можно не перезаливать — даты заказов не поменялись)
+73
View File
@@ -0,0 +1,73 @@
-- ===============================================
-- Проверки качества данных после загрузки STG→ODS→DDS
-- Запускается после 02_dml_stg-dds.sql (и, при необходимости, 03_demo_increment.sql)
-- ===============================================
-- 1. Проверка: dim_customer не пуста
DO $$
BEGIN
ASSERT (SELECT COUNT(*) FROM dds.dim_customer) > 0,
'ОШИБКА: таблица dds.dim_customer пуста — загрузка не прошла';
RAISE NOTICE '✅ dim_customer: НЕ ПУСТА (всего строк: %)',
(SELECT COUNT(*) FROM dds.dim_customer);
END $$;
-- 2. Проверка: каждая строка из ODS попала в DDS-факт
-- Сравниваем количество строк в ods.order_items и dds.fact_sales
DO $$
DECLARE
expected_count bigint;
actual_count bigint;
BEGIN
SELECT COUNT(*) INTO expected_count FROM ods.order_items;
SELECT COUNT(*) INTO actual_count FROM dds.fact_sales;
--
ASSERT actual_count = expected_count,
format('ОШИБКА: в fact_sales %s строк, а в ods.order_items — %s. Разница: %s',
actual_count, expected_count, expected_count - actual_count);
--
RAISE NOTICE '✅ fact_sales: количество строк совпадает с ods.order_items (%)', actual_count;
END $$;
-- 3. Проверка SCD2 (Type 2): у клиента 101 должно быть ≥2 версий (из-за смены email)
DO $$
DECLARE version_count INT;
BEGIN
SELECT COUNT(*) INTO version_count
FROM dds.dim_customer
WHERE customer_bk = 101;
--
ASSERT version_count >= 2,
FORMAT('ОШИБКА: у клиента 101 только %s версия, ожидается ≥2 (должна быть история)', version_count);
RAISE NOTICE '✅ SCD2: клиент 101 имеет % версий — история сохранена', version_count;
END $$;
-- 4. Проверка SCD2 (Type 2): у каждого клиента ровно одна актуальная версия (valid_to IS NULL)
DO $$
DECLARE
customers_cnt BIGINT;
current_cnt BIGINT;
BEGIN
SELECT COUNT(DISTINCT customer_bk) INTO customers_cnt FROM dds.dim_customer;
SELECT COUNT(*) INTO current_cnt
FROM dds.dim_customer
WHERE valid_to IS NULL;
--
ASSERT current_cnt = customers_cnt,
FORMAT('ОШИБКА: актуальных строк %s, а уникальных клиентов %s (ожидается 1 current на клиента)',
current_cnt, customers_cnt);
RAISE NOTICE '✅ SCD2: current-строки = количеству клиентов (%)', current_cnt;
END $$;
-- 5. Проверка SCD2 (Type 2): периоды корректны (valid_to > valid_from или valid_to IS NULL)
DO $$
BEGIN
ASSERT NOT EXISTS (
SELECT 1
FROM dds.dim_customer
WHERE valid_to IS NOT NULL
AND valid_to <= valid_from
),
'ОШИБКА: найдены строки dim_customer с некорректным периодом (valid_to <= valid_from)';
RAISE NOTICE '✅ SCD2: периоды valid_from/valid_to корректны';
END $$;
+34
View File
@@ -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_line_items 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);
+59
View File
@@ -0,0 +1,59 @@
-- ===============================================
-- DML: построение Data Marts из DDS
-- Идемпотентно: можно пересобирать в любое время
-- ===============================================
-- 1. Очистка (full refresh — для простоты; в продакшене — incremental)
TRUNCATE dm.mart_daily_sales, dm.mart_customer_360;
-- 2. mart_daily_sales: продажи по датам и товарам
-- Одна строка = дата × товар × простой сегмент заказа
INSERT INTO dm.mart_daily_sales (
date_actual, product_name, customer_segment, total_qty, total_revenue
)
SELECT
d.date_actual,
p.product_name,
-- Делим заказы на Premium / Basic по сумме
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 -- факт уже ссылается на нужную версию клиента
GROUP BY d.date_actual, p.product_name,
CASE WHEN f.amount >= 200 THEN 'Premium' ELSE 'Basic' END;
-- 3. mart_customer_360: 360‑портрет клиента
-- Одна строка = один клиент
-- Считаем суммы по всей истории его покупок
INSERT INTO dm.mart_customer_360 (
customer_bk, first_order_date, last_order_date,
total_line_items, 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_line_items, -- строки факта (позиции продаж), не бизнес-заказы
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
-- Здесь не фильтруем по valid_to: нужна вся история фактов
GROUP BY c.customer_bk;
@@ -0,0 +1,56 @@
-- ===============================================
-- 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 DATE NOT NULL,
valid_to DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
ALTER TABLE dds.dim_customer_status
ADD CONSTRAINT uq_dim_customer_status_bk_from UNIQUE (customer_bk, valid_from);
CREATE INDEX ix_dim_customer_status_bk_current
ON dds.dim_customer_status (customer_bk)
WHERE valid_to IS NULL;
-- 4. DM: витрина статусов клиентов по датам (опциональная часть домашки)
DROP TABLE IF EXISTS dm.mart_customer_status_daily;
CREATE TABLE dm.mart_customer_status_daily (
date_actual DATE NOT NULL,
status VARCHAR(20) NOT NULL,
customers_cnt INT NOT NULL
);
@@ -0,0 +1,43 @@
-- ===============================================
-- 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
-- (в STG/ODS эта колонка будет жить как _load_ts).
-- 3) Постройте из ods.customer_status измерение dds.dim_customer_status в стиле SCD2:
-- - одна строка на период действия статуса (valid_from / valid_to);
-- - актуальная строка для клиента — та, где valid_to IS NULL;
-- - 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 ...), последняя valid_to = NULL;
-- - вставить в dds.dim_customer_status.
-- 3. DDS: инкрементальная загрузка (по желанию)
-- Можно ориентироваться на примеры в 03_demo_increment.sql.
-- 4. DM: витрина статусов клиентов по датам (по желанию)
-- DDL витрины уже создан в 07_ddl_hw_customer_status.sql (dm.mart_customer_status_daily).
-- Идея: на каждую дату взять актуальный статус клиента
-- через JOIN dds.dim_customer_status + dds.dim_date.
@@ -0,0 +1,340 @@
-- ===============================================
-- 09_dml_hw_customer_status_solution.sql
-- Решение домашки: статусы клиента (STG -> ODS -> DDS SCD2 -> DM)
--
-- Это ЭТАЛОННОЕ РЕШЕНИЕ. Если вы ещё не пробовали решить домашку сами -
-- вернитесь к заданию (Homework_Customer_Status_DDS_DM.md) и шаблону (08_dml_hw_customer_status_template.sql).
-- Основная ценность задания - в самостоятельном разборе.
--
-- Что делает этот файл:
-- 1) Перекладывает события статусов в ODS (приводит типы, чистит пустое).
-- 2) Строит DDS-измерение со "встроенной историей" (SCD2): периоды valid_from/valid_to.
-- 3) Загружает инкрементальную порцию событий и обновляет ODS + DDS.
-- 4) Собирает простую витрину в DM: сколько клиентов в каком статусе по дням.
--
-- Как запускать:
-- - для первого знакомства можно запускать файл целиком;
-- - если хотите потренировать инкремент (п.3): добавьте свои события -> запустите блок 3 ещё раз.
--
-- Важно:
-- - здесь часто используется TRUNCATE (полная очистка), чтобы было легко повторять домашку;
-- - в реальном DWH так делают не всегда, но для обучения это удобнее.
--
-- Предусловия (DDL + данные в STG):
-- 1) dwh-modeling/sql/01_ddl_stg-dds.sql
-- 2) dwh-modeling/sql/02_dml_stg-dds.sql (нужен dim_date)
-- 3) dwh-modeling/sql/05_ddl_dm.sql
-- 4) dwh-modeling/sql/07_ddl_hw_customer_status.sql
-- 5) stg.customer_status_raw заполнена (см. dwh-modeling/Homework_Customer_Status_DDS_DM.md)
-- ===============================================
-- ==========================================================
-- 1) ODS: очистка и типизация (full refresh)
-- ==========================================================
-- Идея:
-- - STG хранит "как пришло" (обычно TEXT);
-- - ODS хранит "аккуратно": правильные типы + простая чистка.
-- Для простоты пересобираем ODS с нуля.
--
-- Обратите внимание: ods.customer_status хранит ВСЕ события (PK = customer_id + event_ts),
-- а не только последнее состояние, как ods.customers (PK = customer_id).
-- Причина: источник данных здесь - поток событий ("статус стал X в момент Y"),
-- а не снимок ("вот текущие данные клиента"). ODS сохраняет природу источника:
-- снимок остаётся снимком, события остаются событиями.
-- Благодаря этому full backfill SCD2 (блок 2) строится прямо из ODS, а не из STG.
TRUNCATE ods.customer_status;
INSERT INTO ods.customer_status (
customer_id, status, event_ts, _load_id, _load_ts
)
SELECT
s.customer_id::INT AS customer_id,
NULLIF(trim(s.status), '') AS status,
NULLIF(trim(s.event_ts), '')::TIMESTAMP AS event_ts,
s._load_id,
COALESCE(s._load_ts, now()) AS _load_ts
FROM stg.customer_status_raw s
WHERE s.customer_id ~ '^\d+$'
AND NULLIF(trim(s.event_ts), '') IS NOT NULL
AND NULLIF(trim(s.status), '') IS NOT NULL;
-- Проверка: что получилось в ODS
SELECT 'ods.customer_status count = ' || COUNT(*) FROM ods.customer_status;
SELECT * FROM ods.customer_status ORDER BY customer_id, event_ts;
-- ==========================================================
-- 2) DDS: начальная загрузка SCD2 (full refresh)
-- ==========================================================
-- Идея SCD2 простыми словами:
-- - одна строка = один период, когда статус был одним и тем же;
-- - valid_from = с какого дня статус "начался";
-- - valid_to = с какого дня статус "закончился" (NULL = текущий статус);
-- - интервалы считаем так: [valid_from, valid_to) (valid_to не включаем).
-- - чтобы найти статус "на дату D":
-- D >= valid_from AND (valid_to IS NULL OR D < valid_to)
--
-- Упрощение для домашки:
-- - считаем, что у клиента нет двух разных смен статуса в один день.
TRUNCATE dds.dim_customer_status;
WITH src AS (
-- src: события из ODS + "контрольная сумма" статуса.
-- Так проще проверять, поменялся статус или остался тем же.
SELECT
customer_id AS customer_bk,
status,
event_ts,
md5(lower(coalesce(status, ''))) AS hashdiff
FROM ods.customer_status
),
ordered AS (
-- ordered: для каждого клиента смотрим "какая версия была до этого" (LAG)
SELECT
*,
lag(hashdiff) OVER (
PARTITION BY customer_bk
ORDER BY event_ts
) AS prev_hash
FROM src
),
changes AS (
-- changes: оставляем только первое состояние и реальные изменения статуса
SELECT *
FROM ordered
WHERE prev_hash IS DISTINCT FROM hashdiff OR prev_hash IS NULL
),
framed AS (
-- framed: превращаем изменения в периоды (valid_to = дата следующего события через LEAD)
SELECT
customer_bk,
status,
hashdiff,
event_ts::DATE AS valid_from,
lead(event_ts::DATE) OVER (
PARTITION BY customer_bk
ORDER BY event_ts
) AS valid_to
FROM changes
)
INSERT INTO dds.dim_customer_status (
customer_bk, status, hashdiff,
valid_from, valid_to,
created_at, updated_at
)
SELECT
customer_bk, status, hashdiff,
valid_from, valid_to,
now(), now()
FROM framed
ORDER BY customer_bk, valid_from;
-- Проверка: периоды в DDS (у клиента 101 должно быть 4 строки: new -> active -> vip -> churned)
SELECT 'dim_customer_status count = ' || COUNT(*) FROM dds.dim_customer_status;
SELECT * FROM dds.dim_customer_status ORDER BY customer_bk, valid_from;
-- ==========================================================
-- 3) Инкрементальная загрузка: STG -> ODS -> DDS
-- ==========================================================
-- Имитируем приход новой порции событий (customer_status_events_increment.csv):
-- - клиент 101: churned -> active (вернулся)
-- - клиент 102: churned -> active
-- - клиент 103: new -> active
-- - клиент 104: новый клиент, статус new
-- 3.0) Новые события в STG
-- При повторном запуске эти строки добавятся в STG ещё раз (дубли).
-- Для демо это не страшно: ODS-вставка ниже использует ON CONFLICT DO NOTHING,
-- а SCD2-блок защищён от повторных вставок через NOT EXISTS.
-- В продакшене STG обычно очищается перед каждой загрузкой (TRUNCATE / партиция по дате).
INSERT INTO stg.customer_status_raw (customer_id, status, event_ts, _load_id, _load_ts) VALUES
('101','active','2024-11-15 09:00:00','batch_20241115_1000','2024-11-15 10:00:00'),
('102','active','2024-05-05 09:30:00','batch_20240505_1000','2024-05-05 10:00:00'),
('103','active','2024-03-20 12:00:00','batch_20240320_1300','2024-03-20 13:00:00'),
('104','new', '2024-06-01 08:00:00','batch_20240601_0900','2024-06-01 09:00:00');
-- 3.1) UPSERT в ODS: добавляем новые события (не трогаем старые)
-- PK в ods.customer_status = (customer_id, event_ts), поэтому каждое уникальное
-- событие встаёт отдельной строкой. Дубли (одинаковый customer_id + event_ts) игнорируем.
INSERT INTO ods.customer_status (
customer_id, status, event_ts, _load_id, _load_ts
)
SELECT
s.customer_id::INT,
NULLIF(trim(s.status), ''),
NULLIF(trim(s.event_ts), '')::TIMESTAMP,
s._load_id,
COALESCE(s._load_ts, now())
FROM stg.customer_status_raw s
WHERE s.customer_id ~ '^\d+$'
AND NULLIF(trim(s.event_ts), '') IS NOT NULL
AND NULLIF(trim(s.status), '') IS NOT NULL
ON CONFLICT (customer_id, event_ts) DO NOTHING;
-- Проверка: в ODS должны появиться новые строки
SELECT 'ods.customer_status after increment = ' || COUNT(*) FROM ods.customer_status;
-- 3.2) Инкрементальное обновление DDS (SCD2)
-- Идея:
-- 1) берём по каждому клиенту самое позднее событие из ODS;
-- 2) сравниваем с текущей версией в DDS (valid_to IS NULL);
-- 3) если статус изменился - закрываем старую версию и вставляем новую.
--
-- Ограничение учебного варианта:
-- - если добавили событие "задним числом" со старой датой, этот блок не пересоберёт всю историю.
-- Для такого кейса обычно делают full refresh (блок 2).
BEGIN;
-- 3.2a) Закрываем предыдущую актуальную версию
WITH ranked AS (
-- ranked: выбираем "самое свежее" событие на клиента
SELECT
customer_id AS customer_bk,
status,
event_ts::DATE AS eff_date,
md5(lower(coalesce(status, ''))) AS hashdiff,
row_number() OVER (
PARTITION BY customer_id
ORDER BY event_ts DESC, _load_ts DESC
) AS rn
FROM ods.customer_status
WHERE event_ts IS NOT NULL
),
delta AS (
-- delta: ровно одна строка на клиента (самое свежее событие)
SELECT * FROM ranked WHERE rn = 1
),
current_ver AS (
-- current_ver: текущие версии в DDS (valid_to IS NULL)
SELECT d.*
FROM dds.dim_customer_status d
WHERE d.valid_to IS NULL
)
UPDATE dds.dim_customer_status d
SET valid_to = x.eff_date,
updated_at = now()
FROM (
-- x: кого "закрываем":
-- клиент уже есть в DDS, и статус действительно изменился.
SELECT
t.customer_bk,
t.eff_date,
c.customer_status_sk
FROM delta t
JOIN current_ver c
ON c.customer_bk = t.customer_bk
WHERE c.hashdiff <> t.hashdiff
AND t.eff_date > c.valid_from -- не создаём период нулевой/отрицательной длины
) x
WHERE d.customer_status_sk = x.customer_status_sk
AND d.valid_to IS NULL;
-- 3.2b) Вставляем новую версию
WITH ranked AS (
-- ranked/delta/current_ver повторяем отдельно, чтобы блок INSERT читался отдельно от UPDATE
SELECT
customer_id AS customer_bk,
status,
event_ts::DATE AS eff_date,
md5(lower(coalesce(status, ''))) AS hashdiff,
row_number() OVER (
PARTITION BY customer_id
ORDER BY event_ts DESC, _load_ts DESC
) AS rn
FROM ods.customer_status
WHERE event_ts IS NOT NULL
),
delta AS (
SELECT * FROM ranked WHERE rn = 1
),
current_ver AS (
SELECT d.*
FROM dds.dim_customer_status d
WHERE d.valid_to IS NULL
),
to_insert AS (
-- to_insert: кого "вставляем":
-- 1) новый клиент (в current_ver нет строки);
-- 2) изменившийся клиент (статус поменялся).
SELECT
t.customer_bk,
t.status,
t.hashdiff,
t.eff_date
FROM delta t
LEFT JOIN current_ver c
ON c.customer_bk = t.customer_bk
WHERE c.customer_status_sk IS NULL
OR (c.hashdiff <> t.hashdiff AND t.eff_date > c.valid_from)
)
INSERT INTO dds.dim_customer_status (
customer_bk, status, hashdiff,
valid_from, valid_to,
created_at, updated_at
)
SELECT
t.customer_bk, t.status, t.hashdiff,
t.eff_date, NULL,
now(), now()
FROM to_insert t
-- защита от повторного запуска: не вставляем одну и ту же версию (BK + valid_from) второй раз
WHERE NOT EXISTS (
SELECT 1
FROM dds.dim_customer_status d
WHERE d.customer_bk = t.customer_bk
AND d.valid_from = t.eff_date
);
COMMIT;
-- Проверка: у клиента 101 должна появиться 5-я строка (active с 2024-11-15),
-- у 104 - первая строка (new с 2024-06-01)
SELECT 'dim_customer_status after increment = ' || COUNT(*) FROM dds.dim_customer_status;
SELECT * FROM dds.dim_customer_status ORDER BY customer_bk, valid_from;
-- ==========================================================
-- 4) DM: витрина статусов клиентов по датам (full refresh)
-- ==========================================================
-- Витрина "снимок на дату":
-- для каждого дня считаем, сколько клиентов было в каждом статусе.
-- Берём календарь dds.dim_date и подбираем статус по периоду valid_from/valid_to.
-- DDL витрины - в 07_ddl_hw_customer_status.sql.
TRUNCATE dm.mart_customer_status_daily;
WITH bounds AS (
SELECT
min(valid_from) AS date_from,
-- CURRENT_DATE для открытых интервалов (valid_to IS NULL = текущий статус),
-- иначе витрина не покроет даты после последней смены статуса.
-- Нюанс: количество строк в витрине зависит от даты запуска (каждый
-- день добавляется ещё один день). Для учебных целей это приемлемо.
max(coalesce(valid_to, CURRENT_DATE)) AS date_to
FROM dds.dim_customer_status
)
INSERT INTO dm.mart_customer_status_daily (
date_actual, status, customers_cnt
)
SELECT
d.date_actual,
s.status,
COUNT(DISTINCT s.customer_bk) AS customers_cnt
FROM dds.dim_date d
JOIN bounds b
ON d.date_actual BETWEEN b.date_from AND b.date_to
JOIN dds.dim_customer_status s
ON d.date_actual >= s.valid_from
AND (s.valid_to IS NULL OR d.date_actual < s.valid_to)
GROUP BY d.date_actual, s.status
ORDER BY d.date_actual, s.status;
-- Проверка: выборочные даты из витрины
SELECT 'mart_customer_status_daily count = ' || COUNT(*) FROM dm.mart_customer_status_daily;
SELECT *
FROM dm.mart_customer_status_daily
WHERE date_actual IN ('2024-01-15', '2024-04-10', '2024-09-15', '2024-12-01')
ORDER BY date_actual, status;
+88
View File
@@ -0,0 +1,88 @@
site_name: "DE Roadmap — Data Engineering с нуля до middle"
site_url: https://de.dementev.space/
site_description: "Роадмап по Data Engineering: SQL, Python, Airflow, Greenplum и далее"
site_author: Dmitry Dementev
repo_url: https://github.com/dementev-dev/de-roadmap
repo_name: dementev-dev/de-roadmap
docs_dir: .
site_dir: site
exclude_docs: |
project/
postgres-bookings/
.github/
.claude/
site/
AGENTS.md
CLAUDE.md
COMMIT_RULES.md
LICENSE
.gitignore
mkdocs.yml
dwh-modeling/TODO.md
overrides/
nav:
- Роадмап: README.md
- Моделирование данных:
- Введение: dwh-modeling/README.md
- SCD: dwh-modeling/SCD.md
- Data Vault: dwh-modeling/DataVault.md
- "Домашка: STG → DDS → DM": dwh-modeling/Homework_Customer_Status_DDS_DM.md
- Разработка с ИИ:
- Введение: ai-dev/README.md
- Лучшие практики: ai-dev/best-practice.md
- Механизм памяти: ai-dev/memory-mechanism.md
theme:
name: material
custom_dir: overrides
language: ru
palette:
- scheme: default
primary: indigo
accent: indigo
toggle:
icon: material/brightness-7
name: Тёмная тема
- scheme: slate
primary: indigo
accent: indigo
toggle:
icon: material/brightness-4
name: Светлая тема
features:
- navigation.top
- navigation.tracking
- search.suggest
- search.highlight
- toc.follow
- content.code.copy
markdown_extensions:
- toc:
slugify: !!python/object/apply:pymdownx.slugs.slugify
kwds:
case: lower
permalink: true
- admonition
- pymdownx.details
- pymdownx.superfences
- pymdownx.highlight
- attr_list
extra:
social:
- icon: fontawesome/brands/telegram
link: https://t.me/dementev_dev
name: Написать в Telegram
- icon: fontawesome/brands/github
link: https://github.com/dementev-dev/de-roadmap
name: GitHub
plugins:
- same-dir
- search:
lang: ru
+70
View File
@@ -0,0 +1,70 @@
{% extends "base.html" %}
{% block extrahead %}
<style>
.tg-fab {
position: fixed;
bottom: 1.6rem;
left: 1.6rem;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
width: 3.2rem;
height: 3.2rem;
border-radius: 50%;
background: #29b6f6;
color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, .25);
transition: transform .2s, box-shadow .2s, background .2s;
text-decoration: none;
}
.tg-fab:hover {
transform: scale(1.12);
box-shadow: 0 4px 16px rgba(0, 0, 0, .3);
background: #039be5;
color: #fff;
}
@media screen and (max-width: 76.25em) {
.tg-fab {
width: 2.8rem;
height: 2.8rem;
bottom: 1.2rem;
left: 1.2rem;
}
.tg-fab svg {
width: 24px;
height: 24px;
}
}
</style>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108294923', 'ym');
ym(108294923, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108294923" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
{% endblock %}
{% block content %}
{{ super() }}
<a
href="https://t.me/dementev_dev"
target="_blank"
rel="noopener"
class="tg-fab"
title="Написать в Telegram"
aria-label="Написать в Telegram"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="28" height="28" fill="currentColor">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.479.33-.913.492-1.302.48-.428-.012-1.252-.242-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
</svg>
</a>
{% endblock %}
+19 -1
View File
@@ -1,2 +1,20 @@
#!/bin/bash #!/bin/bash
docker compose exec -it db psql -U ${POSTGRES_USER:-postgres} -d demo set -euo pipefail
# Usage:
# ./psql_sh # interactive psql inside container
# ./psql_sh -c "SELECT 1;" # run a command
# cat file.csv | ./psql_sh -c "\\copy ... FROM STDIN WITH (FORMAT csv, HEADER true)"
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
compose_file="${script_dir}/docker-compose.yml"
compose() {
docker compose -f "${compose_file}" --project-directory "${script_dir}" "$@"
}
if [ -t 0 ]; then
compose exec -it db psql -U "${POSTGRES_USER:-postgres}" -d demo "$@"
else
compose exec -T db psql -U "${POSTGRES_USER:-postgres}" -d demo "$@"
fi
+350
View File
@@ -0,0 +1,350 @@
# ADR: Архитектура сайта de-roadmap
> Архитектурный документ. Проектные цели и требования — в [PRD](./PRD.md).
---
## 1. Ключевые архитектурные требования
Из PRD вытекают четыре жёстких ограничения, определяющих выбор инструментов:
1. **Сайт опционален.** Репозиторий должен полноценно работать без сайта. Удалили конфиг — всё как было.
2. **Dual-compatible links.** Внутренние ссылки между `.md` файлами должны работать и на GitHub, и на сайте.
3. **Один файл = один источник правды.** Не должно быть копий или генерируемых `.md`. Редактируем один раз — рендерится везде.
4. **Zero-effort deploy.** Push в `main` → сайт обновился. Без ручных шагов, без локальной сборки.
---
## 2. Выбор генератора статического сайта
### Рассмотренные варианты
| Критерий | MkDocs Material | Docusaurus | Hugo | Astro |
|----------|----------------|------------|------|-------|
| Язык/экосистема | Python | Node.js (React) | Go | Node.js |
| Порог входа | Минимальный — `pip install`, один YAML | Средний — npm, React-компоненты | Средний — Go templates | Высокий — фреймворк |
| Поддержка Markdown «как есть» | Отличная, расширения через плагины | Хорошая, но MDX-ориентирован | Хорошая, но shortcodes вместо стандартного MD | Хорошая |
| Навигация по длинной странице | TOC sidebar из коробки | Есть, но заточен под многостраничность | Зависит от темы | Зависит от темы |
| Поиск | Встроенный (lunr.js), работает offline | Algolia (внешний сервис) или плагин | Нет из коробки | Нет из коробки |
| Тёмная тема | Из коробки, переключатель | Из коробки | Зависит от темы | Зависит от темы |
| GitHub Pages деплой | Одна команда / готовый Action | Готовый Action | Готовый Action | Готовый Action |
| Dual-compatible links | Поддерживает с настройкой `use_directory_urls` | Преобразует ссылки, может ломать GitHub | Преобразует ссылки | Преобразует ссылки |
| Один мейнтейнер, Python-стек | ✅ Идеально | ❌ Node.js | ⚠️ Go, но бинарник | ❌ Node.js |
### Решение: MkDocs Material
**Почему:**
- Python-based — совпадает со стеком автора, `pip install` и готово.
- Лучшая из коробки поддержка длинных страниц с TOC — именно наш сценарий.
- Встроенный поиск без внешних сервисов.
- Самый простой конфиг — один `mkdocs.yml`.
- Крупнейшее комьюнити среди генераторов документации, активно развивается.
- Dual-compatible links решаемы (см. раздел 4).
**Почему не Docusaurus:** Node.js-зависимость, ориентирован на многостраничные доки с MDX-компонентами — overkill для «красивый рендер Markdown». Преобразование ссылок может конфликтовать с GitHub-форматом.
**Почему не Hugo:** Быстрый, но Go-шаблоны сложнее отлаживать. Нет встроенного поиска. Для нашего объёма контента скорость сборки не критична.
**Почему не Astro:** Полноценный веб-фреймворк — избыточен. Подошёл бы, если бы мы строили маркетинговый сайт с интерактивом, но это не текущая цель.
---
## 3. Структура файлов
### Решение открытого вопроса: `docs/` vs корень репо
**Решение: `docs_dir: .` (корень репо = корень сайта), с исключениями.**
Причина: не создаём отдельную папку `docs/`, не дублируем и не перемещаем файлы. MkDocs умеет работать с корнем репо как источником, исключая ненужное.
```yaml
# mkdocs.yml (в корне репо)
docs_dir: .
```
### Решение открытого вопроса: README.md как index
**Решение: плагин `awesome-pages` или конфигурация `nav` с явным указанием.**
MkDocs по умолчанию ищет `index.md`. Но мы хотим сохранить `README.md` (GitHub его рендерит на главной репо). Есть два пути:
- **Вариант A:** Симлинк `docs/index.md → ../README.md`. Но мы не используем `docs/`.
- **Вариант B (выбран):** В `mkdocs.yml` явно указать `README.md` как главную:
```yaml
nav:
- Роадмап: README.md
- Моделирование данных:
- Введение: dwh-modeling/README.md
- SCD: dwh-modeling/SCD.md
- Data Vault: dwh-modeling/DataVault.md
- "Домашка: STG → DDS → DM": dwh-modeling/Homework_Customer_Status_DDS_DM.md
```
MkDocs Material корректно обрабатывает `README.md` файлы — рендерит их как `index.html` соответствующей директории.
### Исключения из сборки
Файлы и папки, которые не должны попасть на сайт:
```yaml
exclude_docs: |
project/ # проектная документация (PRD, ADR)
postgres-bookings/ # скрипты стенда (не контент сайта)
AGENTS.md # конфигурация для AI-агентов
LICENSE # лицензия (есть в footer)
.gitignore
```
### Итоговая структура репо
```
de-roadmap/
├── mkdocs.yml ← конфиг сайта (единственный новый файл в корне)
├── README.md ← главная страница сайта И главная страница GitHub
├── dwh-modeling/
│ ├── README.md ← подстраница «Введение в DWH»
│ ├── SCD.md ← подстраница
│ ├── DataVault.md ← подстраница
│ └── Homework_*.md ← подстраница
├── postgres-bookings/ ← исключён из сайта
├── project/ ← PRD, ADR — исключены из сайта
│ ├── PRD.md
│ └── ADR.md
├── .github/
│ └── workflows/
│ └── deploy-site.yml ← CI/CD pipeline
├── AGENTS.md ← исключён из сайта
└── LICENSE
```
---
## 4. Стратегия ссылок (Dual Compatibility)
### Проблема
GitHub рендерит ссылки вида `[текст](dwh-modeling/SCD.md)` как переход к файлу.
MkDocs по умолчанию преобразует `.md``.html` и может менять структуру URL.
### Решение
Комбинация настроек MkDocs:
```yaml
use_directory_urls: true # /dwh-modeling/SCD/ вместо /dwh-modeling/SCD.html
```
И ссылки в Markdown пишем **всегда как относительные пути к `.md` файлам**:
```markdown
<!-- Это работает и на GitHub, и в MkDocs -->
[Теория про SCD](dwh-modeling/SCD.md)
[Введение в DWH](dwh-modeling/README.md)
```
MkDocs Material автоматически резолвит `.md` ссылки в правильные URL сайта.
### Кириллические якоря
GitHub генерирует якоря из кириллических заголовков с URL-encoding:
`## Базы данных``#базы-данных`
MkDocs Material по умолчанию делает то же самое через расширение `toc`:
```yaml
markdown_extensions:
- toc:
slugify: !!python/object/apply:pymdownx.slugs.slugify
kwds:
case: lower
permalink: true
```
**Риск:** поведение может различаться на edge cases (спецсимволы, эмодзи в заголовках). Митигация — на этапе MVP протестировать все существующие якорные ссылки в README.
---
## 5. CI/CD Pipeline
### GitHub Actions Workflow
```yaml
# .github/workflows/deploy-site.yml
name: Deploy MkDocs to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch: # ручной запуск для отладки
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.x'
- run: pip install mkdocs-material
- run: mkdocs build --strict
# --strict: падает на warnings (битые ссылки, отсутствующие файлы)
- uses: actions/upload-pages-artifact@v3
with:
path: site/
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
```
### Что даёт `--strict`
Сборка упадёт, если:
- Есть ссылка на несуществующий `.md` файл.
- Есть битый якорь.
- Есть warning от MkDocs.
Это наш автотест ссылок — бесплатно, в CI.
---
## 6. Конфигурация MkDocs Material
### Минимальный `mkdocs.yml` для MVP
```yaml
site_name: "DE Roadmap — Data Engineering с нуля до middle"
site_url: https://dementev-dev.github.io/de-roadmap/
site_description: "Роадмап по Data Engineering: SQL, Python, Airflow, Greenplum и далее"
site_author: Dmitry Dementev
repo_url: https://github.com/dementev-dev/de-roadmap
repo_name: dementev-dev/de-roadmap
docs_dir: .
site_dir: site
# Исключаем из сборки
exclude_docs: |
project/
postgres-bookings/
AGENTS.md
LICENSE
.gitignore
.github/
site/
nav:
- Роадмап: README.md
- Моделирование данных:
- Введение: dwh-modeling/README.md
- SCD: dwh-modeling/SCD.md
- Data Vault: dwh-modeling/DataVault.md
- "Домашка: STG → DDS → DM": dwh-modeling/Homework_Customer_Status_DDS_DM.md
theme:
name: material
language: ru
palette:
- scheme: default
primary: indigo
accent: indigo
toggle:
icon: material/brightness-7
name: Тёмная тема
- scheme: slate
primary: indigo
accent: indigo
toggle:
icon: material/brightness-4
name: Светлая тема
features:
- navigation.top # кнопка «наверх»
- navigation.tracking # URL обновляется при скролле
- search.suggest # подсказки в поиске
- search.highlight # подсветка найденного
- toc.follow # TOC следит за скроллом
- content.code.copy # кнопка копирования кода
markdown_extensions:
- toc:
permalink: true
- admonition # сворачиваемые блоки (Спринт 2)
- pymdownx.details # для <details>
- pymdownx.superfences # вложенные блоки кода
- pymdownx.highlight # подсветка синтаксиса
- attr_list # атрибуты для элементов
plugins:
- search:
lang: ru
```
### Что уже включено в MVP (бесплатно с Material)
- Тёмная/светлая тема с переключателем.
- Полнотекстовый поиск на русском.
- TOC (оглавление) в правом sidebar.
- Кнопка «наверх» на длинной странице.
- URL обновляется при скролле (можно дать ссылку на конкретный раздел).
- Подсветка синтаксиса в блоках кода.
- Кнопка копирования кода.
- Ссылка на GitHub-репозиторий в шапке.
---
## 7. Кастомный домен (Спринт 2)
Домен `dementev.space` уже есть. Для подключения:
1. Создать CNAME-запись: `roadmap.dementev.space → dementev-dev.github.io`.
2. Добавить файл `CNAME` в корень репо с содержимым `roadmap.dementev.space`.
3. В `mkdocs.yml` обновить `site_url`.
4. Включить HTTPS в настройках GitHub Pages.
---
## 8. Риски и митигации
| Риск | Митигация |
|------|-----------|
| `docs_dir: .` подхватывает лишние файлы | `exclude_docs` со списком исключений; `--strict` ловит проблемы |
| Кириллические якоря ведут себя по-разному | Тестируем все якорные ссылки из README при первом деплое |
| `README.md` содержит GitHub-специфичный синтаксис | Проверяем рендер; при необходимости используем MkDocs-совместимые альтернативы |
| Зависимость от `mkdocs-material` | Активный проект (30k+ stars), Python-пакет; при необходимости — заморозить версию в `requirements.txt` |
| `exclude_docs` — относительно новая фича MkDocs | Альтернатива: `.mkdocsignore` или плагин `mkdocs-exclude`; протестировать на MVP |
---
## 9. Порядок действий (Спринт 1)
1. Создать ветку `feature/site`.
2. Добавить `mkdocs.yml` в корень репо.
3. Добавить `.github/workflows/deploy-site.yml`.
4. Добавить `project/PRD.md` и `project/ADR.md`.
5. Проверить локально: `pip install mkdocs-material && mkdocs serve`.
6. Протестировать все внутренние ссылки и якоря.
7. При необходимости — адаптировать ссылки для dual compatibility.
8. Merge в `main` → автодеплой → проверить `https://dementev-dev.github.io/de-roadmap/`.
9. Включить GitHub Pages (Settings → Pages → Source: GitHub Actions).
+169
View File
@@ -0,0 +1,169 @@
# PRD: Сайт для de-roadmap
> Проектный документ. Техническая архитектура — в отдельном ADR.
---
## 1. Контекст и мотивация
### Текущее состояние
Роадмап по Data Engineering живёт как GitHub-репозиторий ([dementev-dev/de-roadmap](https://github.com/dementev-dev/de-roadmap)):
- Основной контент — монолитный `README.md` (~700 строк) с полным учебным планом.
- Дополнительные материалы — в подпапках (`dwh-modeling/`, `postgres-bookings/`): теория DWH-моделирования, SCD, Data Vault, домашние задания, скрипты.
- 109 коммитов, контент активно развивается.
- Основной поток менти приходит через маркетплейс ОМ (Осознанная Меркантильность) — платформу менторства с системой отзывов, ранжированием менторов и модерацией. Участие платное (абонентская плата + аукцион за позицию в выдаче).
- Роадмап изначально создавался для себя — как конспект подходов к обучению. Со временем стал использоваться и для менти (удобная структура прохождения), и на ознакомительных созвонах с лидами из ОМ (демонстрация программы и подхода к обучению).
- Отдельного маркетингового продвижения роадмапа не было, органических приходов «по ссылке из интернета» — ноль. Сайта ментора тоже пока нет — приходить некуда.
### Проблема
GitHub README — рабочий, но не презентабельный формат:
- Нет удобной навигации по длинному документу (только ручной скролл или оглавление-ссылки).
- Выглядит как «техническая документация», а не как продукт ментора.
- GitHub README плохо индексируется поисковыми системами — роадмап в таком виде даже потенциально не может привлекать органический трафик и «продавать сам себя».
- Неудобно давать ссылку потенциальному менти — GitHub-интерфейс отвлекает (issues, commits, файловое дерево).
- Нет мобильной адаптации контента (GitHub на телефоне — страдание).
### Чего НЕ решаем этим проектом
- **Лидогенерацию и воронку продаж.** Основной поток тёплых лидов идёт через маркетплейс ОМ, и конкурировать с ним по объёму нереалистично — даже топовые менторы не добирают сравнимое количество лидов из других источников. В перспективе сайт может стать элементом маркетинговой воронки (Habr → сайт → менторство), но это отдалённая цель, не влияющая на текущие решения. Сейчас сайт — витрина экспертизы, а не маркетинговый инструмент.
- **Масштабирование менторского бизнеса.** Сейчас приоритет — выпустить текущих менти, а не набрать новых.
- **Создание LMS / интерактивного курса.** Формат остаётся «структурированный текст + ссылки», без трекинга прогресса и автопроверок.
---
## 2. Цели
| # | Цель | Метрика успеха |
|---|------|----------------|
| 1 | Удобная читаемая версия роадмапа | Сайт с навигацией, поиском, мобильной версией |
| 2 | Репозиторий остаётся полностью рабочим без сайта | Все `.md` файлы читаемы на GitHub, ссылки между ними работают |
| 3 | Минимальные накладные расходы на поддержку | Обновил `.md` → запушил → сайт обновился автоматически |
| 4 | Презентабельный вид для демонстрации на созвонах | Ссылка, которую удобно показать лиду из ОМ при обсуждении программы |
---
## 3. Целевая аудитория
**Основная:** менти (текущие и потенциальные) — джуны и переходящие в DE из смежных областей. Приходят по рекомендации из ОМ или по прямой ссылке от ментора. Им нужно быстро оценить объём программы и найти нужный раздел.
**Вторичная:** коллеги-инженеры, которые наткнулись на репозиторий через GitHub/поиск. Для них важна полнота контента и техническая достоверность, а не «продающие» элементы.
---
## 4. Требования
### 4.1. Обязательные (MVP)
**Контент и структура:**
- [x] Главная страница сайта = полный текст текущего `README.md` (одна длинная страница, не разбиваем).
- [x] Подстраницы из существующих `.md` файлов (`dwh-modeling/README.md`, `SCD.md`, `DataVault.md`, домашки).
- [x] Автоматическое оглавление (Table of Contents) по заголовкам H2/H3 на главной странице.
- [x] Внутренние ссылки работают и на GitHub, и на сайте (dual-compatible links).
**Навигация:**
- [x] Боковая панель с разделами сайта (основной README + подразделы).
- [x] Оглавление текущей страницы (правый sidebar / TOC).
- [x] Полнотекстовый поиск по сайту.
**Деплой:**
- [x] Автоматическая сборка и публикация при пуше в `main`.
- [x] Бесплатный хостинг (GitHub Pages).
**Совместимость с репо:**
- [x] Все `.md` файлы остаются читаемыми на GitHub «как есть».
- [x] `README.md` в корне репо продолжает рендериться на главной странице репозитория.
- [x] Добавление сайта не ломает существующую структуру — если удалить конфиг сайта, репо работает как раньше.
### 4.2. Желательные (Спринт 2+)
- [x] Кастомный домен: `de.dementev.space` (подключён 2026-03-27).
- [x] Тёмная тема (переключатель light/dark).
- [x] Сворачиваемые блоки (`<details>`) — точечно, для подсказок/решений в домашках (2026-03-29).
- [x] Кнопка «Написать в Telegram» — floating-кнопка + иконки в футере (2026-03-29).
- [ ] Иконки/бейджи для статуса разделов (пройден / в процессе / не начат) — пока декоративные, без бэкенда.
- [x] Яндекс.Метрика — счётчик 108294923, вебвизор, карта кликов (2026-03-29).
### 4.3. Явно НЕ делаем
- Разбивку основного README на отдельные страницы.
- Интерактивный трекинг прогресса менти.
- Систему авторизации / личный кабинет.
- Блог или раздел новостей (для этого есть Telegram-канал).
- SEO-оптимизацию и маркетинговые landing pages.
---
## 5. Ограничения
- **Бюджет: 0 ₽** (кроме домена, если решим подключить кастомный — `dementev.space` уже есть).
- **Время на поддержку: минимальное.** Рабочий процесс = «редактирую `.md` → push → сайт обновляется». Не должно быть отдельного шага сборки, ручного деплоя или npm-зависимостей, требующих обновления.
- **Стек автора: Python-first.** Решение на Python-тулинге предпочтительнее, чем на Node.js/Ruby (проще отлаживать при необходимости).
- **Один мейнтейнер.** Сайт поддерживает один человек — сложность решения должна быть соответствующей.
---
## 6. Итерации
### Спринт 1 — MVP: «Сайт, который просто работает» ✅ (2026-03-27)
**Scope:**
- Конфигурация генератора статического сайта.
- CI/CD pipeline (GitHub Actions → GitHub Pages).
- Главная страница = README.
- Подстраницы из `dwh-modeling/`.
- Фикс внутренних ссылок для dual compatibility.
- Адаптация Markdown для Python-Markdown (пустые строки перед списками, 4-пробельные отступы, заголовки `#``##`).
**Definition of Done:**
- ~~Сайт доступен по URL `https://dementev-dev.github.io/de-roadmap/`.~~`https://de.dementev.space/`
- Все ссылки внутри README работают и на сайте, и на GitHub.
- При пуше в `main` сайт автоматически пересобирается за < 2 минут (факт: ~34 сек).
- Контент на GitHub выглядит ровно так же, как до добавления сайта.
### Спринт 2 — «Удобство и навигация» (в процессе)
**Scope:**
- ~~Кастомный домен.~~`de.dementev.space` (2026-03-27)
- ~~Тёмная тема.~~ ✅ из коробки Material (2026-03-27)
- ~~Сворачиваемые блоки.~~`<details>` точечно в домашках (2026-03-29)
- ~~Кнопка «Написать ментору» (Telegram).~~ ✅ floating + футер (2026-03-29)
- ~~Базовая аналитика.~~ ✅ Яндекс.Метрика (2026-03-29)
### Спринт 3 — «Контент и визуал» (по необходимости)
**Scope:**
- Визуальная карта роадмапа (интерактивная диаграмма прохождения).
- Страница «О менторе» / «Отзывы выпускников» (когда будут выпускники).
- Расширение контента: новые разделы роадмапа → автоматически появляются на сайте.
---
## 7. Риски
| Риск | Вероятность | Влияние | Митигация |
|------|-------------|---------|-----------|
| Ссылки ломаются при конвертации (GitHub vs сайт) | Средняя | Высокое | Стратегия dual-compatible links в ADR; автотест ссылок в CI |
| Кириллические якоря рендерятся по-разному | Средняя | Среднее | Тестирование конкретного генератора; при необходимости — латинские id |
| Генератор сайта перестаёт поддерживаться | Низкая | Среднее | Контент в plain Markdown — миграция на другой генератор за день |
| Накладные расходы на поддержку растут | Низкая | Среднее | Принцип «сайт опционален»: если мешает — удаляем конфиг, репо работает |
| GitHub Pages ограничения (bandwidth, размер) | Очень низкая | Низкое | Для статического сайта с текстом — не актуально |
---
## 8. Открытые вопросы
1. **Домен.** Использовать GitHub Pages URL или сразу подключить кастомный домен? Можно решить в Спринте 2.
2. **README.md в корне vs docs/.** Оставляем README.md в корне (GitHub его рендерит) и используем его же как `index.md` для сайта, или делаем симлинк / копию? → Решение в ADR.
3. **Структура `docs/` директории.** Нужно ли вообще создавать `docs/`, или генератор может работать прямо с корнем репо? → Решение в ADR.
+17
View File
@@ -0,0 +1,17 @@
# TODO: общие задачи проекта
## Контент
- [ ] **Перенос учебника Airflow в de-roadmap.**
Перенести 9 глав учебника из `airflow-manual` в `airflow/` (de-roadmap).
В `airflow-manual` оставить только стенд (`airflow-docker/`).
Добавить навигацию в `mkdocs.yml`, обеспечить dual-compatible links.
- [ ] **Убрать раздел «Понятие сложности алгоритмов» из README.**
Раздел поверхностный и не самостоятелен. План:
- Упоминание асимптотики (O(n) vs O(n²), pandas/списки) перенести в раздел Python.
- По SQL: либо короткая заметка про планы запросов, либо просто сослаться на курс QPT от Postgres Pro (он уже упомянут в разделе SQL) и не дублировать.
## Сайт
- [ ] **Иконки/бейджи статуса разделов** (пройден / в процессе / не начат) — декоративные, без бэкенда.