Where the build stands
Last session you felt the OLTP vs OLAP split and let a columnar engine loose on 300,000 rows. But Daybreak's warehouse itself is still an empty lot: six raw OLTP tables with text dates, revenue split across two tables, and refunds mixed into every count. Tonight the first layer goes in. Staging is where raw source data gets typed, renamed, and inspected before anything downstream is allowed to touch it - and the rule you will repeat for the rest of your career is simple: never model on raw.
The three layers: staging, core, marts 8 min live
A warehouse is not one big pile of tables - it is an assembly line. Raw source lands in staging, gets modeled into the core (the star schema of b3-b5), and gets served to teams as marts (b7). Each layer has one job, and the discipline of not skipping layers is what separates a warehouse from a swamp.
LiveWhy staging exists: four promises4 min▶
Staging looks boring - it is mostly SELECT with a few CASTs. Its value is the four promises it makes to every layer above it:
- Typed: text dates become DATE, so month grouping and date math stop being string tricks. The source's mess ends here.
- Renamed: app-developer names become analyst names, and computed basics (like
line_amount) get calculated once, in one place. - Quality-gated: every load passes row-count, uniqueness, null, and freshness checks before anything downstream sees it. Part 3 builds all four.
- Replayable: staging is derived entirely from source by a script. Drop it, re-run it, get the same thing. No hand-edited table ever earns trust.
Self-studyWhere staging lives in real stacks3 min read▶
In this course staging is a set of tables inside DuckDB. In production the idea is identical, only the address changes:
- Cloud warehouses: a
stagingschema (or database) in Snowflake / BigQuery / Redshift, loaded by an ingestion tool, rebuilt or appended on a schedule. - dbt shops: staging is literally a folder of models named
stg_*- one per source table, CAST and rename only. The convention you learn tonight is the industry convention. - Lakehouse stacks: the same layer wears medallion names - "bronze" is roughly raw-landed, "silver" is roughly staged. Session b9 connects the vocabularies.
CREATE TABLE AS + CAST: the whole trick 7 min live
A staging table is one statement: CREATE TABLE stg_x AS SELECT ... FROM x, with CASTs where the source lied about types. That is genuinely the whole trick - the craft is in doing it consistently for every table, every load.
LiveBuild stg_orders and prove the date is a date4 min▶
In b1, typeof(order_date) exposed VARCHAR dates in the source. Fix it, then run the same proof against the staged copy:
CREATE TABLE stg_orders AS
SELECT order_id, customer_id,
CAST(order_date AS DATE) AS order_date,
status, channel
FROM orders;
SELECT order_id, order_date,
typeof(order_date) AS stored_as
FROM stg_orders
LIMIT 4;
VARCHAR became DATE. From here on, strftime(order_date, '%Y-%m'), date arithmetic, and calendar joins all just work - because staging paid the tax once.
LiveNaming conventions + TRY_CAST for dirty data3 min▶
Two habits that make a staging layer readable by strangers:
- The
stg_prefix, one source table each:ordersbecomesstg_orders, nothing merged, nothing skipped. Anyone can trace any staged column back to exactly one source column. - TRY_CAST when the source is dirty: CAST fails the whole load on one bad value; TRY_CAST turns the bad value into NULL and lets the load finish - then your null scan (Part 3) reports how many landed.
SELECT CAST('2026-06-22' AS DATE) AS clean_date,
TRY_CAST('oops' AS DATE) AS junk_becomes_null,
TRY_CAST('2026-13-99' AS DATE) AS impossible_date;
Which one should you use? CAST when a bad value should stop the presses (a corrupt export you must not half-load). TRY_CAST when the show must go on and you will count the NULLs after. Teams that pick neither on purpose end up with 3am load failures on Black Friday.
Four quality gates at the dock 7 min live
Staging is a loading dock, and a dock has inspectors. Four cheap checks catch the vast majority of load disasters: row counts reconcile against source, primary keys are unique, critical columns have no NULLs, and the data is fresh. Run them after every load, and a bad batch gets stopped at the door instead of discovered in the CEO's dashboard.
LiveRun all four gates in one script4 min▶
This playground starts with the staging layer already built (data-setup="staging"), so you can play inspector. One script, four gates, one PASS/FAIL summary:
SELECT '1 row counts reconcile' AS gate,
CASE WHEN (SELECT count(*) FROM orders)
= (SELECT count(*) FROM stg_orders)
THEN 'PASS' ELSE 'FAIL' END AS result
UNION ALL
SELECT '2 order_id unique',
CASE WHEN (SELECT count(*) FROM stg_orders)
= (SELECT count(DISTINCT order_id) FROM stg_orders)
THEN 'PASS' ELSE 'FAIL' END
UNION ALL
SELECT '3 no NULL order dates',
CASE WHEN (SELECT count(*) FROM stg_orders
WHERE order_date IS NULL) = 0
THEN 'PASS' ELSE 'FAIL' END
UNION ALL
SELECT '4 data is fresh',
CASE WHEN (SELECT max(order_date) FROM stg_orders)
>= DATE '2026-06-01'
THEN 'PASS' ELSE 'FAIL' END;
Gate 1 catches lost or doubled batches. Gate 2 catches duplicate loads and broken joins-to-be. Gate 3 catches the NULLs that TRY_CAST quietly minted. Gate 4 catches the silent scheduler that stopped running last Tuesday.
Build Daybreak's staging layer in one script ★ 12 min · everyone builds
Time to pour the whole foundation. One script builds three staging tables - customers, orders, order items - with typed dates and the line_amount computed once, then reports the row count of each. This exact layer is what b3 will model into a star.
Read the script top to bottom first. Every table is the same move: CREATE TABLE AS, SELECT, CAST where the source lied.
Run it. The final SELECT reports each staged table with its row count - your gate-1 numbers.
Now check the counts against the source you know: 15 customers, 33 orders. If a number surprised you, staging just did its job.
Change one thing: add WHERE status = 'completed' to stg_orders and re-run. Then undo it - staging COPIES, it does not filter. Filtering is a modeling decision that belongs in the core, made in daylight.
CREATE TABLE stg_customers AS
SELECT customer_id, name, city, country,
CAST(signup_date AS DATE) AS signup_date,
plan
FROM customers;
CREATE TABLE stg_orders AS
SELECT order_id, customer_id,
CAST(order_date AS DATE) AS order_date,
status, channel
FROM orders;
CREATE TABLE stg_order_items AS
SELECT order_id, product_id, quantity, unit_price,
quantity * unit_price AS line_amount
FROM order_items;
SELECT 'stg_customers' AS staged_table,
count(*) AS row_count FROM stg_customers
UNION ALL
SELECT 'stg_orders', count(*) FROM stg_orders
UNION ALL
SELECT 'stg_order_items', count(*) FROM stg_order_items;
This script IS the job. In a dbt project each CREATE TABLE AS above would be one stg_*.sql model file; the scheduler re-runs them every morning; the gates from Part 3 run as tests right after. Same SQL, bigger machine. Nothing about the idea changes at a billion rows.
Your turn: extend the layer, write a contract ★ 10 min · build your own
Same rules as always: write it, run it, read the error, fix it, run again. Q1 extends the staging layer; Q2 makes you think like a data contract; Q3 is the duplicate-catcher you will reuse forever.
Q1 and Q2 are live - try before peeking at the starter SQL. Q3 is your self-study pattern for the week.
Remember: every Run starts a fresh database, so each box must build whatever it needs.
LiveQ1 · Stage subscriptions with typed dates4 min▶
Subscriptions carry TWO text dates, and cancel_date is NULL for active subscribers - CAST handles NULL just fine. Build stg_subscriptions and prove the types:
CREATE TABLE stg_subscriptions AS
SELECT sub_id, customer_id, product_id,
CAST(start_date AS DATE) AS start_date,
CAST(cancel_date AS DATE) AS cancel_date,
monthly_qty
FROM subscriptions;
SELECT sub_id, start_date,
typeof(start_date) AS stored_as,
cancel_date
FROM stg_subscriptions
ORDER BY sub_id;
LiveQ2 · Contract check: any status outside the allowed set?3 min▶
A data contract says: "orders.status is always one of completed, refunded, cancelled." Write the query that finds violations - an empty result means the contract holds, and that emptiness is the answer you want:
SELECT order_id, order_date, status
FROM orders
WHERE status NOT IN ('completed', 'refunded', 'cancelled');
Self-studyQ3 · The duplicate-check pattern3 min▶
GROUP BY the key, HAVING count above one - the four-line pattern that catches double loads, bad merges, and fan-out joins for the rest of your career. Empty result = clean:
SELECT order_id, count(*) AS copies FROM orders GROUP BY order_id HAVING count(*) > 1;
Where it saves you: someone re-runs yesterday's load script by hand, every order now exists twice, and every revenue number silently doubles. Gate 2 plus this pattern is how you find out in minutes, not months.
Try it yourself - this week ◐ 20-30 min total
- Stage the last two source tables yourself:
stg_products(no casts needed) andstg_events(one date to type). Use Demo 1's script as the template. - Write the four quality gates for
stg_order_items. Careful: its natural key is order_id + product_id together, not one column - adjust gate 2. - Find one dataset at work that people query raw, and list the three staging fixes it needs most (a type, a rename, a gate). That list is a staging spec.
- Say the four promises of staging out loud from memory: typed, renamed, quality-gated, replayable. If one escapes you, reread Part 1.
- Bring your staged layer to b3 - the star schema builds directly on top of these exact tables.
Official sources covered
This session teaches the staging and quality material from the major curricula as one live build. Certificates, graded labs, and videos stay on the official platforms. This page covers:
Three questions before you go 🎯 ◐ 90 seconds
1 · Why build a staging layer instead of modeling straight on the raw source tables?
Staging is about trust, not speed. Types are fixed once, gates inspect every load, and the whole layer can be rebuilt from source by script - which is why the rule is "never model on raw".
2 · What does TRY_CAST do that CAST does not?
CAST is strict: one 'oops' in a million rows kills the load. TRY_CAST converts failures to NULL so the load lands - then your null-scan gate reports how many, and you decide what to do in daylight.
3 · Someone re-runs the nightly load script by hand and every order lands in staging twice. Which gate lights up?
A double load doubles the count, so gate 1 fails immediately - and gate 2 (key uniqueness) fails right behind it, since every order_id now appears twice. Freshness only checks the newest date, which looks fine.