Всем привет :) 

Меня зовут Голов Николай, я строю платформу данных на основе Snowflake и Anchor Modeling в ManyChat.

В этой статье я уже подробно рассказывал, как решал аналогичную задачу в Авито, используя Vertica и методологию Anchor Modeling. В комментариях меня спрашивали, с какими сложностями приходится сталкиваться при использовании этой модели, поэтому сегодня я решил поговорить именно о них и заодно поделиться решением, к которому мы пришли.

Дальнейший рассказ требует от читателя знакомства с концепциями моделирования реальности, предлагаемыми в методологиях Data Vault (Дэн Линстедт) и Anchor Modeling (Ларс Ронебак). Методологии две, но так как в вопросе, который мы планируем рассмотреть, они очень близки, достаточно знать одну. Если кто-то хочет предварительно освежить свои знания, вот два свежих видео, где я подробно рассказываю об этих концепциях: Data Vault ,Переход к AnchorModeling/Data Vault 2.0.

Дилемма

Итак, с вводной частью покончено, перейдем к делу. 

Data Vault/Anchor Modeling позволяют моделировать реальность. Прежде всего, объекты реального (людей, машины, товары) и не совсем реального (чеки, email-сообщения, IP-адреса) мира. Сначала я хотел перечислить все эти объекты как объекты реального мира, но подумал, что это может кого-то смутить, поэтому  email-сообщения уехали в отдельную подкатегорию, хотя для меня, как и для Data Vault/Anchor Modeling, они все — одинаковые объекты одного реального мира проглотившего цифровую реальность.В Data Vault объекты получают в соответствие таблицы типа Hub, в Anchor Modeling — таблица типа Anchor. 

Давайте теперь еще раз посмотрим на наш реальный мир с его цифровыми нюансами. 

Представьте, что у нас есть сайт , который посещают пользователи. Можем ли мы сказать, что событие посещения сайта — это объект реального мира? Или это некоторая связка объектов реального мира: пользователь + страница + время посещения? Приведенная выше альтернатива является не абстрактной философской дилеммой, а важным вопросом моделирования: события ClickStream — это Hub/Anchor или Link/Tie?

В Авито выбор был сделан в пользу Hub/Anchor, то есть событие кликстрима было признано объектом. Это крайне помогло в вопросе хранения атрибутов, так как количество опциональных атрибутов события кликстрима выросло с 50 в 2014 до 400+ в 2019 году. Сейчас их, наверное, еще больше. Но в остальном такой подход всё больше ощущался как искусственный: объект «событие ClickStream» очень сильно отличается по своему жизненному циклу от таких объектов, как «объявление», «платеж», «пользователь», как минимум тем, что этот объект никогда не меняется, так как любое событие происходит однократно. 

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

Миллиарды событий прилетали каждый день, их приходилось сначала нормализовывать, разбивать на сотни таблиц Anchor Modeling, а потом собирать обратно в витрину click_stream. Получается какая-то артель «Напрасный труд».

Разница между объектом и событием

Представим, что событие ClickStream — это не объект, а связка объектов, то есть Link/Tie.

В Anchor Modeling (Tie) было бы совсем плохо, так как  400+ опциональных атрибутов было бы некуда повесить. Anchor Modeling запрещает вешать Attribute и Tie на Tie.

Но даже в чистом Data Vault (Link), где на Link можно навесить сколько угодно Sattelite и других Link, было бы сложно. Модель в таком случае, упрощенно, выглядела бы так: 

Однако если событие ClickStream = Link (пользователь + страница + время посещения), то в загрузке надо обязательно пройти следующие шаги: 

0. Прибывают данные

1. Load all Hubs (посетитель + страница + время посещения)

2. Load ClickStream Link

3. Load all Sattelites/Links, ссылающиеся на ClickStream link

4. **

Происходит минимум 3 шага загрузки.

Ну, и в конце концов, не отказываться же от всех плюсов Anchor Modeling и не возвращаться же в Data Vault только из-за одно неидеального случая. 

Год назад, столкнувшись в ManyChat c этой проблемой, я решил попытаться решить её по-новому. 

Может быть, События стоит рассмотреть как нечто фундаментально отличное от Объекта? В самом деле, Объект, если подумать, может меняться со временем. Объект — это цепочка его состояний в течении времени, сшитых вместе некими идентификаторами, вроде номера паспорта для человека или адреса для квартиры.  

Событие же происходит однократно, и не меняется с течением времени. Событие — это «замороженное» состояние. 

Если сделать ещё один шаг в прошлое по линейке методологий моделирования реальности к Dimensional Modeling, можно сказать, что Событие — это Факт (Fact), а Объект — это Измерение (Dimension). Такая параллель служит скорее иллюстративным целям, с ней надо аккуратнее, но возможно, она кому-то поможет понять суть. 

Давайте разберем ещё один практический пример. У нас есть Клиент, покупающий Товар. Клиент и Товар, это, понятное дело, Объекты (Anchor/Hub/Dimension). А что с событием покупки? На первый взгляд, это похоже на Событие (Fact/Link/Tie).

Но если посмотреть глубже, у Покупки есть история и состояния. Покупка может быть инициирована, оплачена, доставлена и финализирована. И у Покупки может быть сшивающий идентификатор — номер чека.

То есть, не смотря на то, что в Dimensional Modeling Покупка = Fact, в Anchor Modeling/Data Vault это скорее Hub/Anchor. А что с событийным рядом вокруг Покупки: создание, редактирование, смена товара, смена клиента, оплата, доставка, потеря, повтор, финализация? Радикальный Anchor Modeling потребует разложить её так же, как Anchor, идентифицируя исключительно подмножеством ее свойств: Покупка, Тип события, Дата события. 

Тут мы снова получаем вырожденный Anchor, схожий с событием ClickStream: большой объем (на каждую покупку — N событий), множество усилий на нормализацию, а потом, в ходе анализа, обязательная денормализация обратно в витрину.

Думаю, приведенные примеры достаточно проиллюстрировали разницу между Объектом и Событием. Объект и Событие оба обладают атрибутами и связями. При этом Объект историчен, нам важна связь его состояний. Например, Объект = Человек. У человека может быть множество атрибутов (пол, рост, вес, паспорт), каждый из которых может меняться или отсутствовать. Но человек останется тем же, даже если у него поменяются вообще все атрибуты. С точки зрения моделирования нам важно построить связь состояний, не смотря на смену конкретных атрибутов, поэтому в задаче представления Объекта Anchor идеален (именно Anchor, Hub немного уступает).

Давайте теперь попробуем адекватно смоделировать Событие (событие ClickStream, событие с покупкой). Событие неисторично, оно не обладает цепочкой состояний. Событие обладает атрибутами и связями, при этом есть подмножество атрибутов/связей, позволяющих его идентифицировать. В данном случае «идентифицировать»  значит «дедуплицировать», не более того. Как максимально адекватно смоделировать событие, избежав бессмысленной работы?  В ManyChat мы применили концепцию стримов (Stream), дополняющую такие основные сущности Anchor Modeling как Anchor/Tie и Attribute. 

Что такое Stream

Давайте рассмотрим пример с событиями покупки, которые идентифицируются номером покупки, типом события и датой события, но при этом обладают множеством других атрибутов и связей. 

С точки зрения загрузки, это обычный Hub, который использует в качестве внешнего ключа массив идентифицирующих Атрибутов/Связей: {Номер покупки (внешний), Тип события (внешний), Дата события}.

Но далее этот Hub проходит трансформацию в новую сущность Stream, похожую на Link/Tie, и содержащую в виде отдельных столбцов все идентифицирующие атрибуты и все связи развернутые в суррогатные ключи. 

Пошагово загрузка выглядит следующим образом: 

0) Событие покупки приходит как запись вида:  {Номер покупки, Тип события, Датой события, Сумма, Код курьера,...}. 

Здесь все ключи — внешние.

1) Загружаются хабы: Покупка (номер покупки), ТипСобытия (тип события), Курьер (код курьера) + наш особый хаб СобытиеПокупки ({номер покупки, тип события, дата события}). 

В каждом хабе формируются недостающие суррогатные ключи. 

2) В параллель загружаются атрибуты и связи всех хабов, в том числе атрибуты и связи для СобытиеПокупки (связь на Курьера, атрибут Сумма). 

Также в параллель загружается Stream, S_СобытиеПокупки, которое содержит следующие столбцы: {событиеПокупки_id (суррогатный ключ), номер покупки (суррогатный), тип события (суррогатный), дата события}.

Как видите, приведенный подход позволяет грузить События в те же 2 (два) этапа, как и классический Anchor Modeling, автоматически получая в объекте стрима (S_) минимальное денормализованное ядро для витрин. Хотя, строго говоря, шестая нормальная форма для объектов Stream не выполняется.

В ManyChat концепция стримов применяется уже год, наши самые большие данные — это как раз стримы событий: События контактов, События ботов, События визитов, События пользователей. Некоторые из приведенных стримов содержат сотни миллиардов записей, но работать с ними по-прежнему удобно.

В общем, вот. Как и с любыми аспектами моделирования данных, когда описываешь концепцию стримов, кажется, что изобретаешь велосипед, и всё это давно всем известно. Надеюсь, структурирование этого подхода кому-то поможет.

Буду рад идеям и конструктивным предложениям :)

Комментарии (2)


  1. SergeyProkhorenko
    27.01.2022 21:21
    +1

    Практика показывает, что классический Anchor Modeling, несмотря на все его несомненные достоинства для ядра (детального слоя) хранилища данных, бывает очень неудобен в некоторых важных случаях. Например, в эту методологию не укладываются такие вещи, связанные с событиями, как таблица бухгалтерских проводок, лог (журнал), очередь сообщений. Поэтому Anchor Modeling модифицируют. Примеры:

    1. Эта статья

    2. https://habr.com/ru/users/sergeyprokhorenko/posts/

    3. https://habr.com/ru/company/yandex/blog/557140/


  1. SergeyProkhorenko
    30.01.2022 12:52

    Я добавил к своей статье комментарий на эту статью Николая Голова