learn-data-warehouse-with-phoebe / Builder session 2 of 10
Learn Data Warehouse with Phoebe · Builder track · Session 2 of 10

Architecture and the staging layer

Session b1 proved WHY the warehouse exists. Tonight you pour its foundation. Every warehouse worth trusting is built in layers - staging, core, marts - and the first layer is the humblest and the most important: a typed, renamed, quality-gated, replayable copy of the source. By the end of this session Daybreak has a real staging layer, built live in your browser, with four quality gates standing guard at the door.

🟡 Builder track Practitioners: analysts · DE · DS · PMs Runs in your browser · DuckDB 45 min
0-3 · Recap 3-20 · Three layers & staging 20-42 · Build-along: quality gates 42-45 · Q&A
Part 0

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.

Live - presented in session Self-study - read after class ▶ Live warehouse - editable & runnable Official sources covered
★ What you walk out with today The three-layer mental map every warehouse team shares (staging, core, marts), a full staging layer for Daybreak built with CREATE TABLE AS + CAST, and four copy-paste quality gates - row counts, key uniqueness, null scan, freshness - that catch bad loads before they poison a dashboard.
Part 1 · covers IBM DW Fundamentals M2 architectures & staging areas, 365DS design

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.

Source · app DB raw OLTP tables orders order_items customers ... dates as TEXT, app names, no gates 1 · Staging typed copies stg_orders stg_order_items stg_customers ... CAST + rename + quality gates · TONIGHT 2 · Core star schema fct_order_line dim_customer dim_date ... facts + dimensions, history · b3-b5 3 · Marts team slices finance mart marketing mart ops mart views + rollups per team · b7 Each layer only reads from the layer before it. Dashboards never touch staging; models never touch raw. Break the load? Re-run staging from source - the copy is replayable, nothing downstream is hand-patched. Tonight you build layer 1 - the dock where every load is inspected before it enters the building.
🔍 Click to zoom - Daybreak's assembly line: raw source, staging, core, marts
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.
The rule to tattoo somewhere: never model on raw. The moment a dashboard or a star schema reads the source tables directly, every source quirk - a renamed column, a text date, a test row - becomes a production incident downstream.
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 staging schema (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.
Part 2 · covers IBM M2 staging lab

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: orders becomes stg_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;
Real world

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.

Part 3 · covers IBM "verify data quality" lab, 365DS quality & governance

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.

Tonight's load orders · 33 rows raw, straight from the app database The four gates 1 · row counts match the source (33 = 33) 2 · primary key unique (no duplicate order_id) 3 · no NULLs in critical columns (dates, keys) 4 · fresh (newest row is recent enough) Accepted stg_orders typed · trusted · gated the only version the core is allowed to read A failed gate stops the load and pages a human. Ten minutes of inspection beats ten days of wrong numbers. These four checks are the seed of a data contract - b2's Demo 2 makes you write one.
🔍 Click to zoom - four cheap checks between the source and everything you will ever trust
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.

Demo 1 of 2

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

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.

Demo 2 of 2

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.

Homework

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

Source material

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:

IBM Data Warehouse Fundamentals (Coursera) - Module 2: architectures & staging areasParts 1-2 + Demo 1 · lab twin: their staging exercise, on Daybreak
IBM Data Warehouse Fundamentals - Module 2: verify data qualityPart 3 + Demo 2 · the four gates as one runnable script
365DS Intro to Data Warehousing - Session 4: design, quality & governanceParts 1 + 3 · layered architecture and quality thinking
DeepLearning.AI Data Engineering C4 - transformation & quality framingParts 2-3 · contract thinking; full modeling depth lands in b3-b5
Check yourself

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.

Builder session 2 cheat sheet · pin this

Three layersStaging (typed copies) → core (star schema) → marts (team slices). Each layer reads only the one before it.
Never model on rawDashboards and models read staged tables, never source. Source quirks stop at the dock.
Staging's four promisesTyped, renamed, quality-gated, replayable. If a layer breaks one, it is not staging.
The build moveCREATE TABLE stg_x AS SELECT ... CAST(...) FROM x. One source table, one stg_ table.
CAST vs TRY_CASTCAST fails the load on bad values; TRY_CAST turns them into NULL and lets the null scan count them.
Four quality gatesRow counts reconcile · keys unique · no NULLs in critical columns · data is fresh. Run after every load.
Duplicate patternGROUP BY key HAVING count(*) > 1 - empty result means clean. Catches double loads in seconds.
Next sessionb3 models these staged tables into a star schema - grain first, then facts and dimensions.