Power BI Star Schema Tutorial: Fix Slow, Wrong Reports at the Model
Picture the file almost every new Power BI user builds first. One Excel export with forty columns: order ID, customer name, city, product, category, order date, quantity, amount. It loads in seconds, the first bar chart looks right, and everyone is pleased. Then someone asks for sales versus the same month last year, a measure returns a number that is clearly too high, and the afternoon disappears into DAX forums.
The formula is rarely the culprit. The shape of the data is.
Why One Wide Table Breaks Down
A flat table repeats everything. Every order row carries the customer's city and the product's category again, so the same text is stored thousands of times and any change has to be made in thousands of places. Counting customers means counting distinct values over a column that was never designed for it. Filtering by product category and by region together works only because both happen to live on the same rows.
Power BI is built for a different layout. The engine stores each column separately and compresses repeated values very well, but it works best when descriptive attributes sit in small lookup tables and the big table holds only keys and numbers. That layout is called a star schema.
Fact table
One row per event: an order line, a payment, a click
Dimension table
One row per thing: a customer, a product, a date
Keys
Facts store keys, dimensions describe them
Measures
Totals and ratios are calculated on the fact table
Build It in Five Moves
You do not need to start over. Take the wide table you already have and split it inside Power Query, before anything reaches the report.
- 1
Duplicate the query
Keep the original untouched as your source
- 2
Make dimensions
Keep only the customer columns, remove duplicates
- 3
Add a key
Use a stable ID, not a name that can change
- 4
Trim the fact table
Leave keys, dates and numeric columns only
- 5
Add a date table
One row per day, covering your full date range
Take the customer dimension as the example. Reference the original query, keep customer ID, name, city and segment, and remove duplicates on customer ID. Do the same for products. What is left in the main query is the fact table: order ID, customer ID, product ID, order date, quantity and amount. Each dimension now holds one row per customer or product, and each description is stored once.
A word on keys, because this is where people stumble. If you split customers using the customer name, two different people called the same thing collapse into one row, and one person who changed their spelling becomes two. Use the ID from the source system. If the export has no ID, build one in Power Query before you split anything, and check that it is unique with a quick count of rows against distinct values.
Wire the Relationships
Switch to Model view and connect each dimension to the fact table on its key. Every relationship should be one-to-many, with the dimension on the "one" side, and the filter should flow in a single direction, from dimension to fact. That is the default for good reason. When you select a region in a slicer, it narrows the customer table, which narrows the orders, which changes your totals.
Be suspicious of any urge to switch a relationship to both directions or to create many-to-many links just to make a visual behave. It usually means a dimension is missing or a key is not unique, and the fix is in the model. Bi-directional filters hide that problem for a week and then produce ambiguous results when a second dimension is added.
Hide the key columns on the fact table once the relationships exist. Report builders should reach for the customer name from the customer table, not a raw ID, and hiding the keys stops the wrong field from ending up on a chart.
The Date Table Is Not Optional
Time intelligence is where flat files fail most visibly. Functions such as same period last year need a continuous calendar with no gaps, and a date column inside a fact table has gaps on every day nothing sold. Create a proper date table, relate it to the order date, and use Mark as date table so DAX knows which column to trust.
Once that is in place, last year's figure is a one-line measure, and it stays right when someone filters by region or product. The course syllabus at SkyTrainings places Time Intelligence inside its DAX module for the same reason: the formulas only behave when the model underneath is sound.
Fact table
Customer
Product
Date
Measures
What Changes When You Get This Right
The visible difference is mundane. A new slicer takes ten seconds to add instead of an hour of debugging. A colleague who opens the file can read the model diagram and understand it without asking you. When the business adds a new product line, you add rows to one dimension and the report keeps working.
A Quick Health Check
Before you share the report, open the model and ask three things. Can you trace every dimension to the fact table in a straight line, like spokes to a hub? Does every relationship flow from the dimension to the fact? Do your measures live in one place, not scattered as calculated columns? If so, report pages tend to load faster and totals tend to agree with the source system, because Power BI is filtering small tables and aggregating one large one.
Row-level security is another reason to care. Restricting a manager to their own region is a filter on one dimension table, and the star shape carries that filter to every number on every page without extra DAX.
Next Step
If you want to practice this on real project files, the PowerBI Training course at SkyTrainings covers Power Query, data modeling and DAX in one two-month program.