skip to content

dbt

Transformation as version-controlled SQL: models referencing each other, materializations, tests, snapshots and Jinja macros, all executed inside the warehouse. Interviewers ask about dbt because it is how analytics engineering brought software practice to SQL.

on this pageshow

explore

questions

page 1 of 2

In dbt, when is the Jinja in a model rendered, and where can you read the resulting SQL?

level: juniorimportance: must knowfreq 70%

answer

  1. the warehouse never sees a curly brace
  2. two steps happen before the query runs
  3. there is a folder holding the rendered output
  4. target/compiled holds the model's SQL

basics

~10 s

dbt renders a model's Jinja into plain SQL before anything reaches the warehouse. The rendered SELECT is written to target/compiled/, and the same SQL wrapped in create or merge statements is written to target/run/.

solid answer

~40 s

A dbt model file is a Jinja template that produces SQL, not SQL itself. dbt first **parses** the project to build the DAG, then **compiles** each selected model by rendering every `{{ }}` expression and `{% %}` block into a finished SQL string, and only then **runs** it. The warehouse never sees a curly brace. The compiled SELECT lands in `target/compiled/<project>/models/...`, and the same SQL wrapped in whatever DDL or DML the materialization needs lands in `target/run/...`. `dbt compile` does the render without building anything, which is the fastest way to see what your templating actually produced. Because Jinja runs before execution, it can decide which SQL text exists, but it can never react to a row value — that still needs `case when`.

code

sql · 10 lines
sql
-- models/stg_orders.sql
select
    order_id,
    customer_id,
    ordered_at,
    amount_cents / 100.0 as amount
from {{ source('shop', 'orders') }}
{% if target.name != 'prod' %}
where ordered_at >= current_date - 3
{% endif %}

go deeper

for a junior

Be ready to say plainly that dbt turns the template into SQL first and sends only SQL, and to name target/compiled as the place you look when output surprises you.

for a middle

Explain the parse, compile and run phases and what differs between them, including that target/run holds the materialization wrapper while target/compiled holds the bare SELECT.

for a senior

Show the debugging habit: reproduce a failure by pulling the compiled statement and running it directly in a warehouse console, and know when compilation itself issues queries.

for a principal

Own the consequence for review and tooling — the artifact reviewers read is not the artifact that runs, so CI or review conventions have to surface compiled SQL for anything heavily templated.

## Two languages in one file A dbt model is a `.sql` file, but it is not pure SQL. It is a Jinja template whose *output* is SQL. Jinja is a Python templating language with three delimiters: - `{{ ... }}` — an expression; its value is rendered into the output text. - `{% ... %}` — a statement (`if`, `for`, `set`, `macro`); it controls what gets rendered but emits no text of its own. - `{# ... #}` — a comment; it disappears entirely. Everything outside those delimiters is copied through untouched. A model, read literally, is a small program that prints a query. ## The order of operations **Parse.** dbt reads every file in the project and renders enough of each model to discover its `ref()`, `source()` and `config()` calls. From those it builds the manifest and the dependency graph. During parsing the special variable `execute` is `False`, and introspective calls such as `run_query()` return nothing — dbt is still working out *what* to build. **Compile.** For each selected node dbt renders the template all the way to a SQL string and writes it to `target/compiled/<project_name>/models/<path>.sql`. During a real run `execute` is `True` here. **Run.** dbt wraps that compiled SELECT in the statements the chosen materialization requires — `create or replace view ... as`, `create table as`, a `merge`, a set of temp-table steps — and sends the wrapped statement to the warehouse. That final text is written to `target/run/...`. The practical consequence: when the warehouse returns a syntax error naming something you cannot find in your model file, you are reading the wrong artifact. Open the compiled file. ## What you can do with it Because a template runs before the query, you get ordinary programming constructs around your SQL: ```sql select * from {{ source('shop', 'orders') }} {% if target.name != 'prod' %} where ordered_at >= current_date - 3 {% endif %} ``` On a dev target that renders with the `where` clause and scans three days; on prod it renders without it. `target` is a dict-like context variable describing the connection dbt is using (`target.name`, `target.schema`, `target.type`). Other everyday context members include `{{ var('start_date', '2020-01-01') }}` for values declared in `dbt_project.yml` or passed with `--vars`, `{{ env_var('SOME_VAR') }}` for environment variables, and `{{ this }}` for the relation the current model writes to. ## Where to look, and with which command `dbt compile` renders every selected model into `target/compiled/` without building models. It still opens a warehouse connection, because introspective macros genuinely execute during compilation. `dbt run` compiles and then builds. `dbt build` compiles, builds, and runs the tests and snapshots interleaved with the models. In dbt Cloud the same compiled text appears in the IDE's compiled-code pane; on the command line it is a file you can `cat`, diff and paste into a warehouse console. Reading compiled SQL is the single most useful debugging habit in dbt. A macro that emits a stray comma, a loop that produces `and` with nothing after it, a variable that rendered empty — all of these are obvious in the compiled file and invisible in the source. ## The boundary Jinja cannot cross Jinja is a text step. It has no access to the rows in your tables at render time. `{% if amount > 0 %}` is not a filter on a column; `amount` there is a Jinja variable, and if it does not exist the expression is undefined. Row-level logic belongs in SQL: `case when amount > 0 then ... end`. The one way templating can look at data is by *running a query during compilation*, which is a deliberate, separate mechanism with its own guard. Quoting is the other common trap. `{{ var('country') }}` renders raw text, so a string literal needs its own quotes in the template: `where country = '{{ var("country") }}'`. Note the swapped inner quote style — nesting the same quote character breaks the expression. ## Whitespace Jinja statement lines leave blank lines behind in the output. The `{%- ... -%}` form trims whitespace on the marked side. This is cosmetic, but compiled SQL that a human can still read is worth the trimming, and it matters when a loop must place commas precisely.

  • How would you make a dbt model read a value supplied at run time rather than hard-coding it?
    Use `{{ var('start_date', '2020-01-01') }}` and declare the default in `dbt_project.yml`, overriding it on the command line with `--vars '{start_date: 2024-01-01}'`. For secrets or environment-specific endpoints use `{{ env_var('MY_VAR') }}` instead, which reads the process environment at render time and fails loudly if the variable is missing and no default is given.
  • How do you print a value from inside a macro while a dbt run is in progress?
    Call `{{ log("payment types: " ~ payment_types, info=True) }}`. Without `info=True` the message goes only to `logs/dbt.log`; with it, the message also appears on stdout. It is the Jinja equivalent of a print statement and is the quickest way to see what a variable held during compilation.
  • Why can a dbt model reference another model with ref() before either has been built?
    `ref()` is Jinja, evaluated during parsing. dbt records the dependency, builds the graph, decides the run order, and only then renders `ref()` into the fully-qualified relation name of the already-built upstream model. The graph is known before a single query executes, which is exactly why the render step exists.

It is like a mail merge: the letter template with placeholders is what you edit, but the printed letters are what actually get posted.

saying these in an interview costs you the question

  • Thinks the warehouse itself evaluates the Jinja
  • Cannot say where compiled SQL is written on disk
  • Believes Jinja conditionals can test column values per row
  • Debugs templating by guessing instead of reading compiled output
  • Assumes dbt run sends the model file verbatim to the database

context

open as a page

How do you define a reusable macro in a dbt project and call it from a model?

level: juniorimportance: must knowfreq 66%

basics

~20 s

Put a {% macro name(args) %} ... {% endmacro %} block in a .sql file under the macros/ directory, then call it from any model with {{ name(args) }}. The macro's rendered text is spliced into the model's SQL at compile time.

open as a page

In dbt, what is the difference between the view and table materializations?

level: juniorimportance: must knowfreq 85%

basics

~20 s

In dbt, a view materialization stores only the SQL, so every read recomputes the query; a table materialization runs the query once per dbt run and stores the rows, making reads fast but data only as fresh as the last build.

open as a page

In dbt, what is a model, and what does a model's .sql file actually contain?

level: juniorimportance: must knowfreq 88%

basics

~20 s

A dbt model is one SELECT statement in a .sql file under models/. dbt renders its Jinja, wraps it in the DDL for the chosen materialization, and builds a table or view named after the file.

open as a page

In dbt, why should a model use ref('stg_orders') instead of a hard-coded table name?

level: juniorimportance: must knowfreq 90%

basics

~20 s

Because ref() does three things a literal name cannot: it declares a dependency so dbt builds the upstream model first, it resolves to the right database and schema for the environment you are running in, and it feeds lineage, docs and selection.

open as a page

What does a dbt snapshot do, and which dbt_ columns does it add?

level: juniorimportance: must knowfreq 78%

basics

~10 s

A dbt snapshot records how mutable source rows change over time as a Type 2 history table. Each run closes changed versions and inserts new ones, maintaining dbt_valid_from, dbt_valid_to, dbt_updated_at and dbt_scd_id.

open as a page

In dbt, when does a model use source() instead of ref()?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Use source() for raw tables dbt reads but never builds, declared in a YAML sources block. Use ref() for anything dbt builds itself: models, seeds and snapshots. Both create DAG edges instead of hardcoded table names.

open as a page

In dbt, what is a seed and what data belongs in one?

level: juniorimportance: must knowfreq 62%

basics

~20 s

A seed is a CSV file in the project's seeds directory that dbt loads into the warehouse as a table when you run dbt seed. It suits small, static, version-controlled reference data, not raw source data.

open as a page

In dbt, what do the generic tests unique, not_null, accepted_values and relationships check?

level: juniorimportance: must knowfreq 80%

basics

~20 s

dbt's four built-in generic tests assert column rules from YAML: unique flags duplicate values, not_null flags missing ones, accepted_values flags values outside a declared list, and relationships flags keys with no matching row in a referenced model.

open as a page

In a dbt incremental model, when does the is_incremental() macro return true?

level: middleimportance: must knowfreq 76%

basics

~20 s

In dbt, is_incremental() returns true only when the model is materialized as incremental, its target relation already exists in the warehouse, and the run is not in full-refresh mode. Otherwise dbt builds the model in full and the guarded filter is skipped.

open as a page

In a dbt snapshot, when do you choose the check strategy over timestamp?

level: middleimportance: must knowfreq 70%

basics

~20 s

Use dbt's check strategy when the source has no trustworthy updated_at column: it compares the values in check_cols instead. Prefer timestamp whenever a reliable modification timestamp exists, because it dates history by when the change happened rather than when dbt ran.

open as a page

In dbt, what SQL does a test compile into, and what makes it pass or fail?

level: middleimportance: must knowfreq 64%

basics

~20 s

Every dbt test compiles to a SELECT that returns the failing rows, wrapped in a count. Zero rows means pass; any rows returned means fail, and the count is reported as the number of failures.

open as a page

In dbt, how do you install the dbt_utils package and call its macros from a model?

level: middleimportance: should knowfreq 55%

basics

~10 s

List the package with a version range in packages.yml, run dbt deps to install it under dbt_packages/, then call its macros namespaced by package name, for example {{ dbt_utils.star(from=ref('orders')) }}.

open as a page

In dbt, how do you generate model columns from values a query returns at run time?

level: middleimportance: should knowfreq 50%

basics

~20 s

Query the values with run_query(), guard the result with {% if execute %} because run_query returns nothing during parsing, pull the column with results.columns[0].values(), then loop over that list to emit one SQL expression per value.

open as a page

In a dbt incremental model, what does the unique_key config do?

level: middleimportance: should knowfreq 62%

basics

~20 s

In dbt, unique_key names the column or columns that identify a row, so an incremental run updates or replaces matching rows in the target instead of only appending. Without it, an incremental model appends and can accumulate duplicates.

open as a page

In dbt, what does compilation turn {{ ref('stg_orders') }} into, and where is the compiled SQL written?

level: middleimportance: should knowfreq 58%

basics

~20 s

Compilation renders the Jinja and replaces the call with the referenced model's fully qualified relation for the active target. The plain SELECT is written under target/compiled; the wrapped DDL dbt actually executes is written under target/run when the model builds.

open as a page

In dbt, what does `dbt run --select orders+` build, and how is `+orders` different?

level: middleimportance: should knowfreq 50%

basics

~20 s

A trailing plus selects the model and everything downstream of it; a leading plus selects the model and everything upstream. Both include the model itself, and both traverse the dependency graph dbt builds from ref() and source() calls.

open as a page

Why does a dbt snapshot miss changes that occur between two snapshot runs?

level: middleimportance: should knowfreq 45%

basics

~20 s

A dbt snapshot samples the source's current state each time it runs. If a row moves through several values between runs, only the last one is seen, so intermediate versions are never recorded and cannot be recovered later.

open as a page

In dbt, how does the source freshness check decide a raw table is stale?

level: middleimportance: should knowfreq 55%

basics

~20 s

It takes the maximum value of the source table's loaded_at_field, measures the lag to the current time, and compares that lag with the warn_after and error_after thresholds in the source YAML, returning pass, warn or error per table.

open as a page

In a dbt source definition, what do database, schema and identifier control?

level: middleimportance: should knowfreq 48%

basics

~20 s

They form the physical relation dbt compiles a source() call into. The source name defaults to the schema and the table name defaults to the identifier, so you set schema or identifier only when the warehouse names differ from the names you want in code.

open as a page

In dbt, what do dbt docs generate and dbt docs serve produce?

level: middleimportance: should knowfreq 45%

basics

~20 s

dbt docs generate writes manifest.json and catalog.json into the target directory — the project graph plus column metadata queried from the warehouse. dbt docs serve starts a local web server rendering those files as a browsable catalog with a lineage graph.

open as a page

In dbt, when would you write a singular test instead of a generic one?

level: middleimportance: should knowfreq 55%

basics

~20 s

Write a dbt singular test when the assertion is one-off and specific to particular models: a plain SQL file in tests/ that selects offending rows. Generic tests are parameterised macros declared in YAML and reused across many models and columns.

open as a page

In dbt, how do the append, merge, delete+insert and insert_overwrite incremental strategies differ?

level: seniorimportance: should knowfreq 54%

basics

~20 s

In dbt, append blindly inserts new rows; merge matches on unique_key and updates in place; delete+insert removes matching keys then reinserts; insert_overwrite replaces whole partitions present in the run. Which are available depends on the adapter.

open as a page

Why does a dbt incremental model filtered on event_at > (select max(event_at) from {{ this }}) miss late-arriving rows?

level: seniorimportance: should knowfreq 47%

basics

~20 s

That filter compares business event time against the highest event time already loaded, so any row arriving later but dated earlier falls below the watermark and is never selected. The run succeeds and the rows are silently lost forever.

open as a page

A dbt model selects from analytics.stg_orders by full table name instead of ref() — what breaks?

level: seniorimportance: should knowfreq 52%

basics

~20 s

The dependency becomes invisible: dbt may build the model before or alongside its upstream, so results are intermittently stale. Development and CI runs read production data, lineage and docs are wrong, and selection or slim CI silently skips the model.

open as a page

In a dbt snapshot, what happens to a row that disappears from the source?

level: seniorimportance: should knowfreq 38%

basics

~10 s

By default nothing: dbt never sees it, so its latest version keeps dbt_valid_to NULL and still looks current forever. Configuring hard-delete handling makes dbt close that version out at run time instead.

open as a page

What is lost when a dbt snapshot table is dropped and rebuilt from source?

level: seniorimportance: should knowfreq 55%

basics

~20 s

Every past version. A rebuilt dbt snapshot contains one open row per current source row, so all closed validity windows vanish permanently — unlike models, snapshots accumulate state that cannot be re-derived from a mutable source.

open as a page

How do you stop dbt models from building on a stale source table?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A dbt run never checks freshness, so you must gate it: run dbt source freshness as a separate orchestrator step that fails the pipeline on error, or put a recency test on the source so dbt build skips every downstream model when it fails.

open as a page

In dbt, why does dbt build skip downstream models on a test failure when dbt run then dbt test does not?

level: seniorimportance: should knowfreq 52%

basics

~20 s

dbt build walks the DAG node by node, running each model and then its own tests before moving on, so a failed test marks that node an error and its descendants are skipped. dbt run then dbt test builds everything first and only tests afterwards.

open as a page

A dbt not_null test fails on 3 rows and blocks the nightly build — how do you unblock it?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Set the test's severity to warn, or give it a threshold with warn_if and error_if so small counts warn while large ones still fail. Turn on store_failures to persist the offending rows for investigation instead of deleting the test.

open as a page

showing 1–30 of 38