Power BI Lesson 38 – Iterator Functions | Dataplexa
DAX · Lesson 38

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

Orders table — used for all examples
OrderIDUnitPriceQuantityDiscount %Revenue (stored)Region
100135040%1,400North
1002250510%1,125South
100350025%950North
10048010%80South
100528060%1,680West
Revenue (stored) = UnitPrice × Quantity × (1 - Discount%) · computed in Power Query and loaded as a column · used to show when SUMX matches vs differs

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.

Phase 1 — Iterate (row context)
The function processes each row of the table one at a time. For each row, it evaluates the expression argument — which can reference any column in that row. This creates a temporary column of results, one per row. Row context is active throughout phase 1.
Phase 2 — Aggregate (collapse to scalar)
Once every row has been evaluated, the function aggregates the temporary results into a single value — summing them (SUMX), averaging them (AVERAGEX), finding the maximum (MAXX), and so on. The row context from phase 1 is gone; only the aggregated result remains.

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.
SUMX row-by-row evaluation — Phase 1 results before aggregation
OrderID UnitPrice Qty Discount Phase 1 result per row
100135040%1,400
1002250510%1,125
100350025%950
10048010%80
100528060%1,680
Phase 2 — SUM of all per-row results5,235
SUMX = SUM here because Revenue was pre-computed with the same formula · SUMX matters when the formula can't be pre-computed or uses measures

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
AVERAGEX — average revenue per unit, row by row
OrderID Revenue Quantity Rev / Unit (per row)
10011,4004350.00
10021,1255225.00
10039502475.00
100480180.00
10051,6806280.00
AVERAGEX result (average of per-row ratios)282.00
WRONG approach: AVERAGE(Revenue) / AVERAGE(Quantity) = 1047 / 3.6 = 290.8 — mathematically incorrect · AVERAGEX = 282.0 — the true average of per-order unit prices

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
Weighted Avg Margin % vs equal-weight AVERAGEX margin
OrderID Revenue Cost Margin per row Margin % per row
10011,40090050035.7%
10021,12578034530.7%
100395059036037.9%
100480552531.3%
10051,6801,05063037.5%
Totals1,860
Weighted Avg Margin % (SUMX / SUM)
1,860 / 5,235 = 35.5% ← revenue-weighted, correct for P&L
AVERAGEX margin % (equal weight)
(35.7+30.7+37.9+31.3+37.5)/5 = 34.6% ← treats $80 order same as $1,680 order
For financial reporting use Weighted Avg Margin · for per-order analysis use AVERAGEX · both are correct for their purpose

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
All six X functions — results against the Orders dataset
SUMX — Total Gross Profit
1,860
(500+345+360+25+630)
AVERAGEX — Avg Gross Profit Per Order
372
(1,860 / 5 orders)
MINX — Min Order Margin %
31.3%
(order 1004: 25/80)
MAXX — Max Order Margin %
37.9%
(order 1003: 360/950)
COUNTX — Orders With Margin
5
(all 5 orders have Revenue > Cost)

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
    )
Product Revenue Rank — table visual, Region on rows
Region Total Revenue Revenue Rank
West1,6801
North2,3501
South1,2053
Total5,2351
North (2,350) and West (1,680) are tied... wait — North has 2 orders (1001+1003=2,350) and West has 1 (1005=1,680). Ranks: North=1, West=2, South=3. Total row gets rank 1 because the grand total is highest of all. DENSE means no gaps when there are no ties here.
Corrected ranking (no tie):
RegionRevenueRank (DESC DENSE)
North2,3501
West1,6802
South1,2053

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.