fix(clickhouse): починена проверка словаря, выметены хвосты переноса
Зачем: ревью в три линии нашло настоящий дефект. Девятая проверка кластера
звала dictGet('dds.products', …) — объекта после переноса не существует, и
make check-clickhouse падал. Промах вырос из недосчёта: упоминания dds.products
я искал с фильтром по расширениям, .sh туда не попал, и в ADR уехало «четыре
упоминания» вместо семи.
Что: проверка переведена на dic.products и сверяет цену, умножив её обратно на
сто, — так утверждается ещё и точность приведения к Decimal. Раздел 3
мастер-спеки и список пользователей README догнали перенос: оба описывали
отменённое устройство. В ADR 0012 добавлено условие пересмотра для дома — для
него его не было, хотя ради дома тикет и заводился; недосчёт записан в
«Что проверено» как урок. Разнобой обозначений сведён: карта таблиц в доке
хранилища перешла на имена баз строчными, таблица мастер-спеки — на заголовок
«Где», зону всюду зовут зоной, а не слоем. Термин «Справочник» переписан без
метафоры и уложен в формат глоссария. Вычтено лишнее: комментарии DDL
сократились вдвое, из ADR 0006 и 0007 убраны самооправдание и дублирующие
абзацы, отраслевой образец получил честную оговорку о непроверяемости.
Проверка: make config-test, make lint, make smoke и make check-clickhouse —
все зелёные, десять проверок кластера из десяти.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+6
-7
@@ -95,15 +95,14 @@ _Избегать_: «покупатель» про того, кто купил
|
|||||||
товара, покупка — на страницу подтверждения заказа.
|
товара, покупка — на страницу подтверждения заказа.
|
||||||
|
|
||||||
**Справочник**:
|
**Справочник**:
|
||||||
Данные, входящие в хранилище сбоку, а не по цепочке слоёв: в хранилище их
|
Данные, которые хранилище не производит, а получает готовыми со стороны:
|
||||||
никто не производит, а читают их несколько слоёв. Дом — база `dic`, общая для
|
содержимым владеет источник вне хранилища, слои только читают. Живут в базе
|
||||||
всех справочников. Первый и пока единственный — каталог товаров.
|
`dic`. Первый и пока единственный — каталог товаров.
|
||||||
_Не путать_: слово «словарь» в проекте значит ещё две вещи — этот глоссарий
|
_Избегать_: словарь — этим словом в проекте зовут и глоссарий понятий, и
|
||||||
понятий и объект `DICTIONARY` ClickHouse, одну из форм, в которой справочник
|
объект `DICTIONARY` ClickHouse, одну из форм, в которой справочник доступен.
|
||||||
доступен.
|
|
||||||
|
|
||||||
**Каталог товаров**:
|
**Каталог товаров**:
|
||||||
`data/catalog/products.csv` — общий справочник генератора и словаря
|
`data/catalog/products.csv` — общий файл генератора и словаря
|
||||||
ClickHouse. Форма файла решена, длина — нет: строки дописываются.
|
ClickHouse. Форма файла решена, длина — нет: строки дописываются.
|
||||||
|
|
||||||
**Уровень спроса**:
|
**Уровень спроса**:
|
||||||
|
|||||||
@@ -278,12 +278,16 @@ uv run --project generator python -m clickstream_generator batch \
|
|||||||
- Prometheus — `http://127.0.0.1:29090`;
|
- Prometheus — `http://127.0.0.1:29090`;
|
||||||
- Grafana — `http://127.0.0.1:23000`, пользователь `admin`, пароль `admin`.
|
- Grafana — `http://127.0.0.1:23000`, пользователь `admin`, пароль `admin`.
|
||||||
|
|
||||||
В ClickHouse четыре пользователя:
|
В ClickHouse пять пользователей:
|
||||||
|
|
||||||
- `etl` применяет DDL и подключает Airflow; роль `etl_writer` читает и пишет
|
- `etl` применяет DDL и подключает Airflow; роль `etl_writer` читает и пишет
|
||||||
слои хранилища;
|
слои хранилища;
|
||||||
- `bi` подключает Superset; роль `bi_reader` читает будущие слои DDS и DM;
|
- `bi` подключает Superset; роль `bi_reader` читает справочники `dic` и будущие
|
||||||
- `analyst` предназначен для подключения человека и читает все слои;
|
слои DDS и DM;
|
||||||
|
- `analyst` предназначен для подключения человека и читает все слои и
|
||||||
|
справочники;
|
||||||
|
- `dict` читает только базу `dic` и пароля не имеет: им словарь товаров ходит
|
||||||
|
за своей подложкой;
|
||||||
- `default` остаётся служебным: им ходят проверки здоровья и скрипты внутри
|
- `default` остаётся служебным: им ходят проверки здоровья и скрипты внутри
|
||||||
контейнеров, но не приложения.
|
контейнеров, но не приложения.
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,8 @@
|
|||||||
топика, `_file` — чтец файла, `_mv` — материализованное представление, `_v` —
|
топика, `_file` — чтец файла, `_mv` — материализованное представление, `_v` —
|
||||||
обычное представление.
|
обычное представление.
|
||||||
|
|
||||||
Суффикс `_file` добавлен 20 августа 2026 года вместе с подложкой под словарём
|
Суффикс `_file` пришёл с подложкой под словарём товаров
|
||||||
товаров ([ADR 0012](0012-dictionary-home.md)). Он ровно параллелен `_kafka`:
|
([ADR 0012](0012-dictionary-home.md)).
|
||||||
чтец внешнего источника, по копии на каждой ноде, без репликации и без
|
|
||||||
распределённой пары.
|
|
||||||
|
|
||||||
Суффикс носит каждый физический объект, поэтому голого имени у таблицы не
|
Суффикс носит каждый физический объект, поэтому голого имени у таблицы не
|
||||||
существует: запрос к `stg.hits_raw` даёт ошибку «нет такой таблицы».
|
существует: запрос к `stg.hits_raw` даёт ошибку «нет такой таблицы».
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
|
|
||||||
Роль здесь шире прав на слои: образ ClickHouse требует отдельного разрешения
|
Роль здесь шире прав на слои: образ ClickHouse требует отдельного разрешения
|
||||||
на запросы `ON CLUSTER`, на чтение системных таблиц и на каждый движок
|
на запросы `ON CLUSTER`, на чтение системных таблиц и на каждый движок
|
||||||
внешнего источника, а без второго клиент не открывает соединение вовсе. Состав прав каждой роли — работа тикета
|
внешнего источника. Без права на системные таблицы клиент вовсе не открывает
|
||||||
|
соединение. Состав прав каждой роли — работа тикета
|
||||||
реализации.
|
реализации.
|
||||||
|
|
||||||
Пользователи, роли и права объявлены файлами настройки сервера, а не
|
Пользователи, роли и права объявлены файлами настройки сервера, а не
|
||||||
@@ -54,12 +55,9 @@ DDS под ними, потому что так чаще всего и быва
|
|||||||
видны всем читателям и никому на запись: зона входит в хранилище сбоку, и её
|
видны всем читателям и никому на запись: зона входит в хранилище сбоку, и её
|
||||||
содержимым владеет файл репозитория, а не слой.
|
содержимым владеет файл репозитория, а не слой.
|
||||||
|
|
||||||
**Беспарольный `dict` — намеренное исключение.** Пароля у него нет не по
|
**Беспарольный `dict`.** Пароль пришлось бы вписать в текст DDL словаря, то
|
||||||
недосмотру: ходить этой учёткой некуда, кроме локального чтения одной базы, а
|
есть положить секрет в git. Ходить этой учёткой некуда: право у неё одно —
|
||||||
пароль пришлось бы вписать в текст DDL словаря — то есть положить секрет в git.
|
`SELECT` на `dic`, и хост источника локальный.
|
||||||
Право у него одно, `SELECT` на `dic`. Отдельная учётка под словарь взята из
|
|
||||||
промышленной практики, где источник словаря читают минимальной служебной
|
|
||||||
учётной записью, а не общим админом.
|
|
||||||
|
|
||||||
**Секрет, а не учётные данные по репликам.** У каждой реплики в описании
|
**Секрет, а не учётные данные по репликам.** У каждой реплики в описании
|
||||||
кластера своя учётка — вписанная явно или подразумеваемая, и тогда это
|
кластера своя учётка — вписанная явно или подразумеваемая, и тогда это
|
||||||
|
|||||||
@@ -33,18 +33,19 @@
|
|||||||
владение, которого у него нет: DDS этот объект не производит, не обновляет и не
|
владение, которого у него нет: DDS этот объект не производит, не обновляет и не
|
||||||
проверяет.
|
проверяет.
|
||||||
|
|
||||||
**На направление данных ссылаться нельзя, и это важно не спутать.** Чтение из
|
**Промах был во владении, а не в направлении.** Чтение из DM объекта, лежащего
|
||||||
DM объекта, лежащего в DDS, — течение вниз, ровно то, которое разрешено.
|
в DDS, — течение вниз, ровно то, которое разрешено; на инверсию слоёв это
|
||||||
Промах был во владении, а не в направлении. Но зона вне цепочки закрывает и
|
решение не опирается. Зато зона закрывает будущий случай: как только справочник
|
||||||
будущий случай: как только справочник понадобится выше по течению — например,
|
понадобится выше по течению — скажем, проверка `sku` при разборе заказа в ODS —
|
||||||
проверка `sku` позиции при разборе заказа в ODS, — чтение `dds.*` из `ods.*`
|
чтение `dds.*` из `ods.*` стало бы настоящей инверсией. У зоны вне цепочки
|
||||||
стало бы настоящей инверсией. У зоны вне цепочки направления относительно слоёв
|
направления относительно слоёв нет, и такой запрос законен по построению.
|
||||||
нет, и такой запрос законен по построению.
|
|
||||||
|
|
||||||
**Отраслевой образец говорит то же самое другим средством.** В хранилищах, где
|
**Отраслевой образец говорит то же самое другим средством.** Владелец принёс
|
||||||
база одна, а слои выражены префиксами имён, справочники несут собственный
|
устройство промышленного хранилища телекома — источник закрытый, проверить по
|
||||||
префикс наравне с префиксами стадий — то есть образуют свой слой, а не
|
ссылке нельзя, поэтому вес у довода такой же, как у полевого опыта, а не как у
|
||||||
приписаны к слою модели. Причём префикс накрывает всю цепочку справочника: и
|
документации. Там база одна, слои выражены префиксами имён, и справочники несут
|
||||||
|
собственный префикс наравне с префиксами стадий — то есть образуют свою зону, а
|
||||||
|
не приписаны к слою модели. Причём префикс накрывает всю цепочку справочника: и
|
||||||
таблицу, и представление над ней, и сам объект словаря. Средство разное,
|
таблицу, и представление над ней, и сам объект словаря. Средство разное,
|
||||||
утверждение одно. У нас слои выражены базами — значит справочникам полагается
|
утверждение одно. У нас слои выражены базами — значит справочникам полагается
|
||||||
база.
|
база.
|
||||||
@@ -82,8 +83,8 @@ DM объекта, лежащего в DDS, — течение вниз, ров
|
|||||||
**Историзацию и представление «последняя загрузка» из образца не берём.** В
|
**Историзацию и представление «последняя загрузка» из образца не берём.** В
|
||||||
промышленном примере таблица копит загрузки с меткой времени, а представление
|
промышленном примере таблица копит загрузки с меткой времени, а представление
|
||||||
берёт последнюю: источник приезжал извне, и версий у него не было нигде. У нас
|
берёт последнюю: источник приезжал извне, и версий у него не было нигде. У нас
|
||||||
версии есть — **история каталога это и есть git**. Дублировать её в ClickHouse значит
|
версии есть — **история каталога это и есть git**. Дублировать её в ClickHouse
|
||||||
поставить конструкцию, у которой на стенде нет причины, а расшифровывать её
|
значит поставить конструкцию, у которой на стенде нет причины, а расшифровывать её
|
||||||
менти всё равно придётся.
|
менти всё равно придётся.
|
||||||
|
|
||||||
**Материализованной таблицы с шагом наполнения тоже не берём.** Шаг наполнения
|
**Материализованной таблицы с шагом наполнения тоже не берём.** Шаг наполнения
|
||||||
@@ -121,6 +122,12 @@ DM объекта, лежащего в DDS, — течение вниз, ров
|
|||||||
|
|
||||||
## Условия пересмотра
|
## Условия пересмотра
|
||||||
|
|
||||||
|
- **Дом.** Появляется справочные данные, которые хранилище производит само —
|
||||||
|
скажем, таблица соответствий, собранная из DDS. Посылка решения «в хранилище
|
||||||
|
их никто не производит» на них не распространяется, и зону придётся либо
|
||||||
|
сузить до пришедших извне, либо переопределить. Дешевле всего это до этапа 4:
|
||||||
|
как только витрины начнут звать словарь по имени, каждый разворот станет
|
||||||
|
правкой по всем ссылкам.
|
||||||
- Справочник перестаёт быть файлом репозитория и начинает приезжать извне —
|
- Справочник перестаёт быть файлом репозитория и начинает приезжать извне —
|
||||||
возвращается вопрос устройства: историзованная таблица, «последняя загрузка»,
|
возвращается вопрос устройства: историзованная таблица, «последняя загрузка»,
|
||||||
словарь поверх. Адрес это переживает.
|
словарь поверх. Адрес это переживает.
|
||||||
@@ -142,8 +149,8 @@ DM объекта, лежащего в DDS, — течение вниз, ров
|
|||||||
почти на все полторы минуты: одна перезагрузилась сразу после правки, вторая
|
почти на все полторы минуты: одна перезагрузилась сразу после правки, вторая
|
||||||
ещё нет. Прежнее решение `LIFETIME(0)` окно не закрывало, а сжимало до
|
ещё нет. Прежнее решение `LIFETIME(0)` окно не закрывало, а сжимало до
|
||||||
разброса исполнения `SYSTEM RELOAD DICTIONARY ON CLUSTER`; теперь оно
|
разброса исполнения `SYSTEM RELOAD DICTIONARY ON CLUSTER`; теперь оно
|
||||||
раскрыто намеренно. Урок записан опорной точкой в мастер-спеке, тащить его в курс —
|
раскрыто намеренно. Урок записан опорной точкой в мастер-спеке, тащить его
|
||||||
отдельное решение.
|
в курс — отдельное решение.
|
||||||
- **Номенклатура товаров — медленно изменяющийся справочник**, и в предметной
|
- **Номенклатура товаров — медленно изменяющийся справочник**, и в предметной
|
||||||
области это так. Стенд упрощает: файл в репозитории под версией git.
|
области это так. Стенд упрощает: файл в репозитории под версией git.
|
||||||
Историзации в хранилище нет, и обещать её этот документ не должен.
|
Историзации в хранилище нет, и обещать её этот документ не должен.
|
||||||
@@ -178,15 +185,25 @@ DM объекта, лежащего в DDS, — течение вниз, ров
|
|||||||
- `SHOW DICTIONARIES` без указания базы возвращает пусто; словарь в базе стоит
|
- `SHOW DICTIONARIES` без указания базы возвращает пусто; словарь в базе стоит
|
||||||
среди таблиц слоя и отличается от них только колонкой движка в
|
среди таблиц слоя и отличается от них только колонкой движка в
|
||||||
`system.tables`.
|
`system.tables`.
|
||||||
- До переноса `dds.products` упоминался в репозитории четырежды, в дагах — ноль.
|
- До переноса `dds.products` стоял в семи строках трёх файлов: дока хранилища,
|
||||||
|
DDL словаря и проверка `scripts/check-clickhouse.sh`. Первый замер насчитал
|
||||||
|
четыре и проверку не увидел — искали с фильтром по расширениям, куда `.sh`
|
||||||
|
не попал. Из недосчёта вырос настоящий дефект: девятая проверка кластера
|
||||||
|
осталась звать несуществующий объект и упала. Поймало ревью, не автор.
|
||||||
|
Урок тот же, что в [ADR 0006](0006-object-naming.md): «прогнал по
|
||||||
|
репозиторию» — такое же утверждение, как утверждение о поведении системы, и
|
||||||
|
проверять его надо так же.
|
||||||
- Движок `File` требует у `etl` права на источник. Отказ называет
|
- Движок `File` требует у `etl` права на источник. Отказ называет
|
||||||
`TABLE ENGINE ON File`, но права с таким именем не хватает: замер тремя
|
`TABLE ENGINE ON File`, но права с таким именем не хватает: замер тремя
|
||||||
пользователями показал, что работает `GRANT FILE ON *.*`, а `TABLE ENGINE ON
|
пользователями показал, что работает `GRANT FILE ON *.*`, а `TABLE ENGINE ON
|
||||||
File` не нужен вовсе.
|
File` не нужен вовсе.
|
||||||
- Стенд, собранный с нуля этим решением, поднимается зелёным: оба объекта зоны
|
- Стенд, собранный с нуля этим решением, поднимается зелёным: оба объекта зоны
|
||||||
на месте, словарь `LOADED` со 180 строками, база `dds` пуста, `bi` читает
|
на месте, словарь `LOADED` со 180 строками, база `dds` пуста, `bi` читает
|
||||||
словарь со второй ноды, `analyst` — подложку соединением. `make smoke` —
|
словарь со второй ноды, `analyst` — подложку соединением. Словарь спрашивает
|
||||||
20 проверок, 0 ошибок.
|
`make check-clickhouse` — 10 проверок из 10; девятая берёт известный `sku` на
|
||||||
|
обеих нодах и сверяет цену с колонкой файла, умножив её обратно на сто, так
|
||||||
|
что приведение к `Decimal` проверено вместе с ответом. `make smoke` словарь
|
||||||
|
не трогает вовсе.
|
||||||
|
|
||||||
Сверено по документации ClickHouse через MCP Context7 20 августа 2026 года.
|
Сверено по документации ClickHouse через MCP Context7 20 августа 2026 года.
|
||||||
|
|
||||||
|
|||||||
@@ -431,15 +431,15 @@ kafka_offset)`: смотрят такую таблицу от класса, а
|
|||||||
|
|
||||||
Первый и пока единственный справочник — каталог товаров: подложка
|
Первый и пока единственный справочник — каталог товаров: подложка
|
||||||
`dic.products_file` над CSV репозитория и словарь `dic.products` поверх неё.
|
`dic.products_file` над CSV репозитория и словарь `dic.products` поверх неё.
|
||||||
Реплик у зоны нет: обе ноды читают свой смонтированный файл и держат свою копию
|
Распределённых пар у зоны нет: обе ноды читают свой смонтированный файл и
|
||||||
словаря в памяти. Словарь обновляется сам, окном `LIFETIME`, и потому после
|
держат свою копию словаря в памяти. Словарь обновляется сам, окном `LIFETIME`,
|
||||||
правки каталога ноды какое-то время отвечают по-разному. Разогнать его раньше
|
и потому после правки каталога ноды какое-то время отвечают по-разному.
|
||||||
срока — `SYSTEM RELOAD DICTIONARY ON CLUSTER`; это административная операция,
|
Разогнать его раньше срока — `SYSTEM RELOAD DICTIONARY ON CLUSTER`; это
|
||||||
роль `etl` права на неё не получает.
|
административная операция, роль `etl` права на неё не получает.
|
||||||
|
|
||||||
Деньги каталога стоит держать в голове отдельно от остальных: в CSV лежат целые
|
Деньги каталога живут в трёх единицах: в CSV — целые копейки, словарь отдаёт
|
||||||
копейки, словарь отдаёт `Decimal(18, 2)`, а `productPrice` события — уже целые
|
`Decimal(18, 2)`, `productPrice` события — целые рубли. Разрыв намеренный, на
|
||||||
рубли. Разрыв намеренный, на нём стоит урок про `Float64` ([описание
|
нём стоит урок про `Float64` ([описание
|
||||||
выгрузки](../formats/clickstream-event.md)).
|
выгрузки](../formats/clickstream-event.md)).
|
||||||
|
|
||||||
Устройство обоих объектов — почему такой движок, такая форма пути, такой
|
Устройство обоих объектов — почему такой движок, такая форма пути, такой
|
||||||
@@ -525,18 +525,18 @@ Airflow читает и собирает эти файлы штатным шаб
|
|||||||
|
|
||||||
| База | Объект | Что это |
|
| База | Объект | Что это |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| STG | `stg.hits_raw_kafka` | чтец топика `hits`, формат `RawBLOB` |
|
| `stg` | `stg.hits_raw_kafka` | чтец топика `hits`, формат `RawBLOB` |
|
||||||
| STG | `stg.hits_raw_rep` / `_dist` | сырая строка сообщения плюс метаданные доставки |
|
| `stg` | `stg.hits_raw_rep` / `_dist` | сырая строка сообщения плюс метаданные доставки |
|
||||||
| STG | `stg.hits_raw_mv` | наполняет сырьё из чтеца |
|
| `stg` | `stg.hits_raw_mv` | наполняет сырьё из чтеца |
|
||||||
| STG | `stg.orders_raw_kafka` | чтец топика `orders`, формат `RawBLOB`, только на ноде 1 и без матвью |
|
| `stg` | `stg.orders_raw_kafka` | чтец топика `orders`, формат `RawBLOB`, только на ноде 1 и без матвью |
|
||||||
| STG | `stg.orders_raw_rep` / `_dist` | сырое сообщение слепка, метаданные доставки и `_load_id` |
|
| `stg` | `stg.orders_raw_rep` / `_dist` | сырое сообщение слепка, метаданные доставки и `_load_id` |
|
||||||
| ODS | `ods.event_rep` / `_dist` | типизированное широкое событие |
|
| `ods` | `ods.event_rep` / `_dist` | типизированное широкое событие |
|
||||||
| ODS | `ods.event_v` | актуальная версия события с полями источника |
|
| `ods` | `ods.event_v` | актуальная версия события с полями источника |
|
||||||
| ODS | `ods.event_errors_rep` / `_dist` | строки, не прошедшие строгий приём |
|
| `ods` | `ods.event_errors_rep` / `_dist` | строки, не прошедшие строгий приём |
|
||||||
| ODS | `ods.event_mv`, `ods.event_errors_mv` | разбор сырья в событие и в ошибки |
|
| `ods` | `ods.event_mv`, `ods.event_errors_mv` | разбор сырья в событие и в ошибки |
|
||||||
| ODS | `ods.order_snapshot_rep` / `_dist` | типизированные версии заказа |
|
| `ods` | `ods.order_snapshot_rep` / `_dist` | типизированные версии заказа |
|
||||||
| ODS | `ods.order_v` | текущая версия заказа на языке источника |
|
| `ods` | `ods.order_v` | текущая версия заказа на языке источника |
|
||||||
| ODS | `ods.order_snapshot_errors_rep` / `_dist` | строки слепка, не прошедшие строгий приём |
|
| `ods` | `ods.order_snapshot_errors_rep` / `_dist` | строки слепка, не прошедшие строгий приём |
|
||||||
| `dic` | `dic.products_file` | чтец CSV-каталога, общего с генератором |
|
| `dic` | `dic.products_file` | чтец CSV-каталога, общего с генератором |
|
||||||
| `dic` | `dic.products` | словарь товаров поверх подложки |
|
| `dic` | `dic.products` | словарь товаров поверх подложки |
|
||||||
|
|
||||||
|
|||||||
@@ -240,10 +240,12 @@ Ecommerce (заполнены только у торговых событий):
|
|||||||
## 3. Каталог товаров
|
## 3. Каталог товаров
|
||||||
|
|
||||||
CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `category`,
|
CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `category`,
|
||||||
`brand`, `price`, `demand`) — **словарь ClickHouse** из файла. Тот же файл
|
`brand`, `price`, `demand`) — **словарь ClickHouse**. Тот же файл использует
|
||||||
использует генератор — расхождений нет по построению. Даёт `dictGet` в
|
генератор — расхождений нет по построению. Даёт `dictGet` в витринах и
|
||||||
витринах и разговор о политике обновления словаря. На кластере файл
|
разговор о политике обновления словаря. На кластере файл монтируется в обе
|
||||||
монтируется в обе ноды, словарь создаётся ON CLUSTER. Последняя колонка —
|
ноды; над ним стоит подложка, а словарь читает её и живёт в зоне справочников
|
||||||
|
`dic` вне цепочки слоёв ([ADR 0012](../adr/0012-dictionary-home.md)).
|
||||||
|
Последняя колонка —
|
||||||
уровень спроса товара, заведена при исполнении #50 (спека генератора,
|
уровень спроса товара, заведена при исполнении #50 (спека генератора,
|
||||||
раздел 9): генератор решает по ней, что уходит из карточки в корзину.
|
раздел 9): генератор решает по ней, что уходит из карточки в корзину.
|
||||||
|
|
||||||
@@ -402,7 +404,7 @@ README.
|
|||||||
|
|
||||||
## 7. Слои: карта таблиц v2
|
## 7. Слои: карта таблиц v2
|
||||||
|
|
||||||
| Слой | Объект | Что это |
|
| Где | Объект | Что это |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Kafka | `hits`, `orders` | два топика: `hits` — 2 партиции, `orders` — одна |
|
| Kafka | `hits`, `orders` | два топика: `hits` — 2 партиции, `orders` — одна |
|
||||||
| STG | `stg.hits_raw_kafka`, `stg.hits_raw` + MV | сырые строки событий, Kafka Engine на обеих нодах |
|
| STG | `stg.hits_raw_kafka`, `stg.hits_raw` + MV | сырые строки событий, Kafka Engine на обеих нодах |
|
||||||
@@ -413,7 +415,7 @@ README.
|
|||||||
| DDS | `dds.event_v` | представление над `ods.event`: snake_case-имена, расшифровка кодов `DeviceCategory`; витрины DM читают его, а не ODS напрямую |
|
| DDS | `dds.event_v` | представление над `ods.event`: snake_case-имена, расшифровка кодов `DeviceCategory`; витрины DM читают его, а не ODS напрямую |
|
||||||
| DDS | модель заказов | зерно, связи и материализация проектируются на этапе DDS |
|
| DDS | модель заказов | зерно, связи и материализация проектируются на этапе DDS |
|
||||||
| DDS | `dds.identity_map` | карта кука↔пользователь |
|
| DDS | `dds.identity_map` | карта кука↔пользователь |
|
||||||
| `dic` | подложка `products_file` и словарь `products` | каталог из CSV, вне цепочки слоёв ([ADR 0012](../adr/0012-dictionary-home.md)) |
|
| DIC | подложка `products_file` и словарь `products` | каталог из CSV, вне цепочки слоёв ([ADR 0012](../adr/0012-dictionary-home.md)) |
|
||||||
| DM | витрины `dm.*_v`, `dm.dq_summary` | см. ниже |
|
| DM | витрины `dm.*_v`, `dm.dq_summary` | см. ниже |
|
||||||
|
|
||||||
У каждой таблицы слоя — пара из локальной и распределённой, имена по конвенции
|
У каждой таблицы слоя — пара из локальной и распределённой, имена по конвенции
|
||||||
@@ -697,9 +699,9 @@ v2, этап 0).
|
|||||||
`RegionCityID`);
|
`RegionCityID`);
|
||||||
- неатомарное обновление словаря на кластере: ноды перезагружают его в
|
- неатомарное обновление словаря на кластере: ноды перезагружают его в
|
||||||
случайный момент внутри окна `LIFETIME`, фазы у них независимы, и после
|
случайный момент внутри окна `LIFETIME`, фазы у них независимы, и после
|
||||||
правки каталога две ноды отвечают на один `dictGet` по-разному — почти всё
|
правки каталога две ноды почти всё окно отвечают на один `dictGet`
|
||||||
окно целиком. Обвязка готова ([ADR
|
по-разному. Обвязка готова ([ADR 0012](../adr/0012-dictionary-home.md)),
|
||||||
0012](../adr/0012-dictionary-home.md)), урок остаётся на выбор;
|
урок остаётся на выбор;
|
||||||
- лаба сессий: менти сначала собирает сессии сам, и только после — рассказ,
|
- лаба сессий: менти сначала собирает сессии сам, и только после — рассказ,
|
||||||
что с октября 2025 Метрика отдаёт `VisitID` прямо в хитах; частично
|
что с октября 2025 Метрика отдаёт `VisitID` прямо в хитах; частично
|
||||||
синтетическая постановка — осознанный приём;
|
синтетическая постановка — осознанный приём;
|
||||||
|
|||||||
@@ -44,12 +44,7 @@
|
|||||||
<query>GRANT SELECT ON system.settings</query>
|
<query>GRANT SELECT ON system.settings</query>
|
||||||
</grants>
|
</grants>
|
||||||
</analyst_reader>
|
</analyst_reader>
|
||||||
<!--
|
<!-- Учётка словаря: пароля нет намеренно, см. ADR 0007. -->
|
||||||
Словарь читает подложку не сам по себе, а от имени пользователя:
|
|
||||||
без явного user он идёт как default с пустым паролем и падает.
|
|
||||||
Пароля нет намеренно — ходить этой учёткой некуда, кроме локального
|
|
||||||
чтения одной базы, а пароль в тексте DDL означал бы секрет в git.
|
|
||||||
-->
|
|
||||||
<dict_reader>
|
<dict_reader>
|
||||||
<grants>
|
<grants>
|
||||||
<query>GRANT SELECT ON dic.*</query>
|
<query>GRANT SELECT ON dic.*</query>
|
||||||
|
|||||||
@@ -264,10 +264,13 @@ printf 'ЗЕЛЁНО: временные таблицы удалены; пров
|
|||||||
# После снятия ловушек: своих объектов эти проверки не заводят и прибирать за
|
# После снятия ловушек: своих объектов эти проверки не заводят и прибирать за
|
||||||
# собой им нечего — они только смотрят на то, что стенд произвёл сам.
|
# собой им нечего — они только смотрят на то, что стенд произвёл сам.
|
||||||
printf 'Проверка 9/10: словарь товаров отвечает на обеих нодах...\n'
|
printf 'Проверка 9/10: словарь товаров отвечает на обеих нодах...\n'
|
||||||
|
# Цену словарь отдаёт в Decimal(18, 2), а каталог хранит целые копейки. Умножаем
|
||||||
|
# обратно и сравниваем с колонкой файла: так проверка заодно утверждает, что
|
||||||
|
# приведение в источнике словаря точное, а не только что словарь отвечает.
|
||||||
product_sql="SELECT
|
product_sql="SELECT
|
||||||
dictGet('dds.products', 'name', tuple('HOME-0001')),
|
dictGet('dic.products', 'name', tuple('HOME-0001')),
|
||||||
dictGet('dds.products', 'category', tuple('HOME-0001')),
|
dictGet('dic.products', 'category', tuple('HOME-0001')),
|
||||||
dictGet('dds.products', 'price', tuple('HOME-0001'))
|
toInt64(dictGet('dic.products', 'price', tuple('HOME-0001')) * 100)
|
||||||
FORMAT TSV"
|
FORMAT TSV"
|
||||||
expected_product="$(
|
expected_product="$(
|
||||||
awk -F, \
|
awk -F, \
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ CREATE DATABASE IF NOT EXISTS ods ON CLUSTER clickstream_cluster;
|
|||||||
CREATE DATABASE IF NOT EXISTS dds ON CLUSTER clickstream_cluster;
|
CREATE DATABASE IF NOT EXISTS dds ON CLUSTER clickstream_cluster;
|
||||||
|
|
||||||
-- Справочники стоят вне цепочки STG → ODS → DDS → DM: в хранилище их никто не
|
-- Справочники стоят вне цепочки STG → ODS → DDS → DM: в хранилище их никто не
|
||||||
-- производит, а читают их несколько слоёв ([ADR 0012]). Поэтому зона своя, и
|
-- производит, а читают их несколько слоёв — ADR 0012. Порядок слоёв к зоне не
|
||||||
-- порядок слоёв к ней не применяется — её файл идёт сразу за этим.
|
-- применяется, её файл идёт сразу за этим.
|
||||||
CREATE DATABASE IF NOT EXISTS dic ON CLUSTER clickstream_cluster;
|
CREATE DATABASE IF NOT EXISTS dic ON CLUSTER clickstream_cluster;
|
||||||
|
|||||||
+15
-35
@@ -1,22 +1,14 @@
|
|||||||
-- Каталог товаров: подложка на файловом движке и словарь поверх неё.
|
-- Каталог товаров: подложка на файловом движке и словарь поверх неё.
|
||||||
--
|
--
|
||||||
-- Словарь читает не файл, а таблицу хранилища — намеренное усложнение
|
-- Словарь читает не файл, а таблицу хранилища. Это намеренное усложнение ради
|
||||||
-- ([ADR 0012](../../docs/adr/0012-dictionary-home.md)). В бою справочник
|
-- урока: в бою справочник приезжает процессом, и предложение SOURCE с запросом
|
||||||
-- приезжает процессом, и предложение SOURCE с запросом и учётной записью —
|
-- и учётной записью — та форма, которую менти встретит. Доводы, отвергнутые
|
||||||
-- та форма, которую менти встретит; файловый источник работает, но редок.
|
-- варианты и условия пересмотра — ADR 0012.
|
||||||
--
|
|
||||||
-- Зона dic лежит вне цепочки STG → ODS → DDS → DM: справочник в хранилище
|
|
||||||
-- никто не производит, а читают его несколько слоёв.
|
|
||||||
|
|
||||||
-- Подложка ничего не хранит: движок File перечитывает CSV на каждом запросе,
|
-- Подложка ничего не хранит: движок File перечитывает CSV на каждом запросе,
|
||||||
-- поэтому правка каталога доезжает до словаря сама. Compose монтирует один и
|
-- поэтому правка каталога доезжает до словаря сама. Путь считается от
|
||||||
-- тот же файл в user_files обеих нод только для чтения.
|
-- user_files, а не от корня данных. Типы повторяют файл, а не модель: цена
|
||||||
--
|
-- лежит целыми копейками, приведение делает словарь.
|
||||||
-- Путь считается ОТ user_files, а не от корня данных: форма
|
|
||||||
-- './user_files/catalog/products.csv' даёт FILE_DOESNT_EXIST. У файлового
|
|
||||||
-- источника словаря база пути была другой — отсюда разница с прежним DDL.
|
|
||||||
-- Типы здесь повторяют файл, а не модель: цена лежит целыми копейками, как её
|
|
||||||
-- пишет генератор. Приведение к деньгам делает словарь.
|
|
||||||
CREATE TABLE IF NOT EXISTS dic.products_file ON CLUSTER clickstream_cluster
|
CREATE TABLE IF NOT EXISTS dic.products_file ON CLUSTER clickstream_cluster
|
||||||
(
|
(
|
||||||
sku String,
|
sku String,
|
||||||
@@ -28,28 +20,16 @@ CREATE TABLE IF NOT EXISTS dic.products_file ON CLUSTER clickstream_cluster
|
|||||||
)
|
)
|
||||||
ENGINE = File(CSVWithNames, './catalog/products.csv');
|
ENGINE = File(CSVWithNames, './catalog/products.csv');
|
||||||
|
|
||||||
-- Пользователь dict объявлен файлом настройки и умеет одно — читать dic.
|
-- Пользователь dict умеет одно — читать dic. Назвать его обязательно: без user
|
||||||
-- Назвать его обязательно: без user словарь идёт как default с пустым паролем
|
-- словарь идёт как default с пустым паролем и падает. Хост локальный, поэтому
|
||||||
-- и падает с AUTHENTICATION_FAILED. Хост локальный, поэтому запрос к подложке
|
-- запрос к подложке идёт без сети.
|
||||||
-- идёт без сети.
|
|
||||||
--
|
--
|
||||||
-- Окно обновления вместо LIFETIME(0): словарь перезагружается сам в случайный
|
-- Форма query, а не table: словарь приводит копейки каталога к Decimal(18, 2)
|
||||||
-- момент внутри окна. Случайность разводит обращения разных серверов к
|
-- прямо на входе, то есть нормализует, а не зеркалит подложку. Ключ строковый,
|
||||||
-- источнику, и цена у неё заявленная — ноды обновляются вразнобой. Ждать
|
-- поэтому COMPLEX_KEY_HASHED: числовой FLAT здесь неприменим.
|
||||||
-- правки каталога каждой из них приходится от нуля до верхней границы окна,
|
|
||||||
-- фазы у них независимы, поэтому расходиться они могут почти на все
|
|
||||||
-- полторы минуты: одна перезагрузилась сразу после правки, вторая ещё нет.
|
|
||||||
--
|
--
|
||||||
-- Ключ строковый, поэтому COMPLEX_KEY_HASHED: числовой FLAT здесь неприменим.
|
-- Окно вместо LIFETIME(0): словарь обновляется сам, а цена этому — ноды
|
||||||
--
|
-- расходятся почти на всё окно (ADR 0012).
|
||||||
-- Цена приводится к деньгам прямо в источнике — оттого форма query, а не
|
|
||||||
-- table: словарь нормализует на входе, а не зеркалит подложку. В файле лежат
|
|
||||||
-- целые копейки, наружу словарь отдаёт Decimal(18, 2), как заказы бэкенда.
|
|
||||||
-- Единица в числе становится видна: 129000 против 1290.00.
|
|
||||||
--
|
|
||||||
-- Стык с событием на этом и стоит: в контракте события productPrice — целые
|
|
||||||
-- РУБЛИ, округление формата. Разрыв между ценой каталога и ценой в событии
|
|
||||||
-- намеренный, на нём держится урок про Float64.
|
|
||||||
CREATE DICTIONARY IF NOT EXISTS dic.products ON CLUSTER clickstream_cluster
|
CREATE DICTIONARY IF NOT EXISTS dic.products ON CLUSTER clickstream_cluster
|
||||||
(
|
(
|
||||||
sku String,
|
sku String,
|
||||||
|
|||||||
Reference in New Issue
Block a user