Compare commits

..
2 Commits
Author SHA1 Message Date
ddadminandClaude Opus 5 94a1ffeee2 docs(ods): описана семантика snapshot_date после схлопывания версий
- Зачем:
  - проверка #137 показала: у заказа, не менявшегося несколько дней, версию
    выбирает только updated_at, и какая из одинаковых строк переживёт
    схлопывание — не обещано; snapshot_date выжившей строки означал не то,
    что обещала документация (у 86 заказов стартового мира выжил серединный
    день наблюдения).
- Что:
  - в docs/architecture/orders/ingestion.md записано: snapshot_date, _load_id
    и _load_ts выжившей строки — координаты одного из наблюдений версии, не
    обязательно последнего; допущение «уточнение поднимает updated_at»
    названо допущением с границей применимости.
  - та же оговорка — в комментарии DDL ods.order_rep.
- Проверка:
  - замеры на живом стенде: во всех 3034 парах строк с равным updated_at
    деловые поля совпали побайтово; монотонность updated_at по слепкам —
    без нарушений; ods.order_v сошёлся с эталоном argMax из сырья построчно.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 04:28:51 -04:00
ddadminandClaude Opus 5 8c76a7a0d8 fix(ods): заказы разложены по своему ключу шардирования
- Зачем:
  - заводское parallel_distributed_insert_select = 2 исполняло вставку из
    Distributed в Distributed локально на каждом шарде, минуя ключ цели:
    1189 из 1662 заказов задвоились на обоих шардах, а ReplacingMergeTree
    через границу шарда дубли не схлопывает.
- Что:
  - в обе вставки sql/ods/order_parse.sql добавлено явное
    parallel_distributed_insert_select = 0 с доводом комментарием.
  - в docs/architecture/storage.md описан капкан «INSERT из Distributed в
    Distributed при несовпадающих ключах» и записана проверка (#137).
- Проверка:
  - межшардовые дубли по ods.order_rep дают 0 (было 1189), каждый шард
    держит только свои ключи; uniqExact(order_id) не изменился.
  - после прожитого дага world_next_day новый слепок принят, дублей
    по-прежнему 0.
  - make smoke, make check-clickhouse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 04:02:52 -04:00
4 changed files with 62 additions and 3 deletions

No files matched your search

+19
View File
@@ -106,6 +106,25 @@ JSON-объектом с точным набором ключей: `order_id`, `
- `_load_id` — какой запуск Airflow принял строку; - `_load_id` — какой запуск Airflow принял строку;
- `_load_ts` — когда строка приехала в хранилище. - `_load_ts` — когда строка приехала в хранилище.
У выжившей после схлопывания строки три последние координаты означают меньше,
чем кажется. Версию выбирает только `updated_at`; заказ, не менявшийся
несколько дней, приезжает в нескольких слепках одинаковыми деловыми полями, и
какая из этих строк переживёт схлопывание — не обещано. Поэтому её
`snapshot_date` — *один из* дней, когда источник показывал эту версию, не
обязательно последний; то же с `_load_id` и `_load_ts`. Замер 22 августа
2026 года (#137): у 86 заказов стартового мира выжил даже не крайний день, а
серединный. Вопрос «когда заказ наблюдался в последний раз» задаётся сырью STG,
пока его не съел срок жизни, — а не одной строке ODS.
Вся схема версий стоит на одном свойстве источника: уточнение заказа поднимает
`updated_at`. Наш генератор моделирует источник с триггерной меткой и это
свойство гарантирует; замеры 22 августа 2026 года его подтвердили — во всех
парах строк с равным `updated_at` деловые поля совпали побайтово, а по слепкам
метка не убывала ни разу. Это допущение, а не закон природы: источник, умеющий
уточнить строку тихо — правка мимо приложения, миграция, ручной `UPDATE`, —
эту схему ломает, и тогда версии по метке времени уже не хватает, нужна сверка
самих полей (в терминах dbt snapshots — стратегия `check` вместо `timestamp`).
Обычное чтение `_dist` показывает физически сохранившиеся версии и нужно для Обычное чтение `_dist` показывает физически сохранившиеся версии и нужно для
диагностики. Их число зависит от фоновых слияний: ODS не служит архивом истории. диагностики. Их число зависит от фоновых слияний: ODS не служит архивом истории.
`ods.order_v` сохраняет те же источник-ориентированные поля без обогащения `ods.order_v` сохраняет те же источник-ориентированные поля без обогащения
+23
View File
@@ -84,6 +84,19 @@ CSV-файла. Живёт он вне цепочки слоёв, в зоне `d
Пошардовые счётчики STG и ODS поэтому не сходятся и сходиться не должны — Пошардовые счётчики STG и ODS поэтому не сходятся и сходиться не должны —
сверять слои можно только через `_dist`. сверять слои можно только через `_dist`.
Разные ключи у слоёв ставят капкан на вставке `INSERT INTO цель_dist SELECT …
FROM источник_dist`. Заводское значение `parallel_distributed_insert_select` —
2: если обе таблицы на одном кластере, вставка исполняется локально на каждом
шарде, и шард пишет прямо в локальную таблицу цели, минуя её ключ шардирования.
Строки наследуют раскладку источника, и ошибки при этом нет: расхождение видно
только пересчётом по `hostName()` — сколько ключей встретилось больше чем на
одном шарде. На разборе заказов так и вышло: 1189 из 1662 заказов легли
одинаковыми строками на оба шарда, а `ReplacingMergeTree` через границу шарда их
не схлопывает. Поэтому у такой вставки в `SETTINGS` стоит явное
`parallel_distributed_insert_select = 0`: раскладку делает инициатор, по ключу
цели (`sql/ods/order_parse.sql`). Правило нужно и дальше — сборки DDS тоже будут
читать `_dist` и писать в `_dist` с другим ключом.
Ключи ко-локации названы заранее, потому что на них стоит политика соединений из Ключи ко-локации названы заранее, потому что на них стоит политика соединений из
раздела 6 спеки: обычное соединение разрешено только по ключу ко-локации, всё раздела 6 спеки: обычное соединение разрешено только по ключу ко-локации, всё
прочее — через `GLOBAL`. Значит `dds.session` и `dds.identity_map` шардируются по прочее — через `GLOBAL`. Значит `dds.session` и `dds.identity_map` шардируются по
@@ -573,6 +586,16 @@ DDL-словарь с файловым источником внутри `user_f
разобрано, почему отказ в праве на движок называет не то право, которого не разобрано, почему отказ в праве на движок называет не то право, которого не
хватает. хватает.
**Проверка ко-локации заказов 22 августа 2026 года (#137).** Context7 и
`system.settings` закреплённого ClickHouse 26.3 назвали одну и ту же тройку
значений `parallel_distributed_insert_select`: `0` — выключено, `1` — `SELECT`
на каждом шарде, `2` — `SELECT` и `INSERT` на каждом шарде; заводское — `2`.
На живых данных видны и дефект, и починка: до правки 1189 из 1662 заказов
лежали на обоих шардах, после явного нуля и повторного разбора того же
`_load_id` — ни одного, каждый шард держит только свои ключи. Вершина версий
уцелела: деловые поля `ods.order_v` сошлись с эталоном, посчитанным прямо из
сырья.
**Проверка версий заказов 18 августа 2026 года (#94).** MCP Context7 подтвердил, **Проверка версий заказов 18 августа 2026 года (#94).** MCP Context7 подтвердил,
что `JSONType` возвращает имя типа значения JSON, — на нём стоит проверка типов что `JSONType` возвращает имя типа значения JSON, — на нём стоит проверка типов
в предикате приёма заказов. Остальное снято на закреплённом ClickHouse 26.3. в предикате приёма заказов. Остальное снято на закреплённом ClickHouse 26.3.
+4 -1
View File
@@ -163,7 +163,10 @@ ENGINE = Distributed('clickstream_cluster', 'ods', 'event_errors_rep', cityHash6
-- updated_at — какая бизнес-версия новее; snapshot_date — в слепке какого -- updated_at — какая бизнес-версия новее; snapshot_date — в слепке какого
-- модельного дня источник показал строку; _load_id — какой запуск Airflow её -- модельного дня источник показал строку; _load_id — какой запуск Airflow её
-- принял; _load_ts — когда она приехала в хранилище. Ключом сущности не -- принял; _load_ts — когда она приехала в хранилище. Ключом сущности не
-- становится ни одна из трёх последних: они про наблюдение и загрузку. -- становится ни одна из трёх последних: они про наблюдение и загрузку. Версию
-- выбирает только updated_at, поэтому у выжившей после схлопывания строки
-- snapshot_date — один из дней, когда источник показывал эту версию, не
-- обязательно последний (docs/architecture/orders/ingestion.md).
-- --
-- PARTITION BY toDate(created_at) — по дню создания строки в источнике. Он -- PARTITION BY toDate(created_at) — по дню создания строки в источнике. Он
-- неизменен у всех версий заказа, поэтому версии лежат в одной партиции и -- неизменен у всех версий заказа, поэтому версии лежат в одной партиции и
+16 -2
View File
@@ -92,7 +92,16 @@ SELECT
_load_ts _load_ts
FROM stg.orders_raw_dist FROM stg.orders_raw_dist
WHERE _load_id = {load_id:String} AND row_is_valid WHERE _load_id = {load_id:String} AND row_is_valid
SETTINGS distributed_foreground_insert = 1; -- Заводское значение parallel_distributed_insert_select — 2: вставку вида
-- INSERT ... SELECT из Distributed в Distributed на одном кластере ClickHouse
-- исполняет локально на каждом шарде, и строки минуют ключ шардирования цели.
-- Ключи здесь разные — сырьё разложено по хешу строки, заказы по
-- cityHash64(order_id), — так версии одного заказа и расползлись по обоим
-- шардам, где ReplacingMergeTree их уже не схлопывает. Ноль возвращает вставку
-- инициатору: он читает срез со всех шардов и пишет через ods.order_dist,
-- то есть по её ключу.
SETTINGS distributed_foreground_insert = 1,
parallel_distributed_insert_select = 0;
-- Брак: тот же срез и буквальное отрицание того же предиката. -- Брак: тот же срез и буквальное отрицание того же предиката.
-- --
@@ -118,4 +127,9 @@ SELECT
_load_ts _load_ts
FROM stg.orders_raw_dist FROM stg.orders_raw_dist
WHERE _load_id = {load_id:String} AND NOT row_is_valid WHERE _load_id = {load_id:String} AND NOT row_is_valid
SETTINGS distributed_foreground_insert = 1; -- Тот же ноль, хотя здесь заводское значение раскладку не ломало: ключ у
-- ods.order_errors_dist — cityHash64(raw), тот же, что у сырья, и локальная
-- запись попадала бы в свой шард сама. Совпадение ключей не гарантия:
-- поменяется ключ любой из двух таблиц — и строки разъедутся молча.
SETTINGS distributed_foreground_insert = 1,
parallel_distributed_insert_select = 0;