Back to Blog

Power BI Calculated Columns - When To Use Them and When To Avoid Them

May 6, 20269 min readMichael Ridland

Calculated columns in Power BI are one of those features that look simple in a five-minute demo and cause months of pain in production. Microsoft's docs explain the mechanics well, but they cannot tell you when you should reach for a calculated column versus a measure, a Power Query custom column, or a column added upstream in your warehouse. That decision is where most Australian businesses I work with lose performance, lose data quality, or both.

I want to walk through what we have actually learned about calculated columns across dozens of Power BI implementations in mining, financial services, healthcare and professional services. If you are a BI lead trying to keep your model fast and your reports trustworthy, this is the stuff that matters.

What a calculated column actually is

A calculated column is a column you add to a table inside your Power BI model using DAX. You right-click the table, pick New Column, and type something like:

CityState = [City] & "," & [State]

Power BI then runs that expression for every row in the table and stores the result. The new column behaves like any other column. You can put it on a slicer, group by it, use it on an axis. It looks innocuous.

The catch is in those two words: stores the result. Calculated columns in Import mode are materialised, which means they sit in memory and consume RAM like any imported column. They are also computed during refresh, which means a slow DAX expression slows your refresh. In DirectQuery mode they are unmaterialised, which means every query hits them at runtime, which can murder performance.

That trade-off is what most people miss when they spam calculated columns into their model.

The three tools that all look similar

Before getting into when to use a calculated column, it helps to understand its siblings. There are three places where you can add a derived column to a Power BI model, and they behave very differently.

A Power Query custom column is added when you transform data in the Power Query Editor before it lands in the model. It is computed during refresh, runs in the M language, can fold back to SQL if your source supports query folding, and the result is just another column in the loaded table.

A DAX calculated column is added after data is loaded, using DAX. It is recomputed during refresh in Import mode and stored in the model.

A DAX measure is also written in DAX, but it does not produce a column. It produces a number that is recalculated every time a visual asks for it, based on whatever filter context is active.

I cannot tell you how many times I have walked into a client's tenant and seen a calculated column doing the job of a measure, or a measure doing the job of a Power Query column, or a calculated column doing something that should have been computed in the SQL view feeding the model. Each wrong choice has a cost.

When a calculated column is the right tool

Use a calculated column when you need a static, row-level value that does not depend on filter context, and where the logic genuinely belongs in the model rather than upstream.

A good example is something like building a financial year bucket from a date. You want every row tagged with FY26 or FY27 so users can slice and dice on it. The logic is simple, the result is the same every time you refresh, and it needs to live in the model so multiple measures and visuals can use it. Calculated column is the right call.

Another good example is concatenating fields for a display label. The Microsoft docs use the example of a shipping manager combining City and State into a single CityState column for use on a map. That is a reasonable use because the result is a stable display value, the source columns are stable, and the work is trivial.

Honestly, those two patterns - bucketing and string concatenation for display - cover about 80% of the legitimate uses I see. Most of the rest fall into one of three traps.

Three traps to watch for

The first trap is using calculated columns to do row-level lookups against related tables. You write something like:

RegionManager = RELATED('Region'[Manager])

This works. It is not the end of the world. But every one of those columns is sitting in memory after refresh, and a relationship lookup that runs once per row scales poorly when your fact table is 200 million rows. In most cases this should be done in your source query, either as a JOIN in SQL or via a merge in Power Query, where the work happens once and gets folded back to the database.

The second trap is using calculated columns for things that should be measures. The classic example is a profit margin column:

Margin = ([Revenue] - [Cost]) / [Revenue]

This looks fine until you realise that what your users actually want is the margin at whatever level they are looking at. They want margin for the selected region, the selected product, the selected month. A calculated column gives you per-row margin, which when averaged or summed in a visual gives you wrong numbers. A measure does the right thing because it respects filter context. I have seen finance teams chase phantom variances for weeks because some analyst put margin in a calculated column.

The third trap is the heavy DAX calculated column. People write 200-line DAX expressions involving CALCULATE, FILTER and SUMX in a calculated column and then wonder why refresh takes 90 minutes. Calculated columns are evaluated row by row at refresh, and complex iterators do not scale. If your DAX is doing serious work, push it into a measure or into the upstream SQL.

The materialisation question

There is a subtle point in the Microsoft docs that most people skip past. The Expression Context property determines whether your calculated column is materialised or not, and the answer depends on the storage mode of the table and whether your DAX uses user-context functions like USERPRINCIPALNAME or CUSTOMDATA.

If you write a normal calculated column on an Import-mode table, it is materialised. The result sits in memory.

If you write a calculated column that uses USERPRINCIPALNAME, it becomes unmaterialised even on Import. That sounds nice for memory, but it also means the column is evaluated at query time on every interaction, which is generally slower than a measure for the same logic.

Direct Lake on OneLake and DirectQuery tables always treat calculated columns as unmaterialised. Calculated columns in those storage modes are essentially measures dressed up as columns. They will hurt query performance.

The practical rule is this. If you are on Import mode, every calculated column costs you RAM and refresh time but is fast at query time. If you are on DirectQuery or Direct Lake, calculated columns cost you query time and add nothing on refresh. Either way, the cheap-looking little calculation is paying tax somewhere.

What we recommend on Australian projects

For most Australian businesses I work with, the rule I push for is what we call upstream-first. If a column can be created upstream - in a SQL view, a Microsoft Fabric notebook, a Dataflow Gen2, a Data Factory pipeline - put it there. The benefits stack up. Your column is reusable across multiple Power BI models. Your transformation logic lives in version-controlled SQL or M rather than scattered DAX expressions hidden inside a .pbix. Your refresh is faster because the work has already been done. And your data engineers, who understand SQL better than DAX, can maintain it.

A calculated column should be the fallback for cases where the column genuinely belongs in the semantic model, like the FY bucket or the display label examples. That keeps the model lean and the responsibility for transformation clear.

If you want help thinking through this for your own organisation, our Power BI consultants work with clients on exactly this kind of model design across Microsoft Fabric consulting and Microsoft Data Factory engagements. The decisions you make about where to put your transformation logic compound. Get them right early, and your models stay healthy for years. Get them wrong, and you end up rebuilding.

A note on the DAX itself

If you are going to write calculated columns, write them well. DAX is its own language with its own evaluation context rules. The biggest mistake I see is people treating DAX as if it were Excel. Excel calculates one cell at a time with explicit cell references. DAX calculates one row at a time in a calculated column, but a measure recalculates against whatever filter context the visual provides.

The functions look similar but they behave differently. SUM in Excel is straightforward. SUM in a DAX measure respects filter context. SUM in a calculated column iterates over rows. If you are unsure which one you are in, you will get unexpected results.

There are over 200 DAX functions and you do not need to know them all. You do need to know the difference between row context and filter context. You do need to know what CALCULATE does, what FILTER does, and when to use ALL versus REMOVEFILTERS. If your team is going to maintain Power BI models, get someone trained properly. We do this through our Microsoft AI consulting practice as part of broader data platform engagements.

The honest summary

Calculated columns are a useful but overused feature. They are fine for static row-level values that genuinely belong in the model. They are wrong for lookups, wrong for aggregations, and wrong for heavy logic. When in doubt, push transformations upstream and reach for a measure instead of a calculated column.

If you have inherited a Power BI tenant with thousands of calculated columns spread across hundreds of reports, you have inherited a refactoring project. That is normal. Most enterprise Power BI estates look like that. The job is to triage which columns are pulling their weight and which are dead weight, and to gradually replace the dead weight with proper SQL views or measures.

We have done this for several Australian clients now, including a few where the Power BI refresh time went from over an hour to under ten minutes once we cleaned up the calculated column mess. The Microsoft docs on calculated columns are a good reference for the mechanics, but the judgement about when to use them comes from doing the work and seeing the consequences. That is what consulting time is for.

If you want to talk through your specific situation, get in touch. I would rather have a 30-minute conversation about your model than see you live with three extra hours of refresh time every night.