learn-analytics-engineering-with-phoebe / Builder session 1 of 10
Learn Analytics Engineering with Phoebe · Builder track · Session 1 of 10

From hand-written SQL to a project

The sibling course, learn-data-modeling, built a marketplace warehouse by hand: DDL, a transform script, four marts, and a Python validator with 27 checks. It works. This course rebuilds exactly the same warehouse as a dbt project - 23 models, 100 tests, one snapshot - and gets the same numbers to the cent. Today you run it end to end, read the output of a real dbt build, and see what the tooling actually bought: not different SQL, but dependencies it works out for you and tests that run every single time.

🟢 Builder track Analytics engineers · analysts · DE Runs in your browser · real DuckDB Start here
0-3 · Welcome 3-18 · What dbt is, and what it is not 18-42 · Build-along: run the project, read the output 42-45 · Q&A
Part 0

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.

Live - presented in session Self-study - read after class ▶ Live SQL - real DuckDB, editable Sources covered
★ What you walk out with today An honest account of what dbt does and does not do, the output of a real build read line by line, and a browser tab running the finished project - staging views, dimensions, facts and marts - that you can query any time.
Part 1 · covers dbt documentation, the analytics-engineering premise

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.

dbt DOES compile SELECTs into tables and views work out the dependency order run tests on every build generate docs and lineage version the whole thing in git dbt does NOT extract or load data schedule itself store data of its own design your model for you fix a bad model - see the note Same warehouse, measured by hand: 8 SQL files, 27 checks as dbt: 23 models, 100 tests revenue: 111,906.48 both ways build time: 1.22 seconds dependency order: derived The load-bearing sentence of this whole course: dbt makes a good model cheaper to maintain and a bad model faster to spread. It has no opinion about grain, and it will happily build 200 models on a wrong one. That is why learn-data-modeling comes first, and why this course reuses its model rather than inventing one.
🔍 Click to zoom - the honest scope of the tool
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.
What it costs A build step, a Python dependency, Jinja in your SQL, and a team that has to learn all three. On a five-model project that is a bad trade. Somewhere around fifteen to twenty models, with more than one person touching them, it stops being close - and session a4 is the leader-track version of that judgement.
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.

Part 2 · the output you will read every day

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.

Real output

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

One real build, at a glance Data tests 100 tests Models 23 models Sources 8 sources Found 23 models, 100 data tests, 8 sources - the same build that runs the labs below.
🔍 Click to zoom - one build, 23 models and 100 tests, every time
LineWhat 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 threadsdbt runs independent models in parallel; the DAG is what makes that safe
1 of 121models 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 modelthe materialization actually used, which is worth checking against what you intended
PASS=121 ERROR=0the only line CI cares about. Any ERROR and the build exits non-zero
SKIPa 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.

✗ Script keeps going star comes out wrong marts rebuilt from it anyway dashboards refresh with bad data ✓ dbt SKIPs downstream a test fails on one model downstream builds are skipped marts keep yesterday's good data A script runs steps and cannot stop itself; a graph knows what to stop before it builds it wrong.
🔍 Click to zoom - a script runs steps; a graph knows what to stop
The one-line version A script runs steps. dbt runs a graph, and a graph knows what to stop.
Demo 1 of 2

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;
Real world

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.

Demo 2 of 2

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.

Homework

Try it yourself - this week ◐ 20-30 min total

Source material

Sources covered

This page covers:

✓dbt documentation - what dbt is, models as SELECTs, build output, materializationsParts 1-2 · read from a real build of this project
✓The dbt layering convention (staging, intermediate, marts)Part 1 self-study · the four layers, with materialization per layer
◐Kimball & Ross - the dimensional model being rebuilt hereDemo 2 · the model itself is learn-data-modeling, not this course
◐Orchestration and schedulingNamed as out of scope - dbt does not schedule itself; that is learn-dataops
Check yourself

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.

Builder session 1 cheat sheet · pin this

dbt doesCompiles SELECTs into tables/views, in dependency order, with tests, docs and git.
dbt does notExtract, load, schedule, store data, or have an opinion about your model.
The tradeA build step, a Python dep and Jinja, for dependency order and tests that always run.
Four layerssources → staging (views) → intermediate (ephemeral) → marts (tables).
This project23 models, 100 tests, 1 snapshot, 8 sources. dbt build: PASS=121, ERROR=0, 1.22s.
Same numbers111,906.48 settled revenue, 957 orders, AOV 116.93 - identical to the hand-built version.
SKIP mattersA failed test stops everything downstream. A script would have rebuilt it anyway.
Blast radiusGood models get cheaper; bad models spread faster. Next: b2, the config.