◐ ai-teacher

🔀 Pipelines & Orchestration · Chapter 6

dbt: models, refs, tests, incremental strategies

dbt's whole idea fits in one line: every table is a SELECT statement in a file, and ref() turns a folder of those files into a dependency graph you can build, test and re-run.

Study this lesson free in the app →

The basics

dbt (data build tool) is the standard way to do the model stage of a pipeline inside the warehouse. You write each table as a model — a .sql file containing one SELECT — and dbt creates it as a view or table, in the right order, then runs tests on the result.

The problem it solves: before dbt, warehouse transformations were hundreds of hand-ordered CREATE TABLE AS scripts, no tests, no docs, and nobody knew what broke what. dbt brings software practice to SQL: version control, dependency order, testing, environments, documentation.

The mental model: a spreadsheet where each sheet's formula references other sheets. Change one sheet and you know exactly which ones depend on it. In dbt, {{ ref('stg_rides') }} is that reference — it tells dbt "build stg_rides first" and resolves to the right schema for dev or prod.

Words you must know 5

Model
one SELECT in one file = one table/view
Materialization
how it's built: view, table, incremental, ephemeral
ref() / source()
references to other models / to raw tables
Test
a query that returns failing rows (unique, not_null…)
Incremental model
on later runs, processes only new/changed rows and merges them in

Explained

Meghalaya Cabs has 140 dbt models between raw rides and the finance dashboard. When stg_rides changes, dbt build --select stg_rides+ rebuilds exactly the 23 models downstream and runs their 180 tests — in order, stopping anything that depends on a failure.

That sentence is why dbt won.

The recipe book with cross-references

A restaurant's recipe book says "Jadoh: use 2 cups of stock (see page 14)". The cook doesn't need to know where stock fits in the day — the cross-reference tells him it must be made first.

Each recipe ends with a taste check: "must not be oversalted". If the stock fails its check, nobody makes the jadoh with it.

And for the big daily dal, nobody cooks the full pot from scratch every hour — they add today's batch to what's already there. That's an incremental model.

Recipes = models. Cross-references = ref(). Taste checks = tests. Topping up the pot = incremental.

Models, refs and materializations

Model — models/marts/fct_rides.sql, one SELECT. dbt wraps it in the DDL.

{{ ref('stg_rides') }} — builds the DAG edge and resolves to the environment's schema (dev_raj.stg_rides in dev, analytics.stg_rides in prod). Never hard-code schema names.

{{ source('app', 'rides') }} — declares raw tables in YAML; dbt source freshness alerts when loaded_at_field is too old.

Materializations:

Layers: staging (1:1 with sources, rename/cast) → intermediate → marts (facts and dims).

Tests

Generic data tests declared in YAML:

models:
  - name: fct_rides
    columns:
      - name: ride_id
        data_tests: [unique, not_null]
      - name: status
        data_tests:
          - accepted_values:
              values: ['completed', 'cancelled', 'no_show']
      - name: driver_id
        data_tests:
          - relationships: {to: ref('dim_driver'), field: driver_id}

Singular tests — a .sql file in tests/ returning rows that violate a rule (select * from {{ ref('fct_rides') }} where status = 'completed' and fare_paise <= 0). Zero rows = pass.

severity: warn for soft rules. dbt build runs models and tests in DAG order and skips everything downstream of a failed test. (dbt 1.8 renamed tests: to data_tests: and added unit tests that check logic on mocked inputs.)

Incremental strategies, precisely

on_schema_change: ignore (default) · append_new_columns · sync_all_columns · fail.

At scale

Slim CI. With 1,000+ models, CI runs dbt build --select state:modified+ --defer --state prod-artifacts: only changed models and their children, reading unchanged parents from prod.

Incremental merges get slow. A MERGE against a 40-crore-row target scans it to match keys. Partition/cluster the target by date and add incremental_predicates (e.g. only match target rows from the last 7 days), or use insert_overwrite / microbatch on date partitions.

Lookback is a trade-off. Longer lookback catches later corrections but costs more per run. Pick it from data — "99.9% of fare corrections happen within 72 hours" — and schedule a periodic full refresh for the rest.

Tests cost compute too. unique on a 40-crore table every hour is expensive; test incrementally (dbt where config on tests) or at lower frequency.

Gotchas

A model is a SELECT. ref() is the graph. Tests are queries that should return nothing. Incremental = is_incremental() filter + a strategy that's safe to re-run.

For mutable rows: merge on a unique key with a lookback. For date-partitioned facts: insert_overwrite or microbatch.

Words to know

ModelA SQL file containing one SELECT that dbt materializes as a table or view.
ref()A Jinja function that references another model, creating a DAG edge and resolving its schema.
MaterializationThe strategy dbt uses to build a model: view, table, incremental or ephemeral.
Generic testA reusable data test declared in YAML, such as unique or not_null.
is_incremental()Macro that is true only when an incremental model's table exists and no full refresh was requested.
Incremental strategyHow new rows are applied to an incremental model: append, merge, delete+insert, insert_overwrite or microbatch.
Snapshotdbt feature that records SCD Type 2 history of a mutable source table.

In two minutes

Test yourself

1. What does {{ ref('stg_rides') }} do besides returning a table name?

  1. Copies stg_rides into a temp table
  2. Adds a dependency edge so stg_rides builds first, and resolves to the current environment's schema
  3. Runs the tests on stg_rides
  4. Nothing else, it's just string substitution
Show the answer

Adds a dependency edge so stg_rides builds first, and resolves to the current environment's schema — ref() is how dbt learns the DAG and how the same code points at dev or prod schemas.

2. What is the default materialization of a dbt model?

  1. view
  2. table
  3. incremental
  4. ephemeral
Show the answer

view — Unless configured, models are views. Projects often set marts to table/incremental in dbt_project.yml.

3. A dbt data test passes when its query…

  1. returns exactly one row
  2. returns true
  3. returns zero rows
  4. runs without syntax errors
Show the answer

returns zero rows — Tests select the violating rows. Any rows returned are failures (or warnings with severity: warn).

4. On which run is is_incremental() false for an incremental model?

  1. Every other run
  2. Only when tests fail
  3. Only in production
  4. The first run, and any run with --full-refresh
Show the answer

The first run, and any run with --full-refresh — It's true only when the target exists as a table, the model is incremental, and no full refresh was requested.

5. fct_rides uses incremental_strategy='append' with a 3-day lookback. What happens?

  1. Rows in the lookback window are inserted again each run, creating duplicates
  2. dbt dedupes automatically
  3. Only new rows are added
  4. dbt raises an error
Show the answer

Rows in the lookback window are inserted again each run, creating duplicates — Append never matches keys. Any overlap or re-run duplicates rows; use merge (or delete+insert) on a unique key.

6. A date-partitioned BigQuery fact table is rebuilt for the last 2 days each run. Which strategy fits best?

  1. append
  2. ephemeral
  3. insert_overwrite — replace the affected partitions
  4. view
Show the answer

insert_overwrite — replace the affected partitions — Overwriting whole partitions is idempotent and avoids row-by-row MERGE matching on a big table.

There are 10 questions, 3 puzzles and 5 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →