Prepare Data · Topic 4 of 7
Star Schema, Denormalize, Aggregate and Join
Build a star schema in a Fabric lakehouse or warehouse: fact and dimension tables, surrogate keys, denormalizing, aggregating, joining and merging data.
- 5 min
- Includes a diagram
- Fact tables hold measures and foreign keys at one grain; dimensions hold attributes and a surrogate key.
- Denormalize snowflaked lookups into one dimension for simpler, faster models.
- SCD Type 1 overwrites; SCD Type 2 adds a new row and keeps history.
What it is
A star schema organizes analytical data into fact tables, which hold measurable events such as sales lines, and dimension tables, which describe them, such as product, customer and date. Each fact row points to its dimensions through keys.
Building one usually means denormalizing (flattening related lookup tables into one dimension), joining sources together and sometimes aggregating facts to a coarser grain. Semantic models, SQL queries and Direct Lake all work best on this shape.
How it works
Facts and dimensions

| Fact table | Dimension table | |
|---|---|---|
| Contains | Numeric measures and foreign keys | Descriptive attributes |
| Row count | Large, grows constantly | Smaller, changes slowly |
| Key | Foreign keys to each dimension | One surrogate key per row |
| Example columns | DateKey, ProductKey, Quantity, SalesAmount | ProductKey, ProductName, Category |
Decide the grain first: what one fact row means, such as "one order line". Every measure in the table must be true at that grain.
A surrogate key is an integer created in the warehouse, separate from the source system's business key. It lets you combine sources and keep history. With slowly changing dimension Type 1 you overwrite changed attributes; with Type 2 you add a new row with its own surrogate key and validity dates, so old facts keep the old attribute values.
Denormalizing a dimension
Source systems often split product, subcategory and category into three tables. A star schema flattens them into one dimension with a CREATE TABLE AS SELECT (CTAS):
CREATE TABLE dbo.DimProduct
AS
SELECT
ROW_NUMBER() OVER (ORDER BY p.ProductID) AS ProductKey,
p.ProductID,
p.ProductName,
sc.SubcategoryName,
c.CategoryName
FROM stg.Product AS p
LEFT JOIN stg.Subcategory AS sc
ON sc.SubcategoryID = p.SubcategoryID
LEFT JOIN stg.Category AS c
ON c.CategoryID = sc.CategoryID;
LEFT JOIN keeps products that have no subcategory. An inner join would silently drop them.
Building the fact table
CREATE TABLE dbo.FactSales
AS
SELECT
d.DateKey,
COALESCE(p.ProductKey, -1) AS ProductKey,
s.OrderID,
s.Quantity,
s.Quantity * s.UnitPrice AS SalesAmount
FROM stg.Sales AS s
JOIN dbo.DimDate AS d
ON d.[Date] = s.OrderDate
LEFT JOIN dbo.DimProduct AS p
ON p.ProductID = s.ProductID;
A -1 key points to an "Unknown" row that you add to the dimension. Facts with a missing product then stay in totals instead of disappearing.
Join types
| Join (SQL) | Power Query merge kind | Keeps |
|---|---|---|
INNER JOIN | Inner | Only rows that match in both tables |
LEFT JOIN | Left outer | All left rows, matched right rows or nulls |
RIGHT JOIN | Right outer | All right rows |
FULL OUTER JOIN | Full outer | All rows from both |
LEFT JOIN ... WHERE right.key IS NULL | Left anti | Left rows with no match, such as orphan facts |
In Power Query, Merge queries joins tables side by side, and Append queries stacks tables with the same columns, like UNION ALL. In PySpark, df.join(other, "key", "left_anti") returns unmatched rows.
Aggregating
An aggregate table stores facts at a coarser grain, such as one row per day and product, so large reports read fewer rows.
from pyspark.sql import functions as F
daily = (spark.table("fact_sales")
.groupBy("DateKey", "ProductKey")
.agg(F.sum("SalesAmount").alias("TotalSales"),
F.countDistinct("OrderID").alias("Orders")))
daily.write.format("delta").mode("overwrite").saveAsTable("agg_daily_sales")
The T-SQL equivalent uses GROUP BY, and Power Query has Group by on the Transform tab.
A worked example
An online shop's source has Orders, OrderLines, Products, Subcategories and Categories.
- Grain: one row per order line.
- Flatten products, subcategories and categories into
DimProductwith a surrogate key and an Unknown row. - Join order lines to orders to bring the order date onto each line, then look up
DateKeyandProductKeyto buildFactSales. - Use a left anti join to list order lines whose product is missing from the product table, and report them to the source owner.
- Build
agg_daily_salesfor an executive dashboard that only shows daily totals.
In a star schema, dimensions connect only to facts, never to each other. If a question shows a dimension joined to another dimension, it is a snowflake, and flattening it is the usual fix.
How the exam asks about this
- "Reduce the number of tables and joins for report authors." Denormalize the snowflaked dimensions into one table.
- "Find fact rows that have no matching customer." A left anti join.
- "Keep the history of customer address changes." A Type 2 slowly changing dimension with surrogate keys.
What to remember for the exam
- Fact tables hold measures and foreign keys at one grain; dimensions hold attributes and a surrogate key.
- Denormalize snowflaked lookups into one dimension for simpler, faster models.
- SCD Type 1 overwrites; SCD Type 2 adds a new row and keeps history.
- Left outer keeps all left rows; left anti returns only unmatched left rows; append stacks rows.
- Aggregate tables pre-summarize facts at a coarser grain for faster reports.
An inner join between facts and a dimension drops every fact whose key is missing, and totals become too low without any error. Use a left join with an Unknown member.
Common mistakes
Mixing grains in one fact table. Order header totals and order line amounts in the same table double count. Keep one grain per fact table.
Using business keys from several sources as the join key. Two systems can reuse the same code. A surrogate key per dimension row avoids collisions and supports history.
Choosing merge when the tables should be stacked. Merge adds columns from a matching table; append adds rows from a table with the same columns, such as monthly files.
Check yourself
What is the grain of a fact table?
The meaning of one row, such as one order line or one product per day. All measures must be valid at that grain.
Which join returns the rows in table A that have no match in table B?
A left anti join, or a left join that keeps only rows where B's key is null.
Why give a dimension a surrogate key instead of using the source key?
It is independent of any source system, so it avoids collisions between sources and lets Type 2 history keep several rows for one business key.
Topic 4 of 7 in Prepare Data
Test yourself on Prepare Data
The full Microsoft DP-600 question bank has 648 questions, with timed practice exams and an explanation for every answer.
This study guide is independent and is not affiliated with, endorsed by, or sponsored by Microsoft. Vendor names, certification names and exam codes are trademarks of their respective owners and are used for identification only.