In one of the Advanced DAX Workshop I taught this year, I had an interesting discussion about how to optimize a SWITCH statement (which could be frequently used checking a slicer, like in the Parameter Table pattern).

Let’s start with the problem. What happen when you have such a statement?

Sales :=

SWITCH (

VALUES ( Period[Period] ),

"Current", [Internet Total Sales],

"MTD", [MTD Sales],

"QTD", [QTD Sales],

"YTD", [YTD Sales],

BLANK ()

)
The SWITCH statement is in reality just syntax sugar for a nested IF statement. When you place such a measure in a pivot table, for every cell of the pivot table the IF options are evaluated. In order to optimize performance, the DAX engine usually does not compute cell-by-cell, but tries to compute the values in bulk-mode. However, if a measure contains an IF statement, every cell might have a different execution path, so the current implementation might evaluate all the possible IF branches in bulk-mode, so that for every cell the result from one of the branches will be already available in a pre-calculated dataset.

The price for that could be high. If you consider the previous Sales measure, the YTD Sales measure could be evaluated for all the cells where it’s not required, and also when YTD is not selected at all in a Pivot Table. The actual optimization made by the DAX engine could be different in every build, and I expect newer builds of Tabular and Power Pivot to be better than older ones. However, we still don’t live in an ideal world, so it could be better trying to help the engine finding a better execution plan.

One student (Niek de Wit) proposed this approach:

Selection :=

IF (

HASONEVALUE ( Period[Period] ),

VALUES ( Period[Period] )

)

Sales :=

CALCULATE (

[Internet Total Sales],

FILTER (

VALUES ( 'Internet Sales'[Order Quantity] ),

'Internet Sales'[Order Quantity]

= IF (

[Selection] = "Current",

'Internet Sales'[Order Quantity],

-1

)

)

)

+ CALCULATE (

[MTD Sales],

FILTER (

VALUES ( 'Internet Sales'[Order Quantity] ),

'Internet Sales'[Order Quantity]

= IF (

[Selection] = "MTD",

'Internet Sales'[Order Quantity],

-1

)

)

)

+ CALCULATE (

[QTD Sales],

FILTER (

VALUES ( 'Internet Sales'[Order Quantity] ),

'Internet Sales'[Order Quantity]

= IF (

[Selection] = "QTD",

'Internet Sales'[Order Quantity],

-1

)

)

)

+ CALCULATE (

[YTD Sales],

FILTER (

VALUES ( 'Internet Sales'[Order Quantity] ),

'Internet Sales'[Order Quantity]

= IF (

[Selection] = "YTD",

'Internet Sales'[Order Quantity],

-1

)

)

)

At first sight, you might think it’s impossible that this approach could be faster. However, if you examine with the profiler what happens, there is a different story. Every original IF’s execution branch is now a separate CALCULATE statement, which applies a filter that does not execute the required measure calculation if the result of the FILTER is empty. I used the ‘Internet Sales’[Order Quantity] column in this example just because in Adventure Works it has only one value (every row has 1): in the real world, you should use a column that has a very low number of distinct values, or use a column that has always the same value for every row (so it will be compressed very well!). Because the value –1 is never used in this column, the IF comparison in the filter discharge all the values iterated in the filter if the selection does not match with the desired value.

I hope to have time in the future to write a longer article about this optimization technique, but in the meantime I’ve seen this optimization has been useful in many other implementations. Please write your feedback if you find scenarios (in both Power Pivot and Tabular) where you obtain performance improvements using this technique!

## Comments

## David Hager said:

Each CALCULATE formula appears to be a separate measure, so could be written as =IS_Current+IS_MTD+IS_QTD+IS_YTD. But I am sure you knew that already :)

## Marco Russo (SQLBI) said:

David, yes you're right, and it would be a more readable pattern with no differences in execution plan. Before writing a pattern, I want to examine in more detail pros and cons of this approach, even if we've seen many scenarios where it's faster, I want to evaluate other possible side effects. But splitting the formula in multiple measures should not affect the query plan.

Thanks!

## Oxenskiold said:

I wonder if converting the IFs into arithmetic expressions will optimize the whole measure even more. I don't have any practical knowledge of what goes and what does not go regarding the storage engine, but if the storage engine can handle the DAX functions FIND() and SIGN() you could try to convert the IF-functions and see if that makes any difference.

(a=s) is similar to FIND(a,s,1,0)

(a<>s) is similar to SIGN(1-FIND(a,s,1,0))

---

(0 equals FALSE; 1 equals TRUE)

So IF ([Selection] = "YTD", 'Internet Sales'[Order Quantity], -1)

Can be converted to:

(FIND([Selection], "YTD",1,0) * 'Internet Sales'[Order Quantity]) + (SIGN(1-FIND(a,s,1,0)) * -1)

As a matter of fact since the FIND() function gives you the option to choose what should be returned in case it doesn't find the string you are looking for you might be able to make do with the following:

(FIND([Selection],"YTD",1,BLANK()) * 'Internet Sales'[Order Quantity])

OR alternatively

(FIND([Selection], "YTD",1,0) * 'Internet Sales'[Order Quantity]) + (FIND([Selection], "YTD",1,-1)

BTW if the IF-condition is a numeric expression you can use these expressions:

(a <> b) is similar to (ABS(SIGN(a - b)))

(a = b) is similar to (1 - ABS(SIGN(a - b)))

(a >= b) is similar to (SIGN(1 + SIGN(a – b)))

(a < b ) is similar to (1 - SIGN(1 + SIGN(a – b)))

(a <= b ) is similar to (SIGN(1 – SIGN(1 – b)))

(a > b ) is similar to (1 - SIGN(1 – SIGN(1 – b)))

---

(0s equals FALSE; 1s equals TRUE)

So

IF([Amountsomething] >= 12000, [SpecialAmount], [plainAmount])

Becomes

(SIGN(1 + SIGN([Amountsomething] – 12000)) * [SpecialAmount]) + ((1 - (SIGN(1 + SIGN([Amountsomething] – 12000)))) * [plainAmount])

Albeit the maintenance of the code might suffer slightly, but you know in the name of optimization. :-)

## Oxenskiold said:

I was a little bit too fast in the writing so here is a CORRECTED version of my former comment.

I wonder if converting the IFs into arithmetic expressions will optimize the whole measure even more. I don't have any practical knowledge of what goes and what does not go regarding the storage engine, but if the storage engine can handle the DAX functions FIND() and SIGN() you could try to convert the IF-functions and see if that makes any difference.

(a=s) is similar to FIND(a,s,1,0)

(a<>s) is similar to SIGN(1-FIND(a,s,1,0))

---

(0s equals FALSE; 1s equals TRUE)

So IF ([Selection] = "YTD", 'Internet Sales'[Order Quantity], -1)

Can be converted to:

(FIND([Selection], "YTD",1,0) * 'Internet Sales'[Order Quantity]) + (SIGN(1-FIND([Selection], "YTD",1,0)) * -1)

As a matter of fact since the FIND() function gives you the option to choose what should be returned in case it doesn't find the string you are looking for you might be able to make do with the following:

(FIND([Selection],"YTD",1,BLANK()) * 'Internet Sales'[Order Quantity])

BTW if the IF-condition is a numeric expression you can use these expressions:

(a <> b) is similar to (ABS(SIGN(a - b)))

(a = b) is similar to (1 - ABS(SIGN(a - b)))

(a >= b) is similar to (SIGN(1 + SIGN(a – b)))

(a < b ) is similar to (1 - SIGN(1 + SIGN(a – b)))

(a <= b ) is similar to (SIGN(1 – SIGN(a – b)))

(a > b ) is similar to (1 - SIGN(1 – SIGN(a – b)))

---

(0 equals FALSE; 1 equals TRUE)

So

IF([Amountsomething] >= 12000, [SpecialAmount], [plainAmount])

Becomes

(SIGN(1 + SIGN([Amountsomething] – 12000)) * [SpecialAmount]) + ((1 - (SIGN(1 + SIGN([Amountsomething] – 12000)))) * [plainAmount])

Albeit the maintenance of the code might suffer slightly, but you know in the name of optimization.

## Marco Russo (SQLBI) said:

Unfortunately the storage engine today doesn't support complex operations. FIND, ABS and SIGN are all functions executed by the formula engine.

In case you execute an iteration function and you have a row context, you see a callback to formula engine made by storage engine. It's not as fast as a native storage engine operation, but at least it could be multi-threaded (useful only for table with 2 million rows or more in Power Pivot, 16 million rows or more in Tabular)

However, when applied to measures, this technique doesn't work in the same way and moves the entire calculation is moved to the formula engine (which is single-threaded in this case), using results obtained by a low number of storage engine queries. This is usually a good idea, but doesn't work well in all situations.

My suggestions is always to optimize DAX only when it's necessary because of performance issues. Trying to optimize in advance might result in a damage to possible optimizations made by future versions of the engine.

## Paul Cunningham said:

Hi Marco

Within the filter, would it make any difference to use:

ALL ( 'Internet Sales'[Order Quantity] )

rather than:

VALUES ( 'Internet Sales'[Order Quantity] )

?

I am curious about whether this removed the need to evaluate the filter context on 'Internet Sales'[Order Quantity], or if it actually costs more to override the filter with ALL?

Thanks

## Marco Russo (SQLBI) said:

VALUES keep the existing filter context, whereas ALL would remove it.

There are not substancial differences, but as I said in the article it would be better using a dedicated column with a single value equal for all the rows - at that point, using VALUES or ALL would not make any difference.

## Geiber said:

The following two measures do exactly the same, but the nested IF solution is faster in a pivot table connected to a SSAS 2012 tabular model. Nested IF statements better than Switch statements, is it possible?

>>USIGN NESTED IF<<

DiscountRate:=IF(ISFILTERED ('DimBuilding'[BuildingID]),

CALCULATE(SUM([DiscountRate])/100, LASTNONBLANK('DATE'[Date], CALCULATE(COUNTROWS('BUILDING_FACT_TABLE')))),

IF(ISFILTERED ('DimBuilding'[Building Name]),

CALCULATE(SUM([DiscountRate])/100, LASTNONBLANK('DATE'[Date], CALCULATE(COUNTROWS('BUILDING_FACT_TABLE')))),

IF(ISFILTERED ('DimBuilding'[Seller]),

CALCULATE(SUM([DiscountRate])/100, LASTNONBLANK('DATE'[Date], CALCULATE(COUNTROWS('BUILDING_FACT_TABLE')))),

IF(ISFILTERED ('DimBuilding'[Status]),

CALCULATE(SUM([DiscountRate])/100, LASTNONBLANK('DATE'[Date], CALCULATE(COUNTROWS('BUILDING_FACT_TABLE'))))

)

)

)

)

>>SING SWITCH STATEMENT<<

DiscountRate:=SWITCH(TRUE(),

ISFILTERED ('DimBuilding'[BuildingID]),

ISFILTERED ('DimBuilding'[Building Name]),

ISFILTERED ('DimBuilding'[Seller]),

ISFILTERED ('DimBuilding'[Status]),

CALCULATE(SUM([DiscountRate])/100, LASTNONBLANK('DATE'[Date], CALCULATE(COUNTROWS('BUILDING_FACT_TABLE'))))

)

## Marco Russo (SQLBI) said:

Geiber, this is strange.

Can you provide me the version of the SSAS Tabular you are running and the SQL Profiler trace of the two executions? (including DAX Query Plan, VertiPaq queries and QueryEnd event). Thanks!

## Geiber said:

Thanx Marco,

It took me a while to prepare dax query plan, vertipaq queries and query end events.

SSAS: Microsoft Analysis Server 11.0.5532.0

To sum up, I use the measure, DiscountRate, with two dimensions, dimBuilding and DimCalendar filtered to 2014-9. Measure duration with nested-if and switch were 297 and 375 respectively in this short scenario. However, duration differences get longer when I used more dimensions along with others measures.

Because profiler trace returned a lot of data, I've shared that here:

https://dl.dropboxusercontent.com/u/18401645/PROFILER.txt

## Marco Russo (SQLBI) said:

Sorry but in this way I cannot see the timing for VertiPaq queries and Duration of formula engine. Can you share the two .TRC files generated by the profiler?

## Geiber said:

Sure Marco!, Here you are: https://www.dropbox.com/sh/71x4dvjep79a0bc/AACEbOXO6wK1-dp15HSWqSLGa?dl=0

You'll find dimension and measure names in Spanish

For instance, DiscountRate is 'PO_TASA_DESCUENTO'.

DimBuilding is 'Inmueble', etc...

Thanx ahead.

## Marco Russo (SQLBI) said:

Geiber,

I received your data and I've seen the issue - I asked some clarification to MS devs but I'm still waiting for an answer - sorry for the delay

## Lewis Hirschfeld said:

I'm looking at a very complex set of conditional statements and was wondering if DAX supports nested SWITCH statements.

## Logan B said:

Marco,

Just curious if you ever revisited and wrote another article about this technique? Do you still believe it is a valid technique without any unintended side effects? I've recently been using it (in Excel 2013) because the initial SWITCH calculation I was using performed poorly if I had too many fields on the pivot table. This technique outlined above seems to perform much better, though it's unfortunate that it looks very inefficient and someone with minimal DAX experience would have a very hard time digesting it.

Thanks,

Logan B

## Marco Russo (SQLBI) said:

@Lewis: yes, you can use nested IF and SWITCH statement. Just be careful about performance.

@Logan: In Excel 2016 (and Power BI and SSAS 2016) this technique is no longer required, and it is actually counterproductive. I suggest you to implement it only if it's really necessary, and to revert to "regular" code once you upgrade to a newer version

## David Cresp said:

Hi Marco,

I am using SWITCH in a large model in PBI current version (Aug 2017) and finding it is slowing things down more than I would like. Testing it in DAX Studio it seems that it is calculating all the measures in the SWITCH formula before returning the answer. I note that you say that the above is no longer necessary. I there a way to optimise SWITCH so that it is not evaluating all of the measures in the SWITCH options?

## Marco Russo (SQLBI) said:

Do you have the same performance using IF? The query plan might be unable to optimize the evaluation, I've seen this in complex measures or reports. I'd like to know whether IF is better, or if the result is the same.

## Rogier W said:

Hi Marco, did you get the answer of your above question?

## Marco Russo (SQLBI) said:

No

## Shawn A said:

Adding my experience to the mix. I'm on SSAS 2016 SP1 CU5, and Excel 2013 is my front-end. I've been using a SWITCH statement to determine which Currency Type to return (there are three possible values: Local Currency, Regional Currency, and US Dollars). This has been working fine until a user added many attributes to the filters and rows.

Replacing the SWITCH statement with nested IF's did not solve the problem. Implementation of the solution as described in the blog post did indeed work.

I'm disappointed that I can't use the SWITCH statement; my code is now much less readable, but at least it works. :) Thanks for the blog post, Marco! I read it a couple of years ago, but it was only today where its use was germane to my situation.

## Marco Russo (SQLBI) said:

Thanks!

## gwp said:

Working with a group of parameterized measures in November 2017 PowerBI Desktop, I observe that:

-SWITCH still appears to evaluate each possible option

-Converting to nested IF's provided no performance benefit

-Converting to the deWit/blog-post FILTER workaround also provided no performance benefit, even using a Very low cardinality column

## Marco Russo (SQLBI) said:

Did you analyze the performance and the query plan in DAX Studio? Are they identical? Maybe the issue is something else. Depending on the conditions of the query, all the branches could be evaluated in both IF and SWITCH. The approach described in this article provides benefits mostly from the storage engine point of view (reducing calls and iterations in FE rather then speeding up SE by itself)

## Chaitanya said:

Hi Marco, MTD measure is working fine at month level but not working at quarter level.

At quarter level , it is showing last month MTD.

Can we make MTD measure to NULL at quarter/year levels ?

Thanks in advance.

## Francisco said:

Marco,

A colleague implemented this switch optimization for a tabular model that I inherited. recently I made a change to the model using Tabular Editor. My findings was that under my account, I was able to query the model using excel and all the variables which had assigned defaults (eg var defaultKey= IF( some condition, value a, value b), however, all of my colleagues would query the results and the engine would not set the defaults accordingly, if they manually selected a filter item, for example value a. then at that point their results behaved like normal.

my work around was revert back to the previous copy of the model (thank you source control). And re-implement my code changes, this second time around while using Tabular Editor, I did not use the DAX formatter tool in Tabular Editor.

please note that my changes were on a calculated measure that was not using the same defaults variable names. It was under the same sales object table, but otherwise was it's own measure referring to other field objects.

when I searched the model.bim for hidden characters I did find that my working copy had added a hidden \r but the previous copy of the model.bim file did not have this \r at the end of the code lines that I had modified.

Is it better to stick to a tool like Visual Studio for editing calculated measures instead of using Microsoft Visual Studio?

## Marco Russo (SQLBI) said:

@Chaitanya: you might try counting the days, e.g.:

IF ( COUNTROWS ( 'Date' ) <= 31, <your current MTD> )

@Francisco: I didn't understand your question!

## Sajid said:

what is the use of -1

## Marco Russo (SQLBI) said:

The -1 is there to make sure there is no value that satisfy the filter (there are no negative quantity values), and the result of the filter is an empty table.

## Tom K said:

FWIW we've raised this issue with Microsoft and just received feedback that a fix should be delivered for this performance issue in June 2018.

## Marco Russo (SQLBI) said:

Thanks for the update!

## Ardy said:

Thanks a lot for this article. I am glad to know that I was not the only one battling with this. I have implemented a variant of this solution and my cube is faster but not as fast as I would like it to be. How I miss multidimensional cube for this! I am really hoping that Microsoft does a proper fix for this in this month. Does Anyone have any update on this fix?