Practice — dbt & Analytics Engineering (4 questions)
Debugging an Incremental Model That Produces Duplicate Rows
Your team's fct_subscriptions incremental model is configured like
this:
{{ config(
materialized='incremental',
unique_key='subscription_id',
incremental_strategy='merge'
) }}
select
subscription_id,
customer_id,
plan_tier,
status,
updated_at
from {{ ref('stg_subscriptions') }}
{% if is_incremental() %}
where updated_at > (select max(updated_at) from {{ this }})
{% endif %}
Two problems have shown up in production:
- Finance reports that some
subscription_ids appear twice infct_subscriptionswith differentstatusvalues (e.g., one rowpending, anotheractive), even thoughunique_keyis set. - Separately, an upstream retry in the billing system occasionally
re-emits an
updated_attimestamp for a subscription that is older than the current maxupdated_atalready in the table (a delayed correction from a downstream system), and that correction never appears infct_subscriptionsat all.
- Explain exactly why
unique_keydid not prevent the duplicate rows Finance is seeing — what has to be true about the incoming batch formergeto still produce duplicates. - Explain why the late correction is silently dropped, mechanically.
- Rewrite the model to fix both problems, and explain what each change fixes.
Share this question
Choosing the Wrong Materialization Is Burning Warehouse Spend
A finance team flags that your warehouse bill jumped noticeably last month. Investigating, you find:
int_orders_with_payments, an intermediate model joining orders to aggregated payments (a moderately expensive join across ~40M rows), is materialized asview. It'sref()'d by four different marts models (fct_orders,fct_revenue_by_region,fct_customer_ltv, andfct_refunds), each queried directly by a Looker dashboard that auto-refreshes every 5 minutes.stg_page_views, a thin staging model over a raw clickstream table with light column renaming and no joins, is materialized astable, rebuilt in full on every hourly run even though only one downstream model (int_sessions) ever reads it, and it's rebuilt completely every run regardless of how much new clickstream data actually arrived.
- Explain the specific cost mechanism behind each of these two choices — why each one is more expensive than it needs to be.
- Propose the correct materialization (or strategy) for each model, and justify the trade-off you're accepting by changing it.
stg_page_viewssits at the very top of the DAG, directly on a source table with genuinely new rows arriving continuously. Would you consider incremental for it, or is a different fix more appropriate here — and why?
Share this question
A Model Is Missing From the Lineage Graph and Running Out of Order
A teammate opens dbt docs serve to trace how fct_customer_ltv is
built and notices it doesn't show any connection to
int_orders_with_payments in the lineage graph, even though
fct_customer_ltv.sql clearly reads from it:
-- models/marts/fct_customer_ltv.sql
{{ config(materialized='table') }}
select
customer_id,
sum(total_amount) as lifetime_value
from analytics_prod.intermediate.int_orders_with_payments
group by 1
Separately, a CI run on a PR that only changed
int_orders_with_payments.sql did not rebuild or retest
fct_customer_ltv at all, even though the PR's intent was to fix a
bug in int_orders_with_payments that clearly affects LTV
calculations downstream.
- Diagnose the root cause connecting the missing lineage edge and the CI gap — they are the same bug. Explain precisely why.
- Fix the model.
- Beyond fixing this one model, what would you check across the rest of the project to make sure this isn't a systemic issue, and how would you check it without manually reading every model file?
Share this question
Reviewing a PR That Breaks the Staging/Intermediate/Marts Layering
A teammate opens a PR adding a new model,
models/staging/stg_orders_with_ltv.sql:
-- models/staging/stg_orders_with_ltv.sql
{{ config(materialized='view') }}
select
o.id as order_id,
o.customer_id,
o.status as order_status,
c.signup_date,
sum(p.amount) over (partition by o.customer_id) as customer_ltv
from {{ source('raw_app', 'orders') }} as o
left join {{ ref('stg_customers') }} as c
on o.customer_id = c.customer_id
left join {{ ref('stg_payments') }} as p
on o.id = p.order_id
The PR description says: "Added a staging model that gives us orders with customer signup date and lifetime value pre-joined, so marts don't have to redo this join every time."
- Explain specifically what's wrong with placing this model in
staging/, independent of whether the SQL itself is correct. - Where should this logic actually live, and how would you restructure it, potentially into more than one model?
- The teammate pushes back: "it works fine and it's faster to just put everything in one folder." What's the concrete cost of that approach as the project grows past this one model?
Share this question