Author SHA1 Message Date
ddadmin 2a1da643c1 fix(site): сборка runner переведена со Snap uv на venv
- Зачем:
  - Snap uv не запускался внутри ограниченного systemd-сервиса без cap_dac_override.
- Что:
  - workflow переведён на постоянный Python venv с pinned requirements.
  - повторные сборки проверяют зависимости через pip без их переустановки.
  - Snap удалён из PATH runner, runbook дополнен python3-venv и описанием окружения.
- Проверка:
  - от имени gitea-runner дважды выполнены pip install, pip check и mkdocs build --strict; второй запуск переиспользовал окружение.
  - обновлённый systemd unit прошёл systemd-analyze verify и успешно перезапущен.
2026-08-04 16:15:30 -04:00
ddmitry 09f2c839f7 Merge pull request 'ci(site): добавлена публикация через Gitea Actions' (#2) from infra/gitea-vps-site-publishing into main
Deploy MkDocs to VPS / deploy (push) Failing after 3s
Reviewed-on: #2
2026-08-04 23:03:13 +03:00
ddmitry c5e9bfe6b3 Merge pull request 'docs(project): согласована публикация сайта через Gitea и VPS' (#1) from docs/gitea-vps-site-publishing into main
Deploy MkDocs to GitHub Pages / build (push) Canceled after 0s
Deploy MkDocs to GitHub Pages / deploy (push) Canceled after 0s
Reviewed-on: #1
2026-08-04 22:59:17 +03:00
ddadmin 1edda33f87 ci(site): добавлена публикация через Gitea Actions
- Зачем:
  - основной сайт не должен зависеть от заблокированной учётной записи GitHub.
- Что:
  - добавлены строгая сборка и атомарная публикация через repository-scoped runner.
  - добавлены воспроизводимые конфигурации systemd, nginx и эксплуатационный runbook.
  - проектная документация и ссылки на репозиторий обновлены для Gitea.
- Проверка:
  - выполнены mkdocs build --strict, Bash/YAML-проверки и локальный HTTP smoke-check.
  - runner зарегистрирован, ограничен средствами systemd и виден в Gitea как online.
2026-08-04 15:16:33 -04:00
ddadmin 8584fc225b docs(project): добавлена спецификация публикации сайта
- Зачем:
  - нужен согласованный вариант публикации сайта без зависимости от GitHub Pages.
- Что:
  - описан контур Gitea Actions, host runner, nginx и резерв на GitHub Pages.
  - в архитектурный документ добавлена ссылка на новую спецификацию.
- Проверка:
  - git diff --cached --check.
2026-08-04 10:01:58 -04:00
ddadminandClaude Fable 5 1ae670410b feat(docs): добавлен подраздел «Linux и терминал» в базовые инструменты
Deploy MkDocs to GitHub Pages / build (push) Canceled after 0s
Deploy MkDocs to GitHub Pages / deploy (push) Canceled after 0s
- Зачем:
  - единственный пробел заявленной базы: все стенды консольные, а в
    роадмапе не было ни одного упоминания bash/ssh/командной строки (P1).
- Что:
  - в блок «Git и базовые инструменты» добавлен подраздел с видео-интро,
    тремя статьями (навигация и grep, права файлов, ssh) и опциональным
    интерактивным курсом Hexlet; примечание про WSL для Windows.
  - критерии готовности блока дополнены терминалом, grep и правами/ssh;
    в оглавление добавлен Linux; задача перенесена в «Сделано» в TODO.
- Проверка:
  - mkdocs build --strict — сборка без ошибок;
  - все ссылки проверены на живость и соответствие содержимого
    (curl --noproxy для RU-доменов, покрытие видео — по субтитрам).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:46:46 +03:00
ddadminandClaude Fable 5 6983c25d10 ci(docs): добавлена еженедельная проверка внешних ссылок (lychee)
- Зачем:
  - в роадмапе ~100 внешних ссылок, репозиторий правится редко — ссылки
    гниют молча (пункт P1 из project/TODO.md).
- Что:
  - добавлен lychee.toml: игнор-лист доменов, режущих ботов (habr, stepik,
    leetcode, realpython), YouTube и t.me; исключены project/ и site/.
  - добавлен workflow check-links.yml: cron по понедельникам, при битых
    ссылках создаёт или обновляет issue с меткой link-check, keepalive
    защищает cron от отключения после 60 дней неактивности.
  - исправлена битая ссылка на доки datetime в README: перевод /ru/
    на docs.python.org отдаёт 404, заменена на английскую версию.
- Проверка:
  - docker run --rm -v "$PWD:/input" -w /input lycheeverse/lychee
    --no-progress './**/*.md' — 177 ссылок, 0 ошибок, 56 исключено.
  - mkdocs build --strict — без ошибок.
  - адверсариальное ревью субагентом, 2 раунда — APPROVED.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:01:45 +03:00
ddadminandClaude Fable 5 ad60d0c738 docs(project): TODO приоритизирован и подключён к AGENTS.md
- Зачем:
  - project/TODO.md не упоминался в AGENTS.md — агенты и участники
    его не находили при планировании работ;
  - список не имел приоритетов, выполненное не отмечалось
- Что:
  - в AGENTS.md добавлен указатель на project/TODO.md как бэклог проекта;
  - задачи сгруппированы по приоритетам P1–P3: наверху CI-проверка
    ссылок и подраздел «Linux и терминал», ниже перенос учебника
    Airflow, GA, трудозатраты, шаблон прогресса, затем dbt-стенд,
    абзац про DQ и бейджи статусов (помечены сомнения);
  - раздел «Сделано»: пункт про сложность алгоритмов закрыт
    коммитом 6f61c0e
- Проверка:
  - mkdocs build --strict (без ошибок; project/ исключён из сайта)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:54:51 +03:00
ddadminandClaude Fable 5 afcce5dc4a docs: выровнены введения секций ClickHouse и Lakehouse
- Зачем:
  - введение Lakehouse было одним громоздким абзацем, тяжело читалось;
  - секция ClickHouse, наоборот, начиналась сразу со списка ссылок
- Что:
  - введение Lakehouse разбито на три коротких абзаца с простыми фразами,
    смысл сохранён (разделение хранения и вычислений, роль табличного
    формата, вариативность технологий);
  - в ClickHouse добавлены два вводных предложения: что это и почему
    тема важна для собеседований
- Проверка:
  - mkdocs build --strict (без ошибок)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:44:13 +03:00
ddadminandClaude Fable 5 efa4dd9a3e docs: расширены материалы Lakehouse, убрана заглушка в карьерном блоке
- Зачем:
  - менти спрашивают, что такое Lakehouse и чем он отличается от
    классического DWH — во введении секции не было ответа и ссылки;
  - материалы по Iceberg лежали в «Дополнительных материалах», где их
    не найти по пути через секцию Lakehouse;
  - заглушка «в подготовке» выглядела незавершённостью раздела
- Что:
  - во введение секции добавлено объяснение отличия Lakehouse от
    классического DWH (открытые форматы, объектное хранилище,
    независимое масштабирование движков);
  - добавлено введение: статья Arenadata «Как не утонуть в данных:
    выбираем между DWH, Data Lake и Lakehouse» (Habr);
  - добавлен блок «Глубже про Iceberg»: видео Владимира Озерова
    «Как на самом деле работает Apache Iceberg» и статья VK Tech
    «Введение в устройство Parquet и Iceberg» (с пометкой о сложности);
  - из «Дополнительных материалов» в этот блок перенесены статья
    ivan-shamaev про Iceberg и видео «Spark + Iceberg in 1 Hour»;
  - удалена строка «Видео по прохождению собеседований из сообщества
    ОМ — в подготовке»
- Проверка:
  - mkdocs build --strict (без ошибок);
  - все три новые ссылки отвечают 200 (проверены с обходом прокси)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:35:41 +03:00
ddadminandClaude Fable 5 70d6da761f feat(docs): переработан раздел «Расширенные навыки»
- Зачем:
  - порядок секций противоречил анонсу раздела (Streaming объявлен первым);
  - Kafka из Streaming-блока — прямой вход в clickstream-стенд ClickHouse;
  - Lakehouse был помечен «в разработке», хотя стенд mini-lakehouse-lab
    уже содержит полноценный курс
- Что:
  - секция Streaming (NiFi + Kafka) перенесена перед ClickHouse,
    добавлена связка «Kafka пригодится на стенде ClickHouse»;
  - ClickHouse: уточнено, что упражнения курса Яндекса можно делать
    в их облаке (платно) или бесплатно на clickhouse-learning-cluster;
    clickstream-стенд обозначен как следующий шаг со своими уроками;
  - Lakehouse: убрана заглушка «Секция в разработке», секция переписана
    вокруг курса «Lakehouse без магии» (8 модулей, ~12–15 часов,
    raw → bronze → silver на NYC Taxi, Spark + Trino, checkpoint'ы);
  - оглавление: порядок «Streaming, ClickHouse, Lakehouse, dbt»
- Проверка:
  - mkdocs build --strict (без ошибок);
  - порядок h3-заголовков в site/index.html соответствует новому

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:19:43 +03:00
ddadminandClaude Fable 5 6f61c0ee90 refactor(docs): раздел о сложности алгоритмов заменён ссылкой в Python
- Зачем:
  - отдельный раздел «Понятие сложности алгоритмов» избыточен для DE-роадмапа;
  - SQL-часть (JOIN, индексы, объём сканирования) уже покрыта курсом QPT
- Что:
  - раздел удалён из блока практики;
  - в «продвинутые» темы Python добавлена статья «Сложность алгоритмов.
    Разбор Big O» (habr.com/ru/articles/782608)
- Проверка:
  - mkdocs build --strict (без ошибок);
  - в оглавлении ссылок на удалённый раздел не было

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:13:09 +03:00
ddadminandClaude Fable 5 9d8696f748 docs(modeling): исправлены ссылки на папки и markdown для сайта
- Зачем:
  - относительные ссылки на каталоги sql/ и data/ давали 404 на GitHub Pages;
  - em-dash в заголовках нарушает правило AGENTS.md (расходятся слаги GitHub/MkDocs);
  - 2-пробельная вложенность и списки без пустой строки ломали рендер в MkDocs
- Что:
  - ссылки на папки заменены на GitHub-tree-ссылки (работают на обеих платформах);
  - в 26 заголовках « — » заменено на «: » или убрано (README, SCD, DataVault, домашка);
  - пустая строка перед списком «Главные правила» в DataVault;
  - вложенные списки DataVault переведены на 4-пробельный отступ
- Проверка:
  - mkdocs build --strict (без ошибок);
  - grep по репо — якорных ссылок на старые слаги заголовков нет;
  - grep по site/dwh-modeling/*.html — списки рендерятся <ul>/<li>, вложенность сохранена

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:11:49 +03:00
ddadminandClaude Fable 5 a4fa66c466 feat(docs): добавлены стенды ClickHouse, починены ссылки и списки
- Зачем:
  - в разделе ClickHouse не было ссылок на учебные стенды;
  - аудит сайта выявил битые ссылки и сломанный рендеринг списков в MkDocs
- Что:
  - добавлены стенды clickhouse-learning-cluster и clickstream-ch-kafka-superset-demo;
  - пустые строки перед списками (CTE, Jira, Яндекс.Трекер) — рендерились абзацем с буквальными маркерами;
  - убраны лишние `**` после ссылки на Yandex Tracker (ломали разметку);
  - голые URL оформлены markdown-ссылками (karpov docker, jupyter-spark-docker, курс Яндекса по CH);
  - ссылка на datetime: зеркало django.fun заменено на docs.python.org/ru;
  - исправлена опечатка «конспектов лекция» → «лекций»;
  - mkdocs.yml: в заголовок домашки в nav добавлен пропущенный слой ODS;
  - AGENTS.md: Site URL обновлён на de.dementev.space (старый адрес отдаёт 404)
- Проверка:
  - mkdocs build --strict (без ошибок);
  - grep по site/index.html — исправленные списки рендерятся как <ul>/<li>

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:11:49 +03:00
ddadmin d19eb2de9d docs(project): добавлен TODO по подключению Google Analytics
- Зачем:
  - планируется подключение счётчика GA для аналитики трафика сайта.
- Что:
  - добавлен пункт в секцию «Сайт» файла project/TODO.md.
- Проверка:
  - cat project/TODO.md.
2026-04-03 22:14:23 +03:00
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
43 changed files with 3190 additions and 359 deletions
+2
View File
@@ -0,0 +1,2 @@
mkdocs-material==9.6.14
mkdocs-same-dir==0.1.3
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
readonly venv_dir="/var/lib/gitea-runner/venvs/site"
if [[ $# -ne 1 ]]; then
echo "Usage: $0 SOURCE_DIR" >&2
exit 2
fi
source_dir=$(realpath "$1")
requirements_file="${source_dir}/.gitea/requirements-site.txt"
if [[ ! -f $requirements_file ]]; then
echo "Requirements file not found: ${requirements_file}" >&2
exit 2
fi
mkdir -p "$(dirname "$venv_dir")"
if [[ ! -x "${venv_dir}/bin/python" ]]; then
python3 -m venv "$venv_dir"
fi
"${venv_dir}/bin/python" -m pip install \
--disable-pip-version-check \
--no-input \
--requirement "$requirements_file"
"${venv_dir}/bin/python" -m pip check
cd "$source_dir"
"${venv_dir}/bin/python" -m mkdocs build --strict
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
readonly deploy_root="/srv/de-roadmap"
readonly releases_root="${deploy_root}/releases"
readonly keep_releases=3
if [[ $# -ne 2 ]]; then
echo "Usage: $0 SITE_DIR COMMIT_SHA-RUN_ID" >&2
exit 2
fi
source_dir=$(realpath "$1")
release_id=$2
if [[ ! $release_id =~ ^[0-9a-f]{40}-[0-9]+$ ]]; then
echo "Invalid release id: ${release_id}" >&2
exit 2
fi
if [[ ! -f "${source_dir}/index.html" ]]; then
echo "Built site has no index.html: ${source_dir}" >&2
exit 2
fi
if [[ ! -d $releases_root || ! -w $releases_root || ! -w $deploy_root ]]; then
echo "Deployment directories are missing or not writable" >&2
exit 1
fi
readonly release_dir="${releases_root}/${release_id}"
readonly staging_dir="${releases_root}/.${release_id}.tmp"
readonly next_link="${deploy_root}/.current.${release_id}.tmp"
if [[ -e $release_dir || -e $staging_dir || -e $next_link ]]; then
echo "Release path already exists: ${release_id}" >&2
exit 1
fi
cleanup() {
rm -rf -- "$staging_dir"
rm -f -- "$next_link"
}
trap cleanup EXIT
umask 0022
mkdir "$staging_dir"
cp -a "${source_dir}/." "$staging_dir/"
chmod -R u=rwX,go=rX "$staging_dir"
mv "$staging_dir" "$release_dir"
ln -s "releases/${release_id}" "$next_link"
mv -Tf "$next_link" "${deploy_root}/current"
mapfile -t old_releases < <(
find "$releases_root" \
-mindepth 1 \
-maxdepth 1 \
-type d \
-regextype posix-extended \
-regex '.*/[0-9a-f]{40}-[0-9]+' \
-printf '%T@ %f\n' \
| sort -nr \
| awk -v keep="$keep_releases" 'NR > keep { print $2 }'
)
for old_release in "${old_releases[@]}"; do
if [[ $old_release =~ ^[0-9a-f]{40}-[0-9]+$ && $old_release != "$release_id" ]]; then
rm -rf -- "${releases_root:?}/${old_release}"
fi
done
trap - EXIT
echo "Published release ${release_id}"
+39
View File
@@ -0,0 +1,39 @@
name: Deploy MkDocs to VPS
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: site-production
cancel-in-progress: false
jobs:
deploy:
runs-on: de-roadmap-host
timeout-minutes: 15
steps:
- name: Check out the triggering commit
env:
COMMIT_SHA: ${{ gitea.sha }}
REPOSITORY_URL: ${{ gitea.server_url }}/${{ gitea.repository }}.git
run: |
set -euo pipefail
[[ "$COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]]
test ! -e source
mkdir source
git -C source init .
git -C source remote add origin "$REPOSITORY_URL"
git -C source fetch --no-tags --depth=1 origin "$COMMIT_SHA"
test "$(git -C source rev-parse FETCH_HEAD)" = "$COMMIT_SHA"
git -C source -c advice.detachedHead=false checkout --detach FETCH_HEAD
- name: Build the site strictly
run: source/.gitea/scripts/build-site.sh source
- name: Publish the complete release atomically
env:
RELEASE_ID: ${{ gitea.sha }}-${{ gitea.run_id }}
run: source/.gitea/scripts/deploy-site.sh source/site "$RELEASE_ID"
+54
View File
@@ -0,0 +1,54 @@
name: Check external links
on:
schedule:
- cron: "0 6 * * 1" # понедельник 06:00 UTC
workflow_dispatch:
permissions:
contents: read
issues: write
actions: write # для keepalive-шага
jobs:
link-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run lychee
id: lychee
uses: lycheeverse/lychee-action@v2
with:
# остальные настройки — в lychee.toml в корне репозитория
args: --no-progress './**/*.md'
fail: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# если открытый issue с меткой link-check уже есть — обновляем его,
# а не создаём дубликат каждую неделю
- name: Find open link-check issue
if: steps.lychee.outputs.exit_code != 0
id: issue
run: |
echo "number=$(gh issue list --repo "$GITHUB_REPOSITORY" --label link-check --state open --json number --jq '.[0].number // empty')" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ github.token }}
- name: Create or update issue on broken links
if: steps.lychee.outputs.exit_code != 0
uses: peter-evans/create-issue-from-file@v5
with:
title: "Битые внешние ссылки: еженедельная проверка"
content-filepath: ./lychee/out.md
labels: link-check
issue-number: ${{ steps.issue.outputs.number }}
# GitHub отключает scheduled-workflows после 60 дней без активности
# в репозитории; повторное включение сбрасывает таймер
- name: Keep scheduled workflow enabled
if: always()
run: gh api -X PUT "repos/$GITHUB_REPOSITORY/actions/workflows/check-links.yml/enable"
env:
GH_TOKEN: ${{ github.token }}
+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
+2
View File
@@ -1,3 +1,5 @@
.env
.idea
.internal/
site/
tmp
+54 -5
View File
@@ -1,9 +1,15 @@
# Repository Guidelines
## Project Structure & Module Organization
- Root `README.md` describes the learning roadmap (RU).
- `dwh-modeling/` contains the article and demo DWH model; SQL lives in `dwh-modeling/sql` as ordered scripts `01_...sql``06_...sql`.
- 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/`, `.gitea/`, `.github/`, `.claude/`.
- `.gitea/workflows/deploy-site.yml` — основной CI/CD: push в `main` → строгая
сборка → атомарная публикация на VPS через repository-scoped Gitea Runner.
- `.github/workflows/deploy-site.yml` — сохранённый workflow для резервной
публикации на GitHub Pages; Gitea его не исполняет.
- `project/` — PRD, ADR, and TODO.md (excluded from site). `project/TODO.md` is the prioritized project backlog: check it when planning or proposing work, and mark items done there when you complete them.
## Build, Test, and Development Commands
- Start demo Postgres:
@@ -20,16 +26,59 @@
- 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://de.dementev.space/` (старый адрес `https://dementev-dev.github.io/de-roadmap/` отдаёт 404)
## 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
- Commit messages are short, imperative or descriptive phrases (often in Russian), e.g. `Добавлено оглавление`, `Переработка структуры`; group related edits into a single commit.
- 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).
**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.
- Gitea Runner работает в host mode: не расширяйте его scope, не добавляйте
пользователя `gitea-runner` в `sudo` или `docker` и не выдавайте ему запись
вне `/var/lib/gitea-runner` и `/srv/de-roadmap`.
- 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
```
+293 -136
View File
@@ -1,40 +1,58 @@
# О роадмапе
## О роадмапе
Этот роадмап — конспект моих подходов к обучению Data Engineering.
Он подойдёт тем, кто хочет:
- системно войти в профессию с нуля или близкого к нулю уровня;
- закрыть пробелы в базе (SQL, Git, Python, DWH, Airflow, GreenPlum);
- закрыть пробелы в базе (SQL, Git, Python, DWH, Airflow, Greenplum);
- подготовиться к собеседованиям и первым рабочим задачам.
Роадмап можно проходить самостоятельно или вместе со мной в формате менторства.
Если хотите идти с поддержкой ментора — напишите в Telegram: [@dementev_dev](https://t.me/dementev_dev).
## Оглавление
### Оглавление
- [Основные знания](#основные-знания)
- [Практика и инструменты](#практика-и-инструменты)
- [Карьера и менторство](#карьера-и-менторство)
- [Расширенные навыки](#расширенные-навыки)
- [Основные знания](#основные-знания) — Linux, Git, SQL, Python, методологии, Docker
- [Практика и инструменты](#практика-и-инструменты) — Airflow, Greenplum, курсовая работа
- [Карьера и менторство](#карьера-и-менторство) — резюме, собеседования, испытательный срок
- [Расширенные навыки](#расширенные-навыки) — Streaming, ClickHouse, Lakehouse, dbt
- [Софт скиллы](#софт-скиллы)
- [Дополнительные материалы](#дополнительные-материалы)
Рекомендуемый способ использования:
- двигаться по разделам последовательно, не перепрыгивая через базу;
- выполнять практику и домашки, а не только смотреть материалы;
- возвращаться к разделам по мере появления реальных задач.
# Основные знания
---
## Git и базовые инструменты
## Основные знания
### База по Git
[[к оглавлению]](#оглавление)
### Git и базовые инструменты
#### Linux и терминал
Командная строка — рабочее место дата-инженера: docker, psql, git, подключение к серверам живут именно там. Отдельная практика не нужна — все стенды этого роадмапа консольные, команды закрепятся сами. На Windows поставьте [WSL](https://learn.microsoft.com/ru-ru/windows/wsl/install) — полноценный Linux внутри Windows.
- [Основы Linux для начинающих за 1.5 часа - Youtube](https://www.youtube.com/watch?v=Be6tB59b7D0) — что такое терминал, навигация, файлы, права, ssh: мягкий вход перед статьями
- [Linux: Файлы, навигация и поиск - Habr](https://habr.com/ru/articles/1003550/) — перемещение по каталогам, чтение файлов и логов: less, tail, grep
- [Права доступа к файлам и папкам в Linux - FirstVDS](https://firstvds.ru/technology/linux-permissions) — rwx, chmod, chown
- [SSH для начинающих - Cloud.ru](https://cloud.ru/blog/ssh-dlya-nachinayuschikh) — подключение к удалённой машине
- [Основы командной строки - Hexlet](https://ru.hexlet.io/programs/cli-basics) — опционально: бесплатный интерактивный курс с терминалом прямо в браузере; достаточно уроков про навигацию, grep и права доступа
#### База по Git
Что такое контроль версий, когда используется, ПОЧЕМУ и как мы в обучении будем использовать.
Как создать репозиторий на GitHub, сохранять в нем изменения.
- [Что такое 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
### Основы Markdown
#### Основы Markdown
- [Язык Markdown и файл README | Git и GitHub для начинающих - Youtube](https://www.youtube.com/watch?v=8lEDTrr-G4U)
- [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/)
@@ -42,41 +60,50 @@
Домашки по остальным темам тренируемся делать в Git, там же пишем документацию.
**Когда блок Git и базовые инструменты считаем пройденным:**
- ориентируетесь в терминале: перемещаетесь по каталогам и находите нужное в логах (grep, tail, less);
- понимаете права файлов (rwx, chmod) и знаете, как подключиться к серверу по ssh;
- вы уверенно создаёте репозиторий, коммитите изменения и отправляете их на GitHub;
- имеете представление о работе с ветками: создание, переключение, что такое merge/PR и разруливание простых конфликтов;
- оформляете базовую документацию в Markdown (README, заголовки, списки, ссылки, кодовые блоки).
## SQL
### База по SQL
### Базы данных: SQL и моделирование данных
SQL и моделирование данных специально идут рядом: сначала учимся уверенно извлекать данные запросами, затем — понимать и проектировать структуру данных, чтобы ETL/витрины были осмысленными.
#### База по SQL
Книга: [PostgreSQL. Основы языка SQL](https://postgrespro.ru/education/books/sqlprimer) - Глава 1 "Введение в базы данных и SQL" + ДЗ
Бесплатный тренажер: [Интерактивный тренажер по SQL – Stepik](https://stepik.org/course/63054/promo)
Целевой уровень знания SQL - Live кодинг на собесе. Проверяем на первом мок-интервью
**СТЕ**
- Зачем нам 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)
**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)
Для дальнейшей тренировки и поддержания уровня можно использовать [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)
Темы - от "Введение" до "SQL" включительно, "Управление доступом", "Резервное копирование". Для лучшего усваивания материала проделываем все примеры и домашние задания из конспектов лекция.
Темы - от "Введение" до "SQL" включительно, "Управление доступом", "Резервное копирование". Для лучшего усваивания материала проделываем все примеры и домашние задания из конспектов лекций.
С темой "PL/pgSQL" можно ознакомиться обзорно.
Для развития навыков инженера будет полезно лабораторные работы делать не в виртуальной машине, а в docker контейнере. Предложенный (не обязательный) вариант - в каталоге `postgres-bookings` репозитория.
Для дальнейшего закрепления материала - читаем книгу [PostgreSQL. Основы языка SQL](https://postgrespro.ru/education/books/sqlprimer)
- Глава 8 - Индексы + ДЗ
- Глава 9 - Транзакции
- Глава 10 - Повышение производительности + ДЗ
Вопросы оптимизации запросов хорошо описаны в курсе [QPT](https://postgrespro.ru/education/courses/QPT) от Postgres Pro. Полученные навыки применимы для работы в том числе с GreenPlum, и частично, другими БД.
Вопросы оптимизации запросов хорошо описаны в курсе [QPT](https://postgrespro.ru/education/courses/QPT) от Postgres Pro. Полученные навыки применимы для работы в том числе с Greenplum, и частично, другими БД.
На момент написания, видеолекции были доступны только для старой версии Postgres 13, но ее вполне достаточно.
### Моделирование данных
#### Моделирование данных
Понимание того, **как устроены данные и зачем они нужны**, — ключ к качественным ETL-процессам.
Мы кратко разбираем:
- Основные подходы: нормализованные (3NF) vs денормализованные (звезда, снежинка)
- Что такое staging, marts, слои raw / clean / business
- Как проектировать таблицы под конкретные сценарии использования
@@ -84,176 +111,249 @@
Цель — не стать архитектором, а **уметь читать и объяснять структуру данных**, чтобы писать осмысленные запросы и трансформации.
Материалы (включая демо 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)
- Краткая теория про [DWH](https://halltape.github.io/HalltapeRoadmapDE/DWH/) - повторим еще раз, в другом изложении
- Конспект и примеры из этого репозитория: [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/)
**Когда блок SQL считаем пройденным:**
**Когда блок «Базы данных» считаем пройденным:**
- вы уверенно пишете запросы с JOIN, агрегатами, подзапросами и CTE;
- можете подробно объяснить план запроса в Postgres, понимаете где планировщик отработал корректно, а где - есть возможность улучшить;
- можете объяснить простую модель данных (3NF/звезда) и прочитать схему DWH;
- решаете типовые задачи уровня SQL live-coding без долгих пауз.
## Python
### 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/
- [Полезные функции](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/)
- Работа с датой/временем: [официальная документация по модулю datetime](https://docs.python.org/3/library/datetime.html)
- [Сложность алгоритмов. Разбор Big O](https://habr.com/ru/articles/782608/) — короткий материал, чтобы понимать O(n) vs O(n²) на собеседованиях и в коде
- ООП
- [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/)
- [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
- [Блог Практикума: «Что такое Jupyter Notebook: как установить и открыть»](https://practicum.yandex.ru/blog/chto-takoe-jupyter-notebook/)
- Готовая реализация Jupyter Lab, включающая в себя Spark, в Docker: [jupyter-spark-docker](https://github.com/dementev-dev/jupyter-spark-docker)
- Pandas
- [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)
- [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)
Цель — уверенно решать простые задачи на Python в формате live-coding; дальше эти навыки пригодятся для создания DAG Airflow.
**Когда блок Python считаем пройденным:**
- вы без подсказок пишете небольшие скрипты с циклами, функциями, обработкой ошибок и работой с коллекциями;
- умеете читать и модифицировать чужой код, в том числе с использованием 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
- Сжатый, но емкий видеогайд: [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
- 2 Основы Git
- 3 Ветвление в Git
- 5 Распределённый Git
- 6 GitHub
- [Курс работы с Git и GitLab - ЭФКО ЦПР | YouTube плейлист](https://www.youtube.com/playlist?list=PLbf8m52BvqlFlblJqQKPuEU26pwgqe7zK). Настоятельно рекомендую проделать за лектором все те действия что он показывает.
Целевой уровень знания - понимание процесса GitFlow. Как создать ветку, влить изменения в другие ветки. Понимание, зачем.
На собесах обычно не спрашивают, но нужно в работе.
### Docker
- Курс https://karpov.courses/docker
#### Docker
- Курс [Docker от karpov.courses](https://karpov.courses/docker)
Основное предназначение для нас - учебные стенды, где мы разбираем и тренируемся с разными технологиями. На работе - иногда пригождается. На собесах спрашивают редко.
### Методы разработки
Кратко знакомимся с основными подходами к организации работы в IT:
- **Водопад** — последовательная разработка,
- **Scrum / Kanban** — гибкие методологии, популярные в data-командах.
Понимание этих концепций помогает быстрее адаптироваться в новых проектах и правильно интерпретировать требования.
### Запись встреч
#### Запись встреч
OBS Studio
- Руководство по OBS: [OBS Studio - Настройка ОБС для Записи Игр и Стрима | Настройка Микрофона в Обс и т.д - Youtube](https://www.youtube.com/watch?v=bj8VEphZ65U)
- [Как записывать собеседования](https://docs.google.com/document/d/1qd8uRYlAaZp9c5zpvCVBOvYQCEukGHI9PEPjnjahI1k/)
**Когда блок технических навыков считаем пройденным:**
- вы понимаете базовый GitFlow: как организована работа с ветками в команде и как ваши коммиты попадают в прод;
- используете Docker для учебных стендов: запускаете контейнеры, смотрите логи и при необходимости перезапускаете сервисы;
- ориентируетесь в основных методологиях разработки (Scrum/Kanban/водопад) и понимаете, как в них живут задачи и отчётность;
- при необходимости умеете настроить запись экрана/созвонов, чтобы сохранять материалы обучения.
# Практика и инструменты
---
## Airflow
## Практика и инструменты
[[к оглавлению]](#оглавление)
### Airflow
Apache Airflow — инструмент для оркестрации ETL-процессов.
Мы используем его для:
- планирования задач,
- отслеживания зависимостей между шагами,
- визуализации статуса выполнения.
Материалы:
- [Учебник по Airflow](https://github.com/dementev-dev/airflow-manual)
**Когда блок Airflow считаем пройденным:**
- вы можете объяснить, что такое DAG, задачи, операторы и сенсоры, и как между ними задаются зависимости;
- на базе учебного стенда подготавливаете, отлаживаете и запускаете свои DAG'и с расписанием и несколькими шагами (например, загрузка данных и последующие трансформации);
- уверенно смотрите логи, находите место падения и понимаете, как перезапустить задачу.
## Greenplum
### Greenplum
Разбираем, чем Greenplum отличается от PostgreSQL и зачем нужны MPP-хранилища.
Предварительно:
#### Фундаментальная теория
Прежде чем нажимать кнопки, нужно понять "физику" больших данных. Почему обычный Postgres начинает тормозить?
- Мартин Клеппман, "Высоконагруженные приложения":
- Глава 1. Надежность, масштабируемость. (Разбираемся, чем вертикальное масштабирование отличается от горизонтального).
- Глава 3 (только конец главы). Читаем разделы:
- «Обработка транзакций или аналитика?» (OLTP or OLAP?) — ключевое различие нагрузок.
- «Хранение по столбцам» — почему аналитика требует другого способа записи данных на диск.
- Зачем: Это объясняет, почему Greenplum устроен именно так. Без этого вы будете пытаться работать с ним как с обычным Postgres.
#### Знакомство с Greenplum
Теперь, понимая теорию, смотрим, как это реализовано в конкретном инструменте.
- Простое введение: [Greenplum | Что это такое и как оно работает? - Youtube](https://www.youtube.com/watch?v=rLG9Z_HcKPY)
- Оно же, но текстом: https://halltape.github.io/HalltapeRoadmapDE/GREENPLUM/
- [Визуализатор распределения Greenplum](https://gpskew.rzvde.pro/)
- Бесплатный, но большой учебный курс от Yandex: https://yandex.cloud/ru/training/greenplum
- [Учебный курс по Greenplum от datafinder](https://datafinder.ru/products/uchebnyy-kurs-po-greenplum) — взять только отдельные главы.
Практика:
- [DE Starter Kit — Airflow + Greenplum + CSV](https://github.com/dementev-dev/airflow-greenplum)
**Курс 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 считаем пройденным:**
- вы понимаете, как данные распределяются по сегментам, что такое skew и как его увидеть;
- на базе учебного стенда (например, DE Starter Kit) можете загружать и выгружать данные в Greenplum, выполнять запросы и разбирать планы выполнения (`EXPLAIN`);
- на базе стенда `airflow-dwh-gp-lab` можете загружать и выгружать данные в Greenplum, выполнять запросы и разбирать планы выполнения (`EXPLAIN`);
- можете объяснить, в чём практическая разница между MPP-хранилищем и одиночным Postgres на уровне типичных задач DE и собеседований.
## Курсовая работа
К финалу роадмапа мы собираем небольшую end-to-end курсовую работу — свой первый «боеподобный» data-проект.
### Курсовая работа
Курсовая работа — важный майлстоун роадмапа: ваш первый end-to-end data-проект. После неё у вас есть ключевые технические навыки для старта карьеры в Data Engineering.
### Стенд в Docker Compose
- Apache Airflow — оркестратор;
- источник данных — TelecomX (или аналогичный открытый датасет);
- Greenplum — основное хранилище;
- вспомогательный Postgres (по желанию);
- ETL-скрипты и DAG'и;
- исходные коды всего — в отдельном Git-репозитории.
Курсовая выполняется на том же стенде [airflow-dwh-gp-lab](https://github.com/dementev-dev/airflow-greenplum), который вы уже использовали для практики по Greenplum.
В качестве альтернативы файловому источнику можно использовать генератор данных для демо-базы `bookings` от Postgres Pro: https://github.com/postgrespro/demodb.
Его удобнее всего встроить в стенд DE Starter Kit (Airflow + Greenplum) как отдельный сервис Postgres с регулярно генерируемыми данными и уже оттуда забирать их в Greenplum (в том числе через PXF, если хочется усложнить архитектуру).
**Что внутри:**
- Стенд содержит DWH с реализованным эталонным срезом (STG → ODS → DDS → DM) — это ваш образец для подражания.
- Задача — довести DWH до полного, реализовав недостающие загрузки по аналогии с эталоном.
- Есть готовый план от аналитика (ТЗ с маппингами и бизнес-правилами) — не нужно придумывать, что делать.
- Встроенная автоматическая проверка реализации поможет убедиться в корректности до проверки ментором.
- Ветка `main` — рабочая (с заготовками для реализации), ветка `solution` — эталон для сверки.
**Когда блок курсовой работы считаем пройденным:**
- у вас есть отдельный репозиторий с docker-compose, DAG'ами Airflow, SQL-скриптами и README по проекту;
- стенд поднимается локально, DAG'и успешно прогоняются на тестовых данных от загрузки сырья до витрин;
- вы можете на собеседовании за 5–10 минут рассказать архитектуру курсового проекта, его цели и показать ключевые части кода.
## Понятие сложности алгоритмов
В Data Engineering редко требуется писать сложные алгоритмы, но важно понимать, как оценивать эффективность кода:
- все загрузки реализованы, DWH заполняется полностью (STG → ODS → DDS → DM);
- автоматическая проверка (валидационный DAG) проходит без ошибок;
- вы можете на собеседовании за 5–10 минут рассказать архитектуру проекта, его цели и показать ключевые части кода.
- в SQL — через объём сканируемых данных, типы JOIN’ов, использование индексов;
- в Python — через асимптотику операций с pandas/списками (например, O(n) vs O(n²)).
---
Это помогает избегать «тормозящих» решений на собеседованиях и в реальных пайплайнах.
## Карьера и менторство
# Карьера и менторство
[[к оглавлению]](#оглавление)
## Менторство по этому роадмапу
### Менторство по этому роадмапу
Если вы нашли этот роадмап в интернете и хотите пройти его не в одиночку, а с поддержкой ментора, можно присоединиться ко мне.
**Что даёт менторство:**
- структурный план прохождения роадмапа под вашу ситуацию;
- разбор вопросов по SQL / DWH / Airflow и другим темам из этого документа;
- разбор домашних заданий и код-ревью;
@@ -264,96 +364,153 @@ Apache Airflow — инструмент для оркестрации ETL-про
Просто напишите мне в Telegram: [@dementev_dev](https://t.me/dementev_dev)
со словами «Хочу пройти роадмап с ментором» — дальше всё обсудим.
## Подготовка к собеседованиям
### Подготовка к собеседованиям
Цель блока — сформировать «опыт от 2 лет» и уметь корректно его показать в резюме и на собеседовании.
### Помощь в подготовке резюме
#### Помощь в подготовке резюме
- Видео от ОМ по составлению резюме
- [Как накрутить опыт в резюме | «Ультимативный гайд» @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/7289cd23-60c6-4010-bb1c-a5b28dac399a)
- Попытки менти написать резюме, моя обратная связь — итеративно.
- [Как накрутить опыт в резюме | «Ультимативный гайд» @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/7289cd23-60c6-4010-bb1c-a5b28dac399a)
- Практика: совместная работа над резюме — ментор помогает переработать опыт, сформировать убедительную карьерную историю и подготовиться к вопросам по ней.
### Навыки поиска работы с HH и Habr карьера
#### Поиск работы и собеседования
- [Как подтвердить опыт без трудовой / Хабр против работяг](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)
**Когда блок подготовки к собеседованиям считаем пройденным:**
- у вас есть актуальное резюме под DE с понятными примерами проектов вместо «пустого» опыта;
- вы умеете искать и отбирать вакансии на HH и Habr Карьера, адаптируя отклики под конкретную позицию;
- вы прошли хотя бы пару мок-собеседований, получили обратную связь и по результатам доработали резюме и стратегию поиска.
# Расширенные навыки
---
## Расширенные навыки
Эти темы выходят за рамки базового минимума для старта в Data Engineering, но дают более полное представление об экосистеме.
Их цель — понимать, зачем и когда используется тот или иной инструмент, а не осваивать его на уровне администратора или DevOps-инженера.
[[к оглавлению]](#оглавление)
Мы кратко знакомимся с:
- **Apache NiFi** и **Kafka** — инструментами для построения потоковых и интеграционных пайплайнов;
- **Streaming** (NiFi + Kafka) — инструментами для построения потоковых и интеграционных пайплайнов;
- **ClickHouse** — колоночной СУБД для высоконагруженной аналитики;
- **Lakehouse** (Spark, Iceberg, Trino) — архитектурой, построенной на разделении compute и storage;
- **dbt** — подходом к трансформации данных как кода.
Практика ограничивается минимальным рабочим примером (например, запуск в Docker, простой пайплайн или SQL-модель).
Практика ограничивается минимальным рабочим примером (запуск в Docker, простой пайплайн или SQL-модель).
Этого достаточно, чтобы уверенно говорить об инструменте на собеседовании и понимать его место в архитектуре — а всё остальное при необходимости осваивается уже на проекте.
## ClickHouse
Бесплатный курс https://yandex.cloud/ru/training/clickhouse
Платный курс [ClickHouse для аналитика – Stepik](https://stepik.org/course/100210/promo?search=6551441002)
### Streaming (NiFi + Kafka)
NiFi — визуальный конструктор потоков данных, Kafka — распределённая очередь сообщений. Вместе они закрывают типичный сценарий: принять данные, буферизовать, доставить в хранилище.
## NiFi
Плейлист [Apache NiFi с нуля за 3 часа. Конструктор вместо кода - Youtube](https://youtube.com/playlist?list=PL4MpKy3QjNp_rOEEibc4Ro8UK4g8vLX6_&si=W_hidjHmBOZ_aUfS) — первые 4 видео, дальше — по желанию.
Материалы:
- [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.
Знакомство с Kafka здесь пригодится и дальше: стенд по ClickHouse в следующей секции принимает данные именно через Kafka.
### ClickHouse
ClickHouse — колоночная СУБД для аналитики на больших объёмах: миллиарды строк, агрегации за секунды. В российских компаниях это фактический стандарт для витрин, отчётности и продуктовой аналитики, поэтому на собеседованиях тема всплывает часто.
Материалы:
- Бесплатный курс [ClickHouse от Yandex Cloud](https://yandex.cloud/ru/training/clickhouse) — берём за основу, в нём много упражнений
- Платный курс [ClickHouse для аналитика – Stepik](https://stepik.org/course/100210/promo?search=6551441002)
Практика:
- собираем отдельный стенд в Docker Compose с Postgres и NiFi;
- в NiFi настраиваем простой генератор данных.
## Kafka
[Лучший Гайд по Kafka для Начинающих За 1 Час - Youtube](https://www.youtube.com/watch?v=hbseyn-CfXY)
- Упражнения курса Яндекса можно выполнять в их облаке (с оплатой за ресурсы) или бесплатно у себя — на учебном кластере [clickhouse-learning-cluster](https://github.com/dementev-dev/clickhouse-learning-cluster): 4 узла ClickHouse в Docker Compose, репликация, шардинг, балансировка через HAProxy.
- Следующий шаг — стенд [clickstream-ch-kafka-superset-demo](https://github.com/dementev-dev/clickstream-ch-kafka-superset-demo), имитирующий полноценное аналитическое хранилище на ClickHouse: Kafka, Airflow, дашборды в Superset, мониторинг (Prometheus/Grafana), слои STG → ODS → DDS → DM. Внутри — собственный продвинутый курс «Кликстрим на ClickHouse» с уроками прямо на стенде.
Практика:
- расширяем предыдущий стенд, добавляя Kafka;
- строим поток данных: NiFi → Kafka;
- добавляем обратный поток: Kafka → NiFi → Postgres.
### Lakehouse (Spark, Iceberg, Trino)
## dbt
Lakehouse — архитектурный подход, который соединяет гибкость Data Lake с гарантиями классического DWH.
В классическом DWH данные и вычисления живут внутри одной СУБД, в её закрытом формате. В Lakehouse они разделены. Данные лежат файлами в дешёвом хранилище (обычно объектном, вроде S3). Открытый табличный формат (Iceberg, Delta, Hudi) добавляет поверх файлов привычные по СУБД вещи: схемы, транзакции, историю изменений. А вычислительные движки (Spark, Trino, Flink и другие) подключаются к данным снаружи — хоть несколько разных к одним и тем же таблицам.
Роадмап фокусируется на классическом DWH-стеке, поэтому цель здесь — знакомство, но с настоящей практикой: стенд ниже собирает один из типовых наборов этого конструктора.
Материалы:
- Введение в тему: [«Как не утонуть в данных: выбираем между DWH, Data Lake и Lakehouse» (Habr, Arenadata)](https://habr.com/ru/companies/arenadata/articles/885722/) — что такое Lakehouse, чем он отличается от классического DWH и Data Lake и зачем появился
- [DataLearn: «Что такое Apache Spark»](https://youtu.be/Tl9YzC-dQLI) — введение в Spark с нуля, ~40 минут
Практика — курс [«Lakehouse без магии»](https://github.com/dementev-dev/mini-lakehouse-lab) на стенде mini-lakehouse-lab (Spark + Iceberg + Trino + MinIO, всё локально в Docker, без облаков и регистраций):
- 8 модулей на ~12–15 часов самостоятельной работы; в каждом — объяснение, демонстрация, задание и checkpoint;
- пайплайн `raw → bronze → silver` на реальном датасете NYC Taxi;
- одна таблица из двух движков: запись через Spark, чтение через Trino — и почему это работает без копирования данных;
- schema evolution, time travel и обслуживание таблиц (compaction, expire_snapshots) — с параллелями к знакомым VACUUM/REORGANIZE из мира Postgres/Greenplum.
Глубже про Iceberg (опционально, лучше после практики на стенде):
- [«Как на самом деле работает Apache Iceberg» — Владимир Озеров, HighLoad Channel (Youtube)](https://www.youtube.com/watch?v=_3fsE2a2FO4)
- [Введение в устройство Parquet и Iceberg (Habr, VK Tech)](https://habr.com/ru/companies/vktech/articles/959398/) — подробный и местами непростой разбор форматов изнутри
- [Введение в Apache Iceberg: основы, архитектура, как работает](https://ivan-shamaev.ru/apache-iceberg-tutorial-architecture-how-to-work/#__Apache_Iceberg-2)
- [Spark + Iceberg in 1 Hour: Memory Tuning, Joins, Partition (Youtube, англ.)](https://www.youtube.com/watch?v=3R-SLYK-P_0)
### 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` — этого достаточно, чтобы увидеть весь цикл.
**Когда блок расширенных навыков считаем пройденным:**
- вы можете на собеседовании кратко объяснить, когда уместны NiFi/Kafka, ClickHouse и dbt, и чем они дополняют базовый стек (Postgres, Airflow, Greenplum);
- понимаете типичные сценарии: потоковые интеграции и очереди (Kafka/NiFi), аналитические витрины и отчёты на ClickHouse, трансформации данных в dbt;
- вы можете на собеседовании кратко объяснить, когда уместны Streaming (NiFi/Kafka), ClickHouse, Lakehouse-стек и dbt, и чем они дополняют базовый стек (Postgres, Airflow, Greenplum);
- понимаете типичные сценарии: потоковые интеграции и очереди (NiFi + Kafka), аналитические витрины и отчёты на ClickHouse, Lakehouse-архитектура (Spark/Iceberg/Trino), трансформации данных как код (dbt);
- не боитесь увидеть эти инструменты в описании вакансии и можете поддержать содержательный разговор об их месте в архитектуре.
# Софт скиллы
---
## Софт скиллы
[[к оглавлению]](#оглавление)
- [Все ветви дохода в IT / Полный гайд по деньгам](https://youtube.com/live/JHClTWwK1EM)
- [Гайд как писать отзывы](https://boosty.to/m0rtymerr/posts/b04040ec-0f46-4524-9c75-188a513140ad?share=post_link)
- [Гайд по Антистрессу](https://youtu.be/bu0YiXOKaoU)
# Дополнительные материалы
---
## Дополнительные материалы
[[к оглавлению]](#оглавление)
- [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)
- [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)
- [Алгоритмы: теория и практика. Методы – Stepik](https://stepik.org/course/217/info)
- [Алгоритмы: теория и практика. Структуры данных – Stepik](https://stepik.org/course/1547/promo)
- [Apache Hadoop для самых маленьких: HDFS, RACK-AWARENESS, репликация и Data Locality - Youtube](https://youtu.be/0fsY5bW2l84)
- [Книга. Введение в Apache Kafka для системных аналитиков и проектировщиков интеграций](https://systems.education/kafka)
- [Перевод документации dbt на русский язык](https://docs.getdbt.tech/)
## Записи ОМ
### Записи ОМ
- [Как пройти собеседование на программиста | Ультимативный гайд с ‪@om_nazarov - Youtube](https://www.youtube.com/watch?v=tzSdiYZ52kI)
- [Как стать программистом в 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`-файлы памяти
- не полагайтесь только на разговор для сохранения правил
+13 -12
View File
@@ -126,17 +126,18 @@ erDiagram
Чуть менее «сказочно», чуть более технично.
### 3.1. Hub сущность и её бизнес-ключ
### 3.1. Hub: сущность и её бизнес-ключ
**Hub** содержит:
* бизнес‑ключ (customer_bk, order_id, contract_number);
* техническую информацию:
* record_source — из какой системы пришла первая запись;
* load_dttm — когда запись попала в DV;
* иногда — хэш бизнес‑ключа (hk_customer).
* record_source — из какой системы пришла первая запись;
* load_dttm — когда запись попала в DV;
* иногда — хэш бизнес‑ключа (hk_customer).
Главные правила:
* один бизнес‑ключ — один хаб (одна строка на сущность, без истории);
* хаб не знает про атрибуты (имя, email) — только идентичность.
@@ -153,7 +154,7 @@ CREATE TABLE hub_customer (
Конкретные типы данных (`BYTEA`, длины `VARCHAR`, детали `hashdiff`) и реализации хэш‑ключей можно не запоминать: на старте важнее понять саму идею — у сущностей есть стабильные ключи, а все изменения атрибутов мы записываем отдельными версиями в сателлитах.
### 3.2. Link связи между сущностями
### 3.2. Link: связи между сущностями
**Link** описывает факт связи, например:
@@ -178,7 +179,7 @@ CREATE TABLE link_order_customer (
);
```
### 3.3. Satellite атрибуты и история
### 3.3. Satellite: атрибуты и история
**Satellite** хранит:
@@ -249,7 +250,7 @@ Star Schema]
* **Raw Vault** — это про приём и хранение данных «как есть», но уже в форме Hub / Link / Satellite.
* **Business Vault** — это про приведение этих данных в более «деловой» вид: с бизнес-правилами, PIT/Bridge и подготовленными представлениями.
### 5.1. Raw Vault «всё прилетевшее, аккуратно разложенное по ящичкам»
### 5.1. Raw Vault: «всё прилетевшее, аккуратно разложенное по ящичкам»
Raw DV — первый слой поверх STG / ODS:
@@ -261,20 +262,20 @@ Raw DV — первый слой поверх STG / ODS:
* минимум бизнес-логики:
* никаких правил вроде «клиент активен, если была хотя бы одна покупка за 90 дней»;
* никаких правил вроде «клиент активен, если была хотя бы одна покупка за 90 дней»;
* все источники показываются «как есть», только приведены к общим ключам;
* структура стабильна: добавился новый источник → появился новый Satellite к тому же Hub.
### 5.2. Business Vault «там, где из Lego собирают модули»
### 5.2. Business Vault: «там, где из Lego собирают модули»
Business Vault (BV) — следующий слой над Raw DV:
* здесь применяются бизнес-правила (что считать активным клиентом, как трактовать статусы);
* здесь строятся вспомогательные структуры:
* PIT-таблицы,
* Bridge-таблицы,
* агрегаты и derived-таблицы.
* PIT-таблицы,
* Bridge-таблицы,
* агрегаты и derived-таблицы.
Именно из BV чаще всего строятся витрины в формате Звезды, к которым подключаются BI и отчётность.
+45 -34
View File
@@ -30,7 +30,7 @@
Структура файла:
```text
customer_id,status,event_ts,_load_id,load_ts
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
...
```
@@ -41,7 +41,7 @@ customer_id,status,event_ts,_load_id,load_ts
- `status` — статус клиента в CRM (`new`, `active`, `vip`, `churned`);
- `event_ts` — момент, когда статус сменился в CRM;
- `_load_id` — идентификатор батча загрузки;
- `load_ts` — момент, когда данные попали в DWH (в таблицах STG/ODS эта колонка будет называться `_load_ts`, но по смыслу это то же самое время загрузки).
- `_load_ts` — момент, когда данные попали в DWH.
Файл содержит несколько клиентов и несколько смен статуса по каждому — этого достаточно, чтобы отработать SCD2.
@@ -52,9 +52,9 @@ customer_id,status,event_ts,_load_id,load_ts
Чтобы не тратить время на DDL, структуры таблиц для домашки уже подготовлены в `dwh-modeling/sql`:
- `07_ddl_hw_customer_status.sql` — создаёт дополнительные таблицы:
- `stg.customer_status_raw` — сырые события о статусе клиента;
- `ods.customer_status` — очищенные и типизированные события;
- `dds.dim_customer_status` — измерение статусов клиента в формате **SCD Type 2**.
- `stg.customer_status_raw` — сырые события о статусе клиента;
- `ods.customer_status` — очищенные и типизированные события;
- `dds.dim_customer_status` — измерение статусов клиента в формате **SCD Type 2**.
- `08_dml_hw_customer_status_template.sql` — шаблон DML-скрипта с подсказками и заготовками блоков.
Перед началом работы:
@@ -62,15 +62,16 @@ customer_id,status,event_ts,_load_id,load_ts
1. Поднимите demo‑Postgres по инструкции из корневого `README.md`.
2. Выполните базовые скрипты DWH:
- `01_ddl_stg-dds.sql`
- `02_dml_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` уже существуют, а дополнительные таблицы для статусов созданы.
После этого схемы `stg`, `ods`, `dds`, `dm` уже существуют, а дополнительные таблицы для статусов созданы.
---
## 3. Часть 1 STG → ODS (обязательно)
## 3. Часть 1: STG → ODS (обязательно)
**Задача:** загрузить CSV в STG и переложить данные в ODS с приведением типов.
@@ -94,13 +95,13 @@ INSERT INTO stg.customer_status_raw (customer_id, status, event_ts, _load_id, _l
('101','churned','2024-09-01 12:15:00','batch_20240901_1300','2024-09-01 13:00:00');
```
> 💡 Здесь `_load_ts` — это время загрузки (в CSV оно называется `load_ts`).
> 💡 Здесь `_load_ts` — это время загрузки.
#### Вариант B: загрузить CSV
Можно загрузить файл `dwh-modeling/data/customer_status_events.csv` в таблицу `stg.customer_status_raw`:
- **Через DBeaver**: Import Data → CSV → `stg.customer_status_raw` (колонку `load_ts` маппить в `_load_ts`).
- **Через DBeaver**: Import Data → CSV → `stg.customer_status_raw`.
- **Через `psql` в контейнере (`./psql_sh`)**: без установки `psql` на хост.
Способ: передайте CSV в `psql` через STDIN и выполните `\copy ... FROM STDIN`:
@@ -121,12 +122,14 @@ 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` (в DWH-таблицах эта колонка будет лежать как `_load_ts`);
- `customer_id``INT`,
- `status``VARCHAR(20)` (можно оставить как есть),
- `event_ts` и `_load_ts``TIMESTAMP`;
- аккуратно обработать возможные пустые значения (если бы они были);
- заполнить `_load_id` и `_load_ts` в `ods.customer_status`.
@@ -142,7 +145,7 @@ ORDER BY customer_id, event_ts;
---
## 4. Часть 2 ODS → DDS (SCD Type 2, обязательно)
## 4. Часть 2: ODS → DDS (SCD Type 2, обязательно)
**Задача:** по событиям в `ods.customer_status` построить измерение `dds.dim_customer_status`, где каждая строка — период действия статуса.
@@ -213,41 +216,36 @@ ORDER BY customer_bk, valid_from;
---
## 5. Часть 3 инкрементальная загрузка (по желанию)
## 5. Часть 3: инкрементальная загрузка (по желанию)
Если хочется потренироваться глубже:
1. Добавьте в CSV ещё несколько событий смены статуса (например, переход части клиентов из `churned` обратно в `active`).
2. Загрузите новые строки только в `stg.customer_status_raw`.
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`.
- корректно «закрыть» старую актуальную строку (заполнить `valid_to` датой начала новой версии);
- вставить новую строку с `valid_to = NULL`.
Эта часть особенно полезна, если вы хотите почувствовать, как SCD2 живёт в реальном DWH.
---
## 6. Часть 4 витрина в DM (по желанию)
## 6. Часть 4: витрина в DM (по желанию)
Опциональное задание для закрепления: собрать небольшую витрину с количеством клиентов по статусам на каждую дату.
Перед началом убедитесь, что слой DM создан (схема `dm` и таблицы):
- выполните `dwh-modeling/sql/05_ddl_dm.sql` (один раз);
- затем можно собирать витрину.
Пример целевой таблицы:
```sql
CREATE TABLE dm.mart_customer_status_daily (
date_actual DATE NOT NULL,
status VARCHAR(20) NOT NULL,
customers_cnt INT NOT NULL
);
```
DDL витрины уже создан в `07_ddl_hw_customer_status.sql` (таблица `dm.mart_customer_status_daily`).
Идея:
@@ -284,3 +282,16 @@ ORDER BY date_actual, 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>
+174 -154
View File
@@ -3,28 +3,30 @@
## Оглавление
- [Что вы уже умеете и что узнаете здесь](#что-вы-уже-умеете-и-что-узнаете-здесь)
- [Что вы уже умеете, и что узнаете здесь](#что-вы-уже-умеете-и-что-узнаете-здесь)
- [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-подхода-и-когда-какой-выбрать)
- [6. Модели данных для слоя DDS: 4 подхода, и когда какой выбрать](#6-модели-данных-для-слоя-dds-4-подхода-и-когда-какой-выбрать)
- [7. Практикум: как собрать первую витрину](#7-практикум-как-собрать-первую-витрину)
- [8. Как выбрать модель данных? Советы от практиков](#8-как-выбрать-модель-данных-советы-от-практиков)
- [9. Эксплуатация: качество данных это не «опция»](#9-эксплуатация-качество-данных-это-не-опция)
- [10. Заключение: главное понимать «почему»](#10-заключение-главное-понимать-почему)
- [9. Эксплуатация: качество данных, это не «опция»](#9-эксплуатация-качество-данных-это-не-опция)
- [10. Заключение: главное, понимать «почему»](#10-заключение-главное-понимать-почему)
- [Приложения](#приложения)
---
## Что вы уже умеете и что узнаете здесь
## Что вы уже умеете, и что узнаете здесь
✅ Уже знаете:
- `SELECT`, `JOIN`, `GROUP BY`;
- как посчитать сумму/среднее/количество по таблице.
🆕 Узнаете в этой статье:
- **слои хранилища** (STG → ODS → DDS → DM) и *зачем они нужны*;
- **факты и измерения** — основные кирпичики аналитики;
- **SCD Type 2** — как хранить историю изменений клиента (например, смену email или города);
@@ -32,6 +34,7 @@
- **четыре модели данных**: 3NF, Звезда (Star), Data Vault, Anchor Modeling — и когда какую использовать.
**Не будем говорить** здесь о:
- физическом хранении (партиции, индексы, ClickHouse-движки);
- распределённых кластерах (Kafka, Spark, Airflow — это отдельный курс);
- настройке производительности (`EXPLAIN`, кэши и т.п.).
@@ -47,6 +50,7 @@
Вы идёте в базу заказов — и… не находите email. Он в CRM. Идёте в CRM — там нет сумм заказов. Возвращаетесь в заказы — сумма есть, но *только текущая цена товара*. А в 2023 году цена была другой!
Знакомо? Это — **проблема OLTP-систем** (оперативного учёта):
- **CRM**, **склад**, **платёжка** — это разные базы;
- каждая оптимизирована под *быструю запись операций* («добавить заказ», «списать товар»);
- историю там не хранят — email меняется «в лоб»: старое значение перезаписывается.
@@ -75,6 +79,7 @@
| `promos` | Маркетинг | Акции: `promo_id`, `code` |
⚠️ Обратите внимание:
- `customer_id = 101` в одном месяце — `a@ex.com`, в другом — `b@ex.com`;
- цена на товар `9001` (Phone) в январе — 100 ₽, в феврале — 110 ₽;
- `order_items` содержит `price_at_sale`*цену в момент покупки*, а не текущую.
@@ -140,27 +145,27 @@ flowchart TD
Давайте проследим, как превращается строка заказа.
### **STG (Staging / Bronze)** «как пришло»
### **STG (Staging / Bronze)**: «как пришло»
- Таблицы: `stg.orders_raw`, `stg.customers_raw`;
- Структура — *точно как в источнике* (может быть `VARCHAR` даже у дат);
- Добавлены технические поля:
- `_load_id` — идентификатор загрузки;
- `_load_ts` — время получения данных;
- `_load_id` — идентификатор загрузки;
- `_load_ts` — время получения данных;
- Главное правило: **неизменяемость**. Если пришла новая порция — либо добавляем новые строки, либо *полностью перезагружаем* слой (идемпотентность).
> 💡 *Пример:* `stg.orders_raw` содержит `"2024-01-10"` как строку — это нормально. Главное — не потерять оригинал.
---
### **ODS (Operational Data Store / Silver)** «почистили, но не трогали смысл»
### **ODS (Operational Data Store / Silver)**: «почистили, но не трогали смысл»
- Таблицы: `ods.orders`, `ods.customers`;
- Здесь:
- привели `order_date` к типу `DATE`;
- убрали заказы без клиента (`customer_id IS NULL` → ошибка или флаг);
- привели телефоны к формату `79991112233`;
- проверили email на валидность (регуляркой или простой проверкой).
- привели `order_date` к типу `DATE`;
- убрали заказы без клиента (`customer_id IS NULL` → ошибка или флаг);
- привели телефоны к формату `79991112233`;
- проверили email на валидность (регуляркой или простой проверкой).
- **Но!** Не объединяем клиента из CRM и клиента из заказов — это будет позже.
- Пока — никакой бизнес-логики. Только *техническая* очистка.
- Дедупликация: если два раза пришёл один и тот же заказ — оставляем один (по `order_id + _load_ts`).
@@ -169,7 +174,7 @@ flowchart TD
---
### **DDS (Data Delivery Store / Core / Conformed)** «интеграция + история»
### **DDS (Data Delivery Store / Core / Conformed)**: «интеграция + история»
Здесь рождается *единая бизнес-модель*.
Появляются понятия: **измерения**, **факты**, **суррогатные ключи**, **SCD**.
@@ -180,7 +185,7 @@ flowchart TD
|---------|------------|
| `dds.dim_customer` | Измерение «Клиент» с историей (SCD Type 2) |
| `dds.dim_product` | Измерение «Товар» |
| `dds.dim_date` | Готовый календарь на 10 лет вперёд (день/неделя/месяц/квартал) |
| `dds.dim_date` | Готовый календарь на 5 лет вперёд (день/неделя/месяц/квартал) |
| `dds.fact_sales` | Факт «Продажа» — строка заказа с суммой и количеством |
💡 **Суррогатный ключ (Surrogate Key, SK)** — это `BIGINT`, который мы генерируем сами (например, `customer_sk = 1001`).
@@ -194,9 +199,10 @@ flowchart TD
---
### **DM (Data Mart / Gold/ «Витрины»)** «готово к употреблению»
### **DM (Data Mart / Gold / «Витрины»)**: «готово к употреблению»
Здесь — таблицы и представления для конкретных задач:
- `dm.mart_daily_sales` — ежедневные продажи по товарам и сегментам;
- `dm.mart_customer_360` — полный портрет клиента: сколько потратил, когда заходил, какие товары любит.
@@ -210,12 +216,13 @@ flowchart TD
> *«10 января 2024 года клиент из Москвы (сегмент Premium) купил Phone за 100 ₽»*.
В DWH это разложится на:
- **Факт (Fact)** — событие, которое можно измерить: *покупка*.
Хранится в `fact_sales`: `quantity = 1`, `amount = 100`.
- **Измерения (Dimensions)***контекст* факта:
- `dim_date` → 10 января 2024;
- `dim_customer` → Москва, Premium;
- `dim_product` → Phone.
- `dim_date` → 10 января 2024;
- `dim_customer` → Москва, Premium;
- `dim_product` → Phone.
```mermaid
erDiagram
@@ -259,9 +266,10 @@ erDiagram
}
```
### SCD Type 2 как хранить историю
### SCD Type 2: как хранить историю
Клиент №101:
- с 1 янв по 15 мая — `email = a@ex.com`, `city = Москва`;
- с 16 мая — `email = b@ex.com`, `city = Москва`;
- с 1 окт — `email = b@ex.com`, `city = Санкт-Петербург`.
@@ -281,55 +289,136 @@ AND (dim_customer.valid_to IS NULL OR fact_sales.order_date < dim_customer.valid
```
и получаем актуальный на тот день email и город.
> 🔍 Подробнее про SCD — в отдельной статье [Slow Changing Dimensions](SCD.md) (сравнение Type 1/2/3, паттерны обновления).
> 🔍 Подробнее про SCD — в отдельной статье [Slowly Changing Dimensions](SCD.md) (сравнение Type 1/2/3, паттерны обновления).
Теперь, когда мы разобрались, что такое факты, измерения и SCD, давайте посмотрим, как именно можно устроить слой DDS внутри — есть несколько вариантов.
---
## 6. Модели данных для слоя DDS: 4 подхода и когда какой выбрать
## 6. Модели данных для слоя DDS: 4 подхода, и когда какой выбрать
В DDS мы можем хранить данные по-разному. Это не «правильно/неправильно», а **выбор под задачу**.
### 1. 3NF (третья нормальная форма)
### 1. 3NF (третья нормальная форма)
*Источник: Билл Инмон (Bill Inmon)*
**Плюсы**:
- Минимум избыточности при строгих ключах и правилах дедупликации.
- Проще поддерживать единую терминологию и НСИ (reference data).
- Атрибуты и справочники легко расширять.
Если упростить, 3NF - это когда данные о разных бизнес-сущностях хранятся в отдельных таблицах и связываются ключами: клиент, заказ, город, регион, страна и т.д. Вместо одной большой таблицы с большим числом дублирующихся данных мы получаем цепочку таблиц, связанных ключами: `Заказ → Клиент → Город → Регион → Страна`. JOIN-ов становится больше, зато одно и то же свойство (например, название города) хранится в одном месте, а не дублируется в каждой строке заказа.
**Минусы**:
- Много JOIN даже для простых отчётов.
- Историчность (SCD2) усложняет таблицы.
- Новые источники дороже гармонизировать (привести к канону).
Исторически подход с ядром в 3NF чаще связывают с Биллом Инмоном (Bill Inmon): сначала проектируют корпоративную модель данных ядра в 3NF (сущности, атрибуты, связи), а уже поверх неё строят витрины.
📌 **Когда выбирать**:
→ Корпоративные DWH, где важна *единая терминология* и *долгосрочная поддержка*.
→ Стабильные домены (финансы, НСИ, договоры) и умеренная динамика изменений.
![Пример цепочки в 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)
### 2. Звезда (Star Schema)
*Источник: Ральф Кимболл (Ralph Kimball)*
**Плюсы**:
- **Простота**: факт + несколько «плоских» измерений;
- **Скорость**: BI-системы любят звезду — запросы пишутся за 5 минут;
- **Понятно бизнесу**: «продажи по товарам и клиентам» — это ровно то, что в таблицах.
Самый распространённый способ построения таблиц для слоя витрин. Именно эту модель мы использовали в [разделе 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`-ов - запросы обычно проще и быстрее;
* хорошо подходит для витрин под конкретные задачи.
**Минусы:**
* измерения денормализованы, поэтому атрибуты дублируются (например, название города повторяется у всех клиентов из этого города);
* изменения атрибутов могут требовать обновлять много строк в измерении.
📌 **Когда выбирать**:
→ Витрины (DM), а не ядро (DDS);
→ Начинающим командам и MVP;
→ Когда отчёты — главная цель.
---
### 3. Data Vault 2.0 «конструктор Lego» для больших DWH
### 3. Data Vault 2.0: «конструктор Lego» для больших DWH
*Идея: Дэн Линстедт (Dan Linstedt). Цель — так организовать хранилище, чтобы можно было спокойно добавлять новые источники и хранить историю, не ломая старую модель.*
@@ -346,88 +435,18 @@ AND (dim_customer.valid_to IS NULL OR fact_sales.order_date < dim_customer.valid
- **Satellite (Сателлит)***«какие у них свойства и как они менялись»*.
Имя клиента, email, статус заказа, цены — всё с историей изменений.
![Пример модели Data Vault](images/data-vault-small.jpg)
💡 **Главная мысль:**
идентичность, связи и атрибуты живут **в разных таблицах**, поэтому:
- историю проще хранить;
- новые источники проще прикручивать;
- меньше шансов «сломать» старые отчёты.
---
Data Vault хорошо подходит там, где много разнородных источников, нужна полная история изменений и прозрачный аудит. За гибкость приходится платить сложностью модели и количеством таблиц — поэтому для небольших проектов (2–5 источников, маленькая команда) DV почти наверняка избыточен.
#### Чем DV отличается от 3NF и Звезды
Если сильно упростить:
- В **3NF/Звезде** мы часто смешиваем:
- бизнес-ключ,
- текущие атрибуты,
- историю (SCD2)
— всё это в одной таблице измерения.
- В **Data Vault** это *разнесено*:
- Hub — только бизнес-ключ;
- Satellite — только атрибуты + история;
- Link — только связи между сущностями.
За это приходится платить сложностью модели и количеством таблиц. Зато DV хорошо выдерживает:
- много разнородных источников;
- «грязные» данные;
- жёсткие требования по аудиту и трассировке.
---
#### Raw Vault и Business Vault — два слоя
Часто говорят «Raw Vault» и «Business Vault». Грубо:
- **Raw Vault** — «как прилетело из источников».
Хабы, линкы и сателлиты, максимально близкие к исходным данным.
Задача: надёжно собрать и сохранить **полную историю**.
- **Business Vault** — «как удобно считать дальше».
На основе Raw Vault появляются:
- служебные таблицы (PIT, Bridge и т.п.),
- подготовленные представления под витрины и отчёты,
- бизнес-правила (например, что считать «активным клиентом»).
Дальше поверх этого уже строятся **обычные витрины в формате Звезды**, с которыми работают аналитики.
Если примерить это к классическим слоям `stg → ods → dds → dm`, то **очень грубо** можно думать так:
- `stg` всё равно остаётся как «приземление» (landing) из источников;
- **Raw Vault** по духу ближе к **ODS**: мало бизнес-логики, зато полная история и интеграция из разных систем;
- **Business Vault** ближе к **DDS**: здесь уже живут бизнес-правила и подготовка данных к витринам;
- `dm` по-прежнему остаётся витринами в формате Звезды, с которыми работают аналитики и BI.
Важно: это именно *аналогия для понимания*, а не жёсткое правило проектирования.
---
#### Когда DV вам, скорее всего, рано
Если у вас:
- 25 источников,
- небольшая команда (1–2 инженера + аналитик),
- задачи уровня «сделать первые отчёты»,
то **Data Vault почти наверняка избыточен**.
Чаще всего хватает связки:
> `stg → ods → dds (3NF или простая Звезда с SCD2) → dm (Звезда)`
---
#### Что важно запомнить из этой статьи
Для этой статьи достаточно:
- знать, что **Data Vault** — это способ строить хранилище как **конструктор из Hub/Link/Satellite**,
- понимать, что он нужен в первую очередь там, где:
- много систем-источников,
- нужна *полная* история и прозрачный аудит.
Детали (Raw vs Business Vault, PIT/Bridge, DV 1.0 vs 2.0 и т.п.) — это уже тема для отдельной, взрослой статьи.
> 🔍 Подробнее про Data Vault — сравнение с 3NF/Звездой, Raw и Business Vault, когда внедрять — в отдельной статье [DataVault: как пережить бурную жизнь источников](DataVault.md).
---
@@ -435,27 +454,21 @@ AND (dim_customer.valid_to IS NULL OR fact_sales.order_date < dim_customer.valid
*Источник: Ларс Рёне (Lars Rönnbäck)*
Ещё более атомарный подход:
- **Anchor** — сущность (клиент, товар);
- **Attribute** — атрибут (email, имя);
- **Tie** — связь (как Link в DV);
- Все таблицы — 2–3 столбца.
Anchor Modeling - ещё более атомарный подход к моделированию ядра, чем Data Vault. Если упростить, он «режет» модель на очень мелкие части, чтобы изменения в атрибутах и связях можно было добавлять почти без переделок схемы.
**Плюсы**:
- **Максимальная гибкость**: поменяли модель — не трогали старые таблицы;
- **Бесконечная эволюция**: можно добавлять атрибуты «задним числом».
Основные типы таблиц:
**Минусы**:
- Очень сложные запросы (JOIN’ов — десятки);
- Почти не используется «в чистом виде» — чаще как концепция.
* **Anchor** - сущности (например, «клиент» или «заказ»).
* **Attribute** - отдельный атрибут сущности, обычно с историей (например, email, город, статус - каждый в своей таблице).
* **Tie** - связь между сущностями (например, «клиент ↔ заказ»).
📌 **Когда выбирать**:
→ Экспериментальные проекты;
→ Когда схема данных *каждый месяц* радикально меняется.
![Пример Anchor Modeling](images/anchor-model-small.jpg)
Плюс подхода - высокая гибкость: проще добавлять новые атрибуты и варианты связей. Минус - цена этой гибкости: получается очень много таблиц, и запросы (и поддержка модели) обычно заметно сложнее, огромное кол-во `JOIN`. Для обычного DWH-проекта это точно не первый выбор - скорее вариант для очень динамичных предметных областей, где структура данных часто меняется.
---
### Сравнение моделей наглядно
### Сравнение моделей наглядно
```mermaid
quadrantChart
@@ -527,7 +540,7 @@ flowchart TD
### Готовые SQL-скрипты
Все необходимые скрипты для построения хранилища находятся в папке [`sql/`](sql/):
Все необходимые скрипты для построения хранилища находятся в папке [`sql/`](https://github.com/dementev-dev/de-roadmap/tree/main/dwh-modeling/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;
@@ -540,11 +553,17 @@ flowchart TD
```sql
-- mart_daily_sales: ежедневные продажи с сегментацией
CREATE MATERIALIZED VIEW dm.mart_daily_sales AS
-- Полная пересборка (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 AS order_date,
d.date_actual,
p.product_name,
c.customer_segment, -- например: '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
@@ -553,13 +572,14 @@ JOIN dds.dim_date d
JOIN dds.dim_product p
ON f.product_sk = p.product_sk
JOIN dds.dim_customer c
ON f.customer_sk = c.customer_sk
AND f.order_date >= c.valid_from
AND (c.valid_to IS NULL OR f.order_date < c.valid_to) -- SCD!
GROUP BY d.date_actual, p.product_name, c.customer_segment;
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)** — это «кэш» результата. Обновляется по расписанию (например, ночью).
> 💡 В продакшене витрину иногда оформляют как **MATERIALIZED VIEW** - «кэш» результата запроса, который обновляется по расписанию. В нашем примере используем обычную таблицу с `TRUNCATE` + `INSERT` - для учебных целей это нагляднее.
✏️ **Попробуйте сами:** [Домашка: статусы клиента от STG до DDS (и немного DM)](Homework_Customer_Status_DDS_DM.md) — пройдёте тот же путь, но самостоятельно.
---
@@ -585,7 +605,7 @@ GROUP BY d.date_actual, p.product_name, c.customer_segment;
---
### ✅ Базовые советы с чего начать, если вы учитесь или делаете первый DWH
### ✅ Базовые советы: с чего начать, если вы учитесь или делаете первый DWH
1. **Начните с витрины в формате Звезды (Star Schema).**
— Это просто: одна таблица фактов + несколько «плоских» измерений.
@@ -612,7 +632,7 @@ GROUP BY d.date_actual, p.product_name, c.customer_segment;
Почему: ему нужны готовые метрики без сложных JOIN’ов. Звезда даёт понятные таблицы: «продажи по дням и товарам» — без углубления в атомарные сущности.
**BI-разработчик в Power BI / Tableau****Звезда**
Почему: все инструменты визуализации оптимизированы под star schema. Один факт + несколько измерений = быстрые отчёты и простою модель.
Почему: все инструменты визуализации оптимизированы под star schema. Один факт + несколько измерений = быстрые отчёты и простую модель.
**Инженер ML (Data Scientist / ML-инженер)****3NF или сырые ODS-таблицы**
Почему: для фичей нужны атомарные события и детальные атрибуты. Машинное обучение ценит полноту и детализацию данных больше, чем удобство отчётов.
@@ -640,7 +660,7 @@ GROUP BY d.date_actual, p.product_name, c.customer_segment;
---
### 📌 Кратко что выбрать *сегодня*, если вы только учитесь
### 📌 Кратко: что выбрать *сегодня*, если вы только учитесь
| У вас… | Делайте… |
|--------|----------|
@@ -652,7 +672,7 @@ GROUP BY d.date_actual, p.product_name, c.customer_segment;
---
## 9. Эксплуатация: качество данных это не «опция»
## 9. Эксплуатация: качество данных, это не «опция»
Самая красивая архитектура бессмысленна, если в `mart_daily_sales` — нули.
Поэтому в каждом слое — **контроль качества (DQ, Data Quality)**.
@@ -695,7 +715,7 @@ SELECT 'OK' WHERE EXISTS (
---
## 10. Заключение: главное понимать «почему»
## 10. Заключение: главное, понимать «почему»
Хранилище данных — это не про «крутые технологии», а про **мышление**:
@@ -771,11 +791,11 @@ SELECT 'OK' WHERE EXISTS (
### Мини-датасет (для практики)
Все данные для практики находятся в папке [`data/`](data/) — тренируйтесь:
Все данные для практики находятся в папке [`data/`](https://github.com/dementev-dev/de-roadmap/tree/main/dwh-modeling/data) — тренируйтесь:
[`customers.csv`](data/customers.csv):
```csv
customer_id,email,phone,city,event_ts,_load_id,load_ts
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
@@ -812,7 +832,7 @@ product_id,valid_from,valid_to,price
9002,2023-01-01,,50
```
> 📂 Все SQL-скрипты для построения хранилища находятся в папке [`sql/`](sql/).
> 📂 Все SQL-скрипты для построения хранилища находятся в папке [`sql/`](https://github.com/dementev-dev/de-roadmap/tree/main/dwh-modeling/sql).
---
+8 -7
View File
@@ -28,15 +28,15 @@ SCD — это подход к хранению изменений в измер
---
## 3. Типы SCD простыми словами
## 3. Типы SCD простыми словами
Существует несколько стандартных стратегий обработки изменений. Рассмотрим самые важные.
### **Type 0Никогда не меняется**
### **Type 0: никогда не меняется**
Атрибут фиксирован навсегда. Например, дата рождения клиента.
Такие поля не требуют специальной обработки — они просто не обновляются.
### **Type 1 — Просто перезаписать**
### **Type 1: просто перезаписать**
Вы просто делаете `UPDATE`, и старое значение исчезает.
✅ Просто.
@@ -44,7 +44,7 @@ SCD — это подход к хранению изменений в измер
> Подходит, если изменение — это исправление ошибки (например, опечатка в имени).
### **Type 2Новая строка для новой версии**
### **Type 2: новая строка для новой версии**
Каждое изменение порождает **новую строку** в таблице. Старая строка остаётся, но помечается как «устаревшая».
✅ Полная история.
@@ -53,7 +53,7 @@ SCD — это подход к хранению изменений в измер
> Это **самый распространённый** подход в аналитике.
### **Type 3 — Добавить колонку «предыдущее значение»**
### **Type 3: добавить колонку «предыдущее значение»**
В таблице появляются поля вроде `previous_category`, `category_change_date`.
✅ Простая история «до/после».
@@ -61,7 +61,7 @@ SCD — это подход к хранению изменений в измер
> Используется редко, чаще как компромисс в очень простых системах.
### **Type 4, 5, 6 — Продвинутые гибриды**
### **Type 4, 5, 6: продвинутые гибриды**
Эти типы существуют, но **встречаются редко** и почти не используются новичками:
- **Type 4**: история выносится в отдельную таблицу («мини-хранилище» для одного измерения).
@@ -99,7 +99,7 @@ WHERE customer_id = 1;
---
### Type 2: сохраняем историю подробнее
### Type 2: сохраняем историю (подробнее)
Чтобы хранить историю, мы меняем структуру таблицы. Вот ключевые поля:
@@ -283,6 +283,7 @@ LEFT JOIN current_customers c ON n.customer_id = c.customer_id
###### Шаг 3: Вставка новых версий
Для подходящих записей создаём новую версию:
- `uuid()` — генерируем уникальный ключ для новой версии
- `current_date` - функция, возвращающая текущую даты
- `COALESCE(n.effective_date, current_date)` — устанавливаем дату начала действия новой версии
+1 -1
View File
@@ -1,4 +1,4 @@
customer_id,status,event_ts,_load_id,load_ts
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
1 customer_id status event_ts _load_id load_ts _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
@@ -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
+1 -1
View File
@@ -1,4 +1,4 @@
customer_id,email,phone,city,event_ts,_load_id,load_ts
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
1 customer_id email phone city event_ts _load_id load_ts _load_ts
2 101 a@ex.com 700 Москва 2024-01-01 batch_20240101_0800 2024-01-01 08:00 2024-01-01 08:00
3 102 c@ex.com 701 СПб 2024-01-01 batch_20240101_0800 2024-01-01 08:00 2024-01-01 08:00
4 101 b@ex.com 700 Москва 2024-05-16 batch_20240516_0800 2024-05-16 08:00 2024-05-16 08:00
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

+7
View File
@@ -2,6 +2,13 @@
-- 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), либо используется партицирование по дате
+1 -1
View File
@@ -21,7 +21,7 @@ CREATE TABLE dm.mart_customer_360 (
customer_bk INT NOT NULL,
first_order_date DATE,
last_order_date DATE,
total_orders INT NOT NULL,
total_line_items INT NOT NULL,
total_items INT NOT NULL,
lifetime_value NUMERIC(18,2) NOT NULL,
last_email VARCHAR(100),
+2 -2
View File
@@ -33,14 +33,14 @@ GROUP BY d.date_actual, p.product_name,
-- Считаем суммы по всей истории его покупок
INSERT INTO dm.mart_customer_360 (
customer_bk, first_order_date, last_order_date,
total_orders, total_items, lifetime_value,
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_orders, -- считаем строки факта (продажи), не бизнес-заказы
COUNT(DISTINCT f.sale_id) AS total_line_items, -- строки факта (позиции продаж), не бизнес-заказы
SUM(f.quantity) AS total_items,
SUM(f.amount) AS lifetime_value,
-- Берём самый свежий email и город клиента
@@ -46,3 +46,11 @@ ALTER TABLE dds.dim_customer_status
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
);
@@ -38,11 +38,6 @@
-- Можно ориентироваться на примеры в 03_demo_increment.sql.
-- 4. DM: витрина статусов клиентов по датам (по желанию)
-- Пример целевой структуры:
-- CREATE TABLE dm.mart_customer_status_daily (
-- date_actual DATE NOT NULL,
-- status VARCHAR(20) NOT NULL,
-- customers_cnt INT NOT NULL
-- );
-- 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;
+29
View File
@@ -0,0 +1,29 @@
# Конфигурация lychee — CI-проверка внешних ссылок
# Используется в .github/workflows/check-links.yml (подхватывается автоматически)
# Локальный запуск: docker run --rm -v "$PWD:/input" -w /input lycheeverse/lychee './**/*.md'
# Служебные каталоги и каталоги, исключённые из сайта
exclude_path = ["project", "site"]
# Локальные адреса стендов (127.0.0.1, localhost и т.п.)
exclude_all_private = true
exclude = [
# Режут ботов и датацентровые IP (проверять вручную из браузера)
"^https?://habr\\.com",
"^https?://stepik\\.org",
"^https?://leetcode\\.com",
"^https?://realpython\\.com",
# YouTube в CI ненадёжен: 429 на пачку запросов, удалённые видео отдают 200
"^https?://(www\\.)?youtube\\.com",
"^https?://youtu\\.be",
# Telegram отдаёт 200 даже для несуществующих каналов
"^https?://t\\.me",
]
# 429 (rate limit) не считаем битой ссылкой
accept = ["200..=204", "429"]
max_retries = 2
timeout = 30
user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"
+89
View File
@@ -0,0 +1,89 @@
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://git.dementev.space/ddmitry/de-roadmap
repo_name: ddmitry/de-roadmap
docs_dir: .
site_dir: site
exclude_docs: |
project/
postgres-bookings/
.github/
.gitea/
.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 → ODS → 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: simple/gitea
link: https://git.dementev.space/ddmitry/de-roadmap
name: Gitea
- icon: fontawesome/brands/telegram
link: https://t.me/dementev_dev
name: Написать в Telegram
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 %}
+353
View File
@@ -0,0 +1,353 @@
# ADR: Архитектура сайта de-roadmap
> Архитектурный документ. Проектные цели и требования — в [PRD](./PRD.md).
> Решения о публикации и custom domain заменены спецификацией
> [«Публикация сайта через Gitea Actions и VPS»](./specs/2026-08-04-gitea-vps-site-publishing.md).
> Решения о MkDocs, структуре файлов и ссылках остаются актуальными.
---
## 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).
+173
View File
@@ -0,0 +1,173 @@
# PRD: Сайт для de-roadmap
> Проектный документ. Техническая архитектура — в отдельном ADR.
---
## 1. Контекст и мотивация
### Текущее состояние
Роадмап по Data Engineering живёт как Git-репозиторий. Основной origin
размещён в собственной Gitea
([ddmitry/de-roadmap](https://git.dementev.space/ddmitry/de-roadmap)), а
GitHub Pages сохраняется как резерв после восстановления доступа к GitHub:
- Основной контент — монолитный `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] Публикация без дополнительных расходов: собственная VPS как основной
контур, 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 — миграция на другой генератор за день |
| Накладные расходы на поддержку растут | Низкая | Среднее | Принцип «сайт опционален»: если мешает — удаляем конфиг, репо работает |
| VPS временно недоступна | Низкая | Высокое | Опубликованный сайт не зависит от Gitea; GitHub Pages сохраняется как ручной резерв |
---
## 8. Открытые вопросы
1. **Домен.** Использовать GitHub Pages URL или сразу подключить кастомный домен? Можно решить в Спринте 2.
2. **README.md в корне vs docs/.** Оставляем README.md в корне (GitHub его рендерит) и используем его же как `index.md` для сайта, или делаем симлинк / копию? → Решение в ADR.
3. **Структура `docs/` директории.** Нужно ли вообще создавать `docs/`, или генератор может работать прямо с корнем репо? → Решение в ADR.
+63
View File
@@ -0,0 +1,63 @@
# TODO: общие задачи проекта
Приоритеты: P1 — делаем в первую очередь; P2 — полезно, когда дойдут руки; P3 — идеи под вопросом.
## P1
- [ ] **Перенести публикацию сайта на Gitea Actions и VPS.**
Настроить repository-scoped host runner, строгую сборку MkDocs, атомарные
релизы, nginx и TLS для `de.dementev.space` по согласованной спецификации.
## P2
- [ ] **Перенос учебника Airflow в de-roadmap.**
Перенести 9 глав учебника из `airflow-manual` в `airflow/` (de-roadmap).
В `airflow-manual` оставить только стенд (`airflow-docker/`).
Добавить навигацию в `mkdocs.yml`, обеспечить dual-compatible links.
- [ ] **Добавить счётчик Google Analytics** на сайт (MkDocs Material
поддерживает GA через `extra.analytics` в `mkdocs.yml`).
- [ ] **Ориентиры трудозатрат по блокам.**
Одна строка на блок («~N часов»), по образцу курса Lakehouse (~1215 часов).
Менти всегда спрашивают «сколько займёт»; ориентир защищает от провала
в практику на месяцы.
- [ ] **Шаблон прогресса менти.**
Файл-чеклист по критериям «когда блок считаем пройденным»; менти копирует
в свой форк и ведёт коммитами. Живая практика Git с первой недели +
прозрачный прогресс для ментора.
## P3
- [ ] **Свой dbt-стенд.**
Сейчас практика dbt — на чужом jaffle-shop; единственная секция без
собственного стенда. Вариант: dbt-модели поверх postgres-bookings или
clickstream-стенда.
- [ ] **Абзац про Data Quality в курсовой.**
Валидационный DAG курсовой — это и есть DQ на практике; добавить абзац,
как об этом говорить на собеседовании. Новая секция не нужна.
- [ ] **Иконки/бейджи статуса разделов** (пройден / в процессе / не начат) —
декоративные, без бэкенда. Сомнение: статус у каждого менти свой,
пересекается с идеей шаблона прогресса — возможно, отпадёт.
## Сделано
- [x] **Подраздел «Linux и терминал» в блоке базовых инструментов**
2026-07-12: видео-интро («Девопс на троечку», покрытие проверено по
субтитрам) + три статьи (навигация и grep — habr, права — FirstVDS,
ssh — Cloud.ru) + опциональный интерактивный курс Hexlet, примечание про
WSL для Windows, два новых критерия готовности блока, Linux добавлен
в строку оглавления. Отдельной практики нет — ею служат стенды.
- [x] **CI-проверка внешних ссылок** — 2026-07-12: `lychee.toml` + workflow
`check-links.yml` (еженедельно по понедельникам, при битых ссылках создаёт
issue). Игнор-лист: habr, stepik, leetcode, realpython (режут ботов),
YouTube и t.me (проверка ненадёжна). Попутно исправлена битая ссылка на
русские доки Python в README (перевод `/ru/` на docs.python.org умер целиком).
- [x] **Убрать раздел «Понятие сложности алгоритмов» из README**
2026-07-12, коммит `6f61c0e`: асимптотика перенесена в Python ссылкой
на разбор Big O (habr), по SQL — без дублирования, тему покрывает QPT.
+165
View File
@@ -0,0 +1,165 @@
# Эксплуатация публикации `de.dementev.space`
Этот runbook реализует спецификацию
[`2026-08-04-gitea-vps-site-publishing.md`](../../specs/2026-08-04-gitea-vps-site-publishing.md).
Команды рассчитаны на Ubuntu 26.04 и Gitea 1.27.
## Зафиксированные параметры
- Gitea Runner: `2.3.0`, Linux amd64.
- SHA256: `1e9fb1bea022fdf40993ecbc1a13e87db1bfd3d7f42666e37f15f71d840d53b3`.
- Runner: repository-scoped, метка `de-roadmap-host:host`.
- Пользователь сервиса: `gitea-runner`, без `sudo` и Docker.
- Корень публикации: `/srv/de-roadmap`.
- Хранение: текущий релиз и две предыдущие версии.
- Окружение сборки: `/var/lib/gitea-runner/venvs/site`.
## Подготовка VPS
Установить HTTP-сервер и Certbot:
```bash
sudo apt-get update
sudo apt-get install nginx certbot python3-certbot-nginx python3-venv
```
Создать пользователя и каталоги:
```bash
sudo useradd \
--system \
--home-dir /var/lib/gitea-runner \
--create-home \
--shell /usr/sbin/nologin \
gitea-runner
sudo install -d -o gitea-runner -g gitea-runner -m 0755 \
/var/lib/gitea-runner/workspaces \
/srv/de-roadmap \
/srv/de-roadmap/releases
sudo install -d -o root -g root -m 0755 /etc/gitea-runner
```
Скачать runner во временный каталог, сверить checksum и установить root-owned
бинарник в `/usr/local/bin/gitea-runner`:
```bash
runner_tmp_dir=$(mktemp -d /tmp/de-roadmap-runner.XXXXXX)
curl -fsSLo "${runner_tmp_dir}/gitea-runner" \
https://dl.gitea.com/gitea-runner/2.3.0/gitea-runner-2.3.0-linux-amd64
printf '%s %s\n' \
'1e9fb1bea022fdf40993ecbc1a13e87db1bfd3d7f42666e37f15f71d840d53b3' \
"${runner_tmp_dir}/gitea-runner" \
| sha256sum --check
sudo install -o root -g root -m 0755 \
"${runner_tmp_dir}/gitea-runner" /usr/local/bin/gitea-runner
rm "${runner_tmp_dir}/gitea-runner"
rmdir "$runner_tmp_dir"
```
Из корня репозитория установить конфигурацию и unit:
```bash
sudo install -o root -g root -m 0644 \
project/ops/gitea-vps-site/gitea-runner.yaml \
/etc/gitea-runner/config.yaml
sudo install -o root -g root -m 0644 \
project/ops/gitea-vps-site/gitea-runner.service \
/etc/systemd/system/gitea-runner.service
sudo systemd-analyze verify /etc/systemd/system/gitea-runner.service
```
## Регистрация runner
Repository registration token получают в Gitea:
`ddmitry/de-roadmap` → Settings → Actions → Runners. Токен не сохраняют в Git
или shell history. Временный файл с токеном создают с владельцем
`gitea-runner:gitea-runner` и режимом `0600`. После регистрации файл
`/var/lib/gitea-runner/.runner` должен принадлежать тому же пользователю и
иметь режим `0600`.
Token-file должен содержать ровно 40 символов без завершающего перевода строки.
При извлечении JSON-ответа через `jq` использовать `jq --join-output '.token'`,
а не `jq --raw-output`, который добавляет newline.
```bash
sudo -u gitea-runner \
/usr/local/bin/gitea-runner \
--config /etc/gitea-runner/config.yaml \
register \
--no-interactive \
--instance https://git.dementev.space \
--token-file /var/lib/gitea-runner/.registration-token \
--name de-roadmap-vps
sudo rm /var/lib/gitea-runner/.registration-token
sudo chmod 0600 /var/lib/gitea-runner/.runner
sudo systemctl daemon-reload
sudo systemctl enable --now gitea-runner
```
Временный файл с токеном удаляют сразу после успешной регистрации.
## Nginx и первичная публикация
Из корня репозитория установить virtual host, создать ссылку и только затем
перечитать проверенную конфигурацию:
```bash
sudo install -o root -g root -m 0644 \
project/ops/gitea-vps-site/nginx.conf \
/etc/nginx/sites-available/de-roadmap
sudo ln -s \
/etc/nginx/sites-available/de-roadmap \
/etc/nginx/sites-enabled/de-roadmap
sudo nginx -t
sudo systemctl reload nginx
```
Default-сайт можно отключить только после успешной проверки нового virtual
host.
До первого workflow можно собрать сайт вручную и опубликовать его тем же
скриптом с тестовым release id. Проверка до переключения DNS:
```bash
curl --header 'Host: de.dementev.space' http://127.0.0.1/
```
После локальной проверки разрешить профили `Nginx Full` в UFW. До этого
публичные порты `80/tcp` и `443/tcp` должны оставаться закрытыми.
## Окружение сборки
Скрипт `.gitea/scripts/build-site.sh` создаёт persistent venv при первом запуске
и переиспользует его в следующих сборках. `pip install` выполняется каждый раз,
чтобы применить изменения `.gitea/requirements-site.txt`, но уже установленные
версии пакетов не переустанавливаются.
## DNS и TLS
1. Уменьшить TTL записи `de.dementev.space`.
2. Направить `A` на VPS; удалить или корректно направить `AAAA`.
3. Убедиться, что сайт доступен извне по HTTP.
4. Выпустить сертификат:
```bash
sudo certbot --nginx -d de.dementev.space
```
5. Проверить HTTPS и `systemctl status certbot.timer`.
6. Вернуть обычный DNS TTL.
## Проверка и откат
Активная версия определяется ссылкой `/srv/de-roadmap/current`. Для ручного
отката создать временную ссылку на нужный каталог в `releases/` и атомарно
заменить `current` через `mv -Tf`. Перед удалением релиза всегда проверять
результат `readlink -f /srv/de-roadmap/current`.
Диагностика:
```bash
systemctl status gitea-runner
journalctl -u gitea-runner
nginx -t
curl --header 'Host: de.dementev.space' http://127.0.0.1/
```
@@ -0,0 +1,45 @@
[Unit]
Description=Gitea Actions runner for de-roadmap
Documentation=https://docs.gitea.com/usage/actions
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=gitea-runner
Group=gitea-runner
WorkingDirectory=/var/lib/gitea-runner
ExecStart=/usr/local/bin/gitea-runner daemon --config /etc/gitea-runner/config.yaml
Restart=always
RestartSec=10s
TimeoutStopSec=45s
KillMode=mixed
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
UMask=0022
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectSystem=strict
ReadWritePaths=/var/lib/gitea-runner /srv/de-roadmap
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
CapabilityBoundingSet=
SystemCallArchitectures=native
MemoryMax=1G
CPUQuota=100%
TasksMax=128
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,37 @@
log:
level: info
runner:
file: /var/lib/gitea-runner/.runner
capacity: 1
timeout: 15m
shutdown_timeout: 30s
insecure: false
fetch_timeout: 5s
fetch_interval: 2s
fetch_interval_max: 10s
workdir_cleanup_age: 24h
idle_cleanup_interval: 10m
labels:
- "de-roadmap-host:host"
allocate_pty: false
cache:
enabled: false
container:
valid_volumes: []
docker_host: "-"
require_docker: false
host:
workdir_parent: /var/lib/gitea-runner/workspaces
health_check:
enabled: true
min_free_disk_space_mb: 1024
interval: 30s
timeout: 10s
metrics:
enabled: false
+21
View File
@@ -0,0 +1,21 @@
server {
listen 80;
listen [::]:80;
server_name de.dementev.space;
root /srv/de-roadmap/current;
index index.html;
charset utf-8;
server_tokens off;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
try_files $uri $uri/ =404;
}
location ~ /\. {
deny all;
}
}
@@ -0,0 +1,114 @@
# Публикация сайта через Gitea Actions и VPS
Статус: согласовано 2026-08-04; механизм синхронизации GitHub-резерва
требует отдельного решения после восстановления доступа к GitHub.
## Проблема
Сайт `de.dementev.space` публиковался через GitHub Actions и GitHub Pages. После блокировки учётной записи GitHub основной домен стал недоступен, хотя исходный Markdown и конфигурация MkDocs сохранились, а основной Git-репозиторий уже размещён в собственной Gitea.
Публикация сайта не должна зависеть от доступности учётной записи GitHub. При этом рабочий процесс из исходной архитектуры сохраняется: изменение попадает в `main`, сайт собирается и обновляется автоматически без отдельного ручного деплоя.
## Цели
- Публиковать `de.dementev.space` с существующей VPS.
- Запускать сборку и публикацию автоматически после push в `main` в Gitea.
- Не заменять работающую версию сайта, если получение исходников или строгая сборка MkDocs завершились ошибкой.
- Ограничить права runner каталогами, необходимыми для сборки и публикации сайта.
- Сохранить GitHub Pages как заранее собираемый резерв на случай проблем с VPS.
- Сохранить минимальные накладные расходы и нулевые дополнительные расходы на хостинг.
## Не цели
- Автоматическое переключение между VPS и GitHub Pages.
- Жёсткие требования к времени восстановления или высокой доступности.
- Перенос еженедельной проверки внешних ссылок с GitHub Actions в Gitea Actions.
- Изменение существующих файлов в `.github/workflows/`.
- Добавление динамического приложения, авторизации или серверной базы данных.
## Текущее состояние
- MkDocs Material собирает статический каталог `site/`; `site_url` уже равен `https://de.dementev.space/`.
- Публичный репозиторий `ddmitry/de-roadmap` в Gitea является текущим `origin`.
- Gitea Actions для репозитория включены, но подходящего runner пока нет.
- Gitea видит существующие GitHub workflows, однако они зависят от GitHub Pages, GitHub-токенов и сторонних actions.
- На VPS, выбранной для сайта, пока нет HTTP-сервера; публично доступен только SSH.
## Выбранное решение
### Основной контур публикации
Gitea становится источником событий для основной публикации. На VPS с сайтом работает repository-scoped Gitea runner, зарегистрированный только для `de-roadmap`. Runner использует host mode и отдельную метку, предназначенную только для этого workflow.
Workflow хранится отдельно в `.gitea/workflows/deploy-site.yml`. Gitea выбирает `.gitea/workflows` раньше `.github/workflows`, поэтому GitHub-specific workflows сохраняются в репозитории, но не исполняются Gitea.
Workflow запускается после push в `main` и вручную. Он получает конкретный commit из публичного Gitea-репозитория обычным Git, создаёт изолированное Python-окружение, устанавливает закреплённые версии зависимостей и выполняет строгую сборку MkDocs. Сторонние `uses:` не применяются, поэтому сборка не зависит от GitHub Actions Marketplace.
### Изоляция runner
Runner работает как отдельный непривилегированный системный пользователь. У него нет `sudo`, членства в группе `docker` и доступа на запись к конфигурации nginx, сертификатам или другим сервисам VPS.
Пользователь runner может записывать только в собственный рабочий каталог и каталог релизов сайта. Workflow не запускается для pull request из недоверенных веток. Потребление памяти, CPU и количество процессов ограничиваются средствами менеджера сервисов операционной системы.
### Публикация и восстановление после ошибки
Каждая успешная сборка создаёт отдельную версию статического сайта. Новая версия становится активной только после завершения всех проверок. Переключение между версиями выполняется атомарно; частично собранный каталог никогда не становится корнем сайта.
Ошибка получения исходников, установки зависимостей или сборки оставляет активной предыдущую версию. Как минимум одна предыдущая успешная версия сохраняется для быстрого ручного отката.
### HTTP и HTTPS
Статические файлы обслуживает штатный nginx из репозитория Ubuntu. Nginx только читает активную версию сайта и не требует перезагрузки при обычной публикации контента.
TLS-сертификат для `de.dementev.space` получает и продлевает Certbot с интеграцией nginx. Перед переключением проверяются локальная сборка и конфигурация nginx, а DNS TTL заранее уменьшается. Затем DNS направляется на VPS, проверяется публичная доступность по HTTP и выпускается сертификат. Короткий интервал между переключением DNS и готовностью HTTPS допустим, поскольку жёсткого требования к непрерывной доступности нет.
### Резерв на GitHub Pages
После восстановления доступа к GitHub существующий GitHub workflow продолжает собирать сайт из GitHub-репозитория. Чтобы резерв оставался актуальным, изменения из основной Gitea должны автоматически синхронизироваться с GitHub. Предпочтительный кандидат — встроенный Gitea push mirror; окончательный механизм и его права будут согласованы отдельно после восстановления учётной записи. Резерв доступен по стандартному адресу GitHub Pages, но основной домен направлен на VPS.
GitHub Pages считается тёплым резервом контента и холодным резервом домена. При отказе VPS владелец вручную переключает DNS и при необходимости повторно активирует custom domain в настройках GitHub Pages. Допустимы задержка распространения DNS и ожидание выпуска TLS-сертификата; автоматический failover не требуется.
## Отклонённые варианты
- **Периодический pull с VPS:** проще инфраструктурно, но не даёт опыта работы с Gitea runner и менее наглядно связывает push с результатом сборки.
- **Runner рядом с Gitea:** усложняет доставку результата на VPS и позволяет сборкам влиять на ресурсы Git-сервера.
- **Jobs в Docker:** дают лучшую изоляцию, но требуют доступа runner к Docker и отдельного механизма передачи результата в каталог nginx. Для одного доверенного репозитория это лишняя сложность.
- **Caddy вместо nginx:** упрощает автоматический TLS, но штатный nginx лучше соответствует предпочтению владельца и доступен с обновлениями безопасности из основного репозитория Ubuntu. Certbot закрывает задачу TLS отдельно.
- **Cloudflare Pages, GitLab Pages или другой внешний Pages-сервис:** уменьшают нагрузку на VPS, но добавляют новую внешнюю учётную запись и зависимость, от которой как раз уходим.
- **Ожидание разблокировки GitHub:** сохраняет старую архитектуру, но оставляет сайт недоступным на неопределённый срок.
## Риски и меры
| Риск | Мера |
|------|------|
| Workflow исполняет команды непосредственно на VPS | Repository-scoped runner, отдельный пользователь без `sudo` и Docker, узкие права на запись, запуск только из `main`, системные лимиты ресурсов |
| Ошибка сборки ломает опубликованный сайт | Сборка в отдельной версии и атомарное переключение только после успеха |
| Сторонний action выполняет неожиданный код | Workflow состоит из собственных команд и не использует `uses:` |
| Gitea временно недоступна | Уже опубликованный сайт обслуживается независимо от Gitea |
| VPS недоступна или потеряна | Исходники остаются в Gitea; GitHub Pages служит ручным резервом после восстановления GitHub |
| После переключения DNS HTTPS ещё не готов | DNS TTL уменьшается заранее; Certbot запускается сразу после подтверждения публичного HTTP; короткий перерыв принят как допустимый |
| Сертификат GitHub Pages не готов во время аварии | Допускается задержка; на время восстановления используется стандартный адрес GitHub Pages |
| GitHub-резерв отстаёт от Gitea | После восстановления GitHub настраивается автоматическая синхронизация; до выбора механизма резерв не считается тёплым |
## Проверка реализации
- Runner после перезагрузки VPS автоматически подключается к Gitea и принимает только jobs с выделенной меткой.
- Push тестового изменения в `main` запускает ровно один Gitea workflow и публикует соответствующий commit.
- Gitea не запускает workflows из `.github/workflows/`, а сами файлы остаются без изменений.
- Workflow не обращается к GitHub за actions и не требует GitHub-токенов.
- Ошибка `mkdocs build --strict` завершает workflow с ошибкой и не изменяет публичную версию сайта.
- Успешная сборка переключает сайт целиком, без периода частично обновлённого содержимого.
- Пользователь runner не может использовать `sudo`, Docker или изменять конфигурацию nginx.
- `https://de.dementev.space/` отдаёт собранный сайт с действующим сертификатом после переключения DNS.
- Nginx и runner восстанавливаются после перезагрузки VPS без ручного запуска.
- После восстановления GitHub и настройки синхронизации резервная сборка получает тот же commit и остаётся доступна по стандартному адресу GitHub Pages.
## Влияние на документацию
Эта спецификация заменяет решения о публикации и custom domain из `project/ADR.md`. Решения того документа о MkDocs Material, структуре файлов и dual-compatible links остаются актуальными.
После реализации нужно обновить `AGENTS.md`, `project/PRD.md` и `project/TODO.md`, чтобы они описывали фактический основной контур публикации. Ссылку на исходный репозиторий в `mkdocs.yml` следует направить на Gitea; ссылку на GitHub можно сохранить как дополнительную после восстановления доступа.
## Открытый вопрос
После восстановления GitHub нужно окончательно выбрать способ автоматической синхронизации резервного репозитория. Базовый кандидат — Gitea push mirror с синхронизацией при каждом push; GitHub при этом становится read-only зеркалом, поскольку mirror перезаписывает расходящиеся изменения.