Why modeling is the session that matters
Stage 3 of the five-stage workflow: Model. Connect and prepare got the data in and clean; now we decide its shape. Get the shape right and every measure in b4 is a one-liner and every chart in b5-b6 is a drag-and-drop. Get it wrong and you fight double counting forever. The industry has converged on one answer - the star schema - and tonight Daybreak gets one.
Facts and dimensions 7 min live
Every business question decomposes the same way: a number you measure sliced by things you describe. The numbers live in a fact table - events that happened: an order line, with its quantity and price. The descriptions live in dimension tables - the who, what, where, and when you slice by: customer, product, date, channel. Put the fact in the middle, dimensions around it, connect with keys - that is the star.
LiveWhy not one big flat table4 min▶
The tempting alternative: join everything into one wide table and chart from that. It works for a week, then hurts four ways:
- Repeats: a customer's city is copied onto every order line they ever bought. Fix a typo in one place, it survives in a thousand others.
- No reuse: next month you build a subscriptions report. The flat table was built for orders - you start from zero. Dimensions built once get reused by every fact you add later.
- Measure errors: flat tables mix grains. Put monthly_qty from subscriptions next to order lines and SUM happily double counts. Stars force one grain per fact.
- Memory: BI engines compress repeated dimension values brilliantly when they live in their own narrow tables - a star is smaller and faster than the flat version of itself.
The 40-column export. Most "our dashboard is slow and the numbers are wrong" tickets trace back to one giant flat extract someone built in year one. The fix is never a faster refresh - it is remodeling into a star. Cheaper to do it on day one, which is today.
Self-studyGrain - the one-sentence contract3 min read▶
The grain is the answer to: what does one row of the fact table represent? For fact_sales the sentence is: "one row = one order line". Write that sentence before you write any SQL, and put it in the table's documentation forever.
- Misdeclared grain = double counting. If someone believes the grain is "one row = one order" and sums a column that repeats per line, every multi-item order counts twice or more. The single most common wrong-number bug in BI.
- One grain per fact. Orders at line grain and subscriptions at month grain do not belong in the same fact table. They become two facts sharing the same dimensions - still one star family, two centers.
- The grain picks your measures. At line grain, revenue is SUM(quantity * unit_price) and it is safe. Coarser grains lose detail you can never recover.
Relationships: cardinality and direction 7 min live
Lines between tables carry two settings every BI tool asks you for. Cardinality: one-to-many is the healthy default - one dimension row (1) matches many fact rows (*). Cross-filter direction: filters usually flow one way, dimension → fact; you click a city, the fact rows filter. Both-directions filtering exists, and it is a last resort - it invites ambiguity and slow models. When an interviewer asks about relationships, "1-to-many, single direction, dim filters fact" is the right reflex.
LiveThe date table - why BI tools want a dedicated one4 min▶
Dates hide inside every table, so why build a separate dim_date? Because a common date table gives you three things raw date columns never will:
- Continuous days: one row per calendar day, even days with zero orders. Without it, a month with no sales silently vanishes from your trend line instead of showing as zero.
- Ready-made attributes: year, month, quarter, weekday, is_weekend - computed once, reused by every chart, always spelled the same way.
- Time intelligence: year-to-date, same-month-last-year, rolling 3 months - the b4 measures - all require a proper date table underneath. No date table, no time intelligence.
SQLite has no calendar generator built in, but here is the seed of one - the distinct months Daybreak has traded. A real dim_date extends this to one row per day:
SELECT DISTINCT strftime('%Y-%m', order_date) AS month
FROM orders
ORDER BY 1;
Self-studyRole-playing dimensions2 min read▶
Daybreak has order dates (orders.order_date) and signup dates (customers.signup_date). Do you build two date tables? No - you build one dim_date and relate it twice, once per role. The same physical table "plays the role" of order date in one relationship and signup date in another. In Power BI the second relationship is created inactive and activated per measure; in Tableau you simply relate the field you need. One calendar, many roles - that is the whole trick, and it is a named PL-300 topic.
Build the star as views 4 min live
In Power BI or Tableau you would click this together in a model view. Here we make it real with SQL: a view is a saved query that behaves like a table - exactly what a modeled fact is underneath. fact_sales combines order_items (the measures) with orders (the keys and context), at one-row-per-order-line grain. Remember: every run button starts from a fresh database, so the CREATE VIEW and the SELECT live in the same box.
Livefact_sales, born4 min▶
Run it, then read the output columns against the star diagram above - they match one for one.
CREATE VIEW fact_sales AS
SELECT oi.order_id,
oi.product_id,
o.customer_id,
o.order_date,
o.status,
o.channel,
oi.quantity,
oi.unit_price,
oi.quantity * oi.unit_price AS line_revenue
FROM order_items oi
JOIN orders o ON o.order_id = oi.order_id;
SELECT * FROM fact_sales LIMIT 8;
Query the star vs the raw tables ★ 12 min · everyone builds
Same question - revenue by product category - answered twice. First the raw way: three tables, two joins, join logic on you every single time. Then the star way: the fact view plus one dimension. Same number, half the joins. That difference, multiplied by every chart on every dashboard, is why BI models are stars.
The raw way. Run the three-table version below. Count the joins and imagine typing them correctly in every report, forever.
The star way. Run the second box: create fact_sales, then answer the same question with one join to products. Compare the revenue numbers - identical.
Say it out loud. "The star pre-pays the join cost once, so every question afterwards is cheap." That sentence is the whole business case for modeling.
LiveRaw tables: three tables, two joins4 min▶
SELECT p.category,
ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue
FROM order_items oi
JOIN orders o ON o.order_id = oi.order_id
JOIN products p ON p.product_id = oi.product_id
GROUP BY 1
ORDER BY 2 DESC;
LiveThe star: fact + one dimension4 min▶
CREATE VIEW fact_sales AS
SELECT oi.order_id, oi.product_id, o.customer_id, o.order_date,
o.status, o.channel, oi.quantity, oi.unit_price,
oi.quantity * oi.unit_price AS line_revenue
FROM order_items oi
JOIN orders o ON o.order_id = oi.order_id;
SELECT p.category,
ROUND(SUM(f.line_revenue), 2) AS revenue
FROM fact_sales f
JOIN products p ON p.product_id = f.product_id
GROUP BY 1
ORDER BY 2 DESC;
Your turn ★ 10 min · build your own
Two exercises: one where you extend the star with a dimension we have not touched yet, and one where you catch the playground red-handed doing exactly what you just learned.
LiveQ1 · Revenue by plan, through the star4 min▶
customers is a dimension too. Join it to fact_sales and slice revenue by plan - the skeleton is ready, run it, then try swapping plan for city or country.
CREATE VIEW fact_sales AS
SELECT oi.order_id, oi.product_id, o.customer_id, o.order_date,
o.status, o.channel, oi.quantity, oi.unit_price,
oi.quantity * oi.unit_price AS line_revenue
FROM order_items oi
JOIN orders o ON o.order_id = oi.order_id;
SELECT c.plan,
ROUND(SUM(f.line_revenue), 2) AS revenue
FROM fact_sales f
JOIN customers c ON c.customer_id = f.customer_id
GROUP BY 1
ORDER BY 2 DESC;
LiveQ2 · Catch the playground using your star4 min▶
The mini-BI has been quietly running this same star logic since session b1. Build revenue by category below, press Show SQL, and read the joins - order_items to orders to products. That is your star, just written inline. A production semantic model does exactly this on your behalf, thousands of times a day.
Try it yourself - this week ◐ 20-30 min total
- Sketch a star for a domain you know well - support tickets, marketing campaigns, HR requests. Pencil and paper beats any tool: fact in the middle, dimensions around it.
- Declare its grain in exactly one sentence ("one row = one ticket status change"). If the sentence will not come out clean, the fact is not designed yet.
- Find two role-playing uses of a date dimension in your sketch (created date vs resolved date, launch date vs end date). Almost every domain has at least two.
- Re-run the fact_sales box and add a line: total revenue from the view vs total from the raw join. Prove to yourself they match.
- Bring to b4: one metric at your company that two teams calculate differently. Measures are next, and that story will be useful.
Official sources covered
Modeling is the heaviest-weighted domain on the PL-300 (25-30% of the exam) and the conceptual heart of the Tableau and Google curricula. This page covers:
Three questions before you go 🎯 ◐ 90 seconds
1 · What belongs in a fact table?
Facts hold the events you measure, at one declared grain. Descriptive attributes are dimensions (A), and the calendar is dim_date (C).
2 · The healthy default relationship between a dimension and a fact is...
One dimension row matches many fact rows, and filters flow dim → fact. Both-directions and many-to-many exist but are last resorts - ambiguity and slow models follow them around.
3 · A "role-playing dimension" means...
One table, multiple relationship roles. You build the calendar once and relate it per role - two copies (B) is exactly what role-playing saves you from.