Files
ddadmin b839a671a1 docs(labs): добавлены история связей и инкрементальный пересчет
Зачем: дать практику учета новых связей пользователей при обновлении
прежних дней витрины.

Что: добавлены две связанные лабораторные с наблюдениями через Airflow,
собственными таблицами student и повтором частично записанной загрузки.
Обновлены программа, индекс и ссылки из справочников DDS и DM.

Проверка: DDL и SQL под student на ClickHouse 26.3.17.56, четыре модельных
дня через Airflow, полные сверки и повтор незавершенной загрузки;
config-test, lint, локальные ссылки и независимое ревью.
2026-09-22 05:57:11 -04:00

9.1 KiB
Raw Permalink Blame History

Карта идентичностей в DDS

Учебный результат: менти строит физический мост между кукой трекера и пользователем магазина, раскладывает его по ключу будущего соединения и видит, как пакетный полный пересчёт сочетается с точным публичным представлением.

Назначение

Карта — адресная книга: «эта кука уже покупала как этот пользователь магазина». Связь становится известна только после покупки, потому что в кликстриме есть client_id, а user_id живёт в заказе. Две куки одного покупателя дают две строки с одним user_id; поэтому витрина может показать рядом число посетителей по кукам и число известных людей после склейки.

Это не полная клиентская база. Куки без покупки остаются анонимными, а карта не пытается угадать их владельца.

Зерно и состав строки

Одна строка — известная пара client_id × user_id. Деловых колонок две:

  • client_id UInt64 — кука трекера;
  • user_id UInt64 — пользователь магазина.

Собственные служебные колонки _load_id String и _load_ts DateTime64(3, 'UTC') показывают, каким запуском и когда пара записана в DDS. Они имеют читателя при расследовании повтора и потому входят в публичный договор вместе с деловой парой.

first_matched_at нет. Запланированные витрины его не читают, а два возможных смысла — «первая покупка вообще» и «момент, с которого связь была известна хранилищу» — требуют разных исторических моделей. Этот вопрос оставлен самостоятельной работе, а текущий стенд не выдаёт один смысл за другой.

Статус заказа связь не меняет: любой заказ, сопоставленный с событием покупки, показывает владельца куки. Отмена меняет состояние заказа, но не личность покупателя.

Физическая модель и публичный интерфейс

  • dds.identity_map_rep — локальная ReplicatedReplacingMergeTree(_load_ts), ORDER BY (client_id, user_id), без партиционирования;
  • dds.identity_map_dist — распределённая поверхность записи, шардирование по cityHash64(client_id);
  • dds.identity_map_v — публичное обычное представление над _dist, которое перечисляет четыре колонки явно и применяет FINAL.

Повторы полного запуска физически могут лежать рядом до фонового слияния. FINAL оставляет одну последнюю версию пары, поэтому пользователь всегда читает точную карту через _v. Роли объектов и доступ следуют общей конвенции, «Имена объектов».

Шардирование совпадает с событиями и сессиями. Благодаря этому витрина может обогащать их картой локально по client_id; точную форму распределённого запроса решает проектирование витрин #135.

Сборка

Карта собирается один раз в суточной пакетной цепочке после обновления модели заказов и до зависимых витрин. При догоняющем запуске за несколько дней хватает одной сборки после всей пачки заказов.

Источники — вся доступная история dds.event_v и текущее состояние заказов dds.order_v. Трансформация:

  1. оставляет события purchase и разворачивает единственный purchase_id;
  2. соединяет его с order_id заказа через GLOBAL INNER JOIN;
  3. выбирает различные пары client_id, user_id;
  4. добавляет координаты текущего запуска и пишет полный набор в dds.identity_map_dist.

GLOBAL нужен потому, что события разложены по client_id, а заказы — по order_id: строки соединения не ко-локированы. Правая сторона мала, поэтому рассылка заказов приемлема. Материализованное представление здесь не подходит: заказ может появиться в пакетном слепке позже уже принятого события покупки.

Повтор запуска

Каждый запуск заново получает все известные пары из источников. Он не читает предыдущую карту и не делает anti-join «вставить только отсутствующее». _load_ts служит версией ReplacingMergeTree, а dds.identity_map_v прячет физические повторы через FINAL.

Полный пересчёт выбран потому, что карта стенда мала, источники уже содержат нужную историю, а повтор задачи не зависит от того, на каком месте оборвался предыдущий. Мир стенда не отзывает связь куки с пользователем, поэтому удалять исчезнувшие пары не требуется.

Читатели

  • dm.daily_traffic_v — посетители по client_id и известные пользователи по user_id рядом;
  • учебные запросы — показать, как несколько кук одного человека меняют uniq;
  • расследование пакетного запуска — _load_id и _load_ts текущей версии пары.

Отклонённые варианты

  • first_matched_at в текущей модели. Нет запланированного читателя и нет однозначной временной семантики; идея передана в issue #145.
  • Словарь ClickHouse. Для малой карты мог бы ускорить точечное обогащение, но спрятал бы физическую модель и не добавил нового урока рядом с уже существующим словарём товаров.
  • Одно динамическое представление без физической карты. Повторяло бы соединение всей истории при каждом чтении, не давало ко-локации результата и не показывало устройство реального слоя хранения.
  • Инкремент через anti-join с самой картой. Делал бы результат предыдущего запуска обязательным входом и усложнял повтор без пользы на объёме стенда.
  • Материализованное представление от событий. Не увидело бы заказ, который приехал позже события покупки.

Связанные решения