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

Iterators, row context and context transition

Why SUM(a) * SUM(b) is the wrong revenue, what an iterator really does, and the one move, context transition, that lets the row you are standing on filter the rest of the model.

The idea

Most DAX you have written so far reads a whole column: SUM(order_items[line_total]). An iterator is the other way to read a table. SUMX, AVERAGEX, MAXX, RANKX and COUNTX take a table and an expression, visit the table one row at a time, work the expression out on that row, and only then collapse the results (add them, average them, rank them).

While the iterator stands on a row, the columns of that row are available by name. That "current row" is the row context. It is a position in a table, nothing more. It is not a filter, and that difference is behind most iterator bugs.

Figure 1

SUMX walking order 1004, one line at a time

order_items for order 1004
order_item_idquantityunit_pricediscount_pct
10192.9910
11220.9910
121149.0010
13266.9910
Per-row result
order_item_idnet
1083.69
1137.78
12134.10
13120.58
SUMX
Net Revenue
$376.15

Step 1 of 5 Four lines of one real order. SUMX(order_items, quantity * unit_price * (1 - discount_pct / 100)) will visit them in turn.

  1. Four lines of one real order. SUMX(order_items, quantity * unit_price * (1 - discount_pct / 100)) will visit them in turn.
  2. Line 10: while the iterator stands on this row, quantity, unit_price and discount_pct mean this row's values. That is the row context. 1 x 92.99 x 0.9 = 83.69.
  3. Lines 11, 12 and 13 follow the same way. Each one is worked out on its own before anything is added.
  4. Only now does the X function add: 83.69 + 37.78 + 134.10 + 120.58 = 376.15.
  5. SUM(quantity) * SUM(unit_price) would give 6 x 329.97 = 1,979.82: every unit times every price. Multiplying before adding is the whole point of an iterator.

Why it exists

Harbor & Pine's Finance Controller wants net revenue built from quantity, unit_price and discount_pct. The number she needs does not exist as a column: it is a product, per line, that then has to be added. SUM can only add a column that is already there.

A spreadsheet user would add a helper column, fill it down, and sum it. An iterator is that helper column without the column: the multiplication happens in memory, for exactly the rows the cell is allowed to see, and nothing is stored in the model. That keeps the file smaller and the number alive: change the slicer and the rows being walked change with it.

The X functions are a family with one shape: SUMX, AVERAGEX, MINX, MAXX and COUNTX differ only in how they collapse the per-row results. Learn one and you have learned all of them.

How it works under the hood

SUMX(table, expression) runs in two stages, and the order matters.

  1. Decide which rows exist. The filter context of the cell (row header, slicers, CALCULATE filters) shrinks table first. A SUMX over order_items in the Lighting row of the matrix walks only Lighting lines.
  2. Walk them. For each remaining row, evaluate the expression with that row as the row context, then combine the results.

Because the expression runs once per row, it can use columns of the row (order_items[quantity]) and, through a many-to-one relationship, columns of the row's parent (RELATED(products[unit_cost])). It cannot reach down the other way without help: from a product you do not automatically see its lines.

Context transition

That "without help" is the interesting case. Inside a calculated column or an iterator, a plain SUM(order_items[line_total]) ignores the row you are on. The current product is a position, not a filter, so nothing reaches order_items.

CALCULATE fixes this. When it runs inside a row context it takes every column of the current row and turns them into filters, then evaluates its expression under those filters. This is context transition. For product 135 it becomes product_id = 135, which flows down the relationship to order_items the way any slicer would.

Figure 2

A calculated column on products: row context versus a filter

products

  • product_id
  • name
  • category

product_id = 135

orders

  • order_id
  • channel
  • status

order_items

  • order_id
  • product_id
  • line_total

Step 1 of 3 A calculated column is evaluated once per row of products. For product 135 the engine is standing on that row: a row context.

  1. A calculated column is evaluated once per row of products. For product 135 the engine is standing on that row: a row context.
  2. SUM(order_items[line_total]) asks order_items for its rows. Nothing has filtered it: a row context is not a filter and does not travel along relationships.
  3. Wrap it in CALCULATE. Context transition turns the current row into a filter, product_id = 135, and that filter flows down like any other.

Seeing it in numbers makes it stick: the same two formulas in a calculated column on products.

Figure 3

The same column, with and without CALCULATE

products, two calculated columns
product_idnamebare_sumwith_calculate
134Sable Linen Table Lamp$6,516,344$51,423
135Ridge Ceramic Chandelier$6,516,344$89,718
186Birch Brass Floor Mirror$6,516,344$46,536

Step 1 of 2 bare_sum = SUM(order_items[line_total]). Every product shows the sales of the whole store.

  1. bare_sum = SUM(order_items[line_total]). Every product shows the sales of the whole store.
  2. with_calculate = CALCULATE(SUM(order_items[line_total])). Each product now sums only its own lines.

Choosing the table to walk

The table you hand to an iterator decides what one "item" is. AVERAGEX(order_items, ...) is an average per order line. AVERAGEX(orders, [Sales]) is an average per order, which is what "average order value" means: each order is visited, its own lines are summed through the context transition, and those order totals are averaged. Same function, different table, a different business question. Ask "what is one unit of this average?" before you type the first argument.

The same idea carries a weighted margin. Walk order_items, and for each line reach up to its product with RELATED(products[unit_cost]). The cost is multiplied by that line's quantity, so products that sell a lot count more. Averaging the margin of the 120 products would give every product the same weight, which is not what the till saw.

Measures carry an invisible CALCULATE

A reference to a measure, [Sales], behaves as if it were wrapped in CALCULATE. That is why SUMX(products, [Sales]) returns each product's own sales added up, while SUMX(products, SUM(order_items[line_total])) repeats the store total once per product. The first transitions the row into a filter; the second does not.

It also means iterators have a price. A measure inside a SUMX over a big table triggers a transition per row, which is slow and, with a hundred-thousand-row table, noticeable. Iterate over the small table (products, 120 rows) rather than the big one (order lines, over 30,000) when the answer is the same.

Where to try it in Power BI Desktop

Use Home → New measure for an iterator and Table tools → New column for a calculated column. To see what a row context does, write the same expression in both places and compare. The DAX query view lets you run EVALUATE ADDCOLUMNS(products, "x", [Sales]) and read the result as a table.

Common misconceptions

"SUM(quantity) * SUM(price) is the same as adding quantity times price"

Multiplying two totals multiplies every quantity by every price, not each line's own pair. For order 1004 it gives 1,979.82 instead of 376.15, and the gap grows with the table. Whenever a number is a product or ratio of two columns per row, it is an iterator.

"A row context filters the other tables"

It does not. Standing on a product row tells the formula which row's columns to read, nothing more. The order_items table sees every line until something creates a filter. That something is CALCULATE, or a measure reference.

"Iterators are always slower, so avoid them"

An iterator over a column expression (SUMX(order_items, quantity * unit_price)) is cheap: the storage engine handles it. The slow pattern is a measure evaluated once per row of a large table. Choose the smallest table that gives the right answer and keep the expression simple.

Check yourself

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

On products you add two calculated columns, A = SUM(order_items[line_total]) and B = CALCULATE(SUM(order_items[line_total])). What does the Ridge Ceramic Chandelier row show?

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