Как мы перестали писать SQL руками и автоматизировали Data Vault 2.0 на основе метаданных

от автора

Привет, Хабр! Меня зовут Алексей Миронов, я главный разработчик отдела разработки хранилищ данныз в «Газпром ЦПС». Сегодня я расскажу о своем опыте внедрения Data Vault в компании.

Методология Data Vault 2.0 на бумаге выглядит безупречно, особенно если ваша команда живёт по Agile. Разделение данных на Хабы, Линки и Сателлиты позволяет расширять хранилище инкрементально. Появился новый источник или изменилась бизнес-логика? Просто достраиваем новые блоки рядом, не ломая старые сущности и не переписывая половину DWH, как это часто бывает в классической архитектуре Кимбалла.

Кроме того, Data Vault даёт чёткие правила игры: стандарты генерации объектов, расчёта хэш-ключей и версионирования истории здесь прописаны до нас. Это полностью убирает «творчество» отдельных инженеров — вся команда пишет код в едином стандарте.

Но когда дело доходит до практики, начинаются сложности. Ручное проектирование однотипных таблиц быстро превращается в ад. В этой статье я расскажу, как мы наступили на все классические грабли ручного Data Vault и как написали собственный гибкий фреймворк автоматизации.

Ожидания vs Реальность: с какими болями мы столкнулись

К проекту мы решили подойти основательно. Подготовку начали с теории: специально купили легендарную книгу Дэна Линстеда «Building a Scalable Data Warehouse with Data Vault 2.0» в оригинале. Честно прочитали (признаюсь, местами сильно по диагонали) и, вооружившись академическими знаниями, бесстрашно ринулись в бой с реальными данными.

На бумаге всё выглядело гладко, но как только книжная теория столкнулась с продакшен-выгрузками, мы моментально упёрлись в классические проблемы роста:

·        Отсутствие практического опыта. Одно дело — читать теорию, и совсем другое — раскладывать грязные бизнес-данные по Хабам и Сателлитам. У команды не было набитых шишек, поэтому правила моделирования приходилось нащупывать на ходу.

·        Распределение атрибутов. Мы постоянно спорили: «В какой именно сателлит положить конкретное поле? Делать под него отдельный сателлит по скорости изменения или объединить с базовым?» Ошибки в таких решениях приводили в архитектурные тупики.

·        Бесконечный рефакторинг опытным путём. Ошибки проектирования вскрывались только после запуска пайплайнов. Опишем модель, запустим, поймём, что логика хромает, удалим таблицы, перепишем SQL руками — и по новой.

·        Человеческий фактор. При ручном написании DDL инженеры регулярно забывали добавить технические поля, путали типы или пропускали индексы на исторические интервалы.

Когда цикл «написал руками SQL -> ошибся -> дропнул -> переписал» повторился в тридцатый раз, стало очевидно: рутина убивает всю гибкость Agile. Команда тратит время на монотонный SQL-код вместо проектирования архитектуры. Процесс нужно было срочно автоматизировать.

Почему не Automate_dv, Vaultspeed и другие готовые решения?

Мы изучили рынок, но сознательно отказались от внешних инструментов по трём причинам:

1.      Безопасность и изоляция. В корпоративном контуре нельзя просто взять сторонний фреймворк — он требует аудита, проверки зависимостей и согласований с ИБ. Наш движок на чистом PL/pgSQL работает внутри СУБД, не требует внешних доступов и полностью прозрачен.

2.      Комплаенс и санкционные риски. Проприетарные зарубежные решения (Vaultspeed) отпали из-за геополитики и отсутствия поддержки. Опенсорсные плагины для dbt тоже не прошли бы внутренние бюрократические фильтры — легализация заняла бы месяцы. Свой инструмент оказался быстрее и безопаснее.

3.      Стоимость владения и контроль. У нас нет бюджета на внешних консультантов и интеграторов. Любое готовое решение требует доработок и сопровождения, а разбираться в чужом коде за свои деньги — невыгодно. Собственный движок мы контролируем полностью, чиним и дорабатываем силами пары внутренних инженеров без лишних расходов.

Архитектура, управляемая метаданными (Metadata-Driven)

Мы решили полностью отвязать проектирование бизнес-модели от написания SQL. Нужна была система, где можно изменить структуру объекта в одном месте, запустить одну функцию — и база данных сама перестроится под новые требования.

Отказались от тяжёлых внешних фреймворков и спроектировали лаконичную реляционную схему метаданных из четырёх таблиц в схеме meta:

·        meta.project — верхнеуровневый контейнер для доменов данных. Хранит имя проекта и имя целевой схемы в БД, где будут создаваться физи1ческие таблицы.

·        meta.entity — описание целевой таблицы. Для нашего движка сущность — это любой физический объект Data Vault. Допустимые типы строго ограничены: HUBLNK или SAT. Важнейшее поле здесь — порядковый номер (ord_no), который задаёт последовательность создания и удаления объектов.

·        meta.entity_columns — атомарный состав физического слоя: имена колонок, их типы, длины и алиасы.

·        meta.entity_links — таблица связей, которая через внешние ключи указывает, как объекты связаны между собой (какие Хабы объединяет Линк, или к какому объекту привязан Сателлит).

Сердце движка: генераторы DDL на PL/pgSQL

В качестве инструмента автоматизации мы выбрали нативный PL/pgSQL. Это позволило выполнять генерацию DDL «на лету» прямо внутри СУБД, без привлечения внешних скриптов.

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

1. Генерация Хабов
Функция вычитывает бизнес-ключи из метаданных, склеивает их в единую строку с проверкой типов данных, автоматически добавляет системные поля Data Vault 2.0 (хэш-ключ хаба, системное время загрузки, источник записи и идентификаторы процессов) и выполняет динамическое создание таблицы. Также автоматически вешается индекс на поле времени загрузки для будущих инкрементов.

2. Генерация Линков
Линк связывает таблицы «многие ко многим». Наш генератор обращается к метаданным связей, находит все ассоциированные Хабы, динамически формирует UUID-колонки для их хэш-ключей и прописывает строгие FOREIGN KEY на родительские таблицы. Система автоматически учитывает дескрипторы связей, если один и тот же хаб участвует в линке несколько раз в разных бизнес-ролях.

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

Оркестрация деплоя: управляемый хаос

Объекты Data Vault нельзя создавать или удалять в случайном порядке — СУБД моментально заблокирует операцию из-за нарушений внешних ключей. Для решения этой проблемы мы написали две функции оркестрации, завязав их на поле порядкового номера (ord_no).

Когда нужно полностью пересоздать структуру проекта (например, после изменения гипотез моделирования), мы сначала запускаем функцию очистки. Она читает метаданные и аккуратно удаляет таблицы в обратном порядке (от большего ord_no к меньшему). Сначала уничтожаются зависимые сателлиты, затем линки и только потом — независимые хабы.

Вслед за очисткой запускается функция деплоя проекта. Она идёт по прямому порядку (по возрастанию ord_no). Здесь мы применили динамический полиморфизм метапрограммирования. Вместо громоздких ветвлений имя исполняемой функции-генератора собирается на лету из строки типа сущности в метаданных: движок сам понимает, какую именно функцию (f_create_hubf_create_lnk или f_create_sat) вызвать для текущей сущности.

Вишенка на торте автоматизация DML-слоя: расчёт Hash Key и Hash Diff для массива атрибутов

В методологии Data Vault 2.0 расчёт хэш-ключей и хэш-диффов — это основа. Чтобы получить эталонный хэш, нужно взять набор бизнес-ключей (или описательных колонок для сателлита), применить трансформации, склеить через специальный разделитель.

Нам требовался универсальный инструмент, который умеет делать это для любого массива атрибутов произвольного состава и длины, на лету подстраиваясь под структуру конкретной таблицы. Штатные функции СУБД вроде приведения всей строки к тексту здесь категорически не подходят: они чувствительны к физическому порядку колонок и ломаются на NULL-значениях. Готовых аналогов мы не нашли, поэтому написали свою связку.

select md5(       lower(                    'Постгрессов Констрэйнтин Вакуумович' ||'^'||                    'Датаинженер'             ))md5                             |--------------------------------+0d6e25195d529dca6723ed5511dd84ed| -- если изменить порядок ключей, то получим другой hkeyselect md5(       lower(             'Датаинженер'||'^'||             'Постгрессов Констрэйнтин Вакуумович'       ))md5                             |--------------------------------+8344090c20bb1b420c2856cd7ba4bddb|

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

·        Сборка эталонной строки изменений. Функция разворачивает входящий массив в плоскую структуру. Внутри агрегатора происходит жёсткая сортировка элементов по порядковому номеру и имени. Это самое главное: в каком бы порядке атрибуты ни пришли на вход, строка изменений всегда соберётся в строго детерминированной последовательности. Все значения очищаются от пробелов, приводятся к нижнему регистру и защищаются от NULL-значений, склеиваясь через разделитель ^.

·        Генерация эталонного UUID. Использует ту же логику сортировки, но на финальном шаге оборачивает полученную эталонную строку в алгоритм хэширования, возвращая компактный и чистый UUID.

selectmeta.make_hash_key(       array[             row(0,'position', 'Датаинженер')::hash_data,             row(0,'fio', 'Постгрессов Констрэйнтин Вакуумович')::hash_data       ])make_hash_key                       |------------------------------------+0d6e2519-5d52-9dca-6723-ed5511dd84ed| -- теперь если меняем порядок ключей – hkey не изменяетсяselectmeta.make_hash_key(       array[             row(0,'fio', 'Постгрессов Констрэйнтин Вакуумович')::hash_data,             row(0,'position', 'Датаинженер')::hash_data       ])make_hash_key                       |------------------------------------+0d6e2519-5d52-9dca-6723-ed5511dd84ed|

No-code для архитекторов и аналитиков: FastAPI + SQLAdmin

Когда движок генерации DDL и хэшей на PL/pgSQL был отлажен, мы столкнулись с новым вызовом. Нашим архитекторам и аналитикам нужно было как-то наполнять эту базу метаданных.

Заставлять людей писать ручные INSERT-ы (генерировать с помощью Excel) или связывать ключи через сырые ID таблиц — это путь к новым ошибкам. Нужен был удобный интерфейс, где можно кликами собирать спецификации будущих таблиц.

Чтобы не тратить месяцы на разработку фронтенда, мы пошли по пути быстрого прототипирования и склепали лёгковесный no-code интерфейс. В качестве бэкенда взяли FastAPI, для визуальной части — SQLAdmin, которая автоматически генерирует админ-панель на основе моделей SQLAlchemy.

За пару дней мы реализовали полноценный визуальный CRUD для всех таблиц метаданных. Теперь аналитик или архитектор заходит в браузер и в удобной форме:

·        создаёт или выбирает Проект;

·        добавляет Сущность, выбирая её тип (HUBLNK или SAT) из выпадающего списка;

·        накликивает Колонки и их типы данных, не боясь ошибиться в синтаксисе;

·        визуально настраивает Связи (Links), выбирая мышкой, какие Хабы должны объединяться.

Как только спецификация готова, архитектор нажимает кнопку «Передеплоить проект», и под капотом запускается наш оркестратор. Спецификация из админки мгновенно превращается в физические таблицы Data Vault в нужной схеме. Это окончательно стёрло барьер между проектированием модели и её воплощением в базе.

Чего мы добились в итоге

Внедрение собственного лёгковесного фреймворка полностью изменило рабочий процесс команды:

·        Скорость выросла кардинально. Проблема долгого рефакторинга «опытным путём» ушла. Если понимаем, что ошиблись со структурой или распределением полей, просто правим строки в no-code интерфейсе и за пару секунд перезапускаем деплой. На выходе — идеально чистая, обновлённая структура. Сейчас наш список содержит порядка 100 сущностей, данные из 4 доменов.

·        Тотальная стандартизация. Полностью исключён человеческий фактор. База данных генерируется по единому стандарту — со всеми техническими полями, хэш-диффами, констрейнтами и правильными индексами.

·        Фокус на важном. Мы перестали быть «машинистками», набивающими терабайты SQL-кода. Инженеры данных наконец сосредоточились на архитектуре, качестве данных и проектировании бизнес-моделей.

Что дальше? В перспективе — автоматическая генерация витрин

Наш фреймворк уже закрывает 100% задач по созданию таблиц (DDL) и подготовке хэшей (DML) для ядра Data Vault. Но мы смотрим дальше. Следующий логичный шаг — автоматизация сборки бизнес-витрин (Business Vault / Information Marts).

Здесь мы уже нащупали алгоритм, который можно и нужно унифицировать. Главная боль при сборке витрин над классическим Data Vault — это необходимость постоянно вычислять срезы актуальности, стыкуя Хабы с множеством Сателлитов по временным интервалам. Если делать это «в лоб» через тяжёлые неэквивалентные JOIN, производительность быстро упадёт.

В перспективе движок будет автоматически сканировать все Сателлиты, привязанные к конкретному бизнес-ключу Хаба, и генерировать общий пул периодов эффективности (единую хронологическую ленту всех изменений). На основе этого пула будет рассчитываться и наполняться аналог классической Point-in-Time (PIT) таблицы. В ней для каждого бизнес-ключа на каждый уникальный момент времени будут зафиксированы готовые ссылки на актуальные строки из всех Сателлитов.

В итоге аналитику или генератору витрин больше не придётся писать сложные оконные функции. Достаточно будет сделать один простой EQUAL JOIN (=) с PIT-таблицей, чтобы мгновенно получить срез данных на любую историческую дату. Разумеется, всю эту логику мы точно так же упакуем в метаданные и наш no-code интерфейс на FastAPI.

Если вам интересно узнать подробнее о том, как реализован конкретный шаг — давайте обсудим в комментариях!

ссылка на оригинал статьи https://habr.com/ru/articles/1079926/