What’s fancy about context transition in DAX? | by Salvatore Cagliari | Towards Data Science

In this article, I show you what context transition is in DAX and how to use it.
I will cover only the basics of this feature. If I want to explain all details, I would have to start an entire series of articles.
Instead, I want to give you some small examples of how you can work with context transition and the most important things to know about it. This way, you can start working with it and learn more as needed.
You can find a list of articles and blog posts that look much deeper into context transition at the end.
I use the Contoso sample dataset, like in my previous articles.
From the entire data model, I need only two tables for my examples in this article:
Customer (DimCustomer)
Online Sales (FactOnlineSales)
One important factor about the Online Sales table is that the orders stored in the Online Sales tables contain an OrderNumber and an OrderLineNumber.
One order can have one or more OrderLineNumber:
Each OrderLineNumber has a product mapped and has a SalesAmount.
To keep the code simple, I will analyse the single OrderLines instead of the Orders.
I would need to change the granularity of the data to be able to analyse the Orders. I will keep the topic of changing granularity in DAX for a future article.
Let’s assume the following situation: We have a list of customers, and we would like to know the total sales per customer and get other information.
To gain the required information, we would like to calculate the following results:
Highest Sale per Customer in the Customer table
Total Sales per Customer and Year
What is the Average of Sales per Customer
Percentage of Sales by Customer of all Sales
Which customer is inside the Top 10 % by Total Sales per Year
I cannot create Measures to calculate these results as I want to use them as filters in Slicers and as reporting attributes in my report.This means that I have to add calculated DAX columns in the Customer table.
This requirement looks easy: get the MAX() from the Online Sales table per customer.
But, I cannot create a calculated column in the Customer table with MAX(‘Online Sales’[SalesAmount]) as the SalesAmount column is not in the Customer table.
I can write the following code:
Highest OrderLine = MAXX(‘Online Sales’      , ‘Online Sales’[SalesAmount])
The result will look like this:
As MAXX has no filter context, it will retrieve the OrderLine with the highest SalesAmount over all the customers.
I have to use context transition to force the DAX engine to switch from row-context to filter-context. During context transition, the content of the actual row will be added to the filter context, and MAXX is executed with this filter context.
I can force context transition with a CALCULATE():
Highest Order Line =    VAR Result =        CALCULATE(            MAXX( ‘Online Sales’                  ,’Online Sales’[SalesAmount])                 )RETURN    Result
You can see the result in the following picture:
What happens?
During the iteration over each row in the Customer table, context transition adds the content of the actual row to the filter context, including the CustomerKey column. The CustomerKey column is used in the Relationship with the Online Sales table.The CustomerKey filters the Online Sales table for each customerNow the maximum value of the SalesAmount column is evaluated for each CustomerKey
During the iteration over each row in the Customer table, context transition adds the content of the actual row to the filter context, including the CustomerKey column. The CustomerKey column is used in the Relationship with the Online Sales table.
The CustomerKey filters the Online Sales table for each customer
Now the maximum value of the SalesAmount column is evaluated for each CustomerKey
You can leverage context transitions with all iterator functions, including FILTER() and other functions.
I used this technique in one of my last articles (To weigh or not to weigh — this is the Average question ) with the following Measure:
Average per Country =    AVERAGEX(VALUES(‘Geography’[Region Country])        ,DIVIDE([Online Sales (By Customer)]                 , [Order Count (By Customer)] )    )
Even though you don’t see any CALCULATE() in that solution, I use Measures within the AVERAGEX function. Using a Measure within a Row context automatically triggers a context transition.
This requirement is a little bit more complicated to solve.
As mentioned above, I don’t want to create a Measure, as I want to use the calculation result in a Slicer.
So, I have to create a calculated column as well:
Year of Highest Order Line =VAR HighestOrderLine = ‘Customer’[Highest Order Line]VAR Result =    CALCULATE(        MAXX ( ‘Online Sales’                ,YEAR(‘Online Sales’[OrderDate])               )            , ‘Online Sales’[SalesAmount] = HighestOrderLine            )RETURN    Result
Again, I use CALCULATE() to start context transition, which automatically adds the CustomerKey column to the filter context for the MAXX() function. Because of this, I don’t need to filter by CustomerKey inside CALCULATE().
I only have to search for the order line with the highest sales, which I calculated in the previous column, and use MAXX to get the last year with this sales value.
The Filter added with CALCULATE() is added to the existing Filter Context, which already contains the row of the current customer in the Customer table through context transition.
You can see the result in the following picture:
With the knowledge you have gained until now, you should be able to solve this requirement independently.
Try to think about it and develop an idea for what the solution could look like.
.
.
.
Done?
OK, this is the solution for this calculated column:
Average Sales = CALCULATE(                          AVERAGEX(‘Online Sales’                          ,’Online Sales’[SalesAmount])                          )
Context transition allows us to write such short and powerful DAX formulas.
Another approach would have been to create a Measure with AVERAGEX(), as shown above, and use this Measure directly for the calculated column.
This requirement requires us to combine the gained knowledge to create a new calculated column with the following DAX formula:
% Sales of Total Sales =VAR SalesOverall = SUMX(‘Online Sales’                        ,’Online Sales’[SalesAmount]                        )VAR SalesOfCustomer = CALCULATE(                             SUMX(‘Online Sales’                                   ,’Online Sales’[SalesAmount]                                  )                              )VAR PctOfTotalSales = DIVIDE(SalesOfCustomer, SalesOverall)RETURN    PctOfTotalSales
Here is the explanation of this formula:
The Variable SalesOverall doesn’t use context transition. The result is the total sales overall customersThe Variable SalesOfCustomer encloses the same expression as SalesOverall with a CALCULATE(), thus enforcing a context transition.The result is the sum of Sales for the current customerThe DIVIDE in the Variable PctOfTotalSales calculates the percentage of SalesOfCustomer of SalesOverall
The Variable SalesOverall doesn’t use context transition. The result is the total sales overall customers
The Variable SalesOfCustomer encloses the same expression as SalesOverall with a CALCULATE(), thus enforcing a context transition.The result is the sum of Sales for the current customer
The DIVIDE in the Variable PctOfTotalSales calculates the percentage of SalesOfCustomer of SalesOverall
The result looks like this:
For this, I need to calculate the ranking of all customers based on the Total Sales Amount. Then I need to get the highest Rank, or the number of customers, and calculate which customers are in the top 10 % of all customers.
The output should be a column that contains a True or False value.
As you can imagine, it is not possible to calculate this in one single step.
Here I want to show you two solutions, which uses context transition in slightly different ways.
With this approach, I create a calculated DAX table. Then I use LOOKUPVALUE() to map the values into the Customer table.
This is the code to create the DAX table:
Customer Ranking =VAR CustAndSales = ADDCOLUMNS( VALUES(Customer[CustomerKey] )                              ,”Amt”, [Online Sales (By Order Date)]                              )VAR RankedCustomer = ADDCOLUMNS(CustAndSales                                ,”Rank”, RANKX(CustAndSales                                               ,[Amt]                                               )                                 )VAR MaxRank = MAXX( RankedCustomer                     ,[Rank]                     )VAR Result = ADDCOLUMNS(                    RankedCustomer                    ,”IsTop10Perc”                        , IF(                               DIVIDE( [Rank]                                     , MaxRank ) <= 0.1                               , TRUE()                               ,FALSE()                               )                         )RETURN    Result
The variable CustAndSales creates a list of all CustomerKey and calculates a sum of the sales per customer with the Measure [Online Sales (By Order Date)]
In the variable RankedCustomer, I use RANKX() to calculate the ranking overall CustomerKeys based on the total sales per customerThe variable MaxRank gets the highest Rank, which I will use in the following variableFinally, I’m adding the column IsTop10Perc with the result of an IF(). This column contains the flag if the customer is in the top 10 % of customers based on the total sales for each customer
In the variable RankedCustomer, I use RANKX() to calculate the ranking overall CustomerKeys based on the total sales per customer
The variable MaxRank gets the highest Rank, which I will use in the following variable
Finally, I’m adding the column IsTop10Perc with the result of an IF(). This column contains the flag if the customer is in the top 10 % of customers based on the total sales for each customer
Now, I use the following expression to create a new calculated column in the Customer table:
Is Top 10 % customer by Sales =VAR Result = LOOKUPVALUE(‘Customer Ranking’[IsTop10Perc]                          , ‘Customer Ranking’[CustomerKey]                          , ‘Customer’[CustomerKey]                          )RETURN    Result
I can use this approach to add the following column from the calculated DAX table:
Rank by Sales
Is Top 10 Percent
Total Sales per customer
With this approach, I create a few calculated columns containing intermediate results to get the final result.
Total Sales per Customer:
Total Sales per Customer:
To get the total sales per customer, I need to use the existing Measure [Online Sales (By Order Date)]:
Total Sales per Customer = [Online Sales (By Order Date)]
As DAX starts a context transition when we use a Measure inside a Row context, this is enough to get the sum of Sales Amount for each customer.
2. Calculate the ranking for each customer
I need the column [Total Sales per Customer] to calculate the ranking.
Like in the first solution, I use RANKX to do the job:
Rank by Sales (local) = RANKX(‘Customer’                              ,’Customer’[Total Sales per Customer])
3. Set the top 10 % customer by Sales
Finally, I can use the same logic as before to find if the current customer is in the top 10 % of customers by total sales:
Is Top 10 Percent (local) =VAR MaxRank = MAXX( ‘Customer’                    ,[Rank by Sales (local)]                    )VAR Result = IF(                DIVIDE( [Rank by Sales (local)]                        , MaxRank ) <= 0.1                  ,TRUE()                  ,FALSE()                  )RETURN    Result
As you can see, both solutions use the same basic logic but with a slightly different technique.
It’s up to you which one you want to use.
The result is the same with both solutions:
Solution 1 was my first approach, as it looked easier to do. I hid the calculated table, as it’s of no use after I included all the columns into the Customer table. But, I don’t like unnecessary objects in my model, so I figured out how to solve the requirement without the DAX table.
Context transition is a complex but very useful feature in DAX. This feature can help you solve problems more quickly than when you don’t know how to use it.
But it has some significant quirks, which you need to know:
Context transition is slow in measures
Duplicate rows in the table used by context transition (The ‘Online Sales’ fact table in the examples above) can cause wrong results, which are extremely hard to spot.
You need to know when context transition happens: always when you are iterating through rows, and you use a Measure, or you use CALCULATE()
I encourage you to read the following SQL article Understanding Context Transition — SQLBI to learn the basics.
You can read the article Context Transition and Filters in CALCULATE — SQLBI to learn about context transition with CALCULATE.
Read this article https://www.burningsuit.co.uk/blog/2021/08/context-transition-where-the-row-context-becomes-a-filter-context/, which contains a throughout explanation of context transition, including what you need to consider when using context transition
Here are some more articles and blog posts about this important topic: