Power BI Course
Measures vs Calculated Columns
You know the conceptual difference from Lesson 31 — measures respond to filter context, calculated columns are fixed per row. This lesson goes deeper: the performance and storage consequences of choosing one over the other, the specific scenarios where the choice is forced, the patterns that look like they need a calculated column but actually need a measure, and how to diagnose which one is causing a problem when a report behaves unexpectedly.
What Actually Happens at Refresh vs Query Time
The timing difference between measures and calculated columns is not just conceptual — it has real consequences for file size, refresh speed, and report interactivity. Understanding the lifecycle of each type explains why the choice matters beyond "which feels more natural."
Storage and Performance Implications
| Scenario | Calculated Column impact | Measure impact |
|---|---|---|
| 10M row fact table, 5 new columns | +50M stored values — significant model size increase, longer refresh, more RAM required | Zero bytes added — 5 measures add nothing to model size |
| Column used in a slicer | Works — calculated columns appear in the slicer field well and filter correctly | Cannot — measures cannot be placed in a slicer or used as a row/column grouping axis |
| Value used in a relationship key | Works — a calculated column can be the key column in a relationship | Cannot — relationships require stored column values, not on-demand computations |
| Aggregation that responds to filters | Cannot — a column's value is fixed; it cannot aggregate differently based on a slicer | Only measures do this — their whole purpose is filter-context-aware aggregation |
| VertiPaq compression | Low-cardinality columns compress very well — a Status column with 5 distinct values is tiny. High-cardinality columns (like a hash or GUID) compress poorly and bloat the model. | No storage at all — compression is irrelevant for measures |
The Complete Decision Framework
Work through these questions in order. The first yes determines the answer.
RELATED() — Pulling Lookup Values into Row Context
RELATED() is a calculated column function — it uses the active row context to traverse a relationship and retrieve a value from the related table. It is the DAX equivalent of VLOOKUP, except it uses the model relationship instead of a key column argument. RELATED() only works in calculated columns (and iterator functions) because it requires row context.
// Model: Orders → (Many-to-One) → Customers
// Orders[CustomerID] relates to Customers[CustomerID]
// Add CustomerName to the Orders table via the relationship
// New calculated column in the Orders table:
CustomerName = RELATED(Customers[CustomerName])
// For each row in Orders, RELATED() traverses the relationship
// to find the matching Customers row and returns its CustomerName
CustomerCity = RELATED(Customers[City])
CustomerTier = RELATED(Customers[Tier])
// RELATED only works with Many-to-One direction (from the many side)
// Orders is the "many" side → RELATED() goes to the "one" side (Customers) ✓
// Customers is the "one" side → RELATED() going to "many" would return
// a table, not a scalar — use RELATEDTABLE() instead for that direction
// RELATEDTABLE — from the one side to many
// Calculated column in Customers table:
OrderCount = COUNTROWS(RELATEDTABLE(Orders))
// Returns the number of Orders rows that relate to each Customer row
// Common RELATED pattern: calculate margin using cost from a Products table
// Orders calculated column:
Margin = Orders[Revenue] - RELATED(Products[StandardCost])
| OrderID | CustomerID | Revenue | CustomerName (RELATED) | CustomerTier (RELATED) |
|---|---|---|---|---|
| 1001 | C101 | 1,200 | Alice Brown | Premium |
| 1002 | C102 | 850 | Bob Singh | Standard |
| 1003 | C103 | 430 | Carol Lee | Premium |
| 1004 | C999 | 95 | BLANK | BLANK |
Patterns That Look Like Calculated Columns But Should Be Measures
Three common DAX requirements fool beginners into creating calculated columns when measures are the correct choice. The tell-tale sign is always the same: the value needs to change based on what the user has filtered.
When a Calculated Column Is Genuinely the Right Choice
// These five patterns are legitimate calculated columns
// 1. Category label for slicing — fixed per row, used in a slicer
-- In Orders table:
RevenueTier =
SWITCH(
TRUE(),
[Revenue] >= 1500, "Platinum",
[Revenue] >= 1000, "Gold",
[Revenue] >= 500, "Silver",
"Bronze"
)
// Used in: slicer, chart axis, filter panel
// 2. RELATED lookup — brings dimension value into fact table row
-- In Orders table:
CustomerCity = RELATED(Customers[City])
CustomerTier = RELATED(Customers[Tier])
ProductCategory = RELATED(Products[Category])
// Needed when: you want to slice Orders by a Customers attribute
// but do not want to add a relationship to a separate table
// 3. Composite key — combining two columns as a relationship key
-- In Orders table (when no single unique key exists):
OrderKey = Orders[CustomerID] & "-" & FORMAT(Orders[OrderDate], "YYYYMMDD")
// 4. Date component for axis sorting
-- In Calendar table:
YearMonthKey = [Year] * 100 + [MonthNumber] -- sorts correctly as integer
// Used in: sort-by-column setting to sort month names chronologically
// 5. Flag column for filtering — used as a filter, not aggregated
-- In Orders table:
IsLateFlag =
IF(
ISBLANK([OrderDate]) || ISBLANK([DueDate]),
0,
IF([OrderDate] > [DueDate], 1, 0)
)
// Used in: filter panel or as a slicer ("Show late orders only")
Teacher's Note: The "% of total" mistake is the most common calculated column error in Power BI, and it is invisible until you filter. The column looks correct in a table visual showing all data — every percentage is calculated correctly and they add up to 100%. The moment a slicer is applied, the denominator does not change but the numerators do, and suddenly the percentages for the filtered set add up to some arbitrary number below 100%. Always build percentage-of-total as a measure with ALLSELECTED. Test it immediately by applying a slicer and confirming the percentages still sum to 100%.
Practice
Practice 1 of 3
To retrieve the value of a column from a related table for each row in the current table — for example, bringing CustomerCity from the Customers table into the Orders table — you use the DAX function ___ inside a calculated column.
Practice 2 of 3
A Revenue % of Total calculated column appears correct when all data is shown, but when a slicer filters to one region the percentages no longer add up to 100%. This is because the column's denominator was fixed at ___ time and cannot respond to the slicer.
Practice 3 of 3
A calculated column stores one value per row in the model and is compressed by the VertiPaq engine. A column with very few distinct values — like a Status column with five possible values — ___ very well and adds little to the model size.
Lesson Quiz
Quiz 1 of 3
You need a field that shows each customer's city on a map visual, pulled from the Customers table into an Orders-based report. Should you use RELATED() in a calculated column, or build a measure?
Quiz 2 of 3
A 50 million row fact table has 8 calculated columns added to it over time. The model file has grown to 2.4GB and refreshes take 45 minutes. Which columns are most likely to be contributing disproportionately to the size problem?
Quiz 3 of 3
A colleague builds a Sales Rank calculated column using RANKX(ALL(Products), Products[Revenue]). They show it in a table visual filtered to Electronics and notice every product shows its rank across all products, not its rank within Electronics. What is the correct fix?
Next up — Lesson 34 covers the most commonly used DAX functions in depth, including every aggregation function, the text functions used in real reports, the date functions that do not require time intelligence, and the logical patterns every DAX author uses daily.