Why Your dbt Model Duplicates Rows (and How to Fix It)

A dbt incremental model doubles its row count on every run when it has no unique_key and no is_incremental() filter - each dbt run just appends the entire source table again. Here's why it happens and the two ways to fix it.

Data Engineerdbtidempotencymaterialization

Why an incremental model duplicates rows

When dbt materializes a model as incremental, it does one of two things on subsequent runs:

So a model like this:

{{ config(materialized='incremental') }}

SELECT order_id, customer_id, total
FROM raw_orders

Has no is_incremental() WHERE filter and no unique_key. On run #1 it builds 5 rows. On run #2 it appends another 5 - now 10 rows. Run #3: 15. Every nightly job makes the table grow by the full source size.

Fix 1: switch to table materialization

The simplest fix is to change the materialization to table. dbt then issues CREATE OR REPLACE TABLE on every run, so the result is always exactly the source rows - idempotent by construction:

{{ config(materialized='table') }}

SELECT order_id, customer_id, total
FROM raw_orders

Use table when the source is small enough that a full rebuild is cheap. This is the right default until you have a performance reason to optimize.

Fix 2: make the incremental model correct

If the source is large and full rebuilds are expensive, keep incremental but add both required guards:

{{ config(
    materialized='incremental',
    unique_key='order_id'
) }}

SELECT order_id, customer_id, total
FROM raw_orders

{% if is_incremental() %}
  WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}

Without both, the incremental materialization is a ticking row-count bomb.

How to verify the fix

cd /workspace
DBT_PROFILES_DIR=/workspace dbt run
psql -h 127.0.0.1 -U postgres -d app -c "SELECT COUNT(*) FROM orders;"
# run it again - count must not change
DBT_PROFILES_DIR=/workspace dbt run
psql -h 127.0.0.1 -U postgres -d app -c "SELECT COUNT(*) FROM orders;"

A correct model returns the same row count on every run. If the number climbs, the incremental guards are missing.

When to use each

Situation Materialization
Source is small, correctness matters most table
Source is large, full rebuild is too slow incremental + unique_key + is_incremental()
Never incremental without both guards

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

Why does my dbt model keep growing in row count?

You are probably using incremental materialization without a unique_key or an is_incremental() filter. Without these, dbt appends the full result set to the existing table on every run - doubling rows each time. Switch to table materialization or add both guards.

What is the difference between dbt table and incremental materialization?

A table model runs CREATE OR REPLACE TABLE every time - always rebuilds from scratch, always correct. An incremental model appends or upserts only new rows - faster on large sources but requires a unique_key and is_incremental() WHERE filter to avoid duplicates.

How do I make a dbt incremental model idempotent?

Set unique_key in the config block so dbt upserts instead of appending, and add a WHERE clause inside {% if is_incremental() %} that filters to only rows newer than what is already in the table. Both are required.

Keep learning

Make an ETL Pipeline IdempotentData projectSpeed Up a Slow SQL Report QueryData projectMake a CSV Importer Resilient to Bad DataData projectData roadmapStep by step to hiredData interview questionsSTAR answersAll Data projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →