docs(course): смягчён регистр уроков 0–1 и убран статус «черновик»

- Зачем:
  - на уроке 2 решили писать разжёванным языком; уроки 0–1 и шапки курса
    остались в сжатом регистре, а слово «черновик»/«Режим: руки» путало менти.
- Что:
  - урок 1: расшифрованы staging, MergeTree-дедуп, Materialized View и
    виртуальные колонки; секция «Загляни внутрь» разбита на ###-подзаголовки;
    плотные абзацы разбиты на пункты; добавлен зачин «О чём урок простыми словами».
  - урок 0: добавлен зачин «О чём урок простыми словами» (лёгкая полировка).
  - шапки всех уроков: «Статус: черновик. Режим: руки/наблюдение» заменены на
    понятное «Формат: практика/наблюдение — …».
  - PRD/LEARNING_PLAN/LESSON_STANDARD: убрано слово «черновик» из статуса.
- Проверка:
  - grep -rn "черновик" docs/course/ — пусто;
  - прочитать урок 1 сверху вниз: термины раскрыты на первом употреблении.
This commit is contained in:
2026-06-05 18:00:13 +03:00
parent 256ac14c66
commit f56166181b
6 changed files with 58 additions and 27 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# План обучения: курс «Кликстрим на ClickHouse» # План обучения: курс «Кликстрим на ClickHouse»
> Статус: черновик. Дата: 2026-06-03 (аудит путей выполнен; середина расщеплена — > Дата: 2026-06-03 (аудит путей выполнен; середина расщеплена —
> один паттерн на урок, всего 7 уроков, см. §1–2). > один паттерн на урок, всего 7 уроков, см. §1–2).
> Назначение: высокоуровневый маршрут менти по курсу — карта уроков, порядок, > Назначение: высокоуровневый маршрут менти по курсу — карта уроков, порядок,
> результаты аудита эталонных путей. Рамка курса (зачем/что/скоуп) — в `PRD.md`; > результаты аудита эталонных путей. Рамка курса (зачем/что/скоуп) — в `PRD.md`;
+1 -1
View File
@@ -1,6 +1,6 @@
# Стандарт уроков курса «Кликстрим на ClickHouse» # Стандарт уроков курса «Кликстрим на ClickHouse»
> Статус: черновик. Дата: 2026-06-01. > Дата: 2026-06-01.
> Назначение: рабочий чеклист, по которому пишется **каждый** урок. Открывается при > Назначение: рабочий чеклист, по которому пишется **каждый** урок. Открывается при
> создании урока. Рамка курса (зачем/что/скоуп) — в `PRD.md`; карта уроков и > создании урока. Рамка курса (зачем/что/скоуп) — в `PRD.md`; карта уроков и
> маршрут — в `LEARNING_PLAN.md`. > маршрут — в `LEARNING_PLAN.md`.
+1 -1
View File
@@ -1,6 +1,6 @@
# PRD: продвинутый курс «Кликстрим на ClickHouse» (со звёздочкой) # PRD: продвинутый курс «Кликстрим на ClickHouse» (со звёздочкой)
> Статус: черновик (прообраз PRD). Дата: 2026-06-01. > Статус: прообраз PRD. Дата: 2026-06-01.
> Поправка 2026-06-03 (разморозка по делу): середина пайплайна расщеплена — ODS и DDS > Поправка 2026-06-03 (разморозка по делу): середина пайплайна расщеплена — ODS и DDS
> теперь разные уроки (принцип «один паттерн на урок»), витрины DM демотированы в > теперь разные уроки (принцип «один паттерн на урок»), витрины DM демотированы в
> поверхность потребления. Обязательных уроков стало 0–5, опциональный Superset — урок 6. > поверхность потребления. Обязательных уроков стало 0–5, опциональный Superset — урок 6.
+4 -1
View File
@@ -1,6 +1,6 @@
# Урок 0. Вводный по Kafka (Kafka UI, наблюдение) # Урок 0. Вводный по Kafka (Kafka UI, наблюдение)
> Статус: черновик. Режим: **наблюдение** (ничего не меняем, только смотрим). > Формат: **наблюдение** ничего не запускаем и не меняем, только смотрим.
> Пререквизит: обзорное видео по Kafka из роадмапа — оттуда ты уже знаешь слова > Пререквизит: обзорное видео по Kafka из роадмапа — оттуда ты уже знаешь слова
> «топик», «партиция», «offset», «consumer-группа», «lag». Этот урок связывает их > «топик», «партиция», «offset», «consumer-группа», «lag». Этот урок связывает их
> с живым стендом, чтобы они перестали быть просто словами. > с живым стендом, чтобы они перестали быть просто словами.
@@ -8,6 +8,9 @@
> >
> Поток данных одной строкой: > Поток данных одной строкой:
> `make data → топики Kafka (партиции, offset'ы) → consumer-группа ClickHouse вычитывает` > `make data → топики Kafka (партиции, offset'ы) → consumer-группа ClickHouse вычитывает`
>
> О чём урок простыми словами: ходим по Kafka UI и разглядываем поток — где лежат события,
> кто их читает и как Kafka помнит, до какого места уже дочитано.
--- ---
+50 -22
View File
@@ -1,20 +1,26 @@
# Урок 1. Заземление Kafka → ClickHouse (слой STG) # Урок 1. Заземление Kafka → ClickHouse (слой STG)
> Статус: черновик. Режим: **руки**. > Формат: **практика** — будешь сам запускать команды и менять код, не только читать.
> Пререквизит: пройден урок 0 (словарь Kafka — топик, партиция, offset, consumer-группа — > Пререквизит: пройден урок 0 (словарь Kafka — топик, партиция, offset, consumer-группа —
> уже знаком и виден в Kafka UI). > уже знаком и виден в Kafka UI).
> Эталонный путь: [`sql/ddl/stg/10_stg.sql`](../../../sql/ddl/stg/10_stg.sql). > Эталонный путь: [`sql/ddl/stg/10_stg.sql`](../../../sql/ddl/stg/10_stg.sql).
> >
> Поток данных одной строкой: > Поток данных одной строкой:
> `Kafka → kafka_*_raw (ENGINE=Kafka) → MV → *_raw (MergeTree)` > `Kafka → kafka_*_raw (ENGINE=Kafka) → MV → *_raw (MergeTree)`
>
> О чём урок простыми словами: смотрим, как сообщение из Kafka-топика само, без нашего
> участия, превращается в строку таблицы ClickHouse — и почему на этом первом слое мы кладём
> JSON целиком, ничего в нём не разбирая.
--- ---
## 1. Зачем и где в проде ## 1. Зачем и где в проде
Первое, что делаем с потоком событий — складываем его в таблицу как есть, а рядом пишем Первое, что делаем с потоком событий — складываем его в таблицу **как есть**, ничего в нём
метаданные доставки: из какого топика и партиции пришло сообщение, с каким offset'ом и не меняя. А рядом, в соседних колонках, пишем метаданные доставки: из какого топика и партиции
временем. Это слой STG — тот самый staging, знакомый тебе по курсовой. пришло сообщение, с каким offset'ом и в какое время. Это слой **STG** — тот самый staging,
знакомый тебе по курсовой: первая «посадочная площадка», куда поток приземляется в сыром виде,
до любой обработки.
Зачем хранить сырой JSON строкой и не парсить его сразу: Зачем хранить сырой JSON строкой и не парсить его сразу:
@@ -24,10 +30,15 @@
- и, главное, чтобы приём не падал из-за одного кривого поля. Разбор JSON и проверки - и, главное, чтобы приём не падал из-за одного кривого поля. Разбор JSON и проверки
качества — это уже следующий слой (урок 2), а STG принимает всё подряд. качества — это уже следующий слой (урок 2), а STG принимает всё подряд.
`kafka_offset` здесь не просто метаданные: пара «партиция + offset» однозначно указывает Отдельно про `kafka_offset` — это не просто справочная метка. Помнишь из урока 0: пара
на конкретное сообщение в топике — по ней всегда понятно, та же это запись или другая. «партиция + offset» однозначно указывает на конкретное сообщение в топике. По ней всегда
Сама таблица повторы при этом не отсеивает (`MergeTree` ничего не дедуплицирует) — если видно, та же это запись или другая, — пригодится, когда дальше начнём сверять данные между
понадобится, дубли убирают уже на следующих слоях. слоями.
При этом **повторы STG не отсеивает**. Движок этих таблиц — `MergeTree`, и он не
дедуплицирует, то есть не убирает строки-дубли: что пришло, то и легло, даже если две записи
окажутся одинаковыми. Если дубли потом помешают — их убирают уже на следующих слоях, а STG
держит всё подряд.
> **В проде иначе.** На потоке в десятки тысяч сообщений в секунду читателей будет > **В проде иначе.** На потоке в десятки тысяч сообщений в секунду читателей будет
> несколько, и Kafka сама делит работу между ними. В этом уроке — один читатель > несколько, и Kafka сама делит работу между ними. В этом уроке — один читатель
@@ -73,6 +84,8 @@ LIMIT 5;
## 3. Загляни внутрь (`sql/ddl/stg/10_stg.sql`) ## 3. Загляни внутрь (`sql/ddl/stg/10_stg.sql`)
### Три кирпича слоя
Весь слой STG собран из **трёх кирпичей**, и каждый топик повторяет одну и ту же тройку: Весь слой STG собран из **трёх кирпичей**, и каждый топик повторяет одну и ту же тройку:
| Кирпич | Объект | Движок | Что делает | | Кирпич | Объект | Движок | Что делает |
@@ -81,13 +94,21 @@ LIMIT 5;
| 2 | `stg.kafka_browser_raw` | `ENGINE = Kafka` | **читает** топик, ничего не хранит | | 2 | `stg.kafka_browser_raw` | `ENGINE = Kafka` | **читает** топик, ничего не хранит |
| 3 | `stg.mv_kafka_browser_to_stg` | `MATERIALIZED VIEW` | **перекладывает** из (2) в (1) на лету | | 3 | `stg.mv_kafka_browser_to_stg` | `MATERIALIZED VIEW` | **перекладывает** из (2) в (1) на лету |
Главное: таблица с `ENGINE = Kafka` — это не хранилище, а «кран» к топику. Сама по себе Связка работает так. Таблица с `ENGINE = Kafka` (кирпич 2) — это не хранилище, а «кран» к
она данные не копит; данные забирает Materialized View и складывает их в обычную топику: через неё ClickHouse читает сообщения, но **сами данные она не копит**. Забирает их
`MergeTree`-таблицу. Сообщение появилось в топике → MV тут же положило его в `*_raw`. третий кирпич — **Materialized View** (MV).
Стоит задержаться на двух местах файла. И тут стоит остановиться на самом слове. Обычное представление (view) — это сохранённый
запрос: данные оно считает только тогда, когда его спросишь. «Materialized» (материализованное)
значит другое: оно срабатывает **само** на каждую новую порцию из источника и сразу
складывает результат в постоянную таблицу. Получается цепочка: сообщение появилось в топике →
MV тут же подхватило его и положило в `MergeTree`-таблицу `*_raw` (кирпич 1), где оно и лежит.
**Таблица-источник Kafka (`kafka_*_raw`)** — здесь живёт вся настройка чтения: Дальше задержимся на двух местах файла.
### Таблица-источник Kafka (`kafka_*_raw`)
Здесь живёт вся настройка чтения топика:
```sql ```sql
ENGINE = Kafka ENGINE = Kafka
@@ -100,12 +121,19 @@ SETTINGS
kafka_handle_error_mode = 'stream'; -- кривое сообщение не рвёт чтение топика kafka_handle_error_mode = 'stream'; -- кривое сообщение не рвёт чтение топика
``` ```
`JSONAsString` — почему мы и можем класть `raw` строкой: ClickHouse не пытается разобрать Две настройки тут — самые важные для всего урока:
JSON на этом этапе. `kafka_handle_error_mode = 'stream'` — ровно то «STG принимает всё»
из секции 1: битое сообщение не уронит консьюмера.
**Materialized View** — здесь сообщение превращается в строку таблицы. Метаданные берутся - `kafka_format = 'JSONAsString'` — вот почему мы и можем класть `raw` одной строкой:
из виртуальных колонок Kafka-движка (`_topic`, `_partition`, `_offset`, `_timestamp_ms`): ClickHouse берёт тело сообщения как текст и **не пытается разобрать** JSON на этом этапе;
- `kafka_handle_error_mode = 'stream'` — это ровно то «STG принимает всё» из секции 1: одно
битое сообщение не уронит консьюмера, чтение топика продолжится.
### Materialized View: как сообщение становится строкой
Здесь сообщение из топика превращается в строку таблицы. Откуда MV берёт метаданные доставки?
Из **виртуальных колонок** Kafka-движка — это служебные поля (`_topic`, `_partition`,
`_offset`, `_timestamp_ms`), которые движок подставляет к каждому сообщению сам, хотя в теле
JSON их нет:
```sql ```sql
SELECT SELECT
@@ -118,10 +146,10 @@ SELECT
FROM stg.kafka_browser_raw; FROM stg.kafka_browser_raw;
``` ```
Тут без фокусов: каждая виртуальная колонка ложится в свою. `_timestamp_ms` — это уже Тут без фокусов: каждая виртуальная колонка ложится в свою. Одно место стоит запомнить —
готовый `DateTime64(3)` (время сообщения с точностью до миллисекунд), поэтому идёт в `_timestamp_ms`: это уже готовый `DateTime64(3)` (время сообщения с точностью до миллисекунд),
`kafka_ts` как есть, без преобразований. В секции 4 ты положишь рядом ещё одно время из поэтому оно идёт в `kafka_ts` как есть, без всякого преобразования. В секции 4 ты положишь
Kafka и увидишь, чем они отличаются. рядом ещё одно время из Kafka и увидишь, чем они отличаются.
--- ---
+1 -1
View File
@@ -1,6 +1,6 @@
# Урок 2. STG → ODS: типизация и DQ-split # Урок 2. STG → ODS: типизация и DQ-split
> Статус: черновик. Режим: **руки**. > Формат: **практика** — будешь сам запускать команды и менять код, не только читать.
> Пререквизит: пройден урок 1 (слой STG — сырой JSON строкой уже лежит в `stg.*_raw`, > Пререквизит: пройден урок 1 (слой STG — сырой JSON строкой уже лежит в `stg.*_raw`,
> рядом метаданные доставки из Kafka). > рядом метаданные доставки из Kafka).
> Эталонный путь: [`sql/ods/20_stg_to_ods.sql`](../../../sql/ods/20_stg_to_ods.sql) > Эталонный путь: [`sql/ods/20_stg_to_ods.sql`](../../../sql/ods/20_stg_to_ods.sql)