Привет! Меня зовут Влад, я DWH-инженер в Циан. Занимаюсь проектированием витрин и пайплайнов для доставки данных в корпоративное хранилище. В этой статье хочу поделиться опытом применения методологии Data Vault на Greenplum.
Data Vault часто упоминают рядом с Kimball и Inmon, но практических материалов по его внедрению заметно меньше. Для инженеров, которые только начинают строить DWH или думают о переходе на Data Vault, я собрал практический разбор: на каких задачах методология действительно помогает, с какими трудностями можно столкнуться и как это выглядит в реальном проекте.
Введение
Greenplum - это масштабируемое аналитическое хранилище данных, реализующее архитектуру MPP (Massively Parallel Processing) и основанное на PostgreSQL. На сегодняшний день оно является одним из наиболее востребованных решений в области аналитических вычислений и обработки больших объемов данных.
Чтобы было понятнее, в каком контексте мы его применяем, расскажу про наш DWH в Циане. Он работает в YC и выглядит так: 32 сегмента, мастер-нода на 16 vCPU и 64 ГБ памяти, на каждом сегменте — по 32 vCPU и 256 ГБ. В DDS слое больше тысячи таблиц и более 500 витрин, около 50 пользователей работают с системой ежедневно.
Это та инфраструктура, на которой мы экспериментируем с Data Vault и проверяем, как методология справляется с реальными задачами.

Data Lake и Staging Area
Данные в Циане централизованно хранятся в Data Lake, построенном на основе HDFS/S3 и управляемом через Apache Hive. Таблицы в Data Lake партиционированы по дням — это позволяет эффективно фильтровать и обрабатывать данные по временным диапазонам. В большинстве случаев именно отсюда мы извлекаем исходные данные для последующей обработки. Дополнительные источники включают внешние API и другие СУБД.
Следующим этапом в цепочке является Staging Area — промежуточное хранилище, куда данные поступают в сыром виде, без значительных преобразований, непосредственно из источников (HDFS/S3, API).
Ключевые особенности:
Структура таблиц полностью соответствует структуре источника — обычно 1:1 с таблицами БД или форматом ответа API
При загрузке применяются минимальные трансформации — основная цель — сохранить данные в исходном виде
Работает по принципу transient: перед каждой новой загрузкой таблицы очищаются, чтобы избежать накопления устаревших или дублирующих данных
Загрузка выполняется батчами раз в сутки через отдельный оператор в Apache Airflow, что обеспечивает предсказуемость и контроль над процессом
DDS area - Data Vault
DDS (Data Delivery/Detail Store) — это слой детализированных, очищенных и структурированных данных, поступающих из staging. В Циане мы проектируем DDS по методологии Data Vault. Для загрузки используется кастомный оператор в Apache Airflow.
Data Vault строится на трёх основных компонентах: хабы, сателлиты и линки. Разберём их по порядку.
Хаб
Хаб хранит бизнес-ключи — уникальные идентификаторы сущностей.
Пример хаба с сотрудниками компании (hub_employee)
employee_id — хэш от бизнес-ключа
original_id — бизнес-ключ
row_system_code — код источника
row_insert_process_id — уникальный числовой идентификатор загрузки, который генерируется автоматически и монотонно возрастает
Благодаря полю row_insert_process_id можно отследить любую загрузку в DWH: её длительность, время старта и окончания, а также собрать дополнительную статистику для анализа.
Во всех объектах dds и dm слоев мы используем буферные таблицы для первоначальной загрузки из stage слоя.
Пример SQL запроса для загрузки хаба:
insert into dds.hub_employee
(employee_id,original_id,row_insert_process_id,row_system_code)
select
src.employee_id,
src.original_id,
6446240,
src.row_system_code
from dds.hub_employee_buffer src
left join dds.hub_employee tgt
on tgt.employee_id = src.employee_id
where tgt.employee_id is null
Сателлит
Сателлит хранит описательные атрибуты для хабов и линков.
Пример: сателлит с информацией о сотрудниках (sat_employee)
employee_id — хэш от бизнес-ключа
actual_dtm — временная отметка, начиная с которой запись считается актуальной
name, employment_date — описательные атрибуты
row_system_code — код источника
row_hash — хэш от конкатенации описательных полей (в нашем случае full_name и employment_date)
row_insert_process_id — числовой идентификатор загрузки (по аналогии с хабом)
Связь с хабом через ключ сателлита (employee_id), а row_hash используется для отслеживания изменений в описательных полях.
Пример SQL запроса для загрузки сателлита:
with buffer as (
select
employee_id,
actual_dtm,
name,
employment_date,
row_hash,
0 as rnum,
999999999999 as row_insert_process_id,
row_system_code
from dds.sat_employee_buffer
),
tgt as (
select
employee_id,
actual_dtm,
row_hash,
rnum,
row_insert_process_id,
row_system_code
from (
select
employee_id,
actual_dtm,
row_hash,
row_number() over (partition by employee_id order by actual_dtm desc,row_insert_process_id desc) as rnum,
row_insert_process_id,
row_system_code
from dds.sat_employee
where actual_dtm < '{rep_dt}'::date + 1) tgt
)
insert into dds.sat_employee
(employee_id,actual_dtm,name,employment_date,row_hash,row_insert_process_id,row_system_code)
select
employee_id,
actual_dtm,
name,
employment_date,
row_hash,
6493247, -- row_insert_process_id
row_system_code
from (
select employee_id, actual_dtm, name, employment_date, row_hash, row_system_code
from
(
select
employee_id,
actual_dtm,
name,
employment_date,
row_hash,
row_system_code,
rnum,
lead(row_hash) over (partition by employee_id order by actual_dtm desc,row_insert_process_id desc) as prev_hash
from (
select
employee_id,
actual_dtm,
name,
employment_date,
row_hash,
row_insert_process_id,
rnum,
row_system_code
from buffer src
union all
select
employee_id,
actual_dtm,
null as name,
null as employment_date,
row_hash,
row_insert_process_id,
rnum,
null as row_system_code
from tgt
) a ) a
where coalesce(prev_hash, '$$$') != row_hash
and rnum = 0 ) a
При проектировании сателлитов мы придерживаемся нескольких правил. В одном сателлите хранятся атрибуты, связанные по смыслу, например паспортные данные сотрудника или информация о расположении объекта недвижимости.
Если атрибуты обновляются с разной скоростью, их лучше вынести в разные таблицы, иначе при каждом изменении «часто обновляемого» поля будут дублироваться и редко меняющиеся данные, что со временем раздувает таблицу и снижает производительность.
А если у объекта появляются новые свойства, мы создаём отдельный сателлит — такой подход не затрагивает существующие таблицы, которые уже используются в пайплайнах и снижает риски для текущих процессов.
Линк
Линк хранит связи между бизнес-ключами из разных хабов. Его можно проектировать:
по SCD1 — добавляются только новые уникальные комбинации ключей
по SCD2 — фиксируется история изменений связей (например, сотрудник перешёл из одного подразделения в другое)
Пример: линк между сотрудником и организацией.
Поля:
employee_organization_id — хэш от конкатенации ключей хабов
employee_id — хэш от бизнес-ключа хаба «Сотрудник»
organization_id — хэш от бизнес-ключа хаба «Организация»
row_system_code — код источника
row_insert_process_id — уникальный идентификатор загрузки (аналогично хабам и сателлитам)
Пример SQL для заполнения линка (вариант без истории):
insert into dds.link_employee_organization
(employee_organization_id,employee_id,organization_id,row_system_code,row_insert_process_id)
select buf.*
from (
select
employee_organization_id,
employee_id,
organization_id,
row_system_code,
6511682 as row_insert_process_id
from dds.link_employee_organization_buffer
) as buf
left join dds.link_employee_organization as tgt
on tgt.employee_organization_id = buf.employee_organization_id
where tgt.employee_organization_id is null
Линк с датами
Связи между хабами могут со временем меняться: сотрудник переводится в другой отдел, объект недвижимости переходит к новому владельцу и т. д. Чтобы хранить такую историю, мы используем линки с датами.
В них к стандартным полям добавляются:
row_start_dt — дата начала действия связи
row_end_dt — дата окончания действия связи
При этом важно определить ключевое поле, по которому отслеживаются изменения. В нашем примере таким полем является employee_id: если в загрузке приходит запись с тем же employee_id, то Airflow-оператор закрывает старую запись (заполняет row_end_dt) и добавляет новую с актуальным row_start_dt.
Пример SQL для заполнения таких линков почти не отличается от базового варианта:
insert into dds.link_employee_information (
employee_information_id,
employee_id,
business_direction_id,
row_start_dt,
row_end_dt,
row_is_closed,
row_system_code,
row_insert_process_id
)
select src.*
from (
select
buf.employee_information_id,
buf.employee_id,
buf.business_direction_id,
buf.row_start_dt,
buf.row_end_dt,
false as row_is_closed,
buf.row_system_code,
6511556 as row_insert_process_id
from dds.link_employee_information_buffer as buf
left join dds.link_employee_information as tgt
on tgt.link_employee_information_id = buf.link_employee_information_id
and buf.row_start_dt between tgt.row_start_dt and tgt.row_end_dt
where tgt.employee_information_id is null
) as src
Такой подход позволяет фиксировать историю связей и использовать её для аналитики или построения витрин с учётом временного контекста.
Плюсы и минусы Data Vault
Главное преимущество Data Vault — унификация. Методология строго регламентирует типы таблиц, и каждая из них выполняет только свою задачу. Благодаря этому модель становится понятной и предсказуемой.
Второй плюс — расширяемость: при появлении нового свойства достаточно создать отдельный сателлит, а если возникает новая связь — добавить линк. Это делает структуру гибкой и позволяет эволюционировать без перестроек всего хранилища.
Наконец, Data Vault изначально ориентирован на хранение полной истории изменений, что критично для аналитики и аудита.
Но вместе с этим подход приносит и сложности.
Во-первых, таблиц становится очень много и SQL-запросы заметно усложняются.
Во-вторых, проектирование схемы по DV занимает больше времени, чем при использовании классических подходов, — это отражается на TTM. Добавляется и дублирование: одно и то же свойство объекта может оказаться сразу в нескольких сателлитах.
В операционной работе нагрузку создаёт и Airflow: каждая таблица превращается в отдельную таску при расчёте витрин, и при большом числе объектов появляются проблемы со слотами.
Наконец, новая команда не сможет «сходу» работать с DV — время на адаптацию инженеров дольше обычного.
Как решаем проблемы
Чтобы нивелировать ограничения Data Vault, мы используем несколько приёмов. Сложные SQL-запросы разбиваем на этапы: сначала собираем базовые витрины, затем бизнес-витрины. Чтобы сократить TTM, применяем гибридный подход — рядом с DV используем простые схемы, ref-таблицы (справочники, загружаемые один раз) и таблицы сырых фактов. Последние позволяют подключать новый источник данных буквально за несколько часов.
Мы также разграничили ответственность: DDS и слой витрин поделены между командами по направлениям бизнеса, а сами витрины — по критичности. Самые важные считаются первыми, что снижает нагрузку на Airflow и Greenplum в ночное время
Сервисные процессы обслуживания БД мы завершаем до старта основных процессов.
DM слой
Витрины мы строим поверх DDS-слоя и делим их на два типа: dimension и fact.
Dimension-витрины поддерживают историчность и строятся по SCD2. Они обновляются ежедневно. Примеры — витрина с объявлениями на сайте Циан.
Fact-витрины содержат данные по дням, неделям или месяцам (обязательно есть поле rep_dt). Такие витрины могут строиться на основе dimension-слоя, при необходимости партицируются по дате и обновляются ежедневно, по неделям либо раз в месяц. На них удобно строить регламентную отчётность или считать агрегаты, а также использовать как основу для BI-отчётов.
У DM-слоя есть несколько особенностей:
для критичных витрин настраиваются DQ-проверки — это YAML-файл с тестом и SQL-запросом. При падении теста инженер получает уведомление
для больших витрин используем расширение Yezzey: "старые" партиции выносятся на S3, что экономит место на нодах Greenplum
если нужно перезагрузить старые периоды, запускаем специальные reload-даги в Airflow — они отслеживают текущее время и работают вне регламентного окна, обычно днём
Итоги
Для нас Data Vault стал способом навести порядок в хранилище и дисциплинировать проектирование. Да, пришлось потратить время на обучение команды и пережить боль от сотен новых таблиц и дагов, но результат стоит того: мы получили прозрачную модель, удобную для сопровождения и расширения, а время на подключение новых источников и разработку витрин удалось сократить.
Data Vault не решит все проблемы, и он точно не универсален. Но в масштабе Циана он себя оправдал: мы научились совмещать его с другими подходами и выработали гибридную стратегию, которая реально ускоряет вывод новых данных в прод. Если вы думаете о внедрении DV, совет простой — пробуйте на реальном проекте, оценивайте накладные расходы и не бойтесь адаптировать методологию под свои процессы.