Привет, Хабр! Меня зовут Марк Порошин, я занимаюсь DataScience в DV Group. Недавно я уже рассказывал про то, как начать трансформировать данные с помощью dbt. Сегодня я решил поделиться, как мы в DV Group поженили dbt, Greenplum и DataVault, собрали все грабли, что могли; немного поконтрибьютили в open-source, но по итогу остались очень довольны результатом. Расскажу сначала пару слов о том, что такое DataVault. DataVault - методология построения хранилища, предполагающая высокую нормализацию данных (3ая нормальная форма). Основными ее компонентами являются: hub - “сущность” хранит только первичный и бизнес-ключ; satellite - “свойства сущности”, относятся многие к одному с хабом и хранит свойства сущности; link - “связь между сущностями” - отношение многие ко многим между сущностями (не обязательно двумя). Чтобы стало чуть понятнее, давайте рассмотрим пример. Предположим мы хотим хранить информацию о запусках рекламных кампаний. У нас есть данные о том, когда клиенты запускали кампанию для каких-то товаров. Как же в этом случае может выглядеть ER диаграмма? Можно заметить, что в сателлитах есть поле effective_from и <entity>_hashdiff, благодаря которому в DavaVault реализуется SCD2, это дает возможность реализовывать   “версионность” данных. Больше почитать про Data Vault можно здесь: Документация статья на Хабре 1 статья на Хабре 2 Прежде чем переходить к основной части, хочу дать поделиться статьей, потому что я начинал изучать эту тему именно с нее и во многом статьи будут пересекаться, но я хочу больше сконцентрироваться на деталях, а еще обсудить ошибки, которые мы совершили. Из внешних источников данных мы периодически загружаем историю покупок пользователей в таблицу pure.pure_transactions на Greenplum и хотим преобразовать ее в структуру Data Vault, т.е. разбить данные на хабы, линки и сателлиты. Преобразование происходит в 3 этапа. Сначала нужно подготовить таблицу с данными, которые будут загружаться (мы будем выбирать данные за 1 день). Далее необходимо обогатить данные всеми необходимыми хешами, но об этом дальше. И, наконец, расщепить данные на сущности. Таблица pure.pure_transactions описывает историю покупок пользователей с некоторой метаинформацией. К сожалению, показать ее полностью я не могу, но в рамках статьи нам необходимы только следующие поля: id транзакции(transaction_id); дата транзакции(transaction_date); цена товара(price); количество купленного товара(quantity); наименование товара(product_name); id категории товара(cat_id); Мы выделили из этих данных две сущности транзакция или строчка в чеке (transaction_id); товар (product_name); Теперь когда у нас есть представление о том, чего мы хотим и какие у нас данные, перейдем к самому интересному. Прежде чем начать писать dbt-код, хочу немного рассказать про особенности работы с Greenplum. Greenplum — база построена на основе Postgres, поэтому синтаксис SQL запросов практически полностью совпадает, но есть ряд значительных отличий, которые будут использоваться в дальнейшем, и которые стали причиной реализации отдельного адаптера для dbt. Подробнее про это можно почитать здесь. А еще хочу поделиться интересным докладом, он будет полезен всем, кто начинает работать Greenplum. Функциональность адаптера Важная особенность Greenplum — возможность указать поле дистрибьюции. По этому полю Greenplum будет “раскладывать” данные по сегментам и по этому же полю будут самые эффективные join-ы. Указать параметр можно следующим образом: Сжатие и колоночная ориентация Greenplum предназначен для работы с большими данными, уменьшение времени на чтение/запись за счет сжатия является значительным фактором, который позволяет сократить время выполнения запроса. В dbt при использовании адаптера для Greenplum, это можно имплементировать следующим образом: Здесь мы указали параметр appendonly='true', он позволяет Greenplum создать таблицу оптимизированную для вставок. А еще мы добавили, что хотим использовать колоночную ориентацию orientation='column'. И, наконец, указывали тип сжатия compresstype='ZSTD', который хотим использовать и его параметры compresslevel=4, blocksize=32768. Указанные значения являются параметрами по умолчанию — их можно не прописывать отдельно, если они для вас подходят. Партиционирование Последней важной особенностью является партиционирование, в postgres тоже есть эта возможность, но такой возможности нет у адаптера dbt-postgres(прошу поправить меня, если я ошибаюсь). Партиционирование позволяет разбить таблицу на несколько физических файлов по некоторому условию и читать только необходимые партиции. Из-за того что в Greenplum нельзя настраивать партиционирование, во время создания таблицы с помощью create table as select, реализация этой фичи получилась не очень симпатичной. Требуется указать строчку с определением полей и строчку с определением партиционирования: Сперва нужно выделить данные за день, для этого создали модель raw.raw_transaction: Здесь мы с помощью CTE выбирали данные за один день и дедублицировали по полю transaction_id. После запуска модели в таблице raw.raw_transaction у нас оказались данные за 1 день, если указать соответствующие переменные var('raw_transactions')['start_date'] и var('raw_transactions')['end_date']: К данным в таблице raw.raw_transaction добавили первичные ключи, которые будут использоваться в сущностях DataVault. Мы использовали пакет dbtvault (его пришлось немного доработать, чтобы он поддерживал последнюю версию dbt). Он позволяет сократить количество boilerplate кода. Чтобы установить необходимые зависимости мы добавили в корень dbt проекта файл package.yml со следующим содержанием: и вызвать команду: После этого у вас появится папка dbt_packages, в которой будут находиться исходники установленных пакетов. В модели stage_transactions мы завели переменную yaml_metadata и указываем поля, которые станут основой для ключей. Их существует два типа: Первичные ключи сущностей: хабы и линки; HASHDIFF — хеши для отслеживания изменений в свойствах сущности, которые строятся из полей сателлита. Результатом вызова модели получится таблица stage.stage_transactions в базе данных со следующими полями: transaction_id transaction_date price quantity product_name cat_id transaction_pk transaction_hashdiff product_pk product_hashdiff link_transaction_product_pk load_date effective_from record_source … Теперь у нас есть все необходимые хеши для того чтобы строить хранилище в методологии DataVault. Рассмотрим создание хаба на примере сущности “продукт”, из исходных данных у нас есть его название cleanedname, которое является бизнес-ключом(natural key), на stage стадии мы создали первичный ключ product_pk, а также поле classid, которое является его свойством. Модель хаба product будет выглядеть следующий образом: В начале мы описываем конфигурацию модели указываем схему, тип материализации и ключ дистрибьюции. После этого определяем переменные: source_model — таблицу источник, из которой будет происходить выгрузка данных для пополнения хаба; src_pk — название поля, в котором хранится первичный ключ хаба; src_nk — название поля, в котором хранится бизнес-ключ хаба; load_date — название поля с датой загрузки данных; src_source — название поля, в котором хранится наименование источника данных. И вызываем макрос для генерации кода. В результате появится таблица с следующим DDL: Рассмотрим еще один пример модели хаба. Дело в том, что сущность "транзакция"(сточка чека), в отличие от продукта однозначно относится со временем, когда совершили покупку, и хочется добавить поле incomingdate для того чтобы реализовать партиционирование по нему. У нас на данный момент число транзакций превысило миллиард и обновление хаба без партиционирования занимает несколько часов. Это уже не совсем соответствует подходу DataVault, но следование ему в точности обходилось бы слишком дорого. DDL модели h_transaction: У хаба продуктов есть свойство cat_id, поэтому мы создали сателлит для его хранения: Модель выглядит практически также, как их хаб за исключением трех дополнительных переменных: src_hashdiff — название поля, хранящие хеш данного набора свойств; src_eff — название поля, хранящее дату, с которой данный кортеж актуален; src_payload — список полей, которые составляют свойства данной сущности. Аналогично с хабом нам потребовалось внедрить партиционирования для сателлитов, для этого рассмотрим сателлит для сущности транзакция: Теперь, когда у нас есть два хаба и сателлиты к ним, осталось только создать таблицу, связывающую их. В роли такой сущности в DavaVault служат ссылки. В методологии DavaVault есть возможность создавать сателлиты для link, однако кажется, что если у связи есть свойства, значит можно выделить еще одну сущность. Кстати, насчет связей между более чем двумя сущностями — лично я пришел к выводу, что лучше избегать подобные структуры без острой необходимости, поскольку последующие запросы получаются нетривиальными особенно если в подобной связи встречаются NULL. Я надеюсь, что мне удалось вас убедить, что применение dbt в связке с datavault позволяет сильно облегчить построение хранилища DavaVault. Я буду очень рад замечаниям, вопросам и комментариям, а так же приглашаю присоединиться к улучшению адаптера для Greenplum =) Хочу подвести небольшой итог и выделить основные рекомендации, которые я могу дать на основе совершенных ошибок: Используйте партиционирование везде, где это возможно; Изучите SQL запросы, которые компилирует dbtvault, поскольку иногда их можно оптимизировать в контексте вашей задачи; В greenplum есть потрясающая фича external table так что можно отказаться от хранения исходных данных (pure схема в статье). У меня осталось непокрытой последняя тема о том, как автоматизировать процесс наполнения хранилища с помощью Dagster(развитие Airflow от его же создателей). Если тема актуальна, пишите в комментарии, я расскажу о ней. Data Scientist