Models & Materializations • Incremental Models • Tests & Snapshots • Macros & Jinja • CI • 2026

dbt Interview Questions

30 questions What each one tests, an answer frame, a spoken answer 34 min read

This page is for analytics engineers, data engineers and analysts facing a dbt round. Most interviews start with what dbt does, materializations, ref and source, then go deep on incremental models, tests, documentation and snapshots. Senior rounds add Jinja and macros, schema naming, CI that builds only what changed, a slow model to fix and a few judgement calls about shared tables. Each question shows what the interviewer is really checking, the shape of a strong answer and a short answer you can say out loud. Practise saying them, then swap the stories for your own. Warehouse internals and SQL basics have their own pages.

Search all questions by round, difficulty and level, or save the ones you want to practise.

Project Basics 4 questions

Easy Technical round Fresher, Mid-level Practice question

1. In your own words, what does dbt do in a data pipeline, and what does it not do?

What the interviewer is really testing:
Whether you know where dbt sits in an ELT stack and can separate transformation from loading and scheduling.
Answer frame:

Role: the T in ELT; data is already loaded, dbt turns it into clean models inside the warehouse.

How: SQL select files plus Jinja, compiled to plain SQL and run in dependency order.

Extras: tests, documentation and lineage, all in version control.

Not its job: extracting or loading data; something else moves data in and triggers runs.

Sample spoken answer:

"dbt is the transform step in an ELT setup. The data is already loaded into the warehouse by some other tool, and dbt lets me write each transformation as a select statement in its own file. It adds Jinja on top, so I can reference other models, reuse logic in macros and change behaviour by environment. When I run it, dbt compiles everything to plain SQL, works out the order from the references, and runs it inside the warehouse, so the warehouse does the heavy lifting. Around that it gives me tests, documentation and lineage, all in version control. What it doesn't do is extract or load data, and it isn't a general scheduler for the whole pipeline. Something else moves the data in, and usually something else triggers the dbt run."

Red flag to avoid:

Describing dbt as a tool that pulls data from source systems, or as a database of its own.

They may ask next:
  • Why do you think teams moved transformations out of ETL tools and into SQL in the warehouse?
  • What does dbt actually send to the warehouse when you run a model?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

2. What is the difference between dbt run, dbt test and dbt build, and which would you schedule in production?

What the interviewer is really testing:
Whether you understand that build interleaves tests with models, so a failed test stops bad data from flowing downstream.
Answer frame:

run: builds models only.

test: runs tests against what is already built.

build: seeds, models, snapshots and tests in dependency order; a failed test skips everything downstream.

Sample spoken answer:

"dbt run builds models only. dbt test runs the tests against whatever is already built. dbt build does everything in one pass, in dependency order: seeds, models, snapshots and tests. The big difference is how failures flow. With build, each model's tests run right after that model, and if a test fails, dbt skips everything downstream of it, so bad data doesn't spread into the marts. If I schedule run and then test as two steps, every model has already been rebuilt by the time a test fails, and dashboards may already be showing bad numbers. So in production I schedule dbt build, often with a selector for one group of models. I use run on its own mostly in development, when I'm iterating quickly on a single model."

Red flag to avoid:

Scheduling dbt run followed by dbt test in production and not seeing why a failed test then arrives too late.

They may ask next:
  • How would you rerun only one failed model and everything downstream of it?
  • Does a test set to warn severity skip downstream models in dbt build?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

3. What are seeds in dbt, and what should and shouldn't go in them?

What the interviewer is really testing:
Whether you use seeds for small, static, reviewed reference data and know their limits.
Answer frame:

What: CSV files in the seeds folder, loaded by dbt seed, used through ref.

Good fit: small static lookups that change rarely and belong with the code.

Bad fit: large or fast-changing data, data owned by another system, anything sensitive.

Sample spoken answer:

"Seeds are CSV files kept in the project's seeds folder. Running dbt seed loads them into the warehouse as tables, and models reference them with ref like any other model. They're for small, static reference data that belongs with the code and changes rarely: a mapping of country codes to regions, a list of test accounts to exclude, a lookup of product categories. Because they live in git, every change is reviewed and versioned. What shouldn't go in them: large data, because loading CSVs through dbt is slow and not what it's built for; data that changes often or comes from another system, which the ingestion tool should load as a source; and anything sensitive like personal data or passwords, because it ends up in the repository. I also set column types when needed, so a code like 007 isn't read as the number 7."

Red flag to avoid:

Using seeds to load large data extracts or files with customer details into the warehouse.

They may ask next:
  • A business team wants to edit a mapping every week. Is a seed still the right place?
  • How do you add tests to a seed?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

4. How do you add a package like dbt_utils to a project, and what would you use it for?

What the interviewer is really testing:
Whether you know how packages are installed and pinned, and reuse proven macros instead of rewriting them.
Answer frame:

Install: list it in packages.yml with a version range, then run dbt deps.

Use: surrogate keys, date spines, star with exclusions, extra tests.

Care: pin versions, upgrade on purpose, keep the list short.

Sample spoken answer:

"I list the package in packages.yml with a version range, then run dbt deps, which downloads it into the project so its macros and tests are available. dbt_utils is the one almost every project has. I use generate_surrogate_key to build a hashed key from several columns, date_spine to make a continuous calendar, star to select all columns except a few, and extra tests like unique_combination_of_columns for tables whose key is more than one column. I always pin to a version range rather than taking whatever is latest, because an upgrade can change how a macro behaves, and I'd rather upgrade on purpose and test it in CI. I also keep the number of packages small. Every package is code I'm trusting inside my build, so I check it's maintained before adding it."

Red flag to avoid:

Leaving package versions unpinned so a build can change behaviour overnight with no code change.

They may ask next:
  • Why would you build a surrogate key instead of using a natural key?
  • What would you check before upgrading a package across a major version?
Say it in 60 seconds

Models & Configs 3 questions

Easy Technical round Fresher, Mid-level Practice question

5. What materializations does dbt offer, and how do you choose between a view, a table, incremental and ephemeral?

What the interviewer is really testing:
Whether you understand the build-time versus query-time trade-off behind each materialization, not just the names.
Answer frame:

View: the default; cheap to build, always fresh, computed every time it is read.

Table: stored result, fast to read, fully rebuilt each run.

Incremental: built once, then only new or changed rows are processed.

Ephemeral: never built; pasted into downstream models as a CTE.

Sample spoken answer:

"The four I use most are view, table, incremental and ephemeral, and view is the default. A view is cheap to build and always fresh, but the query runs every time someone reads it, so I use views for light staging models. A table stores the result, so reads are fast, but each run rebuilds it fully. That suits marts people query a lot, as long as the rebuild is affordable. Incremental builds the table once and then only processes new or changed rows on later runs, which I reach for when the data is large and a full rebuild takes too long. Ephemeral isn't built in the warehouse at all; dbt pastes it into the models that use it as a CTE. I use that sparingly, because you can't query it directly when debugging."

Red flag to avoid:

Saying incremental is always better because it is faster, without mentioning the extra complexity and the late-data risk.

They may ask next:
  • What goes wrong when a mart sits on top of a long chain of views?
  • Why might you avoid making many models ephemeral?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level, Senior Practice question

6. How do you structure the models folder in a dbt project, and what belongs in staging, intermediate and marts?

What the interviewer is really testing:
Whether you can keep a project maintainable as it grows, with each layer doing one clear job.
Answer frame:

Staging: one model per source table; rename, cast, light cleanup; the only layer that reads sources.

Intermediate: joins, dedupe and reshaping shared by several marts; not for end users.

Marts: facts, dimensions and team tables in business language.

Payoff: one place to fix a raw change, and a clear place to look when a number is wrong.

Sample spoken answer:

"I use three layers. Staging has one model per source table, and it only does light cleanup: renaming columns to a consistent style, casting types, maybe converting units. No joins and no business logic, and it's the only layer that reads from source. Intermediate is where the harder logic lives: joins, deduplication and reshaping that several marts need but no business user should query directly. Marts are the finished tables people use, facts and dimensions or wide tables for a team like finance, named in business language. The payoff is that each layer has one job. When a number looks wrong I know where to look, and a renamed column in a raw table only touches one staging model. I usually make staging views and marts tables or incremental, set at folder level in the project file."

Red flag to avoid:

Putting joins and business rules straight on raw tables in every mart, so one upstream rename breaks many models.

They may ask next:
  • Would you ever let a mart read a source directly?
  • How do you name models so people can tell the layer at a glance?
Say it in 60 seconds
Hard Behavioral round Senior Practice question

7. Tell me about a dbt project you inherited that was hard to work with. What did you change, and in what order?

What the interviewer is really testing:
Whether you can improve a live project in safe, prioritised steps rather than a risky big rewrite.
Answer frame:

Situation: what made it hard: hard-coded names, no tests, duplicated logic.

Order: sources and staging, then ref, then tests, then consolidation.

Safety: small pull requests and before-and-after checks.

Result: what got easier or safer.

Sample spoken answer:

"In my last role I took over a project with around two hundred models, and most marts read straight from raw tables with hard-coded names. Nobody trusted the lineage, and one upstream rename broke a dozen models. I didn't try to rewrite it all. First I declared the raw tables as sources and added staging models for the ones used most, so each raw table was read in one place. Then I replaced hard-coded names with ref and source, which fixed the build order and gave us a real lineage graph. Next I added unique and not_null tests on every mart's key, and that immediately found two models with duplicate rows from a bad join. Only after that did I start merging models that repeated the same logic. I did it in small pull requests over a couple of months, comparing row counts and key totals before and after each change."

Red flag to avoid:

Proposing to rebuild the whole project from scratch in one go while others depend on it.

They may ask next:
  • How did you convince people to wait for the cleanup rather than a rewrite?
  • What did you leave alone on purpose, and why?
Say it in 60 seconds

Sources & Refs 2 questions

Easy Technical round Fresher, Mid-level Practice question

8. Why do you use ref() instead of writing the schema and table name directly in a model?

What the interviewer is really testing:
Whether you know that ref drives the dependency graph and environment-aware naming, which is the core of how dbt works.
Answer frame:

Dependencies: ref tells dbt the build order and draws the lineage graph.

Environments: ref resolves to the right database and schema for dev, CI or prod.

Rule: raw tables through source, dbt-built models through ref, nothing hard-coded.

Sample spoken answer:

"ref does two jobs. First, it tells dbt that this model depends on another one, so dbt can build the dependency graph, run things in the right order and show lineage in the docs. If I hard-code a table name, dbt has no idea that dependency exists, so it might run my model before its parent is rebuilt. Second, ref resolves to the right database and schema for whichever environment I'm running in. In development it points at my personal schema, in production at the production schema, and I never change the code. It's also what makes selecting a model plus everything downstream work. The rule I follow is simple: raw tables come in through source, everything built by dbt goes through ref, and there are no hard-coded names in model SQL."

Red flag to avoid:

Saying ref is just a shortcut for typing the table name, with no mention of build order or environments.

They may ask next:
  • What is the difference between ref and source?
  • If two models ref each other, what does dbt do?
Say it in 60 seconds
Medium Technical round Mid-level Practice question

9. How do you declare sources in dbt, and how would you get warned when a source table stops receiving new data?

What the interviewer is really testing:
Whether you treat raw data as a declared, monitored input rather than a table you hope is up to date.
Answer frame:

Declare: a sources block in YAML with the schema and tables the loader writes to.

Use: models read them through source, so raw tables appear in lineage and can be tested.

Freshness: a loaded_at_field plus warn_after and error_after, checked by dbt source freshness.

Sample spoken answer:

"I declare raw tables in a YAML file under a sources block: a source name, the schema where the loader writes, and the list of tables. Models then read them with the source function instead of a hard-coded name, so raw tables show up in the lineage graph and I can test them like any model. For staleness, I set a loaded_at_field, which is a timestamp column the loader fills, and freshness rules with a warn_after and an error_after. Running dbt source freshness compares the newest value in that column with those rules. I schedule that before the main build. So if the loader silently stopped overnight, we hear about it from a warning or a failed job, not from a stakeholder asking why yesterday's numbers are flat. I usually add not_null and unique on the source's key as well."

Code:
sources:
  - name: app
    schema: raw_app
    loaded_at_field: _loaded_at
    freshness:
      warn_after: {count: 6, period: hour}
      error_after: {count: 24, period: hour}
    tables:
      - name: orders
      - name: customers
Red flag to avoid:

Reading raw tables by hard-coded name and relying on someone noticing a flat chart to spot a stopped load.

They may ask next:
  • Which column would you pick as the loaded_at_field if the source has both an updated_at and a load timestamp?
  • Should a stale source stop the whole dbt run or only warn?
Say it in 60 seconds

Testing & Docs 7 questions

Medium Technical round Mid-level, Senior Practice question

10. What is the difference between a unit test and a data test in dbt, and when is a unit test worth writing?

What the interviewer is really testing:
Whether you know that unit tests check a model's logic on fixed input rows, while data tests check the real data after it is built.
Answer frame:

Data tests: run against built tables; catch bad or unexpected data.

Unit tests: fixed input rows and expected output in YAML; catch logic mistakes before the model is built.

Worth it for: tricky logic like many-branch case rules, date math, window functions and past bugs.

Where: development and CI; usually left out of production runs.

Sample spoken answer:

"A data test checks the real data after a model is built, like unique or not_null on a key. It tells me the data is wrong, but not whether my SQL is right. A unit test checks the logic. In YAML I give the model a few fixed input rows for the models it reads, and the exact rows I expect out. dbt runs the model's SQL on those rows and compares the result. With dbt build, a model's unit tests run before the model itself, so broken logic never reaches the table. I don't write them for simple renames. They're worth it where logic is easy to get wrong: a case statement with many rules, date math around month ends, a window function, or an edge case that bit us before, like a refunded order. Because the inputs are fixed, I run them in development and CI and usually leave them out of production runs."

Code:
unit_tests:
  - name: refunded_orders_have_zero_net
    model: fct_orders
    given:
      - input: ref('stg_orders')
        rows:
          - {order_id: 1, amount: 100, status: 'placed'}
          - {order_id: 2, amount: 40, status: 'refunded'}
    expect:
      rows:
        - {order_id: 1, net_amount: 100}
        - {order_id: 2, net_amount: 0}
Red flag to avoid:

Thinking unique and not_null tests prove the model's business logic is correct.

They may ask next:
  • How would you unit test the is_incremental branch of an incremental model?
  • What kind of problem would a unit test never catch that a data test would?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

11. What are the built-in generic tests in dbt, and how do you add them to a model?

What the interviewer is really testing:
Whether you test models by default and know that a dbt test is a query that looks for bad rows.
Answer frame:

The four: unique, not_null, accepted_values, relationships.

Where: in the model YAML, under the column they check.

How they work: each compiles to a query selecting failing rows; zero rows means pass.

Sample spoken answer:

"dbt ships four generic tests: unique, not_null, accepted_values and relationships. I add them in the model's YAML file under the column they check. unique and not_null together are how I state a primary key, and I put them on every model's key. accepted_values checks a column only holds a known list, like order statuses. relationships checks every value exists in another model, which is a foreign key check: for example, every customer_id on orders exists in the customers dimension. Behind the scenes, each test compiles to a query that selects the rows breaking the rule, and the test passes when that query returns nothing. So when a test fails, I can take the compiled query and see exactly which rows are the problem."

Code:
models:
  - name: fct_orders
    columns:
      - name: order_id
        data_tests:
          - unique
          - not_null
      - name: status
        data_tests:
          - accepted_values:
              arguments:
                values: ['placed', 'shipped', 'returned']
      - name: customer_id
        data_tests:
          - relationships:
              arguments:
                to: ref('dim_customers')
                field: customer_id
Red flag to avoid:

Having no tests on model keys, or thinking a test passes when its query returns rows.

They may ask next:
  • How would you test uniqueness when the key is two columns together?
  • Where do you find the SQL a failing test actually ran?
Say it in 60 seconds
Medium Coding round Mid-level Practice question

12. The built-in tests don't cover a rule you need, like a column that must never be negative. How do you write your own test?

What the interviewer is really testing:
Whether you know singular versus custom generic tests and write the query that finds violations, not good rows.
Answer frame:

Singular: a SQL file in the tests folder for one specific check.

Custom generic: a test block taking model and column_name, reusable from YAML.

Logic: select the rows that break the rule; zero rows is a pass.

Check first: packages often already have the rule.

Sample spoken answer:

"There are two ways. A singular test is a SQL file in the tests folder that selects the bad rows for one specific check; if it returns rows, it fails. That's fine for a one-off rule. When the same rule applies to many columns, I write a custom generic test. It's a test block, a lot like a macro, that takes the model and the column name as arguments and returns the failing rows. Once it's in the project, I can attach not_negative in YAML to any column, exactly like unique or not_null. The thing to remember is that the logic is inverted: I write the query that finds violations, not the one that finds good rows. Before writing my own, I check packages like dbt_utils or dbt_expectations, because common rules are often already there."

Code:
-- tests/generic/not_negative.sql
{% test not_negative(model, column_name) %}

select {{ column_name }}
from {{ model }}
where {{ column_name }} < 0

{% endtest %}
Red flag to avoid:

Writing a test query that returns the valid rows, so it fails whenever the data is fine.

They may ask next:
  • How would you let this test accept an extra argument, like a minimum value?
  • When would you write a singular test instead of a generic one?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

13. A test fails on a handful of rows every day and blocks the whole build. How do you set up tests so only serious problems stop the pipeline?

What the interviewer is really testing:
Whether you can tune tests with severity and thresholds without switching them off, and keep failures visible.
Answer frame:

First: decide whether the failures are real or accepted noise.

Severity: warn logs but does not fail or skip downstream; error does.

Thresholds: warn_if and error_if on the count of failing rows.

Visibility: store_failures keeps the bad rows in a table for whoever is on call.

Sample spoken answer:

"First I'd check whether the failures are real, because a test that fails daily and gets ignored is worse than no test. If the handful of bad rows is known and acceptable, I use the test config. Severity can be error or warn: a warning shows in the logs but doesn't fail the run or skip downstream models. There are also thresholds, warn_if and error_if, compared against the number of failing rows. So I can say warn above ten bad rows and error above five hundred, which lets small noise through but stops the build if something really breaks. I also turn on store_failures for tests like this, so the failing rows are saved to a table and whoever is on call can look at them without rerunning anything. And every warning still needs an owner, or it becomes background noise."

Code:
columns:
  - name: email
    data_tests:
      - not_null:
          config:
            severity: error
            warn_if: ">10"
            error_if: ">500"
            store_failures: true
Red flag to avoid:

Deleting or disabling the test to get the build green without finding out why it fails.

They may ask next:
  • How would you stop warnings from piling up unnoticed?
  • Would you set different severities in development and production?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

14. How do you document a dbt project, and what does someone get from the generated docs site?

What the interviewer is really testing:
Whether you treat documentation as part of the code and know what dbt docs and lineage give other people.
Answer frame:

Where: descriptions in the same YAML as tests; doc blocks for long shared text.

Build: dbt docs generate, then serve locally or host the site.

Value: searchable models, columns, tests, compiled SQL and the lineage graph.

Exposures: declare key dashboards so they appear in lineage.

Sample spoken answer:

"I write descriptions in the same YAML files where the tests live, for each model and its important columns. For longer text, like how a revenue metric is defined, I use a doc block in a markdown file and reference it from the YAML, so one definition is reused everywhere. Then dbt docs generate builds the documentation from the project and from the warehouse's catalog, which adds the column types, and dbt docs serve lets me browse it locally, or we host the site. What people get is a searchable site with every model, its description, columns, tests, the SQL and the lineage graph showing what feeds it and what depends on it. The lineage is the part analysts use most. I also declare exposures for key dashboards, so the graph shows which dashboards a model change could hit."

Red flag to avoid:

Keeping model documentation in a separate wiki that nobody updates when the SQL changes.

They may ask next:
  • How do you keep descriptions from going out of date?
  • What is an exposure, and who should own it?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

15. Tell me about a time a dbt test caught a data problem before it reached a dashboard. What happened next?

What the interviewer is really testing:
Whether your tests catch real problems and whether you follow through to the root cause and a better test.
Answer frame:

Catch: which test failed and what dbt build protected.

Dig: how you found the failing rows and the cause.

People: who you told and what you said.

Prevent: the test or change that catches it earlier next time.

Sample spoken answer:

"At my last company we had a relationships test from the orders fact to the customers dimension. One morning it failed on a few thousand rows. Because we ran dbt build, the marts downstream were skipped, so dashboards still showed yesterday's correct data instead of broken numbers. I pulled the failing rows with the compiled test query and saw they were all new customers from one region. The source team had started adding a prefix to IDs for new sign-ups, so our join no longer matched. I told the dashboard owners the data would be a few hours late and why, worked with the source team, and changed the staging model to handle both formats. Afterwards I added a pattern test on the raw ID column from a testing package, so a format change now fails at the source layer, where it's easiest to spot."

Red flag to avoid:

A story where the fix was to turn the test off or widen it until the failure went away.

They may ask next:
  • Would you have done anything differently if the test had been set to warn?
  • How did you agree the ID format with the source team going forward?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

16. The unique test on your main orders model fails in the 6 am production run, and leadership reads the dashboard at 9. What do you do?

What the interviewer is really testing:
Whether you stay calm, know what dbt build did and did not protect, find the cause fast and communicate before people notice.
Answer frame:

Assess: with dbt build, the orders model is already rebuilt but everything downstream was skipped; which one does the dashboard read?

Diagnose: pull duplicate keys from the compiled test and find the cause.

Fix or tell: rebuild the model and its downstream, or warn owners before 9.

Never: switch the test off to force the run through.

Sample spoken answer:

"First I'd check what the failure actually stopped. With dbt build, the orders model itself was already rebuilt with the duplicates, but everything downstream was skipped. If the dashboard reads a downstream mart, it still shows yesterday's data, stale but correct, which buys time. If it reads the orders model directly, it's already wrong and I warn people now. Then I'd pull the duplicate keys with the compiled test query and look for the cause. Often it's a source loaded twice or a join that fans out. If the fix is small and clear, I fix it, rebuild the model and everything downstream with the plus selector, and let the tests confirm it. If it isn't clear by around eight, I'd tell the dashboard owners which numbers to trust and when to expect an update, rather than letting them find out. What I wouldn't do is switch the test off to push the run through, because then leadership reads double-counted revenue."

Red flag to avoid:

Disabling the test so the run completes and the dashboard updates with duplicated rows.

They may ask next:
  • What would be different if the run had used dbt run and then dbt test as two steps?
  • How would you stop the same duplicate from coming back?
Say it in 60 seconds

Incremental Models 4 questions

Medium Coding round Fresher, Mid-level Practice question

17. Write an incremental model that loads only new events from a raw events table on each run, and explain each part.

What the interviewer is really testing:
Whether you can write the standard incremental pattern correctly and know when the filter is and is not applied.
Answer frame:

Config: materialized incremental with a unique key.

Filter: inside an is_incremental block, select only rows newer than what the target table already holds.

First run and full refresh: is_incremental is false, so the whole history is built.

Weak spot: late rows with old timestamps are skipped without a lookback.

Sample spoken answer:

"I set the materialization to incremental and give it a unique key. The first time it runs, the table doesn't exist yet, so is_incremental is false and dbt builds the whole table from the full select. On later runs is_incremental is true, so the filter kicks in: I only select events newer than the latest event_at already in the target, which I reach through this. dbt then loads those rows into the table, and because event_id is the unique key, a row that comes through twice replaces the old copy instead of duplicating. is_incremental is also false when I run with full-refresh, which is how I rebuild after a logic change. The weak spot is late data. Anything arriving with an older timestamp gets skipped, so in practice I'd add a lookback window of a few days."

Code:
{{ config(
    materialized='incremental',
    unique_key='event_id'
) }}

select
    event_id,
    user_id,
    event_type,
    event_at
from {{ source('app', 'events') }}

{% if is_incremental() %}
where event_at > (select max(event_at) from {{ this }})
{% endif %}
Red flag to avoid:

Leaving out the is_incremental check, so the filter runs against a table that does not exist on the first run.

They may ask next:
  • How would you change the filter to pick up events that arrive two days late?
  • What happens if you run this model with full-refresh?
  • Why might you filter on a load timestamp rather than the event timestamp?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

18. What incremental strategies can dbt use, such as append, merge, delete+insert and insert_overwrite, and how do you choose one?

What the interviewer is really testing:
Whether you can match the strategy to how the data changes and to the table size, and know that support differs by adapter.
Answer frame:

Append: insert only; cheapest, but overlaps create duplicates.

Merge: match on unique key, update or insert; the usual choice for changing rows.

Delete+insert: remove target rows whose keys are in the batch, then insert the batch.

Insert_overwrite: replace whole partitions where supported; good for reprocessing recent days.

Sample spoken answer:

"Which ones you get depends on the adapter, but the ideas are the same. Append just inserts the new rows. It's the cheapest, but with no unique key a rerun or an overlapping window creates duplicates, so I only use it for true append-only logs. Merge matches on the unique key, updates existing rows and inserts new ones. It's the usual choice when rows change, but on a huge target the merge can scan a lot unless I limit it. Delete+insert deletes target rows whose keys appear in the new batch, then inserts the batch, which can be faster than merge on some warehouses. Insert_overwrite replaces whole partitions on warehouses that support it, so I can reprocess the last few days completely, as long as each batch holds every row for those partitions. Newer versions also add microbatch, which processes the data in fixed time slices. I choose by how the data changes, the table size and what the warehouse does well."

Red flag to avoid:

Using append on data where rows get updated, or claiming every strategy works on every warehouse.

They may ask next:
  • With insert_overwrite, what happens to a partition if your batch only has half of its rows?
  • How would you stop a merge from scanning the whole target table on every run?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

19. Someone adds a new column to an incremental model. What happens on the next run, and how do you handle it?

What the interviewer is really testing:
Whether you know the on_schema_change setting and that no option fills in history for existing rows.
Answer frame:

Default: on_schema_change is ignore, so the new column does not reach the existing table.

Options: append_new_columns, sync_all_columns, or fail to force a decision.

History: old rows stay null; a full refresh or targeted backfill fixes that.

Sample spoken answer:

"By default nothing obvious happens, which is the trap. on_schema_change defaults to ignore, so the incremental run doesn't add the new column to the existing table; it's just silently not there. I have a few options. append_new_columns adds new columns to the target. sync_all_columns adds new ones and also drops columns that were removed from the model. Or fail stops the run so someone decides on purpose. What none of them does is fill in the new column for rows that were loaded earlier. Those old rows just have nulls. So if history matters, I run the model with full-refresh, or a targeted backfill if the table is too big to rebuild. On models other teams depend on, I lean towards fail, because a silent schema change is how dashboards quietly break."

Red flag to avoid:

Assuming dbt adds the column and backfills it automatically on the next incremental run.

They may ask next:
  • How would you backfill the new column for a table too large to fully refresh?
  • What could go wrong if you use sync_all_columns on a table others query?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

20. You find that an incremental model has been missing late-arriving rows for weeks, and finance needs corrected numbers by Friday. What's your plan?

What the interviewer is really testing:
Whether you size the damage, fix the cause before backfilling, verify the result and explain it clearly to the business.
Answer frame:

Size it: compare the table with the source by day for the affected weeks.

Fix the cause: filter on load time or add a lookback with a unique key.

Backfill: full refresh if affordable, otherwise reprocess the affected range.

Close out: recheck totals and send finance a short note.

Sample spoken answer:

"First, size the damage. I'd compare the incremental table with a fresh query over the source for the affected weeks, day by day, so I know how many rows are missing and which reports are off. Then I fix the root cause in the model. The filter uses the max event timestamp, so anything that arrives late with an older timestamp gets skipped. I'd filter on the load timestamp instead, or add a lookback of a few days with a unique key so reprocessed rows merge instead of duplicating. Then the backfill. If the table is small enough, a full refresh is cleanest. If not, I'd reprocess only the affected date range, using a variable that widens the window for one run. Then I'd compare totals against the source again and send finance a short note: what was wrong, which days changed and what stops it happening again."

Red flag to avoid:

Running a full refresh straight away without fixing the filter, so the same gap opens again next week.

They may ask next:
  • How would you pass that one-off wider window into the model without changing the code each time?
  • What test would have caught this weeks earlier?
Say it in 60 seconds

Snapshots 2 questions

Medium Technical round Mid-level Practice question

21. How do dbt snapshots work, and when would you use the timestamp strategy versus the check strategy?

What the interviewer is really testing:
Whether you can explain how dbt records type 2 history and pick a change-detection strategy that fits the source.
Answer frame:

Purpose: keep type 2 history of a table that only holds current state.

Mechanics: a changed row gets its old version closed with dbt_valid_to and a new version inserted.

Timestamp: trusts an updated_at column; efficient.

Check: compares chosen columns when there is no reliable updated_at.

Sample spoken answer:

"A snapshot keeps the history of a table that only stores its current state, which is a type 2 slowly changing dimension. Each time dbt snapshot runs, it compares the source rows with what's already captured. If a row changed, dbt closes the old version by setting its dbt_valid_to and inserts a new version with a fresh dbt_valid_from. By default the current version is the one where dbt_valid_to is null. To detect changes there are two strategies. Timestamp uses an updated_at column: if it moved forward, the row changed. It's efficient and my first choice when the source keeps that column reliably. Check compares a list of columns, or all of them, which I use when there's no trustworthy updated_at. It's more work for the warehouse, and if I list columns, a change in a column I left out won't be caught."

Code:
{% snapshot customers_snapshot %}

{{ config(
    target_schema='snapshots',
    unique_key='customer_id',
    strategy='timestamp',
    updated_at='updated_at'
) }}

select * from {{ source('app', 'customers') }}

{% endsnapshot %}
Red flag to avoid:

Thinking a snapshot is a full copy of the table taken each day rather than a record of changed rows.

They may ask next:
  • How would you query the snapshot to get each customer as they were on a given date?
  • What happens if the source updates a row but forgets to change updated_at?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

22. What can go wrong with dbt snapshots in production, and how do you protect against it?

What the interviewer is really testing:
Whether you know snapshots hold history that cannot be rebuilt, and the gaps in what they capture.
Answer frame:

Grain: only the state at each run is captured; changes in between are lost.

Irreplaceable: a dropped snapshot cannot be rebuilt; protect and back it up.

Hard deletes: by default a deleted source row stays current unless configured.

What to snapshot: raw sources with minimal logic, tested for one current row per key.

Sample spoken answer:

"The biggest one is that a snapshot only sees the source as it looks when the snapshot runs. If a row changes three times between daily runs, I only capture the last state, so I'm honest with stakeholders about how fine the history is. Second, snapshots can't be rebuilt. Unlike a model, if the table is dropped the history is gone, so I keep it in its own schema, restrict who can drop it, and make sure it's covered by backups. Third, hard deletes. By default a row deleted in the source just stays current for ever, so I turn on the snapshot setting that closes out deleted rows when that matters. I also snapshot raw sources with as little logic as possible, because if I snapshot a transformed model and the logic changes later, the history ends up mixed. And I test that there's exactly one current row per key."

Red flag to avoid:

Treating a snapshot like any other model that can be rebuilt with a full refresh.

They may ask next:
  • How would you recover if a snapshot table was accidentally dropped?
  • Why is snapshotting a mart with business logic risky?
Say it in 60 seconds

Macros & Jinja 3 questions

Medium Coding round Mid-level Practice question

23. Write a macro that saves you from repeating the same SQL in many models, and show how a model calls it.

What the interviewer is really testing:
Whether you can use Jinja loops and arguments to generate SQL, and know when a macro makes a model harder to read.
Answer frame:

What a macro is: a Jinja function that returns SQL text.

Example: loop over a list of values to build one column per value.

Check: read the compiled SQL in the target folder.

Restraint: only for logic that is genuinely repeated.

Sample spoken answer:

"Macros are Jinja functions that return SQL text. A common case is pivoting, like counting orders per customer for each status. Instead of writing a case expression per status in every model, I write a macro that takes the column and a list of values, loops over the list and emits one sum per value, adding a comma after every item except the last. In the model, I call it inside the select. When dbt compiles, the call is replaced by the generated SQL, so the warehouse only ever sees plain SQL, and I can check the result in the compiled folder under target. I keep macros for logic that's really repeated. If a macro is only used once, I'd rather keep the SQL in the model, because heavy Jinja makes models harder for the next person to read."

Code:
-- macros/count_by_status.sql
{% macro count_by_status(column, statuses) %}
    {% for s in statuses %}
    sum(case when {{ column }} = '{{ s }}' then 1 else 0 end)
        as {{ s }}_count{{ ',' if not loop.last }}
    {% endfor %}
{% endmacro %}

-- models/marts/customer_order_counts.sql
select
    customer_id,
    {{ count_by_status('status', ['placed', 'shipped', 'returned']) }}
from {{ ref('stg_orders') }}
group by customer_id
Red flag to avoid:

Wrapping every model in layers of Jinja so nobody can read the SQL without compiling it.

They may ask next:
  • How would you get the list of statuses from the data instead of hard-coding it?
  • What is the difference between the two kinds of curly-brace tags in Jinja?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

24. You set a custom schema called marketing on some models, but in production they land in analytics_marketing instead of marketing. Why, and how do you change it?

What the interviewer is really testing:
Whether you know the generate_schema_name macro, why its default protects developers, and how to override it safely.
Answer frame:

Cause: the default generate_schema_name joins the target schema and the custom schema.

Why: so each developer's models land in their own schema and never overwrite production.

Fix: override the macro to use the custom schema alone in production only.

Check: compile before the first production run.

Sample spoken answer:

"That's dbt's default behaviour, not a bug. Schema names come from a macro called generate_schema_name. If a model has no custom schema, it goes into the target's schema. If it has one, dbt joins the two: target schema, an underscore, then the custom schema. The reason is safety. In development every developer has their own target schema, so their marketing models land in something like dbt_sam_marketing and never overwrite each other or production. To change it, I override that macro in my own project. The common pattern is: in production, use the custom schema on its own; everywhere else, keep the prefixed name. I decide what production is by checking target.name. After the change I compile and check where things will land before the first production run, because a wrong schema macro can write over tables people use."

Code:
-- macros/generate_schema_name.sql
{% macro generate_schema_name(custom_schema_name, node) -%}
    {%- if custom_schema_name is none -%}
        {{ target.schema }}
    {%- elif target.name == 'prod' -%}
        {{ custom_schema_name | trim }}
    {%- else -%}
        {{ target.schema }}_{{ custom_schema_name | trim }}
    {%- endif -%}
{%- endmacro %}
Red flag to avoid:

Overriding the macro to use the bare custom schema everywhere, so developer runs overwrite production tables.

They may ask next:
  • What would go wrong if you used the custom schema alone in every environment?
  • How would CI builds for pull requests fit into this naming?
Say it in 60 seconds
Hard Technical round Senior Practice question

25. Your macro uses run_query to fetch a list of values from the warehouse, but dbt throws an error while parsing the project. What is going on?

What the interviewer is really testing:
Whether you understand dbt's parse and execute phases and the execute flag, which trips up most people writing dynamic SQL.
Answer frame:

Two phases: dbt parses the whole project first without running queries.

execute: false while parsing, true when nodes really run or compile.

Fix: only read run_query results inside an if execute block, with a safe default.

Question it: a seed, a fixed list or a package macro may be simpler.

Sample spoken answer:

"dbt works in two phases. First it parses the whole project to build the graph, and during that phase it renders the Jinja but doesn't actually run queries against the warehouse. A variable called execute is false while parsing and true when dbt is really running or compiling nodes. So if my macro calls run_query and immediately reads the results, like taking the first column's values, it fails at parse time, because there are no results yet. The fix is to wrap anything that reads the results in an if execute check, with a harmless default like an empty list. I'd also ask whether a runtime query is needed at all. It adds a warehouse round trip on every compile and makes the SQL depend on the data. A fixed list or a seed is often simpler, and dbt_utils has get_column_values if it must be dynamic."

Code:
{% set status_query %}
    select distinct status from {{ ref('stg_orders') }}
{% endset %}

{% set results = run_query(status_query) %}

{% if execute %}
    {% set statuses = results.columns[0].values() %}
{% else %}
    {% set statuses = [] %}
{% endif %}
Red flag to avoid:

Blaming the warehouse connection and not knowing that dbt parses the project before it runs anything.

They may ask next:
  • Why does it matter that the model's SQL changes when the data changes?
  • Where else would the execute flag matter in a macro?
Say it in 60 seconds

CI & Deployment 4 questions

Hard System design round Mid-level, Senior Practice question

26. How would you set up CI for a dbt project so that a pull request only builds and tests what it changed?

What the interviewer is really testing:
Whether you know state comparison and deferral, the pieces that make CI fast enough for people to keep using it.
Answer frame:

State: fetch the manifest.json from the last production run.

Select: state:modified+ builds changed models and everything downstream.

Defer: unbuilt parents resolve to production objects instead of failing.

Isolation: a schema per pull request, dropped when it closes; CI must pass to merge.

Sample spoken answer:

"The idea is usually called slim CI. Every production run leaves a manifest.json, a full description of the project as deployed. In CI I fetch that production manifest and pass it with the state flag. Then selecting state:modified plus picks the models whose code or config changed in this pull request, plus everything downstream of them, and dbt build runs and tests just those. The defer flag handles the rest: when a changed model refs something that wasn't rebuilt, dbt points that ref at the production object instead of failing. CI writes into its own schema, usually named after the pull request, and I drop it when the PR closes. On top of that I'd run a SQL linter and require the CI job to pass before merging. It keeps CI fast and cheap while still catching a change that breaks a model three steps downstream."

Code:
# prod-state/ holds manifest.json from the last production run
dbt deps
dbt build \
  --select state:modified+ \
  --defer --state prod-state \
  --target ci
Red flag to avoid:

Running a full build of every model on every pull request until CI is so slow people skip it.

They may ask next:
  • What happens in this setup if the production manifest is days old?
  • How would you also catch a change that makes a downstream model return different numbers?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

27. How do dev, CI and production environments work in dbt, and how do you stop developers overwriting each other's tables?

What the interviewer is really testing:
Whether you understand profiles, targets and per-developer schemas, and keep secrets out of the repository.
Answer frame:

Profiles and targets: dev, ci and prod each with their own credentials, schema and threads.

Isolation: every developer builds into a personal schema; CI gets one per pull request.

Production: only the scheduled job, with a service account, writes to prod schemas.

Secrets: outside the repo, read with env_var.

Sample spoken answer:

"dbt connects through a profile, and each profile has targets, like dev, ci and prod, each with its own credentials, schema and number of threads. With dbt Core that lives in profiles.yml, outside the repo, so secrets aren't committed, and I read passwords with env_var. The trick for isolation is the target schema. Each developer's dev target has their own schema, like dbt_sam, so when I run a model it builds there and can't touch production or anyone else's work. CI gets its own schema per pull request. Only the production job, running with a service account, writes to the production schemas. Inside models I can check target.name when something has to differ, for example limiting dev to the last few days of data so builds are quick. I keep those branches rare, because code that behaves differently per environment is harder to trust."

Red flag to avoid:

Having every developer run against the production schema with a shared admin login.

They may ask next:
  • How would you give developers realistic data without full production volumes?
  • Who should have permission to write to production schemas?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

28. Tell me about a time you got a team to adopt a better dbt workflow, like code review, tests or CI. How did you bring people along?

What the interviewer is really testing:
Whether you can change habits through evidence and low friction, not just by writing rules.
Answer frame:

Problem: what was breaking and how often.

Evidence: the incidents and their cost, shown before any new rule.

Low friction: fast CI, examples, pairing, advisory before blocking.

Outcome: what changed and how you know.

Sample spoken answer:

"At my last company, analysts pushed dbt changes straight to the main branch, and production broke a few times a month. Rather than announcing new rules, I started by showing the cost: I listed the recent incidents and how long each took to find. Then I set up a CI job that built only the changed models into a temporary schema, and I kept it fast, a few minutes, because slow CI is the quickest way to lose people. I wrote a short guide with examples of key tests and paired with two analysts on their first pull requests. For the first month the CI result was advice, not a blocker, so people got used to it. Once they saw it catch real mistakes, turning on required checks was an easy conversation, and production breaks dropped to almost none that quarter."

Red flag to avoid:

Describing the change as a rule you imposed, with no evidence or support for the people affected.

They may ask next:
  • Who pushed back hardest, and how did you handle it?
  • How did you keep CI fast as the project grew?
Say it in 60 seconds
Hard Situational round Senior Practice question

29. A teammate wants to rename a column in a mart that many dashboards use. How do you make that change without breaking anything?

What the interviewer is really testing:
Whether you treat widely used marts as interfaces, with impact analysis, a migration period and enforced contracts.
Answer frame:

Find users: lineage, exposures, and warehouse query history for the old name.

Migrate: add the new column beside the old one, give a switch date, then drop.

Enforce: model contracts and versions for marts other teams build on.

Communicate: the rename is easy; the notice is the real work.

Sample spoken answer:

"I'd start by finding who uses it. The lineage graph shows downstream models, and if we've declared exposures for dashboards they show up there too. For anything else, I'd search the BI tool or the warehouse's query history for the old column name. Then I'd avoid a hard cut-over. The simple safe route is to add the new column next to the old one, tell the owners, and give them a date to switch. After that date, and once query history shows nobody reads the old name, I drop it. For marts that are real interfaces for other teams, I'd use model contracts, so column names and types are enforced at build time and a change can't slip through, and model versions, so a new version can live beside the old one with a deprecation date. The rename is a one-line change; the work is the communication."

Red flag to avoid:

Renaming the column in one pull request and waiting to see which dashboards break.

They may ask next:
  • What does an enforced contract check, and what does it not check?
  • How long would you keep the old column, and who decides?
Say it in 60 seconds

Performance 1 questions

Medium Technical round Mid-level, Senior Practice question

30. A model that used to take a few minutes now takes forty, and it's holding up the morning run. How do you find and fix the cause?

What the interviewer is really testing:
Whether you measure before changing anything and know the usual dbt-level causes of a slow model.
Answer frame:

Measure: run times from run_results.json and logs; when did it slow down?

Look: compiled SQL and the warehouse query profile for the expensive step.

Usual causes: full rebuild of growing history, chains of views, join fan-out, windows over all history.

Prove it: change one thing, compare time and row counts.

Sample spoken answer:

"First I confirm where the time goes. dbt records each model's run time in run_results.json and the logs, so I check it really is this model and when it started slowing down. Then I open the compiled SQL and the warehouse's query profile to see which step is expensive. The usual suspects are these. The table is rebuilt from all of history every day and simply grew, which is a case for making it incremental. It reads from a chain of views, so each run recomputes all the upstream logic, which I fix by materializing a heavy upstream model as a table. A join fans out and multiplies rows before a group by. Or a window function runs over the whole history. I also check that filters apply early, so the warehouse can skip data. Then I change one thing, rerun, and compare the run time and row counts."

Red flag to avoid:

Jumping straight to a bigger warehouse or more compute without finding out what changed.

They may ask next:
  • How would you spot a join that fans out before it reaches the group by?
  • When would raising the number of threads help, and when would it not?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

The questions above are the prep. The call has ten more.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.

Download ClapAssist with 10 free minutes
Mac and Windows · Stays out of screen share · No card