Привет! Меня зовут Влад, я 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 и проверяем, как методология справляется с реальными задачами.

Архитектура DWH в ЦИАН
Архитектура DWH в ЦИАН

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, совет простой — пробуйте на реальном проекте, оценивайте накладные расходы и не бойтесь адаптировать методологию под свои процессы.