¡Hola a todos!

Si trabajas con Azure Analysis Services, SQL Server Analysis Services (SSAS) o Power BI Premium y te has quedado mirando esa barra de progreso durante horas esperando que termine el procesamiento, esta publicación es para ti. Procesar un modelo tabular de cientos de gigabytes en producción, con ventanas de actualización ajustadas y usuarios esperando los datos, es uno de los mayores desafíos del día a día de un DBA o profesional de BI.

En esta guía, cubriré absolutamente todo: desde lo que sucede bajo el capó en el motor VertiPaq, pasando por todos los tipos de procesamiento (con Process Add en profundidad), paralelismo interno vs. externo, optimizaciones en la fuente de datos (SQL Server, Synapse, Query Folding), particionamiento avanzado (Hot/Warm/Cold), Scale-Out con réplicas de lectura para cero tiempo de inactividad, tablas de agregación para modelos con miles de millones de filas, configuración de EncodingHint, uso profesional de Tabular Editor (BPA) y DAX Studio, automatización vía TMSL, PowerShell y ADF, y mucho más.

La mayoría de las técnicas se aplican igualmente a AAS, SSAS 2016+ y Power BI Premium/Fabric.

Esta es la guía que me hubiera gustado encontrar cuando empecé. ¡Así que, vamos allá!

Introducción a la Optimización del Procesamiento en Azure Analysis Services

Azure Analysis Services (AAS) es un servicio PaaS (Platform as a Service) basado en el mismo motor que SQL Server Analysis Services (SSAS) Tabular, pero ejecutándose totalmente gestionado en la nube de Azure. Internamente, utiliza el motor VertiPaq, una base de datos columnar en memoria altamente comprimida, optimizada para cargas de trabajo OLAP analíticas.

Entender lo que sucede bajo el capó es fundamental para cualquier optimización. Cuando procesas un modelo tabular, Analysis Services realiza, en orden, los siguientes pasos:

  1. Lectura de la Fuente (Data Read): El AS se conecta a tu fuente de datos (SQL Server, Synapse, Blob Storage, etc.) y ejecuta las consultas definidas en las particiones/tablas.
  2. Codificación y Compresión (Encoding): Los datos devueltos son codificados por VertiPaq usando Value Encoding o Hash Encoding, dependiendo del tipo de dato y la cardinalidad de la columna.
  3. Construcción del Diccionario de Datos: Para cada columna, el AS mantiene un diccionario de valores únicos mapeados a IDs enteros. Este diccionario es el corazón de la compresión columnar.
  4. Construcción de los Índices: Se construyen dos tipos principales de índice: el Forward Index (mapeo de fila → ID del valor) y el Inverted Index (mapeo de valor → filas que lo contienen, usado para filtros).
  5. Recálculo de las Jerarquías y Relaciones: Una vez que los datos están cargados, el AS reconstruye las relaciones y jerarquías entre tablas.
  6. Recálculo de Medidas (Process Recalc): Todas las medidas calculadas y columnas calculadas se recalculan en el contexto de los nuevos datos.

Cada una de estas etapas puede ser un cuello de botella dependiendo del escenario. Por eso, optimizar el procesamiento no es una única acción, es una estrategia en múltiples capas.

IMPORTANTE: Las optimizaciones descritas en esta guía son válidas para Azure Analysis Services (AAS), SQL Server Analysis Services 2016+ en modo tabular, y Power BI Premium/Fabric (vía XMLA Endpoint). El motor VertiPaq es el mismo en los tres productos.

VertiPaq y el uso de memoria RAM

VertiPaq es una base de datos en memoria. Esto significa que todos los datos del modelo deben caber en la RAM del servidor durante y después del procesamiento. En AAS, el nivel del servidor determina la RAM disponible:

  • D1: 3 GB
  • D2: 5 GB
  • D3: 10 GB
  • D4: 20 GB
  • S0: 25 GB
  • S1: 25 GB
  • S2: 50 GB
  • S4: 100 GB
  • S8/S9: 200–400 GB

El procesamiento de un modelo puede consumir hasta 2–3 veces el tamaño del modelo en disco de memoria RAM, ya que mientras los nuevos datos se cargan, los datos antiguos aún permanecen en la memoria (para no interrumpir consultas en ejecución). Este fenómeno se llama cold swap o hot swap dependiendo de cómo configures el procesamiento.

Cold Swap vs. Hot Swap en el Procesamiento

Este es un detalle técnico fundamental que impacta directamente el consumo de memoria durante el procesamiento:

  • Cold Swap (predeterminado): Los datos antiguos se liberan de la memoria antes de que se carguen los datos nuevos. Durante el procesamiento, las consultas al modelo devuelven error o datos desactualizados. Consume menos memoria RAM pero tiene indisponibilidad.
  • Hot Swap: Una nueva copia del modelo se carga en paralelo con la copia antigua. Cuando la nueva está lista, el AS realiza el intercambio atómico. Las consultas siguen funcionando durante el procesamiento. Requiere el doble de memoria RAM del modelo.

En AAS, el hot swap ocurre automáticamente cuando usas las APIs REST o el XMLA Endpoint para procesar. Para configurar el comportamiento, puedes ajustar la propiedad CommitMode en los scripts XMLA. Para modelos grandes, es común escalar el nivel del servidor verticalmente durante la ventana de procesamiento y volver a escalar después; AAS permite esto a través de la API REST sin tiempo de inactividad.

Entendiendo los Tipos de Procesamiento de Analysis Services

Comprender los diferentes tipos de procesamiento es el primer paso para una estrategia eficiente. Cada tipo tiene un costo y un propósito específico. Usar el tipo incorrecto es uno de los errores más comunes que llevan a procesamientos innecesariamente lentos.

Process Full

El Process Full es el tipo más completo y costoso. Ejecuta, en orden: Process ClearProcess DataProcess IndexProcess Recalc. Básicamente, borra todo y reconstruye desde cero.

  • Cuándo usar: Después de cambios estructurales en el modelo (adición/eliminación/renombramiento de columnas, cambio de tipos de datos, cambio en las consultas de las particiones). También indicado cuando hay sospecha de corrupción de datos o después de restaurar una copia de seguridad desactualizada.
  • Cuándo evitar: Nunca uses Process Full en tablas de hechos con miles de millones de filas como rutina diaria. El impacto en la ventana de mantenimiento puede ser inmanejable.
  • Costo estimado: Alto, lectura completa de la fuente + reconstrucción de todos los índices + recálculo de todas las medidas.

Para ejecutar vía TMSL (Tabular Model Scripting Language), utiliza el script a continuación en SQL Server Management Studio (SSMS) conectado a tu servidor AAS/SSAS:

{
  "refresh": {
    "type": "full",
    "objects": [
      {
        "database": "MeuModeloAAS",
        "table": "FatoVendas"
      }
    ]
  }
}

Process Clear

Elimina todos los datos de un objeto (modelo, tabla o partición), pero mantiene la estructura de metadatos intacta. El objeto queda en estado unprocessed después del Process Clear.

  • Cuándo usar: Cuando necesitas liberar memoria RAM urgentemente. O como primer paso de una estrategia de procesamiento manual en etapas. También útil para limpiar una partición específica antes de recargarla con datos corregidos.
  • Punto de atención: Después del Process Clear, las consultas al objeto devolverán un error hasta que se reprocese.
{
  "refresh": {
    "type": "clearValues",
    "objects": [
      {
        "database": "MeuModeloAAS",
        "table": "FatoVendas",
        "partition": "FatoVendas_Atual"
      }
    ]
  }
}

Process Data

Carga datos de la fuente al objeto especificado, pero no reconstruye los índices y no recalcula medidas. Es más rápido que el Process Full, pero el objeto queda en estado dirty; los datos están presentes, pero los índices de filtro y los cálculos DAX aún apuntan a los datos anteriores.

  • Cuándo usar: Como primer paso de un flujo de procesamiento en dos etapas: procesas los datos de varias tablas/particiones con Process Data (posiblemente en paralelo) y luego disparas un único Process Recalc en todo el modelo. Esto evita que el Process Recalc se ejecute repetidamente en cada tabla.
  • Punto de atención: Mientras una tabla está en estado dirty, las consultas que la involucran devuelven los datos antiguos. Nunca uses Process Data y dejes el modelo así, siempre completa el ciclo con Process Recalc.
  • Costo estimado: Medio, lee la fuente completa de la partición/tabla y reconstruye el diccionario y los segmentos, pero no rehace los índices de relación ni las columnas calculadas.

Ejemplos de uso vía TMSL, procesando una tabla específica, una partición específica y múltiples particiones:

// Exemplo 1: Process Data em uma tabela inteira
{
  "refresh": {
    "type": "dataOnly",
    "objects": [{ "database": "MeuModeloAAS", "table": "DimCliente" }]
  }
}
// Exemplo 2: Process Data em uma partição específica
{
  "refresh": {
    "type": "dataOnly",
    "objects": [{
      "database": "MeuModeloAAS",
      "table": "FatoVendas",
      "partition": "FatoVendas_2024_12"
    }]
  }
}
// Exemplo 3: Process Data em múltiplas partições + Process Recalc ao final
// Tudo num único comando TMSL (o AS gerencia o paralelismo internamente)
{
  "sequence": {
    "maxParallelism": 8,
    "operations": [
      {
        "refresh": {
          "type": "dataOnly",
          "objects": [
            { "database": "MeuModeloAAS", "table": "FatoVendas",   "partition": "FatoVendas_Hot" },
            { "database": "MeuModeloAAS", "table": "FatoEntradas", "partition": "FatoEntradas_Hot" },
            { "database": "MeuModeloAAS", "table": "DimProduto" },
            { "database": "MeuModeloAAS", "table": "DimCliente" }
          ]
        }
      },
      {
        "refresh": {
          "type": "calculate",
          "objects": [{ "database": "MeuModeloAAS" }]
        }
      }
    ]
  }
}

Este patrón sequence + dataOnly paralelo + calculate final es uno de los más eficientes para modelos con múltiples tablas grandes, ya que el Process Recalc se ejecuta solo una vez, después de que todos los datos estén cargados.

Process Add: El Tipo Más Poderoso (y Más Mal Entendido)

El Process Add es, en mi opinión, el tipo de procesamiento más incomprendido de Analysis Services, y por una razón simple: no aparece en la interfaz de usuario de SSMS cuando abres el diálogo "Process Database". Verás Process Full, Process Default, Process Clear, pero Process Add no está ahí. Esto se debe a que Process Add solo tiene sentido a nivel de partición específica, no de toda la base de datos. Para usarlo, necesitas abrir el diálogo de procesamiento de una partición individual en SSMS, o, lo más común en producción, escribir el script TMSL manualmente.

Cómo funciona Process Add Internamente

El Process Add ejecuta la consulta de origen de la partición (exactamente como se define en los metadatos del modelo) y agrega los resultados a lo que ya existe en la partición, sin borrar los datos anteriores. VertiPaq crea nuevos segmentos con los datos nuevos y los añade a la partición existente.

La Trampa Fatal de Process Add

CRÍTICO: El Process Add no sabe lo que es "nuevo". Simplemente ejecuta la consulta de la partición y añade el resultado. Si la consulta de la partición no filtra solo los datos nuevos desde el último procesamiento, tendrás datos duplicados. Por ejemplo: una partición con la consulta SELECT * FROM FatoVendas WHERE Ano = 2024, si ejecutas Process Add hoy, leerá todos los datos de 2024 y duplicará todo lo que ya estaba cargado.

La responsabilidad de garantizar que la consulta de la partición devuelva solo los datos que aún no se han cargado es totalmente tuya. El AS simplemente ejecuta lo que has definido.

Patrones Correctos para Usar Process Add

Existen tres patrones principales que hacen que Process Add sea seguro y eficiente:

Patrón 1, Partición de "delta" con tabla de staging:

La consulta de la partición apunta a una tabla de staging que contiene solo las filas nuevas desde el último procesamiento. Una vez que Process Add finaliza, truncas la tabla de staging (o marcas las filas como "procesadas").

-- Consulta de la partición "FatoVendas_Delta" en el modelo AS:
SELECT DataVendaKey, ProdutoKey, ClienteKey, VlrVenda, QtdVenda
FROM dbo.FatoVendas_Staging_AS
-- Esta tabla se trunca ANTES de cada carga ETL
-- y se llena SÓLO con los datos nuevos del día

-- Después de que Process Add finalice, en su pipeline ETL usted ejecuta:
TRUNCATE TABLE dbo.FatoVendas_Staging_AS;

Patrón 2, Watermark (marca de agua) en la consulta de la partición:

La consulta hace referencia a una tabla de control que almacena el punto de corte del último procesamiento exitoso. Después de Process Add, actualizas esta tabla.

-- Consulta de la partición "FatoVendas_Hot" en el modelo AS:
SELECT DataVendaKey, ProdutoKey, ClienteKey, VlrVenda, QtdVenda
FROM dbo.FatoVendas
WHERE RowVersion > (
    SELECT ISNULL(MAX(LastRowVersion), 0)
    FROM dbo.AS_ProcessControl
    WHERE TableName = 'FatoVendas' AND PartitionName = 'FatoVendas_Hot'
)

-- En el pipeline ETL, DESPUÉS de que Process Add finalice con éxito:
UPDATE dbo.AS_ProcessControl
SET LastRowVersion = (SELECT MAX(RowVersion) FROM dbo.FatoVendas),
    LastProcessedAt = GETDATE()
WHERE TableName = 'FatoVendas' AND PartitionName = 'FatoVendas_Hot';

Patrón 3, Partición de período fijo vacía:

El caso más simple y seguro: creas una partición nueva (que aún no tiene datos) y ejecutas Process Add (o Process Full, el resultado es el mismo, ya que la partición está vacía). Después de esto, la partición no se toca más con Process Add, solo con Process Full cuando sea necesario corregir datos históricos.

// Criar a partição para dezembro/2024 (vazia) via TMSL e processar logo em seguida
// A partição tem query: SELECT * FROM FatoVendas WHERE DataKey >= 20241201 AND DataKey < 20250101
// Como está vazia, Process Add = Process Full nesse caso
{
  "refresh": {
    "type": "add",
    "objects": [{
      "database": "MeuModeloAAS",
      "table": "FatoVendas",
      "partition": "FatoVendas_2024_12"
    }]
  }
}

Cuándo NO Usar Process Add

  • Datos con UPDATE o DELETE en la fuente: Si una venta del día anterior fue cancelada (DELETE) o corregida (UPDATE), Process Add no borrará la línea incorrecta de la partición, esta permanecerá allí. Para líneas mutables, usa siempre Process Full en la partición afectada.
  • Particiones con consulta de rango fijo: Una partición definida como WHERE Mes = 3 AND Ano = 2024 ya tiene todos los datos del período. Ejecutar Process Add en ella duplicará todo. Nunca hagas esto.
  • Sin mecanismo de control de delta: Si no tienes cómo garantizar que la consulta de la partición devuelva solo datos nuevos (sin tabla de marca de agua, sin staging, sin partición nueva vacía), no uses Process Add.
  • Después de muchos Process Add acumulados: Cada Process Add crea nuevos segmentos en la partición. Después de decenas o cientos de ejecuciones, la partición se fragmenta (muchos segmentos pequeños), lo que perjudica el rendimiento de escaneo. Periódicamente, haz un Process Full en la partición para consolidar todo en segmentos grandes y bien comprimidos, o usa Process Defrag.

Lista de Verificación Anti-Duplicación para Process Add

Antes de cada ejecución de Process Add en producción, valida:

  1. ¿La consulta de la partición tiene una cláusula que limita a los datos aún no cargados?
  2. ¿Existe un mecanismo de control (marca de agua, staging, partición vacía) que actualiza este filtro después de cada procesamiento exitoso?
  3. ¿Qué sucede si Process Add falla a la mitad? ¿La consulta intentará de nuevo y reenviará los mismos datos? ¿Esto genera duplicación?
  4. ¿Ya hiciste un SELECT COUNT(*) en la partición después del proceso para verificar el número de filas esperado?

TMSL completo para Process Add en una partición existente:

{
  "refresh": {
    "type": "add",
    "objects": [{
      "database": "MeuModeloAAS",
      "table": "FatoVendas",
      "partition": "FatoVendas_Hot"
    }]
  }
}

Después de Process Add en la partición, aún necesitas ejecutar un Process Recalc en la base de datos (o en la tabla) para que las columnas calculadas, jerarquías y relaciones reflejen los nuevos datos:

{
  "sequence": {
    "operations": [
      {
        "refresh": {
          "type": "add",
          "objects": [{ "database": "MeuModeloAAS", "table": "FatoVendas", "partition": "FatoVendas_Hot" }]
        }
      },
      {
        "refresh": {
          "type": "calculate",
          "objects": [{ "database": "MeuModeloAAS" }]
        }
      }
    ]
  }
}

Desfragmentación después de Process Add Repetido

Cada ejecución de Process Add añade un nuevo conjunto de segmentos a la partición. Una partición que recibió 365 Process Adds diarios a lo largo de un año puede tener cientos de segmentos de tamaños variados. Para verificar la fragmentación y corregir:

-- Verificar cuántos segmentos tiene cada partición
SELECT
    DIMENSION_NAME  AS Tabela,
    PARTITION_NAME  AS Particao,
    COUNT(*)        AS TotalSegmentos,
    SUM(RECORDS_COUNT) AS TotalLinhas,
    AVG(RECORDS_COUNT) AS MediaLinhasPorSegmento
FROM $SYSTEM.DISCOVER_STORAGE_TABLE_COLUMN_SEGMENTS
WHERE ATTRIBUTE_NAME = 'RowNumber'  -- RowNumber = 1 segmento por bloque de datos
GROUP BY DIMENSION_NAME, PARTITION_NAME
HAVING COUNT(*) > 5
ORDER BY TotalSegmentos DESC;

Si una partición tiene decenas o cientos de segmentos con muy pocas filas cada uno, haz un Process Full en ella para consolidar. El Process Full en la partición no afecta a las otras particiones de la misma tabla; los datos de las otras particiones permanecen intactos para consultas durante el reprocesamiento (gracias al Hot Swap).

Process Recalc

Recalcula todas las columnas calculadas, jerarquías, relaciones y medidas DAX del modelo. No lee datos de la fuente. Se ejecuta automáticamente al final de un Process Full, pero puede separarse para escenarios avanzados.

  • Cuándo usar: Al final de un flujo de procesamiento paralelo donde procesaste varias tablas con Process Data independientemente y quieres disparar el recálculo una única vez al final, evitando recalcular varias veces durante el proceso.
  • Costo estimado: Medio a alto, dependiendo de la complejidad de las medidas DAX y del número de columnas calculadas. En modelos con muchas columnas calculadas con fórmulas complejas, el Process Recalc puede ser la fase más demorada.
{
  "refresh": {
    "type": "calculate",
    "objects": [
      {
        "database": "MeuModeloAAS"
      }
    ]
  }
}

Process Defrag

Reorganiza internamente los segmentos de datos en el disco para mejorar el rendimiento de lectura en consultas subsiguientes. Análogo al REORGANIZE INDEX de SQL Server.

  • Cuándo usar: Periódicamente en modelos con muchas operaciones de Process Add incrementales. Con el tiempo, VertiPaq puede crear muchos segmentos pequeños y fragmentados, lo que empeora el rendimiento de escaneo.
  • Costo estimado: Bajo a medio, no lee de la fuente, solo reorganiza datos en memoria/disco.

Process Index

Reconstruye los índices (índice directo e índice invertido) para un objeto sin recargar datos de la fuente. Generalmente se ejecuta después de un Process Data.

  • Cuándo usar: En flujos manuales donde Process Data y Process Index se separan para un control granular. Menos común en automatizaciones modernas donde el refresh TMSL se encarga de todo.

Resumen: ¿Qué tipo usar en cada escenario?

Para ayudar en la toma de decisiones, vea la tabla de referencia rápida:

  • Cambio estructural en el modelo (nueva columna, tipo de dato): Process Full en la tabla afectada.
  • Actualización diaria vía append-only con staging/watermark: Process Add en la partición "hot" + Process Recalc en el modelo.
  • Actualización diaria de tabla grande (con posibles updates/deletes): Process Full en la partición "hot" + Process Recalc en el modelo.
  • Reprocesamiento de dato corregido en período histórico: Process Full solo en la partición histórica afectada + Process Recalc en el modelo.
  • Partición nueva siendo cargada por primera vez: Process Full o Process Add (equivalentes, la partición está vacía).
  • Múltiples tablas/particiones con máximo paralelismo: Process Data en todas + Process Recalc único al final (en un único comando TMSL).
  • Partición con muchos Process Add acumulados y fragmentada: Process Full en la partición para consolidar los segmentos.
  • Modelo con fragmentación acumulada sin necesidad de reload: Process Defrag semanal agendado.

Para la mayoría de las rutinas de actualización en producción, la combinación de Process Add en las particiones incrementales (con query de delta correcta) + Process Full en la partición "hot" + Process Recalc en el modelo es el enfoque más eficiente y seguro para grandes modelos.

Paralelismo en el Procesamiento: El AS Se Encarga vs. Usted "Ayuda" (y Empeora)

El paralelismo es el tema más incomprendido cuando se habla de optimizar el procesamiento de Analysis Services. La intuición lleva a mucha gente por el camino equivocado: "si envío 4 comandos de procesamiento al mismo tiempo, ¡será 4 veces más rápido!" En la práctica, puede ser más lento o fallar.

Vamos a entender la diferencia entre los dos tipos de paralelismo y cuándo cada uno es adecuado.

Paralelismo Interno: Deje que el AS Trabaje

Cuando usted envía un único comando TMSL con múltiples objetos, el AS gestiona todo el paralelismo internamente. Analiza las dependencias entre los objetos (por ejemplo, sabe que el Process Recalc necesita esperar a que todos los Process Data terminen), distribuye el trabajo entre los hilos disponibles y coordina el uso de memoria de forma inteligente.

// Paralelismo interno: 1 comando sequence, N objetos, AS gerencia tudo
// maxParallelism e propriedade do sequence, nao do refresh
{
  "sequence": {
    "maxParallelism": 8,
    "operations": [
      {
        "refresh": {
          "type": "full",
          "objects": [
            { "database": "MeuModeloAAS", "table": "FatoVendas",    "partition": "FatoVendas_2024_11" },
            { "database": "MeuModeloAAS", "table": "FatoVendas",    "partition": "FatoVendas_2024_12" },
            { "database": "MeuModeloAAS", "table": "FatoEntradas",  "partition": "FatoEntradas_Hot" },
            { "database": "MeuModeloAAS", "table": "FatoDevolucoes","partition": "FatoDevolucoes_Hot" },
            { "database": "MeuModeloAAS", "table": "DimProduto" },
            { "database": "MeuModeloAAS", "table": "DimCliente" }
          ]
        }
      }
    ]
  }
}

El parámetro maxParallelism define el límite de objetos procesados simultáneamente. Si no se especifica, el AS usa su propio criterio (generalmente basado en el número de núcleos de CPU). Aumentar más allá de los núcleos disponibles rara vez ayuda y puede empeorar por la contención de recursos.

Este es el patrón recomendado para el 99% de los casos. Es seguro, predecible y el AS lo optimiza automáticamente.

Paralelismo Externo: Por qué Parece una Buena Idea pero (Casi Siempre) No lo Es

El "paralelismo externo" es cuando usted, desde fuera del AS, dispara múltiples comandos de procesamiento simultáneamente, por ejemplo, 4 trabajos de PowerShell ejecutándose en paralelo, cada uno enviando un script TMSL separado al mismo servidor.

# Ejemplo del ANTI-PATRÓN: paralelismo externo
# Disparar 4 procesamientos independientes "al mismo tiempo"
$jobs = @(
    Start-Job { Invoke-ASCmd -Server $srv -Query '{"refresh":{"type":"full","objects":[{"database":"M","table":"FatoVendas","partition":"P1"}]}}' },
    Start-Job { Invoke-ASCmd -Server $srv -Query '{"refresh":{"type":"full","objects":[{"database":"M","table":"FatoVendas","partition":"P2"}]}}' },
    Start-Job { Invoke-ASCmd -Server $srv -Query '{"refresh":{"type":"full","objects":[{"database":"M","table":"FatoEntradas","partition":"P1"}]}}' },
    Start-Job { Invoke-ASCmd -Server $srv -Query '{"refresh":{"type":"full","objects":[{"database":"M","table":"DimProduto"}]}}' }
)
$jobs | Wait-Job | Receive-Job

En teoría, parece genial: ¡4 procesamientos al mismo tiempo! En la práctica, los problemas son serios:

Los Problemas del Paralelismo Externo

  • Pico de memoria RAM no coordinado: Cada comando externo asigna memoria de forma independiente, sin comunicación entre sí. El AS no tiene visibilidad de lo que están haciendo los otros comandos. Si cada uno necesita 10 GB de RAM en el pico y el servidor tiene 32 GB en total, alcanzará el HardMemoryLimit y el AS comenzará a cancelar transacciones o rechazar conexiones.
  • Contención en el Process Recalc: Cada comando TMSL de type: "full" incluye un Process Recalc al final. Cuando dos comandos llegan a la fase de recálculo al mismo tiempo, compiten por un bloqueo exclusivo de escritura en los metadatos del modelo. Uno se ejecutará, el otro esperará (o fallará por tiempo de espera). Usted perdió todo el beneficio del paralelismo.
  • Bloqueo de metadatos y conflictos de transacción: Las operaciones de procesamiento adquieren bloqueos de escritura en el modelo. Dos comandos externos intentando escribir al mismo tiempo generan una cola de serialización o errores de interbloqueo, lo que resulta en una ejecución secuencial empeorada por la sobrecarga de gestionar múltiples transacciones concurrentes.
  • Inconsistencia en caso de fallo parcial: Si 3 de los 4 comandos se completan y 1 falla, el modelo queda en un estado inconsistente, algunas particiones actualizadas, otras no, sin una transacción que agrupe todo. Con el paralelismo interno en un único TMSL, usted controla el commitMode y el comportamiento de fallo de forma explícita.
  • Sin orquestación de dependencias: El AS, dentro de un único TMSL, sabe que necesita procesar las dimensiones antes de realizar el recálculo de las tablas de hechos que dependen de ellas. Con comandos externos, usted necesita implementar esta lógica de dependencia manualmente, y es fácil cometer errores.

Cuando el Paralelismo Externo PUEDE Funcionar

Existen escenarios específicos donde enviar múltiples comandos en paralelo tiene sentido:

  • Bases de datos separadas en el mismo servidor: Procesar la base de datos "Ventas" y la base de datos "Financiero" simultáneamente es seguro, tienen metadatos independientes, sin conflicto de bloqueos.
  • Fase de Process Data puro, sin Process Recalc: Si usted garantiza que todos los comandos externos son del tipo dataOnly (sin recálculo), la contención de bloqueos es mucho menor. Usted dispara los Process Data en paralelo, espera a que todos terminen, y solo entonces envía un único calculate. Pero en este punto, un único TMSL con sequence ya lo hace mejor.
  • Power BI Premium con Enhanced Refresh API: La API de refresh de Power BI (REST API v2) gestiona el paralelismo en el servidor, no en el cliente. Usted puede enviar múltiples solicitudes de refresh que el servicio pone en cola y orquesta con visibilidad de recursos. Este sí es un paralelismo externo bien implementado.

El Patrón Correcto para Máximo Paralelismo

La forma más eficiente y segura de maximizar el paralelismo en el procesamiento del AAS es usar un único TMSL con el patrón sequence + dataOnly paralelo + calculate final:

{
  "sequence": {
    "maxParallelism": 10,
    "operations": [
      {
        "refresh": {
          "type": "dataOnly",
          "objects": [
            { "database": "MeuModeloAAS", "table": "FatoVendas",    "partition": "FatoVendas_2024_12" },
            { "database": "MeuModeloAAS", "table": "FatoVendas",    "partition": "FatoVendas_Hot" },
            { "database": "MeuModeloAAS", "table": "FatoEntradas",  "partition": "FatoEntradas_Hot" },
            { "database": "MeuModeloAAS", "table": "FatoDevolucoes","partition": "FatoDevolucoes_Hot" },
            { "database": "MeuModeloAAS", "table": "DimProduto" },
            { "database": "MeuModeloAAS", "table": "DimCliente" },
            { "database": "MeuModeloAAS", "table": "DimCalendario" }
          ]
        }
      },
      {
        "refresh": {
          "type": "calculate",
          "objects": [{ "database": "MeuModeloAAS" }]
        }
      }
    ]
  }
}

En este patrón: todos los objetos en el paso 1 se procesan en paralelo (hasta el límite de maxParallelism). El AS espera a que todos se completen. Solo entonces ejecuta el calculate único en el paso 2. Usted tiene el máximo de paralelismo con la mínima contención y total consistencia transaccional.

Regla de Oro del Paralelismo

Use un único comando TMSL con múltiples objetos y deje que el AS gestione el paralelismo internamente vía maxParallelism. Solo dispare múltiples comandos externos en paralelo cuando los objetos estén en bases de datos diferentes en el mismo servidor. Para todo dentro de la misma base de datos, confíe en el AS, lo hace mejor que usted.

Optimizando la Fuente de Datos: Su Primer Paso para el Rendimiento

La optimización comienza mucho antes de Analysis Services. El AS es tan rápido como su fuente de datos lo permite. Si la consulta en el origen tarda 2 horas en devolver los datos, no hay optimización en el modelo que resuelva eso. Por eso, vamos a empezar por la capa más ignorada: la fuente.

Diagnosticando el Cuello de Botella en la Fuente

Antes de cualquier optimización, necesita medir. Use DAX Studio para capturar la consulta que el AS está enviando a la fuente de datos:

  1. Conecte DAX Studio a su servidor AAS/SSAS.
  2. Vaya a Advanced > View Metrics y habilite Query Plan y Server Timings.
  3. Ejecute un EVALUATE ROW("x", COUNTROWS(FatoVendas)) para forzar un escaneo completo.
  4. En la pestaña Server Timings, verá el tiempo empleado en Storage Engine (SE), que incluye el tiempo de lectura de la fuente, versus Formula Engine (FE).

Alternativamente, durante el procesamiento, verifique el SQL Server Profiler o los Extended Events del AAS/SSAS para capturar los eventos ProgressReportBegin/End e identificar qué tablas/particiones son más lentas.

SQL Server en el Origen: Buenas Prácticas para la Lectura por el AS

Antes de crear cualquier índice, hay ajustes de comportamiento de ejecución que impactan directamente el tiempo de lectura y que casi nadie configura. El AS envía consultas SQL a la fuente como cualquier otro cliente, pero con patrones que pueden ser subóptimos dependiendo del entorno.

Nivel de Aislamiento: Evitando Bloqueos durante el Procesamiento

Por defecto, el AS ejecuta lecturas con READ COMMITTED. En bases de datos con ETL activo durante el procesamiento del AS (situación común en pipelines ajustados), las lecturas se bloquean en las filas con bloqueo de escritura. El resultado es que el AS espera a que el ETL libere los bloqueos antes de poder leer, lo que puede añadir decenas de minutos al procesamiento. Para tablas de DW donde la carga ya se ha completado antes de que el AS comience:

-- Opción 1: NOLOCK en la consulta de la partición (lecturas sucias, adecuado para DW con carga concluida)
SELECT DataVendaKey, ProdutoKey, ClienteKey, VlrVenda, QtdVenda
FROM dbo.FatoVendas WITH (NOLOCK)
WHERE DataVendaKey >= 20240101 AND DataVendaKey < 20250101;

-- Opción 2: READ_COMMITTED_SNAPSHOT en la base de datos (MVCC: lecturas consistentes sin bloquear escrituras)
-- Más seguro que NOLOCK, sin lecturas sucias, sin costo extra de bloqueo
-- Requiere habilitar una vez en la base de datos de origen:
ALTER DATABASE MeuDW SET READ_COMMITTED_SNAPSHOT ON;
-- Con RCSI activo, el AS lee la versión confirmada más reciente de los datos sin bloquear escrituras

Use NOLOCK solamente cuando la carga de ETL en la tabla haya sido completada antes de que el AS comience a leer. Para datos donde la consistencia importa, prefiera habilitar READ_COMMITTED_SNAPSHOT en la base de datos de origen. Es un ajuste único que beneficia todas las lecturas analíticas sin alterar las consultas.

Estadísticas: Actualizar después de Cada Carga de ETL

Las estadísticas desactualizadas hacen que el optimizador de SQL Server elija planes malos para las consultas que el AS envía, pudiendo causar escaneos completos en tablas de miles de millones de filas cuando un índice columnstore eliminaría el 99% de las filas. Programe la actualización justo después del ETL:

-- Actualizar estadísticas de tablas críticas con escaneo completo después de cada carga
UPDATE STATISTICS dbo.FatoVendas WITH FULLSCAN;
UPDATE STATISTICS dbo.DimProduto  WITH FULLSCAN;

-- Identificar tablas con estadísticas desactualizadas (más del 10% de las filas modificadas)
SELECT
    OBJECT_NAME(s.object_id)       AS tabela,
    s.name                          AS estatistica,
    sp.last_updated                 AS ultima_atualizacao,
    sp.rows                         AS linhas,
    sp.modification_counter         AS modificacoes_desde_atualizacao,
    CAST(sp.modification_counter * 100.0 / NULLIF(sp.rows, 0) AS DECIMAL(5,1)) AS pct_modificado
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE OBJECT_SCHEMA_NAME(s.object_id) = 'dbo'
  AND sp.rows > 0
  AND sp.modification_counter * 100.0 / sp.rows > 10
ORDER BY sp.modification_counter DESC;

OPTION RECOMPILE y MAXDOP en las Consultas de Partición

El AS genera consultas con literales en los filtros de partición (ej: WHERE DataVendaKey >= 20240101). SQL Server puede reutilizar un plan en caché compilado para otros valores, que no es óptimo para los literales específicos. OPTION (RECOMPILE) fuerza la recompilación con estadísticas actuales para cada ejecución, generando un plan ideal para los datos reales. En servidores compartidos, combine con MAXDOP para no afectar las consultas OLTP:

-- Consulta de partición optimizada: plan fresco + paralelismo controlado
SELECT DataVendaKey, ProdutoKey, ClienteKey, VlrVenda, QtdVenda
FROM dbo.FatoVendas WITH (NOLOCK)
WHERE DataVendaKey >= 20240101 AND DataVendaKey < 20250101
OPTION (MAXDOP 8, RECOMPILE);
-- MAXDOP 8: usa 8 núcleos para lectura paralela, deja otros para OLTP
-- RECOMPILE: plan compilado con los literales reales, aprovecha la eliminación de particiones

Índices en el Origen de Datos (SQL Server / Azure Synapse)

Para fuentes relacionales, los índices en el origen son críticos. El AS generalmente ejecuta una consulta del tipo SELECT col1, col2, col3 FROM tabla WHERE filtro y espera un escaneo completo eficiente. Para ello:

Clustered Columnstore Index (CCI): El Rey para DW

Para tablas de hechos en SQL Server o Azure Synapse, el Clustered Columnstore Index es la mejor elección. Almacena datos en formato columnar comprimido, elimina la necesidad de leer columnas innecesarias y es extremadamente eficiente para escaneos analíticos (que es exactamente lo que hace el AS).

Para crear un CCI en una tabla existente, utilice el script a continuación:

-- Creando CCI en tabla de hechos existente (SQL Server / Synapse)
DROP INDEX IF EXISTS CCI_FatoVendas ON dbo.FatoVendas;

CREATE CLUSTERED COLUMNSTORE INDEX CCI_FatoVendas
ON dbo.FatoVendas
WITH (DATA_COMPRESSION = COLUMNSTORE_ARCHIVE, MAXDOP = 8);

-- Verificar calidad de los Row Groups creados
SELECT
    object_name(rg.object_id)   AS tabela,
    rg.partition_number,
    rg.row_group_id,
    rg.state_desc,
    rg.total_rows,
    rg.deleted_rows,
    rg.size_in_bytes / 1024.0   AS tamanho_kb
FROM sys.dm_db_column_store_row_group_physical_stats rg
WHERE object_id = OBJECT_ID('dbo.FatoVendas')
ORDER BY rg.row_group_id;

Resultado: Los grupos de filas ideales tienen cerca de 1.048.576 filas (1M). Los grupos de filas mucho más pequeños (por debajo de 100k filas) indican fragmentación y deben ser reorganizados con ALTER INDEX REORGANIZE.

La opción DATA_COMPRESSION = COLUMNSTORE_ARCHIVE aplica compresión adicional (DEFLATE) sobre la compresión columnar estándar. Para tablas históricas raramente accedidas, puede reducir el tamaño hasta en un 50% adicional con un impacto mínimo en el tiempo de lectura del AS, que ya lee los datos comprimidos.

NonClustered Columnstore Index (NCCI)

Si su tabla ya tiene un Clustered Index (B-Tree) y no puede ser convertida a CCI (por ejemplo, por ser una tabla transaccional con actualizaciones/eliminaciones frecuentes), se puede crear un NonClustered Columnstore Index solo en las columnas que el AS leerá:

-- NCCI solo en las columnas usadas por Analysis Services
CREATE NONCLUSTERED COLUMNSTORE INDEX NCCI_FatoVendas_AS
ON dbo.FatoVendas (DataVendaKey, ProdutoKey, ClienteKey, VlrVenda, QtdVenda)
WHERE DataVendaKey >= 20200101;  -- Filtro para datos relevantes al modelo

Particionamiento en la Fuente (SQL Server)

Si su tabla de hechos está particionada en SQL Server, configure la consulta de la partición del AS para usar partition elimination. En lugar de filtrar por fecha directamente, consulte por la función de partición:

-- Consulta de partición del AS alineada con el particionamiento de SQL Server
-- Partición "2024" en el AS mapea a la partición de 2024 en la tabla de origen
SELECT
    DataVendaKey, ProdutoKey, ClienteKey, VlrVenda, QtdVenda, Margem
FROM dbo.FatoVendas
WHERE DataVendaKey >= 20240101
  AND DataVendaKey < 20250101
  -- SQL Server usa la restricción para hacer eliminación de particiones automáticamente

Cuando la tabla de origen está particionada y la consulta incluye el criterio de filtro de partición, SQL Server realiza la eliminación de particiones y lee solo los filegroups necesarios, drásticamente más rápido que un escaneo completo de la tabla.

Plegado de Consultas en Power Query / M

Cuando su modelo utiliza Power Query (M) como capa de transformación, el Plegado de Consultas es crítico para el rendimiento. El Plegado de Consultas ocurre cuando Power Query logra convertir sus transformaciones M en SQL nativo que se ejecuta directamente en la fuente, en lugar de extraer todos los datos brutos y filtrar/transformar en memoria en el AS.

Para verificar si un paso de su consulta está realizando plegado, haga clic derecho en el paso en el Editor de Power Query y vea si la opción "Ver consulta nativa" está disponible (si está disponible, el plegado está ocurriendo).

ATENCIÓN: Ciertas transformaciones M rompen el Plegado de Consultas, haciendo que Power Query/AS extraiga todos los datos brutos a la memoria antes de filtrar. Las principales son: Table.Buffer(), transformaciones de texto con funciones no soportadas por el conector, Table.FromList(), List.Generate(), y cualquier referencia a otras consultas que no realizan plegado. Después de cualquiera de estas operaciones, todas las transformaciones siguientes también pierden el plegado.

Para forzar el filtro en la fuente y garantizar el plegado, realice el filtro de fecha/clave como primer paso de la consulta, antes de cualquier otra transformación:

-- En el origen SQL, garantice la consulta con WHERE ya filtrado
-- En Power Query, el primer paso debe ser el filtro de fecha:
-- Source = Sql.Database("servidor", "banco"),
-- FatoVendas = Source{[Schema="dbo",Item="FatoVendas"]}[Data],
-- FiltroData = Table.SelectRows(FatoVendas, each [DataVendaKey] >= 20200101)
-- ^^ esto hace query folding, generando WHERE DataVendaKey >= 20200101 en la fuente

Vistas Materializadas en Azure Synapse Analytics

Para fuentes en Azure Synapse Dedicated SQL Pool, una funcionalidad poderosa es la Vista Materializada. A diferencia de una vista común, la vista materializada precalcula y almacena físicamente los resultados de una consulta compleja (joins, agregaciones), siendo automáticamente mantenida actualizada por Synapse:

-- Creando una Materialized View en Azure Synapse para alimentar el AS
CREATE MATERIALIZED VIEW dbo.mv_VendasPorDiaCategoria
WITH (DISTRIBUTION = HASH(DataVendaKey))
AS
SELECT
    fv.DataVendaKey,
    dp.CategoriaKey,
    dp.SubcategoriaKey,
    dc.RegiaoKey,
    SUM(fv.VlrVenda)        AS TotalVendas,
    SUM(fv.QtdVenda)        AS TotalQuantidade,
    SUM(fv.CustoVenda)      AS TotalCusto,
    COUNT(DISTINCT fv.ClienteKey) AS ClientesDistintos
FROM dbo.FatoVendas fv
INNER JOIN dbo.DimProduto dp ON fv.ProdutoKey = dp.ProdutoKey
INNER JOIN dbo.DimCliente dc ON fv.ClienteKey = dc.ClienteKey
GROUP BY fv.DataVendaKey, dp.CategoriaKey, dp.SubcategoriaKey, dc.RegiaoKey;

Resultado: El AS lee de la vista materializada en lugar de la tabla de hechos bruta, saltándose el join y la agregación que antes se realizaban en Synapse en cada procesamiento. Para modelos con joins complejos en el origen, esto puede reducir el tiempo de lectura en un 60-80%.

Distribución de Tablas en Azure Synapse

El Synapse Dedicated SQL Pool distribuye datos entre 60 nodos computacionales. La elección incorrecta de distribución puede causar un movimiento masivo de datos durante los joins, lo que ralentiza las consultas. Para tablas grandes leídas por el AS:

  • Hash Distribution: Ideal para tablas de hechos grandes. Distribuya por la clave utilizada en los joins más frecuentes (generalmente la clave de fecha o producto). Esto coloca los datos de ambos lados del join en el mismo nodo, eliminando el movimiento de datos.
  • Replicated: Ideal para tablas de dimensión pequeñas (hasta ~2 GB). Una copia completa en cada nodo elimina todo el movimiento de datos para los joins con la tabla de hechos.
  • Round Robin: Estándar, evite para tablas leídas por el AS en producción.
-- Tabla de hechos: Hash en la clave de unión con la dimensión principal
CREATE TABLE dbo.FatoVendas
(
    FatoVendaKey    BIGINT NOT NULL,
    DataVendaKey    INT    NOT NULL,
    ProdutoKey      INT    NOT NULL,
    ClienteKey      INT    NOT NULL,
    VlrVenda        DECIMAL(18,2),
    QtdVenda        INT
)
WITH
(
    DISTRIBUTION = HASH(ProdutoKey),  -- alinea con la dimensión más selectiva
    CLUSTERED COLUMNSTORE INDEX
);

-- Tabla de dimensión pequeña: Replicated (copia en todos los nodos)
CREATE TABLE dbo.DimProduto
(
    ProdutoKey      INT NOT NULL,
    NomeProduto     NVARCHAR(200),
    CategoriaKey    INT,
    Preco           DECIMAL(10,2)
)
WITH
(
    DISTRIBUTION = REPLICATE,
    CLUSTERED INDEX (ProdutoKey)
);

Co-Localización y Red: El Cuello de Botella Silencioso

Uno de los factores más ignorados en el procesamiento de AAS es la latencia y el ancho de banda de red entre el servidor AAS y la fuente de datos. Durante el procesamiento de un modelo grande, AS puede transferir cientos de gigabytes de la fuente. Si esta transferencia pasa por una conexión con alta latencia o ancho de banda limitado, todas las ganancias de índice y particionamiento se ven comprometidas.

  • Misma región de Azure: El AAS y la fuente (Azure SQL, Synapse, ADLS) deben estar en la misma región de Azure. El tráfico entre regiones tiene latencia adicional (típicamente 20–100ms dependiendo de la distancia) y puede ser tarifado como tráfico de salida. El tráfico intra-región es gratuito y tiene una latencia de 1–2ms.
  • Firewall de AAS: Azure Analysis Services tiene un firewall IP nativo (configurable en el portal de Azure, pestaña Firewall del servidor). Habilítelo y restrinja el acceso de entrada solo a los rangos de IP necesarios (por ejemplo, la IP del Servicio Power BI, de los administradores y de las herramientas de despliegue). AAS no soporta Azure Private Link/Private Endpoint, a diferencia de servicios más recientes como Fabric. Para proteger la fuente de datos de accesos externos, configure el firewall de Azure SQL o Synapse para aceptar solo la IP de salida del servidor AAS.
  • On-Premises con ExpressRoute: Si la fuente de datos está on-premises, use Azure ExpressRoute para una conexión dedicada de alta velocidad. Evite procesar AAS a través de VPN básica para fuentes grandes, ya que el ancho de banda es insuficiente para transferir decenas de gigabytes en tiempo y forma (una VPN S2S típica tiene 1–10 Gbps teóricos, pero con latencia y sobrecarga que impactan las transferencias largas).
  • On-Premises Data Gateway: Para fuentes on-premises sin ExpressRoute, el Gateway actúa como proxy entre el AAS y la fuente local. Instálelo en una máquina en la misma red de la fuente (no en la misma máquina que la fuente, para no competir por CPU). Monitoree el uso de CPU y memoria del Gateway durante el procesamiento.
  • Mover datos a Azure primero: El patrón más eficiente para entornos híbridos es usar el pipeline de datos (ADF, Synapse Pipelines) para mover los datos del on-premises a Azure (Azure SQL, Synapse, ADLS) como primera etapa, y solo entonces el AAS lee de la fuente, todo dentro de la misma región de Azure. Elimina la latencia on-premises del camino crítico del procesamiento.

Estrategias de Particionamiento para Modelos Tabulares

El particionamiento es la técnica individual más impactante para optimizar el procesamiento de modelos tabulares grandes. Sin particionamiento, todo procesamiento es un Process Full en la tabla completa. Con un buen esquema de particionamiento, puede reducir el volumen de datos reprocesado en más del 90% por operación.

¿Por qué Particionar? Los Tres Beneficios Principales

  • 1. Procesamiento Incremental: Procesar solo la porción de datos que cambió, en lugar de toda la tabla. Una tabla de 5 años de datos con particiones mensuales permite procesar solo el mes actual, 1/60 de los datos totales.
  • 2. Paralelismo: El AS puede procesar múltiples particiones en paralelo, utilizando todos los núcleos de CPU disponibles. Sin particiones, el procesamiento de una única tabla es de un solo hilo.
  • 3. Alta Disponibilidad: Mientras una partición está siendo reprocesada, las otras permanecen disponibles para consultas. Los usuarios no pierden acceso a los datos históricos durante la actualización.

Eligiendo la Granularidad de las Particiones

La granularidad ideal depende del volumen de datos y la frecuencia de actualización. Como regla general:

  • Particiones anuales: Para tablas con pocos millones de filas por año o datos históricos raramente actualizados.
  • Particiones mensuales: El patrón más común para tablas de hechos de tamaño medio-grande (decenas de millones de filas/mes).
  • Particiones diarias: Para tablas con un volumen muy alto (cientos de millones de filas/día) o cuando la granularidad de actualización es por hora/día.
  • Particiones por entidad: Para escenarios específicos como multi-tenancy (una partición por cliente/región), muy usada en Power BI Embedded.

Cuidado con muchas particiones

IMPORTANTE: Crear cientos o miles de particiones tiene un costo. El AS mantiene metadatos para cada partición, y la gestión de muchas particiones puede impactar el tiempo de procesamiento y el rendimiento de las consultas. Mantenga el número de particiones por tabla por debajo de 200–300 para la mayoría de los escenarios. Para Power BI Premium/Fabric, el límite es de 1.000 particiones por tabla.

Patrón Hot/Warm/Cold de Particionamiento

Este es el patrón de particionamiento más sofisticado y eficiente para grandes modelos. La idea es dividir las particiones en tres capas:

  • Hot (Partición Actual): Datos de los últimos días/semanas que aún pueden cambiar. Reprocesada con Process Full en cada ciclo (diario u horario). Normalmente 1 partición.
  • Warm (Últimos meses): Datos de 1–6 meses atrás, consolidados pero aún relevantes para análisis recientes. Reprocesada semanalmente o cuando hay correcciones. Generalmente 6–12 particiones mensuales.
  • Cold (Histórico): Datos de años anteriores. Consolidados e inmutables. Nunca reprocesados en la rutina normal, solo cuando hay una corrección histórica específica. Pueden tener decenas de particiones anuales.

Con este patrón, una actualización diaria típica procesa solo la partición "Hot", que puede contener solo el día actual, en lugar de años de historia.

Creando y Gestionando Particiones vía TMSL

Para automatizar la creación de particiones, puede usar scripts TMSL ejecutados vía SSMS, PowerShell o Azure Data Factory. Vea un ejemplo completo de creación de particiones mensuales para 12 meses:

{
  "createOrReplace": {
    "object": {
      "database": "MeuModeloAAS",
      "table": "FatoVendas",
      "partition": "FatoVendas_2024_01"
    },
    "partition": {
      "name": "FatoVendas_2024_01",
      "source": {
        "type": "query",
        "query": "SELECT DataVendaKey, ProdutoKey, ClienteKey, VlrVenda, QtdVenda FROM dbo.FatoVendas WHERE DataVendaKey >= 20240101 AND DataVendaKey < 20240201",
        "dataSource": "DS_Synapse"
      },
      "mode": "import"
    }
  }
}

Automatizando Particiones con PowerShell y AMO

Para crear particiones dinámicamente (por ejemplo, crear la partición del próximo mes automáticamente), use PowerShell con la biblioteca AMO (Analysis Management Objects) o con el módulo SqlServer:

# Instalar el módulo SqlServer si es necesario: Install-Module -Name SqlServer
Import-Module SqlServer

$servidor   = "asazure://brazilsouth.asazure.windows.net/meuservidor"
$banco       = "MeuModeloAAS"
$tabela      = "FatoVendas"
$dataFonte   = "DS_Synapse"

# Conectar al servidor AS
$server = New-Object Microsoft.AnalysisServices.Tabular.Server
$server.Connect($servidor)
$db    = $server.Databases[$banco]
$table = $db.Model.Tables[$tabela]

# Crear particiones para cada mes de 2024
$ano = 2024
for ($mes = 1; $mes -le 12; $mes++) {
    $inicio  = Get-Date -Year $ano -Month $mes -Day 1
    $fim     = $inicio.AddMonths(1)
    $nomePart = "FatoVendas_{0}_{1:D2}" -f $ano, $mes
    $iniKey  = $inicio.ToString("yyyyMMdd")
    $fimKey  = $fim.ToString("yyyyMMdd")

    # Verificar si ya existe
    if ($table.Partitions[$nomePart]) {
        Write-Host "Partição $nomePart já existe, pulando..." -ForegroundColor Yellow
        continue
    }

    $particao = New-Object Microsoft.AnalysisServices.Tabular.Partition
    $particao.Name = $nomePart

    $fonte = New-Object Microsoft.AnalysisServices.Tabular.QueryPartitionSource
    $fonte.DataSource = $db.Model.DataSources[$dataFonte]
    $fonte.Query = "SELECT DataVendaKey, ProdutoKey, ClienteKey, VlrVenda, QtdVenda FROM dbo.FatoVendas WHERE DataVendaKey >= $iniKey AND DataVendaKey < $fimKey"

    $particao.Source = $fonte
    $table.Partitions.Add($particao)

    Write-Host "Partição $nomePart criada." -ForegroundColor Green
}

$db.Model.SaveChanges()
$server.Disconnect()
Write-Host "Partições salvas com sucesso!" -ForegroundColor Cyan

Resultado: Con este script programado en Azure Automation o Task Scheduler, usted garantiza que la partición del próximo mes se crea automáticamente, sin intervención manual.

Procesamiento Paralelo de Particiones

El AS soporta procesamiento paralelo nativo cuando incluye múltiples objetos en el mismo comando TMSL de refresh. El servidor decide automáticamente el nivel de paralelismo basado en los recursos disponibles. Para forzar el paralelismo máximo:

{
  "sequence": {
    "maxParallelism": 10,
    "operations": [
      {
        "refresh": {
          "type": "full",
          "objects": [
            { "database": "MeuModeloAAS", "table": "FatoVendas",    "partition": "FatoVendas_Hot" },
            { "database": "MeuModeloAAS", "table": "FatoEntradas",  "partition": "FatoEntradas_Hot" },
            { "database": "MeuModeloAAS", "table": "FatoDevolucoes","partition": "FatoDevolucoes_Hot" }
          ],
          "retryCount": 2,
          "commitMode": "transactional"
        }
      }
    ]
  }
}

El parámetro maxParallelism controla cuántas particiones se procesan simultáneamente. El valor predeterminado es el número de núcleos de CPU del servidor. Aumentar este valor puede acelerar el procesamiento en servidores con muchos núcleos, pero aumenta el pico de uso de memoria RAM (cada partición que se procesa ocupa memoria).

El parámetro commitMode acepta dos valores:

  • "transactional": Todas las particiones del lote se confirman juntas al final. Si una falla, todas se revierten. Garantiza la consistencia pero mantiene los datos antiguos disponibles hasta la confirmación final (hot swap).
  • "partialBatch": Cada partición se confirma individualmente tan pronto como termina. Si una falla, las ya confirmadas permanecen. Menos memoria, pero posible inconsistencia temporal entre particiones.

Scale-Out en AAS: Cero Tiempo de Inactividad durante el Procesamiento

El Scale-Out es una de las características más potentes de Azure Analysis Services en producción empresarial y una de las menos utilizadas. Con él, puede añadir hasta 7 réplicas de lectura al servidor. El procesamiento ocurre exclusivamente en la instancia de escritura, mientras que las réplicas de lectura continúan sirviendo consultas a los usuarios con los datos del último ciclo exitoso. El resultado: los usuarios nunca perciben que un procesamiento está ocurriendo.

Cómo Funciona el Scale-Out

El flujo de una actualización con Scale-Out activo tiene cuatro etapas:

  1. Usted envía el TMSL de refresh al endpoint de escritura (sufijo :rw). Las réplicas continúan respondiendo consultas con los datos anteriores, sin interrupción.
  2. El procesamiento ocurre aislado en la instancia de escritura. Toda la asignación de RAM, lectura de la fuente, codificación y recálculo ocurren solo en ella, sin competir con las réplicas de lectura.
  3. Después de que el procesamiento concluye, usted dispara la sincronización. El AS graba el modelo actualizado en el almacenamiento compartido (Azure Blob) y señala a las réplicas para recargar.
  4. Cada réplica realiza un hot-swap interno: carga el modelo nuevo en paralelo con el modelo antiguo y, cuando está listo, lo intercambia atómicamente. Durante este proceso (generalmente segundos a pocos minutos), las réplicas aún sirven los datos antiguos. Después del swap, pasan a usar los datos nuevos.

Configurando y Usando el Scale-Out

Para procesar sin interrumpir consultas, envíe el TMSL al endpoint de escritura (:rw) y sincronice justo después:

# Endpoint de lectura: el predeterminado, enruta automáticamente a las réplicas
$endpointLeitura = "asazure://brazilsouth.asazure.windows.net/meuservidor"

# Endpoint de escritura: solo para procesamiento, va a la instancia principal
$endpointEscrita = "asazure://brazilsouth.asazure.windows.net/meuservidor:rw"

# Paso 1: procesar en la instancia de escritura (las réplicas continúan sirviendo datos antiguos)
Invoke-ASCmd -Server $endpointEscrita -InputFile "refresh.tmsl"

# Paso 2: sincronizar las réplicas (cada réplica carga el modelo nuevo y hace hot-swap)
Invoke-ASCmd -Server $endpointEscrita -Query '{
  "synchronize": {
    "database": "MeuModeloAAS",
    "syncReadOnly": true,
    "syncOnlyCommittedSegments": true
  }
}'

Write-Host "Processamento e sincronizacao concluidos."

El parámetro syncOnlyCommittedSegments: true transfiere solo los segmentos que fueron efectivamente alterados en el procesamiento, en lugar de copiar el modelo entero. Para modelos con particionamiento granular donde solo algunas particiones fueron actualizadas, esto reduce mucho el tiempo de sincronización.

Cuándo Compensa el Scale-Out

  • SLA de disponibilidad 24/7: Si el modelo necesita estar disponible continuamente (usuarios en múltiples zonas horarias, paneles de operaciones en tiempo real), el Scale-Out elimina las ventanas de indisponibilidad.
  • Procesamientos largos (1–4+ horas): Para modelos grandes que tardan horas en procesar, el Scale-Out garantiza que toda esa ventana no cause indisponibilidad para los usuarios.
  • Alta concurrencia de consultas: Las réplicas distribuyen la carga. Una consulta pesada de un usuario no bloquea las respuestas para los demás.

Costo del Scale-Out

Cada réplica se cobra al mismo costo de la instancia principal. Un servidor S2 con 3 réplicas cuesta 4 veces el precio de un S2 simple. En muchos escenarios, una ventana corta de indisponibilidad aceptable (5–15 min con Hot Swap) en un único servidor es más económica que mantener réplicas activas 24/7. Evalúe el costo versus el SLA real del negocio antes de habilitar.

Optimizaciones en el Modelo Semántico con Tabular Editor y DAX Studio

Ahora que la fuente está optimizada y el particionamiento está bien definido, es hora de mirar dentro del modelo. Aquí entran las herramientas más poderosas del arsenal: Tabular Editor y DAX Studio. Ambas son gratuitas (con versiones de pago) e indispensables para cualquier profesional serio con Analysis Services o Power BI.

Conectando con Tabular Editor y DAX Studio

Ambas herramientas se conectan vía XMLA Endpoint:

  • Azure Analysis Services: El endpoint sigue el formato asazure://<región>.asazure.windows.net/<nombre-servidor>. Encuéntrelo en el Portal de Azure, en la hoja del servidor AAS en "Overview".
  • SSAS on-premises: Use el nombre del servidor Windows o la dirección IP.
  • Power BI Premium/Fabric: El endpoint XMLA es powerbi://api.powerbi.com/v1.0/myorg/<nombre-workspace>. Debe estar habilitado en la configuración del workspace (Configuración > Premium > Conexiones de conjunto de datos).
  • Power BI Desktop (local): Abra el modelo en PBI Desktop y conecte DAX Studio vía "Power BI / SSDT". Tabular Editor puede abrirse como Herramienta Externa directamente desde PBI Desktop.

VertiPaq Analyzer: Rayos X del Modelo

El VertiPaq Analyzer es el punto de partida para cualquier análisis de optimización de modelo tabular. Disponible en DAX Studio (menú Advanced > View Metrics) y como add-in en Tabular Editor, muestra:

  • Tamaño de cada tabla y columna en memoria.
  • Tasa de compresión por columna. Las columnas con baja compresión son candidatas a optimización de codificación.
  • Cardinalidad de cada columna. Alta cardinalidad = más memoria = más tiempo de procesamiento.
  • Número de segmentos por columna. Muchos segmentos pequeños = fragmentación = rendimiento deficiente.
  • Tamaño de relaciones y jerarquías.

Para generar el informe vía DAX Studio, utilice el script a continuación:

-- En DAX Studio, vaya a Advanced > View Metrics para el informe visual
-- Para extraer los datos vía DMV (Dynamic Management View):
SELECT
    DIMENSION_NAME        AS Tabela,
    ATTRIBUTE_NAME        AS Coluna,
    DICTIONARY_SIZE       AS TamanhoDicionario_Bytes,
    ATTRIBUTE_DATA_SIZE   AS TamanhoForwardIndex_Bytes,
    ATTRIBUTE_INDEX_SIZE  AS TamanhoInvertedIndex_Bytes,
    ATTRIBUTE_HIERARCHY_SIZE AS TamanhoHierarquia_Bytes,
    USERS_ATTRIBUTE_HIERARCHY_SIZE AS TamanhoHierPublica_Bytes
FROM $SYSTEM.DISCOVER_STORAGE_TABLE_COLUMNS
WHERE ATTRIBUTE_NAME <> 'RowNumber'
ORDER BY (DICTIONARY_SIZE + ATTRIBUTE_DATA_SIZE + ATTRIBUTE_INDEX_SIZE) DESC;

Resultado: Usted ve exactamente qué columnas están consumiendo más memoria y puede priorizar optimizaciones donde el impacto será mayor.

Best Practice Analyzer: Auditoría Automática del Modelo

El Best Practice Analyzer (BPA) de Tabular Editor es un análisis automatizado del modelo contra un conjunto de reglas de buenas prácticas. Piense en él como un linter para modelos tabulares: señala problemas de rendimiento, diseño y mantenibilidad con explicación y sugerencia de corrección.

Para ejecutar: en Tabular Editor 2 o 3, vaya a Tools > Best Practice Analyzer. Las principales categorías de reglas:

  • Rendimiento: Columnas calculadas que deberían ser medidas DAX, tablas grandes sin particionamiento, columnas con alta cardinalidad sin EncodingHint, relaciones bidireccionales sin necesidad, columnas de fecha/hora sin separación de componentes.
  • Modelo: Tablas de dimensión sin columna marcada como clave, relaciones con cardinalidad de muchos a muchos sin tabla puente, jerarquías con solo un nivel (inútiles).
  • Mantenibilidad: Medidas sin descripción, objetos visibles sin formato de número definido, uso de DIVIDE() en lugar de división directa (seguridad contra división por cero).

Microsoft y la comunidad mantienen un conjunto de reglas públicas en GitHub en TabularEditor/BestPracticeRules. Para importar en Tabular Editor:

// No Tabular Editor 2/3: Tools > Best Practice Analyzer > Manage Rules
// Cole a URL do arquivo JSON de regras da comunidade:
// https://raw.githubusercontent.com/TabularEditor/BestPracticeRules/master/BPARules.json
// O TE faz o download e aplica automaticamente

Puede crear reglas personalizadas para los estándares de su equipo (nomenclatura de medidas, prefijos obligatorios, límites de cardinalidad aceptables) y versionar las reglas junto con el modelo en Git. Ejecute el BPA antes de cada despliegue para garantizar que el modelo no ha retrocedido.

Análisis de Columnas y Tipos de Datos: Qué Cortar y Qué Cambiar

Después del VertiPaq Analyzer, la primera acción práctica es eliminar lo que no necesita estar en el modelo y corregir tipos de datos inadecuados.

Eliminando Columnas Innecesarias

Toda columna en el modelo tabular ocupa memoria RAM, consume tiempo de procesamiento (codificación + indexación) y aumenta el tamaño del archivo .abf (copia de seguridad). Si una columna no se utiliza en informes, medidas, filtros o relaciones, no debe estar en el modelo.

Utilice el siguiente script C# en Tabular Editor para identificar columnas que no tienen referencias en medidas o relaciones:

// Script C# en Tabular Editor 2 o 3 - Listar columnas sin uso
// Ejecutar en: Macros > New Macro > Pegar el código abajo

var colunasUsadasEmMedidas = new HashSet<string>();

foreach (var measure in Model.AllMeasures)
{
    foreach (var col in Model.AllColumns)
    {
        var refName = "[" + col.Name + "]";
        var refFull = "'" + col.Table.Name + "'[" + col.Name + "]";
        if (measure.Expression.Contains(refName) || measure.Expression.Contains(refFull))
            colunasUsadasEmMedidas.Add(col.DaxObjectFullName);
    }
}

var colunasEmRelacionamentos = new HashSet<string>();
foreach (var rel in Model.Relationships)
{
    colunasEmRelacionamentos.Add(rel.FromColumn.DaxObjectFullName);
    colunasEmRelacionamentos.Add(rel.ToColumn.DaxObjectFullName);
}

var sem_uso = Model.AllColumns
    .Where(c => c.Type == ColumnType.Data)
    .Where(c => !colunasUsadasEmMedidas.Contains(c.DaxObjectFullName))
    .Where(c => !colunasEmRelacionamentos.Contains(c.DaxObjectFullName))
    .Where(c => !c.IsHidden)  // columna visible (IsHidden = false)
    .Select(c => c.DaxObjectFullName)
    .OrderBy(x => x)
    .ToList();

Info("Colunas possivelmente sem uso:\n" + string.Join("\n", sem_uso));

Cuidado al eliminar columnas

IMPORTANTE: El script anterior es una heurística, no analiza código JavaScript de informes de Power BI o filtros de dashboards externos. Siempre valide con los desarrolladores de informes antes de eliminar cualquier columna en producción. Un enfoque seguro es primero ocultar la columna en el modelo (para que no aparezca en los campos de PBI) y monitorear durante 2–4 semanas antes de eliminarla definitivamente.

Optimizando Tipos de Datos

El tipo de dato impacta directamente la compresión y el uso de memoria de VertiPaq. Vea los tipos más comunes y sus implicaciones:

  • Int64 (Número entero): Mejor compresión con Value Encoding. Ideal para IDs, claves, cantidades.
  • Decimal (Número fijo): Internamente almacenado como entero multiplicado por un factor de escala. Buena compresión para valores monetarios con decimales fijos.
  • Double (Número flotante): Menor compresión que Decimal para valores monetarios. Use Decimal para precios y valores financieros.
  • DateTime: Almacenado como Double internamente en VertiPaq. Alta cardinalidad cuando incluye horario (cada timestamp único es un valor distinto). Prefiera almacenar solo la fecha (sin componente de hora) cuando sea posible, o use una clave entera en formato YYYYMMDD.
  • String: La menor compresión de todos. Evite columnas de string con alta cardinalidad (como URLs, GUIDs, hashes). Si necesita GUID para relaciones, convierta a Int64 en la ETL.
  • Boolean: Excelente compresión (solo 2 valores posibles en el diccionario). Prefiera Boolean a campos string "S/N" o "Sí/No".

Para identificar oportunidades de optimización de tipo de dato vía DMV, utilice el script a continuación:

-- Identificar columnas String con alta cardinalidad (candidatas a conversión a Int)
SELECT
    DIMENSION_NAME          AS Tabela,
    ATTRIBUTE_NAME          AS Coluna,
    DATATYPE                AS TipoDado,
    DICTIONARY_SIZE / 1024  AS DicionarioKB
FROM $SYSTEM.DISCOVER_STORAGE_TABLE_COLUMNS
WHERE DATATYPE = 'WChar'         -- Tipo string en VertiPaq
  AND DICTIONARY_SIZE > 1048576  -- Diccionario mayor que 1MB
  AND ATTRIBUTE_NAME <> 'RowNumber'
ORDER BY DICTIONARY_SIZE DESC;

Optimizando Relaciones: Cardinalidad, Dirección e Impacto

Las relaciones en el modelo tabular tienen un impacto tanto en el procesamiento (tiempo para construir los índices de relación) como en las consultas DAX. Entender las opciones es fundamental.

Cardinalidad de las Relaciones

  • Muchos-a-Uno (Many-to-One): El patrón estándar y más eficiente. La tabla de hechos (Many) se relaciona con la tabla de dimensión (One) por el lado de la clave primaria de la dimensión. El AS optimiza este patrón internamente.
  • Uno-a-Uno (One-to-One): Usado para tablas de extensión o cuando se divide una tabla grande en dos por cuestiones de rendimiento. Tiene un costo similar al M:1.
  • Muchos-a-Muchos (Many-to-Many): Evítelo cuando sea posible. Internamente, el AS implementa M:M usando filtros cruzados bidireccionales y materializaciones intermedias, que son costosas para consultas complejas. Si necesita M:M, prefiera usar una tabla puente (bridge/junction table) con relaciones M:1 explícitas en ambos lados.

Filtrado Bidireccional: Use con Precaución

El filtrado bidireccional (Cross Filter Direction = Both) permite que los filtros aplicados en una tabla se propaguen en ambas direcciones de la relación, en lugar de solo de la dimensión al hecho. Parece útil, pero tiene implicaciones serias:

  • Ambigüedad en modelos complejos: En modelos con múltiples rutas entre tablas, el filtrado bidireccional puede crear bucles de filtrado que devuelven resultados incorrectos o errores de "relación ambigua".
  • Impacto en el rendimiento de las consultas: DAX necesita resolver la propagación de filtros en ambas direcciones, lo que aumenta la complejidad del plan de consulta.
  • Alternativa recomendada: Use la función DAX CROSSFILTER() dentro de medidas específicas donde el comportamiento bidireccional es necesario, en lugar de habilitarlo globalmente en la relación. Así tendrá un control preciso sobre cuándo ocurre el filtro cruzado.
-- Medida DAX usando CROSSFILTER() para bidireccional solo cuando sea necesario
Clientes com Compras Categoria X =
CALCULATE(
    DISTINCTCOUNT(FatoVendas[ClienteKey]),
    CROSSFILTER(FatoVendas[ProdutoKey], DimProduto[ProdutoKey], Both),
    DimProduto[Categoria] = "Eletrônicos"
)

EncodingHint: Control Avanzado de Compresión de Columnas

El EncodingHint es una de las optimizaciones más poderosas y menos conocidas de VertiPaq. Sugiere al motor cómo codificar y almacenar los datos de una columna específica. Existe por columna y puede configurarse a través de Tabular Editor.

Los Dos Tipos de Encoding de VertiPaq

Internamente, VertiPaq utiliza dos algoritmos de codificación:

  • Value Encoding: El valor original (numérico) se almacena directamente en el índice directo, después de una transformación lineal simple (multiplicar por un factor y sumar un offset para que todos los valores quepan en el entero más pequeño posible). Es extremadamente eficiente para columnas numéricas con valores que crecen linealmente (IDs secuenciales, fechas como YYYYMMDD, cantidades, valores monetarios sin muchas casas decimales). El acceso es O(1), dado el ID de fila, se va directamente al valor sin consultar el diccionario.
  • Hash Encoding: Se crea un diccionario de valores únicos, y el índice directo almacena solo el índice en el diccionario (un entero pequeño). Es más eficiente para cadenas y para números con alta cardinalidad y distribución irregular. El acceso requiere una búsqueda en el diccionario, pero el diccionario está altamente comprimido.

Por defecto, AS intenta usar Value Encoding para números y Hash Encoding para cadenas. Sin embargo, para columnas numéricas con alta cardinalidad y valores irregulares (por ejemplo, valores decimales de precios con muchas casas diferentes), AS puede equivocarse en la elección y usar Hash cuando Value sería mejor, o viceversa. Esto empeora la compresión y el tiempo de procesamiento.

Cuándo Forzar Value vs. Hash Encoding

  • Forzar Value: Para claves numéricas enteras (IDs, claves subrogadas), fechas en formato YYYYMMDD, horas en formato HHMM. Columnas donde los valores son continuos o tienen una densidad alta.
  • Forzar Hash: Para cadenas con alta cardinalidad (nombres de productos, descripciones, correos electrónicos). Para números decimales con mucha variación en los valores (precios con descuento, coordenadas GPS).

Para configurar a través de script C# en Tabular Editor:

// Forzar Value Encoding en todas las columnas Int64 (claves enteras)
foreach (var col in Model.AllColumns.Where(c => c.DataType == DataType.Int64))
{
    col.EncodingHint = EncodingHint.Value;
}

// Forzar Hash Encoding en todas las columnas String con cardinalidad > 10.000
// (requiere VertiPaq Analyzer para obtener cardinalidad, aquí usamos heurística por el nombre)
foreach (var col in Model.AllColumns.Where(c => c.DataType == DataType.String))
{
    col.EncodingHint = EncodingHint.Hash;
}

// Guardar los cambios
Model.SaveChanges();
Info("EncodingHint configurado com sucesso em " + Model.AllColumns.Count() + " colunas.");

Resultado: En modelos con muchas claves enteras siendo codificadas como Hash (lo que ocurre cuando AS se equivoca en la heurística durante el primer procesamiento y persiste la codificación incorrecta), forzar Value Encoding puede reducir el tamaño del modelo en un 30–50% y acelerar el procesamiento y las consultas significativamente. ¡¡Wow!!

El EncodingHint es una sugerencia, no una obligación. AS puede ignorarla si determina que el otro tipo es más eficiente para esa columna específica con los datos reales. Por eso se llama "Hint" (pista). En la práctica, AS respeta la pista en la gran mayoría de los casos.

Columnas Calculadas vs. Medidas DAX: Impacto en el Procesamiento

Esta distinción tiene un impacto directo en el tiempo de procesamiento y en el consumo de memoria:

  • Columnas Calculadas: Se calculan en tiempo de procesamiento y se almacenan físicamente en el modelo (en memoria), como cualquier otra columna importada. Aumentan el tamaño del modelo, aumentan el tiempo de procesamiento (Process Recalc las recalcula todas) y pueden usar mucha memoria si son complejas o de alta cardinalidad. Úselas cuando necesite filtrar, agrupar o crear relaciones basadas en el resultado del cálculo.
  • Medidas: Se calculan en tiempo de consulta, en el momento en que se genera el informe. No almacenan datos. No impactan el procesamiento. Use medidas para cualquier cálculo que no necesite ser usado como clave de filtro o relación.

ATENCIÓN: Un error común es usar columnas calculadas para cálculos que podrían ser medidas. Por ejemplo, crear una columna calculada Margen% que calcula [VlrVenda] - [CustoVenda] para cada fila. Esto calcula y almacena el valor para cada una de los millones de filas de la tabla de hechos, cuando una medida Margen% = [Total Ventas] - [Total Costo] haría lo mismo de forma mucho más eficiente, calculando solo para los datos del contexto actual del informe.

Gestionando Segmentos: La Fragmentación de VertiPaq

Internamente, VertiPaq divide cada columna en bloques de datos llamados segmentos. El tamaño ideal de un segmento es de 8 millones de filas. Cuando se realizan muchas operaciones de Process Add incrementales a lo largo del tiempo, VertiPaq crea muchos segmentos pequeños (uno para cada Process Add), lo que degrada el rendimiento de escaneo y compresión.

Para visualizar la fragmentación de segmentos:

-- Verificar fragmentación de segmentos por columna
SELECT
    DIMENSION_NAME   AS Tabela,
    PARTITION_NAME   AS Particao,
    ATTRIBUTE_NAME   AS Coluna,
    COUNT(*)         AS NumSegmentos,
    SUM(RECORDS_COUNT) AS TotalLinhas,
    AVG(RECORDS_COUNT) AS MediaLinhasPorSegmento,
    MIN(RECORDS_COUNT) AS MenorSegmento,
    MAX(RECORDS_COUNT) AS MaiorSegmento
FROM $SYSTEM.DISCOVER_STORAGE_TABLE_COLUMN_SEGMENTS
WHERE ATTRIBUTE_NAME <> 'RowNumber'
GROUP BY DIMENSION_NAME, PARTITION_NAME, ATTRIBUTE_NAME
HAVING COUNT(*) > 1
ORDER BY NumSegmentos DESC, DIMENSION_NAME, ATTRIBUTE_NAME;

Las columnas con muchos segmentos pequeños deben desfragmentarse con Process Defrag en la tabla o partición correspondiente. Un Process Full en la partición también resuelve la fragmentación, reconstruyendo todos los segmentos desde cero.

Tablas de Agregación: La Técnica Más Poderosa para Modelos con Miles de Millones de Filas

Las tablas de agregación (Aggregations) son la funcionalidad más poderosa, más ignorada y peor documentada de Analysis Services moderno. Si trabaja con modelos que tienen tablas de hechos de miles de millones de filas y no conoce este recurso, este puede ser el fragmento más valioso de todo este artículo.

La idea central: usted crea una tabla pre-agregada con pocos millones de filas (totales por día, por producto, por región), la importa en el modelo y configura el mapeo de correspondencia con la tabla de detalle. A partir de ahí, AS enruta automáticamente cada consulta DAX a la tabla de agregación cuando la granularidad lo permite, sin que usted cambie una línea de código DAX o un informe de Power BI. Para consultas que necesitan granularidad de transacción, AS recurre a la tabla de detalle como respaldo.

Cómo AS Usa las Agregaciones Automáticamente

El enrutamiento se realiza en tiempo de consulta por el Storage Engine de VertiPaq. AS evalúa si la consulta puede ser respondida con la tabla de agregación verificando:

  • ¿Todos los campos GROUP BY de la consulta están mapeados en la tabla de agregación?
  • ¿Todos los campos de medida (SUM, COUNT, MIN, MAX) tienen columnas precalculadas correspondientes en la agregación?
  • ¿No hay filtros en campos que no existen en la agregación (lo que requeriría detalles)?

Si todas las condiciones se cumplen, AS usa la agregación (decenas de millones de veces más rápido para modelos grandes). Si una condición falla, automáticamente recurre a la tabla de detalle. Este comportamiento es completamente transparente para quien escribió las medidas DAX y para quien usa los informes.

Creando una Tabla de Agregación en la Fuente

El primer paso es crear la tabla precalculada en la fuente. Debe contener las dimensiones por el nivel de granularidad deseado y las métricas pre-sumadas:

-- Tabla de agregación: totales diarios por producto, cliente y región
-- FatoVendas tiene 5 mil millones de líneas; FatoVendasAgr tendrá ~10 millones
CREATE TABLE dbo.FatoVendasAgr
WITH (
    DISTRIBUTION = HASH(DataVendaKey),
    CLUSTERED COLUMNSTORE INDEX
)
AS
SELECT
    fv.DataVendaKey,
    fv.ProdutoKey,
    fv.ClienteKey,
    dc.RegiaoKey,
    SUM(fv.VlrVenda)    AS SomaVlrVenda,
    SUM(fv.QtdVenda)    AS SomaQtdVenda,
    SUM(fv.CustoVenda)  AS SomaCusto,
    COUNT_BIG(*)        AS ContaLinhas
FROM dbo.FatoVendas fv
INNER JOIN dbo.DimCliente dc ON fv.ClienteKey = dc.ClienteKey
GROUP BY fv.DataVendaKey, fv.ProdutoKey, fv.ClienteKey, dc.RegiaoKey;

-- Actualizar la tabla de agregación después de cada carga del ETL:
-- Truncate + reinsert o Merge incremental, dependiendo del volumen
TRUNCATE TABLE dbo.FatoVendasAgr;
INSERT INTO dbo.FatoVendasAgr
SELECT ... (mesma query acima)

Configurando el Mapeo de Agregación vía TMSL

Después de importar FatoVendasAgr como tabla normal en el modelo, configure la propiedad alternateOf en cada columna para indicar qué columna de la tabla de detalle representa y qué función de resumen aplica:

// Trecho do model.bim configurando a tabela de agregacao
// A tabela deve ser marcada como Hidden para nao aparecer no Power BI
{
  "name": "FatoVendasAgr",
  "isHidden": true,
  "columns": [
    {
      "name": "DataVendaKey",
      "dataType": "int64",
      "alternateOf": {
        "summarization": "groupBy",
        "table": "FatoVendas",
        "column": "DataVendaKey"
      }
    },
    {
      "name": "ProdutoKey",
      "dataType": "int64",
      "alternateOf": {
        "summarization": "groupBy",
        "table": "FatoVendas",
        "column": "ProdutoKey"
      }
    },
    {
      "name": "SomaVlrVenda",
      "dataType": "decimal",
      "alternateOf": {
        "summarization": "sum",
        "table": "FatoVendas",
        "column": "VlrVenda"
      }
    },
    {
      "name": "ContaLinhas",
      "dataType": "int64",
      "alternateOf": {
        "summarization": "countRows",
        "table": "FatoVendas"
      }
    }
  ]
}

En Tabular Editor 3, la interfaz gráfica en la pestaña Aggregations de cada tabla permite configurar los mapeos sin editar TMSL manualmente. Tabular Editor 2 (código abierto) exige la edición vía scripts C# o directamente en el model.bim.

El Patrón Más Poderoso: Composite Model (DirectQuery + Import)

Agregar una tabla de Import con otra de Import ya ayuda, pero la verdadera ganancia ocurre con el Composite Model:

  • Tabla de detalle (FatoVendas): Modo DirectQuery. Cero consumo de RAM en el modelo. Siempre actualizada en tiempo real. Respondida solo cuando la consulta necesita granularidad de transacción.
  • Tabla de agregación (FatoVendasAgr): Modo Import. Suficientemente pequeña para caber en memoria (10 millones de filas en lugar de 5 mil millones). Reprocesada en minutos. Responde a la gran mayoría de las consultas analíticas.

Con esta configuración, se elimina completamente el procesamiento de miles de millones de filas de detalle. El procesamiento pasa a ser solo de la agregación y de las dimensiones, que son mucho menores. El usuario recibe rendimiento de Import para más del 90% de las consultas y datos frescos vía DirectQuery cuando es necesario, todo automáticamente.

Verificando si la Agregación Está Siendo Usada

En DAX Studio, con Server Timings habilitado, ejecute una consulta y observe:

  • Columna "Direct Query": Muestra 0 ms para consultas respondidas íntegramente por la agregación en Import. Si aparece tiempo de DQ, la consulta no fue resuelta por el agregado.
  • Query Plan: Busque el nodo AggregationHit (usó el agregado, respuesta rápida) versus DirectQueryHit (fue a la fuente, más lento).

Si una consulta que debería usar la agregación está yendo a DirectQuery, los motivos más comunes son: la consulta filtra por una columna no mapeada en la agregación, la granularidad de la consulta es más fina que la de la agregación, o el mapeo alternateOf es incorrecto.

Puede tener múltiples tablas de agregación en diferentes capas de granularidad. Por ejemplo: FatoVendasAgrDia (totales diarios), FatoVendasAgrMes (totales mensuales) y FatoVendasAgrAno (totales anuales). AS usa la más granular que aún satisface la consulta. Las consultas de drill-down van descendiendo gradualmente por las capas hasta necesitar el detalle.

Índices Columnstore en el Contexto Tabular: VertiPaq es un Columnstore

Cuando hablamos de optimizar la fuente de datos con Clustered Columnstore Indexes (CCI) en SQL Server, muchas personas se preguntan: "¿Pero el propio VertiPaq también es un columnstore? ¿Por qué necesitaría CCI en el origen?"

La respuesta es sí, y entender cómo funciona VertiPaq internamente explica por qué el CCI en el origen aún ayuda, y cómo las dos tecnologías se complementan.

Cómo VertiPaq Almacena Datos Internamente

VertiPaq es esencialmente un columnstore en memoria. Cada columna se comprime y almacena de forma independiente de las otras. Al consultar solo 3 columnas de una tabla de 100 columnas, VertiPaq lee físicamente solo las 3 columnas necesarias, no escanea todas las 100.

Internamente, cada columna usa dos tipos de índice:

  • Forward Index (Índice Directo): Mapea ID de Fila → Valor de la columna. Permite que, dado un ID de Fila, se obtenga el valor de esa columna. Usado en operaciones que necesitan materializar valores (SUMX, ADDCOLUMNS, etc.).
  • Inverted Index (Índice Invertido): Mapea Valor → Lista de IDs de Fila (bitmap). Permite que, dado un valor (o intervalo), se obtengan todos los IDs de Fila que tienen ese valor. Usado en operaciones de filtro y GROUP BY (CALCULATE, FILTER, SUMMARIZE, etc.).

El Inverted Index usa Bitmap Filter Pushdown, cuando se combinan múltiples filtros (por ejemplo, Año=2024 AND Categoría="Electrónicos"), VertiPaq combina los bitmaps de los dos filtros con una operación AND bit a bit, obteniendo el conjunto de IDs de Fila que satisfacen ambos criterios sin escanear los datos fila por fila. Esto es extremadamente eficiente y es el motivo por el cual los modelos tabulares son tan rápidos para filtros multidimensionales.

Por qué el CCI en el Origen aún Importa

VertiPaq almacena los datos en memoria. Para llenarlos, necesita leer los datos de la fuente durante el procesamiento. Es en esa lectura donde el CCI en el origen ayuda:

  • Lectura más rápida de la fuente: SQL Server con CCI puede ejecutar la consulta de lectura usando Batch Mode, paralelismo total y pushdown de filtros al columnstore. Los datos llegan más rápido para que AS los procese.
  • Compresión on-the-wire: SQL Server puede transferir los datos ya en formato comprimido de columnstore a AS, reduciendo el tráfico de red.
  • Menor I/O en SQL Server: El CCI se almacena comprimido en disco. La lectura de CCI para alimentar AS genera mucho menos I/O en SQL Server que la lectura de un heap o índice rowstore equivalente.

Alineación de Particiones VertiPaq con Segmentos Columnstore

Existe un sutil punto de alineación entre las particiones del modelo tabular y los Row Groups del CCI. Lo ideal es que la consulta de una partición en AS lea Row Groups completos del CCI, esto activa la optimización de Row Group Elimination en SQL Server y garantiza que los datos lleguen en el mejor formato posible para que VertiPaq construya sus segmentos con tamaño óptimo.

Para ello, diseñe sus consultas de partición en AS para alinearse con los criterios de partición del CCI en el origen. Si el CCI usa una partición por año/mes, la consulta de cada partición de AS debe filtrar el mismo rango de fecha.

Consejos Extras para Alto Rendimiento en Producción

DMVs para Monitoreo de Procesamiento en Tiempo Real

Durante las operaciones de procesamiento, especialmente en producciones críticas, es importante monitorear el progreso. AS expone DMVs que permiten esto:

-- Sesiones activas en el servidor
SELECT SESSION_ID, SESSION_CURRENT_DATABASE, SESSION_ELAPSED_TIME_MS,
       SESSION_CPU_TIME_MS, SESSION_USER_NAME, SESSION_STATUS
FROM $SYSTEM.DISCOVER_SESSIONS
WHERE SESSION_CURRENT_DATABASE <> ''
ORDER BY SESSION_ELAPSED_TIME_MS DESC;

-- Trabajos de procesamiento en curso
SELECT JOB_ID, JOB_DESCRIPTION, JOB_THREADPOOL_ID, JOB_READS, JOB_WRITES,
       JOB_CPU_TIME_MS, JOB_START_TIME
FROM $SYSTEM.DISCOVER_JOBS;

-- Actividad de objetos (procesamiento por tabla/partición)
SELECT SPID, START_TIME, OBJECT_TYPE, OBJECT_PATH, OBJECT_ID,
       CURRENT_ACTION, CURRENT_ACTION_STEP, CURRENT_ACTION_STEP_DESCRIPTION
FROM $SYSTEM.DISCOVER_OBJECT_ACTIVITY
ORDER BY START_TIME DESC;

Administración de Memoria: Configuración de AS

Analysis Services expone un conjunto de parámetros de memoria que controlan cómo el motor gestiona VertiPaq, la caché de consultas y la presión de memoria. En SSAS on-premises, estos parámetros se encuentran en el archivo msmdsrv.ini y se pueden ajustar a través de SSMS (clic derecho en el servidor > Propiedades > General > Mostrar Propiedades Avanzadas). En AAS, algunos de estos valores son gestionados por la plataforma y no se pueden cambiar, como detalla el artículo de SQLBI Optimizing memory settings in Analysis Services (Marco Russo). Entender cada parámetro es esencial para diagnosticar errores de memoria y ajustar el comportamiento del servidor:

LowMemoryLimit (predeterminado: 65%)

Porcentaje de memoria a partir del cual AS comienza a liberar cachés proactivamente. Por debajo de este límite, AS mantiene todos los datos en caché sin liberar nada. A medida que el uso supera el 65%, el limpiador se vuelve progresivamente más agresivo hasta alcanzar el TotalMemoryLimit, momento en el que descarga todo lo que no está en uso activo.

  • El valor predeterminado (65%) es adecuado para servidores dedicados. En servidores con más de 100 GB de RAM dedicados exclusivamente a AS, considere aumentarlo a 70–75% para reducir las expulsiones prematuras de caché.
  • Reduzca si otros servicios compiten con AS por la memoria del mismo servidor (múltiples instancias de SSAS, SQL Server en el mismo host).

TotalMemoryLimit (predeterminado: 80%)

Cuando el uso supera este porcentaje, el administrador de memoria descarga todos los datos de caché que no están en uso activo. En AAS, este valor depende del plan de servicio y no se puede cambiar. En SSAS on-premises dedicado con mucha RAM (>100 GB), se puede elevar a 90–95%.

HardMemoryLimit (predeterminado: 0 = automático)

Límite máximo absoluto de memoria. Si AS supera este valor, el sistema cancela sesiones activamente para reducir el consumo, y los usuarios reciben un error de presión de memoria. El valor 0 hace que AS calcule automáticamente un valor intermedio entre el TotalMemoryLimit y la memoria física total. En AAS, este valor no se puede cambiar, ya que el servidor utiliza toda la memoria física disponible del plan. Para SSAS dedicado, definir 99 o 100 es adecuado.

VertiPaqPagingPolicy (predeterminado: 0 en AAS, 1 en SSAS)

Controla si VertiPaq puede paginar a disco cuando la memoria es insuficiente:

  • 0 (paginación deshabilitada, predeterminado en AAS): Si no hay suficiente memoria para el procesamiento, falla con un error de falta de memoria. El conjunto de trabajo del proceso está limitado a VertiPaqMemoryLimit.
  • 1 (paginación habilitada, predeterminado en SSAS): VertiPaq puede usar más memoria de la disponible físicamente, paginando al archivo de paginación del SO. El procesamiento concluye, pero con una degradación severa del rendimiento si ocurre la paginación.

Atención al procesamiento en memoria limitada

Importante: Durante el procesamiento, VertiPaq puede necesitar hasta el doble del tamaño de la base de datos en memoria (datos nuevos + datos antiguos simultáneamente). Si el plan de AAS no tiene suficiente memoria para esto, hay dos alternativas: habilitar VertiPaqPagingPolicy = 1 para permitir la paginación (el procesamiento concluye, pero más lento), o hacer una copia de seguridad de la base de datos, limpiarla con Process Clear y reprocesar desde cero (la base de datos no estará disponible durante el procesamiento).

QueryMemoryLimit

Limita la memoria utilizada durante la ejecución de consultas. En AAS, este límite se aplica a toda la memoria utilizada por las consultas DAX y MDX, no solo a las materializaciones intermedias como en SSAS 2019. El valor 0 desactiva el límite.

  • Reduzca este límite si los usuarios están recibiendo errores de memoria durante las consultas: esto hace que las consultas pesadas fallen antes, protegiendo las demás sesiones que tienen menor demanda de memoria.

VertiPaqMemoryLimit

Define la cantidad de memoria que VertiPaq puede usar. El comportamiento varía según VertiPaqPagingPolicy:

  • Con paginación deshabilitada (0): define el total de memoria que VertiPaq puede bloquear en el conjunto de trabajo.
  • Con paginación habilitada (1): define el límite de memoria física que VertiPaq fija en memoria; por encima de eso, el resto puede ser paginado.
  • Si otros servicios compiten con AS por la memoria, definir VertiPaqMemoryLimit por debajo de LowMemoryLimit reduce la presión de VertiPaq en el servidor.

HeapTypeForObjects y MemoryHeapType

No altere estos valores del predeterminado (-1). Las versiones antiguas de SSAS (2012/2014) tenían problemas de fragmentación y fuga de memoria que requerían un ajuste manual de estos parámetros. En SSAS 2016 SP1+ y en AAS, el motor elige automáticamente el asignador óptimo a través del valor -1. Si actualizó una instancia antigua y estos valores son diferentes de -1, corríjalos a -1.

Automatizando el Procesamiento a través de Azure Data Factory

Azure Data Factory (ADF) es la solución de orquestación de Microsoft para pipelines de datos a gran escala, y funciona muy bien para automatizar el ciclo completo: cargar datos en la fuente → procesar el modelo AAS. Pero es importante saber: ADF no tiene una actividad nativa específica para Azure Analysis Services. La integración se realiza a través de Web Activity llamando a la REST API de AAS, o a través de Azure Function Activity ejecutando código PowerShell/C# con el módulo SqlServer.

Patrón REST API: Web Activity + Bucle de Estado

AAS expone una REST API asíncrona para disparar actualizaciones. El flujo en ADF es:

  1. Web Activity (POST) → dispara la actualización y recibe un ID de operación.
  2. Until Activity (bucle) → se repite hasta que el estado sea succeeded o failed.
  3. Web Activity (GET) dentro de Until → consulta el estado de la operación por el ID.
  4. If Condition Activity → verifica el resultado y, en caso de fallo, dispara una notificación (Logic App, correo electrónico).

La URL del endpoint de actualización es:

# POST para disparar el refresh
https://<region>.asazure.windows.net/servers/<servername>/models/<database>/refreshes

# Ejemplo de body (Process Full vía REST)
{
    "Type": "Full",
    "CommitMode": "transactional",
    "MaxParallelism": 4,
    "RetryCount": 2,
    "Objects": []
}

# GET para verificar el status (usa el operationId retornado por el POST)
https://<region>.asazure.windows.net/servers/<servername>/models/<database>/refreshes/<operationId>

La autenticación de la Web Activity debe usar Managed Identity de ADF (otorgue a esta identidad el rol de Analysis Services Administrator en el servidor AAS) o un Service Principal. Configure en la Web Activity: Authentication = MSI, Resource = https://*.asazure.windows.net.

Patrón Azure Function: Procesamiento Síncrono

Una alternativa más simple es usar una Azure Function Activity que ejecuta PowerShell con el módulo SqlServer. La función se bloquea hasta que el procesamiento termina, devolviendo éxito o fallo directamente, sin necesidad de un bucle de sondeo:

# Azure Function (PowerShell) llamada por el ADF vía Azure Function Activity
param($Request, $TriggerMetadata)

# Autentica con Managed Identity de la Function App
# Requiere Connect-AzAccount -Identity en el profile.ps1 de la Function App,
# o llamado explícitamente aquí:
Connect-AzAccount -Identity | Out-Null
$token = (Get-AzAccessToken -ResourceUrl "https://*.asazure.windows.net").Token

$tmsl = @"
{
  "refresh": {
    "type": "full",
    "objects": [{ "database": "$($Request.Body.database)" }]
  }
}
"@

# Invoke-ASCmd bloquea hasta que el procesamiento concluya (síncrono)
$result = Invoke-ASCmd `
    -Server "asazure://brazilsouth.asazure.windows.net/meuservidor" `
    -AccessToken $token `
    -Query $tmsl

if ($result -match 'error') {
    Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
        StatusCode = 500; Body = $result
    })
} else {
    Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
        StatusCode = 200; Body = 'Processamento concluido com sucesso'
    })
}

Estructura del Pipeline en ADF

Un pipeline típico de producción integrado con AAS en ADF tiene la siguiente estructura:

  • Actividad 1: Copy Data, extrae datos de la fuente (on-premises, S3, APIs) y los carga en la capa de staging (Azure SQL o Synapse).
  • Actividad 2: Stored Procedure / Script, ejecuta la lógica de transformación y prepara las tablas finales.
  • Actividad 3: Azure Function Activity (o Web Activity + Until Loop), dispara la actualización del modelo AAS y espera la conclusión.
  • Actividad 4: Web Activity (condicional), notifica al equipo a través de Logic App, Teams o correo electrónico en caso de fallo.

Synapse Pipelines

Azure Synapse Analytics tiene su propio orquestador (Synapse Pipelines), que se basa en el mismo motor que ADF. El mismo patrón de Web Activity + REST API de AAS se aplica íntegramente. Si ya utiliza Synapse como plataforma de datos, es natural mantener la orquestación dentro de Synapse Pipelines en lugar de añadir una dependencia en ADF.

Scale Up/Down Automático vía REST API: El Truco de la "Ventana de Procesamiento"

Azure Analysis Services le permite cambiar el SKU (tier) a través de la REST API. Esto abre una poderosa estrategia para reducir costos:

  1. Durante el día (horario comercial): Mantener el servidor en un tier menor (ej: S1 o S2) suficiente para las consultas de los usuarios.
  2. Ventana de procesamiento (2h/día): Antes del procesamiento, llamar a la API para escalar a un tier mayor (ej: S4 o S8 con más RAM y CPUs), procesar los datos, y justo después escalar de nuevo al tier menor.

Dependiendo del tiempo de procesamiento y los volúmenes de datos, el costo del scale-up temporal puede ser mucho menor que mantener el tier alto 24/7. El patrón recomendado es encapsular el scale-up y el scale-down en Azure Functions que llaman a la REST API de administración de Azure, activadas como Azure Function Activities en ADF: una antes del procesamiento (scale-up) y otra en el bloque de finalización del pipeline (scale-down), para garantizar que el servidor vuelva al tier menor incluso en caso de fallo. La Function App necesita tener Managed Identity habilitada y recibir el rol Contributor (o un rol personalizado con permiso Microsoft.AnalysisServices/servers/write) en el servidor AAS a través de Azure RBAC:

# Azure Function (PowerShell) — Scale Up o Scale Down vía REST API de administración
# Body esperado: { "sku": "S4" } para scale-up | { "sku": "S1" } para scale-down
# Managed Identity de la Function App necesita tener el rol Contributor en el servidor AAS.
param($Request, $TriggerMetadata)

$subscriptionId = $env:AAS_SUBSCRIPTION_ID
$resourceGroup  = $env:AAS_RESOURCE_GROUP
$serverName     = $env:AAS_SERVER_NAME
$skuTarget      = $Request.Body.sku   # ej: "S4" o "S1"
$apiVersion     = "2017-08-01"
$serverUri      = "https://management.azure.com/subscriptions/$subscriptionId" +
                  "/resourceGroups/$resourceGroup" +
                  "/providers/Microsoft.AnalysisServices/servers/$serverName"

# Token vía IMDS (Managed Identity de la propia Function App)
$imdsUri   = "http://169.254.169.254/metadata/identity/oauth2/token" +
             "?api-version=2018-02-01&resource=https://management.azure.com/"
$tokenResp = Invoke-RestMethod -Method Get -Uri $imdsUri -Headers @{ Metadata = "true" }
$authHeader = @{ Authorization = "Bearer $($tokenResp.access_token)" }

# PATCH para cambiar el SKU (operación asíncrona — Azure provisiona el nuevo tier)
$body = @{ sku = @{ name = $skuTarget; tier = "Standard" } } | ConvertTo-Json -Depth 3
Invoke-RestMethod -Method Patch -Uri "$serverUri`?api-version=$apiVersion" `
    -Headers $authHeader -Body $body -ContentType "application/json"

# Sondear hasta que provisioningState llegue a Succeeded, Failed o Canceled
do {
    Start-Sleep -Seconds 20
    $info  = Invoke-RestMethod -Method Get -Uri "$serverUri`?api-version=$apiVersion" -Headers $authHeader
    $state = $info.properties.provisioningState
    Write-Host "provisioningState: $state"
} while ($state -notin @("Succeeded", "Failed", "Canceled"))

$ok = ($state -eq "Succeeded")
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = if ($ok) { 200 } else { 500 }
    Body       = if ($ok) { "Scale para $skuTarget concluido." } else { "Scale para $skuTarget falhou: $state" }
})

En ADF, el scale-down debe estar en el bloque Finally del flujo de control (o conectado a las ramas de éxito y fallo del procesamiento), garantizando que el servidor se reduzca al tier menor independientemente del resultado.

Modo DirectQuery: Cuándo Usar y Cuándo Evitar

El modo DirectQuery en AAS (y Power BI) no almacena datos en memoria, todas las consultas se traducen a consultas SQL y se envían directamente a la fuente de datos. Elimina el procesamiento, pero tiene serias desventajas:

  • Pros: Datos siempre actuales (sin latencia de procesamiento), sin consumo de RAM del modelo tabular, adecuado para tablas con miles de millones de filas que no caben en memoria.
  • Contras: El rendimiento de la consulta depende al 100% de la fuente de datos. Las medidas DAX complejas son difíciles o imposibles de optimizar. Algunas características DAX no funcionan en DirectQuery. Cada carga de página de informe genera múltiples consultas en la base de datos.
  • Recomendación: Use DirectQuery solo para tablas de hechos muy grandes que cambian continuamente. Combine con el modo Import para tablas de dimensión (modo Compuesto, Composite Model). Nunca use DirectQuery puro en toda la solución sin una evaluación comparativa exhaustiva.

XMLA Endpoint y Power BI Premium: Buenas Prácticas

Para Power BI Premium y Fabric, el XMLA Endpoint abre acceso a todas las capacidades empresariales que antes eran exclusivas de SSAS y AAS:

  • Actualización Incremental con XMLA: Power BI tiene una característica nativa de Actualización Incremental, pero tiene limitaciones. Para un control total, use el XMLA Endpoint con scripts TMSL directamente, puede crear y administrar particiones exactamente como en AAS.
  • Pipelines de Implementación vía XMLA: Automatice la implementación de modelos entre workspaces (Desarrollo → Homologación → Producción) vía scripts TMSL con XMLA, integrando con su pipeline CI/CD en Azure DevOps o GitHub Actions.
  • Copia de Seguridad/Restauración: Realice copias de seguridad de los modelos vía XMLA a archivos .abf en Azure Storage, como contingencia para revertir cambios problemáticos.
  • Herramientas de Terceros: Tabular Editor, DAX Studio y VertiPaq Analyzer funcionan de forma nativa con el XMLA Endpoint de Power BI Premium, los mismos flujos de optimización que utiliza en AAS se aplican directamente.

Copia de Seguridad y Restauración: Protección y Reversión en Minutos

AS admite la copia de seguridad del modelo en un archivo .abf (Analysis Services Backup File), que incluye todos los datos en memoria, metadatos y particiones. La restauración de un .abf es, en muchos casos, mucho más rápida que reprocesar el modelo desde cero, especialmente para modelos con decenas o cientos de gigabytes.

Para configurar la copia de seguridad automática en AAS, acceda al portal de Azure, en la hoja del servidor AAS, vaya a Configuración > Copia de seguridad y apunte a una cuenta de Azure Blob Storage. Con el almacenamiento configurado, puede activar las copias de seguridad a través de TMSL:

// Fazer backup do modelo para o Azure Blob configurado
{
  "backup": {
    "database": "MeuModeloAAS",
    "file": "MeuModeloAAS_20240115.abf",
    "allowOverwrite": true,
    "applyCompression": true
  }
}
// Restaurar o modelo a partir do backup
{
  "restore": {
    "database": "MeuModeloAAS",
    "file": "MeuModeloAAS_20240115.abf",
    "allowOverwrite": true,
    "readWriteMode": "readWrite"
  }
}

Escenarios donde la restauración supera el reprocesamiento:

  • Reversión de implementación: Antes de implementar cambios estructurales en el modelo (adición de columnas, cambio de tipos de datos, alteración de relaciones), realice una copia de seguridad. Si algo sale mal, restaure en minutos en lugar de esperar horas de reprocesamiento.
  • Inicialización de un nuevo servidor: Al crear un servidor AAS de homologación o de DR, restaure la copia de seguridad de producción en lugar de reprocesar todo desde la fuente. La restauración a través de Azure Blob es mucho más rápida para modelos de 50 GB+ que un Process Full completo.
  • Recuperación de fallos de procesamiento: Si un procesamiento corrompió el modelo o dejó particiones en estado inconsistente, restaure la última copia de seguridad buena y reprocese solo las particiones afectadas.

Autenticación Segura para Fuentes de Datos: Cuenta de Servicio y OAuth

AAS no soporta Managed Identity

A diferencia de otros servicios de Azure, Azure Analysis Services no soporta Managed Identity — ya sea para fuentes de datos, copias de seguridad o cualquier otra operación. Esto está declarado explícitamente en la documentación oficial de service principals de AAS: "Analysis Services no soporta operaciones realizadas por identidades administradas". El Service Principal es la alternativa para la automatización sin credenciales de usuario.

Para entornos de producción, nunca use credenciales personales o de administrador en la fuente de datos del modelo AS. Las alternativas soportadas y documentadas son:

  • Cuenta de Servicio SQL (autenticación SQL): Inicio de sesión SQL dedicado con permiso de lectura solamente (db_datareader). Es la opción más simple, funciona en cualquier nivel de compatibilidad y está explícitamente documentada por Microsoft para fuentes de datos en la nube. La documentación oficial de suplantación de identidad de AAS especifica: para fuentes de datos en la nube con autenticación SQL, la suplantación de identidad debe ser Cuenta de Servicio.
  • OAuth / Entra ID (Tabular 1400+, en memoria): Para Azure SQL, Azure Synapse, Dynamics 365 y SharePoint List en modelos 1400+ en memoria, AAS soporta credenciales OAuth gestionadas por el propio servicio (incluida la renovación automática de tokens para evitar el tiempo de espera en actualizaciones largas). La configuración se realiza a través de Power Query en Visual Studio o SSMS → Editar Credenciales, utilizando una cuenta con acceso a la fuente. Para la automatización con Service Principal, consulte la documentación específica de credenciales OAuth de AAS, ya que el flujo de configuración no es un bloque simple de TMSL.

Configuración de la Cuenta de Servicio SQL

Cree un inicio de sesión SQL dedicado en Azure SQL con un permiso mínimo de lectura. En el modelo AAS, configure la fuente de datos con este inicio de sesión y defina la suplantación de identidad como Cuenta de Servicio:

-- En Azure SQL: crear login de servicio dedicado con permiso mínimo
CREATE LOGIN [aas_svc] WITH PASSWORD = 'senha-forte-aqui';

-- En la base de datos de destino:
CREATE USER [aas_svc] FOR LOGIN [aas_svc];
ALTER ROLE db_datareader ADD MEMBER [aas_svc];

-- Verificar
SELECT name, type_desc, authentication_type_desc
FROM sys.database_principals
WHERE name = 'aas_svc';

En SSMS, conectado al servidor AAS: clic derecho en la base de datos → ProcesarEditar Configuración → pestaña Credenciales de Fuente de Datos → edite la fuente de datos e introduzca el inicio de sesión y la contraseña de la cuenta de servicio. AAS almacena las credenciales cifradas en el servidor, sin exponerlas en scripts o repositorios.

Service Principal para la Automatización de Operaciones en AAS

El uso documentado de Service Principal en AAS es para automatizar operaciones en el propio servidor (actualización, escalado, implementación de modelos) a través de PowerShell, Azure Automation y otros orquestadores, no como credencial de fuente de datos. El SP debe agregarse al rol de administrador del servidor o a un rol de base de datos con permiso de Proceso:

# Autenticar con Service Principal para ejecutar Invoke-ASCmd (PS7 o PS5.1)
$appId    = $env:SP_APP_ID
$tenantId = $env:ENTRA_TENANT_ID
$secret   = ConvertTo-SecureString $env:SP_CLIENT_SECRET -AsPlainText -Force

# Usando el módulo SqlServer con Service Principal
Invoke-ProcessTable `
    -Server  "asazure://brazilsouth.asazure.windows.net/meuservidor" `
    -Database "MeuModeloAAS" `
    -TableName "FatoVendas" `
    -RefreshType "Full" `
    -ServicePrincipal `
    -ApplicationId $appId `
    -TenantId $tenantId `
    -Credential (New-Object System.Management.Automation.PSCredential($appId, $secret))

El Service Principal para operaciones en AAS (actualización, implementación) debe agregarse como administrador del servidor a través del Portal de Azure o SSMS. Atención: agregar el SP a un grupo de seguridad y luego agregar el grupo como administrador no funciona en AAS — el SP debe agregarse directamente.

Bueno, espero que les haya gustado este artículo y que les haya sido útil para profundizar en el conocimiento de la optimización del procesamiento en Azure Analysis Services, SSAS Tabular y Power BI Premium.

Cubrimos desde la arquitectura interna de VertiPaq hasta estrategias avanzadas de particionamiento, tablas de agregación, Scale-Out con réplicas de lectura, EncodingHint, gestión de segmentos, automatización con PowerShell y AMO, buenas prácticas de autenticación y monitoreo en producción.

Hay mucho contenido aquí, guarden este enlace, porque es el tipo de cosas que querrán consultar varias veces a lo largo de su viaje.

¡Un fuerte abrazo y hasta la próxima!