Skip to content

perf: prune partitions in get_models_runs - #2352

Draft
tlangton3 wants to merge 4 commits into
elementary-data:masterfrom
tlangton3:perf/prune-get-models-runs
Draft

perf: prune partitions in get_models_runs#2352
tlangton3 wants to merge 4 commits into
elementary-data:masterfrom
tlangton3:perf/prune-get-models-runs

Conversation

@tlangton3

@tlangton3 tlangton3 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Important

Draft — depends on elementary-data/dbt-data-reliability#1057, which exposes created_at on the model_run_results view. Until that is merged and this project's package dependency points at it, the guard below has no column to prune on and CI here will fail. Opening now so the change is ready; happy to rebase or close and re-open once the ordering suits you.

It also stacks on #2351, which adds the days_back_filter macro this uses. The diff shown will shrink to a single file once that merges.

What

get_models_runs computes row_number() over the entire model_run_results view and only then filters to days_back:

with model_runs as (
    select *, row_number() over (partition by unique_id order by generated_at desc) ...
    from {{ ref('elementary', 'model_run_results') }}     -- unbounded
)
select ...
from model_runs
where {{ edr_datediff(edr_cast_as_timestamp('generated_at'), now, 'day') }} < days_back

So every report run reads all history. On one warehouse this is the single most expensive query the CLI issues: ~12.4 GB per invocation against a 17M row / 41GB table, and ~1,115 GB over a week.

The fix

Moving the filter into the CTE and bounding created_at, which is the column BigQuery can prune on.

Worth being precise that the filter move earns nothing by itself — it is the created_at bound that prunes. I had also replaced select * in the CTE with an explicit column list, and dropped it again after measuring: BigQuery already prunes to the columns a query references, so both shapes read 31,868 MB, identical. It was ten lines and a second place to maintain the column set for no gain.

On the warehouse above, the 8-day window is 0.85% of the table (146,402 of 17,207,248 rows across 779 partition-days), so this should fall to a fraction of a GB per invocation. I cannot post a measured after-figure until #1057 is in, and would rather leave it blank than estimate.

Why the move is safe

The filter can move above the window function without changing any returned row's rank. For any row R inside the window, every row ranked above R for the same unique_id is newer than R, and therefore also inside the window. So no row that contributed to R's rank is removed, and R's rank is identical either way — which also preserves the invocations_rank_index = 1 → compiled_code selection exactly.

Confirmed rather than argued, over a 7-day window on real data:

rows_in_window          39,185
rank_mismatches              0
compiled_code rows (old) 1,678
compiled_code rows (new) 1,678

Non-BigQuery adapters

Unchanged. default__days_back_filter renders the same edr_datediff(edr_cast_as_timestamp(...)) < days_back predicate this macro already used; only its position in the query moves, and that is rank-preserving as above.

exclude_elementary deliberately stays in the outer query. It could move into the CTE too — filtering whole unique_id groups cannot affect ranks within other groups — but it prunes nothing, so I left the change surface as small as possible.

`current_tests_run_results_query` filters both of its large CTEs with
`edr_datediff(edr_cast_as_timestamp(col), now, 'day') < days_back`. On BigQuery a
function applied to the column defeats partition pruning, so both scan all
history however small `days_back` is.

Adds a dispatched `days_back_filter` macro: the default implementation renders
exactly the predicate above, so nothing changes off BigQuery, while `bigquery__`
compares the column directly and adds a bound on the partition column.

Measured with a 7-day lookback, selecting a single column, on tables as they
already are — no repartitioning needed to get this:

    elementary_test_results (12M rows, 13GB)   1,368 MB -> 13.68 MB
    dbt_run_results         (17M rows, 41GB)   1,627 MB -> 63.56 MB

For reference, an unfiltered scan of elementary_test_results reads 1,279 MB: the
current predicate reads *more* than no filter at all, since it also pays to read
the column it filters on.

The bound on `created_at` looks redundant — it is implied by the first predicate —
but BigQuery only prunes on the raw partition column, and `execute_completed_at`
is stored as a string, so a cast is unavoidable there and no predicate on it can
prune. dbt_run_results is not even partitioned on the warehouse measured above;
the 26x comes from block-level pruning on `created_at` alone, and partitioning it
would improve that further.

The bound is also safe: `created_at` is written at or after both columns, so any
row satisfying the first predicate satisfies the bound. Verified over 17.2M rows —
`created_at - generated_at` >= 0s, `created_at - execute_completed_at` >= 5s, and
zero rows violating either.

The guard is given a day of slack rather than the same bound: `created_at` is
stamped by the warehouse at insert while the columns it guards come from the dbt
client, so the two are not the same clock. Pruning is at day granularity, so the
slack costs at most one extra partition — measured as no difference at all on the
figures above, since the real predicate is the binding constraint.
…ilter

elementary-data#1940 gave this macro a `bigquery__` override that duplicated the whole body just
to change its `where` clause. Now that `days_back_filter` dispatches on the
predicate itself, the two copies collapse back into one and the dispatch
disappears.

Behaviour is unchanged on every adapter: `default__days_back_filter` renders the
datediff form the original had, and `bigquery__days_back_filter` renders the
direct comparison the override had.

It also gains the `created_at` bound. test_result_rows declares no partitioning
today, so the direct comparison on `detected_at` was relying on block-level
pruning alone; the bound takes a 7-day lookback from 1,060 MB to 101 MB on a 26M
row / 41GB copy of that table, and improves further once it is partitioned.
@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

👋 @tlangton3
Thank you for raising your pull request.
Please make sure to add tests and document all user-facing changes.
You can do this by editing the docs files in this pull request.

Only 4 of 48 macros here carry a doc block and none exceeded 12 lines; this had
24. Keeps the two things a reader cannot infer and would otherwise 'tidy' away —
that the second bound is what makes BigQuery prune, and that its day of slack is
deliberate — and drops the rest, which the PR description already covers.

Also builds the predicate by joining a list rather than capturing a {% set %}
block, so the compiled SQL is a single line instead of carrying the macro's
indentation and blank lines into every query.
`get_models_runs` computes `row_number()` over the whole of model_run_results and
only then filters to `days_back`, so the report reads all history on every run.

Moves the filter into the CTE and gives it a `created_at` bound to prune on. The
move is safe: for any row inside the window, every row ranked above it for the
same unique_id is newer, and therefore also inside the window — so ranks are
unchanged for every row the outer query returns, and the `rank = 1 ->
compiled_code` selection is preserved exactly. Confirmed against 39,185 rows in a
7-day window: zero rank differences, and compiled_code selected for the same 1,678
rows either way.

Requires elementary-data/dbt-data-reliability#1057, which exposes `created_at` on
the model_run_results view. Without it this cannot prune, since the view's other
timestamps are strings and a cast defeats pruning.
@tlangton3
tlangton3 force-pushed the perf/prune-get-models-runs branch from d9c1653 to 74d795b Compare September 11, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant