docs(warehouse): упрощены объяснения витрин, сессий и пересчета
Зачем: читателю ступеней 1–4 нужно проследить выбор дней, расчет строки и публикацию результата без расшифровки метафор. Что: согласованы справочники DM, сессий, ETL и два раздела хранилища с комментариями шести SQL-примеров. Уточнены ограничения повторного запуска и выбора атрибутов при равном времени событий. Проверка: make lint, git diff --check, локальные Markdown-ссылки и якоря. SQL без комментариев совпадает; AST Python без строк документации совпадает. Исторические разделы и материалы вне границ задачи сохранены. Refs: #249
This commit is contained in:
1 parent
84dc718f62
commit
5f317ba8d2
11 files changed
+440
-490
No files matched your search
@@ -30,115 +30,92 @@ keeper, Kafka, каркас сервисов. Этап 2 собран: в `sql/d
|
||||
|
||||
## Имена объектов
|
||||
|
||||
Имя объекта заканчивается тем, что это за объект:
|
||||
Суффикс показывает назначение объекта:
|
||||
|
||||
| Суффикс | Что это |
|
||||
|---|---|
|
||||
| `_rep` | локальная таблица шарда, движок семейства `Replicated*` |
|
||||
| `_dist` | `Distributed` поверх одноимённой локальной |
|
||||
| `_kafka` | таблица на движке `Kafka` |
|
||||
| `_file` | таблица на движке `File` — чтец файла |
|
||||
| `_mv` | материализованное представление |
|
||||
| `_v` | обычное представление, публичная поверхность чтения |
|
||||
| `_rep` | локальная таблица: хранит данные своего шарда, движок семейства `Replicated*` |
|
||||
| `_dist` | таблица `Distributed`: читает данные шардов и распределяет запись между ними |
|
||||
| `_kafka` | таблица на движке `Kafka`: читает сообщения топика |
|
||||
| `_file` | таблица на движке `File`: читает файл |
|
||||
| `_mv` | материализованное представление: обрабатывает вставляемые данные |
|
||||
| `_v` | обычное представление: выполняет сохраненный запрос при чтении |
|
||||
|
||||
Суффикс носит каждый физический объект. Голого имени у таблицы не существует:
|
||||
запрос к `stg.hits_raw` даёт громкую ошибку «нет такой таблицы» — а под голым
|
||||
именем в документах и разговоре понимается сущность, у которой этих объектов
|
||||
несколько. Единственное исключение — словари: у них воплощение одно, шардировать
|
||||
нечего, и суффикс ничего не различал бы. В зоне `dic` обе формы стоят рядом и
|
||||
правило видно целиком: `dic.products_file` — чтец файла, `dic.products` — сам
|
||||
словарь.
|
||||
Для запросов к моделям DDS и витринам DM используйте `_v`. Представления
|
||||
явно перечисляют колонки, поэтому добавление поля в физическую таблицу
|
||||
не меняет набор полей для читателя. У `dds.event_v` нет отдельной таблицы
|
||||
DDS: представление читает события ODS.
|
||||
|
||||
Распространённая конвенция, где голое имя означает локальную таблицу, а
|
||||
распределённая получает суффикс `_all`, ошибается иначе: забытый суффикс тихо
|
||||
возвращает данные одного шарда. Правило спеки «проверки и контрольные суммы —
|
||||
только по `Distributed`» такую тишину переживает плохо.
|
||||
В документации имя без суффикса, например `stg.hits_raw`, обозначает
|
||||
сущность с несколькими объектами. Таблицы с таким именем нет: в запросе
|
||||
нужно указать суффикс. Исключение — словари ClickHouse: `dic.products`
|
||||
обращается к каталогу товаров, а `dic.products_file` читает его файл.
|
||||
|
||||
Второе свойство — в списке по алфавиту объекты группируются по сущности, а не по
|
||||
технологии: все четыре объекта топика `hits` стоят рядом, потому что различаются
|
||||
хвостом, а не началом имени. Речь про дерево в клиенте и про `ORDER BY name`:
|
||||
порядок выдачи `SHOW TABLES` документация не оговаривает.
|
||||
В STG имя берется из топика: `hits_raw`, `orders_raw`. В типизированных
|
||||
слоях оно называет сущность в единственном числе: `event`, `order`,
|
||||
`session`. Объекты одной сущности различаются суффиксом и стоят рядом
|
||||
при сортировке имен по алфавиту.
|
||||
|
||||
Начало имени — сущность, и берётся она в разных слоях из разных мест. В STG имя
|
||||
приходит от транспорта: слой хранит то, что доехало по топику, и зовётся именем
|
||||
топика — `hits_raw` от `hits`, `orders_raw` от `orders`. В типизированных слоях
|
||||
имя приходит от предметной области и стоит в единственном числе: `event`,
|
||||
`order`, `session`. Граница между «как привезли» и «что это такое»
|
||||
проходит по STG, и имена её показывают.
|
||||
|
||||
В DDS публичный договор сущности заканчивается на `_v`. Потребительские запросы
|
||||
и витрины называют только представление; `_rep` и `_dist` служат загрузке,
|
||||
диагностике и обслуживанию. Представление перечисляет колонки явно, даже если
|
||||
его запрос — простая проекция над `_dist`: договор не выдаёт устройство
|
||||
кластера и случайно добавленные в физическую таблицу колонки. У события
|
||||
физической копии в DDS нет, но правило то же: `dds.event_v` — его публичный
|
||||
договор.
|
||||
|
||||
Право доступа здесь намеренно шире договора. Роль `bi_reader` может читать
|
||||
любые объекты ODS, DDS и DM, но физические объекты от этого не становятся
|
||||
публичной поверхностью. Конвенцию держат явный список колонок, документация и
|
||||
ревью, а не запрет на физические таблицы.
|
||||
|
||||
Широкое право принято в [ADR 0007](../adr/0007-clickhouse-access.md): узкая
|
||||
граница поверх `Distributed` потребовала бы отдельной механики. Механизм
|
||||
ClickHouse и отклонённые варианты записаны в
|
||||
`_rep` и `_dist` нужны для загрузки, диагностики и обслуживания. Роль
|
||||
`bi_reader` может читать их в ODS, DDS и DM, но для обычного анализа
|
||||
принято использовать `_v`. Это соглашение о чтении, а не ограничение прав;
|
||||
причина широкого доступа описана в
|
||||
[ADR 0007](../adr/0007-clickhouse-access.md) и
|
||||
[исследовании](../research/2026-08-25-clickhouse-distributed-view-security.md).
|
||||
|
||||
## Раскладка по шардам
|
||||
|
||||
Пишем только в `_dist`. Локальные таблицы остаются для чтения и обслуживания —
|
||||
операций с партициями, ручной переобработки. Правило не про удобство: при записи
|
||||
через распределённую таблицу раскладку определяет ключ шардирования, то есть
|
||||
свойство данных, а при записи в локальную — то, какая нода случайно выполняла
|
||||
код. Отсюда урок стенда: какая нода читала топик, меняется между прогонами
|
||||
(видно в колонке `consumer_host`), а куда легли данные — нет.
|
||||
Шард хранит часть строк. При записи через `_dist` место строки выбирает
|
||||
ключ шардирования. При прямой записи в `_rep` она остается на той ноде,
|
||||
где выполнена вставка. Поэтому данные записывают через `_dist`,
|
||||
а операции с партициями выполняют на локальных `_rep`.
|
||||
|
||||
Ключи шардирования: `cityHash64(ClientID)` у событий, `cityHash64(order_id)` у
|
||||
заказов, `cityHash64` сырой строки у STG и у таблицы ошибок. У первых двух хеш
|
||||
выбран против перекоса: структурированный числовой идентификатор распределяется
|
||||
по остатку от деления неравномерно. У сырого слоя выбора нет — строку иначе не
|
||||
разложишь; там хеш даёт другое свойство, одинаковые сообщения ложатся на один
|
||||
шард.
|
||||
У событий ключ — `cityHash64(ClientID)`, у заказов —
|
||||
`cityHash64(order_id)`, у STG и таблиц ошибок — хеш сырой строки.
|
||||
Хеш распределяет числовые идентификаторы по шардам и направляет одинаковые
|
||||
сообщения на один шард. У сырой строки и разобранного события разные
|
||||
ключи: они могут оказаться на разных шардах. Счетчики STG и ODS сравнивают
|
||||
по всему кластеру через `_dist`, а не отдельно по каждой ноде.
|
||||
|
||||
Ключи у слоёв разные, и это имеет наблюдаемое следствие: сырая строка и
|
||||
разобранное из неё событие почти всегда оказываются на разных шардах.
|
||||
Пошардовые счётчики STG и ODS поэтому не сходятся и сходиться не должны —
|
||||
сверять слои можно только через `_dist`.
|
||||
При `INSERT INTO цель_dist SELECT … FROM источник_dist` важна настройка
|
||||
`parallel_distributed_insert_select`. При значении `2` и одном кластере
|
||||
вставка может выполняться локально на шардах, сохраняя раскладку источника
|
||||
вместо ключа цели. Если ключи различаются, укажите
|
||||
`parallel_distributed_insert_select = 0`: нода, принявшая запрос,
|
||||
соберет результат и распределит его по ключу цели. Так сделано в
|
||||
[`sql/ods/order_parse.sql`](../../sql/ods/order_parse.sql) и при сборке
|
||||
выручки. Неверную раскладку проверяют по `hostName()` и ожидаемому шарду
|
||||
строки; общий счет по `_dist` ее не показывает.
|
||||
|
||||
Разные ключи у слоёв ставят капкан на вставке `INSERT INTO цель_dist SELECT …
|
||||
FROM источник_dist`. Заводское значение `parallel_distributed_insert_select` —
|
||||
2: если обе таблицы на одном кластере, вставка исполняется локально на каждом
|
||||
шарде, минуя ключ шардирования цели. Строки наследуют раскладку источника, и
|
||||
ошибки при этом нет — расхождение видно только пересчётом ключей по
|
||||
`hostName()`. Так половина заказов и легла на оба шарда, где
|
||||
`ReplacingMergeTree` через границу шарда дубли не схлопывает (замер — «Что
|
||||
проверено», #137). Лекарство — явное `parallel_distributed_insert_select = 0`
|
||||
в `SETTINGS` такой вставки: раскладку делает инициатор, по ключу цели
|
||||
(`sql/ods/order_parse.sql`). Капкан ждёт и сборки DDS: они тоже будут читать
|
||||
`_dist` и писать в `_dist` с другим ключом.
|
||||
События, `dds.session_dist` и `dds.identity_map_dist` распределены
|
||||
по `cityHash64(client_id)` (в ODS поле называется `ClientID`). Данные
|
||||
одного посетителя лежат вместе — это ко-локация. Заказы DDS и их сверка
|
||||
сохраняют раскладку по `cityHash64(order_id)`. Обычное соединение допускается
|
||||
по ключу ко-локации; для остальных соединений применяется `GLOBAL`.
|
||||
Само совпадение ключей еще не гарантирует локального выполнения запроса
|
||||
через представления — пример есть в [описании `daily_traffic`](dm.md#dmdaily_traffic_v).
|
||||
|
||||
Ключи ко-локации названы заранее, потому что на них стоит политика соединений из
|
||||
раздела 6 спеки: обычное соединение разрешено только по ключу ко-локации, всё
|
||||
прочее — через `GLOBAL`. Значит `dds.session_dist` и `dds.identity_map_dist`
|
||||
шардируются по `cityHash64(client_id)`. Объекты DDS с зерном заказа сохраняют
|
||||
ко-локацию по `cityHash64(order_id)`; ключи остальных частей модели и витрин
|
||||
появляются вместе с ними.
|
||||
|
||||
Замена партиций не используется для `ods.order`: версии прошлых дней
|
||||
доливаются, а прямое чтение Kafka не задаёт границы полного слепка ([ADR
|
||||
0010](../adr/0010-order-versions-in-ods.md)). Если партиционная пересборка
|
||||
понадобится будущим объектам DDS или DM, партиция-донор должна быть заранее
|
||||
разложена по шардам по тому же ключу. Форму донора следует решать вместе с таким
|
||||
объектом, а не переносить на ODS заранее.
|
||||
|
||||
Первым такой донор понадобился заказу DDS, и правило на нём видно целиком:
|
||||
`dds.order_stage_rep` объявлен через `AS dds.order_rep` и шардируется тем же
|
||||
`cityHash64(order_id)`. Совпадение структуры требует сама операция, а совпадение
|
||||
ключа — устройство замены: она идёт по локальным таблицам, шард за шардом, и
|
||||
строка, попавшая в доноре не на свой шард, при подмене пропала бы с одного шарда
|
||||
и задвоилась на другом. Доводы целиком — в
|
||||
Для дневной пересборки DDS и DM используют донор — служебную таблицу,
|
||||
в которой готовят результат перед заменой партиций. Например,
|
||||
`dds.order_stage_rep` объявлена через `AS dds.order_rep`, а запись
|
||||
в нее идет через `dds.order_stage_dist` с тем же `cityHash64(order_id)`.
|
||||
У цели и донора должны совпадать структура, ключи партиции и сортировки,
|
||||
первичный ключ и политика хранения. Эти требования сверены через Context7
|
||||
с [документацией ClickHouse](https://clickhouse.com/docs/sql-reference/statements/alter/partition#replace-partition)
|
||||
для используемой схемы на ClickHouse 26.3. Поэтому донор объявлен через
|
||||
`AS` цели с совпадающими ключами. DDL показан в
|
||||
[`sql/ddl/45-dds-tables.sql`](../../sql/ddl/45-dds-tables.sql).
|
||||
|
||||
`REPLACE PARTITION` копирует готовую партицию между локальными таблицами
|
||||
на каждом шарде; заново распределять строки по ключу цели эта операция
|
||||
не будет. Поэтому донор должен сразу иметь ту же раскладку, что и цель.
|
||||
Перед заменой вставка с `distributed_foreground_insert = 1` дожидается
|
||||
доставки данных на шарды.
|
||||
|
||||
В `ods.order` этот способ не применяется: туда добавляют версии заказов
|
||||
из слепков, а актуальные строки выбираются при чтении
|
||||
([ADR 0010](../adr/0010-order-versions-in-ods.md)).
|
||||
|
||||
## Служебные колонки
|
||||
|
||||
Собственные колонки не повторяют имён виртуальных. Виртуальные даёт движок:
|
||||
|
||||
Reference in new issue
Block a user