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.
Why an incremental model duplicates rows
When dbt materializes a model as incremental, it does one of two things on subsequent runs:
- If a
unique_keyis set, it merges/upserts on that key. - If no
unique_keyis set, it appends the result of your SELECT to the existing table.
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 %}
unique_key='order_id'tells dbt to upsert on that column instead of blind-append.is_incremental()isfalseon the first full build andtrueon every subsequent run - the WHERE clause then filters to only rows newer than what is already in the table.
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
- Switching a dbt model from incremental to table materialization
- Adding a unique_key and is_incremental() filter to an incremental model
- Verifying idempotency by running dbt twice and checking the row count
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
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 →