DataLane
← All cheat sheets

dbt Interview Questions cheat sheet

Materializations, incremental models, tests, snapshots, and project design questions that come up in analytics engineering interviews.

Interview PrepIntermediate6 sections

Fundamentals

What does dbt actually do?
It compiles Jinja-templated SQL into plain SQL, resolves a dependency graph from ref() calls, and runs the statements against your warehouse in topological order, wrapping them in the DDL implied by the materialization. It is the T in ELT: it does not extract or load, and it has no compute of its own — the warehouse does all the work.
Why is ref() better than hardcoding a table name?
ref() builds the DAG so dbt knows execution order and can run models in parallel where independent, and it resolves the correct schema per target so the same code runs in dev, CI, and production. Hardcoding breaks lineage, breaks environment isolation, and means dbt cannot tell you what breaks when a column changes.
What is the difference between a source and a seed?
A source is a declaration of a table that something else loaded, referenced with source(); it enables freshness checks and lineage back to the ingestion boundary. A seed is a small CSV in the repo that dbt loads into the warehouse with 'dbt seed', appropriate for static mappings such as country codes, not for real data.
Explain the difference between dbt Core and dbt Cloud in 2026.
dbt Core is the open-source engine you run yourself from a CLI or an orchestrator. dbt Cloud adds a hosted scheduler, an IDE, CI integration, the semantic layer, and cross-project references through dbt Mesh. Recent Core versions also introduced the Fusion engine work for faster parsing and native SQL comprehension, which is worth mentioning as current context.
How does dbt handle environments?
Profiles and targets define connection and schema per environment, and the generate_schema_name macro decides where models land — by default developer models go to a personal schema prefix, which is what keeps developers from overwriting production. CI typically builds into a temporary schema and drops it after, using state comparison to build only what changed.

Materializations

Compare view, table, incremental, and ephemeral materializations.
A view stores no data and recomputes on every query — cheap to build, expensive to read repeatedly. A table fully rebuilds each run, giving fast reads and simple semantics. Incremental appends or merges only new rows, trading complexity for runtime on large tables. Ephemeral is inlined as a CTE into downstream models and creates no database object.
When would you choose ephemeral, and what is the downside?
For thin intermediate logic used by one or two models where an object in the warehouse adds no value. The downsides are that you cannot query it directly when debugging, it cannot be tested independently in the usual way, and referencing it in many downstream models duplicates the same CTE and its compute in each.
What is a materialized view materialization and when is it worth it?
It delegates incremental refresh to the warehouse — Snowflake dynamic tables, BigQuery materialized views, Databricks materialized views — so the platform maintains freshness rather than a dbt run. Worth it for near-real-time serving layers with simple SQL; the constraints are dialect-specific and complex joins or window functions are often unsupported.
How do you decide between a table and a view for a mart model?
Compare build cost against read frequency. A mart queried all day by BI should be a table so each dashboard load does not re-execute the joins; a rarely queried model can stay a view and cost nothing to maintain. Also consider freshness: a view is always current, while a table is only as fresh as the last run.

Incremental models

Explain every part of this incremental model: {{ config(materialized='incremental', unique_key='order_id') }} select * from {{ ref('stg_orders') }} {% if is_incremental() %} where updated_at > (select max(updated_at) from {{ this }}) - interval '3 days' {% endif %}
Configure materialized='incremental' with a unique_key, then guard the filter with the is_incremental() macro so the predicate applies only on subsequent runs and a full refresh still reads all history. Inside the guard, filter the source on a watermark selected from the existing table, typically max of an updated_at column, minus a lookback window.
What does unique_key change about an incremental run?
Without it, dbt appends; with it, dbt performs a merge or delete-plus-insert so late-arriving updates replace prior rows instead of duplicating them. The subtlety is that the key must actually be unique in the incoming batch — if two rows share the key, most warehouses raise a nondeterministic merge error, so deduplicate upstream.
What are the incremental strategies and how do they differ?
append is fastest and only correct for immutable events; merge upserts on the unique key and is the default on Snowflake, BigQuery, and Databricks; delete+insert removes matching keys then inserts, which suits partition-scale reloads; insert_overwrite replaces whole partitions and is the standard choice on BigQuery and Spark for date-partitioned tables.
Why add a lookback window to the incremental filter?
Because source systems deliver late data. Filtering on updated_at greater than the current maximum misses any row that arrived after the last run but has an earlier timestamp. A lookback of a few hours or days re-reads a safe overlap, which is harmless with a merge strategy and prevents silent gaps that nobody notices for weeks.
When should you not use an incremental model?
When the table is small enough that a full rebuild takes a couple of minutes, when the logic changes often, or when the source can restate arbitrary history. Incremental models accumulate drift, so teams that use them everywhere end up scheduling periodic full refreshes anyway. Complexity has to be earned by a real runtime or cost saving.

Tests, contracts, and documentation

What are the built-in generic tests and what do they miss?
unique, not_null, accepted_values, and relationships, declared in YAML. They cover schema-level assumptions but say nothing about business correctness: revenue that halved, a currency conversion applied twice, or a join that quietly dropped a third of rows. That is what singular tests and dbt-utils or dbt-expectations packages fill in.
What is the difference between a generic test and a singular test?
A generic test is a parameterized macro applied to many columns from YAML; a singular test is a one-off SQL file in tests/ that must return zero rows to pass. Write singular tests for specific invariants such as reconciling a total against a source system, and promote one to generic once you need it in a second place.
What do test severity and thresholds let you do?
severity: warn keeps a run green while surfacing the issue, and error_if and warn_if take expressions such as more than 100 failing rows, so small known imperfections do not page anyone. store_failures writes failing rows to a table for triage. This is how you keep a test suite that people still trust after six months.
What are model contracts and what do they enforce?
Setting contract enforced true with declared column names, data types, and constraints makes dbt verify the built model matches the declaration and fail the build otherwise. It turns a model into a stable interface for downstream consumers, which matters most for public models exposed across teams through dbt Mesh.
How does dbt generate documentation and lineage?
'dbt docs generate' compiles the manifest and catalog into a site with descriptions, column-level metadata, and a DAG viewer built from ref and source edges. The catalog queries warehouse metadata, so it needs a run first to reflect real columns. Descriptions live in YAML next to the models, which is what keeps them from going stale.

Snapshots and history

What does a dbt snapshot do?
It implements slowly changing dimension type 2 over a mutable source, adding dbt_valid_from and dbt_valid_to columns so you can reconstruct the state of a record at any past point. It runs with 'dbt snapshot', separate from 'dbt run', and it must be scheduled frequently enough to catch changes because it only sees the source's current state.
Compare the timestamp and check snapshot strategies.
timestamp uses a reliable updated_at column and is cheaper and more accurate, since it detects a change even if column values happen to repeat. check compares a list of columns (or all of them) and is the fallback when the source has no trustworthy modification timestamp; it misses a change that reverts between runs.
Why should snapshots read from raw sources rather than transformed models?
Because history should record what the source actually said. If you snapshot a transformed model, a change to your transformation logic writes a fake historical change, and you can never separate real source updates from your own code deployments. Snapshot at the boundary and transform downstream of the snapshot.
What happens if a snapshot job is missed for a week?
Intermediate changes are lost permanently — the snapshot sees only the current state, so a record that changed three times records one transition. There is no backfill for that. This is the main argument for real change data capture from the database log when accurate history genuinely matters.

Project structure and performance

How do you organize models in a dbt project?
The conventional three layers are staging (one model per source table, renaming, casting, light cleaning, materialized as views), intermediate (reusable joins and business logic), and marts (dimensional models consumers query). Staging models are the only place that reference sources, which means a source change touches exactly one file.
What is the dbt node selection syntax you use most?
'dbt build --select model_name+' runs a model and everything downstream, '+model_name' runs its ancestors, 'tag:finance' selects by tag, and 'state:modified+' with a deferred manifest is how CI builds only what changed plus dependents. Knowing 'dbt build' runs models, tests, snapshots, and seeds in DAG order rather than run-then-test is a good signal.
How do you make a slow dbt project faster?
Raise threads to match warehouse concurrency, replace full rebuilds of the largest tables with incremental models, cut redundant intermediate models that each cost a full scan, and use state-based selection in CI so pull requests do not rebuild the world. Then look at the warehouse side: clustering, partition pruning, and right-sized compute.
What is dbt Mesh and what problem does it solve?
It lets multiple dbt projects reference each other's public models with enforced contracts and versions, so a large organization can split one unmanageable monolith into domain-owned projects without losing lineage. The trade-off is governance overhead: you now need clear ownership, deprecation policy, and versioning discipline on public models.
How would you set up CI for a dbt project?
On every pull request, build into a temporary schema using state:modified+ against the production manifest so only changed models and their dependents run, then run their tests, then drop the schema. Add SQL linting with SQLFluff and a check that new models have descriptions and at least a not_null and unique test on the primary key.

From DataLane — tutorials at/blog, practice SQL live in theplayground.

↑↓ navigate openesc close