Learning Platform
Глоссарий Troubleshooting
Урок 17.04 · 17 мин
Начальный
data-vaulthashdiffchange-detectionsatellite

Hashdiff: обнаружение изменений в Satellite

В уроке про Satellite мы сказали: при изменении атрибута Satellite добавляет новую строку, а старая остаётся. Но возникает практический вопрос. Каждую ночь ETL получает из источника полную выгрузку клиентов. Большинство клиентов не менялись — их имя, email, город те же, что вчера. Если для каждого клиента слепо вставлять новую строку в Satellite, таблица будет расти на полный объём источника каждую загрузку, даже когда никто ничего не менял. За год Satellite раздуется в сотни раз без единого реального изменения.

Нужен механизм, который отвечает на вопрос: «эта входящая строка отличается от того, что уже лежит в Satellite, или нет?» В Data Vault этот механизм — hashdiff.


Идея hashdiff

hashdiff — это хеш конкатенации всех описательных атрибутов Satellite. Не business key (как hash key из прошлого урока), а именно атрибутов: имени, email, города — всего, что Satellite хранит как описание.

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

-- hashdiff = хеш конкатенации ВСЕХ описательных атрибутов сателлита
SELECT
    customer_hk,
    SHA256(
        COALESCE(UPPER(TRIM(full_name)), '') || '||' ||
        COALESCE(UPPER(TRIM(email)),     '') || '||' ||
        COALESCE(UPPER(TRIM(city)),      '')
    ) AS hashdiff,
    full_name, email, city
FROM staging_customer;

-- Анна Петрова / [email protected] / Москва -> hashdiff: 0x7a3f...
-- Та же тройка атрибутов всегда даёт 0x7a3f...
-- Город меняется на Казань           -> hashdiff: 0xc05e... (другой)

Два правила вычисления hashdiff повторяют правила hash key. Разделитель между полями — чтобы изменение, «перетёкшее» из одного поля в соседнее, не осталось незамеченным. И обработка NULL через COALESCE — потому что конкатенация с NULL во многих СУБД даёт NULL, и без этого весь hashdiff обнулялся бы из-за одного пустого поля. Важно: набор атрибутов, порядок их следования и правила нормализации должны быть зафиксированы раз и навсегда. Поменяете порядок полей в формуле — все hashdiff пересчитаются, и следующая загрузка решит, что изменилось всё.


Алгоритм загрузки Satellite

Вот как hashdiff работает при загрузке. Для каждой строки из источника:

Решение о вставке строки в Satellite
Строка из источникаОчередной клиент из ночной выгрузки источника
считаем hashdiff атрибутов
hashdiff входящей строкиХеш конкатенации всех описательных атрибутов входящей строки
сравниваем с последней версией в сателлите
СравнениеБерём hashdiff последней по load_date строки этого hash key в сателлите
  • hashdiff совпал с последней версией в Satellite -> атрибуты не изменились -> строка НЕ вставляется. Ничего не делаем.
  • hashdiff не совпал -> хотя бы один атрибут изменился -> вставляется новая строка с новым load_date.
  • В Satellite вообще нет строк для этого hash key -> сущность новая -> вставляется первая строка.

Так в Satellite попадают только реальные изменения. Из ночной выгрузки в миллион клиентов, где за день изменилось 800, в Satellite добавится ровно 800 строк, а не миллион. Это превращает insert-only из теоретически красивого, но прожорливого принципа — в практичный.

Посмотрим на загрузку в SQL-подобном виде:

-- Вставляем в сателлит только строки, чей hashdiff отличается
-- от ПОСЛЕДНЕЙ известной версии этого hash key
INSERT INTO sat_customer_details
    (customer_hk, load_date, record_source, hashdiff, full_name, email, city)
SELECT
    s.customer_hk, CURRENT_TIMESTAMP, s.record_source,
    s.hashdiff, s.full_name, s.email, s.city
FROM staging_customer s
LEFT JOIN (
    -- последняя версия каждого клиента в сателлите
    SELECT DISTINCT ON (customer_hk) customer_hk, hashdiff
    FROM sat_customer_details
    ORDER BY customer_hk, load_date DESC
) latest ON s.customer_hk = latest.customer_hk
WHERE latest.customer_hk IS NULL          -- новая сущность
   OR latest.hashdiff <> s.hashdiff;      -- атрибуты изменились

WHERE делает всю работу: latest.customer_hk IS NULL ловит новых клиентов, latest.hashdiff <> s.hashdiff — изменившихся. Те, у кого hashdiff совпал, не проходят фильтр и в Satellite не попадают.


Почему hashdiff, а не сравнение «в лоб»

Можно ведь сравнивать атрибуты напрямую: WHERE staging.full_name <> sat.full_name OR staging.email <> sat.email OR staging.city <> sat.city. Зачем хеш?

Несколько причин, и они складываются:

  • Краткость и единообразие. У Satellite может быть 40 атрибутов. Условие из 40 OR — длинное, его легко написать с ошибкой и тяжело сопровождать. hashdiff сворачивает это в одно сравнение <>.
  • NULL-ловушки. В SQL NULL <> NULL даёт не TRUE, а UNKNOWN. Прямое сравнение полей, где встречаются NULL, незаметно пропускает изменения вида «было значение -> стало NULL». В hashdiff NULL обработан один раз через COALESCE — ловушка закрыта централизованно.
  • Фиксированная ширина. hashdiff — один столбец 16/32 байта. Сравнение и индексация предсказуемы независимо от того, сколько и каких атрибутов в Satellite.
  • Совместимость с параллельной загрузкой. Как и hash key, hashdiff вычисляется детерминированно из самих данных — его можно посчитать в любом воркере без координации.
TIP

hash key и hashdiff — два разных хеша с двумя разными задачами, не путайте их. Hash key считается от business key и служит первичным ключом (идентичность). Hashdiff считается от описательных атрибутов и служит для change detection (изменилось ли). В одной строке Satellite присутствуют оба: hash key родителя — кто это, hashdiff — в каком состоянии атрибуты.

Incremental-стратегия dbt vs hashdiff — два способа обнаруживать изменения
СвойствоHash keyHashdiff
От чего считаетсяОт business keyОт всех описательных атрибутов
РольПервичный ключ, идентичностьОбнаружение изменений
Где находитсяHub, Link, SatelliteТолько в Satellite
Меняется при изменении атрибутаНетДа

Граничный случай: delete и reload

hashdiff отвечает на вопрос «изменилось ли», но есть нюанс с удалениями. Если запись пропала из источника, в выгрузке её просто нет — сравнивать не с чем, и hashdiff об этом не узнает. Data Vault обычно решает это отдельным механизмом: либо специальным «record tracking» Satellite, который фиксирует сам факт присутствия ключа в выгрузке, либо статусным Satellite с атрибутом вида is_deleted. Это уже за рамками базового hashdiff, но знать о границе его применимости полезно: hashdiff видит изменения атрибутов присутствующих строк, но не исчезновение строк.

И ещё: если источник прислал строку, потом она «вернулась» к старому значению (город сменился на Казань, а через месяц обратно на Москву) — hashdiff честно зафиксирует оба перехода как изменения. В Satellite будет три версии: Москва, Казань, снова Москва. Это правильное поведение: история — это история, повтор значения тоже её часть.


Попробуй сам

  1. Возьмите Satellite клиента с атрибутами full_name, email, city, loyalty_status. Выпишите формулу hashdiff: конкатенация всех четырёх с разделителем и COALESCE для NULL.
  2. Смоделируйте три ночные загрузки одного клиента: (1) Анна / [email protected] / Москва / silver; (2) то же самое; (3) Анна / [email protected] / Москва / gold. Для каждой загрузки скажите, вставится ли строка в Satellite и почему. Сколько строк в Satellite после трёх загрузок?
  3. Объясните, почему изменение порядка атрибутов в формуле hashdiff (например, поменять местами email и city) после полугода работы приведёт к тому, что следующая загрузка решит, будто изменились все клиенты.
  4. Подумайте: hashdiff не заметит, что клиент исчез из выгрузки источника. Каким отдельным механизмом Data Vault это обычно отслеживает?

В следующем уроке поднимемся на уровень выше — разберём Raw Vault и Business Vault и вспомогательные таблицы PIT и bridge.


Проверка знанийKnowledge check
Что такое hashdiff, как он используется при загрузке Satellite, и почему это лучше, чем прямое сравнение всех атрибутов?
ОтветAnswer
Hashdiff — это хеш конкатенации всех описательных атрибутов Satellite. При загрузке для каждой входящей строки вычисляется её hashdiff и сравнивается с hashdiff последней (по load_date) версии этого hash key в Satellite. Если hashdiff совпал — атрибуты не изменились, строка не вставляется. Если не совпал — хотя бы один атрибут изменился, вставляется новая версия. Если строк для этого hash key вообще нет — сущность новая, вставляется первая строка. Так в Satellite попадают только реальные изменения, а не вся выгрузка источника каждую ночь. Hashdiff лучше прямого сравнения атрибутов по нескольким причинам: одно сравнение вместо длинного условия из десятков OR (короче и надёжнее); централизованная обработка NULL через COALESCE (прямое сравнение полей с NULL даёт UNKNOWN и незаметно пропускает изменения вида значение -> NULL); фиксированная ширина столбца независимо от числа атрибутов; детерминированность, совместимая с параллельной загрузкой.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. От чего вычисляется hashdiff и для чего он нужен?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5