Skip to content
L-SYSTEM

DAX II: CALCULATE and context transition

Preview unit: this one is open without enrolling. The rest open after you enrol.

Exam objective (Model the data, 25–30 %): Use the CALCULATE function. Related: Create single aggregation measures and Identify use cases for calculated columns (u10), Implement time intelligence measures (u12), Create calculation groups (u13).

1. The pain: a measure that looks like SQL and lies

Aldervane's Head of Consulting wants one matrix: hours by client, billable hours, the client's share of all hours, and hours on UK clients. Tomasz, a senior developer who has written T-SQL for ten years, writes the measures the way he would write a WHERE:

Billable Hours = CALCULATE ( [Hours], Timesheet[Billable] = "Y" )
UK Hours       = CALCULATE ( [Hours], Client[Country] = "UK" )

On his first page every number looks right. Then a colleague drags Billable onto the rows and Country onto the columns, and the matrix starts showing 28 billable hours on the non-billable row and 26 UK hours in the Ireland column. Nothing is broken. Tomasz read CALCULATE as "add a condition". It actually means "evaluate this expression in a modified filter context", and the modification usually replaces filters rather than adding to them.

This unit is about that one difference. Once you can say what the filter context of a cell is, every CALCULATE question on the exam becomes arithmetic.

2. The model we compute in our heads

client Client ClientName (key) Country ts Timesheet ClientName Billable (Y/N) Hours client->ts 1 → *  (single direction)
FigureThe unit's mini model — Client filters Timesheet through a one-to-many relationship.
Description

Two boxes joined by an arrow from left to right. The left box is the dimension table Client with the columns ClientName, which is the key, and Country. The right box is the fact table Timesheet with the columns ClientName, Billable with values Y or N, and Hours. The arrow is labelled one to many, single direction: a filter on Client flows to Timesheet, never the other way. All examples in this unit use this model.

Client

ClientName Country
Contoso Energy UK
Fabrikam Logistics Ireland
Woodgrove Bank UK

Timesheet

ClientName Billable Hours
Contoso Energy Y 10
Contoso Energy N 2
Fabrikam Logistics Y 6
Fabrikam Logistics Y 4
Woodgrove Bank Y 8
Woodgrove Bank N 6

The base measure is Hours = SUM ( Timesheet[Hours] ). Totals to keep in mind: Contoso 12, Fabrikam 10, Woodgrove 14, all clients 36; billable hours 28; UK clients 26.

3. Filter context in one sentence (from u10)

Every cell of a visual is evaluated separately. Its filter context is the set of filters from the row header, the column header, slicers, the filter pane and RLS. In a matrix with ClientName on rows, the Contoso cell has the filter Client[ClientName] = "Contoso Energy", and that filter flows through the relationship to Timesheet. [Hours] there is 12. The total row has no filter on ClientName, so it shows 36.

Habit for the exam: before you compute anything, write down the cell's filter context. Then apply what CALCULATE does to it.

4. CALCULATE sets filters, and it replaces them

CALCULATE ( <expression>, <filter1>, <filter2>, … )

A filter argument like Timesheet[Billable] = "Y" is shorthand for a table with one column and the allowed values:

-- what the engine actually uses
FILTER ( ALL ( Timesheet[Billable] ), Timesheet[Billable] = "Y" )

Note the ALL. The shorthand ignores whatever filter already exists on Timesheet[Billable] and puts its own filter in its place. Filters on other columns stay as they are.

Isotype picture — the same coins seen through two frames, VISUAL and CALCULATE, each letting different coins through.
FigureIsotype picture — the same coins seen through two frames, VISUAL and CALCULATE, each letting different coins through.
Description

Two square charcoal frames on beige paper, joined by a thick arrow pointing from left to right. The left frame, labelled VISUAL, holds rows of faded coin outlines; only the top row of four slate-blue coins is solid — the selection the visual's filter lets through. The right frame, labelled CALCULATE, holds the same faded coins, but now only the bottom row of four ochre coins is solid. The idea: CALCULATE does not narrow the visual's selection further — it swaps the filter on that column for its own, so a different set of rows is counted. The picture does not show filters on other columns, which CALCULATE leaves in place.

4.1 When replacing is exactly what you want

Matrix with ClientName on rows:

ClientName Hours Billable Hours
Contoso Energy 12 10
Fabrikam Logistics 10 10
Woodgrove Bank 14 8
Total 36 28

The row filter is on ClientName, while CALCULATE filters Billable. These are different columns, so the two filters combine and the numbers are what Tomasz expected.

4.2 When it surprises you

Put Timesheet[Billable] on the rows instead:

Billable Hours Billable Hours
N 8 28
Y 28 28
Total 36 28

On the N row the cell's filter is Billable = "N". CALCULATE replaces it with Billable = "Y", so the cell shows 28. Exam trap: any question that puts on the axis the same column the measure filters is asking whether you know this.

The same thing happens with UK Hours = CALCULATE ( [Hours], Client[Country] = "UK" ):

Axis on rows Row UK Hours Why
ClientName Contoso Energy 12 ClientName filter stays, Country = UK added: Contoso is UK
ClientName Fabrikam Logistics (blank) ClientName = Fabrikam and Country = UK: no such rows
ClientName Woodgrove Bank 14 both filters hold
Country Ireland 26 Country = Ireland is replaced by Country = UK
Country UK 26 replaced by the same value

The Fabrikam row is blank and the Ireland row shows 26, even though Fabrikam is the Irish client. The difference is entirely about which column the visual filters.

4.3 Intersect instead of replace: KEEPFILTERS

If the requirement is "UK hours, but respect what the user has already selected", wrap the filter:

UK Hours (keep) = CALCULATE ( [Hours], KEEPFILTERS ( Client[Country] = "UK" ) )

KEEPFILTERS intersects the new filter with the existing one on the same column. With Country on rows: Ireland → blank, UK → 26.

A table filter also intersects, because it iterates only the rows that are already visible:

UK Hours (table) = CALCULATE ( [Hours], FILTER ( Client, Client[Country] = "UK" ) )

This works, but it is slower and less precise than KEEPFILTERS: it filters every column of Client, not just Country. Use FILTER when the condition needs something a simple predicate cannot express — most often a measure (section 6).

5. Removing filters: percent of total

Hours All Clients = CALCULATE ( [Hours], REMOVEFILTERS ( Client ) )
% of All Clients  = DIVIDE ( [Hours], [Hours All Clients] )
ClientName Hours Hours All Clients % of All Clients
Contoso Energy 12 36 33.3 %
Fabrikam Logistics 10 36 27.8 %
Woodgrove Bank 14 36 38.9 %

The functions that take filters away, and when to choose each one:

Function (as a CALCULATE argument) Removes Keeps Typical use
REMOVEFILTERS ( Client ) filters on every column of Client everything else share of the whole firm
ALL ( Client ) the same as REMOVEFILTERS when used as a modifier everything else older code; ALL can also return a table, REMOVEFILTERS cannot
REMOVEFILTERS ( Client[ClientName] ) the filter on one column only other Client columns, e.g. Country share within the country
ALLEXCEPT ( Client, Client[Country] ) all Client filters except Country Country the same, written the other way round
ALLSELECTED ( Client ) filters the visual itself applies filters from slicers and the outer report "percent of what the user is looking at"

Exam trap: "The percentage must still add up to 100 % when a user selects UK in a slicer" means ALLSELECTED. With REMOVEFILTERS the UK clients would add up to only 72 % (26 / 36), because the denominator would ignore the slicer.

6. Filtering by a measure

A Boolean filter argument may compare columns of one table with constants, but it cannot reference a measure. "Billable hours of clients with more than 10 hours in total" needs a table of clients to iterate:

Billable Hours Big Clients =
CALCULATE (
    [Billable Hours],
    FILTER ( VALUES ( Client[ClientName] ), [Hours] > 10 )
)

VALUES ( Client[ClientName] ) is a small table: one row per visible client. For each row FILTER evaluates [Hours], which is a measure and therefore triggers context transition (next section). Iterate the smallest table that answers the question — a dimension column, never the fact table.

7. The evaluation order of CALCULATE

a 1. Evaluate filter arguments in the ORIGINAL filter context b 2. Context transition (if there is a row context) a->b c 3. Apply modifiers REMOVEFILTERS, ALL, USERELATIONSHIP, CROSSFILTER b->c d 4. Apply filter arguments replace filters on the same columns (KEEPFILTERS: intersect instead) c->d e 5. Evaluate the expression in the new filter context d->e
FigureThe five steps CALCULATE takes, from the original filter context to the result.
Description

Five boxes stacked top to bottom and joined by arrows. One: the filter arguments are evaluated in the original filter context. Two: if there is a row context, context transition turns the current row into filters. Three: modifiers such as REMOVEFILTERS, ALL, USERELATIONSHIP and CROSSFILTER are applied. Four: the filter arguments are applied, replacing existing filters on the same columns, or intersecting with them when wrapped in KEEPFILTERS. Five: the expression is evaluated in the resulting filter context. The order explains why a filter argument wins over a filter that context transition created on the same column.

You rarely need the order on the exam. You need it when two of the steps touch the same column, for example a filter argument on a column that context transition has also filtered: step 4 comes after step 2, so the filter argument wins.

8. Context transition

Isotype picture — one highlighted ROW passes through a funnel and becomes a FILTER that lets only its own coins through.
FigureIsotype picture — one highlighted ROW passes through a funnel and becomes a FILTER that lets only its own coins through.
Description

On the left, a column of five person pictograms stands for the rows of a table, labelled ROW; the middle person is brick red and framed. An arrow carries this one row into a black funnel. From the narrow end of the funnel an arrow enters a frame labelled FILTER, which holds nine coins in three rows: only the middle row of three brick-red coins is solid, the rest are faded. The idea: context transition turns the row being iterated into a filter, so the calculation sees only the fact rows that belong to that row. The picture does not show how many columns of the row become filters — in DAX, all of them do.

A row context exists in calculated columns and in iterators (SUMX, AVERAGEX, FILTER…). It means "I am on this row", but it does not filter anything. CALCULATE converts it: every column of the current row becomes a filter. That conversion is context transition.

Three calculated columns on Client:

Calculated column Contoso Fabrikam Woodgrove
= SUM ( Timesheet[Hours] ) 36 36 36
= CALCULATE ( SUM ( Timesheet[Hours] ) ) 12 10 14
= [Hours] 12 10 14
  • The first column has no filter context, so SUM sees the whole Timesheet. This is the classic SQL-mind error: a developer expects a correlated subquery and gets a grand total.
  • The second column uses CALCULATE with no filter arguments. Its only job here is context transition.
  • The third references a measure. A measure reference always carries an implicit CALCULATE, so it triggers context transition too.

The same rule holds inside measures that iterate:

Avg Hours per Client (right) = AVERAGEX ( Client, [Hours] )                 -- total row: 12
Avg Hours per Client (wrong) = AVERAGEX ( Client, SUM ( Timesheet[Hours] ) ) -- total row: 36

In the wrong version each iteration sums all 36 hours (no transition), and the average of 36, 36 and 36 is 36.

Exam trap: "a calculated column returns the same value on every row" points to a missing context transition. The fix is CALCULATE or a measure reference.

9. The cost

  • FILTER over a fact table is slow. CALCULATE ( [Hours], FILTER ( Timesheet, Timesheet[Billable] = "Y" ) ) iterates every visible timesheet row and can be orders of magnitude slower than the column predicate. On a 10-million-row fact table you will see it in Performance Analyzer (u14).
  • Context transition inside big iterators is expensive. SUMX ( Timesheet, [Some Measure] ) performs a transition for every fact row. Iterate a dimension or VALUES of a column instead.
  • Transition filters every column of the row. If a table has duplicate rows, the transition filters all identical rows at once. Iterating a table without a key gives silently wrong results.
  • Readability. Every replaced filter is invisible on the report. A user who sees 28 on the "N" row will not guess why. Name such measures honestly (Billable Hours (ignores Billable filter)), or use KEEPFILTERS if the visual's filter should win.
SQL intuition What DAX actually does
WHERE Billable = 'Y' adds a condition a CALCULATE filter replaces the filter on that column
SUM(Hours) OVER () for a grand total CALCULATE ( [Hours], REMOVEFILTERS ( … ) )
a correlated subquery per row a calculated column needs context transition to see "its" rows
HAVING SUM(Hours) > 10 FILTER ( VALUES ( … ), [Hours] > 10 ) inside CALCULATE

10. Lab — CALCULATE on the Aldervane dataset

You need: Power BI Desktop (free, Windows) and the public dataset aldervane-pbi-dataset (CSV files plus a README with the relationships). Time: about 40 minutes. Cost: none.

  1. Get data → Text/CSV for DimClient, DimEngagement and FactTimesheet. In Model view create DimClient[ClientKey] 1 → * DimEngagement[ClientKey] and DimEngagement[EngagementKey] 1 → * FactTimesheet[EngagementKey], both single direction.
  2. Create the measures:
    Hours            = SUM ( FactTimesheet[Hours] )
    Billable Hours   = CALCULATE ( [Hours], FactTimesheet[Billable] = "Y" )
    UK Hours         = CALCULATE ( [Hours], DimClient[Country] = "UK" )
    UK Hours (keep)  = CALCULATE ( [Hours], KEEPFILTERS ( DimClient[Country] = "UK" ) )
    Hours All Clients = CALCULATE ( [Hours], REMOVEFILTERS ( DimClient ) )
    % of All Clients = DIVIDE ( [Hours], [Hours All Clients] )
    
  3. Open DAX query view and run:
    EVALUATE
    SUMMARIZECOLUMNS (
        DimClient[Country],
        "Hours", [Hours],
        "UK Hours", [UK Hours],
        "UK Hours (keep)", [UK Hours (keep)]
    )
    
    Expected: Ireland 9 664 / 18 907 / blank; Poland 6 186 / 18 907 / blank; UK 18 907 / 18 907 / 18 907. Explain each number aloud before you look.
  4. Build a matrix with DimClient[ClientName] on rows and [Hours], [% of All Clients]. Contoso Energy should show 19.5 %. Add a slicer on Country, select UK, and watch the percentages stop adding up to 100 %. Change the denominator to ALLSELECTED ( DimClient ) and check again.
  5. On DimEngagement add two calculated columns: Hours (bare) = SUM ( FactTimesheet[Hours] ) and Hours (CT) = [Hours]. For ENG-4401 expect 34 757 and 1 567.5.
  6. The overall total is 34 757 hours as loaded: this lab loads the CSVs fresh, so the deliberate −3-hour entry is still in. The u04 clean-up removes it (34 760 hours after it); the lab numbers above are as loaded.

No Windows machine, or Desktop blocked? Work through steps 2–5 on paper using the expected numbers above. Every practice item in this unit can be solved without Power BI.

11. Exam traps — summary

  • A filter argument replaces the filter on the same column; other columns keep their filters.
  • Same column on the axis and in the filter argument → every row shows the same number.
  • "Respect the user's selection" → KEEPFILTERS (or a table filter).
  • "Percent of everything" → REMOVEFILTERS / ALL; "percent of what is selected" → ALLSELECTED; "percent within the country" → remove the ClientName filter only / ALLEXCEPT.
  • A Boolean filter cannot use a measure → FILTER ( VALUES ( … ), [Measure] > x ).
  • Calculated column shows the grand total on every row → missing context transition; use CALCULATE or a measure reference.
  • A measure reference = an implicit CALCULATE.

12. Remember the pictures

Retell each picture in one sentence. The two frames: CALCULATE swaps the filter on a column, it does not narrow it. The funnel: the current row becomes a filter. If you can say both without looking, you hold the core of this objective.


Sources: Microsoft Learn — "CALCULATE function (DAX)", "KEEPFILTERS function (DAX)", "ALLSELECTED function (DAX)", "Modify DAX filter context in semantic models" (training module, unit "Perform context transition"), "DAX query view". Exam PL-300 study guide, skills measured as of April 20, 2026.

Practice

Checking here changes nothing: answers start counting once you enrol on the course.

  1. Mini model from this unit. Billable Hours = CALCULATE ( [Hours], Timesheet[Billable] = "Y" ). In a matrix with Client[ClientName] on rows, what does Billable Hours return on the Contoso Energy row?

  2. Same measure: Billable Hours = CALCULATE ( [Hours], Timesheet[Billable] = "Y" ). Now Timesheet[Billable] is on the rows of the matrix. What does Billable Hours show on the N row?

  3. UK Hours = CALCULATE ( [Hours], Client[Country] = "UK" ). The matrix has Client[Country] on rows. What does UK Hours show on the Ireland row?

  4. Same UK Hours measure, but now Client[ClientName] is on rows. What does UK Hours show on the Fabrikam Logistics row?

  5. Complete the measure so that, with Country on rows, the Ireland row is blank and the UK row shows 26: UK Hours = CALCULATE ( [Hours], ( Client[Country] = "UK" ) )

  6. Hours All Clients = CALCULATE ( [Hours], REMOVEFILTERS ( Client ) ). Matrix with Client[ClientName] on rows. What does it show on the Woodgrove Bank row?

  7. Aldervane's report has a Country slicer. The % of Hours column in a client table must add up to 100 % for whatever countries the user selects. Which function belongs in the denominator: CALCULATE ( [Hours], ____ ( Client ) )?

  8. Yes or No? CALCULATE ( [Hours], Client[Country] = "UK" ) adds Country = UK to whatever filter the visual already has on Client[Country].

  9. Yes or No? In a calculated column on Client, SUM ( Timesheet[Hours] ) returns the hours of the current row's client.

  10. Yes or No? Writing = [Hours] (a measure reference) in a calculated column triggers context transition.

  11. Avg = AVERAGEX ( Client, SUM ( Timesheet[Hours] ) ). What does Avg return in the total row of a matrix by ClientName?

  12. Used as a CALCULATE argument, what does each expression do to the existing filter on Client?

    1. REMOVEFILTERS ( Client )
    2. ALL ( Client[Country] )
    3. Client[Country] = "UK"
    4. KEEPFILTERS ( Client[Country] = "UK" )
    5. FILTER ( Client, Client[Country] = "UK" )
  13. You need billable hours only for clients whose total [Hours] is above 10. Which filter argument works?

  14. In the CALCULATE picture, the visual's frame lets the blue coins through, but the CALCULATE frame lets only the ochre coins through. What does that show about CALCULATE?