Level: Intermediate
In my last article, I introduced the basics of the DAX FILTER() function. Hopefully that introductory explanation to FILTER() helped you get your head around what FILTER() is doing under the hood. With that knowledge in place, let me help you take your understanding to the next level.
Let’s look back at my example from last article
Total Sales to Customers Earning More Than $50k = CALCULATE( SUM(Sales[ExtendedAmount]), FILTER(Customers, Customers[YearlyIncome] > 50000) )
As I explained last time, the FILTER() portion of the formula above is executed one row at a time through the table specified in the first parameter. Technically, we say that the FILTER() function has a row context, and it iterates through the rows in the specified table, one row at a time to complete its task. For this reason, we say that FILTER() is an iterator. There are many other iterating functions in DAX, including SUMX(), MINX() and all the other “X functions”. All iterators have a row context. The easiest way to spot them is they all have a table as the first parameter in the function. A calculated column is also an iterator, but of course there is no table as the first parameter – because IT IS a table.
Now there are certain things that are not outwardly obvious with what I have covered so far.
Even though FILTER() logically executes one row at a time, in reality there are optimisations in the engine that CAN make it more optimised than the simplistic “row by row” iterator concept suggests (not always – you can write bad DAX). Even though there can be optimisations under the hood, you should still follow some basic rules when using FILTER().
- Generally speaking you should not filter a data (fact) table if you can filter a Lookup (Dimension) table instead. In my example, we are filtering a dimension table, so it is not too bad, but as I will show you below, there is actually a better way to write this formula.
- You should always filter a column in preference to a table where possible. The exception is if you are filtering a table with a primary key and need to iterate at the same level of granularity as the primary key. A simple example is a lookup (dimension) table. Generally speaking, if you are iterating every row in a dimension table, it doesn’t matter if you iterate the whole table or the primary key of the table – the efficiency is effectively the same.
Filter Columns in Preference to Tables
Now this is interesting. The syntax for FILTER() requires that the first parameter be a table, not a column. If I place a column in the first parameter, I get an error.
Illegal measure = CALCULATE( SUM(Sales[ExtendedAmount]), FILTER(Customer[Customer Key], Customers[YearlyIncome] > 50000) )
The formula above is illegal because it has a column as the first parameter in FILTER(). This is not allowed. So how can you filter a column? The trick is to turn the column into a virtual table first. The two common ways of doing this are:
- ALL(Table[Column])
- VALUES(Table[Column])
Technically you could use any table function that achieves the same outcome (SUMMARIZE, DISTINCT, ALLSELECTED etc)
Let’s look at the two common approaches above. Which should you use? Well, ALL() and VALUES() do different things.
- The ALL() function will contain all the unique values in the column after removing all the filters affecting the visual.
- The VALUES() function contains all the unique values in the column but RETAINS the filters affecting the visual.
Use the one that does what you need.
Both of these virtual tables will retain lineage to the data model. I talked about lineage in the last article, and I wrote about lineage in depth in this older article. I find the easiest way to think of lineage is to imagine a virtual table that is temporarily living inside the data model and this temp table contains a virtual relationship back to the table from where it was born (hence the term lineage).
Let’s look at one possible modified version of the formula.
Different, but not great formula = CALCULATE( SUM(Sales[ExtendedAmount]), FILTER( ALL(Customers[CustomerKey]), CALCULATE(SUM(Customers[YearlyIncome])) > 50000 ) )
The new formula follows the rule “don’t filter a table if you can filter a column”. But in this case the column and the table have the same cardinality, so there is little benefit there. Also, the new formula requires a second CALCULATE() and SUM() inside the FILTER() function. This is required because the column Customers[YearlyIncome] is no longer in the same table that FILTER() is iterating. The FILTER() function is iterating a virtual, single column table that contains all customer keys in the customer table. The column Customers[YearlyIncome] doesn’t exist in this virtual table, it exists in the Customers table, so you must wrap the column in an aggregation function, SUM() in this case. Further, as the FILTER() function iterates in a row context through the virtual table, the virtual relationship does not filter the connected tables UNLESS you specifically tell the formula to do so. Technically, to make the filter propagate from the new virtual table created by ALL(Customers[CustomerKey]), we need to convert the row context into an equivalent filter context via context transition. Context transition is triggered by the inner CALCULATE() inside the FILTER() function in this case.
Ok, this formula works, but it is probably no more efficient than the first one that referred to the Customers table (maybe worse due to all those context transitions). It is iterating the same number of rows, plus we now have a context transition required for each step. I generally would not write this formula in preference to the first.
A Much Better Formula
Ok, so what then? Ideally we need to find a column with a lower level of granularity that we can iterate through (less often), that will give us the same result. The prime candidate in this case is the Customers[YearlyIncome] Column as shown below.
Much better formula = CALCULATE( SUM(Sales[ExtendedAmount]), FILTER(ALL(Customers[Yearly Income]), Customers[Yearly Income] > 50000) )
This new formula is much better and ticks all the boxes.
- It iterates over the smallest granularity table possible to accurately complete the test – in this case the table is a single column table containing all the unique possible income bands for all customers. Because we are testing for income, we can deal with all the customers with the same income at once – this is FAR superior to dealing with each customer individually.
- This time the Customers[YearlyIncome] column is in the same table that we are iterating, so no need for another CALCULATE() to force context transition at each step of the journey.
- The outer CALCULATE() performs the final context transition between the virtual FILTER Table and the rest of the model at the end of the process.
Now, Back to Simple Filters
OK, now back to the beginning. Last week I told you that there were 2 types of filter parameters inside CALCULATE(). Here is a refresher.
There are 2 ways you can add filter parameters inside CALCULATE():
-
As a simple filter:
-
Table[Column] compared to some value, eg
-
Customers[YearlyIncome]>50000
-
Product[Category]=”Accessories”
-
-
As a table filter:
-
A table function is added as the filter. The table function is evaluated first, and the results in that table are used as a filter. eg
-
FILTER(Customers,Customers[YearlyIncome]>50000)
-
ALL(Products)
-
Up until now, I have only been talking about the second type of filter parameter – the table filter. So let me come back to the simple filter. The simple filter is just a trick – it is syntax sugar!
Simple Filter Version (syntax sugar) = CALCULATE( SUM(Sales[ExtendedAmount]), Customers[Yearly Income] > 50000 )
When you write the above formula, what ACTUALLY gets executed by CALCULATE() is the following:
Actual Formula Executed Under the Hood = CALCULATE( SUM(Sales[ExtendedAmount]), FILTER( ALL(Customers[Yearly Income]), Customers[Yearly Income] > 50000 ) )
So why did Microsoft do this? Because they wanted to make it simple to do regular tasks that would be otherwise hard to do. Imagine if you had to learn about FILTER() and ALL() to efficiently write this formula? The barrier to learning for beginners would be too high, hence the concept of syntax sugar was born to help you do complex tasks easily.
What Next?
Ok, so if you like the way I teach and explain concepts, you can learn from me in a structured way. You may like to consider reading my book, participating in one of my online training courses, or attending a live Australian training course, or have me come into your organisation for a custom live in house training course.
Hi Matt,
This article clearly explains to understand the difference between Simple Filters and Filter table function. I have two questions.
A simple filter internally uses Filter and All functions. So, in terms of 029 updates, it uses FILTER and REMOVEFILTER functions internally. This is just for my understanding.
Now, what happens internally if I use KEEPFILTERS with a simple filter expression. Something like
Calculate([Sales Amount],KEEPFILTERS(Products[Color]=”Red”))
Thanks,
Charu Ambekar
REMOVEFILTERS() is just syntax sugar for ALL(). It is ALL() that is used.
Calculate([Sales Amount],KEEPFILTERS(Products[Color]=”Red”)) is the same as
Calculate([Sales Amount],KEEPFILTERS(FILTER(ALL(Products[Color]),Products[Color]=”Red”)))
Thanks Matt,
My Running Total for a specific product doesn’t work…
The regular running total works fine:
Running Total = CALCULATE(SalesAmt,FILTER(ALL(Calendar),Calendar[Date]<=MAX(Calendar[Date]))
But when I do:
Running Total Apples = CALCULATE(Running Total,FILTER(SalesFact,SalesFact[Product]="Apples"))
then it returns regular Apples sales, not running total
Only when I filter on a product dimension it is working.
But I MUST filter on my Fact in my real world scenario…
How can I do it?
Please help
Thank you
Michael
Hi Michael,
The issue you’re experiencing is because of the way Calculate and Filter work, and how filters are applied to tables.
I have a couple of solutions for you, which I will reveal in a minute, but first, I’m not really clear on why you need to filter directly on your fact table. This is not good practice and should be avoided if possible, as complications will arise with your calculations as your model complexity increases if you are filtering on the Fact table(s) directly.
The DAX language is designed to work with dimension tables that apply filters to your fact table data through relationships. Where possible, working with flat tables (i.e. directly on your fact tables) should be avoided.
Okay, now let’s explore some potential solutions to your problem.
First, let’s look at modifying your formula:
Running Total Apples = CALCULATE([Running Total], FILTER(Sales, Sales[Product]="Apples"))
This formula doesn’t work because it doesn’t take into consideration the initial filter context coming from your visuals. In order to address this, you need to adjust the formula to first remove any filters that have been applied on your “Apples” column. Thus:
Running Total Apples = CALCULATE([Running Total], FILTER(ALL(Sales[Product]), Sales[Product]="Apples"))
There is however syntax sugar that you could take advantage of in this instance, and simplify your formula as follows:
Running Total Apples = CALCULATE([Running Total], Sales[Product]="Apples")
You don’t need to use Filter in this measure, because you’re looking to Calculate your [Running Total] expression against a simple filter context; a fixed value in a table column.
The syntax sugar then fills in the missing Filter component of the formula.
While this solution should yield the answer you seek, you might want to also consider an alternate approach. Rather than calculating Running Total, and then trying to apply filters to it, I would instead calculate Total Apple Sales, and then apply a running total filter to that.
Total Apple Sales = CALCULATE([Total Sales], Products[Product]="Apples"
and then calculate
Running Total Apples = CALCULATE([Total Apple Sales], FILTER(ALL('Calendar'), 'Calendar'[Date] <= MAX('Calendar'[Date])))
If you're feeling brave you could achieve this all in one step, by adding two filters to your CALCULATE.
Running Total Apples = CALCULATE([Total Sales], Products[Product]="Apples", FILTER(ALL('Calendar'), 'Calendar'[Date] <= MAX('Calendar'[Date])))
This would be my preferred approach to the solution, but as you said you need to filter the fact table directly, so this formula could instead be applied as:
Running Total Apples = CALCULATE([Total Sales], Sales[Product]="Apples", FILTER(ALL('Calendar'), 'Calendar'[Date] <= MAX('Calendar'[Date])))
I hope this fully answers your question, and leads you to a fruitful harvest.
Hi Matt, I have come to your page hunting for filtering sales by multiple dimension tables. Please consider problem of filtering Sales table by dimension table colors and dimension table sizes. How to filter products with color=red OR size=big? Would you be so kind to direct me anywhere or give a hint? How to achieve this using FILTER and KEEPFILTERS? Greetings Przemyslaw Remin
I believe this is what you need
=
CALCULATE (
[your measure],
DISTINCT (
UNION (
FILTER ( ALL ( product[colour] ), product[colour] = “red” ),
FILTER ( ALL ( product[size] ), product[Size] = “big” )
)
)
)
Thank you. This is amazing approach.
Thanks Matt. I have used Filter quite a bit, but I see now I really never understood it completely. Many times I would get it to work without complete understanding. The concept of the virtual table has brought much clarification.
One of the simplest yet effective explanation i’ve read so far abou FILTER.
Thanks!
Great article, I didn’t know that I could make a less expensive formula using ALL to filter by a column instead of the whole virtual table. BTW I did read your book Learn to write DAX, I highly recommend it.
I was struggling to learn DAX function, I was looking for good resource where I can learn and utilize the same in my day to day working life. Their are many paid and free resources are available in internet.I found your study material which is really helpful to learn easy and connect to my work. Your examples practical base.
Thanks Matt for your great help.
Excellent post. I am from Mexico, I bought your book and is great too.
Hi Matt, love the way you’ve explained this!
When talking about the Much Better Formula you say “in this case the table is a single column table containing all the unique possible income bands for all customers”. This might seem strange to some readers as the definition of ALL clearly states you get all the rows in the table/column with no filters.
So here i assume ALL(Customers[Yearly Income]) implies a unique list of income bands since the compressed column in VertiPaq internally contains the unique values.
Am i following your logic correctly here?
Hey Bhavik. You are exactly right. It is the fact that the Vertipaq engine stores data in a compressed way in columns, not rows, that makes this the preferred way to filter the data. We are helping the engine filter the data the best way possible. A good way to visualise this is to create a new table in Power BI using the New Table button. Here you CAN materialise the table so you can “see” it. Table = ALL(Customers[YearlyIncome]) will display a distinct list of all the incomes. The other way to “see” it is to go to the data view and click the drop down filter at the Customers[YearlyIncome] column – it is the same list.
Awesome Explanation!! Thank you so much Matt. This article helped me to understand how the engine behaves when using “All” within a filter() which was a question inside my head for ages.