Slow reports get blamed on complex measures. Complex measures get rewritten. The report is still slow.
The reason is that DAX is rarely the actual problem. The actual problem is a flat table with 47 columns, many-to-many relationships nobody quite remembers setting up, and bidirectional filters doing things the original author didn't intend. The measures are dancing as fast as they can on a model that won't let them move. Tuning the dance doesn't help when the floor is the issue.
Why people start with the DAX
Because DAX is where the visible work lives. The measures have names. They have author history. They sit in the model with little fx icons that scream "I am the calculation, audit me." When something is slow, the eye lands on the measures because they're the part you wrote on purpose.
The data model, by contrast, mostly happened. Someone imported a wide table from a system extract because that's what was available. Someone else added a related lookup. A third person turned on a bidirectional filter to make a slicer work, then never turned it off. Nobody wrote any of this; it accumulated. Performance reviews tend to skip the parts that accumulated without authorship, because there's no obvious thing to attribute the problem to.
DAX also gives you the satisfying illusion of control. Refactoring a CALCULATE feels like making the report faster. The query plan changes, you can tell yourself the SUMX is cleaner now, and you've done something. The benchmark, if you ran one, would tell you a different story.
Why a bad model dwarfs bad DAX
The Vertipaq engine is enormously good at scanning star-schema-shaped data. Compressed columns, a dimension table joined to a fact table on a single key, no cycles in the relationship graph - this is the shape the engine was built around. On data that fits this shape, even sloppy measures run in milliseconds.
Take the same engine and point it at a single wide flat table - the kind that comes out of "I exported this from the operational system as one big extract" - and the compression falls apart. High-cardinality string columns balloon the dictionary size. Repeated values in what should be dimensions multiply the storage. Aggregations that should hit a small dimension and a narrow fact instead scan every row of a wide structure. The work the engine is doing per query goes up by a factor that varies between 5x and 50x depending on how flat the table is and how varied its columns are. No amount of DAX cleverness recovers this.
Many-to-many relationships do something specifically nasty. Every time a measure crosses one, the engine has to materialise a temporary table that resolves the relationship, and that work happens during query execution rather than at refresh time. One M2M with a small bridge is fine. Three M2Ms with bidirectional filters chained across the model is a query plan that looks like a small graph theory problem.
Bidirectional filters are the silent slow-down. The original use case is real - cross-filtering a dimension off a fact for a specific slicer behaviour - but the cost is hidden. Every measure that touches that relationship now has to consider filter propagation in both directions, which means the engine considers more candidate plans, picks one that's often suboptimal, and pays for the propagation per query. Many models have three or four bidirectional filters turned on speculatively, none of which the report needs. Each one costs.
What "fix the model" actually means
The honest version of this advice is unglamorous, because the work is structural rather than clever.
Split the wide flat table into a star. Pull the dimensional columns - customer attributes, product attributes, calendar attributes - into separate tables, related to the fact on integer surrogate keys. The fact table shrinks to keys and measures. The dimensions live in their own narrow tables. Compression takes care of the rest.
Default every relationship to single direction unless there is a specific, named reason for bidirectional. If you can't articulate the reason in one sentence, the relationship is single-direction. The exceptions are rare and usually involve a single bridge table for an unavoidable many-to-many.
Reduce many-to-many to one-to-many wherever the data shape allows it. Most M2Ms exist because nobody built the bridge table, or because the bridge table was avoided to skip a refresh step. The bridge is faster than the M2M in almost every case.
Date columns belong on a date dimension, not on the fact table. Slicers, filters, and time intelligence functions all target the date dimension. The fact table's date column exists for the relationship and nothing else. This is the same point the DAX Time Intelligence post closes on, and it's the same fix.
Once the model is shaped right, the DAX usually doesn't need rewriting. The measures that were "complex" were complex because they were compensating for the model. Once the model carries its own weight, the measures relax.
The closing point
A model in star schema shape with single-direction relationships will outperform a model with a flat table and clever DAX by a margin that has no chart axis. The first model is doing the right work at refresh time and the easy work at query time. The second model is doing the wrong work at query time and asking the DAX to apologise for it.
DAX optimisation is the last thing to reach for, not the first. The first move is to look at the model and ask whether it is the shape Vertipaq was built to run on. If the answer is no, no measure rewrite will save you.
For the deep-dive on what the star schema actually changes about query behaviour - with a real before-and-after on a model that was performing badly - see the May 21 Between the Sheets. The full series on building a clean star schema from scratch starts in June.
Frequently Asked Questions
How do I know if my model is the problem and not my DAX?
Open Performance Analyzer in Power BI Desktop and capture a slow visual. Look at the breakdown - if "DAX query" dominates the time, the measure might be a candidate for optimisation. If "Storage Engine" dominates and the engine is doing massive scans, the model is the issue. Most slow reports show Storage Engine as the lion's share when the model is the cause.
Can't I just import the wide flat table and use it directly?
You can, and it will work on small data. Vertipaq handles single-table imports under a few million rows reasonably well. The wheels come off as the row count grows and the cardinality of the columns goes up. By the time you notice the slowdown, restructuring is harder because every measure already references the wide table.
What about composite models or DirectQuery?
The same model-shape principles apply, and the cost of getting them wrong is higher because slow queries hit the source system. DirectQuery on a flat table with M2Ms is the worst of both worlds - all the performance penalties of bad shape, plus network round-trips. If you're going DirectQuery, the model has to be star schema shaped.
Is it ever right to use a bidirectional filter?
Sometimes. The specific case is a single bridge table for a true many-to-many, where you need the filter to propagate from one side of the bridge to the other. Beyond that, bidirectional is almost always premature optimisation that turned into actual de-optimisation. Single-direction is the default; bidirectional is the exception.
What if I inherited the model and can't rebuild it?
Start with the cheapest wins. Turn off bidirectional filters that aren't load-bearing for a known visual. Audit the relationships and reduce M2Ms where the data shape allows. Move date columns onto a proper date dimension if one isn't there. These three changes alone often deliver more performance improvement than rewriting any DAX.
Does this apply to Tabular models outside Power BI too?
Yes. The same Vertipaq engine powers Analysis Services Tabular and Power BI semantic models. Everything in this post applies to both. The reason most discussions are framed as "Power BI performance" is that Power BI is where the largest number of badly-shaped models live, not because the engine cares which front-end you're using.