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

Building dimensions

The star stands, but its arms were built in a hurry. Tonight you build dimensions the way a career warehouse engineer does: surrogate keys defended from first principles (not cargo-culted), a proper date dimension generated from a calendar series, and the conformed-dimension discipline that keeps every dashboard in the company agreeing with every other. Small tables, huge leverage - dimensions are where warehouse craft lives.

🟡 Builder track Practitioners: analysts · DE · DS · PMs Runs in your browser · DuckDB 45 min
0-3 · Recap 3-18 · Keys & the date dimension 18-42 · Build-along: dimensions 42-45 · Q&A
Part 0

Where the build stands

Daybreak now has staging (b2) and a first star (b3): fct_order_line ringed by dim_customer, dim_product, and a dim_date you used before meeting. Tonight the dimensions get their full treatment - because in practice, fact tables are almost boring (declare grain, load measures, done) while dimensions carry the judgment calls: what key, what calendar, what gets shared across the whole company. Get these right and b5's history-tracking and b7's second fact table land softly.

Live - presented in session Self-study - read after class ▶ Live warehouse - editable & runnable Official sources covered
★ What you walk out with today The three-count case for surrogate keys (they change, they collide, history needs room), a dim_date you generated yourself with generate_series and will reuse in every warehouse you ever build, and the conformed-dimension idea - the single discipline that makes company dashboards agree.
Part 1 · covers IBM M2 dimension design, DLAI C4 M1 keys

Surrogate keys: the case, properly made 7 min live

In b3 you stamped every dimension row with row_number() and took it on faith. Here is the argument. A natural key (customer_id, email, SKU) belongs to the source system - it lives by the source's rules. A surrogate key is a meaningless integer the warehouse itself assigns, and owning your keys buys you three escapes.

Natural-key world · keys belong to sources app: customer_id 7 shop: customer_id 7 collision! Same "7", two different people - the join happily merges them into one phantom. And when the app migrates and renumbers its IDs, every old fact row points nowhere. One key value can also only ever mean ONE row - no room for "Ava before the move" and "Ava after the move" side by side. Surrogate-key world · warehouse owns the keys key 3 · app 7 · Ava Chen · Seattle key 9 · shop 7 · Ben Ito · Osaka key 16 · app 7 · Ava Chen · Denver (b5) The dimension keeps BOTH source IDs as plain attributes - collisions become two honest rows. Sources can renumber freely: only the staging lookup changes, the fact table never moves. And row 16 is the preview: TWO versions of Ava can coexist - exactly what b5's history needs. Three escapes: keys that change, keys that collide, history that needs room. That is the whole case.
🔍 Click to zoom - natural keys obey the source's rules; surrogate keys obey yours
LiveThe row_number() pattern and its production cousins4 min

This track mints keys with row_number() OVER (ORDER BY natural_key) - perfect for a full rebuild, because the whole dimension is created in one statement and numbering is deterministic. Production warehouses that load incrementally use the same idea with different machinery:

  • Sequences / IDENTITY columns: the database hands out the next integer as each new row arrives - the incremental-load version of row_number(). DuckDB has CREATE SEQUENCE; Snowflake and friends have AUTOINCREMENT.
  • Hash keys: some teams hash the natural key instead, so the key is computable without a lookup. Trade-offs galore; know the pattern exists.
  • The invariant that matters: whatever mints it, the surrogate key is meaningless. Nobody parses it, nobody predicts it, nothing breaks when the business changes.
Keep the natural key on the row. The surrogate key is for joins; the natural key (customer_id) stays on the dimension as a regular column - it is how staging finds the right dimension row during loads (b6), and how humans trace a row back to the source.
Self-studyWhen natural keys are fine, honestly2 min read

Craft means knowing the exceptions. dim_date's key is the date itself - natural, and correct, because a calendar date never changes meaning, never collides, and needs no versions. A tiny static lookup (country codes) can also skip the ceremony. The rule of thumb: surrogate keys for dimensions that describe things that change (customers, products, employees); natural keys where the domain itself is immutable. Decide per dimension, out loud.

Part 2 · covers IBM M2 dimension build lab, DLAI C4 M1

dim_date: the dimension every warehouse shares 6 min live

Every warehouse on earth has a date dimension, and they all look alike: one row per calendar day, with the calendar's facts precomputed - year, month, weekday, weekend flag, fiscal periods later. You do not load it from a source; you generate it, one row per day, straight from a series.

LiveGenerate the calendar with generate_series4 min

generate_series(start, end, INTERVAL 1 DAY) emits every date in the range; the SELECT around it decorates each day with attributes. This is the exact table the star setup has been handing you:

CREATE TABLE dim_date AS
SELECT CAST(gs AS DATE)               AS date_key,
       EXTRACT(year FROM gs)          AS year,
       EXTRACT(month FROM gs)         AS month,
       strftime(gs, '%b')             AS month_name,
       EXTRACT(dow FROM gs)           AS day_of_week,
       EXTRACT(dow FROM gs) IN (0, 6) AS is_weekend
FROM generate_series(DATE '2025-11-01', DATE '2026-06-30',
                     INTERVAL 1 DAY) t(gs);

SELECT * FROM dim_date
WHERE is_weekend
LIMIT 6;

What belongs on it: anything the business says about a day. Year, quarter, month, weekday, weekend - tonight. Fiscal periods, holidays, promo windows - the moment the business defines them, they get a column here and only here.

LiveWhy not just date functions everywhere?2 min

You could write strftime(order_date, '%b') in every query forever. Three reasons the join beats the function:

  • Consistency: one analyst's month starts Monday, another's week is ISO, a third forgets the year boundary. A shared dim_date means one definition, everywhere, forever.
  • Business logic lives once: "fiscal year starts in February" is a column update on one table - not a hunt through 200 dashboard queries.
  • Joins beat functions for pruning: engines can skip whole chunks of a fact table when you filter on a joined date attribute - but a function wrapped around a column often defeats that skipping. b8 measures this for real.
Part 3 · covers DLAI Kimball bus, 365DS design

Conformed dimensions: why dashboards agree 5 min live

Daybreak has one fact table tonight. By b7 it will have two (orders, subscriptions), and someday a third (support events). The Kimball promise from b3 - stars that agree - is kept by one discipline: every fact joins the SAME dimension tables. One dim_customer, shared. That shared dimension is called conformed, and it is the bus the whole warehouse rides.

ONE dim_customer · conformed customer_key · city · plan one definition of "customer" fct_order_line · b3 revenue by city, plan ... fct_subscription · b7 churn by city, plan ... fct_support · someday tickets by city, plan ... "Seattle" means the same customers in every chart, because every fact joined the same arm. Two teams with two private customer tables = two revenue numbers in the same board meeting.
🔍 Click to zoom - one shared dimension, many facts: the reason company dashboards can agree
LiveWhat conformance costs, and who pays it3 min

Conformed dimensions are free tonight (one team, one browser tab). At company scale they cost governance: someone must own dim_customer, arbitrate what "active customer" means, and stop the marketing team from quietly forking their own copy with a friendlier definition. That ownership conversation is the leader track's a3 - this session is its boardroom twin made concrete in SQL.

Real world

The fork always starts innocently. A team copies dim_customer to add one column, the copy drifts, and eight months later two VPs present different customer counts. The cure is boring and works: additions go into the ONE shared dimension, reviewed, or they do not ship.

Demo 1 of 2

Build dim_date + dim_customer, then the payoff query ★ 12 min · everyone builds

One script, three moves: generate the calendar, mint the customer dimension, then answer a question that is miserable without them - weekend vs weekday revenue, by plan. Starts from b2's staging layer (data-setup="staging").

Move 1 - dim_date: generate_series plus decorations. Read each attribute line: this is the calendar's knowledge, computed once.

Move 2 - dim_customer: the b3 pattern - row_number() surrogate key, natural key kept aboard as a column.

Move 3 - the payoff: the final query never calls a date function - it just joins the calendar and reads is_weekend like any other column.

Bend it: group by day_of_week instead of the weekend flag, or by city instead of plan. Each variant is a one-line change - that ease is the whole point of dimensions.

CREATE TABLE dim_date AS
SELECT CAST(gs AS DATE)               AS date_key,
       EXTRACT(year FROM gs)          AS year,
       EXTRACT(month FROM gs)         AS month,
       strftime(gs, '%b')             AS month_name,
       EXTRACT(dow FROM gs)           AS day_of_week,
       EXTRACT(dow FROM gs) IN (0, 6) AS is_weekend
FROM generate_series(DATE '2025-11-01', DATE '2026-06-30',
                     INTERVAL 1 DAY) t(gs);

CREATE TABLE dim_customer AS
SELECT row_number() OVER (ORDER BY customer_id) AS customer_key,
       customer_id, name, city, country, plan, signup_date
FROM stg_customers;

SELECT c.plan,
       CASE WHEN d.is_weekend THEN 'weekend'
            ELSE 'weekday' END AS day_type,
       ROUND(SUM(oi.line_amount), 2) AS revenue
FROM stg_orders o
JOIN stg_order_items oi USING (order_id)
JOIN dim_date d     ON o.order_date  = d.date_key
JOIN dim_customer c ON o.customer_id = c.customer_id
WHERE o.status = 'completed'
GROUP BY c.plan, day_type
ORDER BY c.plan, day_type;
Real world

This dim_date outlives the project. Warehouse engineers carry their date-dimension script from job to job like a chef's knife. Extend the range to ten years, add fiscal columns when finance defines them, and never write it from scratch again.

Demo 2 of 2

Your turn: extend the dimensions ★ 10 min · build your own

Q1 grows the calendar, Q2 grows the product dimension, Q3 is the conformed-dimension thought exercise - no SQL, just the scar tissue of everyone who skipped it.

Q1 and Q2 are live - attempt each before reading the starter SQL. Both start from staging, so each script builds its own dimension.

Q3 is prose: read the scenario, answer out loud, then open the card for the debrief.

LiveQ1 · Add a quarter column to dim_date4 min

Finance thinks in quarters, so the calendar should too. Derive it from the month with a CASE (strftime has no quarter code, and writing the logic yourself means you can move the fiscal year-end later):

CREATE TABLE dim_date AS
SELECT CAST(gs AS DATE)               AS date_key,
       EXTRACT(year FROM gs)          AS year,
       EXTRACT(month FROM gs)         AS month,
       strftime(gs, '%b')             AS month_name,
       CASE WHEN EXTRACT(month FROM gs) <= 3 THEN 'Q1'
            WHEN EXTRACT(month FROM gs) <= 6 THEN 'Q2'
            WHEN EXTRACT(month FROM gs) <= 9 THEN 'Q3'
            ELSE 'Q4' END             AS quarter,
       EXTRACT(dow FROM gs) IN (0, 6) AS is_weekend
FROM generate_series(DATE '2025-11-01', DATE '2026-06-30',
                     INTERVAL 1 DAY) t(gs);

SELECT quarter, count(*) AS days,
       min(date_key) AS first_day,
       max(date_key) AS last_day
FROM dim_date
GROUP BY quarter
ORDER BY first_day;
LiveQ2 · Build dim_product with a price_band attribute3 min

Dimensions earn their keep by carrying derived attributes the business thinks in. Nobody asks "revenue where price is 16.00" - they ask "how do budget items do vs premium". Band the price once, here:

CREATE TABLE dim_product AS
SELECT row_number() OVER (ORDER BY product_id) AS product_key,
       product_id, name, category, price, roast,
       CASE WHEN price < 15 THEN 'under 15'
            WHEN price < 25 THEN '15 to 24'
            ELSE '25 and up' END AS price_band
FROM stg_products;

SELECT price_band, count(*) AS products,
       ROUND(AVG(price), 2) AS avg_price
FROM dim_product
GROUP BY price_band
ORDER BY avg_price;
Self-studyQ3 · Two sources both say "customer_id 7". Now what?3 min

The scenario: Daybreak acquires a wholesale arm with its own ordering system. Its database also numbers customers from 1 - so both sources contain a customer_id 7, and they are different people. Marketing wants one warehouse, one dim_customer, one set of dashboards. What breaks if you join on customer_id, and what does the conformed dimension have to do instead?

The debrief: joining on the natural key silently merges the two number-sevens into one phantom customer - their orders sum together, their cities fight, and no error is ever thrown. The conformed dim_customer fixes it structurally: each person gets their own surrogate key, and the dimension carries two lineage columns (source_system, source_customer_id) instead of pretending one ID rules both worlds. Staging maps each source's IDs to the shared dimension during loads (the b6 lookup step). This is Part 1's collision escape and Part 3's conformance discipline meeting in one table - and it is why acquisitions are where warehouse teams earn their salary.

Homework

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

Source material

Official sources covered

This session teaches the dimension-building 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: dimension design & build labsParts 1-2 + Demo 1 · surrogate keys and the date dimension, built live
DeepLearning.AI Data Engineering C4 - Module 1: keys, date dimension, Kimball busParts 1 + 3 · the conformed-dimension discipline behind agreeing stars
365DS Intro to Data Warehousing - schema designParts 2-3 · advanced schema variants (snowflake, galaxy) land in b9
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · The case for surrogate keys over natural keys, in one breath:

The three escapes: keys that change (source migrations), keys that collide (two sources both counting from 1), and history that needs room (b5 stores multiple versions of one customer - impossible if the natural key is the only identity).

2 · What belongs in dim_date?

The calendar's knowledge lives on the calendar - once, consistently, prunably. Revenue is a measure and stays on the fact; per-query date functions are exactly what dim_date exists to retire.

3 · The payoff of a CONFORMED dim_customer, once Daybreak has orders, subscriptions, and support facts:

Conformance means one shared definition of each noun. Private copies are the anti-pattern: they drift, and eight months later two executives present two different customer counts.

Builder session 4 cheat sheet · pin this

Surrogate keyMeaningless warehouse-owned integer per dimension row. Three escapes: source keys change, collide, and cannot hold versions.
Natural keyThe source's ID (customer_id). Keep it ON the dimension as a column - loads and humans trace by it.
Minting patternrow_number() OVER (ORDER BY natural_key) for full rebuilds; sequences / IDENTITY for incremental loads (b6).
dim_dateGenerated, not loaded: generate_series(start, end, INTERVAL 1 DAY) + decorations. One row per day.
Calendar logic lives onceWeekend flags, quarters, fiscal periods: columns on dim_date, never per-query functions. Joins also prune better (b8).
Conformed dimensionONE dim_customer joined by every fact. Dashboards agree by construction; forks drift into boardroom fights.
Derived attributesprice_band, day_type: encode how the business talks, once, on the dimension - not in 200 dashboards.
Next sessionb5: slowly changing dimensions - the surrogate keys earn their keep when Ava moves to Denver.