A New Approach To Handling SSRS Multi-Valued Parameters in DAX Queries
Reposted from Chris Webb's blog with the author's permission.
Back in 2012 I wrote a blog post explaining how to handle multi-value parameters in DAX queries in Reporting Services reports. The approach I took back then was basically:
- Generate a string containing a pipe-delimited list of all the parameter values that the user has selected (I did this in DAX too, but to be honest it’s better to use the SSRS Join function to do this outside the DAX query)
- Use the DAX PathContains() function in a Filter() to check whether the value on the current row of the table being filtered appears in the pipe-delimited list
Here’s a deliberately simplified example of how this works based on Adventure Works DW data. The following query filters the FactInternetSales fact table and returns the rows for the Sales Order Numbers that are present in the OrderList variable:
EVALUATE VAR OrderList = "SO43713|SO43758|SO43784|SO43821" RETURN FILTER ( FactInternetSales, PATHCONTAINS(OrderList, FactInternetSales[SalesOrderNumber]) )
The trouble with this approach is that is that it can be very slow. Running a trace in DAX Studio for the query above reveals the problem:
The presence of CallbackDataID shows that the Storage Engine is calling the Formula Engine to handle the use of PathContains() in the filter, and this is often a cause of poor query performance. However back when I wrote the post the only alternative was, as Chris Koester points out here, to dynamically generate the entire DAX query as an SSRS expression and that is very painful to do.
The good news is that recent changes in DAX mean that there is another way to tackle this problem that can give much better performance. Here’s an example of this new approach:
EVALUATE VAR OrderList = "SO43713|SO43758|SO43784|SO43821" VAR OrderCount = PATHLENGTH ( OrderList ) VAR NumberTable = GENERATESERIES ( 1, OrderCount, 1 ) VAR OrderTable = GENERATE ( NumberTable, VAR CurrentKey = [Value] RETURN ROW ( "Key", PATHITEM ( OrderList, CurrentKey ) ) ) VAR GetKeyColumn = SELECTCOLUMNS ( OrderTable, "Key", [Key] ) VAR FilterTable = TREATAS ( GetKeyColumn, FactInternetSales[SalesOrderNumber] ) RETURN CALCULATETABLE ( FactInternetSales, FilterTable )
Broken down variable by variable, here’s how it works:
- OrderList is the pipe-delimited list of key values passed from SSRS
- OrderCount uses the PathLength() DAX function to find the number of parameter values in this list
- NumberTable uses the GenerateSeries() function to create a table of numbers with one row for each number between 1 and the number of parameter values in the list
- OrderTable uses the trick Marco describes here to iterate over NumberTable and, for each row, uses the PathItem() function to return one parameter value from the list for each row in the able
- GetKeyColumn uses the SelectColumns() DAX function to only return the column from OrderTable that contains the parameter values
- FilterTable uses the TreatAs() DAX function to take the table of values returned by GetKeyColumn and treat them as values in the FactInternetSales[SalesOrderNumber] column
- Finally, the query returns the contents of the FactInternetSales table filtered by the values in FilterTable using the CalculateTable() DAX function
There’s a lot of extra code here and in some cases you may find that performance with smaller data volumes is worse as a result, but in this particular case the new approach is twice as fast at the old one. There’s certainly no CallBackDataID:
Chris has been working with Microsoft BI tools since he started using beta 3 of OLAP Services back in the late 90s. Since then he has worked with Analysis Services in a number of roles (including three years spent with Microsoft Consulting Services) and he is now an independent consultant specialising in complex MDX, Analysis Services cube design and Analysis Services query performance problems. His company website can be found at http://www.crossjoin.co.uk and his blog can be found at http://cwebbbi.wordpress.com/ .