Введение

Общеизвестно, что в хранилищах данных для связи таблиц фактов со справочниками используются суррогатные ключи. В большинстве случаев это целочисленный счетчик, который взаимно однозначно определяет бизнес ключ (или бизнес ключ плюс зависимость от времени для медленно меняющихся справочников). С увеличением объемов обрабатываемой информации в случае большой кардинальности справочников использование счетчиков в качестве суррогатных ключей становится проблемой с точки зрения производительности, т.к. при загрузке фактов необходимо определить значение суррогатного ключа по довольно большому справочнику. Для решения этой проблемы многие компании переходят на формирование суррогатных значений на основе хеш-значений бизнес-ключей. Очень подробно такой подход описан в докладе Евгения Ермакова и Николая Гребенщикова о модели данных хранилища Яндекс Такси (Евгений Ермаков, Николай Гребенщиков — Highly Normalized Hybrid Model - YouTube)

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

В явном виде с этой проблемой мы столкнулись в одном из тендерных запросов и позже в реальном проекте в результате чего, стало понятно, что было здорово выработать какое-то шаблонное решение, тем более, что быстрый поиск по просторам интернета готового решения не дал.

Постановка задачи

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

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

Очень часто в качестве хеш-функции используют алгоритм MD5, который дает низкую вероятность дублей значений, но на выходе у него получается строка в 32 символа, что не оптимально с точки зрения хранения данных и использования в качестве ключа связи. Небольшое обсуждение по опыту использования данных алгоритмов хеширования можно найти по ссылке: https://www.sql.ru/forum/1266277/hash-vs-surrogate-keys   

СУБД Oracle 11.2g имеет встроенную функцию хеширования ORA_HASH(), которая на выходе дает целое число, однако имеет заметную вероятность получения дублей значений. Если решить проблему с дублями, то ее можно будет использовать для генерации суррогатных ключей.

Предлагаемое решение

Формирование справочника

Общая идея в формировании справочника с суррогатными ключами на основе хеш-функции с обработкой коллизий заключалась в том, что для большинства значений справочника используется значения хеш-функции, принимающие положительные значения, а для дублей – используются счетчик значений в отрицательной области. Таким образом пространства возможных значений не пересекаются, что исключает дубли.

Для демонстрации реализации этой идеи нам потребуются следующие таблицы:

Таблица SRC – наш входной поток данных, который в самом простейшем случае содержит только бизнес-ключ Bkey, для которого мы хотим рассчитать суррогатный ключ

Таблица TMP – таблица со временными данными, в которую попадают только те бизнес-ключи, которых нет в целевом справочнике HUB, в рамках одного цикла загрузки. Т.е. это дельта, для которой необходимо сгенерировать суррогатный ключ.

Поля таблицы:

Bkey – бизнес ключ

Hkey – хеш-значение бизнес-ключа

Hkey_qty – порядковый номер хеш-значения для бизнес ключа внутри дельты

Skey – суррогатный ключ для дубля, рассчитанный через последовательность/счетчик, имеющий отрицательное значение.

Таблица HUB – наш целевой справочник, который в самом простом виде содержит первичный ключ Pk_bkey и бизнес-ключ Bkey. Множество значений первичного ключа является объединением множеств значений Hkey и Skey.

Таблица Excl – таблица исключений, содержащая суррогатные ключи для дублей бизнес-ключей. Используется при загрузке фактов.

На диаграмме ниже представлен алгоритм формирования справочника, реализующий подход, описанный выше.

На первом шаге мы очищаем нашу временную таблицу.

На втором шаге осуществляется вставка записей из входного потока SRC во временную таблицу TMP по тем бизнес-ключам, который отсутствуют в целевом справочнике HUB. При заполнении идет расчет хеш-значений бизнес-ключа и для тех бизнес-ключей, которые имеют дубли на данных входного потока, рассчитывается ключ-исключения на основе последовательности.

Пример запроса:

INSERT INTO tmp
           (bkey, hkey, hkey_qty, skey)
    SELECT DISTINCT
           bkey,
    ORA_HASH(bkey) as hkey,
           DENSE_RANK() OVER (PARTITION BY ORA_HASH(bkey) ORDER BY bkey) as HKEY_QTY,
           CASE
	      WHEN DENSE_RANK() OVER (PARTITION BY ORA_HASH(bkey) ORDER BY bkey) > 1
		THEN -1* seq_excl.nextval
	    END as skey
      FROM src
     WHERE bkey IS NOT NULL
       AND not in (SELECT bkey FROM hub)

На третьем шаге мы проверяем, есть ли дубли хеш-значений для новых записей по отношению к тем записям, которые уже есть в справочнике HUB. Для таких записей рассчитывается ключ-исключения на основе той же последовательности.

Пример запроса:

UPDATE tmp
  SET tmp.hkey_qty = hkey_qty + 1,
      tmp.skey = -1* seq_excl.nextval
  WHERE tmp.skey is null
    and tmp.hkey in (select pk_bkey from hub)

В результате выполнения третьего шага в таблице TMP для всех новых бизнес ключей рассчитаны хеш-значения, а для дублей сгенерированы значения ключей-исключений и все готово для финального шага – вставки записей в целевые таблицы.

На четвертом шаге осуществляется вставка записей в целевой справочник HUB и таблицу исключений EXCL.

Пример запроса:

INSERT INTO hub
            (pk_bkey, bkey)
     SELECT nvl(skey, hkey) as pk_bkey
          , bkey
       FROM tmp;
 
INSERT INTO EXCL
            (bkey, skey)
     SELECT bkey, skey 
       FROM tmp
      WHERE skey is not null;

Загрузка фактов

Теперь, когда сформирована таблица исключений EXCL, загрузка фактов из источника (назовем его LOADF) с привязкой к справочнику HUB может осуществляться без обращения к этому справочнику с помощью запроса:

INSERT INTO fact
            (pk_bkey, …)
     SELECT nvl(EXCL.skey, ORA_HASH(LOAD.bkey)) AS pk_bkey
            , …
       FROM LOADF 
         LEFT JOIN EXCL ON (EXCL.bkey = LOADF.bkey);

Опыт применения

Описанный выше подход был опробован в одном из проектов по оптимизации имеющейся структуры хранилища данных по данным кликов (визитов) интернет магазина.

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

Например, из строки вида:

Колонка

Значение

url

/katalog/bumaga-i-bumazhnye-izdeliya/bumaga-dlya-ofisnoj-tekhniki/formatnaya-bumaga/c/135000/?sort=price-asc&listingMode=GRID&sortingParam=price-asc&email=polydkinamargo%40mail.ru&sc_src=email_5359903&sc_lid=321430171&sc_uid=ipz4a29dvS&sc_llid=16237&sc_eh=53cbe15b802f05341&utm_source=mailing1-mail-ems-v4&utm_campaign=20220419_1709_mailing1-priyatnyye_syurprizy_20220418-b2c-op_z1-nbr-ntr-ntm-v4&utm_term=B2C&utm_medium=%D0%91%D1%83%D0%BC%D0%B0%D0%B3%D0%B0+%D0%904+%D0%BF%D0%BE+%D1%81%D1%83%D0%BF%D0%B5%D1%80%D1%86%D0%B5%D0%BD%D0%B5&utm_content=Akcion_block

Формировалась строка вида:

Колонка

Значение

url

/katalog/bumaga-i-bumazhnye-izdeliya/bumaga-dlya-ofisnoj-tekhniki/formatnaya-bumaga/c/135000/?sort=price-asc&listingMode=GRID&sortingParam=price-asc&email=polydkinamargo%40mail.ru&sc_src=email_5359903&sc_lid=321430171&sc_uid=ipz4a29dvS&sc_llid=16237&sc_eh=53cbe15b802f05341&utm_source=mailing1-mail-ems-v4&utm_campaign=20220419_1709_mailing1-priyatnyye_syurprizy_20220418-b2c-op_z1-nbr-ntr-ntm-v4&utm_term=B2C&utm_medium=%D0%91%D1%83%D0%BC%D0%B0%D0%B3%D0%B0+%D0%904+%D0%BF%D0%BE+%D1%81%D1%83%D0%BF%D0%B5%D1%80%D1%86%D0%B5%D0%BD%D0%B5&utm_content=Action_block

cat_lvl_1

katalog

cat_lvl_2

bumaga-i-bumazhnye-izdeliya

cat_lvl_3

bumaga-dlya-ofisnoj-tekhniki

cat_lvl_4

formatnaya-bumaga

utm_source

mailing1-mail-ems-v4

utm_campaign

20220419_1709_mailing1-priyatnyye_syurprizy_20220418-b2c-op_z1-nbr-ntr-ntm-v4

utm_term

B2C

utm_medium

Бумага А4 по суперцене

utm_content

Action_block

В результате рефакторинга было предложено:

  • разделить таблицу фактов, выделив малоиспользуемые широкие исходные поля в отдельную таблицу (в приведенном примере это поле URL);

  • логически сгруппировать поля, получаемые в результате парсинга в справочники с генерацией суррогатных ключей на основе хэш-значений (в примере выше выделены справочники структур каталога страниц сайта - Cat_lvl_1, Cat_lvl_2, Cat_lvl_3, Cat_lvl_4 и справочник маркетинговых акций - utm*).

В итоге большинство аналитических запросов стало выполняться по значительно более «узкой» таблице фактов и одновременно за счет выделения справочников было сокращено на 30% занимаемое место на дисках.

С учетом того, что в качестве СУБД для хранилища использовалась не колоночная Oracle 11.2g полученный выигрыш был значительным.

Вот несколько цифр:

Тип операции

Описание операции

Время выполнения операции - До

Время выполнения операции - После

1

Загрузка данных

Слой Stage

25 мин

69 мин

2

Загрузка данных

Слой DDS

71 мин

10 мин

3

Загрузка данных

Полная загрузка данных до целевых объектов

96 мин

 84 мин

4

Выполнение sql-запроса

Количество операций за день

2731 сек

6 сек

5

Выполнение sql-запроса

Количество операций за календарную неделю

2864 сек

10 сек

6

Выполнение sql-запроса

Выполнение регламентного отчета с выборкой данных за  календарную неделю

980 сек

281 сек

Заключение

Описанное решение:

  • гарантирует уникальность генерируемых суррогатных ключей для входных бизнес-ключей;

  • позволяет использовать метод как при полной перезагрузке данных, так и дельта-загрузке;

  • имеет лучшую производительность при загрузке объемных таблиц фактов за счет отсутствия необходимости связывания непосредственно со справочниками для определения суррогатного ключа.

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

Комментарии (13)


  1. netricks
    03.06.2022 16:40
    -2

    Общеизвестно. Ага… Ещё бы знать, что такое сурогатный ключ? :)
    Но это так… Погуглим.


    1. Ivan22
      03.06.2022 17:57

      ну тег Data Engineering тут какбы не просто так


      1. netricks
        03.06.2022 18:32
        -1

        Тогда не общеизвесто, а «дата» инженерам известно.


  1. OBIEESupport
    03.06.2022 17:11
    -1

    С увеличением объемов обрабатываемой информации в случае большой кардинальности справочников использование счетчиков в качестве суррогатных ключей становится проблемой с точки зрения производительности, т.к. при загрузке фактов необходимо определить значение суррогатного ключа по довольно большому справочнику.

    Переводим на русский язык с хранилищного. Кто-то так спроектировал справочники, что триггеры не успевают вставлять уникальные значения, либо путаются, когда вставляется хоть сколько-то сложная структура данных. Решение автора: давайте отделим то, что можно оценить и найти сразу в виде конкретных значений хэш-функции, от тех значений, которые в справочниках отсутствуют, поэтому не могут быть загружены сразу. Сразу возникает вопрос: а не проще ли сразу было спроектировать измерения так, чтобы коллизии физически не могли наступить? Как бы хорошие примеры этого в книжках описаны: вспомогательная универсальная развязка для набора ранее неопределимых значений, наличие таблицы-справочника с основными оперативными хэшами, иные дополнительные искусственные приемы тоже сравнительно неплохи для хранилищ среднего и малого размера. Вот уже и в новых версиях Oracle (18, 19, 21) они входят в оборот СУБД как система обработки метаданных таблиц-измерений. И не надо ничего лишнего городить. И еще, вдогонку, приложение Oracle Real User Experience решает задачу по конструированию воронки продаж на основании вызова пользователем страниц интернет-магазина (и любого сложного сайта) прямо из коробки, то есть индексация пути пользователя является частью его функционала. К пользе перехода на новый софт заметим, что 12.1 была первой версией с колоночным хранением in-memory.


    1. Ivan22
      03.06.2022 18:00

      какие еще триггеры в DWH ? ну и по сути как раз автор и показывает как сделать так чтобы коллизии не были проблемой.


      1. OBIEESupport
        03.06.2022 19:24

        Buffer the data warehouse from operational changes. Surrogate keys enable
        the warehouse team to maintain control of the DW/BI environment rather
        than being whipsawed by operational rules for generating, updating, deleting,
        recycling, and reusing production codes. In many organizations, historical
        operational codes, such as inactive account numbers or obsolete product
        codes, get reassigned after a period of dormancy. If account numbers get
        recycled following 12 months of inactivity, the operational systems don’t miss
        a beat because their business rules prohibit data from hanging around for that
        long. But the DW/BI system may retain data for years. Surrogate keys provide
        the warehouse with a mechanism to differentiate these two separate instances
        of the same operational account number. If you rely solely on operational
        codes, you might also be vulnerable to key overlaps in the case of an acquisi-
        tion or consolidation of data.

        Цитата по книге: Kimball "The Data Warehouse Toolkit 3rd Edition" стр. 99 английского издания, ISBN: 978-1-118-53080-1


        1. astoulov Автор
          03.06.2022 19:55

          Не очень понимаю, как комментарий выше относится к тому, что написано в статье.

          Поясню на примере. У вас на входе есть информация кликах сайта. Одна из характеристик клика - параметр User Agent, который представляет собой строку довольно большой длины. Хранить ее непосредственно в таблице фактов невыгодно, значит надо делать справочник и суррогатный ключ для него. Просто перенумеровывать все возможные значения по производительности хуже, чем использовать хэш функцию. Но хэш функция может давать дубли, которые надо обрабатывать.

          Например, ORA_HASH() для следующих двух строк дает одно значение ключа:

          • "Opera/5.0 (Linux (Wine); U; Linux i686; en-us) Chrome/41.6.250.668 Safari/587.44"

          • "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.88 (KHTML, like Gecko) Chrome/54.0.1608.78 Safari/537.88"


          1. OBIEESupport
            03.06.2022 22:10

            Я с трудом представляю, где вы функцией "ORA_HASH is a function that computes a hash value for a given expression. This function is useful for operations such as analyzing a subset of data and generating a random sample" с параметрами ORA_HASH(string, max_bucket,seed_value), где по дефолту max_bucket = 4294967295, seed_value = 4294967295 в промышленном коде по одной строке будете разделять User Agent, если действительно легко получается коллизия. Давайте чуть проще сделаем: select ORA_HASH('Opera/5.0 (Linux (Wine); U; Linux i686; en-us) Chrome/41.6.250.668 Safari/587.44',2,5) from dual; вернет 1, а select ORA_HASH('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.88 (KHTML, like Gecko) Chrome/54.0.1608.78 Safari/537.88',2,5) from dual; вернет 0. Как печально указано в статье https://danischnider.wordpress.com/2017/01/24/how-to-build-hash-keys-in-oracle/ "The probability of two input values with the same hash key is quite high. With 9300 input rows, the probability is 1%, with 50’000 rows already 25%, and with 77’000 rows 50%." Дальше там про другие способы генерации написано очень хорошо с примерами кода.

            Поэтому я привел ссылку на приложение Oracle Real User Experience сделанное специально для описываемой вами задачи и более того - заточенное на анализ web приложения целиком.


            1. EvgenyVilkov
              05.06.2022 16:16

              А может просто делать хд в колоночной аналитической базе с кэшируемым наборов суррогатных ключей? :)


              1. astoulov Автор
                06.06.2022 12:03

                Конечно, к колоночной СУБД придут. Но вопрос как формировать суррогатные ключи остается и для колоночной базы. Для того же объема данных, наверно, он будет стоять менее остро и можно будет использовать MD5 или SHA-1, но все равно останется.


                1. EvgenyVilkov
                  06.06.2022 12:22

                  суррогат в виде hash = performance issue. Понятно же что придется потом выполнять соединения по таким полям и работать это будет гораздо медленнее чем если бы ключи были целочисленными.


                  1. astoulov Автор
                    06.06.2022 13:51

                    Именно, поэтому в конкретном случае попробовали использовать ORA_HASH, который дает целое число на выходе.


            1. astoulov Автор
              06.06.2022 12:16

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

              Большинство компаний идет по пути применения MD5, принимая риск того, что коллизии возможны, но они пренебрежимо малы.

              Мне было интересно решить задачу обработки коллизий, что позволило использовать фунцию с большей вероятностью дублей.

              Не претендую на единственно верное решение. Поделился со всеми, т.к. описания подхода обработки дублей ранее не видел. Алгоритм апробовали на реальной системе и он показал свою применимость и эффективность.

              Тем более не готов сравнивать на одной доске точечный алгоритм с решением класса Enterprise - Oracle Real User Experience. Может быть поделитесь опытом внедрения на Российском рынке?