This repository provides an incremental model in dbt for managing historical data with support for change data capture (CDC). It includes macros for DRY (Don't Repeat Yourself) coding and an example model for handling changes and capturing historical data efficiently.
- Incremental Model: Handles data changes efficiently with incremental materialization.
- Macros: Modularized logic to keep the code DRY.
- CDC Support: Identifies new, changed, and removed records using change data capture techniques.
Compares corresponding fields between two datasets and returns a logical expression for identifying changes.
Parameters:
fields(list of strings): Fields to compare.prefix1(string): Prefix (or alias) for the first dataset.prefix2(string): Prefix (or alias) for the second dataset.
{% macro compare_fields(fields, prefix1, prefix2) %}
{% set conditions = [] %}
{% for field in fields %}
{{ conditions.append(prefix1 ~ '.' ~ field ~ ' <> ' ~ prefix2 ~ '.' ~ field) }}
{% endfor %}
{{ return(' OR '.join(conditions)) }}
{% endmacro %}Generates a list of fields prefixed with a specified alias.
Parameters:
alias(string): The alias to prefix the fields.fields(list of strings): List of field names to prefix.
{% macro prefix_fields(alias, fields) %}
{% set prefixed_fields = [] %}
{% for field in fields %}
{{ prefixed_fields.append(alias ~ '.' ~ field) }}
{% endfor %}
{{ return(prefixed_fields | join(', ')) }}
{% endmacro %}This model manages a historical table for product data, handling changes incrementally. It:
- Tracks active records.
- Identifies records that are no longer present.
- Deactivates outdated versions of records with changes.
- Inserts new records or new versions of existing records.
SQL Code:
{% set source_table_alias = 'products' %}
{% set unique_key = 'product_id' %}
{{
config(
alias=source_table_alias,
materialized='incremental',
unique_key=unique_key,
dist=unique_key,
sort='dbt_created_at',
incremental_predicates = [
"dbt_valid_to is null"
]
)
}}
{% set fields_to_track = [
'product_name', 'description', 'price', 'stock_quantity', 'category_id'
] %}
-- Step 1: Define the source and existing records
WITH source_records AS (
SELECT * FROM {{ ref(source_table_alias)}}
),
active_target_records AS (
SELECT * FROM {{ this }} WHERE dbt_valid_to IS NULL
),
target_left_join_source AS (
SELECT atr.*, src.{{ unique_key }} AS src_{{ unique_key }}
FROM active_target_records AS atr
LEFT JOIN source_records AS src
ON atr.{{ unique_key }} = src.{{ unique_key }}
),
-- Step 2: Identify Records That Are No Longer Present
to_deactivate_absent_records AS (
SELECT
tgt.{{ unique_key }},
{{ prefix_fields('tgt', fields_to_track) }},
-- dbt meta fields
tgt.dbt_batch_id,
tgt.dbt_created_at,
CURRENT_TIMESTAMP AS dbt_updated_at,
tgt.dbt_valid_from,
CURRENT_TIMESTAMP AS dbt_valid_to
FROM target_left_join_source tgt
WHERE src_{{ unique_key }} IS NULL
),
-- Step 3: Identify Changing Records to Deactivate
to_deactivate_existing_records AS (
SELECT
atr.{{ unique_key }},
{{ prefix_fields('atr', fields_to_track) }},
-- dbt meta fields
atr.dbt_batch_id,
atr.dbt_created_at,
CURRENT_TIMESTAMP AS dbt_updated_at,
atr.dbt_valid_from,
CURRENT_TIMESTAMP AS dbt_valid_to
FROM active_target_records atr
JOIN source_records src ON atr.{{ unique_key }} = src.{{ unique_key }}
WHERE
-- change data capture
{{ compare_fields(fields_to_track, 'atr', 'src') }}
),
-- Step 4: Identify New Versions of Existing Records
new_versions_of_existing_records AS (
SELECT
src.{{ unique_key }},
{{ prefix_fields('src', fields_to_track) }},
-- dbt meta fields
'{{ invocation_id }}' AS dbt_batch_id,
CURRENT_TIMESTAMP AS dbt_created_at,
CURRENT_TIMESTAMP AS dbt_updated_at,
CURRENT_TIMESTAMP AS dbt_valid_from,
NULL AS dbt_valid_to
FROM source_records src
JOIN active_target_records atr ON src.{{ unique_key }} = atr.{{ unique_key }}
WHERE
-- change data capture
{{ compare_fields(fields_to_track, 'atr', 'src') }}
),
-- Step 5: Identify New Records to Insert
new_records AS (
SELECT
src.{{ unique_key }},
{{ prefix_fields('src', fields_to_track) }},
-- dbt meta fields
'{{ invocation_id }}' AS dbt_batch_id,
CURRENT_TIMESTAMP AS dbt_created_at,
CURRENT_TIMESTAMP AS dbt_updated_at,
CURRENT_TIMESTAMP AS dbt_valid_from,
NULL AS dbt_valid_to
FROM source_records src
LEFT JOIN active_target_records atr ON src.{{ unique_key }} = atr.{{ unique_key }}
WHERE atr.{{ unique_key }} IS NULL
),
-- Final Step: Combine all transformations for incremental update
final AS (
SELECT * FROM to_deactivate_existing_records
UNION ALL
SELECT * FROM to_deactivate_absent_records
UNION ALL
SELECT * FROM new_versions_of_existing_records
UNION ALL
SELECT * FROM new_records
)
SELECT * FROM final- Macros: Copy the macros to a
.sqlfile inside yourmacrosfolder. - Model: Place the provided SQL model inside your
modelsfolder. - Run dbt: Execute
dbt runto apply the incremental model.
- Adjust the
fields_to_trackvariable to include the fields that need change tracking. - Modify the
unique_keyto match your data schema's primary key.
To create a new incremental model for another data source using dbt, you only need to configure a few parameters specific to your new source. Here’s a straightforward guide to setting up your model:
-
Define Source Table Alias: Specify the alias for the source table from which the data will be pulled. This alias is used to reference the table in your SQL code.
{% set source_table_alias = '<new_source_table>' %} -
Set Unique Key: Identify the unique key of the source table. This key is used to detect changes and manage data over incremental runs effectively.
{% set unique_key = '<unique_key_of_new_source_table>' %} -
List Fields to Track: Determine which fields you want to track for changes. These fields are used in the change detection logic of the incremental model.
{% set fields_to_track = [ '<field1>', '<field2>', '<field3>', ... // List all fields you want to track separated by commas ] %}
Below is a new incremental model for stores table:
{% set source_table_alias = 'stores' %}
{% set unique_key = 'store_id' %}
{{
config(
alias=source_table_alias,
materialized='incremental',
unique_key=unique_key,
dist=unique_key,
sort='dbt_created_at',
incremental_predicates = [
"dbt_valid_to is null"
]
)
}}
{% set fields_to_track = [
'store_id', 'store_name', 'location', 'manager_name', 'contact_number'
] %}
-- Include the SQL logic for the incremental model using the defined variables and macrosThis project is licensed under the MIT License.