When dbt build runs a model, it saves the result in the warehouse. How it saves it is the model's materialization. With no setting, a model is a view: dbt stores only the SELECT, and the warehouse runs it again each time someone reads the view, so it always shows the raw rows as they are now. A table stores the rows themselves at build time: reading it is fast, but it shows the data of the last build until the next one. Data teams pick one per model: light staging models stay views, and marts that people and dashboards read all day become tables.
You choose with a config line at the top of the model, in Jinja's {{ }}: dbt reads it and leaves it out of the SQL. Its two values here are 'view' and 'table'. For example, a products model that should stay a view:
{{ config(materialized='view') }}
SELECT product, SUM(quantity) AS sold
FROM {{ ref('stg_orders') }}
GROUP BY product
You edit models/marts/daily_revenue.sql, the daily revenue mart. It reads this given staging model, models/staging/stg_orders.sql, which keeps the last copy of each order:
SELECT
order_id,
customer_id,
product,
TRY_CAST(quantity AS INTEGER) AS quantity,
CAST(unit_price AS INTEGER) AS unit_price,
status,
CAST(ordered_at AS TIMESTAMP) AS ordered_at
FROM {{ source('shop', 'raw_orders') }}
QUALIFY ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY filename DESC) = 1
The check builds the project and asks DuckDB what each model became. Then a loader adds orders/2026-05-08.csv to raw_orders (two paid orders worth 24 and 29, and one cancelled), and the check compares daily_revenue's totals with the paid orders of stg_orders, before and after the next build. That is why the mart needs an orders column: the number of paid orders each day. Your code must print:
model stg_orders (view): OK model daily_revenue (table): OK table_name | table_type daily_revenue | BASE TABLE stg_orders | VIEW (2 rows) The loader added orders/2026-05-08.csv, no dbt build yet: model | paid_orders | revenue daily_revenue | 64 | 2226 stg_orders | 66 | 2279 (2 rows) After dbt build: model stg_orders (view): OK model daily_revenue (table): OK model | paid_orders | revenue daily_revenue | 66 | 2279 stg_orders | 66 | 2279 (2 rows)
Until the second build, the table keeps the 64 orders of the first one, while the view stg_orders already sees 66. At work you type dbt build in a terminal, and the warehouse is Snowflake or BigQuery instead of a DuckDB file; there a table also saves money, since a dashboard reading it doesn't rerun the whole query.
Your task: add an orders column that counts each day's paid orders, then make daily_revenue a table with config.
Assignment
Follow the Coding Tutorial to count each day's orders and store daily_revenue as a table.
Hint
A model with no config line is a view. COUNT(*) counts the rows in each day's group, and the WHERE already keeps paid orders only. The config line goes first in the file, on its own line.
Free plan: 30 tutor messages. PRO is unlimited.
