perf: make the append-only elementary tables actually append-only - #1044
Conversation
The model body is always empty (rows are inserted by the on-run-end hook), but unique_key made dbt emit a MERGE with a 'when matched then update' clause, which full-scans the whole table on every dbt run.
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
👋 @haritamar |
📝 WalkthroughWalkthroughFive incremental models remove explicit unique keys, add deduplication metadata, and use Elementary’s append-only incremental strategy. ChangesAppend-only incremental model configuration
Estimated code review effort: 1 (Trivial) | ~2 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
| }, | ||
| table_type=elementary.get_default_table_type(), | ||
| incremental_strategy=elementary.get_default_incremental_strategy(), | ||
| incremental_strategy=elementary.get_append_only_incremental_strategy(), |
There was a problem hiding this comment.
I think you mentioned we should also do the same change to other append only models?
(I'm wondering if it can also be in other artifacts as anyway we only modify them in the post hook / on run end hook)
There was a problem hiding this comment.
Yes — no elementary table's rows come from the model body (all writes are hook-side inserts/replaces), so the merge is dead weight everywhere. But the tables split into two groups:
Append-only history tables — elementary_test_results, dbt_source_freshness_results, data_monitoring_metrics, schema_columns_snapshot, plus test_result_rows (already has no unique_key). Same cost profile as dbt_run_results (they grow with history, so the scan gets more expensive over time). Blocker: none of them set meta.dedup_by_column, so dropping unique_key makes sync_elementary_schema flip them from upsert to replace_time_range — exactly the case you flagged with customers pruning old rows. Adding the meta is not free either: dump_table uses meta.dedup_by_column for its dedup and it currently defaults to unique_id, so setting it changes what a dedup=true dump collapses (e.g. elementary_test_results would go from deduping by unique_id to id) — needs to be checked per table rather than done blind.
Snapshot artifact tables — dbt_models, dbt_tests, dbt_sources, dbt_columns, etc. Written via delete+insert / full replace, so they're bounded by project size, not history — the wasted merge is small and constant. Removing unique_key there also flips internal to replace_time_range, which for full-replace semantics is arguably wrong.
So I'd keep this PR to dbt_run_results (the actual reported cost) and do the append-only group as a follow-up where each table gets the right meta.dedup_by_column plus a dump-behavior check. Want me to open that follow-up now?
Co-Authored-By: Itamar Hartstein <haritamar@gmail.com>
Co-Authored-By: Itamar Hartstein <haritamar@gmail.com>
Summary
dbt_run_results,elementary_test_results,dbt_source_freshness_results,data_monitoring_metricsandschema_columns_snapshotnever get rows from their model body — it is an emptyselect ... where 1=0, and the actual rows are inserted by theon-run-endhooks. But each was configuredmaterialized="incremental"with aunique_key, so everydbt runemitted a merge with awhen matched then updateclause against an empty source:The
when matchedclause plus an ON predicate with no partition filter makes BigQuery scan the entire target table — cost grows linearly with history and is paid on every run, for zero rows written. This is the most expensive statement in some users' projects (reported by an OSS user; also option 3 in elementary-data/elementary#1959, which was closed as stale).Per model:
Without a
unique_key, dbt generateson (FALSE) ... when not matched then insert, which doesn't read the target at all (same astest_result_rowstoday). Nothing is lost: uniqueness was never enforced by the materialization (rows bypass it via direct inserts) and dedup happens at read/dump time viameta.dedup_by_column, which is now set on all five models.One behavior change to be aware of:
dump_tablededups bymeta.dedup_by_column, defaulting tounique_id. Fordbt_source_freshness_results(the only newly-annotated model that has bothunique_idandgenerated_at) dumps now dedup bysource_freshness_execution_id, i.e. every freshness execution is kept rather than the latest per source. The other models have nounique_id/generated_at, so no dedup was happening and none starts.The snapshot-style artifact tables (
dbt_models,dbt_tests,dbt_sources, ...) are deliberately untouched: they are written with delete+insert / full replace and are bounded by project size rather than history, so the merge there is cheap and its upsert semantics are meaningful.Elementary Cloud's synced-schema loader must read the upsert key from
meta.dedup_by_column(elementary-data/elementary-internal#7249) and be deployed before this is released, otherwise these tables fall back to a time-range replace on sync.Note for existing users: tables also need a
--full-refreshto pick up thecreated_atpartitioning added in #939/#972.Link to Devin session: https://app.devin.ai/sessions/28a642e68cf64b94950aea4e21610310
Requested by: @haritamar
Summary by CodeRabbit