Business Intelligence with Power BI

L04 · The Data Model

The Data Model

Learning objectives. By the end of this lesson you will be able to:
  • Distinguish fact tables from dimension tables using the concept of grain
  • Explain why a star schema beats one big flat table the moment a second fact arrives
  • Create relationships in Model view and read their cardinality and filter direction
  • Test whether a column can serve as a key — and predict what happens when it cannot
  • Recognize the fact-to-fact trap and route around it through shared dimensions

Estimated time: 60–75 minutes of reading and follow-along, before practice.

Why this matters

Module 1 ended with you combining files like a professional. Now the business asks a question that ends the flat-file era for good: "What is our return rate by category?"

Think about what answering it requires. Sales live in one table — 165,042 rows, one per product per order, covering three and a half years. Returns live in another — 6,599 rows, one per returned item. You cannot append them (different shapes, different meanings). You cannot merge returns into sales row-by-row without double-counting or losing partial returns. And you certainly cannot pre-flatten everything into one giant table, because a row of that table would have to be simultaneously a sale and a return and, once finance's targets arrive, a monthly target — three different kinds of fact that happen at three different resolutions.

The answer to this problem was worked out decades before Power BI existed, and Power BI is built around it: keep the tables separate, relate them, and let the model do the joining at query time. This lesson is where your pile of clean tables becomes a model — and Model view, nearly empty since Lesson 1, becomes the most important screen in the application.

Grain: the one question that classifies every table

Ask of any table: "what does one row mean?" That answer is the table's grain, and it splits every table you will ever meet into two families:

FamilyOne row means…Lantern & Pine tablesTypical size
FactSomething happened — an event, at a moment, with numbers attachedsales (165,042) · returns (6,599) · targets (1,080) · inventory snapshots (5,700)Long, and growing daily
DimensionSomething exists — a thing you describe events withproducts (240) · stores (39) · customers (8,000) · employees (165)Short, and mostly stable

Facts are verbs; dimensions are nouns. A sale happened; a product exists. Notice the sizes: facts are three to four orders of magnitude longer than dimensions here, and that ratio is typical. Notice also that Lantern & Pine has four fact tables at four different grains — an order line, a returned item, a monthly target, a monthly stock snapshot. That is precisely why no single flat table could ever hold this business.

The star schema

Arrange the model with facts in the middle and dimensions around them, one relationship from each fact to each dimension it mentions, and the diagram draws itself into a star:

[Screenshot l04-star-model — Model view: sales and returns facts related to products, stores, customers; layout arranged as a star, cardinality markers visible]
Model view, arranged deliberately: facts center, dimensions around them. The 1 and * markers on each line are the next section.

The star's payoff is that dimensions are shared. The products table describes sales rows and returns rows. So "return rate by category" — the question that broke the flat file — becomes almost embarrassingly simple: Category (from products) filters both facts at once, units returned divides by units sold, done. Across the whole dataset that is 8,334 returned units against 259,421 sold — a 3.2% return rate — and the same calculation per-category lands between 3.15% and 3.30%. One dimension, two facts, one honest answer.

Relationships: cardinality is a fact, not a setting

Drag ProductID from sales onto ProductID in products and Power BI creates a relationship marked many-to-one (*:1): many sales rows point at one product row. But here is the part to internalize: Power BI did not decide that — the data did. A relationship's cardinality follows from whether each side's column is unique:

ColumnRowsDistinctVerdict
products.ProductID240240Unique — can be the "one" side
stores.StoreID3939Unique — "one" side
customers.CustomerID8,0008,000Unique — "one" side
sales.OrderID165,04285,538Not unique — one order holds many lines
returns.OrderID6,5996,388Not unique — one order can have several returns

This is Lesson 3's key-profiling habit graduating into a law: the "one" side of a relationship must be a unique column. Check it in ten seconds — rows versus distinct — before creating any relationship, and you will never be surprised by what the dialog offers.

About auto-detect Power BI offers to detect relationships automatically, and on this tidy universe it would guess well — the key columns share names and the dimensions are clean. Real models are not this polite: auto-detect happily relates any same-named columns, including two unrelated "Name" columns or a Region text column to a Region lookup you meant to delete. Treat its suggestions as suggestions. You read the cardinality; you confirm the key; you approve the line. A model whose relationships you cannot explain is a model you cannot defend.

Filter direction: how a click becomes a calculation

The arrows on each relationship line show filter direction, and they answer a question you have been taking on faith since Lesson 1: how does clicking "Lighting" in a slicer change a revenue card? Like this: the filter lands on the products dimension (40 lighting products of the 240), flows down the relationship from the one side to the many side, and selects exactly the sales rows pointing at those products — 38,369 of the 165,042 lines. Every measure then computes over the surviving rows: Lighting's $8.47M of the company's $66.81M total.

Dimensions filter facts. That is the default, single direction, and it is the right direction so consistently that this course will not turn on bidirectional filtering even once — when you meet a problem that seems to need it, the model usually needs a shared dimension instead.

The trap: relating facts to facts

Now the mistake this dataset was built to let you make honestly. It seems reasonable to relate returns to sales directly — they share an OrderID column, and returns "belong to" sales. Try it, and the dialog announces a many-to-many relationship, because — as the table above showed — OrderID is unique in neither table. Power BI will let you proceed. Do not. A many-to-many between two facts means one return row matches every line of its order, and totals quietly multiply — the join that grows, from Lesson 3, now living permanently inside your model.

The star answer: facts never relate to facts. They meet in the dimensions they share. Returns already relates to products (its ProductID is many-to-one, cleanly) and through it shares Category with sales. Every question that made you want the fact-to-fact line — return rate by category, by store, by month — routes through a shared dimension instead, without a single multiplied row.

[Screenshot l04-m2m-warning — the relationship dialog on returns.OrderID × sales.OrderID showing the many-to-many cardinality warning]
The dialog will warn you and then let you do it anyway. The warning is the lesson.

What the model buys you: one number, honestly

A model earns its keep the first time one filter answers across tables. With the star built, put Region on a slicer and revenue on a card, click Midwest, and the model walks stores → sales and reports $2.76M for 2025 — the same verified figure your L01 flat extract produced, now computed live across related tables instead of pre-baked into an export. Same answer, but this version also knows the Midwest's returns, its targets, and its inventory — because the model relates, where the extract froze.

Knowledge check. One row of the inventory table means "this store held this many units of this category at the start of this month." Fact or dimension, and why?
Knowledge check. A colleague wants to relate sales.OrderID to returns.OrderID "so the tables connect." The key profiles read: sales 165,042 rows / 85,538 distinct; returns 6,599 / 6,388. What will Power BI create, and what should your colleague do instead?
Knowledge check. With the star built, a reader clicks "Lighting" in a category slicer. Trace what happens to a revenue card.

Common mistakes

  • Building one giant flat table because "it worked in Lesson 1." It worked because there was one fact at one grain. The moment returns or targets arrive, flat means double-counted or unanswerable.
  • Accepting auto-detected relationships unread. Same-named columns are a hint, not a truth. Read every line's cardinality; delete what you cannot explain.
  • Relating two facts because they share a column. Shared columns make a many-to-many, not a model. Facts meet in dimensions.
  • Turning on bidirectional filtering to "fix" a filter that won't flow. The missing flow is almost always a missing shared dimension. Solve the shape, not the setting.
  • Skipping the key check. Rows versus distinct, ten seconds, before every relationship. 8,000 = 8,000 is a key; 165,042 vs 85,538 is a grain.

Summary

  • Grain classifies: facts record what happened (sales, 165,042 rows; returns, 6,599); dimensions record what exists (products, 240; customers, 8,000). Verbs and nouns.
  • The star shares dimensions across facts — which is why "return rate by category" (3.2% overall) is easy in a model and impossible in a flat file.
  • Cardinality is a fact about uniqueness, verified in ten seconds. The "one" side must be a key.
  • Filters flow dimension → fact: one click on Lighting selects 38,369 of 165,042 lines, and every measure recomputes.
  • Facts never relate to facts. The many-to-many warning is the model asking you to reconsider the shape.