Power BI Course
Iterator Functions
Iterator functions — the X functions — are the solution to one of DAX's most common problems: how do you aggregate a calculation that must be performed row by row before the results are combined? SUM(Orders[Revenue]) adds a column that already exists. SUMX(Orders, [Revenue] * [Quantity]) calculates a value per row first, then sums the results. The X suffix is the signal: this function evaluates an expression for each row, then aggregates. Understanding when the difference matters — and when it does not — separates correct DAX from subtly wrong DAX.
The Working Dataset
| OrderID | UnitPrice | Quantity | Discount % | Revenue (stored) | Region |
|---|---|---|---|---|---|
| 1001 | 350 | 4 | 0% | 1,400 | North |
| 1002 | 250 | 5 | 10% | 1,125 | South |
| 1003 | 500 | 2 | 5% | 950 | North |
| 1004 | 80 | 1 | 0% | 80 | South |
| 1005 | 280 | 6 | 0% | 1,680 | West |
How Iterators Work — The Two-Phase Pattern
Every X function follows the same two-phase process. Understanding the phases is more useful than memorising each function individually — once you understand the pattern, all X functions become predictable.
SUMX — Row-by-Row Sum
SUMX is the most-used iterator. The key question to ask: "Do I need to compute something per row before summing?" If yes, use SUMX. If you are simply summing a column that already exists, SUM is faster and identical in result.
// SUM on a stored column — simple aggregation
Total Revenue (SUM) = SUM(Orders[Revenue])
// Adds the pre-computed Revenue column: 1400+1125+950+80+1680 = 5,235
// SUMX computing revenue per row from component columns
Total Revenue (SUMX) =
SUMX(
Orders,
Orders[UnitPrice] * Orders[Quantity] * (1 - Orders[Discount%])
)
// Phase 1 — per row:
// 1001: 350 × 4 × 1.00 = 1,400
// 1002: 250 × 5 × 0.90 = 1,125
// 1003: 500 × 2 × 0.95 = 950
// 1004: 80 × 1 × 1.00 = 80
// 1005: 280 × 6 × 1.00 = 1,680
// Phase 2 — sum: 1400+1125+950+80+1680 = 5,235
// Both return 5,235 here — SUMX matches SUM because Revenue
// was computed using the same formula in Power Query.
| OrderID | UnitPrice | Qty | Discount | Phase 1 result per row |
|---|---|---|---|---|
| 1001 | 350 | 4 | 0% | 1,400 |
| 1002 | 250 | 5 | 10% | 1,125 |
| 1003 | 500 | 2 | 5% | 950 |
| 1004 | 80 | 1 | 0% | 80 |
| 1005 | 280 | 6 | 0% | 1,680 |
| Phase 2 — SUM of all per-row results | 5,235 | |||
When SUMX Gives a Different Answer Than SUM
The critical difference appears when you want to aggregate a ratio across rows. Summing a ratio column and taking the ratio of sums are mathematically different. AVERAGEX makes this even more important — the average of per-row ratios is not the same as dividing total revenue by total quantity.
// Scenario: Average revenue per unit across all orders
// WRONG — average of the Revenue column divided by average Quantity
// This is (avg revenue) / (avg quantity) — mathematically incorrect
Avg Rev Per Unit WRONG =
DIVIDE(AVERAGE(Orders[Revenue]), AVERAGE(Orders[Quantity]))
// = DIVIDE(1047, 3.6) = 290.8 ← wrong
// CORRECT — AVERAGEX computes revenue/unit per row, then averages those
Avg Rev Per Unit =
AVERAGEX(
Orders,
DIVIDE(Orders[Revenue], Orders[Quantity])
)
// Phase 1 — per row:
// 1001: 1400 / 4 = 350.0
// 1002: 1125 / 5 = 225.0
// 1003: 950 / 2 = 475.0
// 1004: 80 / 1 = 80.0
// 1005: 1680 / 6 = 280.0
// Phase 2 — AVERAGE of {350, 225, 475, 80, 280}
// = 1410 / 5 = 282.0 ← correct row-level average
| OrderID | Revenue | Quantity | Rev / Unit (per row) |
|---|---|---|---|
| 1001 | 1,400 | 4 | 350.00 |
| 1002 | 1,125 | 5 | 225.00 |
| 1003 | 950 | 2 | 475.00 |
| 1004 | 80 | 1 | 80.00 |
| 1005 | 1,680 | 6 | 280.00 |
| AVERAGEX result (average of per-row ratios) | 282.00 | ||
SUMX With a Measure — The Most Powerful Pattern
SUMX can evaluate any expression per row — including measures. When SUMX iterates over a table and calls a measure, that measure evaluates inside the row context of each row. This is how you build calculations that are impossible with simple aggregation functions alone.
// Pattern: SUMX over a dimension table to aggregate a measure
// Scenario: total revenue only for orders where the customer is Premium
// (Customer tier lives in the Customers table, not Orders)
// Naive attempt — WRONG, cannot filter across relationship in a simple SUM
// Premium Revenue = SUM(Orders[Revenue]) -- returns all revenue, ignores tier
// Correct: SUMX iterates Customers, filters to Premium, sums their Orders revenue
Premium Revenue =
SUMX(
FILTER(Customers, Customers[Tier] = "Premium"),
CALCULATE(SUM(Orders[Revenue]))
-- CALCULATE triggers context transition: each Customers row
-- becomes a filter context that restricts Orders to that customer
)
// Pattern: weighted average margin using SUMX
// Margin = (Revenue - Cost) / Revenue, weighted by Revenue
Weighted Avg Margin % =
DIVIDE(
SUMX(Orders, Orders[Revenue] - Orders[Cost]),
SUM(Orders[Revenue])
)
// = total gross profit / total revenue = correct weighted margin
// Compare: AVERAGEX(Orders, DIVIDE([Revenue]-[Cost],[Revenue]))
// gives equal-weight average margin per order — different result
| OrderID | Revenue | Cost | Margin per row | Margin % per row |
|---|---|---|---|---|
| 1001 | 1,400 | 900 | 500 | 35.7% |
| 1002 | 1,125 | 780 | 345 | 30.7% |
| 1003 | 950 | 590 | 360 | 37.9% |
| 1004 | 80 | 55 | 25 | 31.3% |
| 1005 | 1,680 | 1,050 | 630 | 37.5% |
| Totals | 1,860 | — | ||
All Six X Functions
// SUMX — sum of per-row expressions
Total Gross Profit =
SUMX(Orders, Orders[Revenue] - Orders[Cost])
// AVERAGEX — average of per-row expressions
Avg Gross Profit Per Order =
AVERAGEX(Orders, Orders[Revenue] - Orders[Cost])
// MINX — minimum of per-row expressions
Min Order Margin =
MINX(Orders, DIVIDE(Orders[Revenue] - Orders[Cost], Orders[Revenue]))
// MAXX — maximum of per-row expressions
Max Order Margin =
MAXX(Orders, DIVIDE(Orders[Revenue] - Orders[Cost], Orders[Revenue]))
// COUNTX — count rows where expression is not blank
Orders With Margin =
COUNTX(Orders, IF(Orders[Revenue] > Orders[Cost], 1, BLANK()))
// PRODUCTX — multiplies all per-row results (rarely used outside finance)
Compound Growth =
PRODUCTX(
GrowthRates,
1 + GrowthRates[MonthlyRate]
) -- multiplies (1 + rate) for each month = compound growth factor
RANKX — Ranking Within Context
RANKX ranks the current context's value (usually a measure result) against the same measure evaluated over every row in the first argument table. It is an iterator because it evaluates the expression for every row in the ranking table before assigning a rank to the current context.
// RANKX(table, expression, value, order, ties)
// table: the set of items to rank across
// expression: what to rank by (evaluated for every row in table)
// value: the value to rank (blank = use current context)
// order: DESC (1=highest) or ASC (1=lowest)
// ties: SKIP (default gaps) or DENSE (no gaps)
// Rank products by revenue — highest = rank 1
Product Revenue Rank =
RANKX(
ALLSELECTED(Products[ProductName]),
[Total Revenue],
, -- blank value = use measure in current context
DESC,
DENSE -- no gaps: 1,2,3 not 1,2,4 if tie at rank 2
)
// Rank regions by order count — lowest count = rank 1
Region Order Count Rank =
RANKX(
ALLSELECTED(Orders[Region]),
COUNTROWS(Orders),
,
ASC,
DENSE
)
| Region | Total Revenue | Revenue Rank |
|---|---|---|
| West | 1,680 | 1 |
| North | 2,350 | 1 |
| South | 1,205 | 3 |
| Total | 5,235 | 1 |
| Region | Revenue | Rank (DESC DENSE) |
|---|---|---|
| North | 2,350 | 1 |
| West | 1,680 | 2 |
| South | 1,205 | 3 |
Iterator Performance — When to Be Careful
Because iterators evaluate an expression for every row in the table, their performance scales with the number of rows and the complexity of the expression. The patterns below help you get correct results without unnecessary performance costs.
| Situation | Guidance |
|---|---|
| Summing a stored column | Use SUM(), not SUMX(Orders, Orders[Revenue]). SUM is a native aggregation and is faster than iterating over rows. SUMX(table, column) and SUM(column) return the same result but SUM is more efficient. |
| SUMX on a large fact table with a complex expression | Keep the expression simple — avoid nesting CALCULATE inside SUMX unless necessary. Each CALCULATE inside an iterator triggers a full context transition per row, which can be expensive on million-row tables. |
| AVERAGEX vs AVERAGE | If the expression is just a column reference, AVERAGE(column) is faster. Use AVERAGEX only when you need to compute something per row first — a ratio, a conditional, a multi-column formula. |
| RANKX on a large dimension | RANKX evaluates the expression for every row in the first argument table. On a 10,000-product table, it evaluates the measure 10,000 times per visual cell. Use ALLSELECTED instead of ALL to limit the ranking to visible rows, reducing iterations. |
Teacher's Note: The single most common iterator mistake is using AVERAGEX when AVERAGE would give the same result — adding unnecessary iteration overhead. The rule is simple: if the expression in AVERAGEX is just a column name with no arithmetic, use AVERAGE instead. Use AVERAGEX when you need the average of a calculation that does not exist as a column. The same principle applies to SUMX vs SUM — reach for the simpler function first, and only escalate to the iterator when the simpler version cannot express what you need.
Practice
Practice 1 of 3
To calculate the total revenue from component columns (UnitPrice × Quantity × (1 − Discount)) where each row's result must be computed before summing, you use ___(Orders, Orders[UnitPrice] * Orders[Quantity] * (1 - Orders[Discount%])).
Practice 2 of 3
The average of per-order margin percentages — treating each order equally regardless of size — is computed with AVERAGEX(Orders, DIVIDE([Revenue]-[Cost],[Revenue])). This gives a different result from DIVIDE(SUM([Revenue]-[Cost]), SUM([Revenue])), which is the revenue-___ average margin.
Practice 3 of 3
In RANKX, passing ___ as the fifth argument means tied values receive consecutive ranks with no gaps — so if two products are tied for second place, the next rank is third, not fourth.
Lesson Quiz
Quiz 1 of 3
A colleague writes Avg Margin = DIVIDE(AVERAGE(Orders[Revenue]), AVERAGE(Orders[Cost])) to calculate average margin ratio. You say this is wrong. Why, and what is the correct formula?
Quiz 2 of 3
You write Total Revenue = SUMX(Orders, Orders[Revenue]). A senior colleague replaces it with Total Revenue = SUM(Orders[Revenue]). Both return the same number. Why did the colleague change it?
Quiz 3 of 3
A RANKX measure uses ALL(Products) as the first argument. When a user applies a Category slicer to filter to Electronics only, every product still shows its rank across all products — not its rank within Electronics. What is the fix?
Next up — Lesson 39 covers Advanced DAX Patterns, including the classic problems every Power BI developer encounters — dynamic segmentation, ABC analysis, top-N filtering, conditional formatting measures, and the disconnected slicer table pattern.