Power BI · Chapter 4 · Concept lesson · 7 min read

Calculated column or measure?

DAX has two jobs. A calculated column adds a stored value to every row when data refreshes; a measure is computed on the fly for each cell under that cell's filters. One question tells you which to write.

The idea

DAX does two different jobs, and they look almost the same on the screen. You type Name = expression, press Enter, and a new thing appears in the Data pane. But the engine treats the two in opposite ways.

  • A calculated column is evaluated once per row, when the data is loaded or refreshed. The result is stored in the table and takes up memory like any other column.
  • A measure is evaluated once per cell, when a visual is drawn, under the filters that cell carries. Nothing is stored; it is a formula waiting to be asked.

One rule picks between them: if you need to group, filter or slice by it, it must be a column. If you need to add up or average it, it must be a measure.

Figure 1

A calculated column: one value per row, stored in the table

products with Price Band = IF(products[unit_price] >= 100, "Premium", "Everyday")
product_idnameunit_pricePrice Band
101Vale Velvet Loveseat1659.00Premium
102Alder Oak Coffee Table919.00Premium
121Bramble Ceramic Floor Lamp31.99Everyday
154Vale Enamel Bowl Set11.99Everyday
194Chester Brass Wall Mirror159.00Premium

Step 1 of 4 A calculated column is written once and evaluated for every row of its table, one row at a time. DAX calls the row being visited the row context.

  1. A calculated column is written once and evaluated for every row of its table, one row at a time. DAX calls the row being visited the row context.
  2. For row 101 the formula sees only that row's unit_price, 1659.00. The test >= 100 is true, so the cell is Premium.
  3. Then the next row: 31.99 is under 100, so Everyday. The result is stored in the table like any other column, computed again only at refresh.
  4. Because Price Band is now real data, you can put it on a matrix row, a slicer or a filter. A measure could not do that.

Why it exists

A spreadsheet has only one kind of formula: it lives in a cell and shows a value. Power BI tables have thousands or millions of rows, and its questions are of two different kinds.

Some questions are about a row: "is this product premium?", "what month was this order?", "what did this line cost?". They have one answer per row and the answer does not change when you look at the row from a different chart. That is a column.

Other questions are about a set of rows: "what is our average order value for the web channel?". The answer depends on which rows the visual, the slicers and the row header hand over. You can't store it, because it is not one number. That is a measure.

Mixing them up has predictable costs. A ratio built as a column returns one value per row that the matrix can only add or average, which is wrong. A band built as a measure cannot go on a row or a slicer, because a measure is not a column to group by.

How it works under the hood

Row context: the current row

When a calculated column runs, DAX visits the table's rows in turn. At each stop, the row is the row context: products[unit_price] means that row's price. That is why IF, SWITCH, YEAR and MONTH work directly in a column. They need one value, and the row context supplies it.

Row context carries no filter. If a column on orders calls SUM(order_items[line_total]), it returns the grand total on every row, because the current order does not filter the lines. Making a row act as a filter takes CALCULATE, which is the next chapter's subject.

Filter context: the cell's rows

A measure starts from the cell. The row header, the column header, the slicers and the page filters become the filter context, and the measure's expression runs over the rows that survive.

Figure 2

A measure: recomputed for every cell from that cell's rows

Seven completed orders, March 2025 (order totals)
order_idchannelorder_total
8749web514.98
8750web192.59
8751app1483.99
8752web795.94
8753app3111.88
8754web1314.77
8757app55.97
Matrix by channel: Average Order Value = DIVIDE([Sales], [Orders])
channelSalesOrdersAverage Order Value
web2818.284704.57
app4651.8431550.61
Total7469.1271067.02

Step 1 of 4 Seven orders. A measure is not stored on any of them; it exists only when a cell asks for it.

  1. Seven orders. A measure is not stored on any of them; it exists only when a cell asks for it.
  2. The web row filters the orders to channel = web: four orders. Sales adds their totals, 2818.28, Orders counts 4, and DIVIDE gives 704.57.
  3. The app row runs the same formula on a different set of rows: three orders, 4651.84 in sales, 1550.61 each.
  4. The Total row has no channel filter: all seven orders, 7469.12 divided by 7 is 1067.02. It is the measure evaluated again, not the sum or the mean of the rows above.

Two details come out of this. First, a measure never lists rows: SUM, COUNTROWS, DISTINCTCOUNT, AVERAGE, MIN and MAX all collapse a column or table to one number. Second, the Total row is not a sum. It is the measure with the row header's filter missing, which for a ratio gives a different answer than adding the rows.

Following a relationship in a column

A column can only read its own row, but the model has related tables. RELATED reads a column from the one side for the current row: Line Cost = order_items[quantity] * RELATED(products[unit_cost]). It needs row context and follows the relationship from the many side to the one side.

Measures calling measures

Once Sales exists, you can write Average Order Value = DIVIDE([Sales], [Orders]). A measure reference is written with square brackets and no table name, which is how DAX keeps columns (table[column]) and measures apart on sight. Build small measures, then compose them. Use DIVIDE rather than / so a cell with no orders shows blank, not an error.

Where to create each

Both start in the same place. In Power BI Desktop, select a table in the Data pane and choose Table tools → New column or New measure (a measure can live in any table, because it is not a column of it). In Table view you will see a column's values; you never see a measure's.

A quick test before you type

Ask two questions. Do I need this value on a row header, a slicer or a filter? Then it is a column. Does its answer change with the cell it sits in? Then it is a measure. A price band, an order month and a line cost pass the first test. Customers who bought, price range and average order value pass the second. If both answers are no, you probably need a different measure or none at all.

Common misconceptions

"A measure is just a column that is calculated later"

They are different objects. A column is data: it has a value per row and is stored. A measure has no value of its own, only a result for a given context. The same measure can show 704.57 in one cell and 1550.61 in the next. A column cannot change like that.

"I'll make it a column so it is faster"

A column is computed at refresh and costs memory on every row, including the ones nobody looks at. A measure costs nothing until a cell asks. For anything that you aggregate, a measure is usually both smaller and correct. Reach for a column when the value is a property of the row and you need to group or filter by it.

"The Total row adds up the rows above it"

It runs the measure again, without the row header's filter. For Sales the result happens to equal the sum. For Average Order Value it does not: 1067.02 is neither the sum of 704.57 and 1550.61 nor their average, because it is the average over all seven orders.

Check yourself

One question. Answering it ticks this lesson off on the map; it never locks a level.

A matrix has channel on its rows and you want each channel's average order value. You write AOV = DIVIDE(SUM(order_items[line_total]), COUNTROWS(orders)) as a calculated column on orders. What do you get?

Pick what may run on this site. You can change this any time from "Cookie settings" at the bottom of every page.