43 changed files with 393 additions and 2994 deletions
-2
View File
@@ -1,2 +0,0 @@
mkdocs-material==9.6.14
mkdocs-same-dir==0.1.3
-31
View File
@@ -1,31 +0,0 @@
#!/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
@@ -1,75 +0,0 @@
#!/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
@@ -1,39 +0,0 @@
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
@@ -1,54 +0,0 @@
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
@@ -1,43 +0,0 @@
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,5 +1,3 @@
.env .env
.idea .idea
.internal/ .internal/
site/
tmp
+5 -54
View File
@@ -1,15 +1,9 @@
# Repository Guidelines # Repository Guidelines
## Project Structure & Module Organization ## Project Structure & Module Organization
- Root `README.md` describes the learning roadmap (RU) and serves as the main page of the MkDocs site. - 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``09_...sql` (0709 are homework DDL, template and solution). - `dwh-modeling/` contains the article and demo DWH model; SQL lives in `dwh-modeling/sql` as ordered scripts `01_...sql``06_...sql`.
- `postgres-bookings/` is a Dockerized PostgreSQL + demo “bookings” DB; start it first, then apply DWH scripts against the `demo` database. - `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 ## Build, Test, and Development Commands
- Start demo Postgres: - Start demo Postgres:
@@ -26,59 +20,16 @@
- SQL files: keep numeric prefixes (`01_`, `02_`, …) to reflect execution order and use descriptive suffixes like `ddl_*` / `dml_*`. - 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/`. - 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 ## Testing Guidelines
- There is no dedicated test framework; treat SQL scripts as executable documentation. - 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 `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;`. - 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 & Pull Request Guidelines
- Commit messages are short, imperative or descriptive phrases (often in Russian), e.g. `Добавлено оглавление`, `Переработка структуры`; group related edits into a single commit.
**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).
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 ## Security & Configuration Tips
- Do not commit personal `.env` files or credentials; use local overrides only. - 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. - Demo credentials and ports in `postgres-bookings` are for local training only—never reuse them in shared or production environments.
-1
View File
@@ -1 +0,0 @@
@AGENTS.md
-1
View File
@@ -1 +0,0 @@
de.dementev.space
-220
View File
@@ -1,220 +0,0 @@
# 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
```
+109 -266
View File
@@ -1,58 +1,40 @@
## О роадмапе # О роадмапе
Этот роадмап — конспект моих подходов к обучению Data Engineering. Этот роадмап — конспект моих подходов к обучению Data Engineering.
Он подойдёт тем, кто хочет: Он подойдёт тем, кто хочет:
- системно войти в профессию с нуля или близкого к нулю уровня; - системно войти в профессию с нуля или близкого к нулю уровня;
- закрыть пробелы в базе (SQL, Git, Python, DWH, Airflow, Greenplum); - закрыть пробелы в базе (SQL, Git, Python, DWH, Airflow, GreenPlum);
- подготовиться к собеседованиям и первым рабочим задачам. - подготовиться к собеседованиям и первым рабочим задачам.
Роадмап можно проходить самостоятельно или вместе со мной в формате менторства. Роадмап можно проходить самостоятельно или вместе со мной в формате менторства.
Если хотите идти с поддержкой ментора — напишите в Telegram: [@dementev_dev](https://t.me/dementev_dev). Если хотите идти с поддержкой ментора — напишите в 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, сохранять в нем изменения. Как создать репозиторий на GitHub, сохранять в нем изменения.
- [Что такое Git для Начинающих / GitHub за 30 минут / Git Уроки - Youtube](https://www.youtube.com/watch?v=VJm_AjiTEEc) - [Что такое Git для Начинающих / GitHub за 30 минут / Git Уроки - Youtube](https://www.youtube.com/watch?v=VJm_AjiTEEc)
- [Git: Конфликты для Начинающих // Git Cherry Pick, Git Revert, Git Reset - Youtube](https://www.youtube.com/watch?v=F7FnnfnB9YY) - [Git: Конфликты для Начинающих // Git Cherry Pick, Git Revert, Git Reset - Youtube](https://www.youtube.com/watch?v=F7FnnfnB9YY)
- Книга [Pro Git](https://git-scm.com/book/ru/v2) - читать главу 1 - Книга [Pro Git](https://git-scm.com/book/ru/v2) - читать главу 1
#### Основы Markdown ### Основы Markdown
- [Язык Markdown и файл README | Git и GitHub для начинающих - Youtube](https://www.youtube.com/watch?v=8lEDTrr-G4U) - [Язык Markdown и файл README | Git и GitHub для начинающих - Youtube](https://www.youtube.com/watch?v=8lEDTrr-G4U)
- [Markdown и его возможности: простой способ оформления текста](https://kurshub.ru/journal/blog/markdown-chto-eto/) - [Markdown и его возможности: простой способ оформления текста](https://kurshub.ru/journal/blog/markdown-chto-eto/)
- [Синтаксис Markdown: подробная шпаргалка для веб-разработчиков / Skillbox Media](https://skillbox.ru/media/code/yazyk-razmetki-markdown-shpargalka-po-sintaksisu-s-primerami/) - [Синтаксис Markdown: подробная шпаргалка для веб-разработчиков / Skillbox Media](https://skillbox.ru/media/code/yazyk-razmetki-markdown-shpargalka-po-sintaksisu-s-primerami/)
@@ -60,50 +42,41 @@
Домашки по остальным темам тренируемся делать в Git, там же пишем документацию. Домашки по остальным темам тренируемся делать в Git, там же пишем документацию.
**Когда блок Git и базовые инструменты считаем пройденным:** **Когда блок Git и базовые инструменты считаем пройденным:**
- ориентируетесь в терминале: перемещаетесь по каталогам и находите нужное в логах (grep, tail, less);
- понимаете права файлов (rwx, chmod) и знаете, как подключиться к серверу по ssh;
- вы уверенно создаёте репозиторий, коммитите изменения и отправляете их на GitHub; - вы уверенно создаёте репозиторий, коммитите изменения и отправляете их на GitHub;
- имеете представление о работе с ветками: создание, переключение, что такое merge/PR и разруливание простых конфликтов; - имеете представление о работе с ветками: создание, переключение, что такое merge/PR и разруливание простых конфликтов;
- оформляете базовую документацию в Markdown (README, заголовки, списки, ссылки, кодовые блоки). - оформляете базовую документацию в Markdown (README, заголовки, списки, ссылки, кодовые блоки).
### Базы данных: SQL и моделирование данных ## SQL
### База по SQL
SQL и моделирование данных специально идут рядом: сначала учимся уверенно извлекать данные запросами, затем — понимать и проектировать структуру данных, чтобы ETL/витрины были осмысленными.
#### База по SQL
Книга: [PostgreSQL. Основы языка SQL](https://postgrespro.ru/education/books/sqlprimer) - Глава 1 "Введение в базы данных и SQL" + ДЗ Книга: [PostgreSQL. Основы языка SQL](https://postgrespro.ru/education/books/sqlprimer) - Глава 1 "Введение в базы данных и SQL" + ДЗ
Бесплатный тренажер: [Интерактивный тренажер по SQL – Stepik](https://stepik.org/course/63054/promo) Бесплатный тренажер: [Интерактивный тренажер по SQL – Stepik](https://stepik.org/course/63054/promo)
Целевой уровень знания SQL - Live кодинг на собесе. Проверяем на первом мок-интервью Целевой уровень знания SQL - Live кодинг на собесе. Проверяем на первом мок-интервью
**СТЕ**
**CTE** - Зачем нам CTE: [Getting started with CTEs | dbt Labs](https://www.getdbt.com/blog/getting-started-with-cte)
- Подробнее про синтаксис: [PostgreSQL : Документация: 17: 7.8. Запросы WITH (Общие табличные выражения) : Компания Postgres Professional](https://postgrespro.ru/docs/postgresql/17/queries-with)
- Зачем нам 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/) Для дальнейшей тренировки и поддержания уровня можно использовать [Database - LeetCode](https://leetcode.com/problem-list/database/). Хорошая подборка задачек: [SQL 50 - Study Plan - LeetCode](https://leetcode.com/studyplan/top-sql-50/)
#### Повышение знаний SQL ### Повышение знаний SQL
Смотрим курс от Postgres Pro [DEV1](https://postgrespro.ru/education/courses/DEV1) Смотрим курс от Postgres Pro [DEV1](https://postgrespro.ru/education/courses/DEV1)
Темы - от "Введение" до "SQL" включительно, "Управление доступом", "Резервное копирование". Для лучшего усваивания материала проделываем все примеры и домашние задания из конспектов лекций. Темы - от "Введение" до "SQL" включительно, "Управление доступом", "Резервное копирование". Для лучшего усваивания материала проделываем все примеры и домашние задания из конспектов лекция.
С темой "PL/pgSQL" можно ознакомиться обзорно. С темой "PL/pgSQL" можно ознакомиться обзорно.
Для развития навыков инженера будет полезно лабораторные работы делать не в виртуальной машине, а в docker контейнере. Предложенный (не обязательный) вариант - в каталоге `postgres-bookings` репозитория. Для развития навыков инженера будет полезно лабораторные работы делать не в виртуальной машине, а в docker контейнере. Предложенный (не обязательный) вариант - в каталоге `postgres-bookings` репозитория.
Для дальнейшего закрепления материала - читаем книгу [PostgreSQL. Основы языка SQL](https://postgrespro.ru/education/books/sqlprimer) Для дальнейшего закрепления материала - читаем книгу [PostgreSQL. Основы языка SQL](https://postgrespro.ru/education/books/sqlprimer)
- Глава 8 - Индексы + ДЗ - Глава 8 - Индексы + ДЗ
- Глава 9 - Транзакции - Глава 9 - Транзакции
- Глава 10 - Повышение производительности + ДЗ - Глава 10 - Повышение производительности + ДЗ
Вопросы оптимизации запросов хорошо описаны в курсе [QPT](https://postgrespro.ru/education/courses/QPT) от Postgres Pro. Полученные навыки применимы для работы в том числе с Greenplum, и частично, другими БД. Вопросы оптимизации запросов хорошо описаны в курсе [QPT](https://postgrespro.ru/education/courses/QPT) от Postgres Pro. Полученные навыки применимы для работы в том числе с GreenPlum, и частично, другими БД.
На момент написания, видеолекции были доступны только для старой версии Postgres 13, но ее вполне достаточно. На момент написания, видеолекции были доступны только для старой версии Postgres 13, но ее вполне достаточно.
#### Моделирование данных ### Моделирование данных
Понимание того, **как устроены данные и зачем они нужны**, — ключ к качественным ETL-процессам. Понимание того, **как устроены данные и зачем они нужны**, — ключ к качественным ETL-процессам.
Мы кратко разбираем: Мы кратко разбираем:
- Основные подходы: нормализованные (3NF) vs денормализованные (звезда, снежинка) - Основные подходы: нормализованные (3NF) vs денормализованные (звезда, снежинка)
- Что такое staging, marts, слои raw / clean / business - Что такое staging, marts, слои raw / clean / business
- Как проектировать таблицы под конкретные сценарии использования - Как проектировать таблицы под конкретные сценарии использования
@@ -111,14 +84,10 @@ SQL и моделирование данных специально идут р
Цель — не стать архитектором, а **уметь читать и объяснять структуру данных**, чтобы писать осмысленные запросы и трансформации. Цель — не стать архитектором, а **уметь читать и объяснять структуру данных**, чтобы писать осмысленные запросы и трансформации.
Материалы (включая демо DWH-модель из этого репозитория): Материалы (включая демо DWH-модель из этого репозитория):
- Мартин Клеппман — «Высоконагруженные приложения» - Глава 2: Модели данных и языки запросов. - Для понимания, чем реляционная модель (SQL) отличается от документной (NoSQL) и графовой, и почему для аналитики мы всё ещё любим таблицы
(Важно: не перепутайте главу 2 с Частью 2 про распределенные данные!).
- [Яндекс Практикум: что такое нормализация, простыми словами (для самых начинающих)](https://practicum.yandex.ru/blog/chto-takoe-normalizaciya-dannyh/) - [Яндекс Практикум: что такое нормализация, простыми словами (для самых начинающих)](https://practicum.yandex.ru/blog/chto-takoe-normalizaciya-dannyh/)
- [Базы данных. 1,2,3 нормальные формы. - Youtube](https://www.youtube.com/watch?v=zwQzL80U51c) - [Базы данных. 1,2,3 нормальные формы. - Youtube](https://www.youtube.com/watch?v=zwQzL80U51c)
- [Введение в структуру хранилища данных](dwh-modeling/README.md) - [Введение в структуру хранилища данных](dwh-modeling/README.md)
- Теория про Slowly Changing Dimensions: [SCD](dwh-modeling/SCD.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) - Практика по моделированию статусов клиента: [домашка STG → ODS → DDS → DM](dwh-modeling/Homework_Customer_Status_DDS_DM.md)
- Еще про Data Vault: - Еще про Data Vault:
- Конспект и примеры из этого репозитория: [DataVault.md](dwh-modeling/DataVault.md) - Конспект и примеры из этого репозитория: [DataVault.md](dwh-modeling/DataVault.md)
@@ -127,17 +96,17 @@ SQL и моделирование данных специально идут р
- [Гибкие методологии проектирования Data Vault и Anchor Modeling | Евгений Ермаков | karpov.courses](https://www.youtube.com/watch?v=fNGIOb8SJvU) - [Гибкие методологии проектирования Data Vault и Anchor Modeling | Евгений Ермаков | karpov.courses](https://www.youtube.com/watch?v=fNGIOb8SJvU)
- Лекция в рамках курса по DWH: именно [«Основы Data Vault, создаем первую модель»](https://www.youtube.com/watch?v=65b99XCuiR4) — хороший формат: теория + пример. - Лекция в рамках курса по DWH: именно [«Основы Data Vault, создаем первую модель»](https://www.youtube.com/watch?v=65b99XCuiR4) — хороший формат: теория + пример.
- Доклад - практический пример: [Денис Лукьянов — Data Vault 2.0. Когда внедрять, проблемы применения при построении DWH на Greenplum](https://www.youtube.com/watch?v=oGwQbeP5iss) - Доклад - практический пример: [Денис Лукьянов — Data Vault 2.0. Когда внедрять, проблемы применения при построении DWH на Greenplum](https://www.youtube.com/watch?v=oGwQbeP5iss)
- Краткая теория про [DWH](https://halltape.github.io/HalltapeRoadmapDE/DWH/) - повторим еще раз, в другом изложении
- Хорошее общее введение в модели данных дано в статье и докладе от Yandex: [Как мы внедрили свою модель хранения данных — highly Normalized hybrid Model. Доклад Яндекса](https://habr.com/ru/companies/yandex/articles/557140/) - Хорошее общее введение в модели данных дано в статье и докладе от Yandex: [Как мы внедрили свою модель хранения данных — highly Normalized hybrid Model. Доклад Яндекса](https://habr.com/ru/companies/yandex/articles/557140/)
**Когда блок «Базы данных» считаем пройденным:** **Когда блок SQL считаем пройденным:**
- вы уверенно пишете запросы с JOIN, агрегатами, подзапросами и CTE; - вы уверенно пишете запросы с JOIN, агрегатами, подзапросами и CTE;
- можете подробно объяснить план запроса в Postgres, понимаете где планировщик отработал корректно, а где - есть возможность улучшить; - можете подробно объяснить план запроса в Postgres, понимаете где планировщик отработал корректно, а где - есть возможность улучшить;
- можете объяснить простую модель данных (3NF/звезда) и прочитать схему DWH; - можете объяснить простую модель данных (3NF/звезда) и прочитать схему DWH;
- решаете типовые задачи уровня SQL live-coding без долгих пауз. - решаете типовые задачи уровня SQL live-coding без долгих пауз.
### Python ## Python
- Если совсем не знакомы с Python, начинаем с курса ["Поколение Python": курс для начинающих – Stepik](https://stepik.org/course/58852/info) - Если совсем не знакомы с Python, начинаем с курса ["Поколение Python": курс для начинающих – Stepik](https://stepik.org/course/58852/info)
- Изучаем глубже и "оттачиваем" live coding: ["Поколение Python": курс для продвинутых – Stepik](https://stepik.org/course/68343/info) - Изучаем глубже и "оттачиваем" live coding: ["Поколение Python": курс для продвинутых – Stepik](https://stepik.org/course/68343/info)
@@ -147,19 +116,15 @@ SQL и моделирование данных специально идут р
- [Работа с файлами в формате CSV, JSON, YAML](https://pyneng.readthedocs.io/ru/latest/book/17_serialization/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) - [Итераторы, итерируемые объекты и генераторы](https://pyneng.readthedocs.io/ru/latest/book/13_iterator_generator/index.html)
- [Декораторы Python: пошаговое руководство](https://habr.com/ru/companies/otus/articles/727590/) - [Декораторы Python: пошаговое руководство](https://habr.com/ru/companies/otus/articles/727590/)
- Работа с датой/временем: [официальная документация по модулю datetime](https://docs.python.org/3/library/datetime.html) - Работа с датой/временем: https://django.fun/docs/python/3.10/library/datetime/
- [Сложность алгоритмов. Разбор Big O](https://habr.com/ru/articles/782608/) — короткий материал, чтобы понимать O(n) vs O(n²) на собеседованиях и в коде
- ООП - ООП
- [Tproger: «ООП простыми словами»](https://tproger.ru/experts/oop-in-simple-words) - [Tproger: «ООП простыми словами»](https://tproger.ru/experts/oop-in-simple-words)
- Введение в [ООП](https://metanit.com/python/tutorial/7.1.php) - Введение в [ООП](https://metanit.com/python/tutorial/7.1.php)
- [Яндекс Учебник: «Объектная модель Python: классы, поля и методы»](https://education.yandex.ru/handbook/python/article/obuektnaya-model-python-klassy-polya-i-metody) - [Яндекс Учебник: «Объектная модель 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/) - [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 Lab
- [Блог Практикума: «Что такое Jupyter Notebook: как установить и открыть»](https://practicum.yandex.ru/blog/chto-takoe-jupyter-notebook/) - [Блог Практикума: «Что такое 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) - Готовая реализация Jupyter Lab, включающая в себя Spark, в Docker: https://github.com/dementev-dev/jupyter-spark-docker
- Pandas - Pandas
- [GeeksforGeeks: “Why Pandas is Used in Python”](https://www.geeksforgeeks.org/pandas/why-pandas-is-used-in-python/) - [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/) - [Skillbox: «Для чего нужна библиотека Pandas»](https://skillbox.ru/media/code/rabotaem-s-pandas-osnovnye-ponyatiya-i-realnye-dannye/)
@@ -167,76 +132,20 @@ SQL и моделирование данных специально идут р
- [Хабр (RUVDS): «Моя шпаргалка по pandas»](https://habr.com/ru/companies/ruvds/articles/494720/) - [Хабр (RUVDS): «Моя шпаргалка по pandas»](https://habr.com/ru/companies/ruvds/articles/494720/)
- [Tproger: «Наглядная шпаргалка по операциям с DataFrame»](https://tproger.ru/articles/pandas-data-wrangling-cheatsheet) - [Tproger: «Наглядная шпаргалка по операциям с DataFrame»](https://tproger.ru/articles/pandas-data-wrangling-cheatsheet)
Полезно, но дороговато и не обязательно: хорошее комбо SQL + Python — ["Поколение Python": профи + ООП + SQL Stepik](https://stepik.org/course/233341/promo?search=7181036958) Полезно, но дороговато и не обязательно: хорошее комбо SQL + Python — ["Поколение Python": профи + ООП + SQL Stepik](https://stepik.org/course/233341/promo?search=7181036958)
Цель — уверенно решать простые задачи на Python в формате live-coding; дальше эти навыки пригодятся для создания DAG Airflow. Цель — уверенно решать простые задачи на Python в формате live-coding; дальше эти навыки пригодятся для создания DAG Airflow.
**Когда блок Python считаем пройденным:** **Когда блок Python считаем пройденным:**
- вы без подсказок пишете небольшие скрипты с циклами, функциями, обработкой ошибок и работой с коллекциями; - вы без подсказок пишете небольшие скрипты с циклами, функциями, обработкой ошибок и работой с коллекциями;
- умеете читать и модифицировать чужой код, в том числе с использованием pandas и DataFrame; - умеете читать и модифицировать чужой код, в том числе с использованием pandas и DataFrame;
- уверенно проходите простой live-coding по Python для DE: прочитать CSV/JSON, отфильтровать, сгруппировать данные и посчитать агрегаты. - уверенно проходите простой 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) - Сжатый, но емкий видеогайд: [GIT, GitHub, GitLab. Полный АКТУАЛЬНЫЙ гайд ЗА ПОЛТОРА ЧАСА. Без этого выгонят с работы - Youtube](https://www.youtube.com/watch?v=0Y-fneoUIO8)
- Книга: [Pro Git](https://git-scm.com/book/ru/v2) - главы - Книга: [Pro Git](https://git-scm.com/book/ru/v2) - главы
- 2 Основы Git - 2 Основы Git
@@ -248,112 +157,103 @@ Agile — это не метод, а философия. Scrum и Kanban — э
Целевой уровень знания - понимание процесса GitFlow. Как создать ветку, влить изменения в другие ветки. Понимание, зачем. Целевой уровень знания - понимание процесса GitFlow. Как создать ветку, влить изменения в другие ветки. Понимание, зачем.
На собесах обычно не спрашивают, но нужно в работе. На собесах обычно не спрашивают, но нужно в работе.
#### Docker ### Docker
- Курс https://karpov.courses/docker
- Курс [Docker от karpov.courses](https://karpov.courses/docker)
Основное предназначение для нас - учебные стенды, где мы разбираем и тренируемся с разными технологиями. На работе - иногда пригождается. На собесах спрашивают редко. Основное предназначение для нас - учебные стенды, где мы разбираем и тренируемся с разными технологиями. На работе - иногда пригождается. На собесах спрашивают редко.
#### Запись встреч ### Методы разработки
OBS Studio
Кратко знакомимся с основными подходами к организации работы в IT:
- **Водопад** — последовательная разработка,
- **Scrum / Kanban** — гибкие методологии, популярные в data-командах.
Понимание этих концепций помогает быстрее адаптироваться в новых проектах и правильно интерпретировать требования.
### Запись встреч
OBS Studio
- Руководство по OBS: [OBS Studio - Настройка ОБС для Записи Игр и Стрима | Настройка Микрофона в Обс и т.д - Youtube](https://www.youtube.com/watch?v=bj8VEphZ65U) - Руководство по OBS: [OBS Studio - Настройка ОБС для Записи Игр и Стрима | Настройка Микрофона в Обс и т.д - Youtube](https://www.youtube.com/watch?v=bj8VEphZ65U)
- [Как записывать собеседования](https://docs.google.com/document/d/1qd8uRYlAaZp9c5zpvCVBOvYQCEukGHI9PEPjnjahI1k/) - [Как записывать собеседования](https://docs.google.com/document/d/1qd8uRYlAaZp9c5zpvCVBOvYQCEukGHI9PEPjnjahI1k/)
**Когда блок технических навыков считаем пройденным:** **Когда блок технических навыков считаем пройденным:**
- вы понимаете базовый GitFlow: как организована работа с ветками в команде и как ваши коммиты попадают в прод; - вы понимаете базовый GitFlow: как организована работа с ветками в команде и как ваши коммиты попадают в прод;
- используете Docker для учебных стендов: запускаете контейнеры, смотрите логи и при необходимости перезапускаете сервисы; - используете Docker для учебных стендов: запускаете контейнеры, смотрите логи и при необходимости перезапускаете сервисы;
- ориентируетесь в основных методологиях разработки (Scrum/Kanban/водопад) и понимаете, как в них живут задачи и отчётность;
- при необходимости умеете настроить запись экрана/созвонов, чтобы сохранять материалы обучения. - при необходимости умеете настроить запись экрана/созвонов, чтобы сохранять материалы обучения.
--- # Практика и инструменты
## Практика и инструменты ## Airflow
[[к оглавлению]](#оглавление)
### Airflow
Apache Airflow — инструмент для оркестрации ETL-процессов. Apache Airflow — инструмент для оркестрации ETL-процессов.
Мы используем его для: Мы используем его для:
- планирования задач, - планирования задач,
- отслеживания зависимостей между шагами, - отслеживания зависимостей между шагами,
- визуализации статуса выполнения. - визуализации статуса выполнения.
Материалы: Материалы:
- [Учебник по Airflow](https://github.com/dementev-dev/airflow-manual) - [Учебник по Airflow](https://github.com/dementev-dev/airflow-manual)
**Когда блок Airflow считаем пройденным:** **Когда блок Airflow считаем пройденным:**
- вы можете объяснить, что такое DAG, задачи, операторы и сенсоры, и как между ними задаются зависимости; - вы можете объяснить, что такое DAG, задачи, операторы и сенсоры, и как между ними задаются зависимости;
- на базе учебного стенда подготавливаете, отлаживаете и запускаете свои DAG'и с расписанием и несколькими шагами (например, загрузка данных и последующие трансформации); - на базе учебного стенда подготавливаете, отлаживаете и запускаете свои DAG'и с расписанием и несколькими шагами (например, загрузка данных и последующие трансформации);
- уверенно смотрите логи, находите место падения и понимаете, как перезапустить задачу. - уверенно смотрите логи, находите место падения и понимаете, как перезапустить задачу.
### Greenplum ## Greenplum
Разбираем, чем Greenplum отличается от PostgreSQL и зачем нужны MPP-хранилища. Разбираем, чем Greenplum отличается от PostgreSQL и зачем нужны MPP-хранилища.
#### Фундаментальная теория Предварительно:
Прежде чем нажимать кнопки, нужно понять "физику" больших данных. Почему обычный Postgres начинает тормозить?
- Мартин Клеппман, "Высоконагруженные приложения":
- Глава 1. Надежность, масштабируемость. (Разбираемся, чем вертикальное масштабирование отличается от горизонтального).
- Глава 3 (только конец главы). Читаем разделы:
- «Обработка транзакций или аналитика?» (OLTP or OLAP?) — ключевое различие нагрузок.
- «Хранение по столбцам» — почему аналитика требует другого способа записи данных на диск.
- Зачем: Это объясняет, почему Greenplum устроен именно так. Без этого вы будете пытаться работать с ним как с обычным Postgres.
#### Знакомство с Greenplum
Теперь, понимая теорию, смотрим, как это реализовано в конкретном инструменте.
- Простое введение: [Greenplum | Что это такое и как оно работает? - Youtube](https://www.youtube.com/watch?v=rLG9Z_HcKPY) - Простое введение: [Greenplum | Что это такое и как оно работает? - Youtube](https://www.youtube.com/watch?v=rLG9Z_HcKPY)
- Оно же, но текстом: https://halltape.github.io/HalltapeRoadmapDE/GREENPLUM/
- [Визуализатор распределения Greenplum](https://gpskew.rzvde.pro/) - [Визуализатор распределения Greenplum](https://gpskew.rzvde.pro/)
- Бесплатный, но большой учебный курс от Yandex: https://yandex.cloud/ru/training/greenplum
- [Учебный курс по Greenplum от datafinder](https://datafinder.ru/products/uchebnyy-kurs-po-greenplum) — взять только отдельные главы.
**Курс Yandex по Greenplum** — основной учебный курс, рекомендуется пройти целиком: Практика:
- [DE Starter Kit — Airflow + Greenplum + CSV](https://github.com/dementev-dev/airflow-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 считаем пройденным:** **Когда блок Greenplum считаем пройденным:**
- вы понимаете, как данные распределяются по сегментам, что такое skew и как его увидеть; - вы понимаете, как данные распределяются по сегментам, что такое skew и как его увидеть;
- на базе стенда `airflow-dwh-gp-lab` можете загружать и выгружать данные в Greenplum, выполнять запросы и разбирать планы выполнения (`EXPLAIN`); - на базе учебного стенда (например, DE Starter Kit) можете загружать и выгружать данные в Greenplum, выполнять запросы и разбирать планы выполнения (`EXPLAIN`);
- можете объяснить, в чём практическая разница между MPP-хранилищем и одиночным Postgres на уровне типичных задач DE и собеседований. - можете объяснить, в чём практическая разница между MPP-хранилищем и одиночным Postgres на уровне типичных задач DE и собеседований.
### Курсовая работа ## Курсовая работа
Курсовая работа — важный майлстоун роадмапа: ваш первый end-to-end data-проект. После неё у вас есть ключевые технические навыки для старта карьеры в Data Engineering. К финалу роадмапа мы собираем небольшую end-to-end курсовую работу — свой первый «боеподобный» data-проект.
Курсовая выполняется на том же стенде [airflow-dwh-gp-lab](https://github.com/dementev-dev/airflow-greenplum), который вы уже использовали для практики по Greenplum. ### Стенд в Docker Compose
- Apache Airflow — оркестратор;
- источник данных — TelecomX (или аналогичный открытый датасет);
- Greenplum — основное хранилище;
- вспомогательный Postgres (по желанию);
- ETL-скрипты и DAG'и;
- исходные коды всего — в отдельном Git-репозитории.
**Что внутри:** В качестве альтернативы файловому источнику можно использовать генератор данных для демо-базы `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 минут рассказать архитектуру курсового проекта, его цели и показать ключевые части кода.
- все загрузки реализованы, DWH заполняется полностью (STG → ODS → DDS → DM); ## Понятие сложности алгоритмов
- автоматическая проверка (валидационный DAG) проходит без ошибок; В Data Engineering редко требуется писать сложные алгоритмы, но важно понимать, как оценивать эффективность кода:
- вы можете на собеседовании за 5–10 минут рассказать архитектуру проекта, его цели и показать ключевые части кода.
--- - в SQL — через объём сканируемых данных, типы JOIN’ов, использование индексов;
- в Python — через асимптотику операций с pandas/списками (например, O(n) vs O(n²)).
## Карьера и менторство Это помогает избегать «тормозящих» решений на собеседованиях и в реальных пайплайнах.
[[к оглавлению]](#оглавление) # Карьера и менторство
### Менторство по этому роадмапу ## Менторство по этому роадмапу
Если вы нашли этот роадмап в интернете и хотите пройти его не в одиночку, а с поддержкой ментора, можно присоединиться ко мне. Если вы нашли этот роадмап в интернете и хотите пройти его не в одиночку, а с поддержкой ментора, можно присоединиться ко мне.
**Что даёт менторство:** **Что даёт менторство:**
- структурный план прохождения роадмапа под вашу ситуацию; - структурный план прохождения роадмапа под вашу ситуацию;
- разбор вопросов по SQL / DWH / Airflow и другим темам из этого документа; - разбор вопросов по SQL / DWH / Airflow и другим темам из этого документа;
- разбор домашних заданий и код-ревью; - разбор домашних заданий и код-ревью;
@@ -364,153 +264,96 @@ Apache Airflow — инструмент для оркестрации ETL-про
Просто напишите мне в Telegram: [@dementev_dev](https://t.me/dementev_dev) Просто напишите мне в Telegram: [@dementev_dev](https://t.me/dementev_dev)
со словами «Хочу пройти роадмап с ментором» — дальше всё обсудим. со словами «Хочу пройти роадмап с ментором» — дальше всё обсудим.
### Подготовка к собеседованиям ## Подготовка к собеседованиям
Цель блока — сформировать «опыт от 2 лет» и уметь корректно его показать в резюме и на собеседовании. Цель блока — сформировать «опыт от 2 лет» и уметь корректно его показать в резюме и на собеседовании.
#### Помощь в подготовке резюме ### Помощь в подготовке резюме
- Видео от ОМ по составлению резюме - Видео от ОМ по составлению резюме
- [Как накрутить опыт в резюме | «Ультимативный гайд» @digital_ninja](https://www.youtube.com/watch?v=EPuogJuYsvY) - [Как накрутить опыт в резюме | «Ультимативный гайд» @digital_ninja](https://www.youtube.com/watch?v=EPuogJuYsvY)
- [Как писать резюме, чтобы его читали - доклад - Boosty](https://boosty.to/m0rtymerr/posts/71b02a6b-8116-466a-b945-b2ed793abd8f) - [Как писать резюме, чтобы его читали - доклад - Boosty](https://boosty.to/m0rtymerr/posts/71b02a6b-8116-466a-b945-b2ed793abd8f)
- [Как грамотно продать себя на собеседовании / Созвон сообщества - Boosty](https://boosty.to/m0rtymerr/posts/7289cd23-60c6-4010-bb1c-a5b28dac399a) - [Как грамотно продать себя на собеседовании / Созвон сообщества - Boosty](https://boosty.to/m0rtymerr/posts/7289cd23-60c6-4010-bb1c-a5b28dac399a)
- Практика: совместная работа над резюме — ментор помогает переработать опыт, сформировать убедительную карьерную историю и подготовиться к вопросам по ней. - Попытки менти написать резюме, моя обратная связь — итеративно.
#### Поиск работы и собеседования ### Навыки поиска работы с HH и Habr карьера
- [Как подтвердить опыт без трудовой / Хабр против работяг](https://www.youtube.com/watch?v=GHqABzA1zi8) - [Как подтвердить опыт без трудовой / Хабр против работяг](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) - [Как успешно пройти испытательный срок в IT | «Ультимативный гайд» c @digital_ninja - Youtube](https://www.youtube.com/watch?v=r1lWP5rYVdk)
- [Испытательный срок - доклад - Boosty](https://boosty.to/m0rtymerr/posts/40e7f17e-022b-495c-8d03-dabbe4383b8e) - [Испытательный срок - доклад - Boosty](https://boosty.to/m0rtymerr/posts/40e7f17e-022b-495c-8d03-dabbe4383b8e)
**Когда блок подготовки к собеседованиям считаем пройденным:** **Когда блок подготовки к собеседованиям считаем пройденным:**
- у вас есть актуальное резюме под DE с понятными примерами проектов вместо «пустого» опыта; - у вас есть актуальное резюме под DE с понятными примерами проектов вместо «пустого» опыта;
- вы умеете искать и отбирать вакансии на HH и Habr Карьера, адаптируя отклики под конкретную позицию; - вы умеете искать и отбирать вакансии на HH и Habr Карьера, адаптируя отклики под конкретную позицию;
- вы прошли хотя бы пару мок-собеседований, получили обратную связь и по результатам доработали резюме и стратегию поиска. - вы прошли хотя бы пару мок-собеседований, получили обратную связь и по результатам доработали резюме и стратегию поиска.
--- # Расширенные навыки
## Расширенные навыки
Эти темы выходят за рамки базового минимума для старта в Data Engineering, но дают более полное представление об экосистеме. Эти темы выходят за рамки базового минимума для старта в Data Engineering, но дают более полное представление об экосистеме.
Их цель — понимать, зачем и когда используется тот или иной инструмент, а не осваивать его на уровне администратора или DevOps-инженера. Их цель — понимать, зачем и когда используется тот или иной инструмент, а не осваивать его на уровне администратора или DevOps-инженера.
[[к оглавлению]](#оглавление)
Мы кратко знакомимся с: Мы кратко знакомимся с:
- **Streaming** (NiFi + Kafka) — инструментами для построения потоковых и интеграционных пайплайнов; - **Apache NiFi** и **Kafka** — инструментами для построения потоковых и интеграционных пайплайнов;
- **ClickHouse** — колоночной СУБД для высоконагруженной аналитики; - **ClickHouse** — колоночной СУБД для высоконагруженной аналитики;
- **Lakehouse** (Spark, Iceberg, Trino) — архитектурой, построенной на разделении compute и storage;
- **dbt** — подходом к трансформации данных как кода. - **dbt** — подходом к трансформации данных как кода.
Практика ограничивается минимальным рабочим примером (запуск в Docker, простой пайплайн или SQL-модель). Практика ограничивается минимальным рабочим примером (например, запуск в Docker, простой пайплайн или SQL-модель).
Этого достаточно, чтобы уверенно говорить об инструменте на собеседовании и понимать его место в архитектуре — а всё остальное при необходимости осваивается уже на проекте. Этого достаточно, чтобы уверенно говорить об инструменте на собеседовании и понимать его место в архитектуре — а всё остальное при необходимости осваивается уже на проекте.
### Streaming (NiFi + Kafka) ## ClickHouse
NiFi — визуальный конструктор потоков данных, Kafka — распределённая очередь сообщений. Вместе они закрывают типичный сценарий: принять данные, буферизовать, доставить в хранилище. Бесплатный курс https://yandex.cloud/ru/training/clickhouse
Платный курс [ClickHouse для аналитика – Stepik](https://stepik.org/course/100210/promo?search=6551441002)
Материалы: ## 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 настраиваем простой генератор данных.
- Упражнения курса Яндекса можно выполнять в их облаке (с оплатой за ресурсы) или бесплатно у себя — на учебном кластере [clickhouse-learning-cluster](https://github.com/dementev-dev/clickhouse-learning-cluster): 4 узла ClickHouse в Docker Compose, репликация, шардинг, балансировка через HAProxy. ## Kafka
- Следующий шаг — стенд [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 для Начинающих За 1 Час - Youtube](https://www.youtube.com/watch?v=hbseyn-CfXY)
### Lakehouse (Spark, Iceberg, Trino) Практика:
- расширяем предыдущий стенд, добавляя Kafka;
- строим поток данных: NiFi → Kafka;
- добавляем обратный поток: Kafka → NiFi → Postgres.
Lakehouse — архитектурный подход, который соединяет гибкость Data Lake с гарантиями классического DWH. ## dbt
В классическом 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) — инструмент для трансформации данных в хранилище. dbt (data build tool) — инструмент для трансформации данных в хранилище.
Мы рассматриваем его как альтернативу «ручному» написанию сложных CTE и для понимания современного подхода к моделированию данных как кода. Мы рассматриваем его как альтернативу «ручному» написанию сложных 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);
- вы можете на собеседовании кратко объяснить, когда уместны Streaming (NiFi/Kafka), ClickHouse, Lakehouse-стек и dbt, и чем они дополняют базовый стек (Postgres, Airflow, Greenplum); - понимаете типичные сценарии: потоковые интеграции и очереди (Kafka/NiFi), аналитические витрины и отчёты на ClickHouse, трансформации данных в dbt;
- понимаете типичные сценарии: потоковые интеграции и очереди (NiFi + Kafka), аналитические витрины и отчёты на ClickHouse, Lakehouse-архитектура (Spark/Iceberg/Trino), трансформации данных как код (dbt);
- не боитесь увидеть эти инструменты в описании вакансии и можете поддержать содержательный разговор об их месте в архитектуре. - не боитесь увидеть эти инструменты в описании вакансии и можете поддержать содержательный разговор об их месте в архитектуре.
--- # Софт скиллы
## Софт скиллы
[[к оглавлению]](#оглавление)
- [Все ветви дохода в IT / Полный гайд по деньгам](https://youtube.com/live/JHClTWwK1EM) - [Все ветви дохода в IT / Полный гайд по деньгам](https://youtube.com/live/JHClTWwK1EM)
- [Гайд как писать отзывы](https://boosty.to/m0rtymerr/posts/b04040ec-0f46-4524-9c75-188a513140ad?share=post_link) - [Гайд как писать отзывы](https://boosty.to/m0rtymerr/posts/b04040ec-0f46-4524-9c75-188a513140ad?share=post_link)
- [Гайд по Антистрессу](https://youtu.be/bu0YiXOKaoU) - [Гайд по Антистрессу](https://youtu.be/bu0YiXOKaoU)
--- # Дополнительные материалы
## Дополнительные материалы
[[к оглавлению]](#оглавление)
- [ananevsyu/SandBox_DB_public: Песочница для изучения различных технологий связанных с инженерией данных](https://gitflic.ru/project/ananevsyu/sandbox_db_public) - [ananevsyu/SandBox_DB_public: Песочница для изучения различных технологий связанных с инженерией данных](https://gitflic.ru/project/ananevsyu/sandbox_db_public)
- Клон проекта [dementev_dev/sandbox_db_public-форк](https://gitflic.ru/project/dementev_dev/sandbox_db_public-fork) - Клон проекта [dementev_dev/sandbox_db_public-форк](https://gitflic.ru/project/dementev_dev/sandbox_db_public-fork)
- [System Design. Разбор книги "Высоконагруженные приложения". Глава 1 - Youtube](https://www.youtube.com/watch?v=owjrIB_5go8) — отличный видео-конспект первой главы Клеппмана на русском.
- [Индексы в БД - Youtube](https://www.youtube.com/watch?v=DyqtBiDrz3g) - [Индексы в БД - Youtube](https://www.youtube.com/watch?v=DyqtBiDrz3g)
- [Spark + Iceberg in 1 Hour - Memory Tuning, Joins, Partition - Youtube](https://www.youtube.com/watch?v=3R-SLYK-P_0)
- [Введение в устройство 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/217/info)
- [Алгоритмы: теория и практика. Структуры данных – Stepik](https://stepik.org/course/1547/promo) - [Алгоритмы: теория и практика. Структуры данных – Stepik](https://stepik.org/course/1547/promo)
- [Apache Hadoop для самых маленьких: HDFS, RACK-AWARENESS, репликация и Data Locality - Youtube](https://youtu.be/0fsY5bW2l84) - [Apache Hadoop для самых маленьких: HDFS, RACK-AWARENESS, репликация и Data Locality - Youtube](https://youtu.be/0fsY5bW2l84)
- [Книга. Введение в Apache Kafka для системных аналитиков и проектировщиков интеграций](https://systems.education/kafka) - [Книга. Введение в Apache Kafka для системных аналитиков и проектировщиков интеграций](https://systems.education/kafka)
- [Перевод документации dbt на русский язык](https://docs.getdbt.tech/) - [Перевод документации dbt на русский язык](https://docs.getdbt.tech/)
### Записи ОМ ## Записи ОМ
- [Как пройти собеседование на программиста | Ультимативный гайд с ‪@om_nazarov - Youtube](https://www.youtube.com/watch?v=tzSdiYZ52kI) - [Как пройти собеседование на программиста | Ультимативный гайд с ‪@om_nazarov - Youtube](https://www.youtube.com/watch?v=tzSdiYZ52kI)
- [Как стать программистом в 2025 | «Ультимативный гайд» с ‪@om_nazarov](https://www.youtube.com/watch?v=6151ekTOl38) - [Как стать программистом в 2025 | «Ультимативный гайд» с ‪@om_nazarov](https://www.youtube.com/watch?v=6151ekTOl38)
-14
View File
@@ -1,14 +0,0 @@
# Разработка с ИИ
Практические материалы о работе с ИИ-ассистентами для разработчиков: как эффективно взаимодействовать с 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
@@ -1,234 +0,0 @@
# Лучшие практики работы с 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
@@ -1,362 +0,0 @@
# Механизм памяти 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`-файлы памяти
- не полагайтесь только на разговор для сохранения правил
+5 -6
View File
@@ -126,7 +126,7 @@ erDiagram
Чуть менее «сказочно», чуть более технично. Чуть менее «сказочно», чуть более технично.
### 3.1. Hub: сущность и её бизнес-ключ ### 3.1. Hub сущность и её бизнес-ключ
**Hub** содержит: **Hub** содержит:
@@ -137,7 +137,6 @@ erDiagram
* иногда — хэш бизнес‑ключа (hk_customer). * иногда — хэш бизнес‑ключа (hk_customer).
Главные правила: Главные правила:
* один бизнес‑ключ — один хаб (одна строка на сущность, без истории); * один бизнес‑ключ — один хаб (одна строка на сущность, без истории);
* хаб не знает про атрибуты (имя, email) — только идентичность. * хаб не знает про атрибуты (имя, email) — только идентичность.
@@ -154,7 +153,7 @@ CREATE TABLE hub_customer (
Конкретные типы данных (`BYTEA`, длины `VARCHAR`, детали `hashdiff`) и реализации хэш‑ключей можно не запоминать: на старте важнее понять саму идею — у сущностей есть стабильные ключи, а все изменения атрибутов мы записываем отдельными версиями в сателлитах. Конкретные типы данных (`BYTEA`, длины `VARCHAR`, детали `hashdiff`) и реализации хэш‑ключей можно не запоминать: на старте важнее понять саму идею — у сущностей есть стабильные ключи, а все изменения атрибутов мы записываем отдельными версиями в сателлитах.
### 3.2. Link: связи между сущностями ### 3.2. Link связи между сущностями
**Link** описывает факт связи, например: **Link** описывает факт связи, например:
@@ -179,7 +178,7 @@ CREATE TABLE link_order_customer (
); );
``` ```
### 3.3. Satellite: атрибуты и история ### 3.3. Satellite атрибуты и история
**Satellite** хранит: **Satellite** хранит:
@@ -250,7 +249,7 @@ Star Schema]
* **Raw Vault** — это про приём и хранение данных «как есть», но уже в форме Hub / Link / Satellite. * **Raw Vault** — это про приём и хранение данных «как есть», но уже в форме Hub / Link / Satellite.
* **Business Vault** — это про приведение этих данных в более «деловой» вид: с бизнес-правилами, PIT/Bridge и подготовленными представлениями. * **Business Vault** — это про приведение этих данных в более «деловой» вид: с бизнес-правилами, PIT/Bridge и подготовленными представлениями.
### 5.1. Raw Vault: «всё прилетевшее, аккуратно разложенное по ящичкам» ### 5.1. Raw Vault «всё прилетевшее, аккуратно разложенное по ящичкам»
Raw DV — первый слой поверх STG / ODS: Raw DV — первый слой поверх STG / ODS:
@@ -266,7 +265,7 @@ Raw DV — первый слой поверх STG / ODS:
* все источники показываются «как есть», только приведены к общим ключам; * все источники показываются «как есть», только приведены к общим ключам;
* структура стабильна: добавился новый источник → появился новый Satellite к тому же Hub. * структура стабильна: добавился новый источник → появился новый Satellite к тому же Hub.
### 5.2. Business Vault: «там, где из Lego собирают модули» ### 5.2. Business Vault «там, где из Lego собирают модули»
Business Vault (BV) — следующий слой над Raw DV: Business Vault (BV) — следующий слой над Raw DV:
+25 -28
View File
@@ -30,7 +30,7 @@
Структура файла: Структура файла:
```text ```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 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`); - `status` — статус клиента в CRM (`new`, `active`, `vip`, `churned`);
- `event_ts` — момент, когда статус сменился в CRM; - `event_ts` — момент, когда статус сменился в CRM;
- `_load_id` — идентификатор батча загрузки; - `_load_id` — идентификатор батча загрузки;
- `_load_ts` — момент, когда данные попали в DWH. - `load_ts` — момент, когда данные попали в DWH (в таблицах STG/ODS эта колонка будет называться `_load_ts`, но по смыслу это то же самое время загрузки).
Файл содержит несколько клиентов и несколько смен статуса по каждому — этого достаточно, чтобы отработать SCD2. Файл содержит несколько клиентов и несколько смен статуса по каждому — этого достаточно, чтобы отработать SCD2.
@@ -62,16 +62,15 @@ customer_id,status,event_ts,_load_id,_load_ts
1. Поднимите demo‑Postgres по инструкции из корневого `README.md`. 1. Поднимите demo‑Postgres по инструкции из корневого `README.md`.
2. Выполните базовые скрипты DWH: 2. Выполните базовые скрипты DWH:
- `01_ddl_stg-dds.sql` - `01_ddl_stg-dds.sql`
- `02_dml_stg-dds.sql` (нужен как минимум для `dds.dim_date`) - `02_dml_stg-dds.sql`
- `05_ddl_dm.sql` (создаёт схему `dm` для витрин)
3. Выполните DDL для домашки: 3. Выполните DDL для домашки:
- `07_ddl_hw_customer_status.sql` - `07_ddl_hw_customer_status.sql`
После этого схемы `stg`, `ods`, `dds`, `dm` уже существуют, а дополнительные таблицы для статусов созданы. После этого схемы `stg`, `ods`, `dds` уже существуют, а дополнительные таблицы для статусов созданы.
--- ---
## 3. Часть 1: STG → ODS (обязательно) ## 3. Часть 1 STG → ODS (обязательно)
**Задача:** загрузить CSV в STG и переложить данные в ODS с приведением типов. **Задача:** загрузить CSV в STG и переложить данные в ODS с приведением типов.
@@ -95,13 +94,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'); ('101','churned','2024-09-01 12:15:00','batch_20240901_1300','2024-09-01 13:00:00');
``` ```
> 💡 Здесь `_load_ts` — это время загрузки. > 💡 Здесь `_load_ts` — это время загрузки (в CSV оно называется `load_ts`).
#### Вариант B: загрузить CSV #### Вариант B: загрузить CSV
Можно загрузить файл `dwh-modeling/data/customer_status_events.csv` в таблицу `stg.customer_status_raw`: Можно загрузить файл `dwh-modeling/data/customer_status_events.csv` в таблицу `stg.customer_status_raw`:
- **Через DBeaver**: Import Data → CSV → `stg.customer_status_raw`. - **Через DBeaver**: Import Data → CSV → `stg.customer_status_raw` (колонку `load_ts` маппить в `_load_ts`).
- **Через `psql` в контейнере (`./psql_sh`)**: без установки `psql` на хост. - **Через `psql` в контейнере (`./psql_sh`)**: без установки `psql` на хост.
Способ: передайте CSV в `psql` через STDIN и выполните `\copy ... FROM STDIN`: Способ: передайте CSV в `psql` через STDIN и выполните `\copy ... FROM STDIN`:
@@ -122,14 +121,12 @@ SELECT * FROM stg.customer_status_raw LIMIT 10;
### 3.2. ODS: очистка и типизация ### 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: В файле `08_dml_hw_customer_status_template.sql` найдите заготовку блока ODS и допишите SQL:
- привести: - привести:
- `customer_id``INT`, - `customer_id``INT`,
- `status``VARCHAR(20)` (можно оставить как есть), - `status``VARCHAR(20)` (можно оставить как есть),
- `event_ts` и `_load_ts``TIMESTAMP`; - `event_ts` и `load_ts``TIMESTAMP` (в DWH-таблицах эта колонка будет лежать как `_load_ts`);
- аккуратно обработать возможные пустые значения (если бы они были); - аккуратно обработать возможные пустые значения (если бы они были);
- заполнить `_load_id` и `_load_ts` в `ods.customer_status`. - заполнить `_load_id` и `_load_ts` в `ods.customer_status`.
@@ -145,7 +142,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`, где каждая строка — период действия статуса. **Задача:** по событиям в `ods.customer_status` построить измерение `dds.dim_customer_status`, где каждая строка — период действия статуса.
@@ -216,7 +213,7 @@ ORDER BY customer_bk, valid_from;
--- ---
## 5. Часть 3: инкрементальная загрузка (по желанию) ## 5. Часть 3 инкрементальная загрузка (по желанию)
Если хочется потренироваться глубже: Если хочется потренироваться глубже:
@@ -241,11 +238,24 @@ cat dwh-modeling/data/customer_status_events_increment.csv | ./postgres-bookings
--- ---
## 6. Часть 4: витрина в DM (по желанию) ## 6. Часть 4 витрина в DM (по желанию)
Опциональное задание для закрепления: собрать небольшую витрину с количеством клиентов по статусам на каждую дату. Опциональное задание для закрепления: собрать небольшую витрину с количеством клиентов по статусам на каждую дату.
DDL витрины уже создан в `07_ddl_hw_customer_status.sql` (таблица `dm.mart_customer_status_daily`). Перед началом убедитесь, что слой 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
);
```
Идея: Идея:
@@ -282,16 +292,3 @@ ORDER BY date_actual, status;
- при желании — собрать простую витрину в `dm`. - при желании — собрать простую витрину в `dm`.
Если что‑то не получается — можно разбирать решения по шагам вместе с ментором: от простого `SELECT` из STG до полноценного SCD2 в DDS. Если что‑то не получается — можно разбирать решения по шагам вместе с ментором: от простого `SELECT` из STG до полноценного SCD2 в DDS.
---
## 8. Эталонное решение
<details>
<summary>Показать ссылку на решение</summary>
Когда выполните домашку и захотите сверить результат — готовое решение лежит в файле [`09_dml_hw_customer_status_solution.sql`](sql/09_dml_hw_customer_status_solution.sql).
Постарайтесь не подглядывать до того, как напишете свой вариант — основная ценность задания именно в самостоятельном разборе.
</details>
+143 -163
View File
@@ -3,30 +3,28 @@
## Оглавление ## Оглавление
- [Что вы уже умеете, и что узнаете здесь](#что-вы-уже-умеете-и-что-узнаете-здесь) - [Что вы уже умеете и что узнаете здесь](#что-вы-уже-умеете-и-что-узнаете-здесь)
- [1. Введение: почему нельзя просто SELECT из базы заказов?](#1-введение-почему-нельзя-просто-select-из-базы-заказов) - [1. Введение: почему нельзя просто SELECT из базы заказов?](#1-введение-почему-нельзя-просто-select-из-базы-заказов)
- [2. Учебный пример: интернет-магазин](#2-учебный-пример-интернет-магазин) - [2. Учебный пример: интернет-магазин](#2-учебный-пример-интернет-магазин)
- [3. Зачем делить DWH на слои?](#3-зачем-делить-dwh-на-слои) - [3. Зачем делить DWH на слои?](#3-зачем-делить-dwh-на-слои)
- [4. Путешествие данных: от STG до DM](#4-путешествие-данных-от-stg-до-dm) - [4. Путешествие данных: от STG до DM](#4-путешествие-данных-от-stg-до-dm)
- [5. Базовые понятия: факты, измерения, SCD](#5-базовые-понятия-факты-измерения-scd) - [5. Базовые понятия: факты, измерения, SCD](#5-базовые-понятия-факты-измерения-scd)
- [6. Модели данных для слоя DDS: 4 подхода, и когда какой выбрать](#6-модели-данных-для-слоя-dds-4-подхода-и-когда-какой-выбрать) - [6. Модели данных для слоя DDS: 4 подхода и когда какой выбрать](#6-модели-данных-для-слоя-dds-4-подхода-и-когда-какой-выбрать)
- [7. Практикум: как собрать первую витрину](#7-практикум-как-собрать-первую-витрину) - [7. Практикум: как собрать первую витрину](#7-практикум-как-собрать-первую-витрину)
- [8. Как выбрать модель данных? Советы от практиков](#8-как-выбрать-модель-данных-советы-от-практиков) - [8. Как выбрать модель данных? Советы от практиков](#8-как-выбрать-модель-данных-советы-от-практиков)
- [9. Эксплуатация: качество данных, это не «опция»](#9-эксплуатация-качество-данных-это-не-опция) - [9. Эксплуатация: качество данных это не «опция»](#9-эксплуатация-качество-данных-это-не-опция)
- [10. Заключение: главное, понимать «почему»](#10-заключение-главное-понимать-почему) - [10. Заключение: главное понимать «почему»](#10-заключение-главное-понимать-почему)
- [Приложения](#приложения) - [Приложения](#приложения)
--- ---
## Что вы уже умеете, и что узнаете здесь ## Что вы уже умеете и что узнаете здесь
✅ Уже знаете: ✅ Уже знаете:
- `SELECT`, `JOIN`, `GROUP BY`; - `SELECT`, `JOIN`, `GROUP BY`;
- как посчитать сумму/среднее/количество по таблице. - как посчитать сумму/среднее/количество по таблице.
🆕 Узнаете в этой статье: 🆕 Узнаете в этой статье:
- **слои хранилища** (STG → ODS → DDS → DM) и *зачем они нужны*; - **слои хранилища** (STG → ODS → DDS → DM) и *зачем они нужны*;
- **факты и измерения** — основные кирпичики аналитики; - **факты и измерения** — основные кирпичики аналитики;
- **SCD Type 2** — как хранить историю изменений клиента (например, смену email или города); - **SCD Type 2** — как хранить историю изменений клиента (например, смену email или города);
@@ -34,7 +32,6 @@
- **четыре модели данных**: 3NF, Звезда (Star), Data Vault, Anchor Modeling — и когда какую использовать. - **четыре модели данных**: 3NF, Звезда (Star), Data Vault, Anchor Modeling — и когда какую использовать.
**Не будем говорить** здесь о: **Не будем говорить** здесь о:
- физическом хранении (партиции, индексы, ClickHouse-движки); - физическом хранении (партиции, индексы, ClickHouse-движки);
- распределённых кластерах (Kafka, Spark, Airflow — это отдельный курс); - распределённых кластерах (Kafka, Spark, Airflow — это отдельный курс);
- настройке производительности (`EXPLAIN`, кэши и т.п.). - настройке производительности (`EXPLAIN`, кэши и т.п.).
@@ -50,7 +47,6 @@
Вы идёте в базу заказов — и… не находите email. Он в CRM. Идёте в CRM — там нет сумм заказов. Возвращаетесь в заказы — сумма есть, но *только текущая цена товара*. А в 2023 году цена была другой! Вы идёте в базу заказов — и… не находите email. Он в CRM. Идёте в CRM — там нет сумм заказов. Возвращаетесь в заказы — сумма есть, но *только текущая цена товара*. А в 2023 году цена была другой!
Знакомо? Это — **проблема OLTP-систем** (оперативного учёта): Знакомо? Это — **проблема OLTP-систем** (оперативного учёта):
- **CRM**, **склад**, **платёжка** — это разные базы; - **CRM**, **склад**, **платёжка** — это разные базы;
- каждая оптимизирована под *быструю запись операций* («добавить заказ», «списать товар»); - каждая оптимизирована под *быструю запись операций* («добавить заказ», «списать товар»);
- историю там не хранят — email меняется «в лоб»: старое значение перезаписывается. - историю там не хранят — email меняется «в лоб»: старое значение перезаписывается.
@@ -79,7 +75,6 @@
| `promos` | Маркетинг | Акции: `promo_id`, `code` | | `promos` | Маркетинг | Акции: `promo_id`, `code` |
⚠️ Обратите внимание: ⚠️ Обратите внимание:
- `customer_id = 101` в одном месяце — `a@ex.com`, в другом — `b@ex.com`; - `customer_id = 101` в одном месяце — `a@ex.com`, в другом — `b@ex.com`;
- цена на товар `9001` (Phone) в январе — 100 ₽, в феврале — 110 ₽; - цена на товар `9001` (Phone) в январе — 100 ₽, в феврале — 110 ₽;
- `order_items` содержит `price_at_sale`*цену в момент покупки*, а не текущую. - `order_items` содержит `price_at_sale`*цену в момент покупки*, а не текущую.
@@ -145,7 +140,7 @@ flowchart TD
Давайте проследим, как превращается строка заказа. Давайте проследим, как превращается строка заказа.
### **STG (Staging / Bronze)**: «как пришло» ### **STG (Staging / Bronze)** «как пришло»
- Таблицы: `stg.orders_raw`, `stg.customers_raw`; - Таблицы: `stg.orders_raw`, `stg.customers_raw`;
- Структура — *точно как в источнике* (может быть `VARCHAR` даже у дат); - Структура — *точно как в источнике* (может быть `VARCHAR` даже у дат);
@@ -158,7 +153,7 @@ flowchart TD
--- ---
### **ODS (Operational Data Store / Silver)**: «почистили, но не трогали смысл» ### **ODS (Operational Data Store / Silver)** «почистили, но не трогали смысл»
- Таблицы: `ods.orders`, `ods.customers`; - Таблицы: `ods.orders`, `ods.customers`;
- Здесь: - Здесь:
@@ -174,7 +169,7 @@ flowchart TD
--- ---
### **DDS (Data Delivery Store / Core / Conformed)**: «интеграция + история» ### **DDS (Data Delivery Store / Core / Conformed)** «интеграция + история»
Здесь рождается *единая бизнес-модель*. Здесь рождается *единая бизнес-модель*.
Появляются понятия: **измерения**, **факты**, **суррогатные ключи**, **SCD**. Появляются понятия: **измерения**, **факты**, **суррогатные ключи**, **SCD**.
@@ -185,7 +180,7 @@ flowchart TD
|---------|------------| |---------|------------|
| `dds.dim_customer` | Измерение «Клиент» с историей (SCD Type 2) | | `dds.dim_customer` | Измерение «Клиент» с историей (SCD Type 2) |
| `dds.dim_product` | Измерение «Товар» | | `dds.dim_product` | Измерение «Товар» |
| `dds.dim_date` | Готовый календарь на 5 лет вперёд (день/неделя/месяц/квартал) | | `dds.dim_date` | Готовый календарь на 10 лет вперёд (день/неделя/месяц/квартал) |
| `dds.fact_sales` | Факт «Продажа» — строка заказа с суммой и количеством | | `dds.fact_sales` | Факт «Продажа» — строка заказа с суммой и количеством |
💡 **Суррогатный ключ (Surrogate Key, SK)** — это `BIGINT`, который мы генерируем сами (например, `customer_sk = 1001`). 💡 **Суррогатный ключ (Surrogate Key, SK)** — это `BIGINT`, который мы генерируем сами (например, `customer_sk = 1001`).
@@ -199,10 +194,9 @@ flowchart TD
--- ---
### **DM (Data Mart / Gold / «Витрины»)**: «готово к употреблению» ### **DM (Data Mart / Gold/ «Витрины»)** «готово к употреблению»
Здесь — таблицы и представления для конкретных задач: Здесь — таблицы и представления для конкретных задач:
- `dm.mart_daily_sales` — ежедневные продажи по товарам и сегментам; - `dm.mart_daily_sales` — ежедневные продажи по товарам и сегментам;
- `dm.mart_customer_360` — полный портрет клиента: сколько потратил, когда заходил, какие товары любит. - `dm.mart_customer_360` — полный портрет клиента: сколько потратил, когда заходил, какие товары любит.
@@ -216,7 +210,6 @@ flowchart TD
> *«10 января 2024 года клиент из Москвы (сегмент Premium) купил Phone за 100 ₽»*. > *«10 января 2024 года клиент из Москвы (сегмент Premium) купил Phone за 100 ₽»*.
В DWH это разложится на: В DWH это разложится на:
- **Факт (Fact)** — событие, которое можно измерить: *покупка*. - **Факт (Fact)** — событие, которое можно измерить: *покупка*.
Хранится в `fact_sales`: `quantity = 1`, `amount = 100`. Хранится в `fact_sales`: `quantity = 1`, `amount = 100`.
- **Измерения (Dimensions)***контекст* факта: - **Измерения (Dimensions)***контекст* факта:
@@ -266,10 +259,9 @@ erDiagram
} }
``` ```
### SCD Type 2: как хранить историю ### SCD Type 2 как хранить историю
Клиент №101: Клиент №101:
- с 1 янв по 15 мая — `email = a@ex.com`, `city = Москва`; - с 1 янв по 15 мая — `email = a@ex.com`, `city = Москва`;
- с 16 мая — `email = b@ex.com`, `city = Москва`; - с 16 мая — `email = b@ex.com`, `city = Москва`;
- с 1 окт — `email = b@ex.com`, `city = Санкт-Петербург`. - с 1 окт — `email = b@ex.com`, `city = Санкт-Петербург`.
@@ -289,136 +281,55 @@ AND (dim_customer.valid_to IS NULL OR fact_sales.order_date < dim_customer.valid
``` ```
и получаем актуальный на тот день email и город. и получаем актуальный на тот день email и город.
> 🔍 Подробнее про SCD — в отдельной статье [Slowly Changing Dimensions](SCD.md) (сравнение Type 1/2/3, паттерны обновления). > 🔍 Подробнее про SCD — в отдельной статье [Slow Changing Dimensions](SCD.md) (сравнение Type 1/2/3, паттерны обновления).
Теперь, когда мы разобрались, что такое факты, измерения и SCD, давайте посмотрим, как именно можно устроить слой DDS внутри — есть несколько вариантов. Теперь, когда мы разобрались, что такое факты, измерения и SCD, давайте посмотрим, как именно можно устроить слой DDS внутри — есть несколько вариантов.
--- ---
## 6. Модели данных для слоя DDS: 4 подхода, и когда какой выбрать ## 6. Модели данных для слоя DDS: 4 подхода и когда какой выбрать
В DDS мы можем хранить данные по-разному. Это не «правильно/неправильно», а **выбор под задачу**. В DDS мы можем хранить данные по-разному. Это не «правильно/неправильно», а **выбор под задачу**.
### 1. 3NF (третья нормальная форма) ### 1. 3NF (третья нормальная форма)
*Источник: Билл Инмон (Bill Inmon)* *Источник: Билл Инмон (Bill Inmon)*
Если упростить, 3NF - это когда данные о разных бизнес-сущностях хранятся в отдельных таблицах и связываются ключами: клиент, заказ, город, регион, страна и т.д. Вместо одной большой таблицы с большим числом дублирующихся данных мы получаем цепочку таблиц, связанных ключами: `Заказ → Клиент → Город → Регион → Страна`. JOIN-ов становится больше, зато одно и то же свойство (например, название города) хранится в одном месте, а не дублируется в каждой строке заказа. **Плюсы**:
- Минимум избыточности при строгих ключах и правилах дедупликации.
- Проще поддерживать единую терминологию и НСИ (reference data).
- Атрибуты и справочники легко расширять.
Исторически подход с ядром в 3NF чаще связывают с Биллом Инмоном (Bill Inmon): сначала проектируют корпоративную модель данных ядра в 3NF (сущности, атрибуты, связи), а уже поверх неё строят витрины. **Минусы**:
- Много JOIN даже для простых отчётов.
- Историчность (SCD2) усложняет таблицы.
- Новые источники дороже гармонизировать (привести к канону).
![Пример цепочки в 3NF](images/3NF-small.jpg) 📌 **Когда выбирать**:
→ Корпоративные DWH, где важна *единая терминология* и *долгосрочная поддержка*.
* Данные о сущностях разнесены по отдельным таблицам: клиент, заказ, продукт. → Стабильные домены (финансы, НСИ, договоры) и умеренная динамика изменений.
* Минимум дублирования: общие атрибуты хранятся в одном месте, таблицы связаны ключами.
#### Как выглядел бы наш магазин в 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)* *Источник: Ральф Кимболл (Ralph Kimball)*
Самый распространённый способ построения таблиц для слоя витрин. Именно эту модель мы использовали в [разделе 5](#5-базовые-понятия-факты-измерения-scd): схема `fact_sales` + `dim_date` / `dim_customer` / `dim_product` - это и есть Звезда. **Плюсы**:
- **Простота**: факт + несколько «плоских» измерений;
- **Скорость**: BI-системы любят звезду — запросы пишутся за 5 минут;
- **Понятно бизнесу**: «продажи по товарам и клиентам» — это ровно то, что в таблицах.
Структура: **Минусы**:
- Дублирование: город будет повторяться в каждой строке клиента;
* в центре - таблица фактов (события и метрики); - Изменение структуры измерения — дорого (перестроить всю витрину).
* вокруг - измерения, обычно денормализованные («плоские») - широкие таблицы со всеми атрибутами сущности. Мы сознательно избегаем цепочек справочников ради простоты запросов.
![Звёздная схема вокруг 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). Цель — так организовать хранилище, чтобы можно было спокойно добавлять новые источники и хранить историю, не ломая старую модель.* *Идея: Дэн Линстедт (Dan Linstedt). Цель — так организовать хранилище, чтобы можно было спокойно добавлять новые источники и хранить историю, не ломая старую модель.*
@@ -435,18 +346,88 @@ WHERE c.city = 'Москва'
- **Satellite (Сателлит)***«какие у них свойства и как они менялись»*. - **Satellite (Сателлит)***«какие у них свойства и как они менялись»*.
Имя клиента, email, статус заказа, цены — всё с историей изменений. Имя клиента, email, статус заказа, цены — всё с историей изменений.
![Пример модели Data Vault](images/data-vault-small.jpg)
💡 **Главная мысль:** 💡 **Главная мысль:**
идентичность, связи и атрибуты живут **в разных таблицах**, поэтому: идентичность, связи и атрибуты живут **в разных таблицах**, поэтому:
- историю проще хранить; - историю проще хранить;
- новые источники проще прикручивать; - новые источники проще прикручивать;
- меньше шансов «сломать» старые отчёты. - меньше шансов «сломать» старые отчёты.
Data Vault хорошо подходит там, где много разнородных источников, нужна полная история изменений и прозрачный аудит. За гибкость приходится платить сложностью модели и количеством таблиц — поэтому для небольших проектов (2–5 источников, маленькая команда) DV почти наверняка избыточен. ---
> 🔍 Подробнее про Data Vault — сравнение с 3NF/Звездой, Raw и Business Vault, когда внедрять — в отдельной статье [DataVault: как пережить бурную жизнь источников](DataVault.md). #### Чем 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 и т.п.) — это уже тема для отдельной, взрослой статьи.
--- ---
@@ -454,21 +435,27 @@ Data Vault хорошо подходит там, где много разнор
*Источник: Ларс Рёне (Lars Rönnbäck)* *Источник: Ларс Рёне (Lars Rönnbäck)*
Anchor Modeling - ещё более атомарный подход к моделированию ядра, чем Data Vault. Если упростить, он «режет» модель на очень мелкие части, чтобы изменения в атрибутах и связях можно было добавлять почти без переделок схемы. Ещё более атомарный подход:
- **Anchor** — сущность (клиент, товар);
- **Attribute** — атрибут (email, имя);
- **Tie** — связь (как Link в DV);
- Все таблицы — 2–3 столбца.
Основные типы таблиц: **Плюсы**:
- **Максимальная гибкость**: поменяли модель — не трогали старые таблицы;
- **Бесконечная эволюция**: можно добавлять атрибуты «задним числом».
* **Anchor** - сущности (например, «клиент» или «заказ»). **Минусы**:
* **Attribute** - отдельный атрибут сущности, обычно с историей (например, email, город, статус - каждый в своей таблице). - Очень сложные запросы (JOIN’ов — десятки);
* **Tie** - связь между сущностями (например, «клиент ↔ заказ»). - Почти не используется «в чистом виде» — чаще как концепция.
![Пример Anchor Modeling](images/anchor-model-small.jpg) 📌 **Когда выбирать**:
→ Экспериментальные проекты;
Плюс подхода - высокая гибкость: проще добавлять новые атрибуты и варианты связей. Минус - цена этой гибкости: получается очень много таблиц, и запросы (и поддержка модели) обычно заметно сложнее, огромное кол-во `JOIN`. Для обычного DWH-проекта это точно не первый выбор - скорее вариант для очень динамичных предметных областей, где структура данных часто меняется. → Когда схема данных *каждый месяц* радикально меняется.
--- ---
### Сравнение моделей наглядно ### Сравнение моделей наглядно
```mermaid ```mermaid
quadrantChart quadrantChart
@@ -540,7 +527,7 @@ flowchart TD
### Готовые SQL-скрипты ### Готовые SQL-скрипты
Все необходимые скрипты для построения хранилища находятся в папке [`sql/`](https://github.com/dementev-dev/de-roadmap/tree/main/dwh-modeling/sql): Все необходимые скрипты для построения хранилища находятся в папке [`sql/`](sql/):
- [`01_ddl_stg-dds.sql`](sql/01_ddl_stg-dds.sql) — создание схем и таблиц (STG, ODS, DDS); - [`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; - [`02_dml_stg-dds.sql`](sql/02_dml_stg-dds.sql) — первичная загрузка данных и демонстрация SCD2 через полный пересчёт (`full backfill`) из STG;
@@ -553,17 +540,11 @@ flowchart TD
```sql ```sql
-- mart_daily_sales: ежедневные продажи с сегментацией -- mart_daily_sales: ежедневные продажи с сегментацией
-- Полная пересборка (full refresh) - для простоты; в продакшене бывает incremental. CREATE MATERIALIZED VIEW dm.mart_daily_sales AS
TRUNCATE dm.mart_daily_sales;
INSERT INTO dm.mart_daily_sales (
date_actual, product_name, customer_segment, total_qty, total_revenue
)
SELECT SELECT
d.date_actual, d.date_actual AS order_date,
p.product_name, 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.quantity) AS total_qty,
SUM(f.amount) AS total_revenue SUM(f.amount) AS total_revenue
FROM dds.fact_sales f FROM dds.fact_sales f
@@ -572,14 +553,13 @@ JOIN dds.dim_date d
JOIN dds.dim_product p JOIN dds.dim_product p
ON f.product_sk = p.product_sk ON f.product_sk = p.product_sk
JOIN dds.dim_customer c JOIN dds.dim_customer c
ON f.customer_sk = c.customer_sk -- факт ссылается на нужную версию SK ON f.customer_sk = c.customer_sk
GROUP BY d.date_actual, p.product_name, AND f.order_date >= c.valid_from
CASE WHEN f.amount >= 200 THEN 'Premium' ELSE 'Basic' END; 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;
``` ```
> 💡 В продакшене витрину иногда оформляют как **MATERIALIZED VIEW** - «кэш» результата запроса, который обновляется по расписанию. В нашем примере используем обычную таблицу с `TRUNCATE` + `INSERT` - для учебных целей это нагляднее. > 💡 **Материализованное представление (MATERIALIZED VIEW)** — это «кэш» результата. Обновляется по расписанию (например, ночью).
✏️ **Попробуйте сами:** [Домашка: статусы клиента от STG до DDS (и немного DM)](Homework_Customer_Status_DDS_DM.md) — пройдёте тот же путь, но самостоятельно.
--- ---
@@ -605,7 +585,7 @@ GROUP BY d.date_actual, p.product_name,
--- ---
### ✅ Базовые советы: с чего начать, если вы учитесь или делаете первый DWH ### ✅ Базовые советы с чего начать, если вы учитесь или делаете первый DWH
1. **Начните с витрины в формате Звезды (Star Schema).** 1. **Начните с витрины в формате Звезды (Star Schema).**
— Это просто: одна таблица фактов + несколько «плоских» измерений. — Это просто: одна таблица фактов + несколько «плоских» измерений.
@@ -632,7 +612,7 @@ GROUP BY d.date_actual, p.product_name,
Почему: ему нужны готовые метрики без сложных JOIN’ов. Звезда даёт понятные таблицы: «продажи по дням и товарам» — без углубления в атомарные сущности. Почему: ему нужны готовые метрики без сложных JOIN’ов. Звезда даёт понятные таблицы: «продажи по дням и товарам» — без углубления в атомарные сущности.
**BI-разработчик в Power BI / Tableau****Звезда** **BI-разработчик в Power BI / Tableau****Звезда**
Почему: все инструменты визуализации оптимизированы под star schema. Один факт + несколько измерений = быстрые отчёты и простую модель. Почему: все инструменты визуализации оптимизированы под star schema. Один факт + несколько измерений = быстрые отчёты и простою модель.
**Инженер ML (Data Scientist / ML-инженер)****3NF или сырые ODS-таблицы** **Инженер ML (Data Scientist / ML-инженер)****3NF или сырые ODS-таблицы**
Почему: для фичей нужны атомарные события и детальные атрибуты. Машинное обучение ценит полноту и детализацию данных больше, чем удобство отчётов. Почему: для фичей нужны атомарные события и детальные атрибуты. Машинное обучение ценит полноту и детализацию данных больше, чем удобство отчётов.
@@ -660,7 +640,7 @@ GROUP BY d.date_actual, p.product_name,
--- ---
### 📌 Кратко: что выбрать *сегодня*, если вы только учитесь ### 📌 Кратко что выбрать *сегодня*, если вы только учитесь
| У вас… | Делайте… | | У вас… | Делайте… |
|--------|----------| |--------|----------|
@@ -672,7 +652,7 @@ GROUP BY d.date_actual, p.product_name,
--- ---
## 9. Эксплуатация: качество данных, это не «опция» ## 9. Эксплуатация: качество данных это не «опция»
Самая красивая архитектура бессмысленна, если в `mart_daily_sales` — нули. Самая красивая архитектура бессмысленна, если в `mart_daily_sales` — нули.
Поэтому в каждом слое — **контроль качества (DQ, Data Quality)**. Поэтому в каждом слое — **контроль качества (DQ, Data Quality)**.
@@ -715,7 +695,7 @@ SELECT 'OK' WHERE EXISTS (
--- ---
## 10. Заключение: главное, понимать «почему» ## 10. Заключение: главное понимать «почему»
Хранилище данных — это не про «крутые технологии», а про **мышление**: Хранилище данных — это не про «крутые технологии», а про **мышление**:
@@ -791,11 +771,11 @@ SELECT 'OK' WHERE EXISTS (
### Мини-датасет (для практики) ### Мини-датасет (для практики)
Все данные для практики находятся в папке [`data/`](https://github.com/dementev-dev/de-roadmap/tree/main/dwh-modeling/data) — тренируйтесь: Все данные для практики находятся в папке [`data/`](data/) — тренируйтесь:
[`customers.csv`](data/customers.csv): [`customers.csv`](data/customers.csv):
```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 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 102,c@ex.com,701,СПб,2024-01-01,batch_20240101_0800,2024-01-01 08:00
101,b@ex.com,700,Москва,2024-05-16,batch_20240516_0800,2024-05-16 08:00 101,b@ex.com,700,Москва,2024-05-16,batch_20240516_0800,2024-05-16 08:00
@@ -832,7 +812,7 @@ product_id,valid_from,valid_to,price
9002,2023-01-01,,50 9002,2023-01-01,,50
``` ```
> 📂 Все SQL-скрипты для построения хранилища находятся в папке [`sql/`](https://github.com/dementev-dev/de-roadmap/tree/main/dwh-modeling/sql). > 📂 Все SQL-скрипты для построения хранилища находятся в папке [`sql/`](sql/).
--- ---
+7 -8
View File
@@ -28,15 +28,15 @@ SCD — это подход к хранению изменений в измер
--- ---
## 3. Типы SCD простыми словами ## 3. Типы SCD простыми словами
Существует несколько стандартных стратегий обработки изменений. Рассмотрим самые важные. Существует несколько стандартных стратегий обработки изменений. Рассмотрим самые важные.
### **Type 0: никогда не меняется** ### **Type 0Никогда не меняется**
Атрибут фиксирован навсегда. Например, дата рождения клиента. Атрибут фиксирован навсегда. Например, дата рождения клиента.
Такие поля не требуют специальной обработки — они просто не обновляются. Такие поля не требуют специальной обработки — они просто не обновляются.
### **Type 1: просто перезаписать** ### **Type 1 — Просто перезаписать**
Вы просто делаете `UPDATE`, и старое значение исчезает. Вы просто делаете `UPDATE`, и старое значение исчезает.
✅ Просто. ✅ Просто.
@@ -44,7 +44,7 @@ SCD — это подход к хранению изменений в измер
> Подходит, если изменение — это исправление ошибки (например, опечатка в имени). > Подходит, если изменение — это исправление ошибки (например, опечатка в имени).
### **Type 2: новая строка для новой версии** ### **Type 2Новая строка для новой версии**
Каждое изменение порождает **новую строку** в таблице. Старая строка остаётся, но помечается как «устаревшая». Каждое изменение порождает **новую строку** в таблице. Старая строка остаётся, но помечается как «устаревшая».
✅ Полная история. ✅ Полная история.
@@ -53,7 +53,7 @@ SCD — это подход к хранению изменений в измер
> Это **самый распространённый** подход в аналитике. > Это **самый распространённый** подход в аналитике.
### **Type 3: добавить колонку «предыдущее значение»** ### **Type 3 — Добавить колонку «предыдущее значение»**
В таблице появляются поля вроде `previous_category`, `category_change_date`. В таблице появляются поля вроде `previous_category`, `category_change_date`.
✅ Простая история «до/после». ✅ Простая история «до/после».
@@ -61,7 +61,7 @@ SCD — это подход к хранению изменений в измер
> Используется редко, чаще как компромисс в очень простых системах. > Используется редко, чаще как компромисс в очень простых системах.
### **Type 4, 5, 6: продвинутые гибриды** ### **Type 4, 5, 6 — Продвинутые гибриды**
Эти типы существуют, но **встречаются редко** и почти не используются новичками: Эти типы существуют, но **встречаются редко** и почти не используются новичками:
- **Type 4**: история выносится в отдельную таблицу («мини-хранилище» для одного измерения). - **Type 4**: история выносится в отдельную таблицу («мини-хранилище» для одного измерения).
@@ -99,7 +99,7 @@ WHERE customer_id = 1;
--- ---
### Type 2: сохраняем историю (подробнее) ### Type 2: сохраняем историю подробнее
Чтобы хранить историю, мы меняем структуру таблицы. Вот ключевые поля: Чтобы хранить историю, мы меняем структуру таблицы. Вот ключевые поля:
@@ -283,7 +283,6 @@ LEFT JOIN current_customers c ON n.customer_id = c.customer_id
###### Шаг 3: Вставка новых версий ###### Шаг 3: Вставка новых версий
Для подходящих записей создаём новую версию: Для подходящих записей создаём новую версию:
- `uuid()` — генерируем уникальный ключ для новой версии - `uuid()` — генерируем уникальный ключ для новой версии
- `current_date` - функция, возвращающая текущую даты - `current_date` - функция, возвращающая текущую даты
- `COALESCE(n.effective_date, 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,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,active,2024-02-15 10:30:00,batch_20240215_1100,2024-02-15 11:00:00
101,vip,2024-05-10 11:00:00,batch_20240510_1200,2024-05-10 12:00:00 101,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
@@ -1,4 +1,4 @@
customer_id,status,event_ts,_load_id,_load_ts 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 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 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 103,active,2024-03-20 12:00:00,batch_20240320_1300,2024-03-20 13:00:00
1 customer_id status event_ts _load_id _load_ts load_ts
2 101 active 2024-11-15 09:00:00 batch_20241115_1000 2024-11-15 10:00:00 2024-11-15 10:00:00
3 102 active 2024-05-05 09:30:00 batch_20240505_1000 2024-05-05 10:00:00 2024-05-05 10:00:00
4 103 active 2024-03-20 12:00:00 batch_20240320_1300 2024-03-20 13:00:00 2024-03-20 13: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 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 102,c@ex.com,701,СПб,2024-01-01,batch_20240101_0800,2024-01-01 08:00
101,b@ex.com,700,Москва,2024-05-16,batch_20240516_0800,2024-05-16 08:00 101,b@ex.com,700,Москва,2024-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.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

-7
View File
@@ -2,13 +2,6 @@
-- DML-скрипт: загрузка и трансформация данных -- 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) -- 1. STG: имитация загрузки из источников (в реальности — COPY или INSERT из Kafka/NiFi)
-- ⚠️ В продакшене STG часто очищается перед загрузкой (TRUNCATE), либо используется партицирование по дате -- ⚠️ В продакшене STG часто очищается перед загрузкой (TRUNCATE), либо используется партицирование по дате
+1 -1
View File
@@ -21,7 +21,7 @@ CREATE TABLE dm.mart_customer_360 (
customer_bk INT NOT NULL, customer_bk INT NOT NULL,
first_order_date DATE, first_order_date DATE,
last_order_date DATE, last_order_date DATE,
total_line_items INT NOT NULL, total_orders INT NOT NULL,
total_items INT NOT NULL, total_items INT NOT NULL,
lifetime_value NUMERIC(18,2) NOT NULL, lifetime_value NUMERIC(18,2) NOT NULL,
last_email VARCHAR(100), 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 ( INSERT INTO dm.mart_customer_360 (
customer_bk, first_order_date, last_order_date, customer_bk, first_order_date, last_order_date,
total_line_items, total_items, lifetime_value, total_orders, total_items, lifetime_value,
last_email, last_city last_email, last_city
) )
SELECT SELECT
c.customer_bk, c.customer_bk,
MIN(d.date_actual) AS first_order_date, MIN(d.date_actual) AS first_order_date,
MAX(d.date_actual) AS last_order_date, MAX(d.date_actual) AS last_order_date,
COUNT(DISTINCT f.sale_id) AS total_line_items, -- строки факта (позиции продаж), не бизнес-заказы COUNT(DISTINCT f.sale_id) AS total_orders, -- считаем строки факта (продажи), не бизнес-заказы
SUM(f.quantity) AS total_items, SUM(f.quantity) AS total_items,
SUM(f.amount) AS lifetime_value, SUM(f.amount) AS lifetime_value,
-- Берём самый свежий email и город клиента -- Берём самый свежий email и город клиента
@@ -46,11 +46,3 @@ ALTER TABLE dds.dim_customer_status
CREATE INDEX ix_dim_customer_status_bk_current CREATE INDEX ix_dim_customer_status_bk_current
ON dds.dim_customer_status (customer_bk) ON dds.dim_customer_status (customer_bk)
WHERE valid_to IS NULL; 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,6 +38,11 @@
-- Можно ориентироваться на примеры в 03_demo_increment.sql. -- Можно ориентироваться на примеры в 03_demo_increment.sql.
-- 4. DM: витрина статусов клиентов по датам (по желанию) -- 4. DM: витрина статусов клиентов по датам (по желанию)
-- DDL витрины уже создан в 07_ddl_hw_customer_status.sql (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
-- );
-- Идея: на каждую дату взять актуальный статус клиента -- Идея: на каждую дату взять актуальный статус клиента
-- через JOIN dds.dim_customer_status + dds.dim_date. -- через JOIN dds.dim_customer_status + dds.dim_date.
@@ -2,19 +2,15 @@
-- 09_dml_hw_customer_status_solution.sql -- 09_dml_hw_customer_status_solution.sql
-- Решение домашки: статусы клиента (STG -> ODS -> DDS SCD2 -> DM) -- Решение домашки: статусы клиента (STG -> ODS -> DDS SCD2 -> DM)
-- --
-- Это ЭТАЛОННОЕ РЕШЕНИЕ. Если вы ещё не пробовали решить домашку сами -
-- вернитесь к заданию (Homework_Customer_Status_DDS_DM.md) и шаблону (08_dml_hw_customer_status_template.sql).
-- Основная ценность задания - в самостоятельном разборе.
--
-- Что делает этот файл: -- Что делает этот файл:
-- 1) Перекладывает события статусов в ODS (приводит типы, чистит пустое). -- 1) Перекладывает события статусов в ODS (приводит типы, чистит пустое).
-- 2) Строит DDS-измерение со "встроенной историей" (SCD2): периоды valid_from/valid_to. -- 2) Строит DDS-измерение со "встроенной историей" (SCD2): периоды valid_from/valid_to.
-- 3) Загружает инкрементальную порцию событий и обновляет ODS + DDS. -- 3) Показывает пример обновления DDS маленькой порцией (инкремент): закрыть старое + вставить новое.
-- 4) Собирает простую витрину в DM: сколько клиентов в каком статусе по дням. -- 4) Собирает простую витрину в DM: сколько клиентов в каком статусе по дням.
-- --
-- Как запускать: -- Как запускать:
-- - для первого знакомства можно запускать файл целиком; -- - для первого знакомства можно запускать файл целиком;
-- - если хотите потренировать инкремент (п.3): добавьте свои события -> запустите блок 3 ещё раз. -- - если хотите потренировать инкремент (п.3): добавьте новые события -> обновите ODS -> запустите блок 3 ещё раз.
-- --
-- Важно: -- Важно:
-- - здесь часто используется TRUNCATE (полная очистка), чтобы было легко повторять домашку; -- - здесь часто используется TRUNCATE (полная очистка), чтобы было легко повторять домашку;
@@ -22,10 +18,9 @@
-- --
-- Предусловия (DDL + данные в STG): -- Предусловия (DDL + данные в STG):
-- 1) dwh-modeling/sql/01_ddl_stg-dds.sql -- 1) dwh-modeling/sql/01_ddl_stg-dds.sql
-- 2) dwh-modeling/sql/02_dml_stg-dds.sql (нужен dim_date) -- 2) dwh-modeling/sql/05_ddl_dm.sql
-- 3) dwh-modeling/sql/05_ddl_dm.sql -- 3) dwh-modeling/sql/07_ddl_hw_customer_status.sql
-- 4) dwh-modeling/sql/07_ddl_hw_customer_status.sql -- 4) stg.customer_status_raw заполнена (см. dwh-modeling/Homework_Customer_Status_DDS_DM.md)
-- 5) stg.customer_status_raw заполнена (см. dwh-modeling/Homework_Customer_Status_DDS_DM.md)
-- =============================================== -- ===============================================
-- ========================================================== -- ==========================================================
@@ -36,13 +31,6 @@
-- - STG хранит "как пришло" (обычно TEXT); -- - STG хранит "как пришло" (обычно TEXT);
-- - ODS хранит "аккуратно": правильные типы + простая чистка. -- - ODS хранит "аккуратно": правильные типы + простая чистка.
-- Для простоты пересобираем 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; TRUNCATE ods.customer_status;
@@ -60,10 +48,6 @@ WHERE s.customer_id ~ '^\d+$'
AND NULLIF(trim(s.event_ts), '') IS NOT NULL AND NULLIF(trim(s.event_ts), '') IS NOT NULL
AND NULLIF(trim(s.status), '') 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) -- 2) DDS: начальная загрузка SCD2 (full refresh)
-- ========================================================== -- ==========================================================
@@ -132,66 +116,29 @@ SELECT
FROM framed FROM framed
ORDER BY customer_bk, valid_from; 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 -- 3) DDS: инкрементальная загрузка SCD2 (по последним событиям)
-- ========================================================== -- ==========================================================
-- Имитируем приход новой порции событий (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; -- 1) берём по каждому клиенту самое позднее событие из ODS;
-- 2) сравниваем с текущей версией в DDS (valid_to IS NULL); -- 2) сравниваем его с текущей версией в DDS (valid_to IS NULL);
-- 3) если статус изменился - закрываем старую версию и вставляем новую. -- 3) если статус изменился закрываем старую версию и вставляем новую.
-- --
-- Ограничение учебного варианта: -- Ограничение учебного варианта (в домашке можно игнорировать):
-- - если добавили событие "задним числом" со старой датой, этот блок не пересоберёт всю историю. -- - если вы добавили событие "задним числом" со старой датой, этот блок не пересоберёт всю историю.
-- Для такого кейса обычно делают full refresh (блок 2). -- Для такого кейса обычно делают отдельную логику или full refresh.
--
-- Примечание:
-- - в этом файле блок 2 (full refresh) запускается раньше, поэтому сразу после него
-- блок 3, скорее всего, ничего не изменит. Зато его можно повторять после новых событий.
BEGIN; BEGIN;
-- 3.2a) Закрываем предыдущую актуальную версию -- 3.1) Закрываем предыдущую актуальную версию
WITH ranked AS ( WITH ranked AS (
-- ranked: выбираем "самое свежее" событие на клиента -- ranked: выбираем "самое свежее" событие на клиента.
-- Если event_ts одинаковый, берём то, что загрузилось позже (_load_ts).
SELECT SELECT
customer_id AS customer_bk, customer_id AS customer_bk,
status, status,
@@ -208,8 +155,8 @@ BEGIN;
-- delta: ровно одна строка на клиента (самое свежее событие) -- delta: ровно одна строка на клиента (самое свежее событие)
SELECT * FROM ranked WHERE rn = 1 SELECT * FROM ranked WHERE rn = 1
), ),
current_ver AS ( current AS (
-- current_ver: текущие версии в DDS (valid_to IS NULL) -- current: текущие версии в DDS (valid_to IS NULL)
SELECT d.* SELECT d.*
FROM dds.dim_customer_status d FROM dds.dim_customer_status d
WHERE d.valid_to IS NULL WHERE d.valid_to IS NULL
@@ -225,7 +172,7 @@ BEGIN;
t.eff_date, t.eff_date,
c.customer_status_sk c.customer_status_sk
FROM delta t FROM delta t
JOIN current_ver c JOIN current c
ON c.customer_bk = t.customer_bk ON c.customer_bk = t.customer_bk
WHERE c.hashdiff <> t.hashdiff WHERE c.hashdiff <> t.hashdiff
AND t.eff_date > c.valid_from -- не создаём период нулевой/отрицательной длины AND t.eff_date > c.valid_from -- не создаём период нулевой/отрицательной длины
@@ -233,9 +180,9 @@ BEGIN;
WHERE d.customer_status_sk = x.customer_status_sk WHERE d.customer_status_sk = x.customer_status_sk
AND d.valid_to IS NULL; AND d.valid_to IS NULL;
-- 3.2b) Вставляем новую версию -- 3.2) Вставляем новую версию
WITH ranked AS ( WITH ranked AS (
-- ranked/delta/current_ver повторяем отдельно, чтобы блок INSERT читался отдельно от UPDATE -- ranked/delta/current повторяем отдельно, чтобы блок INSERT читался отдельно от UPDATE
SELECT SELECT
customer_id AS customer_bk, customer_id AS customer_bk,
status, status,
@@ -251,14 +198,14 @@ BEGIN;
delta AS ( delta AS (
SELECT * FROM ranked WHERE rn = 1 SELECT * FROM ranked WHERE rn = 1
), ),
current_ver AS ( current AS (
SELECT d.* SELECT d.*
FROM dds.dim_customer_status d FROM dds.dim_customer_status d
WHERE d.valid_to IS NULL WHERE d.valid_to IS NULL
), ),
to_insert AS ( to_insert AS (
-- to_insert: кого "вставляем": -- to_insert: кого "вставляем":
-- 1) новый клиент (в current_ver нет строки); -- 1) новый клиент (в current нет строки);
-- 2) изменившийся клиент (статус поменялся). -- 2) изменившийся клиент (статус поменялся).
SELECT SELECT
t.customer_bk, t.customer_bk,
@@ -266,7 +213,7 @@ BEGIN;
t.hashdiff, t.hashdiff,
t.eff_date t.eff_date
FROM delta t FROM delta t
LEFT JOIN current_ver c LEFT JOIN current c
ON c.customer_bk = t.customer_bk ON c.customer_bk = t.customer_bk
WHERE c.customer_status_sk IS NULL WHERE c.customer_status_sk IS NULL
OR (c.hashdiff <> t.hashdiff AND t.eff_date > c.valid_from) OR (c.hashdiff <> t.hashdiff AND t.eff_date > c.valid_from)
@@ -290,11 +237,6 @@ BEGIN;
); );
COMMIT; 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) -- 4) DM: витрина статусов клиентов по датам (full refresh)
-- ========================================================== -- ==========================================================
@@ -302,18 +244,19 @@ SELECT * FROM dds.dim_customer_status ORDER BY customer_bk, valid_from;
-- Витрина "снимок на дату": -- Витрина "снимок на дату":
-- для каждого дня считаем, сколько клиентов было в каждом статусе. -- для каждого дня считаем, сколько клиентов было в каждом статусе.
-- Берём календарь dds.dim_date и подбираем статус по периоду valid_from/valid_to. -- Берём календарь dds.dim_date и подбираем статус по периоду valid_from/valid_to.
-- DDL витрины - в 07_ddl_hw_customer_status.sql.
CREATE TABLE IF NOT EXISTS dm.mart_customer_status_daily (
date_actual DATE NOT NULL,
status VARCHAR(20) NOT NULL,
customers_cnt INT NOT NULL
);
TRUNCATE dm.mart_customer_status_daily; TRUNCATE dm.mart_customer_status_daily;
WITH bounds AS ( WITH bounds AS (
SELECT SELECT
min(valid_from) AS date_from, min(valid_from) AS date_from,
-- CURRENT_DATE для открытых интервалов (valid_to IS NULL = текущий статус), max(coalesce(valid_to, valid_from)) AS date_to
-- иначе витрина не покроет даты после последней смены статуса.
-- Нюанс: количество строк в витрине зависит от даты запуска (каждый
-- день добавляется ещё один день). Для учебных целей это приемлемо.
max(coalesce(valid_to, CURRENT_DATE)) AS date_to
FROM dds.dim_customer_status FROM dds.dim_customer_status
) )
INSERT INTO dm.mart_customer_status_daily ( INSERT INTO dm.mart_customer_status_daily (
@@ -331,10 +274,3 @@ JOIN dds.dim_customer_status s
AND (s.valid_to IS NULL OR d.date_actual < s.valid_to) AND (s.valid_to IS NULL OR d.date_actual < s.valid_to)
GROUP BY d.date_actual, s.status GROUP BY d.date_actual, s.status
ORDER 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
@@ -1,29 +0,0 @@
# Конфигурация 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
@@ -1,89 +0,0 @@
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
@@ -1,70 +0,0 @@
{% 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
@@ -1,353 +0,0 @@
# 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
@@ -1,173 +0,0 @@
# 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.
-62
View File
@@ -1,62 +0,0 @@
# TODO: общие задачи проекта
Приоритеты: P1 — делаем в первую очередь; P2 — полезно, когда дойдут руки; P3 — идеи под вопросом.
## 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] **Публикация сайта перенесена на Gitea Actions и VPS**
2026-08-05: настроены repository-scoped host runner, строгая сборка MkDocs,
атомарные релизы, nginx и TLS для `de.dementev.space`; GitHub Pages сохранён
как неактивный резерв.
- [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.
-212
View File
@@ -1,212 +0,0 @@
# Эксплуатация публикации `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`.
- Публичный IPv4 VPS: `167.224.64.252`.
## Подготовка 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 и первичная публикация
Из корня репозитория установить bootstrap-конфигурацию nginx, создать ссылку,
отключить стандартный сайт Ubuntu и только затем перечитать проверенную
конфигурацию:
```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 unlink /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
```
Первый server block в `nginx.conf` возвращает `404` для неизвестных HTTP Host,
не раскрывает версию nginx и отклоняет TLS handshake для IP или неизвестного
SNI. Поэтому сертификат `de.dementev.space` не выдаётся при обращении к VPS по
IP. Если проверка конфигурации не прошла, отключить новый virtual host,
восстановить стандартный сайт и перечитать проверенную конфигурацию:
```bash
sudo unlink /etc/nginx/sites-enabled/de-roadmap
sudo ln -s \
/etc/nginx/sites-available/default \
/etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
```
До первого workflow можно собрать сайт вручную и опубликовать его тем же
скриптом с тестовым release id. Проверка до переключения DNS:
```bash
curl --header 'Host: de.dementev.space' http://127.0.0.1/
```
После локальной проверки разрешить профили `Nginx Full` в UFW. До этого
публичные порты `80/tcp` и `443/tcp` должны оставаться закрытыми.
Файл `project/ops/gitea-vps-site/nginx.conf` предназначен только для запуска до
выпуска сертификата. После выпуска сертификата Certbot изменяет установленный
virtual host. Повторная установка bootstrap-файла поверх рабочего конфига
удалит TLS-директивы.
## Окружение сборки
Скрипт `.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. Выпустить сертификат и включить перенаправление HTTP на HTTPS:
```bash
sudo certbot --nginx \
--non-interactive \
--agree-tos \
--email me@dementev.space \
--redirect \
-d de.dementev.space
```
5. В созданном Certbot HTTP-блоке для `de.dementev.space` добавить
`server_tokens off;`, затем проверить и перечитать конфигурацию:
```bash
sudoedit /etc/nginx/sites-available/de-roadmap
sudo nginx -t
sudo systemctl reload nginx
```
6. Проверить перенаправление, HTTPS, сертификат и автоматическое продление:
```bash
curl --head http://de.dementev.space/
curl --fail --head https://de.dementev.space/
! curl --insecure --head https://167.224.64.252/
sudo certbot certificates
systemctl is-enabled certbot.timer
systemctl is-active certbot.timer
```
Ожидаются `301 Moved Permanently`, затем `200 OK`, отказ TLS по IP,
действующий сертификат и состояния таймера `enabled` и `active`.
7. Вернуть обычный 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/
```
@@ -1,45 +0,0 @@
[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
@@ -1,37 +0,0 @@
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
-34
View File
@@ -1,34 +0,0 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
server_tokens off;
ssl_reject_handshake on;
return 404;
}
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;
}
}
@@ -1,114 +0,0 @@
# Публикация сайта через 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 перезаписывает расходящиеся изменения.