learn-business-intelligence-with-phoebe / Builder session 4 of 10
Learn Business Intelligence with Phoebe · Builder track · Session 4 of 10

Measures that mean something

Your star schema from b3 is only as trustworthy as the numbers computed on top of it. Tonight you learn the split that organizes every calculation in every BI tool - stored columns vs live measures - and filter context, the one idea that makes DAX learnable instead of mystical. Then you define Daybreak's governed measures and meet the metrics that quietly lie: distinct counts that refuse to sum, and ratios that break when you average them.

🟡 Builder track Practitioners: analysts · DE · DS · PMs Runs in your browser · no install
0-3 · Welcome 3-20 · Columns, measures, filter context 20-42 · Build-along: governed measures 42-45 · Q&A
Part 0

Why this session exists

Everyone who opens Power BI hits the same wall: "should this be a calculated column or a measure?" and later, "why does my number change when I click a slicer?" Both walls are the same wall. Once you see that a measure re-runs inside whatever filters surround it, DAX stops being incantations and becomes one repeated move. We build that intuition on Daybreak, in the browser, with the SQL visible - because the generated GROUP BY is filter context wearing its work clothes.

Live - presented in session Self-study - read after class ▶ Mini-BI - interactive playground Official sources covered
★ What you walk out with today The measure-vs-column split and a rule of thumb that settles it in one second, a working intuition for filter context and CALCULATE (the heart of the PL-300 DAX domain), and three governed Daybreak measures - Revenue, AOV, Active Customers - defined, documented, and tested against the live database.
Part 1 · covers PL-300 "Create model calculations by using DAX" - the measure/column split

Measures vs calculated columns 6 min live

Every calculation in a BI model runs at one of two moments. A calculated column runs once per row when the data refreshes, and the result is stored in the table - line_revenue = quantity * price is the classic. A measure runs at query time, inside the filters of whoever is looking - Total Revenue = SUM of line_revenue. Rule of thumb, worth the whole session: if it aggregates, it is a measure. Columns are for row-level labels and keys; measures are for every number a human reads on a dashboard.

Calculated column line_revenue = quantity * price refresh-time lane Runs once per row, at refresh Stored sits in the table Frozen until the next refresh Measure Total Revenue = SUM(line_revenue) query-time lane Runs live at query time Obeys filters rows, slicers, pages Fresh answer per filter context Rule of thumb: if it aggregates, it is a measure. Columns are for row-level labels and keys.
🔍 Click to zoom - the two lanes every BI calculation runs in: stored at refresh vs live at query time
LiveThe cost of getting it wrong4 min

Imagine storing "revenue" as a calculated column on the customers table: each row gets that customer's lifetime total, computed at refresh. It looks fine - until someone adds a month slicer. The column cannot react: it was computed before the slicer existed, so every filter shows the same frozen lifetime numbers. The dashboard is confidently wrong.

  • Frozen numbers: columns ignore slicers, page filters, and cross-highlighting - they were computed at refresh, full stop.
  • Bloated model: every stored column costs memory on every row. Measures cost nothing until queried.
  • Fork risk: a "revenue" column here and a "revenue" measure there will eventually disagree, and the meeting where they disagree is not a fun meeting.
Real world

The audit that started with one slicer. A retail team shipped a dashboard where margin was a calculated column summed by region. Legal-entity filters were added later; the frozen column ignored them, and two quarters of board decks carried region margins that no filter had ever actually touched. The fix was one line - rebuild margin as a measure - but rebuilding trust took longer.

In Power BI / In Tableau Power BI splits the two lanes explicitly: calculated columns (stored, refresh-time) vs measures (DAX, query-time) - PL-300 tests the choice directly, alongside calculated tables, quick measures, and calculation groups. Tableau folds both into calculated fields, with LOD expressions like FIXED playing the "control the filters" role and quick table calcs (running total, % of total, YoY) covering the common patterns. Power BI also now has visual calculations - a newer, lighter DAX that lives on one visual and operates on what that visual already shows; it is on PL-300 as its own skill, and it is the right tool for quick on-chart math that does not deserve a governed model measure.
Part 2 · covers MS Learn "Modify DAX filter context"

Filter context - the one idea that unlocks DAX 7 min live

Here is the secret that makes a matrix visual work: every cell is the same measure, evaluated under that cell's own filters. The Coffee row filters to Coffee, the March column filters to March, the plan slicer filters everything - and [Revenue] re-runs inside each combination. That set of active filters is the filter context. And CALCULATE is simply the function that says: run this measure, but with changed filters - add one, replace one, or ignore one, regardless of what the visual is doing.

Watch it happen. Each bar below is one measure - revenue - evaluated under a different category filter. Now press Show SQL: the GROUP BY you see is the filter context made visible. One declaration, four evaluations.

Slicer plan = Pro filters all cells Jan filter Feb filter Mar filter Coffee row filter Equipment row filter [Revenue] Coffee + Jan + Pro [Revenue] Coffee + Feb + Pro [Revenue] Coffee + Mar + Pro [Revenue] Equip + Jan + Pro [Revenue] Equip + Feb + Pro [Revenue] Equip + Mar + Pro One measure, six evaluations. Each cell runs [Revenue] under its own filter set - that set is the filter context.
🔍 Click to zoom - one measure, six filter contexts: row, column, and slicer filters combine per cell
LiveCALCULATE in plain words3 min

CALCULATE is not magic; it is a filter editor. Read it as: "run this measure, but with these filters forced" - whatever the visual, rows, or slicers are doing. Want Coffee revenue shown on every row of a matrix so you can compare each category against it? Force the category filter:

Pseudo-DAXCoffee Revenue = CALCULATE( [Revenue], products[category] = "Coffee" ) Read it aloud: "revenue, but pretend the only category filter is Coffee." The visual can slice by month, city, plan - CALCULATE overrides just the category.

That is the whole trick. Time intelligence (Part 3), "% of total", and "vs all customers" comparisons are all CALCULATE wearing different filter edits. Exact function syntax and the deeper row-context rules stay with MS Learn's five DAX modules - what you own after tonight is the mental model those modules assume.

Part 3 · covers PL-300 time intelligence + semi-additive measures

Time intelligence, and the measures that lie 7 min live

Once filter context clicks, time intelligence is just CALCULATE editing the date filter: MTD stretches it to "month so far", YTD to "year so far", YoY swaps it for "same period last year". All of it leans on the date table you built in b3 - no proper date table, no time intelligence. But some measures need more care than a date filter: semi-additive measures sum happily across some dimensions and lie across others.

Exhibit A: distinct customers. A customer who orders in February and March is one customer, not two - so you may not add monthly customer counts together. Watch the monthly line, then let SQL catch the lie:

SELECT 'sum of 6 monthly distinct counts' AS method,
       SUM(monthly_customers) AS customers
FROM (SELECT COUNT(DISTINCT customer_id) AS monthly_customers
      FROM orders
      GROUP BY strftime('%Y-%m', order_date))
UNION ALL
SELECT 'distinct customers, Jan-Jun as one period',
       COUNT(DISTINCT customer_id)
FROM orders;
LiveRatios divide last4 min

Same disease, second strain: ratio measures. Average order value is total revenue divided by total orders - computed at the level you are looking at. The wrong move is averaging the monthly AOVs: months with few orders get the same vote as months with many, and the "average" drifts away from the truth. Define AOV once as a division of two measures, and filter context does the leveling for you.

Our playground's Customers and Avg order value measures are exactly these two trap species - a distinct count and a ratio - which is why they are in the dropdown at all. Every time you pick them, the generated SQL recomputes them inside the current filter, never by summing or averaging pre-cooked numbers.

Self-studyThe full semi-additive family3 min read

Distinct counts are the gateway case, but the family is bigger, and PL-300 names it explicitly:

  • Balances and inventory: bank balance sums across accounts but not across months - you want the closing balance, not January plus February. Daybreak's active subscription count behaves the same way.
  • Rates and percentages: churn rate, margin %, conversion - all ratios; all divide last.
  • Min/max style measures: "largest order this period" recomputes per period; summing is meaningless.

The test to memorize: "can I add this across every dimension and stay truthful?" If any dimension says no, write the aggregation rule into the measure's definition card - which is exactly what Demo 1 is about.

Demo 1 of 2

Define Daybreak's governed measures ★ 12 min · everyone builds

A governed measure is a definition card: name, formula in business words, filters included and excluded, aggregation traps, owner. Tonight we write four of them for Daybreak and test each against the live database. This little document IS the semantic model - the same "one number, one truth" contract the leader track builds in a3, written by the people who compute it.

Revenue - decide what counts. Daybreak orders can be completed, cancelled, or refunded. Does Revenue include refunded orders? Decide, then run the query below and see how much the choice moves the number. Whatever you pick, write it on the card - the decision matters less than the documentation.

Orders and Active Customers. Orders = count of orders in filter context. Active Customers = distinct customers with at least one order - mark it semi-additive on the card: "never sum across time". Test it: Dimension Month · Measure Customers in the playground.

AOV - a ratio of two measures. AOV = [Revenue] / [Orders], divided inside the current filter. Card note: "never average monthly AOVs". Test: Dimension Plan · Measure Avg order value.

Read your four cards back. Name, formula, inclusions, traps, owner. Congratulations - you just wrote a semantic model. In Power BI these cards become DAX measures with descriptions; in Tableau, calculated fields with comments; in a3's language, the metric dictionary.

SELECT ROUND(SUM(oi.quantity * oi.unit_price), 2) AS revenue_all_orders,
       ROUND(SUM(CASE WHEN o.status = 'completed'
                      THEN oi.quantity * oi.unit_price
                      ELSE 0 END), 2)               AS revenue_completed_only
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id;
Real world

The definitions doc outlives the dashboard. Tools get replaced every few years; a one-page measure dictionary survives every migration, because it is the business logic itself. Teams that write cards first port to a new BI tool in weeks. Teams that let each dashboard define its own "revenue" port never - they rebuild, and re-argue every definition in meetings.

Demo 2 of 2

Your turn: stress-test the measures ★ 10 min · build your own

Three exercises, rising difficulty: read a chart with a data wrinkle, prove a pitfall with SQL, and write your first CALCULATE. Get the third one right and you have written the pattern behind half of production DAX.

LiveE1 · Units by roast - and the missing roast3 min

Roast only applies to coffee - equipment and add-ons carry NULL, which the playground shows as (none). That row is not an error; it is a modeling decision surfacing in a chart. Your measure card should say how NULLs in a dimension are handled and labeled.

LiveE2 · Prove the AOV pitfall yourself4 min

Run it: the average of six monthly AOVs vs AOV computed once over the half year. If the numbers differ, months with fewer orders are voting above their weight in the left-hand version. Which one belongs on the exec dashboard?

SELECT ROUND(AVG(monthly_aov), 2) AS avg_of_monthly_aovs,
       (SELECT ROUND(SUM(oi.quantity * oi.unit_price) * 1.0
                     / COUNT(DISTINCT o.order_id), 2)
        FROM orders o
        JOIN order_items oi ON oi.order_id = o.order_id) AS true_overall_aov
FROM (
  SELECT SUM(oi.quantity * oi.unit_price) * 1.0
         / COUNT(DISTINCT o.order_id) AS monthly_aov
  FROM orders o
  JOIN order_items oi ON oi.order_id = o.order_id
  GROUP BY strftime('%Y-%m', o.order_date)
);
Self-studyE3 · Write the CALCULATE yourself3 min

Daybreak's founder wants "Basic-plan revenue" pinned next to total revenue on every page, whatever the slicers say about plan. Write the pseudo-DAX before peeking. Hint: plan lives on the customers table.

Pseudo-DAXBasic Revenue = CALCULATE( [Revenue], customers[plan] = "Basic" ) The plan filter is forced; month, city, and category filters still flow in from the visual. That mix - override one filter, respect the rest - is the CALCULATE move you will use weekly.
Homework

Try it yourself - this week ◐ 20-30 min total

Source material

Official sources covered

Tonight was the conceptual core of the DAX and calculations domain, tool-agnostic and testable in the browser. Exact function syntax, click-paths, and the deeper evaluation-context rules stay with the vendors. This page covers:

PL-300 · Create model calculations by using DAXParts 1-3 · measures vs columns, CALCULATE, time intelligence, semi-additive - concepts + pseudo-DAX; exact syntax stays with MS Learn
MS Learn · DAX modules incl "Modify DAX filter context" + time intelligenceParts 2-3 · the mental model those five modules assume; function-by-function drills stay with Microsoft
Tableau · calculated fields, quick table calcs, LOD expressions (FIXED)Part 1 sidebar · the same two lanes in Tableau nouns
PL-300 · Create visual calculations by using DAXPart 1 sidebar · the newer on-visual option and when it beats a model measure
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · The real difference between a measure and a calculated column is...

Columns run once per row at refresh and get stored; measures re-run live inside whatever filters surround them. That is why columns freeze when slicers change - and why anything that aggregates should be a measure.

2 · CALCULATE([Revenue], products[category] = "Coffee") does what?

CALCULATE is a filter editor: it re-runs the measure with the filters you specify overridden, while every other filter from the visual and slicers keeps flowing in. Nothing is stored, nothing is deleted.

3 · Why can you not sum January's and February's distinct-customer counts to get "customers so far"?

One loyal customer, two monthly appearances, but still one customer. Distinct counts (and balances, and ratios) refuse naive summing across time - recompute them in the wider filter context instead, like the UNION ALL proof showed.

Builder session 4 cheat sheet · pin this

Calculated columnComputed once per row at refresh, stored in the table. For row-level labels and keys.
MeasureComputed at query time inside the current filter context. For every number a human reads.
Rule of thumbIf it aggregates, it is a measure. One second, decision made.
Filter contextEach visual cell = the same measure under that cell's filters. The generated GROUP BY made visible.
CALCULATERun this measure with changed filters - override some, respect the rest. Half of production DAX.
Time intelligenceMTD / YTD / YoY = CALCULATE editing the date filter. Needs the b3 date table.
Semi-additiveDistinct customers, balances, rates: never sum across time - recompute in the wider filter.
Ratios divide lastAOV = total revenue / total orders in context. Never average the monthly AOVs.