chore(main): объединены локальные и удалённые изменения

- Зачем:
  - локальные уточнения документации нужно опубликовать без потери слитых изменений #96 и #109.
- Что:
  - актуальный `origin/main` объединён с двумя локальными коммитами документации.
  - сохранены уточнения применения настроек ClickHouse и границ проектирования заказов.
- Проверка:
  - `make config-test`.
  - `make lint`.
This commit is contained in:
ddadmin committed 2026-08-21 11:09:49 +03:00
commit 67313bf997
18 files changed
+198 -70

No files matched your search

+1 -1
View File
@@ -37,7 +37,7 @@
`person_id` заказ показывает как `user_id`, кликстрим остаётся анонимным —
[мост к склейке](identity.md).
- **Приём — пакетный забор**: одно прямое чтение Kafka в STG, два
`INSERT SELECT` в типизированный ODS и таблицу ошибок; `ods.order_snapshot`
`INSERT SELECT` в типизированный ODS и таблицу ошибок; `ods.order`
принимает версии заказа на `ReplacingMergeTree(updated_at)` —
[приём из Kafka в ODS](ingestion.md).
- **Стартовый мир отправляет семь слепков** (дни 0…6): у последнего прожитого
+1 -1
View File
@@ -61,7 +61,7 @@
**Что из двух дорог видно.** В `dds.order` дороги неразличимы — там последняя
версия; различает их история версий: сырьё STG и физические версии
`ods.order_snapshot` до фоновых слияний, а в витринах — выручка дня, которая
`ods.order` до фоновых слияний, а в витринах — выручка дня, которая
сначала выросла, потом убыла. Полное различение не обещано: слепок — состояние
на границе суток, и оплата с отменой в один день в сырье неразличимы; то же у
сильно опоздавших, приехавших уже терминальными. Точную форму этого урока
+9 -9
View File
@@ -43,12 +43,12 @@
Один следующий `task_id` отвечает за весь переход STG → ODS. Внутри него два
последовательных `INSERT SELECT` читают неизменный срез по `_load_id`: первый
пишет годные строки в `ods.order_snapshot`, второй — брак в
`ods.order_snapshot_errors`. Транзакции между запросами нет. При частичном сбое
Airflow повторяет весь `task_id`; одинаковые исходные строки и служебные метки
не вычисляются заново. Запросы лежат рядом с целями:
[`order_snapshot_load.sql`](../../../sql/ods/order_snapshot_load.sql) и
[`order_snapshot_errors_load.sql`](../../../sql/ods/order_snapshot_errors_load.sql).
пишет годные строки в `ods.order`, второй — брак в `ods.order_errors`.
Транзакции между запросами нет. При частичном сбое Airflow повторяет весь
`task_id`; одинаковые исходные строки и служебные метки не вычисляются заново.
Запросы лежат рядом с целями:
[`order_load.sql`](../../../sql/ods/order_load.sql) и
[`order_errors_load.sql`](../../../sql/ods/order_errors_load.sql).
Условия запросов взаимодополняющие: один общий предикат определяет брак, а
годная ветвь использует его буквальное отрицание. Все функции предиката
@@ -90,9 +90,9 @@ JSON-объектом с точным набором ключей: `order_id`, `
## Роль ODS
`ods.order_snapshot_rep` хранит физически принятые версии в
`ods.order_rep` хранит физически принятые версии в
`ReplacingMergeTree(updated_at)`. Ключ сортировки — `order_id`, партиция — день
неизменного `created_at`. `ods.order_snapshot_dist` шардирует по
неизменного `created_at`. `ods.order_dist` шардирует по
`cityHash64(order_id)`: только так все версии заказа попадают на один шард и
`FINAL` даёт корректный результат через распределённую таблицу.
@@ -174,7 +174,7 @@ JSON-объектом с точным набором ключей: `order_id`, `
После исправления
[#102](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/102)
честный прогон дня снят на живом стенде 20 августа 2026 года: все 1694 строки
слепка дня 7 приняты, `ods.order_snapshot_errors` осталась пустой. У заказа
слепка дня 7 приняты, `ods.order_errors` осталась пустой. У заказа
`20260608-0007`, который прежде приходил с отрицательным итогом, теперь
`5490.00 − 823.50 + 299.00 = 4965.50`.
+1 -1
View File
@@ -25,6 +25,6 @@
пользователей и число двухкуковых пар растут, пока мир едет, — опоры из них
не выходит; генератор сторожит хеш, транспорт — счёт событий.
- **Опись описывает мир, а не доставку.** Работа генератора кончается на
Kafka: дошли ли байты до `ods.order_snapshot` — вопрос стенда и его
Kafka: дошли ли байты до `ods.order` — вопрос стенда и его
проверок. Поэтому счёта строк у слепка в описи нет; понадобится проверка
приёма заказов — число заведётся вместе с ней.
+17 -18
View File
@@ -60,7 +60,7 @@ CSV-файла. Живёт он вне цепочки слоёв, в зоне `d
приходит от транспорта: слой хранит то, что доехало по топику, и зовётся именем
топика — `hits_raw` от `hits`, `orders_raw` от `orders`. В типизированных слоях
имя приходит от предметной области и стоит в единственном числе: `event`,
`order_snapshot`, `session`. Граница между «как привезли» и «что это такое»
`order`, `session`. Граница между «как привезли» и «что это такое»
проходит по STG, и имена её показывают.
## Раскладка по шардам
@@ -91,7 +91,7 @@ CSV-файла. Живёт он вне цепочки слоёв, в зоне `d
по `cityHash64(order_id)`; ключи остальных частей будущей модели и витрин
появятся вместе с ними.
Замена партиций не используется для `ods.order_snapshot`: версии прошлых дней
Замена партиций не используется для `ods.order`: версии прошлых дней
доливаются, а прямое чтение Kafka не задаёт границы полного слепка ([ADR
0010](../adr/0010-order-versions-in-ods.md)). Если партиционная пересборка
понадобится будущим объектам DDS или DM, партиция-донор должна быть заранее
@@ -141,7 +141,7 @@ kafka_offset)`: разбор полётов идёт от «какое сооб
заказов — пакетным шагом. Дальше метка переносится в ODS как есть и отвечает на
вопрос «когда строка приехала в хранилище», а не «когда её разобрали». В
`ods.event` она же служит колонкой версии `ReplacingMergeTree` и схлопывает
повтор доставки. У `ods.order_snapshot` версию задаёт `updated_at` источника;
повтор доставки. У `ods.order` версию задаёт `updated_at` источника;
`_load_ts` только показывает, когда конкретная строка приехала.
`created_at` и `updated_at` заказа к служебным колонкам хранилища не относятся.
@@ -181,9 +181,9 @@ snake_case-имена и расшифровку кодов.
## Версии заказов
`ods.order_snapshot_rep` хранит принятые версии в
`ods.order_rep` хранит принятые версии в
`ReplacingMergeTree(updated_at)`: ключ сортировки — `order_id`, партиция —
`toDate(created_at)`. `ods.order_snapshot_dist` шардирует по
`toDate(created_at)`. `ods.order_dist` шардирует по
`cityHash64(order_id)`. Все версии заказа лежат в одной партиции, чтобы их могли
схлопывать фоновые слияния, и на одном шарде, чтобы распределённый `FINAL`
выбрал одного победителя.
@@ -414,7 +414,7 @@ D0 и к реальному календарю не привязана; паке
kafka_offset)`: смотрят такую таблицу от класса, а внутри класса — по координатам
доставки.
`ods.order_snapshot_errors` держит тот же диагностический минимум и `_load_id`
`ods.order_errors` держит тот же диагностический минимум и `_load_id`
запуска. У заказов три класса по приоритету: `not_an_object`,
`keyset_mismatch`, `field_invalid`. Сырой текст остаётся рядом, поэтому класс не
разрастается до имени отдельного поля. Точная граница приёма — в
@@ -534,15 +534,15 @@ Airflow читает и собирает эти файлы штатным шаб
| `ods` | `ods.event_v` | актуальная версия события с полями источника |
| `ods` | `ods.event_errors_rep` / `_dist` | строки, не прошедшие строгий приём |
| `ods` | `ods.event_mv`, `ods.event_errors_mv` | разбор сырья в событие и в ошибки |
| `ods` | `ods.order_snapshot_rep` / `_dist` | типизированные версии заказа |
| `ods` | `ods.order_rep` / `_dist` | типизированные версии заказа |
| `ods` | `ods.order_v` | текущая версия заказа на языке источника |
| `ods` | `ods.order_snapshot_errors_rep` / `_dist` | строки слепка, не прошедшие строгий приём |
| `ods` | `ods.order_errors_rep` / `_dist` | строки слепка, не прошедшие строгий приём |
| `dic` | `dic.products_file` | чтец CSV-каталога, общего с генератором |
| `dic` | `dic.products` | словарь товаров поверх подложки |
Матвью разбора у заказов нет: срез сырья раскладывают по этим двум целям два
`INSERT SELECT` шага `parse_batch` из файлов `sql/ods/order_snapshot_load.sql`
и `sql/ods/order_snapshot_errors_load.sql`.
`INSERT SELECT` шага `parse_batch` из файлов `sql/ods/order_load.sql` и
`sql/ods/order_errors_load.sql`.
Слой DDS и слой DM появляются на следующих этапах; их состав задан разделом
7 мастер-спеки и переносится сюда по мере постройки.
@@ -606,14 +606,13 @@ DDL-словарь с файловым источником внутри `user_f
`_load_id`, пересечение ветвей — ноль. Все восемь строк брака — версии одного
заказа с отрицательным `total`; это дефект генератора, разобранный в
[спецификации приёма](orders/ingestion.md), «Что проверено». На управляемой
порции из пяти строк (две версии
одного заказа плюс по одной строке каждого класса брака) годные ушли в
`ods.order_snapshot`, брак — в `ods.order_snapshot_errors` с ожидаемыми
классами. Обе версии легли в одну партицию и на один шард; `FINAL` через
распределённую таблицу вернул одну строку — ту, у которой `updated_at` позже, —
тогда как физический счёт показывал две. Повтор задачи с тем же `_load_id`
строку в `ods.order_v` и её `_load_ts` не изменил, а таблица ошибок записала
тот же брак второй раз, как и обещано спецификацией приёма.
порции из пяти строк (две версии одного заказа плюс по одной строке каждого
класса брака) годные ушли в `ods.order`, брак — в `ods.order_errors` с
ожидаемыми классами. Обе версии легли в одну партицию и на один шард; `FINAL`
через распределённую таблицу вернул одну строку — ту, у которой `updated_at`
позже, — тогда как физический счёт показывал две. Повтор задачи с тем же
`_load_id` строку в `ods.order_v` и её `_load_ts` не изменил, а таблица ошибок
записала тот же брак второй раз, как и обещано спецификацией приёма.
Отдельно измерено, что **сводка вставки не годится в счётчики строк**: у
запроса с `WHERE` `read_rows` считает прочитанное с диска, а не подошедшее, —
+18 -9
View File
@@ -71,9 +71,9 @@
тогда окружение окупится, и условие возврата — именно это, а не «стало
неудобно».
Цена самого `make up` — 2 м 50 с с нуля (`make clean` перед ним — ещё 23 с) и
1 м 5 с на живом стенде. В цену целей она не входит, но записана здесь по той
же причине: с #42 подъём стенда заливает стартовый мир и стал заметно дороже.
Цена самого `make up` — 8 м 18 с с нуля и 3 м 35 с на живом стенде. В цену
целей она не входит, но записана здесь по той же причине: подъём пересчитывает
оба источника стартового мира и ждёт их приёма.
## Правило: смоук обязан оставаться быстрым
@@ -194,12 +194,12 @@ Kafka → STG → ODS, и живёт он в `make check-clickhouse`.
| `make check-services` | `scripts/stand-services.sh` |
| `make check-clickhouse` | `scripts/check-clickhouse.sh` |
Особняком — `scripts/wait-for-world.sh`: он не проверка, а вторая половина
`make up`. `docker compose up --wait` дожидается служб, в том числе успешно
отработавшей заливки, но «заливка кончилась» значит «события в топике», а не
«события в хранилище»: приём асинхронный. Скрипт ждёт доезда ограниченным
циклом опроса. Без него `make check-clickhouse` следом краснел бы по
устройству, а не по поломке.
Особняком — два сценария продолжения `make up`, оба не проверки.
`scripts/wait-for-world.sh` ждёт, пока события доедут из Kafka в ODS:
`docker compose up --wait` видит конец отправки, но приём идёт асинхронно.
Затем `scripts/ingest-starting-orders.sh` разово запускает единственный даг
приёма заказов `orders_ingest` и ждёт его конца. Оба ждут ограниченным циклом,
а не паузой наугад.
Общее у смоука и `check-services` — счёт проверок, обращение к Compose и две
проверки — вынесено в `scripts/stand-common.sh`; сам он не запускается.
@@ -221,6 +221,15 @@ Kafka → STG → ODS, и живёт он в `make check-clickhouse`.
## Что проверено
**Перезамер 20 августа 2026 года при исполнении #96.** `make up` после
`make clean` прошёл за 8 м 18 с, повтор на живом стенде — за 3 м 35 с. Служба
семи слепков заняла около 28 секунд между концом `world-init` и своим концом.
Прежний замер ниже — 2 м 50 с и 1 м 5 с до появления приёма заказов и пульта
мира. Весь рост приписывать слепкам нельзя: на холодном прогоне основное время
ушло ещё до сценариев ожидания: `airflow-init` завершился примерно на 320-й
секунде, обработчик DAG стал здоров примерно на 458-й. Числа здесь называют
цену нынешнего стенда, а не выносят вердикт о регрессии.
**Перезамер 19 августа 2026 года при исполнении #95.** После добавления десятой
проверки словаря `make check-clickhouse` на чисто поднятом стенде прошёл за
24 секунды. Это цена всей цели, а не одной новой строки.