VERSICH

Power BI Performance Optimization: 10 Mistakes That Make Reports Slow

power bi performance optimization: 10 mistakes that make reports slow

Introduction

We have opened up a lot of Power BI reports over the years, and a good number of them share the same problem. The dashboard looks great in the demo, then a few months later someone opens it on a Monday morning and it takes twenty seconds to load a page that should take two. Filters lag. Slicers freeze for a moment before the visuals catch up. Refreshes that used to finish in ten minutes start taking an hour, and nobody can point to the exact day it got bad.

In our experience, slow Power BI reports are rarely caused by one dramatic mistake. They are usually caused by a handful of small decisions made early in the build, decisions that seemed harmless at the time but compound as the data grows. A model built for a fifty thousand row dataset behaves very differently once it hits five million rows, and most of the choices that hold up fine at small scale start to break down long before anyone notices.

This blog walks through ten of the most common mistakes we see in Power BI reports that end up slow, heavy, or unreliable. Some of these are modeling choices, some are DAX habits, and some are just things that get skipped under deadline pressure. We have grouped them the way we would explain them to a client, in plain language, with the reasoning behind each one so the fix makes sense rather than feeling like a rule handed down from nowhere. If you manage Power BI reports for your organization, or you are the person who gets the Slack message when a dashboard freezes, we think you will recognize at least a few of these.

Signs Your Report Already Has a Performance Problem

Before we get into the ten mistakes themselves, it is worth naming what a performance problem actually looks like from the outside, because it rarely announces itself clearly. It usually shows up as a series of small complaints that get dismissed individually before someone connects them. A visual takes a beat longer to render than it used to. A slicer selection lags for a second or two before the page catches up. The scheduled refresh that used to finish before the morning stand up now finishes closer to lunch, and the gateway logs show a timeout every few days that someone quietly reruns and forgets about.

None of these on their own feels urgent, which is exactly why they tend to pile up. We have walked into more than one client environment where the report had been slow for months, but because it degraded gradually, nobody had flagged it as a real issue. It had just become the accepted normal, the thing people worked around by exporting to Excel instead of trusting the dashboard to load in time for a meeting.

If any of that sounds familiar, the ten mistakes below are the most likely places to look first. We have ordered them roughly the way we would investigate a slow report ourselves, starting with the data model, moving into the query layer, and finishing with the habits that catch problems before they reach production.

Mistake 1: Importing Every Column and Every Row Instead of What the Report Needs

We see this one the most, by far. Someone connects Power BI to a source system and just pulls the whole table in, every column, years of history, all of it, because trimming it down feels like extra work up front and it is easier to grab everything now than come back for one more column later.

It rarely feels like a problem on day one. It becomes a problem three months in, once the model has grown past what anyone actually queries. Power BI's engine compresses data well, but it still has to store and scan whatever you hand it. We looked at a transactions table not long ago with eighty odd columns where the visuals only ever touched twelve of them. Seventy percent of that model was dead weight, sitting there on every refresh and every calculation. History works the same way. If the reporting only ever needs three years, but the source table goes back a decade, you are paying a performance tax on rows nobody will ever filter to.

Before we build a model now, we map out which columns each visual and measure will actually touch, and we agree a cutoff on history with whoever owns the report. Anything that only existed for a lookup during transformation gets dropped once it has done its job. Not glamorous work, but trimming an import from eighty columns to twenty can cut refresh time in half, sometimes more.

Mistake 2: Using DirectQuery When Import Mode Would Serve Better

DirectQuery is not the villain here. If data changes by the minute and people genuinely need it live, or the dataset is too big to import at all, it is the right call. What we run into more often is DirectQuery used out of habit, on a report where the source only changes once a day, because it was the default someone picked and never revisited.

Here is the catch. Every click in a DirectQuery report, a slicer, a filter, a page switch, sends a fresh query to the source. Your report's speed is now capped by the source system's speed, plus whatever the network adds on top. And if that source database is also handling transactional work during the day, your reporting users and your operational users are fighting over the same resources. Neither side wins.

Import mode puts the data into Power BI's own compressed engine, which is built for exactly this kind of query. A scheduled refresh a few times a day covers most reporting needs, and the report itself feels dramatically faster because it stops waiting on a round trip somewhere else. We default to Import and save DirectQuery, or a composite model that blends both, for the cases where live data is a real requirement and not just a preference.

Mistake 3: Skipping a Proper Star Schema and Relying on One Flat Table

A lot of reports start as one wide table because that is how the data showed up, one big export from an ERP or a spreadsheet somebody was already maintaining. Fine for a proof of concept. The trouble starts when that flat table quietly becomes the permanent foundation of a production report and nobody ever circles back to restructure it.

A flat table repeats the same descriptive detail, customer name, category, region, on every single transaction row. That repetition costs more than people expect, because Power BI's compression works best on columns with fewer distinct values, and endlessly repeated text fights against that. DAX has to work harder too, since there is no clean line between what you are measuring and what you are slicing by.

A star schema fixes this: one fact table for the numbers, a handful of dimension tables for date, customer, product, region, and so on. Old pattern, still holds up. It shrinks the model, speeds up filtering, and makes relationships predictable instead of tangled. When we inherit a struggling report, turning a flat table into a star schema is often the single change that produces the biggest jump in speed, and we usually see it before we have touched a single DAX formula.

Mistake 4: Writing Calculated Columns When a Measure Would Do the Job

Calculated columns and measures both run on DAX, and it is easy to grab whichever one you thought of first without weighing the difference. They behave very differently under the hood though, and mixing them up is one of the more common reasons a model ends up heavier than it should be.

A calculated column gets computed once, at refresh, and the result sits in the model permanently for every row, taking up memory and adding to file size whether anyone ever looks at it or not. A measure calculates on the fly, only for the specific context a visual asks about, and nothing is stored. If you need something row specific to filter on or build a relationship against, a calculated column is the right call. Total sales, average margin, year over year growth, that is measure territory almost every time.

We regularly open models where basic aggregations got built as calculated columns, usually because that was the first pattern someone learned and it stuck. Converting those to measures shrinks the stored model and shifts the calculation to only run when it is actually needed. Small conceptual shift, but it pays off across the whole file.

Mistake 5: Overusing Bidirectional Relationships

Relationships control how filtering flows between tables. By default that flow runs one way, from a dimension table down to a fact table. Bidirectional filtering lets it run both ways, and it genuinely earns its keep in specific situations, many to many relationships between certain dimension tables being the usual example.

The trouble is when it gets switched on across the model as a general habit, often because a slicer was not filtering the way someone expected and bidirectional felt like the fastest fix. Every bidirectional relationship adds another filter path the engine has to work through, and stack a few of them together and the engine ends up doing noticeably more work per query. Sometimes it introduces filter paths that are genuinely ambiguous, and the numbers on screen become hard for anyone to explain with confidence.

We keep relationships single direction by default and only turn on bidirectional filtering when there is a specific, documented reason for it. If a slicer misbehaves, we look at the model structure or the measure logic first. It fixes the symptom while quietly slowing down every query in the model, which is not a trade we make lightly.

Mistake 6: Packing Too Many Visuals Onto a Single Page

Less about the data model, more about layout, but the performance hit is just as real. Every visual on a page fires its own query. Four or five well chosen visuals is manageable. Twenty small ones, cards and mini charts and matrices crammed in because someone wanted everything visible at once, means twenty queries firing together every time the page loads or a filter changes.

We get the instinct. Stakeholders like a single page that shows everything, and it feels more efficient than clicking between tabs. But the cost compounds with every added slicer, since each one triggers a fresh round of queries across every visual on the page. Users end up waiting, and a report can feel unreliable even when the model underneath it is well built.

We design with a rough ceiling in mind, usually six to ten visuals a page depending on complexity, and split anything denser into a summary view with drill through pages underneath. Bookmarks and drill through let people get to detail without loading everything at once. Reports get faster and, honestly, easier to use, since nobody is hunting through a wall of charts for one number.

Mistake 7: Breaking Query Folding in Power Query

Query folding does not get talked about enough because it happens quietly in the background. When the source supports it, Power Query translates your steps back into a native query, SQL for example, that runs on the source server. Filtering, grouping, basic transforms all happen where the data already lives, and only the result travels across the network.

Folding can break without anyone noticing. Adding an index column, referencing a custom function, certain merges done in the wrong order, any of these can stop the chain. Once it breaks, every step after that point runs locally on the machine or gateway doing the refresh, pulling the full unfiltered dataset first and filtering it afterward. Barely matters on a small table. On a fact table with millions of rows it can turn a five minute refresh into an hour.

We check the query plan on the major queries during model review, right clicking a step to see whether View Native Query is still available. Where folding breaks earlier than it should, we reorder so filtering and column selection happen first, while folding is still active, and push whatever cannot fold to the very end. Worth building into the habit, not something you only check once refreshes start complaining.

Mistake 8: Writing DAX That Forces Row by Row Evaluation

DAX gives you plenty of rope, including iterating functions like SUMX, FILTER, and CALCULATE with heavy filter arguments. Used with care, these are genuinely powerful. Used carelessly, they force the engine to evaluate row by row across a table instead of using the columnar, set based operations it was actually built for.

A common one is wrapping CALCULATE around a FILTER that scans the whole fact table row by row, when a simpler boolean filter would get the same result through the engine's native filtering. Nesting SUMX inside SUMX is another. So is referencing a measure inside an iterating function in a way that recalculates the same value over and over instead of once. These patterns often test fine on a small sample and then crawl once the table hits real production volume.

Fixing it usually means leaning on the engine's strengths, boolean filters over row by row FILTER where you can get away with it, variables to store intermediate results so they compute once, and iterating functions reserved for cases that genuinely need row context. We also test DAX against realistic data volumes rather than a small sample, since a lot of DAX performance problems simply do not show up until the table size matches what production will actually be.

Mistake 9: Skipping Aggregation Tables for Large Fact Tables

Mostly a large report problem, once fact tables run into the tens or hundreds of millions of rows. Most dashboards lean on summary visuals, a monthly trend, a total by region, a year over year comparison. If the model only holds transaction level detail, every one of those summary visuals still has to scan and aggregate the full table to produce what is, underneath it all, a simple sum.

Aggregation tables pre-summarize the data at the grain your visuals actually need, monthly totals by region for instance, and Power BI automatically redirects a query to the smaller table whenever it can, falling back to full detail only when someone drills into something the aggregation cannot answer. The visuals people check most often end up loading from a table that might be a fraction of the full detail's size, while drill through still works when it is genuinely needed.

Not every report needs this. On a smaller fact table the added modeling complexity outweighs the gain. But once a fact table grows large enough that summary visuals start to drag, aggregation tables are one of the more effective tools we reach for, and they are worth setting up before performance becomes a visible complaint rather than after.

Mistake 10: Publishing Without Ever Running Performance Analyzer

Power BI Desktop ships with a built in Performance Analyzer that shows exactly how long each visual on a page takes to render, broken down into DAX query time, visual display time, and other processing. One of the most useful diagnostic tools in the whole product. Also one of the least used, in our experience, mostly because nobody thinks to open it until users are already complaining.

That is the real mistake, treating performance as something you deal with after the fact, once a report is live and someone flags that it feels slow. By then you are debugging in production with no clear baseline to compare against, and the fix has to happen under pressure with users already frustrated. Performance Analyzer flips that, letting you catch slow visuals and heavy queries before anything reaches a client.

We run it on every page before a report ships, jot down load times for each visual, and treat anything noticeably slower than the rest of the page as worth investigating before we publish. We keep those baseline numbers on file too, so if a report slows down months later we have something concrete to compare against instead of guessing where the regression crept in. Takes a few extra minutes before every release. Saves far more than it costs.

Testing at Realistic Scale, Not Sample Size

One theme runs underneath several of the mistakes above, and it deserves its own mention. A lot of Power BI performance problems do not exist during development. They exist only once real data volume shows up, and by then the report is already live and people are already relying on it.

It is common to build and test a report against a development dataset with a few thousand rows, because that is what is available or because pulling the full production volume into a dev environment feels like overkill early on. The report feels fast, everyone signs off, and it ships. Then it connects to the real source, with a few million rows of transaction history, and behavior nobody saw in testing starts to appear. A DAX pattern that looked fine at small scale turns out to scale badly. A relationship that seemed harmless starts producing ambiguous filter paths once there are enough distinct values on both sides.

Our practice is to test performance against a dataset that is as close to production volume as we can reasonably get, even if that means asking for a larger data extract earlier in the project than a client initially expects to provide one. It adds a bit of upfront friction, but it is far cheaper than discovering a scaling problem after go live, when fixing it means reworking a model that people are already actively using for daily decisions.

Quick Reference: Mistakes and Fixes at a Glance

For anyone skimming this before a meeting, here is the short version of everything above, laid out side by side.

MistakeWhy It Slows Reports DownWhat To Do Instead
No star schemaWide flat tables force the engine to scan far more data than a proper model needsSplit data into fact and dimension tables
Calculated columns instead of measuresCalculated columns are stored and computed at refresh, bloating the modelUse measures so DAX calculates only what is asked for, when it is asked for
Bidirectional relationships everywhereFilters travel in both directions and the engine has to work out extra filter pathsKeep relationships single direction unless there is a specific reason not to
Too many visuals per pageEvery visual fires its own query, so a crowded page multiplies query loadLimit visuals per page and split dense reports across pages
Query folding broken in Power QueryTransformations run locally instead of at the source, so refreshes crawlCheck the query plan and keep folding-friendly steps at the top
Row-by-row DAXIterating functions like FILTER inside CALCULATE force row-level evaluation across large tablesFavor set-based DAX patterns and filter with simpler boolean expressions where possible
No aggregation tablesEvery visual hits the full-grain fact table even for high-level summariesBuild aggregation tables for common summary levels
Skipping Performance AnalyzerSlow visuals and queries ship because nobody measured them before releaseRun Performance Analyzer before every publish and record baseline numbers
Importing every column and rowBloats the data model and forces Power BI to scan data nobody queriesImport only the columns and the date range the report actually uses
DirectQuery used by defaultEvery visual sends a live query to the source, so the report is only as fast as the sourceUse Import mode unless the data truly has to be live

How We Approach Power BI Performance at Versich

When we take on a Power BI project, whether it is a new build or a report handed to us because it has become slow and unreliable, performance is not a step we tack on at the end. We treat it as part of the modeling decisions from the very first conversation about what data the report actually needs to answer.

In practice, that means we scope the data before we import it, we default to Import mode and star schema modeling unless there is a clear reason to deviate, we write DAX with an eye toward how it will behave at production scale rather than on a sample dataset, and we run Performance Analyzer as a standard step before anything goes out the door. When we inherit an existing report that has slowed down over time, we usually start with a model audit, tracing through the same ten areas covered in this blog, before touching a single visual, because in our experience the root cause is almost always sitting in the model or the query layer rather than in the report canvas itself.

We have rebuilt reports for clients where page load times dropped from twenty or thirty seconds down to two or three, and refresh times that used to run overnight finished in under an hour, without changing what the report actually showed to end users. The visuals looked the same. The data was the same. What changed was everything happening underneath, and that is usually where the real performance gains are hiding.

We also try to leave a client's team in a position to keep the model healthy on their own, since performance is not a one time fix. Data grows, new columns get requested, new visuals get added, and small decisions made six months from now can quietly undo the work put in today. Documenting the model, explaining the reasoning behind the structure, and flagging the patterns to avoid going forward tends to matter just as much as the initial rebuild.

Conclusion

None of the ten mistakes covered here are exotic. They are common, they are easy to make under a deadline, and most Power BI builders have made at least a few of them at some point, ourselves included when we were still learning the tool. The good news is that none of them require a rebuild from scratch to fix. A model audit, a few structural changes to how data is imported and related, some DAX cleanup, and a habit of checking Performance Analyzer before publishing will resolve the majority of the slowness we see in the reports that come across our desk.

If your Power BI reports have started to feel heavy, or you are building something new and want it fast from day one, this is the kind of work we do regularly through our Power BI Services. We offer dedicated Power BI Consulting Services for teams planning a new report or dashboard, and ongoing Power BI Support Services for organizations that need someone keeping an existing model healthy over time.

If your Power BI environment pulls data from an ERP, our NetSuite and Power BI Integration Services page covers how we handle that connection without turning it into the source of your performance problems. You can also look through our Power BI Case Studies to see how this has played out for other clients, or browse our wider Data and Technology services if reporting is only one piece of a larger data puzzle you are working through.

If you want a second opinion on a report that has gotten slow, or you are starting a new Power BI project and want the model built right the first time, get in touch with us and we will be glad to take a look.