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 работает при загрузке. Для каждой строки из источника:
- 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 вычисляется детерминированно из самих данных — его можно посчитать в любом воркере без координации.
hash key и hashdiff — два разных хеша с двумя разными задачами, не путайте их. Hash key считается от business key и служит первичным ключом (идентичность). Hashdiff считается от описательных атрибутов и служит для change detection (изменилось ли). В одной строке Satellite присутствуют оба: hash key родителя — кто это, hashdiff — в каком состоянии атрибуты.
| Свойство | Hash key | Hashdiff |
|---|---|---|
| От чего считается | От business key | От всех описательных атрибутов |
| Роль | Первичный ключ, идентичность | Обнаружение изменений |
| Где находится | Hub, Link, Satellite | Только в Satellite |
| Меняется при изменении атрибута | Нет | Да |
Граничный случай: delete и reload
hashdiff отвечает на вопрос «изменилось ли», но есть нюанс с удалениями. Если запись пропала из источника, в выгрузке её просто нет — сравнивать не с чем, и hashdiff об этом не узнает. Data Vault обычно решает это отдельным механизмом: либо специальным «record tracking» Satellite, который фиксирует сам факт присутствия ключа в выгрузке, либо статусным Satellite с атрибутом вида is_deleted. Это уже за рамками базового hashdiff, но знать о границе его применимости полезно: hashdiff видит изменения атрибутов присутствующих строк, но не исчезновение строк.
И ещё: если источник прислал строку, потом она «вернулась» к старому значению (город сменился на Казань, а через месяц обратно на Москву) — hashdiff честно зафиксирует оба перехода как изменения. В Satellite будет три версии: Москва, Казань, снова Москва. Это правильное поведение: история — это история, повтор значения тоже её часть.
Попробуй сам
- Возьмите Satellite клиента с атрибутами
full_name,email,city,loyalty_status. Выпишите формулу hashdiff: конкатенация всех четырёх с разделителем иCOALESCEдля NULL. - Смоделируйте три ночные загрузки одного клиента: (1) Анна / [email protected] / Москва / silver; (2) то же самое; (3) Анна / [email protected] / Москва / gold. Для каждой загрузки скажите, вставится ли строка в Satellite и почему. Сколько строк в Satellite после трёх загрузок?
- Объясните, почему изменение порядка атрибутов в формуле hashdiff (например, поменять местами
emailиcity) после полугода работы приведёт к тому, что следующая загрузка решит, будто изменились все клиенты. - Подумайте: hashdiff не заметит, что клиент исчез из выгрузки источника. Каким отдельным механизмом Data Vault это обычно отслеживает?
В следующем уроке поднимемся на уровень выше — разберём Raw Vault и Business Vault и вспомогательные таблицы PIT и bridge.