Funciones de tabla en DAX: FILTER, ADDCOLUMNS y el coste de la materialización

Funciones de tabla en DAX: FILTER, ADDCOLUMNS y el coste de la materialización

0
(0)

En el desarrollo de modelos de datos complejos, las funciones de tabla son las herramientas que nos permiten ir más allá de las agregaciones simples. Sin embargo, en mi experiencia como consultor en sectores como el retail o la industria, donde manejamos tablas de hechos con decenas de millones de filas, estas funciones suelen ser la causa principal de que un informe pase de ser fluido a ser inmanejable. El problema no suele ser la lógica de negocio, sino el desconocimiento de cómo el motor de DAX gestiona la memoria y la CPU al trabajar con tablas virtuales.

Entender la diferencia entre operar sobre columnas y operar sobre tablas completas es la línea que separa a un desarrollador junior de un arquitecto de datos senior. En este artículo vamos a profundizar en por qué funciones como FILTER, ADDCOLUMNS y SELECTCOLUMNS pueden convertirse en cuellos de botella si no se comprende el concepto de materialización y el comportamiento del Motor de Fórmulas (Formula Engine) frente al Motor de Almacenamiento (Storage Engine).

Antes de entrar en optimizaciones, es fundamental dominar los cimientos. Si tienes dudas sobre cómo se propagan los filtros, te recomiendo revisar estos Conceptos avanzados de DAX: Row Context, Filter Context, Iteradores y más.

El peligro de FILTER sobre tablas completas

Uno de los errores más recurrentes que veo en auditorías de rendimiento es el uso de FILTER pasando como primer argumento una tabla completa de hechos. La función FILTER es un iterador: recorre fila por fila la tabla que recibe. Si le pasas la tabla de ‘Ventas’ con 50 millones de filas para filtrar un solo producto, estás obligando al Motor de Fórmulas a evaluar 50 millones de condiciones.

Cuando usas FILTER(Ventas, Ventas[ID_Producto] = 10), el motor no puede aprovechar los índices del Motor de Almacenamiento (VertiPaq) de forma eficiente. En su lugar, debe materializar la tabla en memoria para iterarla. La regla de oro es clara: filtra siempre columnas, no tablas. En lugar de filtrar la tabla de hechos, filtra la tabla de dimensiones o usa KEEPFILTERS sobre una columna específica.

Este comportamiento es una de las razones principales por las que CALCULATE es más eficiente que FILTER en la gran mayoría de escenarios. CALCULATE permite que el motor utilice filtros directos sobre el Storage Engine, lo cual es órdenes de magnitud más rápido que una iteración en el Formula Engine.

-- EJEMPLO DE MALA PRÁCTICA
-- Itera toda la tabla de ventas, incluso columnas que no necesita.
CALCULATE(
 [Total Importe],
 FILTER( Ventas, Ventas[Estado] = "Completado" )
)

-- EJEMPLO OPTIMIZADO
-- Solo itera sobre los valores distintos de la columna 'Estado'.
CALCULATE(
 [Total Importe],
 KEEPFILTERS( Ventas[Estado] = "Completado" )
)

Materialización: El coste invisible

La materialización ocurre cuando una consulta DAX requiere la creación de una tabla temporal en la memoria RAM para realizar operaciones que el Storage Engine no puede resolver por sí solo. Esto sucede frecuentemente cuando anidamos funciones de tabla o cuando usamos iteradores complejos. El problema de la materialización es doble: consume memoria RAM de forma exponencial y bloquea un hilo de la CPU en el Formula Engine, que es monohilo por naturaleza para estas tareas.

En proyectos de sector público con grandes volúmenes de datos, he visto informes caer por falta de memoria simplemente porque una medida intentaba crear una tabla virtual cruzando dos dimensiones de alta cardinalidad. El motor VertiPaq es excelente comprimiendo columnas, pero cuando materializas una tabla, esa compresión desaparece y los datos se almacenan «en bruto» en la memoria de trabajo.

Para profundizar en cómo estas funciones interactúan con el modelo, es útil entender las DAX Table Functions y cómo cada una afecta al linaje de los datos y a la memoria del servidor.

ADDCOLUMNS vs SELECTCOLUMNS: No son intercambiables

Aunque ambas funciones sirven para crear o modificar tablas virtuales, su impacto en el rendimiento es muy distinto debido a cómo gestionan el linaje y las columnas existentes. ADDCOLUMNS mantiene todas las columnas de la tabla original y añade las nuevas. SELECTCOLUMNS, por el contrario, solo mantiene las columnas que tú defines explícitamente.

En términos de rendimiento, SELECTCOLUMNS suele ser superior cuando trabajamos con tablas anchas (muchas columnas). Al limitar el número de columnas que se mantienen en la tabla virtual, reducimos drásticamente el tamaño de la materialización. Si solo necesitas dos columnas para un cálculo intermedio, no arrastres las otras 40 que pueda tener tu tabla de ‘Clientes’.

La trampa de la transición de contexto en ADDCOLUMNS

Un error crítico al usar ADDCOLUMNS es invocar medidas dentro de ella sin entender la transición de contexto. Cada vez que llamas a una medida dentro de un iterador como ADDCOLUMNS, se produce un CALCULATE implícito que transforma el Contexto de Fila en Contexto de Filtro. Si la tabla que estás iterando es grande, esta transición de contexto se ejecutará millones de veces, destrozando el tiempo de respuesta. Para más detalle sobre este fenómeno, consulta la guía sobre Interacciones entre el Row Context y el Filter Context con DAX.

CaracterísticaFILTERADDCOLUMNSSELECTCOLUMNS
Propósito principalReducir filas de una tablaAñadir columnas a una tabla existenteProyectar columnas específicas (vistas)
Impacto en memoriaAlto si se aplica a tablas de hechosMuy alto (mantiene columnas originales)Controlado (solo columnas seleccionadas)
IteradorSíSíSí
Uso recomendadoFiltros complejos no expresables en CALCULATECreación de tablas intermedias pequeñasOptimización de tablas virtuales para iteraciones

Casos de uso reales: El patrón de optimización

Imagina un escenario en una empresa de energía donde necesitamos calcular el consumo medio diario, pero solo para aquellos días donde hubo una incidencia técnica registrada. Si intentamos filtrar la tabla de hechos de ‘Lecturas’ por la tabla de ‘Incidencias’ usando un FILTER mal planteado, la consulta fallará por tiempo de espera.

El enfoque correcto consiste en pre-filtrar las claves necesarias usando SUMMARIZE o VALUES y luego utilizar esa tabla reducida para filtrar el resto del modelo. Esto minimiza el trabajo del Motor de Fórmulas.

-- PATRÓN DE OPTIMIZACIÓN: REDUCIR ANTES DE AGREGAR
VAR DiasConIncidencia = 
 SELECTCOLUMNS(
 FILTER( 'Incidencias', 'Incidencias[Gravedad] = "Alta" ),
 "Fecha", 'Incidencias'[FechaIncidencia]
 )

RETURN
CALCULATE(
 AVERAGEX( DiasConIncidencia, [ConsumoTotal] ),
 TREATAS( DiasConIncidencia, 'Calendario'[Fecha] )
)

En este ejemplo, SELECTCOLUMNS crea una lista mínima de fechas, reduciendo la carga de memoria. El uso de TREATAS es una técnica avanzada para aplicar esos filtros de forma eficiente sin necesidad de relaciones físicas pesadas en el modelo si no fueran necesarias.

Errores frecuentes que destruyen el rendimiento

  • Filtrar la tabla de hechos en lugar de la dimensión: Este es el error número uno. Siempre que sea posible, aplica los filtros en el lado «uno» de la relación.
  • Uso excesivo de ALL(Tabla): Al usar FILTER(ALL(Tabla), ...), estás forzando al motor a ignorar todos los filtros existentes y a escanear la tabla completa. Si solo necesitas quitar el filtro de una columna, usa ALL(Tabla[Columna]).
  • Anidamiento de iteradores: Un FILTER dentro de un ADDCOLUMNS sobre tablas grandes crea una complejidad cuadrática (N*M) que el motor difícilmente puede optimizar.
  • No considerar la cardinalidad: Crear tablas virtuales sobre columnas con millones de valores únicos (como IDs de transacción o timestamps) es una receta para el desastre en términos de materialización.
  • Ignorar la diferencia entre medidas y columnas: Si intentas usar estas funciones para crear columnas calculadas en tablas masivas, aumentarás el tamaño del archivo .pbix y el tiempo de refresco innecesariamente. Consulta la diferencia entre columnas calculadas y medidas en DAX antes de decidirte.

Resumen y checklist de optimización

Dominar las funciones de tabla requiere un cambio de mentalidad: dejar de pensar en filas individuales y empezar a pensar en el coste de los conjuntos de datos que estamos moviendo entre el Storage Engine y el Formula Engine. Cada vez que escribas una función de tabla, pregúntate si estás obligando al motor a hacer un trabajo que podría evitarse simplificando el filtro.

  1. ¿He filtrado por columnas específicas en lugar de por la tabla completa?
  2. ¿He usado SELECTCOLUMNS para limitar la cantidad de datos materializados?
  3. ¿Puedo sustituir un FILTER complejo por un CALCULATE con filtros simples?
  4. ¿La cardinalidad de las columnas en mi tabla virtual es la mínima necesaria?
  5. ¿He verificado en el DAX Studio si mi consulta genera CallbackDataID excesivos?

Preguntas frecuentes

¿Cuándo es aceptable usar FILTER sobre una tabla completa?

Solo es aceptable cuando la tabla es muy pequeña (tablas de parámetros o dimensiones con pocas filas) o cuando la lógica de filtrado requiere comparar múltiples columnas de la misma fila de forma que no se puede expresar con filtros de columna simples.

¿Por qué SELECTCOLUMNS es más rápido que ADDCOLUMNS?

No siempre es más rápido por la ejecución en sí, sino por la memoria. ADDCOLUMNS arrastra el linaje y todas las columnas de la tabla origen al Motor de Fórmulas, mientras que SELECTCOLUMNS genera una estructura mucho más ligera y específica, lo que reduce la presión sobre la RAM.

¿Qué significa que una tabla se «materializa» en DAX?

Significa que los datos dejan de estar comprimidos en el motor VertiPaq y se crea una estructura de datos temporal en la memoria RAM para que el Formula Engine pueda procesarla. Si esta tabla es grande, puede agotar la memoria disponible o ralentizar drásticamente el informe.


¿De cuánta utilidad te ha parecido este contenido?

¡Haz clic en una estrella para puntuarlo!

Puntuación media 0 / 5. Recuento de votos: 0

Hasta ahora, ¡no hay votos!. Sé el primero en puntuar este contenido.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *