Concluding my series of blog posts on seemingly harmless things you can do in a Power BI report that can lead to performance problems and the “This visual has exceeded the available resources” error (see also “Calculate(), Filter() and DAX memory usage” and “DAX measures that never return blank“), in this post I’ll show how table visuals with vertical scrollbars that potentially show thousands of rows can be a Bad Thing.
Using the Import mode semantic model I’ve used in the last two posts, consider this table visual:

The two measures displayed are trivial counts and averages. However there are 538753 combinations of County and Postcode that have data, and so in theory the table can display 538753 rows if you scroll down far enough. That can’t be good, can it?
Those of you who know how Power BI generates DAX queries for table visuals like this might disagree, though. Here’s the DAX query for this table visual:
DEFINE
VAR __DS0Core =
SUMMARIZECOLUMNS(
'Property Transactions'[County],
'Property Transactions'[Postcode],
"Count_Of_Sales", 'Property Transactions'[Count Of Sales],
"Average_Price_Paid", 'Property Transactions'[Average Price Paid]
)
VAR __DS0PrimaryWindowed =
TOPN(501, __DS0Core, 'Property Transactions'[County], 1, 'Property Transactions'[Postcode], 1)
EVALUATE
__DS0PrimaryWindowed
ORDER BY
'Property Transactions'[County], 'Property Transactions'[Postcode]
As you can see, the query contains a TOPN filter which means that it actually only returns the first 501 rows out of those potential 538753. It’s only if you scroll down far enough that another query is triggered to get the next 501 rows. So maybe it’s not so bad?
Actually it can be bad, even with the TOPN filter. Here are the Execution Metrics for the query above:
{
"timeStart": "2024-07-10T22:54:30.330Z",
"timeEnd": "2024-07-10T22:54:31.064Z",
"durationMs": 734,
"vertipaqJobCpuTimeMs": 219,
"queryProcessingCpuTimeMs": 516,
"totalCpuTimeMs": 734,
"executionDelayMs": 0,
"approximatePeakMemConsumptionKB": 66599,
"commandType": 27,
"queryDialect": 3,
"queryResultRows": 501
}
Notice the durationMS and approximatePeakMemConsumptionKB metrics, which tell you how long the query takes to run and how much memory it uses.
Now consider the following version of a report with a slicer for the County column and the county “Bath and North East Somerset” selected.

Here’s the DAX query for the table visual now:
DEFINE
VAR __DS0FilterTable =
TREATAS({"BATH AND NORTH EAST SOMERSET"}, 'Property Transactions'[County])
VAR __DS0Core =
SUMMARIZECOLUMNS(
'Property Transactions'[County],
'Property Transactions'[Postcode],
__DS0FilterTable,
"Count_Of_Sales", 'Property Transactions'[Count Of Sales],
"Average_Price_Paid", 'Property Transactions'[Average Price Paid]
)
VAR __DS0PrimaryWindowed =
TOPN(501, __DS0Core, 'Property Transactions'[County], 1, 'Property Transactions'[Postcode], 1)
EVALUATE
__DS0PrimaryWindowed
ORDER BY
'Property Transactions'[County], 'Property Transactions'[Postcode]
As you would expect, this query now contains a filter on the county selected in the slicer. It also contains a TOPN filter but the interesting thing is that since there are more than 501 postcodes in the selected county, the query returns exactly the same rows as before. Of course you can’t now scroll down and see data for other counties than the one selected in the slicer but you can always select a different county in the slicer.
Here are the Execution Metrics for the new query:
{
"timeStart": "2024-07-10T22:58:29.725Z",
"timeEnd": "2024-07-10T22:58:29.741Z",
"durationMs": 16,
"vertipaqJobCpuTimeMs": 0,
"queryProcessingCpuTimeMs": 16,
"totalCpuTimeMs": 16,
"executionDelayMs": 0,
"approximatePeakMemConsumptionKB": 2564,
"commandType": 27,
"queryDialect": 3,
"queryResultRows": 501
}
Notice that both the duration and peak memory usage of the query are both much, much lower: duration has gone from 734ms to 16ms, while peak memory usage has gone from 66599KB to 2564KB. If you have more measures which are more complex and/or inefficiently written the difference could be much bigger.
Why is there such a big difference? Well if you look at the physical query plan generated for both queries you can see that before the TOPN filter is applied, the Vertipaq engine still has to spool the entire, unfiltered table. In the first query this unfiltered table has 538753 rows, which is why it’s relatively slow and memory hungry, whereas in the second query this table, with the County slicer, has only 1869 rows (the number of postcodes in the selected county). As a result slicing on County, even though it doesn’t affect which rows are returned after TOPN filter is applied, leads to a more efficient query. If you’re using DirectQuery mode something similar happens because, as I noted here, the TOPN filter can only be pushed back to the relational source in a few basic scenarios which means the SQL queries generated can return a very large number of rows and which can hit the Max Intermediate Row Set Count limit.
What can you learn from this? Table visuals that can display a large number of rows should be avoided. I’m not report design guru but I would go so far as to say that any table with a scrollbar on is a mistake: if your user needs to scroll down to see all the rows or scroll right to see all the columns, why not use a slicer to allow them to select the rows they want to see more easily or use field parameters to allow them to select the columns they want to see? Not only will this be better from a usability point of view, performance will be a lot better and memory usage will be lower. And yes, I know that your end users ask for tables with lots of rows and columns because that’s what they are used to seeing in Excel, but giving them what they want risks slow performance and memory errors. So they need to make a choice.


















































