Skip to the page
Tecyfy

dbt cluster_by on Snowflake: the ORDER BY it adds

Setting cluster_by on a dbt Snowflake model does 2 things: it sorts the whole result with an ORDER BY when the table is built, and it adds a clustering key. dbt documents the sort as intended, but on a large table model that rebuilds every run, or on a full refresh, the sort is a large share of the build. How to see it, find every model it touches, and decide per model.

Tecyfy · Published · 6 min read

A paper machine labelled ORDER BY sorts a messy pile of snowflake ice cubes into a neat stack: the sort dbt cluster_by adds on Snowflake

Setting cluster_by on a dbt Snowflake model does 2 things: when dbt creates the table, it sorts the whole result with an ORDER BY on those columns, and then it adds them as the table's clustering key. dbt documents the sort as intended, so that the table starts out clustered. The catch is when the whole table is built: a large table model sorts all its rows on every run, and an incremental model sorts everything on each --full-refresh. Keep cluster_by only where the clustering earns that sort.

What dbt sends to Snowflake

For a model like this:

{{ config(materialized='table', cluster_by=['order_date']) }}

select order_id, customer_id, order_date, amount_usd
from {{ ref('stg_orders') }}

dbt-snowflake's create table macro wraps your query in a sort and clusters the table afterwards. The SQL it runs has this shape:

create or replace table analytics.marts.orders as (
    select * from (
        select order_id, customer_id, order_date, amount_usd
        from analytics.staging.stg_orders
    )
    order by (order_date)
);

alter table analytics.marts.orders cluster by (order_date);

Without cluster_by, the same model is a plain create or replace table ... as (select ...), with no sort. dbt's own docs show this example and explain the reason: a table built in key order leaves Snowflake's automatic clustering less to do afterwards.

When the sort runs

From dbt-snowflake's code as of this writing:

Model and runSorts the whole result?
table, every runYes: every run rebuilds the table
incremental, first buildYes
incremental, --full-refreshYes
incremental, a normal runNo: new rows are staged in a view or temporary table and merged unsorted

So the cost lands where the issue reporters found it: a full refresh of a big incremental model, and, less noticed, a big table model that rebuilds every night.

Try itWhich dbt runs sort the whole table?
Full refresh
500 GB
Sorts the whole tableMerges new rows

Day 2order by on every rowa table model rebuilds the whole table every run

create or replace table analytics.marts.orders as (
    select * from (
        select order_id, customer_id, order_date, amount_usd
        from analytics.staging.stg_orders
    )
    order by (order_date)
);

alter table analytics.marts.orders cluster by (order_date);

30 of 30 nightly runs sort the whole 500 GB table: 15 TB sorted in a month. Making it incremental leaves the sort to the first build and full refreshes.

A model with cluster_by=['order_date'], run once a night for 30 days. The SQL is the shape dbt-snowflake sends, shortened; your project's names, merge keys and temporary relation type change the details. Every sort of the whole table needs memory, and spills to disk once it runs out.

Why it costs credits

Sorting 2 billion rows is far more work than writing them. A sort of that size needs the warehouse's memory, and when that runs out the data spills to local and then remote disk, which Snowflake's query history records for each statement. The build runs longer, and a longer build is more credits on the same warehouse, or a larger warehouse to keep it on time.

The sort also buys something. Without it, Snowflake's automatic clustering would reorganise the new table in the background, and that uses credits too. Which costs more depends on the table: we have not measured it, and the dbt issue gives no figures. Measure on your own table (below).

See it in your own project

dbt compile shows only your SELECT. The ORDER BY is in the statement dbt runs:

  1. Run the model: dbt run -s orders.
  2. Open target/run/<your_project>/models/.../orders.sql. The create or replace table with order by is there.

To compare the cost, find the builds in Snowflake's query history:

select start_time, warehouse_size,
       total_elapsed_time / 1000 as seconds,
       bytes_spilled_to_local_storage, bytes_spilled_to_remote_storage
from snowflake.account_usage.query_history
where query_text ilike '%create or replace%table analytics.marts.orders%'
order by start_time desc
limit 20;

Run it after a build with cluster_by and after one without (on a dev target); the view can lag by up to 45 minutes. Spill to remote storage is the sign the sort is the expensive part.

Find every model with cluster_by

After dbt parse, the manifest lists every model's config:

jq -r '.nodes[] | select(.resource_type == "model" and .config.cluster_by != null)
  | [.name, .config.materialized, (.config.cluster_by | tostring)] | @tsv' target/manifest.json

The list shows each model's materialization, so the table models, which sort on every run, stand out.

Your options

  1. Drop cluster_by where clustering does not pay. Clustering is usually worth it only on tables of several terabytes, queried often with filters on the key, and changed rarely. A 50 GB mart gains little from it and pays for the sort every run.
  2. Make a big clustered table incremental. The sort then happens only on the first build and on full refreshes, and you choose when those run.
  3. Full-refresh less, and on purpose. Run --full-refresh for one model when its logic changes, not as part of a routine job, and on a warehouse sized for it.
  4. Keep the clustering key, skip the sort. Set the key yourself in a post-hook (alter table {{ this }} cluster by (order_date)) and remove cluster_by, so automatic clustering does the ordering in the background. You trade the build-time sort for background clustering credits, so measure both.
  5. Override dbt's macro. dbt looks for dispatched macros in your project first, so a snowflake__create_table_as in your macros/ folder replaces the adapter's. It works, but you own a copy of dbt-snowflake's code and must check it on every upgrade; options 1 to 4 are easier to live with.

Keep or drop: a check per model

  • Is the table several terabytes, or heading there within the year?
  • Do the slow queries on it filter or join on the cluster_by columns?
  • Is it read far more often than it is rebuilt?
  • Is it incremental, so the sort runs only on a full refresh?

4 yeses: keep it. A no on the first 2: drop it. A table model with 4 yeses on the rest: make it incremental.

Where to follow it

The report is dbt issue 16685, open and awaiting triage as of 19 October 2026. Since dbt's docs describe the sort as intended, a change, if one comes, is more likely to be a setting to turn it off than a removal. Watch the dbt-snowflake changelog.

For the warehouseData WarehouseA database built for analysis: cleaned, structured data from many systems, kept in one place and shaped for fast reports and dashboards. bill itself, the Snowflake cost calculator prices a warehouse by size, hours and idle time before suspend.

Questions

Does cluster_by in dbt add an ORDER BY on Snowflake?
Yes. When a table is created with cluster_by, dbt-snowflake wraps the model's query in select * from ( ... ) order by (your columns) and then runs alter table ... cluster by. dbt's docs say this is deliberate: building the table already sorted saves Snowflake's automatic clustering work later.
Does the ORDER BY run on incremental runs too?
In the current dbt-snowflake code, no. The sort is in the statement that creates the whole table, so it runs on a table model's every build and on an incremental model's first build and every --full-refresh. A normal incremental run stages the new rows in a view or temporary table without sorting and merges them in.
How do I see the SQL dbt sends to Snowflake for a model with cluster_by?
Run the model and open its file under target/run/. target/compiled/ holds only your SELECT; target/run/ holds the full create table statement with the ORDER BY and the alter table ... cluster by that follows it.
Should I remove cluster_by from my dbt models?
From any table under a few terabytes, and from tables whose queries do not filter on the key: clustering usually pays off only on multi-terabyte tables that are queried often and change rarely. For a large table that does benefit, keep it, and make the model incremental so the full sort happens only on a full refresh.
Is this a dbt bug?
It is reported as one in dbt issue 16685 (open, in triage as of 19 October 2026), but dbt's documentation describes the ORDER BY as intended. Treat it as a documented trade-off: a sort at build time in exchange for less background reclustering.