Skip to content
Carmel Eve By Carmel Eve Software Engineer II · 2 min read
Optimising DAX: The Formula Engine and Storage Engine

We've now covered the model optimisation half of the Optimising DAX series - storage, encoding, cardinality, relationships, and model design. Now we're shifting to the query layer: what actually happens when you execute a DAX query?

Two Engines, One Query

There are two engines involved in processing DAX queries:

The Formula Engine knows about all DAX functions and handles complex calculations. It's powerful, but slow - and critically, it's single-threaded.

The Storage Engine (VertiPaq) can only do very simple operations, but it's very fast and multi-threaded.

How They Work Together

When you write a DAX query, the following happens:

  1. The query is parsed into an expression tree, then a logical plan.
  2. The formula engine optimises this plan and decides what it needs from the storage engine.
  3. The storage engine executes simple queries and puts the results into a data cache.
  4. The formula engine processes the data cache to produce the final result.
Microsoft Fabric Weekly is a summary of the week's top news to help you build on the Microsoft Fabric Platform.

The important detail: the formula engine can't access the raw data directly. It can only work with what the storage engine puts in the cache. Overall, the storage engine retrieves data, the formula engine computes data.

DAX query execution flow: formula engine plans, storage engine populates the data cache, formula engine calculates the result

The Optimisation Principle

The key takeaway follows naturally from this architecture: push as much work as possible into the storage engine.

It's faster, and it's multi-threaded - where the formula engine is single-threaded. If the storage engine can filter, group, and aggregate your data before building the cache, the formula engine only needs to do a small amount of final processing. If the formula engine has to process millions of rows itself, things get slow.

What the Storage Engine Can Do

It's useful to keep a mental checklist:

The storage engine can:

  • Scan columns
  • Evaluate simple conditions (equality, inequality, IN)
  • Perform GROUP BY
  • Do joins across relationships
  • Handle basic aggregations (SUM, COUNT, DISTINCTCOUNT, and by extension AVG, MIN, MAX)
  • Follow relationships between tables

An example: Bitmap Filter Combination

Simple columnar filtering is a good example of the storage engine's efficiency. Each filter condition is transformed into a bitmap - a compact representation of which rows match. When multiple filters are applied, the bitmaps are combined with a logical AND:

Filter bitmaps built for each condition and combined with AND

And then this combined bitmap is applied to the original column:

Applying the combined bitmap filters the column to matching rows

All of this happens within the storage engine, without the formula engine needing to do anything.

It's worth keeping the storage engine's operation list in mind when writing DAX. If you can decompose a complex expression into operations the storage engine understands, it can often handle the work itself. If any part of the expression falls outside this list, the formula engine gets involved - and things get slower.

The best hour you can spend to refine your own data strategy and leverage the latest capabilities on Azure to accelerate your road map.

Anything beyond this gets handed to the formula engine.

Callbacks

Sometimes you do need functions the storage engine doesn't understand. Consider:

SUMX(Sales, TRUNC(Quantity * NetPay))

The storage engine doesn't know what TRUNC is. So it would have to materialise the whole columns and pass them back to the formula engine. But instead, it uses a callback - calling the formula engine for each row to evaluate the unknown function, while still handling the iteration itself.

This is slow (it calls back for every single row), but it's better than full materialisation. It's worth avoiding callbacks where you can restructure the expression (e.g. decompose it into simple mathematical operations), but sometimes it's simply necessary.

Testing Tip

It's worth noting that model performance and query performance have different testing requirements. For model performance (everything we covered in the first half of the series), you need to work with real data - the data distribution is crucial to how well VertiPaq compresses and stores it. For query performance, if something is faster on a subset of data, it will be faster on the full dataset too. So it's better to test DAX queries locally on a subset, where performance is more predictable - no contention from other workloads.

Enable both Server Timings and Query Plan in DAX Studio to see what's happening. You'll typically see the flow: Formula Engine (reasoning), then Storage Engine (retrieval), then back to the Formula Engine (calculating).

DAX Studio showing Server Timings and Query Plan enabled

What's Next

So the goal is to keep work in the storage engine. But what happens when you can't? That's where data materialisation comes in - and it's one of the biggest causes of slow queries. Look out for the next post where we'll dig into what it is and why it matters.

FAQs

What is the formula engine in Power BI? The formula engine understands all DAX functions and handles complex calculations, but it is single-threaded and relatively slow. It works by processing data that the storage engine has placed into a data cache.
What is the storage engine in Power BI? The storage engine (VertiPaq) handles simple operations like scanning, filtering, grouping, and basic aggregations. It is very fast and multi-threaded, but can only perform a limited set of operations.
What are callbacks in DAX query execution? When the storage engine encounters a function it doesn't understand (like TRUNC), instead of materialising the entire table, it uses a callback to call the formula engine for each row. This is slow but better than full materialisation.

Carmel Eve

Software Engineer II

Carmel Eve

Carmel is a software engineer and LinkedIn Learning instructor. She worked at endjin from 2016 to 2021, focused on delivering cloud-first solutions to a variety of problems. These included highly performant serverless architectures, web applications, reporting and insight pipelines, and data analytics engines. After a three-year career break spent travelling around the world, she rejoined endjin in 2024.

Carmel has written many blog posts covering a huge range of topics, including deconstructing Rx operators, agile estimation and planning and mental well-being and managing remote working.

Carmel has released two courses on LinkedIn Learning - one on the Az-204 exam (developing solutions for Microsoft Azure) and one on Azure Data Lake. She has also spoken at NDC, APISpecs, and SQLBits, covering a range of topics from reactive big-data processing to secure Azure architectures.

She is passionate about diversity and inclusivity in tech. She spent two years as a STEM ambassador in her local community and taking part in a local mentorship scheme. Through this work she hopes to be a part of positive change in the industry.

Carmel won "Apprentice Engineer of the Year" at the Computing Rising Star Awards 2019.