How this track works
Ten sessions, one project. Bazaar is the multi-merchant marketplace from learn-data-modeling: 12 merchants, 60 products, 985 orders, 1,060 payment attempts, 90 days. You already know the model - which is the point. Nothing in this track asks you to learn a new domain while you learn a new tool, so every session is about the craft: sources and staging (b3), ref() and the DAG (b4), intermediate models (b5), dimensions, facts and snapshots (b6), tests (b7), marts (b8), contracts and deployment (b9), and a new subject area end to end in the capstone (b10).
The project in dbt_project/ is real and it runs. Every number on these pages comes from an actual dbt build, and the model SQL in the browser labs is the SQL dbt compiled - not a transcription.
What dbt is, and what it is not 7 min live
dbt does one thing: it takes SELECT statements you write and turns them into tables and views in your warehouse, in dependency order, with tests. It does not move data, it does not schedule itself, and it does not have an opinion about your model. Everything else it appears to offer follows from that one job being done properly.
LiveWhy "models as code" changes the job4 min▶
The SQL in this project is not better SQL than the hand-written version. What changed is everything around it:
- Dependencies are declared once, in the SQL. You write
{{ ref('stg_bazaar__orders') }}instead of a table name, and dbt derives the build order. Nobody maintains a runbook saying what to run first - session b4 is about exactly this. - Tests live next to the models and run every time. The hand-written version had a 27-check Python validator somebody had to remember to run. This project has 100 tests that run on
dbt build, and a failure stops the build. - Everything is reviewable. A model change is a pull request with a diff, so a definition change is visible before it ships instead of being discovered in a dashboard.
- Documentation is generated from the same files. Descriptions live beside the columns they describe, so the docs cannot drift from what is queryable.
Self-studyThe four layers this project uses3 min read▶
The layering is a convention, not a dbt feature, and it is the single most useful convention in the ecosystem:
- sources - the eight raw Bazaar tables, declared once in
_bazaar__sources.yml. The only place a raw table name appears. - staging (8 models, materialized as views) - one per source table: rename, recast, light cleaning, no joins. Cheap and disposable.
- intermediate (3 models, ephemeral) - the joins staging is not allowed to do. Ephemeral means dbt inlines them into whatever references them, so nothing is materialized and nobody can query a half-finished shape by accident.
- marts (12 models, materialized as tables) - the dimensions, facts and aggregates people actually query.
Materialization is a decision per layer, written once in dbt_project.yml: views for staging because they are cheap and always fresh, tables for marts because they are queried constantly, ephemeral for intermediate because they are plumbing. Session b2 walks that config line by line.
What a real build says 4 min live
This is the tail of an actual dbt build on this project, on a laptop, against DuckDB. Learn to read it and you can debug most of what goes wrong.
Found 23 models, 100 data tests, 1 snapshot, 8 sources, 489 macros
Concurrency: 4 threads (target='dev')
1 of 121 START sql view model main_staging.stg_bazaar__users .... [RUN]
1 of 121 OK created sql view model main_staging.stg_bazaar__users [OK in 0.04s]
...
119 of 121 PASS unique_combination_mart_merchant_daily_activity_date__merchant_id [PASS]
Finished running 1 snapshot, 12 table models, 100 data tests, 8 view models in 1.22s
Completed successfully
Done. PASS=121 WARN=0 ERROR=0 SKIP=0 NO-OP=0 TOTAL=121
| Line | What it tells you |
|---|---|
Found 23 models, 100 data tests... | parsing worked. If a model has a syntax error in its Jinja, you never get past this line |
Concurrency: 4 threads | dbt runs independent models in parallel; the DAG is what makes that safe |
1 of 121 | models and tests are one queue - dbt build interleaves them, so a model's tests run as soon as it is built |
OK created sql view model | the materialization actually used, which is worth checking against what you intended |
PASS=121 ERROR=0 | the only line CI cares about. Any ERROR and the build exits non-zero |
SKIP | a model whose upstream failed. In a hand-written script it would have run anyway, on stale data |
LiveWhy SKIP is the most underrated word in that output3 min▶
When a test fails, dbt does not just report it - it refuses to build anything downstream of the broken model. Your marts keep yesterday's data rather than being rebuilt from something known to be wrong.
Compare with the hand-written version of this warehouse: the transform script ran top to bottom, and if the star came out wrong the marts were rebuilt from it anyway, because a script has no idea what depends on what. Then the dashboards refreshed. The validator would have caught it - if someone had run the validator.
Query the built project ★ 12 min · everyone builds
These editors run real DuckDB in your browser, with this project already built from Bazaar's raw schema using dbt's own compiled SQL. The first Run downloads the engine (about 8 MB, once), then everything is local.
Look at what exists. Three schemas: raw (the loader's output), main_staging (8 views), main_marts (12 tables).
Reconcile. Ask the marts the same question the hand-built warehouse answered, and check the number matches to the cent.
Then edit. Every box is editable - this is a real engine, so break things and see what happens.
SELECT table_schema, table_type, count(*) AS objects
FROM information_schema.tables
WHERE table_schema IN ('raw', 'main_staging', 'main_marts')
GROUP BY table_schema, table_type
ORDER BY table_schema, table_type;
SELECT round(sum(net_amount), 2) AS net_revenue,
count(DISTINCT order_id) AS settled_orders,
round(sum(net_amount) / count(DISTINCT order_id), 2) AS aov,
count(*) AS fact_rows
FROM main_marts.fct_order_items
WHERE is_settled;
That identical number is the honest sales pitch for dbt. Not "better numbers" - the same numbers, produced by SQL that is now version-controlled, dependency-ordered, tested on every run and documented from the same files. Anybody promising that a transformation tool improves your numbers is selling you something else; what it improves is your ability to change them safely.
Your turn: the four questions, one layer up ★ 10 min · build your own
The modeling course answered four business questions with hand-written SQL against hand-built marts. Same questions, same answers, now against dbt-built marts.
LiveQ1 · Which merchant dropped?4 min▶
SELECT merchant_id, merchant_name,
round(sum(CASE WHEN activity_date >= '2026-06-16' THEN net_revenue ELSE 0 END), 0) AS recent_14d,
round(sum(CASE WHEN activity_date >= '2026-06-02'
AND activity_date < '2026-06-16' THEN net_revenue ELSE 0 END), 0) AS prior_14d
FROM main_marts.mart_merchant_daily
GROUP BY 1, 2
ORDER BY recent_14d - prior_14d
LIMIT 5;
LiveQ2 · Peak transaction hours3 min▶
SELECT hour_label, daypart, sum(attempts) AS attempts, sum(approvals) AS approvals FROM main_marts.mart_hourly_transactions GROUP BY 1, 2 ORDER BY attempts DESC LIMIT 6;
Self-studyQ3 · What could this project NOT protect you from?3 min▶
No SQL. dbt would have built this project happily if the fact table had been at the wrong grain, if revenue had been defined as gross, or if the two facts had been joined together in a mart. It has no opinion about any of that: it compiles what you write, in the right order, and tests what you told it to test.
Which means the risk moves rather than disappearing. A hand-written estate fails slowly and visibly - queries diverge, people argue, somebody eventually digs in. A dbt estate fails quickly and consistently: one wrong definition in a staging model propagates to every mart and every dashboard on the next build, in seconds, everywhere. That is a real improvement in traceability and a real increase in blast radius, and session b9's contracts exist because of it.
Try it yourself - this week ◐ 20-30 min total
- Clone this repo and run it:
python python/load_raw.py, thencd dbt_project && dbt build. You need dbt-core and dbt-duckdb; there is no cloud account and no warehouse bill. - Read the build output and find the first test that runs. Notice how early it is -
dbt buildtests each model as soon as it exists rather than at the end. - Break something on purpose: change a column name in a staging model and rebuild. Read which models fail and which get skipped.
- Count the models in your own warehouse's most complicated transformation script. If it is over fifteen and more than one person edits it, you have session a4's answer already.
- Bring one question about your own stack to b2 - the config session is where "how would this work at my company" gets answered.
Sources covered
This page covers:
Three questions before you go 🎯 ◐ 90 seconds
1 · Which of these does dbt do?
That is the whole job. It does not extract, it does not schedule itself, and it stores nothing - your warehouse holds the data. Everything else dbt appears to offer follows from doing that one job properly.
2 · Rebuilt as a dbt project, Bazaar's settled revenue is 111,906.48 - identical to the hand-built warehouse. What does that prove?
Same numbers, produced by SQL that is now version-controlled, dependency-ordered, tested on every run and documented from the same files. A tool that claimed to improve your numbers would be selling you something else.
3 · A test fails on a staging model. What happens to the marts downstream of it?
SKIP is the most underrated word in the build output. A hand-written script runs steps and has no idea what depends on what; dbt runs a graph, and a graph knows what to stop.