Skip to content

Commit 4d05c57

Browse files
feat(optimize_spell): skip OPTIMIZE on incremental no-op runs (#9679)
Make optimize_spell conditional on rows_affected for incremental models: skip ALTER TABLE EXECUTE optimize when the just-run main statement wrote fewer than OPTIMIZE_MIN_ROWS rows (default 1000). Measurement across 5 representative cadence runs (Hourly, Tokens, Daily, DEX, Solana, last 7 days) showed ~25% of incremental model runs MERGE zero rows, with the OPTIMIZE post-hook accounting for an estimated ~30% of total dbt execute time. Several chainlink_bnb and chainlink_polygon models burn 700-1000+ seconds per Daily run with 0 rows merged. The threshold is overridable globally via --vars '{OPTIMIZE_MIN_ROWS: N}' or per-model via the optimize_min_rows config. Table materialization keeps the existing always-optimize behavior. Estimated savings (per cadence cycle): 5h at threshold=1, 11h at threshold=1000, 13h at threshold=10000. Conservative go-live default of 1000 captures most of the win with minimal risk. Co-authored-by: jeff-dude <102681548+jeff-dude@users.noreply.github.com>
1 parent c63ec2f commit 4d05c57

1 file changed

Lines changed: 52 additions & 3 deletions

File tree

dbt_macros/dune/optimize_spell.sql

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,59 @@
1+
{#-
2+
optimize_spell: post-hook that runs Delta-Lake OPTIMIZE on a materialized
3+
model, conditional on the most recent main statement having affected enough
4+
rows to make compaction worthwhile.
5+
6+
Background: prior versions ran `ALTER TABLE ... EXECUTE optimize` on every
7+
prod incremental model regardless of whether the MERGE wrote any rows. About
8+
one in four incremental cadence runs writes 0 rows (no new source data, no
9+
late-arriving rows in the lookback window), making OPTIMIZE pure overhead.
10+
Measured across recent cadence runs (Hourly, Tokens, Daily, DEX, Solana),
11+
the OPTIMIZE post-hook accounts for an estimated ~30% of total dbt execute
12+
time, and ~12% is burned on zero-row models alone.
13+
14+
This macro now skips OPTIMIZE when `rows_affected < OPTIMIZE_MIN_ROWS`
15+
(default 1000) for incremental models. The threshold is overridable
16+
per-invocation via `--vars '{OPTIMIZE_MIN_ROWS: N}'` or per-model via the
17+
`optimize_min_rows` config.
18+
19+
`table` materialization is unchanged: CTAS replaces the table wholesale,
20+
so we keep the conservative always-optimize behavior there for now.
21+
22+
Small-file accumulation on rarely-touched tables is mitigated by Delta's
23+
internal compaction and can be addressed by a separate, scheduled OPTIMIZE
24+
pass (out of scope for this PR).
25+
26+
Testing affordance: setting `OPTIMIZE_SPELL_FORCE=true` enables the macro
27+
on non-prod targets so the conditional logic can be exercised against a
28+
personal schema. Default is false (behavior unchanged on dev/ci targets).
29+
-#}
130
{% macro optimize_spell(this, materialization) %}
2-
{%- if target.name == 'prod' and materialization in ('table', 'incremental') -%}
31+
{%- set is_optimize_target = (target.name == 'prod') or var('OPTIMIZE_SPELL_FORCE', false) -%}
32+
{%- if not is_optimize_target -%}
33+
{#- non-prod (and OPTIMIZE_SPELL_FORCE not set): no-op (unchanged) -#}
34+
{%- elif materialization not in ('table', 'incremental') -%}
35+
{#- views and other materializations: nothing to compact -#}
36+
{%- elif materialization == 'table' -%}
37+
{#- full refresh table: keep existing always-optimize behavior -#}
338
{%- if target.type == 'trino' -%}
4-
ALTER TABLE {{this}} EXECUTE optimize
39+
ALTER TABLE {{ this }} EXECUTE optimize
540
{%- else -%}
6-
OPTIMIZE {{this}};
41+
OPTIMIZE {{ this }};
742
{%- endif -%}
843
{%- else -%}
44+
{#- incremental: gate on rows actually written by the main statement -#}
45+
{%- set main_result = load_result('main') -%}
46+
{%- set rows_affected = 0 -%}
47+
{%- if main_result is not none and main_result.response is not none and main_result.response.rows_affected is not none -%}
48+
{%- set rows_affected = main_result.response.rows_affected | int -%}
49+
{%- endif -%}
50+
{%- set min_rows = config.get('optimize_min_rows', var('OPTIMIZE_MIN_ROWS', 1000)) | int -%}
51+
{%- if rows_affected >= min_rows -%}
52+
{%- if target.type == 'trino' -%}
53+
ALTER TABLE {{ this }} EXECUTE optimize
54+
{%- else -%}
55+
OPTIMIZE {{ this }};
56+
{%- endif -%}
57+
{%- endif -%}
958
{%- endif -%}
1059
{%- endmacro -%}

0 commit comments

Comments
 (0)