diff --git a/docs/course/LEARNING_PLAN.md b/docs/course/LEARNING_PLAN.md index db33bd6..3b79f1a 100644 --- a/docs/course/LEARNING_PLAN.md +++ b/docs/course/LEARNING_PLAN.md @@ -1,6 +1,6 @@ # План обучения: курс «Кликстрим на ClickHouse» -> Статус: черновик. Дата: 2026-06-03 (аудит путей выполнен; середина расщеплена — +> Дата: 2026-06-03 (аудит путей выполнен; середина расщеплена — > один паттерн на урок, всего 7 уроков, см. §1–2). > Назначение: высокоуровневый маршрут менти по курсу — карта уроков, порядок, > результаты аудита эталонных путей. Рамка курса (зачем/что/скоуп) — в `PRD.md`; diff --git a/docs/course/LESSON_STANDARD.md b/docs/course/LESSON_STANDARD.md index a4d97a6..395b7af 100644 --- a/docs/course/LESSON_STANDARD.md +++ b/docs/course/LESSON_STANDARD.md @@ -1,6 +1,6 @@ # Стандарт уроков курса «Кликстрим на ClickHouse» -> Статус: черновик. Дата: 2026-06-01. +> Дата: 2026-06-01. > Назначение: рабочий чеклист, по которому пишется **каждый** урок. Открывается при > создании урока. Рамка курса (зачем/что/скоуп) — в `PRD.md`; карта уроков и > маршрут — в `LEARNING_PLAN.md`. diff --git a/docs/course/PRD.md b/docs/course/PRD.md index f661f19..518d2e2 100644 --- a/docs/course/PRD.md +++ b/docs/course/PRD.md @@ -1,6 +1,6 @@ # PRD: продвинутый курс «Кликстрим на ClickHouse» (со звёздочкой) -> Статус: черновик (прообраз PRD). Дата: 2026-06-01. +> Статус: прообраз PRD. Дата: 2026-06-01. > Поправка 2026-06-03 (разморозка по делу): середина пайплайна расщеплена — ODS и DDS > теперь разные уроки (принцип «один паттерн на урок»), витрины DM демотированы в > поверхность потребления. Обязательных уроков стало 0–5, опциональный Superset — урок 6. diff --git a/docs/course/lessons/00_kafka_intro.md b/docs/course/lessons/00_kafka_intro.md index aff8b1e..b24d436 100644 --- a/docs/course/lessons/00_kafka_intro.md +++ b/docs/course/lessons/00_kafka_intro.md @@ -1,6 +1,6 @@ # Урок 0. Вводный по Kafka (Kafka UI, наблюдение) -> Статус: черновик. Режим: **наблюдение** (ничего не меняем, только смотрим). +> Формат: **наблюдение** — ничего не запускаем и не меняем, только смотрим. > Пререквизит: обзорное видео по Kafka из роадмапа — оттуда ты уже знаешь слова > «топик», «партиция», «offset», «consumer-группа», «lag». Этот урок связывает их > с живым стендом, чтобы они перестали быть просто словами. @@ -8,6 +8,9 @@ > > Поток данных одной строкой: > `make data → топики Kafka (партиции, offset'ы) → consumer-группа ClickHouse вычитывает` +> +> О чём урок простыми словами: ходим по Kafka UI и разглядываем поток — где лежат события, +> кто их читает и как Kafka помнит, до какого места уже дочитано. --- diff --git a/docs/course/lessons/01_kafka_to_clickhouse.md b/docs/course/lessons/01_kafka_to_clickhouse.md index 742ce0e..2c12e1a 100644 --- a/docs/course/lessons/01_kafka_to_clickhouse.md +++ b/docs/course/lessons/01_kafka_to_clickhouse.md @@ -1,20 +1,26 @@ # Урок 1. Заземление Kafka → ClickHouse (слой STG) -> Статус: черновик. Режим: **руки**. +> Формат: **практика** — будешь сам запускать команды и менять код, не только читать. > Пререквизит: пройден урок 0 (словарь Kafka — топик, партиция, offset, consumer-группа — > уже знаком и виден в Kafka UI). > Эталонный путь: [`sql/ddl/stg/10_stg.sql`](../../../sql/ddl/stg/10_stg.sql). > > Поток данных одной строкой: > `Kafka → kafka_*_raw (ENGINE=Kafka) → MV → *_raw (MergeTree)` +> +> О чём урок простыми словами: смотрим, как сообщение из Kafka-топика само, без нашего +> участия, превращается в строку таблицы ClickHouse — и почему на этом первом слое мы кладём +> JSON целиком, ничего в нём не разбирая. --- ## 1. Зачем и где в проде -Первое, что делаем с потоком событий — складываем его в таблицу как есть, а рядом пишем -метаданные доставки: из какого топика и партиции пришло сообщение, с каким offset'ом и -временем. Это слой STG — тот самый staging, знакомый тебе по курсовой. +Первое, что делаем с потоком событий — складываем его в таблицу **как есть**, ничего в нём +не меняя. А рядом, в соседних колонках, пишем метаданные доставки: из какого топика и партиции +пришло сообщение, с каким offset'ом и в какое время. Это слой **STG** — тот самый staging, +знакомый тебе по курсовой: первая «посадочная площадка», куда поток приземляется в сыром виде, +до любой обработки. Зачем хранить сырой JSON строкой и не парсить его сразу: @@ -24,10 +30,15 @@ - и, главное, чтобы приём не падал из-за одного кривого поля. Разбор JSON и проверки качества — это уже следующий слой (урок 2), а STG принимает всё подряд. -`kafka_offset` здесь не просто метаданные: пара «партиция + offset» однозначно указывает -на конкретное сообщение в топике — по ней всегда понятно, та же это запись или другая. -Сама таблица повторы при этом не отсеивает (`MergeTree` ничего не дедуплицирует) — если -понадобится, дубли убирают уже на следующих слоях. +Отдельно про `kafka_offset` — это не просто справочная метка. Помнишь из урока 0: пара +«партиция + offset» однозначно указывает на конкретное сообщение в топике. По ней всегда +видно, та же это запись или другая, — пригодится, когда дальше начнём сверять данные между +слоями. + +При этом **повторы STG не отсеивает**. Движок этих таблиц — `MergeTree`, и он не +дедуплицирует, то есть не убирает строки-дубли: что пришло, то и легло, даже если две записи +окажутся одинаковыми. Если дубли потом помешают — их убирают уже на следующих слоях, а STG +держит всё подряд. > **В проде иначе.** На потоке в десятки тысяч сообщений в секунду читателей будет > несколько, и Kafka сама делит работу между ними. В этом уроке — один читатель @@ -73,6 +84,8 @@ LIMIT 5; ## 3. Загляни внутрь (`sql/ddl/stg/10_stg.sql`) +### Три кирпича слоя + Весь слой STG собран из **трёх кирпичей**, и каждый топик повторяет одну и ту же тройку: | Кирпич | Объект | Движок | Что делает | @@ -81,13 +94,21 @@ LIMIT 5; | 2 | `stg.kafka_browser_raw` | `ENGINE = Kafka` | **читает** топик, ничего не хранит | | 3 | `stg.mv_kafka_browser_to_stg` | `MATERIALIZED VIEW` | **перекладывает** из (2) в (1) на лету | -Главное: таблица с `ENGINE = Kafka` — это не хранилище, а «кран» к топику. Сама по себе -она данные не копит; данные забирает Materialized View и складывает их в обычную -`MergeTree`-таблицу. Сообщение появилось в топике → MV тут же положило его в `*_raw`. +Связка работает так. Таблица с `ENGINE = Kafka` (кирпич 2) — это не хранилище, а «кран» к +топику: через неё ClickHouse читает сообщения, но **сами данные она не копит**. Забирает их +третий кирпич — **Materialized View** (MV). -Стоит задержаться на двух местах файла. +И тут стоит остановиться на самом слове. Обычное представление (view) — это сохранённый +запрос: данные оно считает только тогда, когда его спросишь. «Materialized» (материализованное) +значит другое: оно срабатывает **само** на каждую новую порцию из источника и сразу +складывает результат в постоянную таблицу. Получается цепочка: сообщение появилось в топике → +MV тут же подхватило его и положило в `MergeTree`-таблицу `*_raw` (кирпич 1), где оно и лежит. -**Таблица-источник Kafka (`kafka_*_raw`)** — здесь живёт вся настройка чтения: +Дальше задержимся на двух местах файла. + +### Таблица-источник Kafka (`kafka_*_raw`) + +Здесь живёт вся настройка чтения топика: ```sql ENGINE = Kafka @@ -100,12 +121,19 @@ SETTINGS kafka_handle_error_mode = 'stream'; -- кривое сообщение не рвёт чтение топика ``` -`JSONAsString` — почему мы и можем класть `raw` строкой: ClickHouse не пытается разобрать -JSON на этом этапе. `kafka_handle_error_mode = 'stream'` — ровно то «STG принимает всё» -из секции 1: битое сообщение не уронит консьюмера. +Две настройки тут — самые важные для всего урока: -**Materialized View** — здесь сообщение превращается в строку таблицы. Метаданные берутся -из виртуальных колонок Kafka-движка (`_topic`, `_partition`, `_offset`, `_timestamp_ms`): +- `kafka_format = 'JSONAsString'` — вот почему мы и можем класть `raw` одной строкой: + ClickHouse берёт тело сообщения как текст и **не пытается разобрать** JSON на этом этапе; +- `kafka_handle_error_mode = 'stream'` — это ровно то «STG принимает всё» из секции 1: одно + битое сообщение не уронит консьюмера, чтение топика продолжится. + +### Materialized View: как сообщение становится строкой + +Здесь сообщение из топика превращается в строку таблицы. Откуда MV берёт метаданные доставки? +Из **виртуальных колонок** Kafka-движка — это служебные поля (`_topic`, `_partition`, +`_offset`, `_timestamp_ms`), которые движок подставляет к каждому сообщению сам, хотя в теле +JSON их нет: ```sql SELECT @@ -118,10 +146,10 @@ SELECT FROM stg.kafka_browser_raw; ``` -Тут без фокусов: каждая виртуальная колонка ложится в свою. `_timestamp_ms` — это уже -готовый `DateTime64(3)` (время сообщения с точностью до миллисекунд), поэтому идёт в -`kafka_ts` как есть, без преобразований. В секции 4 ты положишь рядом ещё одно время из -Kafka и увидишь, чем они отличаются. +Тут без фокусов: каждая виртуальная колонка ложится в свою. Одно место стоит запомнить — +`_timestamp_ms`: это уже готовый `DateTime64(3)` (время сообщения с точностью до миллисекунд), +поэтому оно идёт в `kafka_ts` как есть, без всякого преобразования. В секции 4 ты положишь +рядом ещё одно время из Kafka и увидишь, чем они отличаются. --- diff --git a/docs/course/lessons/02_stg_to_ods.md b/docs/course/lessons/02_stg_to_ods.md index 5c0f39a..88a7d92 100644 --- a/docs/course/lessons/02_stg_to_ods.md +++ b/docs/course/lessons/02_stg_to_ods.md @@ -1,6 +1,6 @@ # Урок 2. STG → ODS: типизация и DQ-split -> Статус: черновик. Режим: **руки**. +> Формат: **практика** — будешь сам запускать команды и менять код, не только читать. > Пререквизит: пройден урок 1 (слой STG — сырой JSON строкой уже лежит в `stg.*_raw`, > рядом метаданные доставки из Kafka). > Эталонный путь: [`sql/ods/20_stg_to_ods.sql`](../../../sql/ods/20_stg_to_ods.sql)