feat(dds): добавлена карта идентичностей

- Зачем:
  - реализованы требования issue #153 к мосту между кукой и пользователем.
- Что:
  - добавлены физическая карта, точное представление и полный пересчёт через GLOBAL JOIN.
  - задача identity_map поставлена после сборки заказов в dds_transform.
  - устройство и живые проверки карты записаны в документации хранилища.
- Проверка:
  - make lint, make smoke, make check-clickhouse, make check-services.
  - три запуска dds_transform: 4770 физических строк и 1590 уникальных пар в _v.
This commit is contained in:
ddadmin committed 2026-08-26 12:30:40 -04:00
1 parent cdd44c469f
commit 39ff0b5bb4
5 files changed
+124 -21

No files matched your search

+19 -9
View File
@@ -1,18 +1,18 @@
"""Трансформации слоя DDS: сборка модели заменой дневных партиций. """Трансформации слоя DDS: сборка моделей.
Даг собирает модель поверх принятого ODS. Сущностей у него две — заказ и Заказ и сессия собираются заменой дневных партиций. Карта идентичностей
сессия, зависимости по данным между ними нет; карта идентичностей встанет пересчитывается целиком после заказа, потому что читает его текущую модель.
соседней группой задач.
Расписания у дага нет и параметров тоже: объём каждого прогона он выводит сам Расписания у дага нет и параметров тоже: объём каждого прогона он выводит сам
из состояния слоёв и позиции мира, поэтому один и тот же запуск одинаково из состояния слоёв и позиции мира, поэтому один и тот же запуск одинаково
верен из триггера дневного конвейера, с кнопки менти и после падения верен из триггера дневного конвейера, с кнопки менти и после падения
(docs/architecture/etl.md, «Объём прогона»). (docs/architecture/etl.md, «Объём прогона»).
Повтор прогона гасит не колонка версии, а замена партиции целиком: день Заказ и сессия гасят повтор заменой партиции целиком: день собирается в
собирается в таблицу-двойник и подменяет собой партицию цели. Почему слой таблицу-двойник и подменяет собой партицию цели. Карта пишет полный набор пар
устроен так — sql/ddl/45-dds-tables.sql; механика самой подмены — новой версией, а точное чтение даёт FINAL. Замена партиции разобрана в
sql/dds/order_replace.sql. sql/dds/order_replace.sql; точное чтение карты описано в
sql/ddl/50-dds-views.sql.
""" """
from __future__ import annotations from __future__ import annotations
@@ -229,8 +229,18 @@ def dds_transform():
rebuild >> replace rebuild >> replace
order() orders = order()
session() session()
identity_map = SQLExecuteQueryOperator(
task_id="identity_map",
conn_id=CLICKHOUSE_CONNECTION,
sql="dds/identity_map_rebuild.sql",
parameters={"load_id": "{{ run_id }}"},
do_xcom_push=False,
)
orders >> identity_map
dds_transform() dds_transform()
+12 -5
View File
@@ -587,6 +587,8 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
| `dds` | `dds.session_rep` / `_dist` | визит, нарезанный слоем; партиция — день сессии | | `dds` | `dds.session_rep` / `_dist` | визит, нарезанный слоем; партиция — день сессии |
| `dds` | `dds.session_stage_rep` / `_dist` | двойник сессии: в нём день собирается перед заменой партиции | | `dds` | `dds.session_stage_rep` / `_dist` | двойник сессии: в нём день собирается перед заменой партиции |
| `dds` | `dds.session_v` | публичный договор сессии | | `dds` | `dds.session_v` | публичный договор сессии |
| `dds` | `dds.identity_map_rep` / `_dist` | известная пара куки с пользователем; таблицы полного пересчёта |
| `dds` | `dds.identity_map_v` | точный публичный договор карты идентичностей с `FINAL` |
| `dic` | `dic.products_file` | чтец CSV-каталога, общего с генератором | | `dic` | `dic.products_file` | чтец CSV-каталога, общего с генератором |
| `dic` | `dic.products` | словарь товаров поверх подложки | | `dic` | `dic.products` | словарь товаров поверх подложки |
@@ -594,11 +596,11 @@ SQL-файлы исполняет штатный `SQLExecuteQueryOperator`. Пр
`INSERT SELECT` одной задачи `parse_batch` из файла `INSERT SELECT` одной задачи `parse_batch` из файла
`sql/ods/order_parse.sql`. `sql/ods/order_parse.sql`.
В слое DDS построены событие, заказ и сессия, и устроены они намеренно В слое DDS построены событие, заказ, сессия и карта идентичностей. Событие
по-разному: событие остаётся представлением над ODS, у заказа и сессии — остаётся представлением над ODS; заказ и сессия гасят повтор заменой партиции;
физические модели. Карту идентичностей и слой DM принесут следующие тикеты карта пишет полный набор пар в версионную таблицу. Слой DM принесут следующие
этапа. Их состав задан разделом 7 мастер-спеки и переносится сюда по мере тикеты этапа. Его состав задан разделом 7 мастер-спеки и переносится сюда по
постройки. мере постройки.
## Что проверено ## Что проверено
@@ -818,6 +820,11 @@ Kafka с пустым значением (ноль байт) и запись-н
двойник разложила 1662 заказа строго по ключу: шард 1 получил только чётные двойник разложила 1662 заказа строго по ключу: шард 1 получил только чётные
`cityHash64(order_id)`, шард 2 — только нечётные. `cityHash64(order_id)`, шард 2 — только нечётные.
**Проверка карты идентичностей 26 августа 2026 года (#153).** Context7
подтвердил версию в `ReplacingMergeTree` и точное чтение через `FINAL`. Два
полных прогона оставили 3180 физических строк и 1590 пар в `_v`; все пары
подтверждены покупкой и заказом, а строк на чужом шарде нет.
Тем же днём измерены ещё три поведения ClickHouse. Тем же днём измерены ещё три поведения ClickHouse.
- **Параметр в `ALTER … PARTITION` не подставляется.** Запрос с `{day:Date}` в - **Параметр в `ALTER … PARTITION` не подставляется.** Запрос с `{day:Date}` в
+33 -7
View File
@@ -1,11 +1,7 @@
-- DDS: физические модели слоя и доноры их партиций. -- DDS: физические модели слоя и доноры партиций.
-- --
-- Идемпотентность здесь устроена иначе, чем в приёме, и это главный урок обеих -- Заказ и сессия гасят повтор заменой дневной партиции, без колонки версии.
-- сущностей файла. В ODS повтор гасит колонка версии: ReplacingMergeTree по -- Карта идентичностей пересчитывается целиком и пишет новую версию пары.
-- snapshot_date оставляет строку позднейшего слепка. Здесь колонки
-- версии нет вовсе — повтор гасит замена партиции целиком: день собирается
-- заново и подменяется одним атомарным движением. Поэтому строка таблицы
-- ровно одна на своё зерно, а чтению не нужен FINAL.
-- Локальная таблица заказа. -- Локальная таблица заказа.
-- --
@@ -170,3 +166,33 @@ ORDER BY (client_id, started_at);
CREATE TABLE IF NOT EXISTS dds.session_stage_dist ON CLUSTER clickstream_cluster CREATE TABLE IF NOT EXISTS dds.session_stage_dist ON CLUSTER clickstream_cluster
AS dds.session_stage_rep AS dds.session_stage_rep
ENGINE = Distributed('clickstream_cluster', 'dds', 'session_stage_rep', cityHash64(client_id)); ENGINE = Distributed('clickstream_cluster', 'dds', 'session_stage_rep', cityHash64(client_id));
-- DDS: известная связь куки трекера с пользователем магазина.
--
-- Полный запуск снова пишет все известные пары. Физические повторы до
-- фонового слияния допустимы: _load_ts выбирает последнюю версию пары.
-- Партиции нет, потому что карта пересчитывается целиком и её зерно не
-- связано с днём.
CREATE TABLE IF NOT EXISTS dds.identity_map_rep ON CLUSTER clickstream_cluster
(
client_id UInt64,
user_id UInt64,
_load_id String,
_load_ts DateTime64(3, 'UTC')
)
ENGINE = ReplicatedReplacingMergeTree(
'/clickhouse/tables/{shard}/{database}/{table}',
'{replica}',
_load_ts
)
ORDER BY (client_id, user_id);
-- Карта лежит по ключу будущего соединения с событиями и сессиями.
CREATE TABLE IF NOT EXISTS dds.identity_map_dist ON CLUSTER clickstream_cluster
AS dds.identity_map_rep
ENGINE = Distributed(
'clickstream_cluster',
'dds',
'identity_map_rep',
cityHash64(client_id)
);
+14
View File
@@ -134,3 +134,17 @@ SELECT
_load_id, _load_id,
_load_ts _load_ts
FROM dds.session_dist; FROM dds.session_dist;
-- DDS: точная известная связь куки с пользователем магазина.
--
-- Полный пересчёт оставляет физические версии одной пары до слияния частей.
-- FINAL прячет это устройство от читателя; явный список колонок не даёт
-- служебным изменениям таблицы случайно расширить договор.
CREATE VIEW IF NOT EXISTS dds.identity_map_v ON CLUSTER clickstream_cluster
AS
SELECT
client_id,
user_id,
_load_id,
_load_ts
FROM dds.identity_map_dist FINAL;
+46
View File
@@ -0,0 +1,46 @@
-- DDS: полный пересчёт известных пар куки с пользователем.
--
-- События разложены по client_id; заказы по order_id. Ключи соединения не
-- ко-локированы, поэтому правило слоя требует явный GLOBAL. С подзапросами с
-- обеих сторон ClickHouse 26.3 выполняет соединение один раз на инициаторе
-- (docs/research/2026-08-22-distributed-join.md).
INSERT INTO dds.identity_map_dist
(
client_id,
user_id,
_load_id,
_load_ts
)
SELECT
client_id,
user_id,
{load_id:String},
now64(3, 'UTC')
FROM
(
SELECT DISTINCT
purchase.client_id,
orders.user_id
FROM
(
SELECT
client_id,
-- По договору события purchase_id содержит ровно один номер
-- заказа: одно подтверждение оформляет один заказ (спека, раздел 4).
purchase_id[1] AS order_id
FROM dds.event_v
WHERE event_type = 'purchase'
) AS purchase
GLOBAL INNER JOIN
(
SELECT
order_id,
user_id
FROM dds.order_v
) AS orders ON purchase.order_id = orders.order_id
)
-- В этой форме ClickHouse 26.3 уже пишет через инициатор. Явный ноль фиксирует
-- раскладку по ключу цели, если форма запроса или план изменятся (капкан #137).
SETTINGS distributed_foreground_insert = 1,
parallel_distributed_insert_select = 0;