上文我们介绍了增量策略的理论知识,本文结合实际场景介绍如何合理利用增量策略,内容包括应用场景、常见问题及解决方案。
应用场景
增量模型是定义如何增量向数据模型添加数据的有效方法——假设我们有描述信用卡交易的数据表——我们创建DBT模型,内容如下:
{{
config(
materialized="table",
) }}
select
transaction_id,
transaction_date,
user_id,
store_name_description,
transaction_amount
from {{ ref('external_table_transaction') }}
这将在目标数据库中创建一张表,从表external_table_transaction加载事务数据。问题是,每次我们重新运行这个查询时,它都会重新加载整个表——表中的数据越多,我们的查询就会变得越慢,运行时间越长——解决该问题的方法是使用增量模型:
{{
config(
materialized="incremental",
unique_key=["transaction_id"],
incremental_strategy="delete+insert",
) }}
select
transaction_id,
transaction_date,
user_id,
store_name_description,
transaction_amount
from {{ ref('external_table_transaction') }}
{%- if is_incremental() %}
where transaction_date = (select max(transaction_date) + 1 as next_date from {{ this }})
{%- endif %}
我们看到宏函数及jinja模版功能让DBT如此强大。上面代码基本实现了我们的诉求,现在应该只从external_table_transaction加载事务增量数据,其中transaction_date
比表中的最新数据大1天——它简单而强大。我们现在只需要处理以前看不见的行,而不是处理每次更新都会变大的数十亿行数据;如果需要,仍然可以选择完全刷新重新加载全部数据。
问题分析
增量模型非常吸引人——它们在逻辑上非常漂亮,在定期处理数据流的情况下,效率非常高。当我们需要控制正在处理哪些数据时,问题就出现了:增量模型不能处理需要重新运行特定分区的情况,而是根据增量模型的规则加载数据。
也许从理论上讲,这不是问题,因为如果增量模型在理想环境中运行,所有数据将只加载一次。但现实是混乱的——数据处理流程中断,数据延迟交付,或者在某些情况下根本不交付,有时我们需要重新加载历史记录。此外,如果调度dbt任务程序出错,也会需要重新运行弥补问题。典型造成问题的场景如下:
-
数据流中原始一些数据需要回溯加载2年前的修复数据,我们需要加载历史数据,这需要以一种特殊的方式完成,因为增量策略无法加载2年前的历史记录。
-
数据流由于上游问题而中断了3天,3天没有加载数据,当数据流在第4天运行时,它正在加载第1天的数据-换句话说,它已经不同步其他数据。当然我们可以修改条件,加载大于最大日期数据,但是即使这样也会因为日期问题漏加载数据。
-
数据流的上游有跳过日(缺少数据的日子),我们的增量模型试图通过在数据中的最大日期上添加“1”来加载数据,但该日期从未出现,因此数据从未加载,导致需要人工干预。
即使有这些问题,我们也不能简单放弃增量模型,对于大数据量场景,处理任务是非常缓慢、且成本高。
- 幂等和分区
增量模型的关键问题是它们不是幂等的,并且不能配置为针对特定日期分区运行。对于幂等脚本可以多次重新运行而不会产生副作用。如果历史数据有问题,我们总是可以重新生成一些特定的分区——由于脚本是幂等的,我们可以在给定的一天内多次运行,而不会产生任何问题。
增量模型不具备重新运行数据的特定分区的能力——相反,它们将所有数据视为流,只加载看不见的数据——基本上加载满足特定规则的数据,而不是数据的特定分区。
问题是有时我们需要数据流符合某种时间分区运行——可以是每小时、每天、每周、每月。如果我们重新运行数据流任务,希望它在对应的时间分区上运行;但是同时也需要增量模型只会“向前看”,而不是在历史分区上配置。总之,就是既要增量、又要灵活按时间分区幂等方式运行。
解决方案
解决方案很简单:我们可以使用DBT变量,并且不需要完全抛弃增量模型的功能。我们可以添加变量来显式地针特定分区运行:
{%- set target_date = var("target_date", "") %}
{{
config(
materialized="incremental",
unique_key=["transaction_id"],
incremental_strategy="delete+insert",
) }}
select
transaction_id,
transaction_date,
user_id,
store_name_description,
transaction_amount
from {{ ref('external_table_transaction') }}
{%- if target_date != "" %}
where transaction_date = '{{ target_date }}'
{%- else %}
{%- if is_incremental() %}
where transaction_date = (select max(transaction_date) + 1 as next_date from {{ this }})
{%- endif %}
{%- endif %}
这里在DBT模型添加了’ target_date ‘变量。如果’ target_date '未定义,则模型将以增量行为运行,但如果传入变量,则模型将针对指定分区运行。当通过调度程序执行时,这种方式会工作得很好。
此外,我们采用"delete+insert"
增量策略,模型现在已经变成幂等的——假设源数据是相同的,我们可以用相同的参数运行相同的查询,并期望得到相同的结果——而对于增量模型,加载的数据取决于表的内容,以及上游发生的更改。
这个解决方案有效地为我们提供了三种模式: 完全重新加载、增量加载和分区加载。因此在实际应用中非常实用,而且可以很好地配合Airflow或其他调度工具实现自动化运行:
dbt run --select my_model
-- 显示完全刷新数据模型
dbt run --select my_model --full-refresh
-- 指定参数执行
dbt run --select my_model --vars "{target_date : '2024-01-01'}"
最后总结
本文介绍了增量策略实际应用中的问题:如何让增量模型能够高效幂等运行。我们提供良好的解决方案同时满足三种场景应用,让数据转换流程更健壮、更高效。期待您的真诚反馈,更多内容请阅读数据分析工程专栏。