Power BI · Capítulo 7 · Lección de conceptos · 7 min de lectura

Iteradores, contexto de fila y transición de contexto

Por qué SUM(a) * SUM(b) es un ingreso equivocado, qué hace de verdad un iterador y el movimiento, la transición de contexto, que deja que la fila donde estás filtre el resto del modelo.

La idea

Casi todo el DAX que has escrito hasta ahora lee una columna entera: SUM(order_items[line_total]). Un iterador es la otra forma de leer una tabla. SUMX, AVERAGEX, MAXX, RANKX y COUNTX reciben una tabla y una expresión, visitan la tabla fila por fila, calculan la expresión en esa fila y solo después reúnen los resultados (los suman, los promedian, los ordenan).

Mientras el iterador está parado en una fila, las columnas de esa fila están disponibles por su nombre. Esa "fila actual" es el contexto de fila. Es una posición en una tabla y nada más. No es un filtro, y esa diferencia está detrás de casi todos los errores con iteradores.

Figura 1

SUMX recorriendo el pedido 1004, línea por línea

order_items del pedido 1004
order_item_idquantityunit_pricediscount_pct
10192.9910
11220.9910
121149.0010
13266.9910
Resultado por fila
order_item_idnet
1083.69
1137.78
12134.10
13120.58
SUMX
Net Revenue
$376.15

Paso 1 de 5 Cuatro líneas de un pedido real. SUMX(order_items, quantity * unit_price * (1 - discount_pct / 100)) las visitará una a una.

  1. Cuatro líneas de un pedido real. SUMX(order_items, quantity * unit_price * (1 - discount_pct / 100)) las visitará una a una.
  2. Línea 10: mientras el iterador está parado en esta fila, quantity, unit_price y discount_pct significan los valores de esta fila. Eso es el contexto de fila. 1 x 92.99 x 0.9 = 83.69.
  3. Las líneas 11, 12 y 13 siguen el mismo camino. Cada una se calcula por separado antes de sumar nada.
  4. Solo ahora suma la función X: 83.69 + 37.78 + 134.10 + 120.58 = 376.15.
  5. SUM(quantity) * SUM(unit_price) daría 6 x 329.97 = 1,979.82: cada unidad por cada precio. Multiplicar antes de sumar es justo lo que hace un iterador.

Por qué existe

La Finance Controller de Harbor & Pine quiere el ingreso neto construido a partir de quantity, unit_price y discount_pct. El número que necesita no existe como columna: es un producto, por línea, que después hay que sumar. SUM solo puede sumar una columna que ya está ahí.

Quien viene de las hojas de cálculo agregaría una columna auxiliar, la rellenaría hacia abajo y la sumaría. Un iterador es esa columna auxiliar sin la columna: la multiplicación ocurre en memoria, justo para las filas que la celda puede ver, y no se guarda nada en el modelo. El archivo pesa menos y el número sigue vivo: cambia el segmentador y cambian las filas que se recorren.

Las funciones X son una familia con una sola forma: SUMX, AVERAGEX, MINX, MAXX y COUNTX solo se diferencian en cómo reúnen los resultados por fila. Aprende una y las sabes todas.

Cómo funciona por dentro

SUMX(tabla, expresión) se ejecuta en dos etapas, y el orden importa.

  1. Decidir qué filas existen. El contexto de filtro de la celda (encabezado de fila, segmentadores, filtros de CALCULATE) reduce primero tabla. Un SUMX sobre order_items en la fila de Lighting de la matriz recorre solo las líneas de Lighting.
  2. Recorrerlas. Para cada fila que queda, se evalúa la expresión con esa fila como contexto de fila y luego se combinan los resultados.

Como la expresión se ejecuta una vez por fila, puede usar columnas de la fila (order_items[quantity]) y, por una relación de muchos a uno, columnas de la fila padre (RELATED(products[unit_cost])). En sentido contrario no llega sin ayuda: desde un producto no ves automáticamente sus líneas.

Transición de contexto

Ese "sin ayuda" es el caso interesante. Dentro de una columna calculada o de un iterador, un SUM(order_items[line_total]) normal ignora la fila en la que estás. El producto actual es una posición, no un filtro, así que nada llega a order_items.

CALCULATE lo arregla. Cuando se ejecuta dentro de un contexto de fila, toma cada columna de la fila actual, las convierte en filtros y evalúa su expresión bajo esos filtros. Eso es la transición de contexto. Para el producto 135 se convierte en product_id = 135, que baja por la relación hasta order_items como lo haría cualquier segmentador.

Figura 2

Una columna calculada en products: contexto de fila frente a un filtro

products

  • product_id
  • name
  • category

product_id = 135

orders

  • order_id
  • channel
  • status

order_items

  • order_id
  • product_id
  • line_total

Paso 1 de 3 Una columna calculada se evalúa una vez por cada fila de products. Para el producto 135 el motor está parado en esa fila: un contexto de fila.

  1. Una columna calculada se evalúa una vez por cada fila de products. Para el producto 135 el motor está parado en esa fila: un contexto de fila.
  2. SUM(order_items[line_total]) le pide filas a order_items. Nada las ha filtrado: un contexto de fila no es un filtro y no viaja por las relaciones.
  3. Envuélvelo en CALCULATE. La transición de contexto convierte la fila actual en un filtro, product_id = 135, y ese filtro baja como cualquier otro.

Con números se entiende mejor: las mismas dos fórmulas en una columna calculada de products.

Figura 3

La misma columna, con y sin CALCULATE

products, dos columnas calculadas
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

Paso 1 de 2 bare_sum = SUM(order_items[line_total]). Todos los productos muestran las ventas de toda la tienda.

  1. bare_sum = SUM(order_items[line_total]). Todos los productos muestran las ventas de toda la tienda.
  2. with_calculate = CALCULATE(SUM(order_items[line_total])). Ahora cada producto suma solo sus propias líneas.

Elegir la tabla que se recorre

La tabla que le pasas al iterador decide qué es un "elemento". AVERAGEX(order_items, ...) es un promedio por línea de pedido. AVERAGEX(orders, [Sales]) es un promedio por pedido, que es lo que significa "valor medio del pedido": se visita cada pedido, se suman sus propias líneas mediante la transición de contexto y se promedian esos totales. La misma función con otra tabla responde otra pregunta de negocio. Antes de escribir el primer argumento, pregúntate qué es una unidad de ese promedio.

La misma idea sirve para un margen ponderado. Recorre order_items y, en cada línea, sube a su producto con RELATED(products[unit_cost]). El costo se multiplica por la cantidad de esa línea, así que los productos que más se venden pesan más. Promediar el margen de los 120 productos daría a todos el mismo peso, y no es lo que vio la caja.

Las medidas llevan un CALCULATE invisible

Una referencia a una medida, [Sales], se comporta como si estuviera envuelta en CALCULATE. Por eso SUMX(products, [Sales]) devuelve las ventas propias de cada producto sumadas, mientras que SUMX(products, SUM(order_items[line_total])) repite el total de la tienda una vez por producto. La primera convierte la fila en un filtro; la segunda no.

También significa que los iteradores tienen un precio. Una medida dentro de un SUMX sobre una tabla grande dispara una transición por fila, lo cual es lento y, con cien mil filas, se nota. Itera sobre la tabla pequeña (products, 120 filas) y no sobre la grande (líneas de pedido, más de 30,000) cuando la respuesta es la misma.

Dónde probarlo en Power BI Desktop

Usa Home → New measure para un iterador y Table tools → New column para una columna calculada. Para ver qué hace un contexto de fila, escribe la misma expresión en ambos sitios y compara. La DAX query view te deja ejecutar EVALUATE ADDCOLUMNS(products, "x", [Sales]) y leer el resultado como una tabla.

Errores comunes

"SUM(quantity) * SUM(price) es lo mismo que sumar cantidad por precio"

Multiplicar dos totales multiplica cada cantidad por cada precio, no el par de cada línea. Para el pedido 1004 da 1,979.82 en vez de 376.15, y la diferencia crece con la tabla. Siempre que un número sea un producto o un cociente de dos columnas por fila, es un iterador.

"Un contexto de fila filtra las otras tablas"

No. Estar parado en la fila de un producto solo indica de qué fila leer las columnas, nada más. La tabla order_items ve todas las líneas hasta que algo crea un filtro. Ese algo es CALCULATE o una referencia a una medida.

"Los iteradores son siempre más lentos, así que hay que evitarlos"

Un iterador sobre una expresión de columnas (SUMX(order_items, quantity * unit_price)) es barato: lo resuelve el motor de almacenamiento. El patrón lento es una medida evaluada una vez por cada fila de una tabla grande. Elige la tabla más pequeña que dé la respuesta correcta y mantén la expresión simple.

Ponte a prueba

Una pregunta. Al responderla, la lección queda marcada en el mapa; nunca bloquea un nivel.

En products agregas dos columnas calculadas, A = SUM(order_items[line_total]) y B = CALCULATE(SUM(order_items[line_total])). ¿Qué muestra la fila de Ridge Ceramic Chandelier?

Elige qué puede ejecutarse en este sitio. Puedes cambiarlo cuando quieras desde "Configurar cookies", al pie de cada página.