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.
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.
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.
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.
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.
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.
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.
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.
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;
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.
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.
Try it yourself - this week ◐ 20-30 min total
- Add is_month_start and is_month_end flags to dim_date. Hint: EXTRACT(day FROM ...) is 1 on the first; compare against the month's max for the last.
- Rebuild the weekend-by-plan query from Demo 1, slicing by your Q2 price_band instead of plan. Two of tonight's dimensions in one query.
- Write the surrogate-key case from memory - the three escapes - as three bullet points. Then check them against Part 1's diagram.
- Find the "customer" definition at your company: how many tables claim to be it? If more than one, you have found an unconformed dimension in the wild.
- Rest up: b5 uses tonight's surrogate keys to do the trick they were built for - keeping history when a customer changes city or plan.
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:
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.