🗃️

Manipulación de Tablas

SUMMARIZECOLUMNS, ADDCOLUMNS, UNION, TOPN, GENERATE, TREATAS — construye y transforma tablas con todo el poder de DAX

Intermedio Avanzado
🔍

¿Cuándo necesitas tablas calculadas?

Las funciones de manipulación de tablas son el territorio de las DAX avanzadas. Con ellas puedes construir cualquier estructura de datos que necesites directamente en el modelo, sin tocar la fuente de datos ni Power Query. Son especialmente útiles para crear tablas auxiliares, parámetros dinámicos, rankings complejos y transformaciones que serían imposibles en el modelo de datos de Power BI.

¿Cuándo usar tablas calculadas?

  • Cuando necesitas una tabla que no existe en los datos pero puedes derivarla (ej: tabla de calendario)
  • Para crear parámetros What-If personalizados
  • Para pre-calcular rankings o clasificaciones complejas
  • Para generar tablas de unión o puente entre tablas sin relación
  • Para crear tablas de referencia estáticas sin necesidad de importarlas
⚠️ Performance de tablas calculadas
Las tablas calculadas se calculan en el momento del refresh del modelo y se almacenan en memoria. Para tablas muy grandes, considera si es mejor hacerlo en Power Query. Pero para tablas medianas (< 1M filas), las tablas calculadas son perfectas.
🏗️

Crear tablas desde cero

DATATABLE
Intermedio
Crea una tabla estática con datos definidos directamente en DAX. Perfecta para tablas de referencia pequeñas, listas de valores fijos, tablas de configuración y parámetros.

Tipos de datos válidos: STRING, INTEGER, DOUBLE, DATETIME, BOOLEAN, CURRENCY

Tabla de configuración de KPIs:

KPI Config =
DATATABLE(
    "KPI", STRING,
    "Umbral Verde", DOUBLE,
    "Umbral Naranja", DOUBLE,
    "Descripción", STRING,
    {
        {"Ticket Medio", 500, 300, "Importe medio por pedido"},
        {"% Conversión", 0.05, 0.03, "Ratio de conversión de leads"},
        {"NPS", 50, 30, "Net Promoter Score"},
        {"Margen %", 0.25, 0.15, "Margen bruto sobre ventas"}
    }
)

Tabla de selector de período:

Selector Período =
DATATABLE(
    "Período", STRING,
    "Días", INTEGER,
    {
        {"Últimos 7 días", 7},
        {"Últimos 30 días", 30},
        {"Últimos 90 días", 90},
        {"Último año", 365}
    }
)

Tabla de meses con nombre:

Meses =
DATATABLE(
    "NumMes", INTEGER,
    "NombreMes", STRING,
    "Abreviatura", STRING,
    {
        {1, "Enero", "Ene"},   {2, "Febrero", "Feb"},   {3, "Marzo", "Mar"},
        {4, "Abril", "Abr"},   {5, "Mayo", "May"},      {6, "Junio", "Jun"},
        {7, "Julio", "Jul"},   {8, "Agosto", "Ago"},    {9, "Septiembre", "Sep"},
        {10, "Octubre", "Oct"}, {11, "Noviembre", "Nov"}, {12, "Diciembre", "Dic"}
    }
)
ROW
Intermedio
Crea una tabla de UNA SOLA FILA con las columnas y valores especificados. Útil para devolver múltiples valores escalares como si fueran una fila de tabla.

Fila de resumen:

// Tabla con una fila de resumen (para usar en UNION o como referencia)
Resumen Actual =
ROW(
    "Total Ventas", [Total Ventas],
    "Nº Pedidos", [Nº Pedidos],
    "Ticket Medio", [Ticket Medio],
    "Fecha Calculo", NOW()
)

Uso en medidas para devolver múltiples valores:

// ROW es útil cuando necesitas devolver una estructura temporal
Metricas Principales =
VAR TablaMetricas = ROW(
    "Ventas", [Total Ventas],
    "Coste", [Total Coste],
    "Margen", [Margen Bruto]
)
RETURN
    TablaMetricas
GENERATESERIES
Intermedio
Genera una tabla de una columna ([Value]) con una serie numérica o de fechas. Perfecta para crear ejes personalizados, rangos de valores y tablas de parámetros.

Series numéricas y de fechas:

// Serie numérica de 1 a 100
Numeros = GENERATESERIES(1, 100, 1)

// Percentiles para análisis (0%, 5%, 10%...100%)
Percentiles = GENERATESERIES(0, 1, 0.05)

// Serie de fechas (una fecha por fila)
Fechas 2024 = GENERATESERIES(DATE(2024,1,1), DATE(2024,12,31), 1)
// Devuelve 366 filas, una por cada día de 2024

// Parámetro What-If personalizado (tasa de descuento 0%-30%)
Descuento Param =
ADDCOLUMNS(
    GENERATESERIES(0, 0.30, 0.01),
    "Descuento Label", FORMAT([Value], "0%")
)

Tabla de calendario simple:

// Generar calendario desde primera a última fecha de ventas
Calendario =
VAR MinFecha = MIN('Ventas'[Fecha])
VAR MaxFecha = MAX('Ventas'[Fecha])
RETURN
ADDCOLUMNS(
    GENERATESERIES(MinFecha, MaxFecha, 1),
    "Fecha", [Value],
    "Año", YEAR([Value]),
    "Mes", MONTH([Value]),
    "Día", DAY([Value]),
    "NombreMes", FORMAT([Value], "MMMM"),
    "Día Semana", FORMAT([Value], "DDDD")
)
🔧

Modificar tablas: agregar y seleccionar columnas

ADDCOLUMNS
Intermedio
Toma una tabla base y agrega nuevas columnas calculadas. La tabla resultante tiene todas las columnas originales MÁS las nuevas. Una de las funciones más usadas en DAX avanzado para construir estructuras complejas.

Sintaxis:

ADDCOLUMNS(
    ,
    "NombreColumna1", ,
    "NombreColumna2", ,
    ...
)

Ejemplo básico - Agregar columnas calculadas a una tabla:

// Tabla de productos con métricas adicionales
Productos Enriquecidos =
ADDCOLUMNS(
    'Productos',
    "Total Vendido", [Total Ventas],
    "Unidades Vendidas", [Unidades],
    "Precio Medio", DIVIDE([Total Ventas], [Unidades]),
    "Ranking", RANKX(ALL('Productos'), [Total Ventas], , DESC)
)

Ejemplo avanzado - Dashboard de KPIs:

// Tabla resumen por categoría con múltiples métricas
Resumen Categorias =
ADDCOLUMNS(
    VALUES('Productos'[Categoria]),
    "Ventas", [Total Ventas],
    "Ventas AA", [Total Ventas AA],
    "Variacion", [Variacion YoY],
    "% Variacion", DIVIDE([Variacion YoY], [Total Ventas AA]),
    "Nº Productos", DISTINCTCOUNT('Productos'[ID_Producto]),
    "Margen %", DIVIDE([Margen Bruto], [Total Ventas])
)

Pattern: ADDCOLUMNS con SUMMARIZE para resúmenes complejos:

// Top productos por región con métricas
Top Productos Region =
ADDCOLUMNS(
    SUMMARIZE('Ventas', 'Productos'[Nombre], 'Clientes'[Region]),
    "Ventas", [Total Ventas],
    "Margen", [Margen Bruto],
    "Clientes", DISTINCTCOUNT('Ventas'[ID_Cliente])
)
💡 Tip profesional
ADDCOLUMNS respeta el contexto de fila automáticamente — puedes usar columnas de la tabla base directamente en las expresiones. Cada expresión se evalúa fila por fila con el contexto de esa fila.
SELECTCOLUMNS
Intermedio
Crea una nueva tabla seleccionando SOLO las columnas especificadas (y opcionalmente renombrándolas). A diferencia de ADDCOLUMNS, SELECTCOLUMNS NO preserva las columnas originales — solo devuelve las que especifiques.

Sintaxis:

SELECTCOLUMNS(
    ,
    "NuevoNombre1", ,
    "NuevoNombre2", ,
    ...
)

Ejemplo básico - Seleccionar y renombrar columnas:

// Tabla simplificada de productos
Productos Simple =
SELECTCOLUMNS(
    'Productos',
    "ID", 'Productos'[ID_Producto],
    "Nombre", 'Productos'[NombreProducto],
    "Categoría", 'Productos'[Categoria],
    "Precio", 'Productos'[PrecioUnitario]
)

Ejemplo avanzado - Normalizar formato de fechas:

// Extraer solo fecha y cliente de ventas
Ventas Normalizado =
SELECTCOLUMNS(
    'Ventas',
    "Fecha", DATE(YEAR('Ventas'[Fecha]), MONTH('Ventas'[Fecha]), DAY('Ventas'[Fecha])),
    "Cliente ID", 'Ventas'[ID_Cliente],
    "Importe Neto", 'Ventas'[Precio] * 'Ventas'[Cantidad] * (1 - 'Ventas'[Descuento])
)

Pattern: SELECTCOLUMNS para listas únicas con formato:

// Lista de clientes con formato para dropdown
Lista Clientes =
SELECTCOLUMNS(
    DISTINCT('Clientes'[ID_Cliente]),
    "ID", 'Clientes'[ID_Cliente],
    "Display", 'Clientes'[Nombre] & " (" & 'Clientes'[Ciudad] & ")"
)
🏆 ¿Cuándo usar ADDCOLUMNS vs SELECTCOLUMNS?
- ADDCOLUMNS: cuando quieres AGREGAR columnas sin eliminar las originales
- SELECTCOLUMNS: cuando quieres PROYECTAR solo ciertas columnas (como SELECT en SQL)
- Puedes combinarlos: SELECTCOLUMNS(ADDCOLUMNS(...)) para agregar primero y luego filtrar
REMOVECOLUMNS
Intermedio
Elimina columnas específicas de una tabla. Lo opuesto a SELECTCOLUMNS: en lugar de especificar qué mantener, especificas qué eliminar.

Ejemplo:

// Tabla sin columnas sensibles
Productos Sin Coste =
REMOVECOLUMNS(
    'Productos',
    'Productos'[CosteUnitario],
    'Productos'[Proveedor]
)
RENAMECOLUMNS
Intermedio
Renombra columnas de una tabla sin modificar el contenido. Útil para normalizar nombres antes de hacer UNION o joins.

Ejemplo:

// Normalizar nombres de columnas
Ventas Normalizado =
RENAMECOLUMNS(
    'Ventas',
    "ID_Pedido", "PedidoID",
    "Fecha_Venta", "Fecha"
)
📊

Resumir datos: SUMMARIZE y SUMMARIZECOLUMNS

SUMMARIZE
Avanzado
Agrupa filas por las columnas especificadas (como GROUP BY en SQL) y opcionalmente agrega columnas calculadas. Es la función clásica para crear tablas de resumen, aunque SUMMARIZECOLUMNS suele ser mejor opción en medidas.

Sintaxis:

SUMMARIZE(
    ,
    ,
    ,
    ...
    ["NombreColumnaCalc", ],
    ...
)

Ejemplo básico - Ventas por categoría:

// Tabla resumen por categoría
Ventas por Categoria =
SUMMARIZE(
    'Ventas',
    'Productos'[Categoria]
)

Ejemplo con múltiples niveles:

// Resumen por categoría y subcategoría
Jerarquia Productos =
SUMMARIZE(
    'Ventas',
    'Productos'[Categoria],
    'Productos'[Subcategoria]
)

Ejemplo con columnas calculadas (⚠️ limitaciones):

// SUMMARIZE con columnas agregadas (NO recomendado)
Ventas por Mes =
SUMMARIZE(
    'Ventas',
    'Calendario'[Año],
    'Calendario'[Mes],
    "Total", SUM('Ventas'[Importe])  // ⚠️ Puede dar resultados incorrectos
)

// MEJOR: usar ADDCOLUMNS + SUMMARIZE
Ventas por Mes CORRECTO =
ADDCOLUMNS(
    SUMMARIZE('Ventas', 'Calendario'[Año], 'Calendario'[Mes]),
    "Total", [Total Ventas]
)
⚠️ Advertencia importante
Microsoft recomienda NO usar agregaciones directas dentro de SUMMARIZE (como SUM, COUNT, etc.). En su lugar, usa ADDCOLUMNS(SUMMARIZE(...), "Columna", [Medida]). Esto evita comportamientos inesperados con el contexto de filtro.
SUMMARIZECOLUMNS
Avanzado
La evolución moderna de SUMMARIZE. Más potente, más rápida y con mejor manejo del contexto de filtro. Es la función recomendada para crear tablas de resumen en medidas y para reemplazar consultas complejas. Es la base del motor de consultas de Power BI.

Sintaxis:

SUMMARIZECOLUMNS(
    ,
    ,
    ...
    [],
    ["NombreMedida1", ],
    ["NombreMedida2", ],
    ...
)

Ejemplo básico - Resumen con medidas:

// Ventas y margen por categoría
Resumen Categorias =
SUMMARIZECOLUMNS(
    'Productos'[Categoria],
    "Ventas", [Total Ventas],
    "Margen", [Margen Bruto],
    "Margen %", [Margen Porcentaje]
)

Ejemplo con múltiples dimensiones:

// Análisis multidimensional: Año x Región x Categoría
Cubo Ventas =
SUMMARIZECOLUMNS(
    'Calendario'[Año],
    'Clientes'[Region],
    'Productos'[Categoria],
    "Ventas", [Total Ventas],
    "Ventas AA", [Total Ventas AA],
    "Variacion", [Variacion YoY],
    "Clientes", [Clientes Unicos]
)

Ejemplo con filtros explícitos:

// Resumen solo para productos activos en 2024
Ventas Activos 2024 =
SUMMARIZECOLUMNS(
    'Productos'[Categoria],
    FILTER('Productos', 'Productos'[Estado] = "Activo"),
    FILTER('Calendario', 'Calendario'[Año] = 2024),
    "Ventas", [Total Ventas],
    "Unidades", [Unidades Vendidas]
)

Pattern avanzado - Top N por categoría:

// Top 3 productos por categoría
Top 3 por Categoria =
GENERATE(
    VALUES('Productos'[Categoria]),
    VAR CategoriaActual = 'Productos'[Categoria]
    RETURN
        TOPN(
            3,
            SUMMARIZECOLUMNS(
                'Productos'[NombreProducto],
                FILTER('Productos', 'Productos'[Categoria] = CategoriaActual),
                "Ventas", [Total Ventas]
            ),
            [Ventas], DESC
        )
)
🏆 SUMMARIZECOLUMNS vs SUMMARIZE
- SUMMARIZECOLUMNS: mejor rendimiento, manejo correcto de contexto, sintaxis más clara
- SUMMARIZE: útil en tablas calculadas, más simple para agrupaciones básicas sin medidas
- Regla general: usa SUMMARIZECOLUMNS en medidas, SUMMARIZE en tablas calculadas simples
GROUPBY
Avanzado
Agrupa filas y aplica agregaciones usando iteradores X. Similar a SUMMARIZE pero con agregaciones explícitas usando CURRENTGROUP(). Útil cuando necesitas control total sobre la agregación.

Sintaxis:

GROUPBY(
    ,
    ,
    ,
    ...
    "NombreAgregacion", (CURRENTGROUP(), )
)

Ejemplo - Agregaciones personalizadas:

// Ventas por categoría con agregaciones custom
Ventas Agregadas =
GROUPBY(
    'Ventas',
    'Productos'[Categoria],
    "Total Ventas", SUMX(CURRENTGROUP(), 'Ventas'[Importe]),
    "Nº Pedidos", COUNTROWS(CURRENTGROUP()),
    "Ticket Medio", AVERAGEX(CURRENTGROUP(), 'Ventas'[Importe])
)

Ejemplo con agregaciones complejas:

// Métricas avanzadas por región
Analisis Regiones =
GROUPBY(
    'Ventas',
    'Clientes'[Region],
    "Total Ventas", SUMX(CURRENTGROUP(), 'Ventas'[Precio] * 'Ventas'[Cantidad]),
    "Pedidos >1000", COUNTX(
        FILTER(CURRENTGROUP(), 'Ventas'[Precio] * 'Ventas'[Cantidad] > 1000),
        'Ventas'[ID_Pedido]
    ),
    "Valor Max Pedido", MAXX(CURRENTGROUP(), 'Ventas'[Precio] * 'Ventas'[Cantidad])
)
🔗

Operaciones de conjuntos: UNION, INTERSECT, EXCEPT

UNION
Intermedio
Combina múltiples tablas verticalmente (una debajo de otra). Las tablas deben tener el mismo número de columnas y tipos compatibles. No elimina duplicados automáticamente — conserva todas las filas.

Sintaxis:

UNION(
    ,
    ,
    ,
    ...
)

Ejemplo básico - Combinar ventas de dos años:

// Unir ventas 2023 y 2024
Ventas Combinadas =
UNION(
    SELECTCOLUMNS(
        FILTER('Ventas', YEAR('Ventas'[Fecha]) = 2023),
        "Año", 2023,
        "Mes", MONTH('Ventas'[Fecha]),
        "Importe", 'Ventas'[Importe]
    ),
    SELECTCOLUMNS(
        FILTER('Ventas', YEAR('Ventas'[Fecha]) = 2024),
        "Año", 2024,
        "Mes", MONTH('Ventas'[Fecha]),
        "Importe", 'Ventas'[Importe]
    )
)

Ejemplo - Tabla de escenarios (Real vs Presupuesto):

// Combinar datos reales con presupuesto
Real vs Presupuesto =
UNION(
    ADDCOLUMNS(
        'Ventas',
        "Tipo", "Real",
        "Importe", 'Ventas'[ImporteVenta]
    ),
    ADDCOLUMNS(
        'Presupuesto',
        "Tipo", "Presupuesto",
        "Importe", 'Presupuesto'[ImportePresupuesto]
    )
)

Pattern - Tabla de métricas verticales:

// Crear tabla de KPIs para visual vertical
Tabla KPIs =
UNION(
    ROW("Métrica", "Ventas", "Valor", [Total Ventas]),
    ROW("Métrica", "Coste", "Valor", [Total Coste]),
    ROW("Métrica", "Margen", "Valor", [Margen Bruto]),
    ROW("Métrica", "Margen %", "Valor", [Margen Porcentaje]),
    ROW("Métrica", "Nº Pedidos", "Valor", [Numero Pedidos]),
    ROW("Métrica", "Ticket Medio", "Valor", [Ticket Medio])
)
💡 UNION vs APPEND en Power Query
UNION es para operaciones dinámicas en DAX (se recalcula en cada consulta). Si necesitas combinar tablas estáticas de forma permanente, es más eficiente hacerlo en Power Query con Append.
INTERSECT
Intermedio
Devuelve solo las filas que existen en AMBAS tablas (intersección). Las tablas deben tener el mismo número de columnas y tipos compatibles. Muy útil para encontrar elementos comunes.

Ejemplo - Clientes que compraron en ambos períodos:

// Clientes que compraron en 2023 Y en 2024
Clientes Recurrentes =
INTERSECT(
    DISTINCT(
        SELECTCOLUMNS(
            FILTER('Ventas', YEAR('Ventas'[Fecha]) = 2023),
            "ClienteID", 'Ventas'[ID_Cliente]
        )
    ),
    DISTINCT(
        SELECTCOLUMNS(
            FILTER('Ventas', YEAR('Ventas'[Fecha]) = 2024),
            "ClienteID", 'Ventas'[ID_Cliente]
        )
    )
)

Medida - Tasa de retención:

// % de clientes del año anterior que volvieron este año
Tasa Retencion =
VAR ClientesAA = DISTINCT(
    SELECTCOLUMNS(
        CALCULATETABLE('Ventas', YEAR('Ventas'[Fecha]) = YEAR(TODAY()) - 1),
        "ID", 'Ventas'[ID_Cliente]
    )
)
VAR ClientesActual = DISTINCT(
    SELECTCOLUMNS(
        CALCULATETABLE('Ventas', YEAR('Ventas'[Fecha]) = YEAR(TODAY())),
        "ID", 'Ventas'[ID_Cliente]
    )
)
VAR ClientesRetenidos = INTERSECT(ClientesAA, ClientesActual)
RETURN
    DIVIDE(COUNTROWS(ClientesRetenidos), COUNTROWS(ClientesAA))
EXCEPT
Intermedio
Devuelve las filas de la primera tabla que NO están en la segunda tabla (diferencia de conjuntos). Ideal para encontrar elementos faltantes o exclusivos.

Ejemplo - Productos sin ventas:

// Productos que no se han vendido en el período actual
Productos Sin Ventas =
EXCEPT(
    DISTINCT(SELECTCOLUMNS('Productos', "ID", 'Productos'[ID_Producto])),
    DISTINCT(SELECTCOLUMNS('Ventas', "ID", 'Ventas'[ID_Producto]))
)

Ejemplo - Clientes perdidos (churn):

// Clientes que compraron el año pasado pero NO este año
Clientes Perdidos =
EXCEPT(
    DISTINCT(
        SELECTCOLUMNS(
            FILTER('Ventas', YEAR('Ventas'[Fecha]) = YEAR(TODAY()) - 1),
            "ID", 'Ventas'[ID_Cliente]
        )
    ),
    DISTINCT(
        SELECTCOLUMNS(
            FILTER('Ventas', YEAR('Ventas'[Fecha]) = YEAR(TODAY())),
            "ID", 'Ventas'[ID_Cliente]
        )
    )
)

// Medida de tasa de churn
Tasa Churn =
VAR ClientesAA = COUNTROWS(
    DISTINCT(
        SELECTCOLUMNS(
            CALCULATETABLE('Ventas', YEAR('Ventas'[Fecha]) = YEAR(TODAY()) - 1),
            "ID", 'Ventas'[ID_Cliente]
        )
    )
)
VAR ClientesPerdidos = COUNTROWS([Clientes Perdidos])
RETURN
    DIVIDE(ClientesPerdidos, ClientesAA)
🏆 Memoria de conjuntos
UNION, INTERSECT y EXCEPT: las tres operaciones clásicas de teoría de conjuntos, ahora en DAX.
- UNION: todo de A + todo de B (A ∪ B)
- INTERSECT: solo lo común de A y B (A ∩ B)
- EXCEPT: lo que está en A pero no en B (A - B)
🔀

Joins: combinar tablas horizontalmente

NATURALINNERJOIN
Avanzado
Realiza un INNER JOIN entre dos tablas usando las columnas con el mismo nombre como claves. Combina horizontalmente solo las filas donde hay coincidencia en ambas tablas.

Ejemplo - Combinar productos con categorías:

// Unir productos con información de categorías
Productos con Categoria =
NATURALINNERJOIN(
    SELECTCOLUMNS('Productos', "CategoriaID", 'Productos'[ID_Categoria], "Producto", 'Productos'[Nombre]),
    SELECTCOLUMNS('Categorias', "CategoriaID", 'Categorias'[ID], "Categoria", 'Categorias'[Nombre])
)
// Resultado: solo productos que tienen categoría asignada
⚠️ Limitación importante
NATURALINNERJOIN hace join automáticamente por columnas con el MISMO NOMBRE. Si tus columnas se llaman diferente, necesitas usar SELECTCOLUMNS primero para renombrarlas, o usar NATURALLEFTOUTERJOIN.
NATURALLEFTOUTERJOIN
Avanzado
Como NATURALINNERJOIN pero tipo LEFT JOIN — conserva TODAS las filas de la primera tabla, aunque no tengan coincidencia en la segunda.

Ejemplo - Productos con categorías (incluyendo sin categoría):

// LEFT JOIN: todos los productos, con o sin categoría
Productos con Categoria Completo =
NATURALLEFTOUTERJOIN(
    SELECTCOLUMNS('Productos', "ID", 'Productos'[ID_Categoria], "Producto", 'Productos'[Nombre]),
    SELECTCOLUMNS('Categorias', "ID", 'Categorias'[ID], "Categoria", 'Categorias'[Nombre])
)
// Las columnas de Categorias serán BLANK() para productos sin categoría
CROSSJOIN
Avanzado
Crea el producto cartesiano de dos o más tablas — cada fila de la primera con cada fila de la segunda. ⚠️ Genera una tabla enorme (filas1 × filas2). Úsalo con cuidado y solo con tablas pequeñas.

Ejemplo - Matriz de todos los productos × regiones:

// Todas las combinaciones posibles de producto y región
Matriz Producto Region =
CROSSJOIN(
    VALUES('Productos'[Nombre]),
    VALUES('Clientes'[Region])
)

Uso típico - Tabla de parámetros multidimensional:

// Todas las combinaciones de categoría y período para análisis what-if
Escenarios =
CROSSJOIN(
    VALUES('Productos'[Categoria]),
    DATATABLE(
        "Periodo", STRING,
        {{"Actual"}, {"Presupuesto"}, {"Forecast"}}
    )
)
⚠️ Cuidado con CROSSJOIN
Si tienes 1000 productos y 50 regiones, CROSSJOIN genera 50,000 filas. Para tablas grandes, esto puede explotar en memoria. Usa filtros después del CROSSJOIN para reducir el resultado.
🏆

Filtrado avanzado: TOPN y TOPNPERLEVEL

TOPN
Intermedio
Devuelve las N primeras filas de una tabla ordenadas por una expresión. Imprescindible para rankings, top clientes, productos más vendidos, etc. Mucho más eficiente que usar RANKX con filtro.

Sintaxis:

TOPN(
    ,
    ,
    ,
    [DESC o ASC]
)

Ejemplo básico - Top 10 productos:

// Los 10 productos más vendidos
Top 10 Productos =
TOPN(
    10,
    ADDCOLUMNS(
        VALUES('Productos'[Nombre]),
        "Ventas", [Total Ventas]
    ),
    [Ventas], DESC
)

Ejemplo dinámico - Top N variable:

// Top N basado en parámetro de usuario
Top N Dinamico =
VAR TopN_Param = SELECTEDVALUE('Parametros'[Top N], 10)
RETURN
TOPN(
    TopN_Param,
    ADDCOLUMNS(
        VALUES('Productos'[Nombre]),
        "Ventas", [Total Ventas],
        "Margen", [Margen Bruto]
    ),
    [Ventas], DESC
)

Pattern - Top N con "Otros":

// Top 5 + categoría "Otros" con el resto
Top 5 Mas Otros =
VAR Top5 = TOPN(
    5,
    ADDCOLUMNS(VALUES('Productos'[Categoria]), "Ventas", [Total Ventas]),
    [Ventas], DESC
)
VAR VentasTop5 = SUMX(Top5, [Ventas])
VAR VentasOtros = [Total Ventas] - VentasTop5
RETURN
UNION(
    Top5,
    ROW("Categoria", "Otros", "Ventas", VentasOtros)
)

Medida - Suma del Top 10:

// Ventas de los 10 mejores clientes
Ventas Top 10 Clientes =
SUMX(
    TOPN(
        10,
        ADDCOLUMNS(VALUES('Clientes'[Nombre]), "Ventas", [Total Ventas]),
        [Ventas], DESC
    ),
    [Ventas]
)
💡 TOPN vs FILTER con RANKX
TOPN es mucho más eficiente que calcular RANKX para toda la tabla y luego filtrar. Si solo necesitas los N primeros, siempre usa TOPN — es más rápido y usa menos memoria.
TOPNPERLEVEL
Avanzado
Como TOPN pero aplicado por cada nivel de una jerarquía o agrupación. Devuelve los N mejores elementos POR CADA grupo. Perfecta para "top 3 productos por categoría", "mejores vendedores por región", etc.

Sintaxis:

TOPNPERLEVEL(
    ,
    ,
    [, ...],
    ,
    [DESC o ASC]
)

Ejemplo - Top 3 productos por categoría:

// Los 3 mejores productos de cada categoría
Top 3 por Categoria =
ADDCOLUMNS(
    TOPNPERLEVEL(
        3,
        'Productos'[Categoria],
        'Productos'[Nombre],
        [Total Ventas], DESC
    ),
    "Ventas", [Total Ventas]
)

Ejemplo - Top vendedores por región y año:

// Top 5 vendedores por región y año
Top Vendedores Region Año =
ADDCOLUMNS(
    TOPNPERLEVEL(
        5,
        'Calendario'[Año],
        'Vendedores'[Region],
        'Vendedores'[Nombre],
        [Total Ventas], DESC
    ),
    "Ventas", [Total Ventas],
    "Ranking", RANKX(
        FILTER(
            ALL('Vendedores'[Nombre]),
            'Vendedores'[Region] = EARLIER('Vendedores'[Region])
        ),
        [Total Ventas], , DESC
    )
)

GENERATE y GENERATEALL — iteración avanzada

GENERATE
Avanzado
Itera cada fila de la primera tabla y para cada una evalúa la segunda expresión (que devuelve una tabla). El resultado es el CROSS JOIN de ambas con contexto de fila. Es como un bucle FOR en programación. Solo devuelve filas donde la segunda tabla NO está vacía (comportamiento INNER JOIN).

Sintaxis:

GENERATE(
    ,
      // evaluada por cada fila de tabla1
)

Ejemplo básico - Expandir jerarquía:

// Para cada categoría, listar sus productos
Categorias con Productos =
GENERATE(
    VALUES('Productos'[Categoria]),
    CALCULATETABLE(
        VALUES('Productos'[Nombre]),
        'Productos'[Categoria] = EARLIER('Productos'[Categoria])
    )
)

Pattern avanzado - Top N por grupo:

// Top 3 productos por cada categoría (alternativa a TOPNPERLEVEL)
Top 3 por Categoria v2 =
GENERATE(
    VALUES('Productos'[Categoria]),
    VAR CategoriaActual = 'Productos'[Categoria]
    RETURN
        TOPN(
            3,
            CALCULATETABLE(
                ADDCOLUMNS(
                    VALUES('Productos'[Nombre]),
                    "Ventas", [Total Ventas]
                ),
                'Productos'[Categoria] = CategoriaActual
            ),
            [Ventas], DESC
        )
)

Ejemplo - Calendario de ventas real:

// Para cada cliente, generar las fechas en las que compró
Compras por Cliente =
GENERATE(
    VALUES('Clientes'[Nombre]),
    VAR ClienteActual = 'Clientes'[Nombre]
    RETURN
        CALCULATETABLE(
            ADDCOLUMNS(
                VALUES('Ventas'[Fecha]),
                "Importe", [Total Ventas]
            ),
            'Clientes'[Nombre] = ClienteActual
        )
)
💡 GENERATE como bucle FOR
Piensa en GENERATE como:
FOR cada fila en tabla1:
  calcula tabla2 con el contexto de esa fila
  agrega las filas de tabla2 al resultado
Es la forma de hacer iteraciones anidadas en DAX.
GENERATEALL
Avanzado
Como GENERATE pero con comportamiento LEFT OUTER JOIN — conserva TODAS las filas de la primera tabla aunque la segunda expresión devuelva una tabla vacía. Las columnas de la segunda tabla serán BLANK().

Ejemplo - Todas las categorías con sus productos (incluso sin ventas):

// Incluir categorías sin productos vendidos
Todas Categorias con Productos =
GENERATEALL(
    VALUES('Productos'[Categoria]),
    VAR CategoriaActual = 'Productos'[Categoria]
    RETURN
        CALCULATETABLE(
            ADDCOLUMNS(
                VALUES('Productos'[Nombre]),
                "Ventas", [Total Ventas]
            ),
            'Productos'[Categoria] = CategoriaActual,
            [Total Ventas] > 0  // solo productos con ventas
        )
)
// Si una categoría no tiene productos con ventas, aparecerá con columnas vacías
🏆 GENERATE vs GENERATEALL
- GENERATE: solo filas donde ambas tablas tienen datos (INNER JOIN)
- GENERATEALL: todas las filas de la primera tabla, rellena con BLANK si la segunda está vacía (LEFT JOIN)
Elige según si quieres excluir o conservar grupos sin datos.
📈

ROLLUP y ROLLUPGROUP — subtotales automáticos

ROLLUP / ROLLUPGROUP
Avanzado
Se usan dentro de SUMMARIZECOLUMNS para generar automáticamente filas de subtotales y totales. Equivalente a GROUP BY ... WITH ROLLUP en SQL. Muy útil para crear tablas de resumen con niveles agregados.

Ejemplo básico - Ventas por categoría con subtotal:

// Ventas por categoría + fila de total general
Ventas con Total =
SUMMARIZECOLUMNS(
    ROLLUPGROUP('Productos'[Categoria]),
    "Ventas", [Total Ventas],
    "Margen", [Margen Bruto]
)
// Genera filas por cada categoría + una fila final con Categoria = BLANK (total)

Ejemplo con múltiples niveles:

// Ventas con subtotales por año y región
Ventas con Subtotales =
SUMMARIZECOLUMNS(
    ROLLUPGROUP('Calendario'[Año], 'Clientes'[Region]),
    "Ventas", [Total Ventas],
    "Clientes", [Clientes Unicos]
)
// Genera:
// - Filas por año + región
// - Filas subtotal por año (región = BLANK)
// - Fila de total general (año = BLANK, región = BLANK)

Pattern - Detectar niveles de subtotal:

// Tabla con indicador del nivel de agregación
Ventas Jerarquia =
ADDCOLUMNS(
    SUMMARIZECOLUMNS(
        ROLLUPGROUP('Productos'[Categoria], 'Productos'[Subcategoria]),
        "Ventas", [Total Ventas]
    ),
    "Nivel",
        IF(
            ISBLANK('Productos'[Categoria]),
            "Total General",
            IF(
                ISBLANK('Productos'[Subcategoria]),
                "Subtotal Categoría",
                "Detalle"
            )
        )
)
⚠️ ROLLUP solo en SUMMARIZECOLUMNS
ROLLUP y ROLLUPGROUP solo funcionan dentro de SUMMARIZECOLUMNS. No se pueden usar con SUMMARIZE. Si necesitas subtotales en otras funciones, usa UNION para agregar filas de total manualmente.
🔗

TREATAS — relaciones virtuales

TREATAS
Avanzado
Aplica las columnas de una tabla como si fueran columnas de otra tabla, creando una "relación virtual" temporal. Permite propagar filtros entre tablas sin relación directa. Es como decirle a DAX "trata esta columna como si fuera esta otra". Fundamental para patrones de modelo complejos y relaciones many-to-many custom.

Sintaxis:

TREATAS(
    ,
    ,
    [, ...]
)

Caso de uso típico - Filtrar por tabla intermedia:

// Problema: Ventas no tiene relación directa con Campañas
// Solución: usar TREATAS para propagar filtro a través de tabla puente

Ventas de Campaña =
CALCULATE(
    [Total Ventas],
    TREATAS(
        VALUES('Campañas'[ID_Producto]),  // IDs de productos de la campaña seleccionada
        'Productos'[ID_Producto]          // aplicar como filtro en Productos
    )
)
// Ahora el filtro de Campañas se propaga a Ventas a través de Productos

Pattern - Relación many-to-many virtual:

// Escenario: tabla TablaPuente conecta Clientes con Segmentos (many-to-many)
// Calcular ventas por segmento sin relación física

Ventas por Segmento =
CALCULATE(
    [Total Ventas],
    TREATAS(
        SUMMARIZE('TablaPuente', 'TablaPuente'[ID_Cliente]),
        'Ventas'[ID_Cliente]
    )
)

Ejemplo - Aplicar filtro de año a tabla sin fechas:

// Presupuesto tiene [Año] y [Mes] como números, no fechas
// Aplicar filtro de Calendario (que sí tiene relación con Ventas)

Presupuesto Filtrado =
CALCULATE(
    SUM('Presupuesto'[Importe]),
    TREATAS(
        VALUES('Calendario'[Año]),
        'Presupuesto'[Año]
    ),
    TREATAS(
        VALUES('Calendario'[NumMes]),
        'Presupuesto'[Mes]
    )
)
// Ahora el slicer de Calendario también filtra Presupuesto

Pattern avanzado - TREATAS con múltiples columnas:

// Aplicar filtro compuesto (año + región)
Ventas Filtro Compuesto =
CALCULATE(
    [Total Ventas],
    TREATAS(
        SUMMARIZE('OtraTabla', 'OtraTabla'[Año], 'OtraTabla'[Region]),
        'Calendario'[Año],
        'Clientes'[Region]
    )
)
💡 ¿Cuándo usar TREATAS?
- Cuando tienes tablas sin relación directa pero con columnas comunes
- Para implementar relaciones many-to-many personalizadas sin tabla puente física
- Para aplicar filtros de una dimensión temporal a tablas con años/meses como números
- Para modelos con múltiples tablas de hechos sin tabla de calendario común
⚠️ Limitaciones de TREATAS
- Las columnas deben tener el mismo tipo de datos
- Es una relación "virtual" — no aprovecha las optimizaciones del motor de relaciones físicas
- Puede ser más lenta que una relación real — úsala solo cuando sea necesario
- No funciona con bidireccionalidad como las relaciones físicas
🎯

Patrones avanzados de tablas

🏆 Patrón 1: Tabla de resumen multinivel con ADDCOLUMNS + SUMMARIZE
// Dashboard completo en una tabla
Dashboard Ventas =
ADDCOLUMNS(
    SUMMARIZE(
        'Ventas',
        'Calendario'[Año],
        'Clientes'[Region],
        'Productos'[Categoria]
    ),
    "Ventas", [Total Ventas],
    "Ventas AA", [Total Ventas AA],
    "Variacion", [Variacion YoY],
    "% Variacion", [% Variacion YoY],
    "Margen", [Margen Bruto],
    "% Margen", [Margen Porcentaje],
    "Clientes", [Clientes Unicos],
    "Pedidos", [Numero Pedidos],
    "Ticket Medio", [Ticket Medio]
)
🏆 Patrón 2: Top N con categoría "Otros"
// Top 10 productos + fila "Otros" con el resto
Top 10 Productos Completo =
VAR TopN_Value = 10
VAR TablaCompleta = ADDCOLUMNS(
    VALUES('Productos'[Nombre]),
    "Ventas", [Total Ventas]
)
VAR Top10 = TOPN(TopN_Value, TablaCompleta, [Ventas], DESC)
VAR VentasTop10 = SUMX(Top10, [Ventas])
VAR VentasTotal = SUMX(TablaCompleta, [Ventas])
VAR VentasOtros = VentasTotal - VentasTop10
RETURN
UNION(
    Top10,
    ROW("Nombre", "📦 Otros productos", "Ventas", VentasOtros)
)
🏆 Patrón 3: Tabla de escenarios (Real vs Budget vs Forecast)
// Combinar tres fuentes en una tabla unificada
Escenarios Unificados =
VAR Real = ADDCOLUMNS(
    SUMMARIZE('Ventas', 'Calendario'[Fecha], 'Productos'[Categoria]),
    "Tipo", "Real",
    "Importe", [Total Ventas]
)
VAR Budget = ADDCOLUMNS(
    SUMMARIZE('Presupuesto', 'Presupuesto'[Fecha], 'Presupuesto'[Categoria]),
    "Tipo", "Presupuesto",
    "Importe", SUM('Presupuesto'[Importe])
)
VAR Forecast = ADDCOLUMNS(
    SUMMARIZE('Forecast', 'Forecast'[Fecha], 'Forecast'[Categoria]),
    "Tipo", "Forecast",
    "Importe", SUM('Forecast'[Importe])
)
RETURN
UNION(Real, Budget, Forecast)
🏆 Patrón 4: Análisis de cohortes con GENERATE
// Cohorte de clientes por mes de primera compra
Cohorte Clientes =
GENERATE(
    SUMMARIZE('Calendario', 'Calendario'[Año-Mes]),
    VAR MesCohorte = 'Calendario'[Año-Mes]
    VAR ClientesNuevos = CALCULATETABLE(
        VALUES('Clientes'[ID_Cliente]),
        'Calendario'[Año-Mes] = MesCohorte,
        FILTER(
            ALL('Calendario'),
            'Calendario'[Fecha] <= MIN('Ventas'[Fecha])
        )
    )
    RETURN
        ADDCOLUMNS(
            GENERATESERIES(0, 12, 1),  // meses desde cohorte
            "Mes Cohorte", MesCohorte,
            "Mes +N", [Value],
            "Clientes Activos", CALCULATE(
                COUNTROWS(INTERSECT(ClientesNuevos, VALUES('Ventas'[ID_Cliente]))),
                DATEADD('Calendario'[Fecha], [Value], MONTH)
            )
        )
)
🏆 Patrón 5: Tabla de calendario completa desde cero
// Calendario completo con todas las columnas útiles
Calendario Completo =
VAR MinDate = DATE(2020, 1, 1)
VAR MaxDate = DATE(2030, 12, 31)
RETURN
ADDCOLUMNS(
    GENERATESERIES(MinDate, MaxDate, 1),
    "Fecha", [Value],
    "Año", YEAR([Value]),
    "Mes", MONTH([Value]),
    "Día", DAY([Value]),
    "Año-Mes", FORMAT([Value], "YYYY-MM"),
    "Trimestre", "T" & QUARTER([Value]),
    "Año-Trimestre", YEAR([Value]) & "-T" & QUARTER([Value]),
    "NombreMes", FORMAT([Value], "MMMM"),
    "NombreMesCorto", FORMAT([Value], "MMM"),
    "DiaSemana", WEEKDAY([Value], 2),
    "NombreDia", FORMAT([Value], "DDDD"),
    "NombreDiaCorto", FORMAT([Value], "DDD"),
    "NumSemana", WEEKNUM([Value], 2),
    "EsFinSemana", WEEKDAY([Value], 2) >= 6,
    "DiaAño", DATEDIFF(DATE(YEAR([Value]), 1, 1), [Value], DAY) + 1,
    "Mes Relativo", DATEDIFF(DATE(YEAR(TODAY()), MONTH(TODAY()), 1), DATE(YEAR([Value]), MONTH([Value]), 1), MONTH),
    "Año Relativo", YEAR([Value]) - YEAR(TODAY())
)
💡 Tips finales para tablas calculadas
1. Usa VAR para descomponer expresiones complejas — mejora legibilidad y a veces performance
2. ADDCOLUMNS(SUMMARIZE(...)) es mejor que SUMMARIZE con agregaciones directas
3. Para Top N por grupo: GENERATE + TOPN es más flexible que TOPNPERLEVEL
4. UNION no elimina duplicados — si necesitas valores únicos, usa DISTINCT después
5. TREATAS es poderoso pero lento — úsalo solo cuando no puedas crear relación física
6. Para tablas grandes, considera si Power Query es mejor opción que tabla calculada
🚀 Siguiente paso
¡Las tablas calculadas ya no tienen secretos para ti! Con SUMMARIZECOLUMNS, GENERATE, UNION, INTERSECT y TREATAS puedes construir prácticamente cualquier estructura de datos que necesites. El siguiente paso es la Inteligencia de Tiempo — el conjunto de funciones que más preguntas genera en la comunidad DAX.