| |

Rendimiento DAX

5
(1)



Análisis Detallado de Rendimiento DAX en DirectQuery

🔍 Análisis Detallado de Rendimiento DAX en DirectQuery

Guía completa para identificar, diagnosticar y corregir los anti-patrones que destruyen el rendimiento de tus medidas en modelos DirectQuery — Agosto 2026

📑 Tabla de Contenidos

  1. El problema de DirectQuery: por qué el DAX importa el doble
  2. Herramientas de diagnóstico: DAX Studio y Performance Analyzer
  3. Los 8 anti-patrones más destructivos en DirectQuery
  4. Caso práctico: auditoría del modelo de ejemplo
  5. Patrones de reescritura avanzada
  6. Checklist de optimización y mantenimiento
  7. Conclusión y próximos pasos

1. El problema de DirectQuery: por qué el DAX importa el doble

En Import Mode, Power BI carga los datos en el motor VertiPaq (columnar, comprimido, en memoria). Una medida DAX mal escrita puede ser lenta, pero el Storage Engine (SE) resuelve la mayoría de las agregaciones en milisegundos gracias a la compresión y los índices columnares.

En DirectQuery, cada medida genera una o más consultas SQL que se envían a la fuente de datos remota. Si tu DAX fuerza iteraciones fila a fila, no es Power BI quien paga el coste: es tu base de datos relacional ejecutando miles de subconsultas anidadas. En DirectQuery, un anti-patrón DAX no es solo lento: puede colapsar el servidor de origen.

⚡ Diferencia clave: Import vs. DirectQuery

AspectoImport ModeDirectQuery
Motor de almacenamientoVertiPaq (in-memory)Fuente relacional (SQL)
Iterador sobre tabla de hechosFE itera en memoria (~ms)Genera N consultas SQL
Context transitionLookup en caché comprimidaJOIN + subconsulta SQL
DISTINCTCOUNT alta cardinalidadScan columnar optimizadoGROUP BY + COUNT(DISTINCT)
Tiempo de respuesta objetivo< 500 ms< 3 segundos

🚨 Advertencia crítica: En DirectQuery, cada medida en un visual genera una consulta SQL independiente. Si tienes 12 visuales en una página y cada uno usa 3 medidas, estás lanzando hasta 36 consultas simultáneas contra tu base de datos. Un solo anti-patrón multiplicado por 36 puede saturar la conexión.

2. Herramientas de diagnóstico: DAX Studio y Performance Analyzer

2.1 Performance Analyzer (Power BI Desktop)

Es tu primera línea de defensa. Ve a Vista → Performance Analyzer → Iniciar grabación. Interactúa con el reporte y obtendrás tres métricas por visual:

  • Tiempo DAX (ms): Lo que tarda en ejecutarse la consulta DAX. Si esto es alto, el problema está en la medida.
  • Tiempo Visual (ms): Renderizado del gráfico en el cliente. Normalmente < 50 ms.
  • Tiempo Otros (ms): Latencia de red, esperas entre consultas.

💡 Regla de oro: Si Tiempo DAX representa más del 90% del tiempo total, el problema es DAX puro. Si Tiempo Otros es alto, revisa la latencia de red o el gateway.

2.2 DAX Studio: análisis de Query Plan y Server Timings

Conecta DAX Studio a tu modelo, activa Server Timings y Query Plan, y pega la consulta DAX que copiaste del Performance Analyzer. Verás:

MétricaQué indicaUmbral crítico
FE Time (Formula Engine)Tiempo de cálculo fila a fila> 50% del total
SE Time (Storage Engine)Tiempo de scan/agregación> 3 segundos
CallbackDataIDCallbacks de FE a SE (iteración)> 10 por consulta
VertiPaq Scan / SQL QueryConsultas SQL generadas> 5 por visual
RecordsFilas procesadas> 1M sin filtro previo

🔬 Cómo interpretar el split FE vs. SE en DirectQuery

En DirectQuery, el SE no es VertiPaq: es tu base de datos SQL. DAX Studio muestra las consultas SQL traducidas. Si ves múltiples SQL Queries para una sola medida, significa que el FE está fragmentando el trabajo y no consigue empujar todo al origen.

Objetivo: Maximizar el tiempo en SE (SQL) y minimizar CallbackDataID. Un plan ideal muestra 1-2 consultas SQL con agregaciones GROUP BY nativas y FE Time cercano a cero.

3. Los 8 anti-patrones más destructivos en DirectQuery

A continuación, los anti-patrones que más daño causan en entornos DirectQuery, ordenados por severidad. Cada uno incluye: por qué es lento en DirectQuery, cómo detectarlo, y la reescritura correcta.

CRÍTICO #1 SUMX / AVERAGEX / COUNTX sobre tabla de hechos

¿Por qué destruye el rendimiento en DirectQuery?

SUMX itera fila a fila sobre la tabla de hechos. En Import, el FE hace esto en memoria comprimida. En DirectQuery, cada iteración puede forzar al FE a solicitar datos al SE (SQL) fila a fila, generando un CallbackDataID por cada fila. Con 1 millón de filas, eso son 1 millón de callbacks.

— ANTI-PATRÓN: iterador sobre hechos
Total Sales SUMX =
SUMX(Sales, Sales[Qty] * Sales[Price])

Genera: 1 callback por fila de Sales. Si Sales tiene 5M filas → 5M callbacks SQL.

— SOLUCIÓN A: columna pre-calculada en Power Query
— Añadir [LineTotal] = [Qty] * [Price] en la fuente

Total Sales = SUM(Sales[LineTotal])

— SOLUCIÓN B: si no es posible, usar SUM directo
— (solo si la multiplicación ya existe como columna)

Genera: 1 consulta SQL con SUM agrupado. Cero callbacks.

— ANTI-PATRÓN: condición dentro del iterador
London Sales =
SUMX(Sales,
IF(Sales[Region] = «London»,
Sales[Amount], BLANK()     )   )

— SOLUCIÓN: filtro fuera del iterador
London Sales =
CALCULATE(
SUM(Sales[Amount]),
Sales[Region] = «London»
  )

💡 Detección automática: Usa la medida _Medidas con SUMX del cuadro de mando. Si devuelve >0 en un modelo DirectQuery, revisa inmediatamente.

CRÍTICO #2 CallbackDataID por filtros dinámicos en iteradores

¿Qué es CallbackDataID?

Es el evento que registra DAX Studio cuando el Formula Engine necesita pedir datos al Storage Engine fila a fila durante una iteración. En DirectQuery, cada CallbackDataID se traduce en una subconsulta SQL. Es el peor enemigo del rendimiento.

— ANTI-PATRÓN: FILTER anidado + CALCULATE
Sales con Descuento =
SUMX(Sales,
VAR _Check =
CALCULATE(
COUNTROWS(Sales),
FILTER(ALL(Sales),
Sales[Region] = «London»         )
      )
RETURN
IF(_Check > 0, Sales[Amount], BLANK())
  )

Query plan: 104 CallbackDataID. FE: 44 ms. SE: 3 ms.

— SOLUCIÓN: predicado simple empujado a SQL
Sales con Descuento =
CALCULATE(
SUM(Sales[Amount]),
Sales[Region] = «London»
  )

Query plan: 5 CallbackDataID. FE: 2 ms. SE: 3 ms.

🚨 En DirectQuery, el impacto es exponencial: Un CallbackDataID en Import puede costar 0.01 ms. En DirectQuery contra una base de datos remota, cada callback puede costar 50-200 ms por latencia de red. 100 callbacks = 5-20 segundos de espera.

ALTO #3 FILTER sobre tabla de hechos completa

¿Por qué es peligroso en DirectQuery?

FILTER(Sales, …) materializa la tabla de hechos completa en memoria antes de aplicar la condición. En DirectQuery, esto fuerza un SELECT * FROM Sales completo desde la fuente. Si Sales tiene 100M filas, estás transfiriendo 100M filas por red solo para filtrar 5 de ellas.

— ANTI-PATRÓN: FILTER sobre hechos
Filtered Sales =
CALCULATE(
SUM(Sales[Amount]),
FILTER(Sales, Sales[Region] = «East»)
  )

— SOLUCIÓN A: predicado directo en CALCULATE
Filtered Sales =
CALCULATE(
SUM(Sales[Amount]),
Sales[Region] = «East»
  )

— SOLUCIÓN B: filtro sobre dimensión
Filtered Sales =
CALCULATE(
SUM(Sales[Amount]),
DimRegion[RegionName] = «East»
  )

💡 Regla mnemotécnica: Nunca pongas el nombre de una tabla de hechos dentro de FILTER(). Si ves FILTER(Sales o FILTER(Fact, es una señal de alarma. El filtro debe ir sobre una dimensión o como predicado de columna en CALCULATE.

ALTO #4 SUMMARIZECOLUMNS dentro de medidas con filtros dinámicos

El problema específico de DirectQuery

SUMMARIZECOLUMNS está optimizado para consultas de reporte (queries de visual), pero cuando se usa dentro de una medida con filtros dinámicos, rompe la fusión de consultas SQL. Cada medida con SUMMARIZECOLUMNS genera su propio SELECT ... GROUP BY en la fuente, impidiendo que Power BI agrupe múltiples medidas en una sola consulta SQL.

— ANTI-PATRÓN: SUMMARIZECOLUMNS en medida
Top Products =
SUMMARIZECOLUMNS(
Sales[Product],
«Sales», SUM(Sales[Amount])
  )

— SOLUCIÓN: SUMX + VALUES o ADDCOLUMNS
Top Products =
SUMX(
VALUES(Sales[Product]),
CALCULATE(SUM(Sales[Amount]))
  )

— O mejor aún, si Product es dimensión:
Top Products =
SUMX(
VALUES(DimProduct[ProductName]),
    [Total Sales]
  )

ALTO #5 DISTINCTCOUNT sobre columnas de alta cardinalidad en DirectQuery

El coste real en SQL

DISTINCTCOUNT genera COUNT(DISTINCT columna) en SQL. Sobre una columna de texto con millones de valores únicos (emails, GUIDs, descripciones), el motor de base de datos debe ordenar y deduplicar toda la columna. En DirectQuery, esto no se cachea: cada interacción del usuario vuelve a ejecutar el COUNT DISTINCT completo.

— ANTI-PATRÓN: DISTINCTCOUNT en texto
Unique Customers =
DISTINCTCOUNT(Sales[CustomerEmail])

SQL generado: SELECT COUNT(DISTINCT CustomerEmail) FROM Sales

— SOLUCIÓN A: usar ID numérico (relación)
Unique Customers =
DISTINCTCOUNT(Sales[CustomerID])

— SOLUCIÓN B: aproximación (si es aceptable)
Unique Customers Approx =
SUMX(VALUES(Sales[CustomerID]), 1)

— SOLUCIÓN C: pre-agregado en la fuente
— Crear vista SQL: SELECT COUNT(DISTINCT CustomerID) …

💡 Alternativa avanzada: Si CustomerID es clave primaria de la dimensión, usa COUNTROWS(DimCustomer) en lugar de DISTINCTCOUNT. Es una simple consulta SELECT COUNT(*) sobre la tabla de dimensiones, mucho más rápida.

MEDIO #6 ALL / REMOVEFILTERS indiscriminados sobre tablas grandes

Impacto en DirectQuery

ALL(Sales) elimina todos los filtros de la tabla de hechos. En DirectQuery, esto fuerza un SELECT SUM(Amount) FROM Sales sin WHERE clause, escaneando toda la tabla remota. Si Sales tiene 500M filas, esto es un scan completo de 500M filas por cada celda del visual.

— ANTI-PATRÓN: ALL sobre tabla grande
All Sales =
CALCULATE(
SUM(Sales[Amount]),
ALL(Sales)
  )

— SOLUCIÓN A: ALLSELECTED (respeta selección)
All Sales Selected =
CALCULATE(
SUM(Sales[Amount]),
ALLSELECTED(Sales)
  )

— SOLUCIÓN B: ALLEXCEPT (preserva dimensión)
All Sales by Region =
CALCULATE(
SUM(Sales[Amount]),
ALLEXCEPT(Sales, Sales[Region])
  )

ALTO #7 DirectQuery sin agregaciones automáticas ni tablas de agregación

La solución arquitectónica

Este no es un anti-patrón DAX puro, sino una decisión de modelado. En DirectQuery, cada visual consulta la fuente en tiempo real. Si tienes un gráfico de barras mensual sobre 5 años de datos transaccionales, Power BI está ejecutando SELECT ... GROUP BY Month sobre miles de millones de filas cada vez que el usuario cambia un filtro.

— ESTRATEGIA: Tabla de agregación mensual en Import (Dual)
— 1. Crear tabla Agg_Sales_Month en la fuente o Power Query
— 2. Configurar modo de almacenamiento: Import
— 3. Power BI usará automáticamente Agg_Sales_Month cuando el usuario agrupe por mes

Total Sales = SUM(Sales[Amount])
— Esta medida se evaluará sobre Agg_Sales_Month (Import, rápido)
— en lugar de Sales (DirectQuery, lento) cuando sea posible

💡 Configuración: Modelado → Agregaciones automáticas → Configurar. Define la granularidad de la tabla de agregación (ej. mes + categoría). Power BI redirige las consultas automáticamente sin cambiar el DAX.

MEDIO #8 Funciones de inteligencia temporal nativas en DirectQuery

El problema documentado

Funciones como SAMEPERIODLASTYEAR, DATESYTD, DATEADD y DATESBETWEEN generan SQL complejo con múltiples subconsultas y uniones. En muchos casos, el motor no consigue optimizar la granularidad y devuelve datos a nivel de día cuando solo necesitas mes, forzando al FE a agregar.

— ANTI-PATRÓN: SAMEPERIODLASTYEAR nativo
Sales LY =
CALCULATE(
SUM(Sales[Amount]),
SAMEPERIODLASTYEAR(Date[Date])
  )

SQL generado: subconsulta compleja con DATEADD. FE time: 343 ms.

— SOLUCIÓN: inteligencia temporal manual
Sales LY =
VAR _CurrentYear = SELECTEDVALUE(Date[Year])
VAR _CurrentMonth = SELECTEDVALUE(Date[Month])
RETURN
CALCULATE(
SUM(Sales[Amount]),
ALL(Date),
Date[Year] = _CurrentYear – 1,
Date[Month] = _CurrentMonth
    )

SQL generado: predicados simples con índices. FE time: 20 ms.

⚠️ Nota importante: SAMEPERIODLASTYEAR y DATEADD reciben optimización de granularidad en versiones recientes (agrupan al nivel necesario). Pero DATESBETWEEN, DATESYTD y lógica temporal compleja no se benefician de esta optimización. Usa DAX Studio para verificar qué SQL genera tu medida.

En la siguiente sección vamos a analizar un caso práctico en DirectQuery con la BD de SQL Server. Este caso práctico a partir de un Pbix con medidas de Anti-patrones y medidas corregidas vamos a buscar en los planes de ejecución y en las consultas SQL que se generan en la BD problemas de rendimiento como CallbackDataID, todos los scripts se pueden bajar para hacer el mismo ejercicio en vuestros entornos.





Construcción del PBIX Anti-Patrones DAX – DirectQuery

🛠️ Construcción del PBIX: Anti-Patrones DAX en DirectQuery

Guía completa para crear un informe de Power BI que demuestre los principales problemas de rendimiento con SQL Server en modo DirectQuery

📦 Archivos incluidos en este paquete

ArchivoDescripción
01_Crear_BaseDatos.sqlCrea la base de datos DAXPerformanceDemo, tablas, índices y relaciones
02_Cargar_Datos.sqlScripts BULK INSERT para cargar los CSV generados
03_Monitorizar_Consultas.sqlExtended Events, vistas y procedimientos para monitorizar queries
datos_csv/30 archivos CSV con 3M filas de FactSales + dimensiones
04_Medidas_DAX.txtTodas las medidas listas para copiar y pegar en Power BI
05_DAX_Studio_Guide.txtInstrucciones para capturar CallbackDataID y Query Plans

Paso 1: Preparar SQL Server

Crear la base de datos

Abre SQL Server Management Studio (SSMS), conecta a tu instancia y ejecuta 01_Crear_BaseDatos.sql. Esto creará la base de datos DAXPerformanceDemo con todas las tablas e índices.

Copiar los archivos CSV al servidor

Copia la carpeta datos_csv/ a una ruta accesible por SQL Server, por ejemplo C:\datos_csv\. El servicio SQL Server debe tener permisos de lectura sobre esa carpeta.

Cargar los datos

Ejecuta 02_Cargar_Datos.sql. Cargará ~3 millones de filas en FactSales y las dimensiones. Verifica que el conteo final sea correcto.

Configurar monitorización

Ejecuta 03_Monitorizar_Consultas.sql. Crea la sesión de Extended Events, vistas y procedimientos almacenados para contar las consultas lanzadas por Power BI.

⚠️ Importante: Asegúrate de que la carpeta C:\XEvents\ existe en el servidor SQL antes de iniciar la sesión de Extended Events. Si no existe, créala manualmente.

Paso 2: Crear el modelo en Power BI Desktop (DirectQuery)

Conectar a SQL Server en modo DirectQuery

  • Abre Power BI Desktop → Obtener datos → SQL Server
  • Servidor: localhost (o tu instancia)
  • Base de datos: DAXPerformanceDemo
  • En el navegador, selecciona las 5 tablas: DimDate, DimProduct, DimCustomer, DimRegion, FactSales
  • Crítico: En la ventana de carga, selecciona DirectQuery (no Import)

Configurar relaciones

En la vista de modelo, verifica que las relaciones se hayan detectado automáticamente. Si no, créalas manualmente:

  • FactSales[DateKey]DimDate[DateKey] (1:N, filtro único)
  • FactSales[ProductID]DimProduct[ProductID] (1:N)
  • FactSales[CustomerID]DimCustomer[CustomerID] (1:N)
  • FactSales[RegionID]DimRegion[RegionID] (1:N)

Configurar propiedades de relación (crítico para rendimiento)

Para cada relación, haz doble clic y activa «Asumir integridad referencial». Esto permite que Power BI genere INNER JOINs en lugar de OUTER JOINs, reduciendo drásticamente el tamaño de las consultas SQL.

Paso 3: Crear las medidas DAX (Anti-Patrones vs. Optimizadas)

Crea una tabla dedicada llamada Medidas (Modelado → Nueva tabla → Medidas = {0}) y añade las siguientes medidas. Cada anti-patrón tiene su versión optimizada para comparar rendimiento.

CRÍTICO #1 SUMX sobre tabla de hechos

— ANTI-PATRÓN: SUMX sobre FactSales
— Genera CallbackDataID por cada fila
Total Sales SUMX (MAL) =
SUMX(FactSales, FactSales[Qty] * FactSales[UnitPrice])

— OPTIMIZADO: SUM directo sobre columna existente
Total Sales SUM (BIEN) =
SUM(FactSales[Amount])

CRÍTICO #2 Condición dentro de SUMX (London Sales)

— ANTI-PATRÓN: IF dentro de SUMX
London Sales SUMX (MAL) =
SUMX(FactSales,
IF(FactSales[RegionID] = 6,
FactSales[Amount],
BLANK()
    )
  )

— OPTIMIZADO: CALCULATE + predicado
London Sales CALC (BIEN) =
CALCULATE(
SUM(FactSales[Amount]),
DimRegion[RegionName] = «London»
  )

ALTO #3 FILTER sobre tabla de hechos

— ANTI-PATRÓN: FILTER(FactSales, …)
— Materializa toda la tabla de hechos
Filtered Sales FILTER (MAL) =
CALCULATE(
SUM(FactSales[Amount]),
FILTER(FactSales, FactSales[RegionID] = 3)
  )

— OPTIMIZADO: predicado directo en CALCULATE
Filtered Sales Pred (BIEN) =
CALCULATE(
SUM(FactSales[Amount]),
DimRegion[RegionName] = «East»
  )

ALTO #4 SUMMARIZECOLUMNS en medida

— ANTI-PATRÓN: SUMMARIZECOLUMNS dentro de medida
— Rompe fusión de consultas SQL
Top Products SUMMARIZE (MAL) =
SUMX(
SUMMARIZECOLUMNS(
FactSales[ProductID],
«Sales», SUM(FactSales[Amount])
    ),
    [Sales]
  )

— OPTIMIZADO: SUMX + VALUES sobre dimensión
Top Products VALUES (BIEN) =
SUMX(
VALUES(DimProduct[ProductName]),
CALCULATE(SUM(FactSales[Amount]))
  )

MEDIO #5 DISTINCTCOUNT alta cardinalidad

— ANTI-PATRÓN: DISTINCTCOUNT en columna de texto
— Genera COUNT DISTINCT sobre email
Unique Emails DC (MAL) =
DISTINCTCOUNT(DimCustomer[CustomerEmail])

— OPTIMIZADO: COUNTROWS sobre VALUES de ID
Unique Customers COUNT (BIEN) =
COUNTROWS(VALUES(FactSales[CustomerID]))

MEDIO #6 ALL indiscriminado

— ANTI-PATRÓN: ALL sobre tabla grande
— Fuerza scan completo sin WHERE
All Sales ALL (MAL) =
CALCULATE(
SUM(FactSales[Amount]),
ALL(FactSales)
  )

— OPTIMIZADO: ALLSELECTED respeta selección
All Sales Selected (BIEN) =
CALCULATE(
SUM(FactSales[Amount]),
ALLSELECTED(FactSales)
  )

ALTO #7 Inteligencia temporal nativa en DirectQuery

— ANTI-PATRÓN: SAMEPERIODLASTYEAR nativo
Sales LY Native (MAL) =
CALCULATE(
SUM(FactSales[Amount]),
SAMEPERIODLASTYEAR(DimDate[FullDate])
  )

— OPTIMIZADO: inteligencia temporal manual
Sales LY Manual (BIEN) =
VAR _Year = SELECTEDVALUE(DimDate[Year])
VAR _Month = SELECTEDVALUE(DimDate[Month])
RETURN
CALCULATE(
SUM(FactSales[Amount]),
ALL(DimDate),
DimDate[Year] = _Year – 1,
DimDate[Month] = _Month
    )

CRÍTICO #8 CallbackDataID por filtros dinámicos en iteradores

— ANTI-PATRÓN: CALCULATE anidado dentro de SUMX
— Genera 100+ CallbackDataID
Sales con Check (MAL) =
SUMX(FactSales,
VAR _Check =
CALCULATE(
COUNTROWS(FactSales),
FILTER(ALL(FactSales),
FactSales[RegionID] = 6
        )
      )
RETURN
IF(_Check > 0, FactSales[Amount], BLANK())
  )

— OPTIMIZADO: predicado simple
London Sales Simple (BIEN) =
CALCULATE(
SUM(FactSales[Amount]),
DimRegion[RegionName] = «London»
  )

Paso 4: Diseñar las páginas del informe

📄 Página 1: «Anti-Patrones vs. Optimizado»

Crea una tabla comparativa con dos columnas: medida MAL (izquierda) y medida BIEN (derecha). Añade un slicer de DimDate[Year] para forzar recálculos al cambiar el filtro.

Visuales recomendados:

  • Tabla con: Total Sales SUMX (MAL) | Total Sales SUM (BIEN)
  • Tabla con: London Sales SUMX (MAL) | London Sales CALC (BIEN)
  • Tabla con: Filtered Sales FILTER (MAL) | Filtered Sales Pred (BIEN)
  • Tarjeta con: Unique Emails DC (MAL) | Unique Customers COUNT (BIEN)

📄 Página 2: «Inteligencia Temporal»

Gráfico de líneas con meses en el eje X y dos valores:

  • Sales LY Native (MAL) — línea roja
  • Sales LY Manual (BIEN) — línea verde

Añade un slicer de categoría de producto para aumentar la complejidad de las consultas.

📄 Página 3: «Auditoría DAX»

Tabla con todas las medidas del modelo usando INFO.MEASURES(). Incluye:

  • Nombre de la medida
  • Expresión completa
  • Flags de detección (Tiene_SUMX, Tiene_FILTER, etc.)
  • Nivel de riesgo calculado

Usa el script M del cuadro de mando original para generar esta tabla.

Paso 5: Medir el rendimiento y detectar CallbackDataID

5.1 Performance Analyzer (Power BI Desktop)

Grabar interacciones

Vista → Performance Analyzer → Iniciar grabación. Cambia el slicer de año, haz clic en diferentes visuales, cambia de página. Detén la grabación.

Copiar consulta DAX

Haz clic en el icono de copiar junto a un visual lento. Pega la consulta en DAX Studio para analizar el Query Plan.

5.2 DAX Studio: detectar CallbackDataID

Conectar y configurar

Abre DAX Studio → conecta al modelo de Power BI Desktop → activa Server Timings y Query Plan.

Ejecutar y analizar

Pega la consulta DAX del Performance Analyzer y ejecuta. En la pestaña Server Timings, busca:

  • CallbackDataID — Cada instancia representa una llamada fila a fila del FE al SE. En DirectQuery, esto se traduce en subconsultas SQL.
  • FE Time — Si es alto (>50% del total), el FE está haciendo trabajo que debería estar en SQL.
  • SQL Queries — En DirectQuery, DAX Studio muestra las consultas SQL generadas. Si hay muchas para una sola medida, hay iteración.

💡 Cómo identificar CallbackDataID en el Query Plan: Busca nodos del plan con la etiqueta CallbackDataID o VertiPaq SE Query que aparecen repetidamente. En DirectQuery, estos se traducen en SQL Query con nombres como DirectQuery-1, DirectQuery-2, etc. Si ves más de 5 SQL Queries para una sola medida, tienes un problema de iteración.

5.3 SQL Server: contar consultas en tiempo real

Iniciar Extended Events

En SSMS, ejecuta:
ALTER EVENT SESSION PowerBI_DirectQuery_Monitor ON SERVER STATE = START;

Contar queries por interacción

Ejecuta antes de interactuar con Power BI:
EXEC sp_CountPowerBIQueries @SecondsWindow = 15;
Mientras espera, cambia un slicer o filtro en Power BI. Al finalizar, verás cuántas consultas SQL se lanzaron.

Detectar CallbackDataID desde SQL

Ejecuta:
EXEC sp_DetectCallbackDataID @MinQueriesPerSPID = 20, @MaxDurationMs = 50;
Si devuelve «POSIBLE CallbackDataID», significa que Power BI está lanzando muchas consultas cortas desde el mismo SPID — el patrón clásico de iteración fila a fila.

Paso 6: Tabla de auditoría automática (DAX)

Crea una tabla AuditoriaMedidas en Power BI con el siguiente DAX. Esto escanea automáticamente todas las medidas del modelo y clasifica su riesgo.

AuditoriaMedidas =
ADDCOLUMNS(
INFO.MEASURES(),
«Tiene_SUMX», IF(CONTAINSSTRING([Expression], «SUMX»), «SÍ», «NO»),
«Tiene_AVERAGEX», IF(CONTAINSSTRING([Expression], «AVERAGEX»), «SÍ», «NO»),
«Tiene_FILTER», IF(CONTAINSSTRING([Expression], «FILTER(«), «SÍ», «NO»),
«Tiene_ALL», IF(CONTAINSSTRING([Expression], «ALL(«) || CONTAINSSTRING([Expression], «REMOVEFILTERS»), «SÍ», «NO»),
«Tiene_SUMMARIZECOLUMNS», IF(CONTAINSSTRING([Expression], «SUMMARIZECOLUMNS»), «SÍ», «NO»),
«Tiene_DISTINCTCOUNT», IF(CONTAINSSTRING([Expression], «DISTINCTCOUNT»), «SÍ», «NO»),
«Tiene_SEARCH», IF(CONTAINSSTRING([Expression], «SEARCH») || CONTAINSSTRING([Expression], «CONTAINSSTRING»), «SÍ», «NO»),
«Nivel_Riesgo»,
SWITCH(TRUE(),
CONTAINSSTRING([Expression], «SUMX») || CONTAINSSTRING([Expression], «AVERAGEX») || CONTAINSSTRING([Expression], «SUMMARIZECOLUMNS»), «CRÍTICO»,
CONTAINSSTRING([Expression], «FILTER(«), «ALTO»,
CONTAINSSTRING([Expression], «ALL(«) || CONTAINSSTRING([Expression], «REMOVEFILTERS») || CONTAINSSTRING([Expression], «DISTINCTCOUNT»), «MEDIO»,
«OK»
      )
  )

Paso 7: Medidas de diagnóstico del dashboard

Crea estas medidas en la tabla Medidas para tener KPIs de rendimiento en el propio informe:

_Total Medidas = COUNTROWS(INFO.MEASURES())

_Medidas Críticas =
COUNTROWS(
FILTER(INFO.MEASURES(),
CONTAINSSTRING([Expression], «SUMX») ||
CONTAINSSTRING([Expression], «AVERAGEX») ||
CONTAINSSTRING([Expression], «SUMMARIZECOLUMNS»)
    )
  )

_Medidas de Alto Riesgo =
COUNTROWS(
FILTER(INFO.MEASURES(),
CONTAINSSTRING([Expression], «FILTER(«)
    )
  )

_Riesgo Total = [_Medidas Críticas] + [_Medidas de Alto Riesgo]

_Porcentaje Riesgo = DIVIDE([_Riesgo Total], [_Total Medidas], 0)

_Color Riesgo =
SWITCH(TRUE(),
    [_Porcentaje Riesgo] > 0.30, «🔴 CRÍTICO»,
    [_Porcentaje Riesgo] > 0.15, «🟡 ALTO»,
«🟢 OK»
  )

_Estado Dashboard =
IF([_Porcentaje Riesgo] > 0.30, «Requiere optimización urgente»,
IF([_Porcentaje Riesgo] > 0.15, «Revisar medidas lentas», «Rendimiento óptimo»)   )

Resumen del flujo de trabajo

FaseHerramientaQué buscar
ConstrucciónPower BI DesktopMedidas MAL y BIEN en paralelo
Medición inicialPerformance AnalyzerTiempo DAX > 3 segundos en visuales MAL
Diagnóstico DAXDAX StudioCallbackDataID, múltiples SQL Queries, FE Time alto
Diagnóstico SQLSSMS + Extended EventsRáfagas de consultas cortas del mismo SPID
CorrecciónPower BI DesktopReescribir medidas usando patrones BIEN
ValidaciónTodas las anterioresComparar tiempos antes/después

🚨 Nota sobre DirectQuery y 3M filas: Con 3 millones de filas, los anti-patrones MAL pueden tardar 10-60 segundos en responder. Los patrones BIEN deberían responder en menos de 2 segundos. Si una medida MAL tarda menos de 5 segundos, aumenta la carga interactuando con múltiples slicers simultáneamente o añadiendo más visuales en la misma página.

— Guía de construcción PBIX · Agosto 2026 · DAXPerformanceDemo —

Podéis descargar la prueba en el siguiente enlace:

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

¡Haz clic en una estrella para puntuarlo!

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

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 *