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
| product_id | name | unit_price | Price Band |
|---|---|---|---|
| 101 | Vale Velvet Loveseat | 1659.00 | Premium |
| 102 | Alder Oak Coffee Table | 919.00 | Premium |
| 121 | Bramble Ceramic Floor Lamp | 31.99 | Everyday |
| 154 | Vale Enamel Bowl Set | 11.99 | Everyday |
| 194 | Chester Brass Wall Mirror | 159.00 | Premium |
- 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.
- For row 101 the formula sees only that row's
unit_price, 1659.00. The test>= 100is true, so the cell is Premium. - 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.
- 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
| order_id | channel | order_total |
|---|---|---|
| 8749 | web | 514.98 |
| 8750 | web | 192.59 |
| 8751 | app | 1483.99 |
| 8752 | web | 795.94 |
| 8753 | app | 3111.88 |
| 8754 | web | 1314.77 |
| 8757 | app | 55.97 |
| channel | Sales | Orders | Average Order Value |
|---|---|---|---|
| web | 2818.28 | 4 | 704.57 |
| app | 4651.84 | 3 | 1550.61 |
| Total | 7469.12 | 7 | 1067.02 |
- Seven orders. A measure is not stored on any of them; it exists only when a cell asks for it.
- The web row filters the orders to channel = web: four orders.
Salesadds their totals, 2818.28,Orderscounts 4, and DIVIDE gives 704.57. - The app row runs the same formula on a different set of rows: three orders, 4651.84 in sales, 1550.61 each.
- 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.
Figure 3
RELATED: a row reaching up to its product
products
- product_id
- category
- unit_cost
category = "Lighting"
order_items
- order_item_id
- product_id
- quantity
- A calculated column
Line Costonorder_itemsneeds the product'sunit_cost, which sits on the 1 side of the relationship. - A filter can only run down the relationship, from
productstoorder_items, never back up. RELATED(products[unit_cost])is different: it follows the current row'sproduct_idup to its one product row and reads the cost. It works in row context only, and only from the many 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.
Right. A calculated column runs once per row at refresh with no cell filters. SUM in row context sees all lines and COUNTROWS(orders) all orders, so every row stores the same grand ratio, and the matrix cannot re-run it per channel.
Not quite. The stored numbers are per row, and adding or averaging them in the matrix does not equal sales divided by orders for the channel. A ratio belongs in a measure.Try another answer.
Not quite. DIVIDE works anywhere DAX does. The problem is when the formula runs, not which functions it uses.Try another answer.
Not quite. Not the same: the column is fixed per row, the measure is recomputed for every cell. Refresh time is not what differs.Try another answer.
Lesson done: it is ticked on the map.