skip to content

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

level: juniorimportance: must knowfreq 88%

answer

  1. one file, one query
  2. you never type CREATE TABLE
  3. the filename is the name
  4. dbt supplies the wrapper around your SELECT

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.

solid answer

~50 s

A **model** in dbt is a single `SELECT` statement stored in a `.sql` file under the project's `models/` directory. You never write `CREATE TABLE` or `INSERT` yourself: at build time dbt renders any Jinja in the file, then wraps the resulting query in whatever DDL the model's **materialization** calls for — `create view as` for a view, `create table as` for a table, a merge or insert for an incremental model. The filename is the model name and, unless you set an `alias`, the name of the relation dbt creates in the warehouse; the database and schema come from the connection target plus any `database`/`schema` config. Model names must be unique across a project because `ref()` looks models up by name, not by path. Folders under `models/` are for organisation and for applying config in `dbt_project.yml`.

code

sql · 12 lines
sql
-- models/marts/fct_orders.sql
{{ config(materialized='table') }}

select
    o.order_id,
    o.customer_id,
    o.ordered_at,
    sum(l.amount) as order_total
from {{ ref('stg_orders') }} as o
join {{ ref('stg_order_lines') }} as l
    on l.order_id = o.order_id
group by 1, 2, 3

go deeper

for a junior

Be able to say it in one line: a model is a SELECT in a .sql file under models/, and dbt turns it into a table or view named after the file. Know that you never write the CREATE statement yourself.

for a middle

Explain the mechanics: Jinja is rendered first, the materialization decides the DDL wrapper, and the relation address comes from the target's database and schema plus any config. Know that alias renames the relation without renaming the model.

for a senior

Show you have operated a real project: model names are a flat project-wide namespace, folder layout drives config in dbt_project.yml, and custom schema behaviour is macro-controlled and worth pinning down before a team grows.

for a principal

Own the conventions. Decide the layering, naming and directory-level config that keep a project navigable at hundreds of models, and be able to justify why the SELECT-only constraint is a feature rather than a limitation.

## What a model is A dbt **model** is a file of SQL that lives under the `models/` directory of a dbt project and contains exactly one `SELECT` statement. dbt does not execute that file as written. When you run `dbt run` (or `dbt build`), dbt renders the Jinja templating in the file, wraps the resulting query in the DDL appropriate to the model's *materialization*, and executes that statement against the warehouse connection defined by your profile. The file you author describes **what the result set is**; dbt decides **how it is persisted**. That split is the whole idea. Analysts write the part they are good at — a query — and the boilerplate that differs per warehouse and per environment (create-or-replace semantics, transactions, temp tables, schema names) is generated for them and is identical across every model in the project. ## One file, one SELECT, one relation The conventions are strict and worth stating precisely: - **One model per file.** A model file holds a single `SELECT`. It is not a script: you cannot put two statements in it, and DML such as `INSERT`, `UPDATE` or `DELETE` does not belong there. Work that genuinely needs extra statements goes into `pre_hook`/`post_hook` config or a separate operation. - **The filename is the model name.** `models/marts/fct_orders.sql` is the model `fct_orders`. That name is what `ref('fct_orders')` resolves against, and by default it is also the identifier of the relation dbt creates in the warehouse. The `alias` config overrides the relation name while leaving the model name — and therefore every `ref()` to it — unchanged. - **Model names must be unique project-wide.** Two files called `orders.sql` in different folders is an error, because `ref()` selects by name and dbt would not know which one you meant. - **Folders are organisation, not namespacing.** A common layout is `models/staging/`, `models/intermediate/` and `models/marts/`. Paths matter because `dbt_project.yml` can apply configuration by directory (for example, everything under `staging` materialized as a view), and because selection syntax can target a path — but they never form part of the model's name. ## You never write the DDL The materialization decides the wrapper. A model configured as a view becomes roughly `create or replace view <db>.<schema>.<name> as (<your select>)`; a table becomes a `create table as select`; an incremental model becomes a create on first run and a merge or insert on later runs. You change the strategy by changing one config value, not by rewriting the SQL: ```sql {{ config(materialized='table') }} select o.order_id, o.customer_id, sum(l.amount) as order_total from {{ ref('stg_orders') }} o join {{ ref('stg_order_lines') }} l using (order_id) group by 1, 2 ``` Everything outside the `select` in that file is Jinja: a `config()` call that sets options, and `ref()` calls that both name upstream models and declare dependencies on them. ## Where the model lands The relation dbt builds is addressed as `database.schema.identifier`. The database and schema default to the ones in the active target of your `profiles.yml`, which is why the same project builds into a personal dev schema for you and into the production schema on the scheduled run. A `schema` config on a model is, by default, *appended* to the target schema rather than replacing it, so a model configured with `schema='marts'` running against a target schema of `dbt_prod` lands in `dbt_prod_marts`. That behaviour is produced by a macro the project can override. ## What is not a model Several other resource types live in a dbt project and are frequently confused with models: **seeds** are CSV files loaded as small static tables, **snapshots** capture history of a changing source, **tests** are assertions, **macros** are reusable Jinja, and **analyses** are compiled but never built. Only files under the model paths become relations from a `SELECT`. ## Why interviewers ask this It is the screening question for the tool. A candidate who describes a model as "a table I create with DDL" has not used dbt; a candidate who says "a `SELECT` that dbt materialises for me, named after the file, with dependencies declared through `ref()`" has. It also sets up everything else: build order, environments, testing and documentation all hang off the fact that a model is a named query dbt owns end to end.

  • If a dbt model file is named fct_orders.sql, can the table in the warehouse be called something else?
    Yes. The `alias` config sets the relation identifier independently of the model name — `{{ config(alias='orders_fact') }}` builds a table called `orders_fact`. The model is still `fct_orders` everywhere inside the project, so every `ref('fct_orders')` keeps working. Aliasing is how you satisfy an external naming standard without renaming files or breaking references.
  • Why can't you put two SQL statements in one dbt model file?
    Because dbt wraps the file's contents as the body of a single generated statement — a `create view as (...)` or `create table as (...)`. A second statement would land inside that wrapper and fail to parse. Work that needs extra statements belongs in `pre_hook`/`post_hook` config, in a macro run as an operation, or in its own model.
  • What happens if two dbt model files in different folders have the same filename?
    dbt raises a duplicate-name error at parse time and refuses to run. Model names are the project-wide namespace that `ref()` resolves against, so folders do not disambiguate them. You rename one of the files, or use `alias` if the two need the same relation name in different schemas.

saying these in an interview costs you the question

  • Says a model is a table you create with your own DDL
  • Claims the model file must include CREATE OR REPLACE
  • Thinks a model file can hold several statements or INSERTs
  • Believes model names come from a YAML entry rather than the filename
  • Assumes folder paths are part of the model's name

context