Решение проблемы сложности объединения снимков
Давайте представим ситуацию. Вы — инженер по аналитике в вашей компании. У вас есть несколько реляционных наборов данных, поступающих в ваш хранилище, и, конечно, вы можете легко получить доступ к этим таблицам и преобразовать их с помощью dbt. Вы правильно объединили таблицы и имеете почти в реальном времени отчеты о связях для каждого entity_id
, как они существуют в данный момент.
Но в какой-то момент ваш заинтересованный пользователь хочет знать, как каждый объект изменяется с течением времени. Возможно, важно понять тенденцию продукта на протяжении его жизненного цикла. Вам нужна история каждого entity_id
во всех ваших наборах данных, потому что каждая связанная таблица обновляется по своему собственному графику.
Какая ваша первая мысль? Ну, вы опытный инженер по аналитике и знаете, что хорошие люди из dbt Labs имеют решение для вас. И тут вас осеняет — ответ в снимках!
Что такое снимки и где они становятся сложными?
Снимки предоставляют картину изменений на протяжении истории — снимок во времени, если хотите. Когда значение в строке сырой таблицы обновляется, новая строка добавляется в вашу таблицу-снимок, создавая историческую запись ваших данных.
Вот пример набора данных.
entity_id | important_status | updated_at |
---|---|---|
1 | available | 2021-11-15 16:00:0000 |
2 | not_available | 2021-11-15 15:30:0000 |
Когда вы применяете снимок к этим данным, вы увидите историю данных, а также временные метки valid_from
и valid_to
, чтобы зафиксировать, когда значения строк были действительными, и значения в эти периоды времени.
entity_id | important_status | dbt_valid_from | dbt_valid_to |
---|---|---|---|
1 | available | 2021-11-15 16:00:0000 | NULL |
1 | pending | 2021-11-10 08:00:000 | 2021-11-15 16:00:0000 |
1 | not_available | 2021-10-01 10:00:000 | 2021-11-10 08:00:000 |
2 | not_available | 2021-11-15 15:30:0000 | NULL |
Снимки невероятно полезны, но они добавляют немного сложности при объединении таблиц вниз по потоку, потому что вы добавили несколько строк истории на каждый id. Что происходит, когда у вас есть 10 снимков, которые вы хотите объединить, и вы хотите зафиксировать историю всех наборов данных?
Рассмотрим сложность проблемы: вы успешно зафиксировали историю всех ваших таблиц, используя снимки. У вас есть history_table_1
и history_table_2
, и вы хотите объединить их по общему ключу, product_id
. Однако мы не можем просто объединить по первичному ключу, потому что каждая таблица имеет несколько строк истории для одного и того же id, все действительные в разные временные промежутки.
history_table_1
:
product_id | important_status | dbt_valid_from | dbt_valid_to |
---|---|---|---|
1 | available | 2021-11-15 16:00:0000 | NULL |
1 | pending | 2021-11-10 08:00:000 | 2021-11-15 16:00:0000 |
1 | not_available | 2021-10-01 10:00:000 | 2021-11-10 08:00:000 |
2 | not_available | 2021-11-15 15:30:0000 | NULL |
history_table_2
:
product_id | order_id | product_order_id | order_status | dbt_valid_from | dbt_valid_to |
---|---|---|---|---|---|
1 | A | 1A | available | 2021-11-15 16:00:0000 | NULL |
1 | A | 1A | pending | 2021-10-31 12:00:000 | 2021-11-15 16:00:0000 |
1 | B | 1B | available | 2021-11-15 15:30:0000 | NULL |
1 | B | 1B | pending | 2021-11-10 10:00:000 | 2021-11-15 15:30:0000 |
2 | C | 2C | available | 2021-11-10 15:00:0000 | NULL |
Это не выглядит так уж плохо. Насколько это может быть сложно? Давайте посмотрим на математику. Скажем, historical_table_1
имеет x исторических строк на product_id
, и y id всего. Это x*y = n строк данных. historical_table_2
имеет z исторических строк на product_id
, и w id (z*w = m строк). Последующее объединение по product_id
затем изменяет сложность с O(n) на O(n*m) очень быстро (x*y*z*w возможностей!). Сложность продолжает увеличиваться по мере объединения все большего количества исторических таблиц.
Я знаю, о чем вы думаете — какой беспорядок! Не можем ли мы просто все объединить и сделать снимок полученной таблицы? Это не такая уж плохая мысль. Это избавило бы вас от необходимости продумывать проблему с O(n*m*a*b*c*d*...*q) сложностью. И в некоторых случаях это может зафиксировать всю историю, которая вам нужна!
Однако это не предоставляет решения проблемы, поставленной изначально. Исторические записи отслеживают, когда каждая таблица действительна, а не когда действительна объединенная таблица, и эта история для каждого набора данных будет отражена только тогда, когда вы сделаете снимок каждой таблицы, а затем объедините их, а не объедините и затем сделаете снимок таблицы. valid_from
и valid_to
, встроенные в объединенную-затем-снимочную таблицу, будут построены только из временных меток updated_at
, где обновляется объединенная таблица, и, следовательно, изменения в исходных данных могут не быть зафиксированы. Мы хотим понять, когда записи действительно действительны во всех таблицах, что означает, что нам нужно учитывать действительные временные метки из каждого отдельного набор а данных.
Хорошо, мы исключили легкий способ решения этого вопроса. Так что давайте решим эту проблему O(n*m*a*b*c*d*...*q)! Мы можем это сделать.
План действий для нашего решения
В конечном итоге наша цель — зафиксировать историю для product_id
и объединить строки, которые действительны в одно и то же время. В результате мы можем получить представление о наших данных в данный момент времени, которое точно представляет действительное состояние на любую заданную дату.
Для historical_table_1
и historical_table_2
мы будем объединять по product_id
, где historical_table_1.valid_from
до historical_table_1.valid_to
имеет пересекающееся время с historical_table_2.valid_from
до historical_table_2.valid_to
.
Это сводится к следующим шагам:
- Избавьтесь от дубликатов, если это необходимо
- Сделайте снимок ваших таблиц данных
- Защитите ваши даты
valid_to
на будущее - Объедините ваши таблицы с несовпадающим зерном, чтобы построить разветвленный стержень, содержащий зерновые id, к которым мы будем присоединять остальные данные
- Присоедините снимки к стержню данных по соответствующему id в пересекающихся временных промежутках, сужая действительные временные промежутки на строку по мере присоединения большего количества таблиц
- Очистите ваши столбцы в финальной
- Необязательное добавление глобальной переменной для фильтрации только текущих значений
Итак, давайте погрузимся! С головой! Шаг 1 описан в этом блоге: Как удалить частичные дубликаты. Его нужно реализовать только в том случае, если вы имеете дело с дубликатами в ваших данных. Если у вас нет дубликатов в данных (вау! пришлите мне номер вашего инженера по данным СРОЧНО), вы можете пропустить этот шаг.
Шаг 1: Убедитесь, что ваши таблицы данных не содержат дубликатов
Шаг 1 проведет вас через процесс создания суррогатного ключа из значений столбцов с помощью макроса, а затем удаления этих дубликатов из ваших данных. Нет дубликатов? Перейдите к Шагу 2.
Почему этот шаг важен? Потому что вы будете объединять так много строк по одному и тому же id, и действительные временные метки для каждой строки будут определять точное место для объединения одной таблицы с другой. Мы не можем сделать это точно с дубликатами! (Но также, вы должны проверять на наличие дубликатов в любом случае, потому что мы инженеры по аналитике, верно?)
Смотрите этот блог для удаления частичных дубликатов!
Шаг 2: Сделайте снимок ваших данных
Шаг 2 проведет вас через процесс создания снимка ваших данных. Пример предполагает, что вы прошли Шаг 1, но если вы пропустили этот шаг, просто сделайте снимок ваших данных, основываясь на ссылках, предоставленных ниже.
Знаете ли вы, как сделать снимок данных? Это простой блок Jinja с некоторыми указанными конфигурациями. Существует так много объяснений, как реализовать это, поэтому я не буду вас утомлять. Но вы знаете, что я дам вам несколько ссылок. Бум. И фубар!
Вы можете сделать снимок, проверяя ваш change_id
, если вы реализовали логику удаления дубликатов из Шага 1, или используя стратегию временных меток, если у вас есть надежная временная метка.
{% snapshot snp_product %}
{{
config(
target_schema=generate_schema_name('snapshots'),
unique_key='assetid',
strategy='check',
check_cols=['change_id']
)
}}
select * from {{ ref('base_product') }}
{% endsnapshot %}