Improve DBT Incremental Performance on Snowflake using Custom Incremental Strategy - Superpayments

Improve DBT Incremental Performance on Snowflake using Custom Incremental Strategy

Dec 19, 2025

Data

The following presents how to improve the performance of the DBT built-in delete-insert incremental strategy on snowflake so we can control snowflake query costs. It is broken down into:

  1. Defining the problem, with supporting performance statistics
  2. Desired solution requirements
  3. Solution implementation, with supporting performance statistics

TL;DR

We implemented a DBT custom incremental strategy, along with incremental predicates to improve snowflake query performance:

Less data is being scanned, so the snowflake warehouse is waiting less time on I/O, so the query completes faster.

Problem

When using the DBT built-in delete-insert incremental strategy on large volumes of data, you can get inefficient queries on snowflake when the delete statement is executed. This means queries take longer and increase warehouse costs.

Taking an example target table:

With a DBT model that:

With DBT model config:

- name: model_name

config:

materialized: "incremental"

incremental_strategy: "delete+insert"

on_schema_change: "append_new_columns"

unique_key: ["dw_order_created_skey"] -- varchar(100)

cluster_by: ["to_date(order_created_at)"]

Default delete SQL generated by DBT, before it inserts data in the same transaction:

delete from target_table as DBT_INTERNAL_DEST

where (dw_order_created_skey) in (

select distinct dw_order_created_skey

from source_temp_table as DBT_INTERNAL_SOURCE

);

Performance Statistics

To find the rows in the target table to delete with the matching dw_order_created_skey (see node profile overview image below), snowflake has to:

Why? - The query is not filtering on order_created_at to allow snowflake to use the clustering key of to_date(order_created_at) to find the matching rows to delete.

Query plan

Desired Solution

To limit the data read in the target table above. We can make use of incremental_predicates in the model config. This will add SQL to filter the target table.

DBT model config:

- name: model_name

config:

materialized: "incremental"

incremental_strategy: "delete+insert"

on_schema_change: "append_new_columns"

unique_key: ["dw_order_created_skey"]

cluster_by: ["to_date(order_created_at)"]

incremental_predicates:

- "order_created_at >= (select dateadd(hour, -24, min(order_created_at)) from DBT_INTERNAL_SOURCE)"

Issues with this

Solution Implementation

We need to:

delete from target_table as DBT_INTERNAL_DEST

where (dw_order_created_skey) in (

select distinct dw_order_created_skey

from source_temp_table as DBT_INTERNAL_SOURCE

)

-- Added by incremental_predicates

and order_created_at >= (select dateadd(hour, -24, min(order_created_at)) from source_temp_table)

;

How - The below macro implements a light-weight custom incremental strategy do this. You can see at the end it calls the default get_incremental_delete_insert_sql DBT code.

{% macro get_incremental_custom_delete_insert_sql(arg_dict) %}

{% set custom_arg_dict = arg_dict.copy() %}

{% set source = custom_arg_dict.get('temp_relation') | string %}

{% set target = custom_arg_dict.get('target_relation') | string %}

{% if source is none %}

{{ exceptions.raise_compiler_error('temp_relation is not present in arguments!') }}

{% endif %}

{% if target is none %}

{{ exceptions.raise_compiler_error('target_relation is not present in arguments!') }}

{% endif %}

{% set raw_predicates = custom_arg_dict.get('incremental_predicates', []) %}

{% if raw_predicates is string %}

{% set predicates = [raw_predicates] %}

{% else %}

{% set predicates = raw_predicates %}

{% endif %}

{% if predicates %}

{% set replaced_predicates = [] %}

{% for predicate in predicates %}

{% set replaced = predicate

| replace('DBT_INTERNAL_SOURCE', source)

| replace('DBT_INTERNAL_DEST', target)

%}

{% do replaced_predicates.append(replaced) %}

{% endfor %}

{% do custom_arg_dict.update({'incremental_predicates': replaced_predicates}) %}

{% endif %}

{{ log('Calling get_incremental_delete_insert_sql with args: ' ~ custom_arg_dict, info=False) }}

{{ get_incremental_delete_insert_sql(custom_arg_dict) }}

{% endmacro %}

This is now callable from the DBT model config by setting incremental_strategy to custom_delete_insert.

- name: model_name

config:

materialized: "incremental"

incremental_strategy: "custom_delete_insert"

on_schema_change: "append_new_columns"

unique_key: ["dw_order_created_skey"]

cluster_by: ["to_date(order_created_at)"]

incremental_predicates:

- "order_created_at >= (select dateadd(hour, -24, min(order_created_at)) from DBT_INTERNAL_SOURCE)"

Performance Improvement Statistics

To find ~100K rows to delete in the target table, now snowflake has to only:

Less data is being scanned, so the snowflake warehouse is waiting less time on I/O, so the query completes faster.

Query plan