Fala pessoal!!!

Se você trabalha com Azure Analysis Services, SQL Server Analysis Services (SSAS) ou Power BI Premium e já ficou encarando aquela barra de progresso por horas esperando o processamento terminar, esse post é pra você. Processar um modelo tabular de centenas de gigabytes em produção, com janelas de atualização apertadas e usuários esperando os dados, é um dos maiores desafios do dia a dia de um DBA ou profissional de BI.

Nesse guia, vou cobrir absolutamente tudo: desde o que acontece por baixo dos panos no motor VertiPaq, passando por todos os tipos de processamento (com Process Add em profundidade), paralelismo interno vs externo, otimizações na fonte de dados (SQL Server, Synapse, Query Folding), particionamento avançado (Hot/Warm/Cold), Scale-Out com réplicas de leitura para zero downtime, tabelas de agregação para modelos com bilhões de linhas, configuração de EncodingHint, uso profissional do Tabular Editor (BPA) e DAX Studio, automação via TMSL, PowerShell e ADF, e muito mais.

A maioria das técnicas se aplica igualmente ao AAS, SSAS 2016+ e Power BI Premium/Fabric.

Esse é o guia que eu gostaria de ter encontrado quando comecei. Então, vamos lá!

Introdução à Otimização de Processamento no Azure Analysis Services

O Azure Analysis Services (AAS) é um serviço PaaS (Platform as a Service) baseado na mesma engine do SQL Server Analysis Services (SSAS) Tabular, porém rodando totalmente gerenciado na nuvem Azure. Internamente, ele usa o motor VertiPaq, um banco de dados colunar in-memory altamente comprimido, otimizado para cargas de trabalho OLAP analíticas.

Entender o que acontece por baixo dos panos é fundamental para qualquer otimização. Quando você processa um modelo tabular, o Analysis Services realiza, em ordem, as seguintes etapas:

  1. Leitura da Fonte (Data Read): O AS conecta na sua fonte de dados (SQL Server, Synapse, Blob Storage etc.) e executa as queries definidas nas partições/tabelas.
  2. Codificação e Compressão (Encoding): Os dados retornados são codificados pelo VertiPaq usando Value Encoding ou Hash Encoding, dependendo do tipo de dado e cardinalidade da coluna.
  3. Construção do Dicionário de Dados: Para cada coluna, o AS mantém um dicionário de valores únicos mapeados para IDs inteiros. Esse dicionário é o coração da compressão colunar.
  4. Construção dos Índices: Dois tipos principais de índice são construídos: o Forward Index (mapeamento de linha → ID do valor) e o Inverted Index (mapeamento de valor → linhas que o contêm, usado para filtros).
  5. Recálculo das Hierarquias e Relações: Após os dados estarem carregados, o AS reconstrói os relacionamentos e hierarquias entre tabelas.
  6. Recálculo de Medidas (Process Recalc): Todas as medidas calculadas e colunas calculadas são recalculadas no contexto dos novos dados.

Cada uma dessas etapas pode ser um gargalo dependendo do cenário. Por isso, otimizar processamento não é uma única ação, é uma estratégia em múltiplas camadas.

IMPORTANTE: As otimizações descritas nesse guia são válidas para Azure Analysis Services (AAS), SQL Server Analysis Services 2016+ em modo tabular, e Power BI Premium/Fabric (via XMLA Endpoint). O motor VertiPaq é o mesmo nos três produtos.

VertiPaq e o uso de memória RAM

O VertiPaq é um banco de dados in-memory. Isso significa que todos os dados do modelo precisam caber na RAM do servidor durante e após o processamento. No AAS, o tier do servidor determina a RAM disponível:

  • 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

O processamento de um modelo pode consumir até 2–3x o tamanho do modelo em disco de memória RAM, pois enquanto os novos dados são carregados, os dados antigos ainda ficam na memória (para não interromper consultas em execução). Esse fenômeno é chamado de cold swap ou hot swap dependendo de como você configura o processamento.

Cold Swap vs. Hot Swap no Processamento

Esse é um detalhe técnico fundamental que impacta diretamente o consumo de memória durante o processamento:

  • Cold Swap (padrão): Os dados antigos são liberados da memória antes de os dados novos serem carregados. Durante o processamento, consultas ao modelo retornam erro ou dados desatualizados. Consome menos memória RAM mas tem indisponibilidade.
  • Hot Swap: Uma nova cópia do modelo é carregada em paralelo com a cópia antiga. Quando a nova está pronta, o AS faz o swap atômico. Consultas continuam funcionando durante o processamento. Requer o dobro de memória RAM do modelo.

No AAS, o hot swap acontece automaticamente quando você usa as APIs REST ou o XMLA Endpoint para processar. Para configurar o comportamento, você pode ajustar a propriedade CommitMode nos scripts XMLA. Para modelos grandes, é comum escalar o tier do servidor verticalmente durante a janela de processamento e escalar de volta depois, o AAS permite isso via REST API sem downtime.

Entendendo os Tipos de Processamento do Analysis Services

Compreender os diferentes tipos de processamento é o primeiro passo para uma estratégia eficiente. Cada tipo tem um custo e um propósito específico. Usar o tipo errado é um dos erros mais comuns que levam a processamentos desnecessariamente lentos.

Process Full

O Process Full é o tipo mais abrangente e mais custoso. Ele executa, na ordem: Process ClearProcess DataProcess IndexProcess Recalc. Basicamente, apaga tudo e reconstrói do zero.

  • Quando usar: Após mudanças estruturais no modelo (adição/remoção/renomeação de colunas, alteração de tipos de dados, mudança nas queries das partições). Também indicado quando há suspeita de corrupção de dados ou após restaurar um backup desatualizado.
  • Quando evitar: Nunca use Process Full em tabelas de fato com bilhões de linhas como rotina diária. O impacto na janela de manutenção pode ser inadministrável.
  • Custo estimado: Alto, leitura completa da fonte + reconstrução de todos os índices + recálculo de todas as medidas.

Para executar via TMSL (Tabular Model Scripting Language), utilize o script abaixo no SQL Server Management Studio (SSMS) conectado ao seu servidor AAS/SSAS:

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

Process Clear

Remove todos os dados de um objeto (modelo, tabela ou partição), mas mantém a estrutura de metadados intacta. O objeto fica em estado unprocessed após o Process Clear.

  • Quando usar: Quando você precisa liberar memória RAM urgentemente. Ou como primeiro passo de uma estratégia de processamento manual em estágios. Também útil para limpar uma partição específica antes de recarregá-la com dados corrigidos.
  • Ponto de atenção: Após o Process Clear, consultas ao objeto retornarão erro até que ele seja reprocessado.
{
  "refresh": {
    "type": "clearValues",
    "objects": [
      {
        "database": "MeuModeloAAS",
        "table": "FatoVendas",
        "partition": "FatoVendas_Atual"
      }
    ]
  }
}

Process Data

Carrega dados da fonte para o objeto especificado, mas não reconstrói os índices e não recalcula medidas. É mais rápido que o Process Full, mas o objeto fica em estado dirty, os dados estão presentes, mas os índices de filtro e os cálculos DAX ainda apontam para os dados anteriores.

  • Quando usar: Como primeiro passo de um fluxo de processamento em dois estágios: você processa os dados de várias tabelas/partições com Process Data (possivelmente em paralelo) e depois dispara um único Process Recalc no modelo inteiro. Isso evita que o Process Recalc rode repetidamente a cada tabela.
  • Ponto de atenção: Enquanto uma tabela está em estado dirty, consultas que a envolvem retornam os dados antigos. Nunca use Process Data e deixe o modelo assim, sempre complete o ciclo com Process Recalc.
  • Custo estimado: Médio, lê a fonte inteira da partição/tabela e reconstrói o dicionário e os segmentos, mas não refaz os índices de relacionamento nem as colunas calculadas.

Exemplos de uso via TMSL, processando uma tabela específica, uma partição específica e múltiplas partições:

// 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" }]
        }
      }
    ]
  }
}

Esse padrão sequence + dataOnly paralelo + calculate final é um dos mais eficientes para modelos com múltiplas tabelas grandes, pois o Process Recalc roda apenas uma vez, após todos os dados estarem carregados.

Process Add: O Tipo Mais Poderoso (e Mais Mal-Entendido)

O Process Add é, na minha opinião, o tipo de processamento mais incompreendido do Analysis Services, e por uma razão simples: ele não aparece na UI do SSMS quando você abre o diálogo "Process Database". Você vai ver Process Full, Process Default, Process Clear, mas Process Add não está lá. Isso porque o Process Add só faz sentido em nível de partição específica, não do banco inteiro. Para usá-lo, você precisa ou abrir o diálogo de processamento de uma partição individual no SSMS, ou, o mais comum em produção, escrever o script TMSL manualmente.

Como o Process Add Funciona Internamente

O Process Add executa a query de fonte da partição (exatamente como definida nos metadados do modelo) e acrescenta os resultados ao que já existe na partição, sem apagar os dados anteriores. O VertiPaq cria novos segmentos com os dados novos e os adiciona à partição existente.

A Armadilha Fatal do Process Add

CRÍTICO: O Process Add não sabe o que é "novo". Ele simplesmente executa a query da partição e adiciona o resultado. Se a query da partição não filtrar apenas os dados novos desde o último processamento, você terá dados duplicados. Por exemplo: uma partição com a query SELECT * FROM FatoVendas WHERE Ano = 2024, se você rodar Process Add hoje, ela vai ler todos os dados de 2024 e duplicar tudo que já estava carregado.

A responsabilidade de garantir que a query da partição retorne apenas os dados que ainda não foram carregados é totalmente sua. O AS simplesmente executa o que você definiu.

Padrões Corretos para Usar Process Add

Existem três padrões principais que tornam o Process Add seguro e eficiente:

Padrão 1, Partição de "delta" com tabela de staging:

A query da partição aponta para uma tabela de staging que contém apenas as linhas novas desde o último processamento. Após o Process Add concluir, você trunca a tabela de staging (ou marca as linhas como "processadas").

-- Query da partição "FatoVendas_Delta" no modelo AS:
SELECT DataVendaKey, ProdutoKey, ClienteKey, VlrVenda, QtdVenda
FROM dbo.FatoVendas_Staging_AS
-- Essa tabela é truncada ANTES de cada carga ETL
-- e preenchida APENAS com os dados novos do dia

-- Após o Process Add concluir, no seu pipeline ETL você executa:
TRUNCATE TABLE dbo.FatoVendas_Staging_AS;

Padrão 2, Watermark (marca d'água) na query da partição:

A query referencia uma tabela de controle que armazena o ponto de corte do último processamento bem-sucedido. Após o Process Add, você atualiza essa tabela.

-- Query da partição "FatoVendas_Hot" no 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'
)

-- No pipeline ETL, APÓS o Process Add concluir com sucesso:
UPDATE dbo.AS_ProcessControl
SET LastRowVersion = (SELECT MAX(RowVersion) FROM dbo.FatoVendas),
    LastProcessedAt = GETDATE()
WHERE TableName = 'FatoVendas' AND PartitionName = 'FatoVendas_Hot';

Padrão 3, Partição de período fixo vazia:

O caso mais simples e seguro: você cria uma partição nova (que ainda não tem dados) e roda Process Add (ou Process Full, o resultado é igual, pois a partição está vazia). Depois disso, a partição não é mais tocada com Process Add, apenas com Process Full quando necessário corrigir dados 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"
    }]
  }
}

Quando NÃO Usar Process Add

  • Dados com UPDATE ou DELETE na fonte: Se uma venda do dia anterior foi cancelada (DELETE) ou corrigida (UPDATE), o Process Add não vai apagar a linha errada da partição, ela permanece lá. Para linhas mutáveis, use sempre Process Full na partição afetada.
  • Partições com query de range fixo: Uma partição definida como WHERE Mes = 3 AND Ano = 2024 já tem todos os dados do período. Rodar Process Add nela vai duplicar tudo. Nunca faça isso.
  • Sem mecanismo de controle de delta: Se você não tem como garantir que a query da partição retorna apenas dados novos (sem tabela de watermark, sem staging, sem partição nova vazia), não use Process Add.
  • Após muitos Process Add acumulados: Cada Process Add cria novos segmentos na partição. Após dezenas ou centenas de execuções, a partição fica fragmentada (muitos segmentos pequenos), o que prejudica a performance de scan. Periodicamente, faça um Process Full na partição para consolidar tudo em segmentos grandes e bem comprimidos, ou use Process Defrag.

Checklist Anti-Duplicação para Process Add

Antes de cada execução de Process Add em produção, valide:

  1. A query da partição tem uma cláusula que limita aos dados ainda não carregados?
  2. Existe um mecanismo de controle (watermark, staging, partição vazia) que atualiza esse filtro após cada processamento bem-sucedido?
  3. O que acontece se o Process Add falhar no meio? A query vai re-tentar e re-enviar os mesmos dados? Isso gera duplicação?
  4. Você já fez um SELECT COUNT(*) na partição após o processo para verificar o número de linhas esperado?

TMSL completo para Process Add numa partição existente:

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

Após o Process Add na partição, você ainda precisa rodar um Process Recalc no banco (ou na tabela) para que as colunas calculadas, hierarquias e relacionamentos reflitam os novos dados:

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

Desfragmentação após Process Add Repetido

Cada execução de Process Add adiciona um novo conjunto de segmentos à partição. Uma partição que recebeu 365 Process Adds diários ao longo de um ano pode ter centenas de segmentos de tamanhos variados. Para verificar a fragmentação e corrigir:

-- Verificar quantos segmentos tem cada partição
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 bloco de dados
GROUP BY DIMENSION_NAME, PARTITION_NAME
HAVING COUNT(*) > 5
ORDER BY TotalSegmentos DESC;

Se uma partição tiver dezenas ou centenas de segmentos com pouquíssimas linhas cada, faça um Process Full nela para consolidar. O Process Full na partição não afeta as outras partições da mesma tabela, os dados das outras partições permanecem intactos para consultas durante o reprocessamento (graças ao Hot Swap).

Process Recalc

Recalcula todas as colunas calculadas, hierarquias, relacionamentos e medidas DAX do modelo. Não lê dados da fonte. É executado automaticamente ao final de um Process Full, mas pode ser separado para cenários avançados.

  • Quando usar: Ao final de um fluxo de processamento paralelo onde você processou várias tabelas com Process Data independentemente e quer disparar o recálculo uma única vez no final, evitando recalcular várias vezes durante o processo.
  • Custo estimado: Médio a alto, dependendo da complexidade das medidas DAX e do número de colunas calculadas. Em modelos com muitas colunas calculadas com fórmulas complexas, o Process Recalc pode ser a fase mais demorada.
{
  "refresh": {
    "type": "calculate",
    "objects": [
      {
        "database": "MeuModeloAAS"
      }
    ]
  }
}

Process Defrag

Reorganiza internamente os segmentos de dados no disco para melhorar a performance de leitura em consultas subsequentes. Analógico ao REORGANIZE INDEX do SQL Server.

  • Quando usar: Periodicamente em modelos com muitas operações de Process Add incrementais. Ao longo do tempo, o VertiPaq pode criar muitos segmentos pequenos e fragmentados, o que piora a performance de scan.
  • Custo estimado: Baixo a médio, não lê da fonte, apenas reorganiza dados em memória/disco.

Process Index

Reconstrói os índices (forward index e inverted index) para um objeto sem recarregar dados da fonte. Geralmente executado após um Process Data.

  • Quando usar: Em fluxos manuais onde Process Data e Process Index são separados para controle granular. Menos comum em automações modernas onde o refresh TMSL cuida de tudo.

Resumo: Qual tipo usar em cada cenário?

Para ajudar na tomada de decisão, veja a tabela de referência rápida:

  • Mudança estrutural no modelo (nova coluna, tipo de dado): Process Full na tabela afetada.
  • Atualização diária via append-only com staging/watermark: Process Add na partição "hot" + Process Recalc no modelo.
  • Atualização diária de tabela grande (com possíveis updates/deletes): Process Full na partição "hot" + Process Recalc no modelo.
  • Reprocessamento de dado corrigido em período histórico: Process Full apenas na partição histórica afetada + Process Recalc no modelo.
  • Partição nova sendo carregada pela primeira vez: Process Full ou Process Add (equivalentes, a partição está vazia).
  • Múltiplas tabelas/partições com máximo paralelismo: Process Data em todas + Process Recalc único ao final (num único comando TMSL).
  • Partição com muitos Process Add acumulados e fragmentada: Process Full na partição para consolidar os segmentos.
  • Modelo com fragmentação acumulada sem necessidade de reload: Process Defrag semanal agendado.

Para a maioria das rotinas de atualização em produção, a combinação de Process Add nas partições incrementais (com query de delta correta) + Process Full na partição "hot" + Process Recalc no modelo é a abordagem mais eficiente e segura para grandes modelos.

Paralelismo no Processamento: O AS Cuida vs. Você "Ajuda" (e Piora)

Paralelismo é o assunto mais mal-compreendido quando se fala em otimizar o processamento do Analysis Services. A intuição leva muita gente ao caminho errado: "se eu mandar 4 comandos de processamento ao mesmo tempo, vai ficar 4x mais rápido!" Na prática, pode ficar mais lento, ou quebrar.

Vamos entender a diferença entre os dois tipos de paralelismo e quando cada um é adequado.

Paralelismo Interno: Deixa o AS Trabalhar

Quando você envia um único comando TMSL com múltiplos objetos, o AS gerencia todo o paralelismo internamente. Ele analisa as dependências entre os objetos (por exemplo, sabe que o Process Recalc precisa esperar todos os Process Data terminarem), distribui o trabalho entre as threads disponíveis, e coordena o uso de memória 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" }
          ]
        }
      }
    ]
  }
}

O parâmetro maxParallelism define o teto de objetos processados simultaneamente. Se não for especificado, o AS usa seu próprio critério (geralmente baseado no número de núcleos de CPU). Aumentar além dos núcleos disponíveis raramente ajuda e pode piorar pela contenção de recursos.

Este é o padrão recomendado para 99% dos casos. É seguro, previsível e o AS o otimiza automaticamente.

Paralelismo Externo: Por que Parece uma Boa Ideia mas (Quase Sempre) Não É

O "paralelismo externo" é quando você, por fora do AS, dispara múltiplos comandos de processamento simultaneamente, por exemplo, 4 jobs PowerShell rodando em paralelo, cada um enviando um script TMSL separado para o mesmo servidor.

# Exemplo do ANTI-PADRÃO: paralelismo externo
# Disparar 4 processamentos independentes "ao mesmo tempo"
$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

Na teoria, parece ótimo: 4 processamentos ao mesmo tempo! Na prática, os problemas são sérios:

Os Problemas do Paralelismo Externo

  • Pico de memória RAM não coordenado: Cada comando externo aloca memória de forma independente, sem comunicação entre si. O AS não tem visibilidade do que os outros comandos estão fazendo. Se cada um precisar de 10GB de RAM no pico e o servidor tem 32GB total, você vai atingir o HardMemoryLimit e o AS vai começar a cancelar transações ou rejeitar conexões.
  • Contenção no Process Recalc: Cada comando TMSL de type: "full" inclui um Process Recalc ao final. Quando dois comandos chegam à fase de recálculo ao mesmo tempo, eles disputam um lock exclusivo de escrita nos metadados do modelo. Um vai executar, o outro vai esperar (ou falhar com timeout). Você perdeu todo o benefício do paralelismo.
  • Lock de metadata e conflitos de transação: Operações de processamento adquirem locks de escrita no modelo. Dois comandos externos tentando escrever ao mesmo tempo geram fila de serialização ou erros de deadlock, você acaba com execução sequencial piorada pela sobrecarga de gerenciar múltiplas transações concorrentes.
  • Inconsistência em caso de falha parcial: Se 3 dos 4 comandos completam e 1 falha, o modelo fica em estado inconsistente, algumas partições atualizadas, outras não, sem uma transação que agrupe tudo. Com o paralelismo interno num único TMSL, você controla o commitMode e o comportamento de falha de forma explícita.
  • Sem orquestração de dependências: O AS, dentro de um único TMSL, sabe que precisa processar as dimensões antes de fazer o recálculo das tabelas de fato que dependem delas. Com comandos externos, você precisa implementar essa lógica de dependência manualmente, e é fácil errar.

Quando o Paralelismo Externo PODE Funcionar

Existem cenários específicos onde enviar múltiplos comandos em paralelo faz sentido:

  • Bancos de dados separados no mesmo servidor: Processar o banco "Vendas" e o banco "Financeiro" simultaneamente é seguro, eles têm metadados independentes, sem conflito de locks.
  • Fase de Process Data puro, sem Process Recalc: Se você garante que todos os comandos externos são do tipo dataOnly (sem recálculo), a contenção de locks é muito menor. Você dispara os Process Data em paralelo, espera todos terminarem, e só então envia um único calculate. Mas nesse ponto, um único TMSL com sequence já faz isso melhor.
  • Power BI Premium com Enhanced Refresh API: A API de refresh do Power BI (REST API v2) gerencia o paralelismo no servidor, não no cliente. Você pode submeter múltiplas requisições de refresh que o serviço coloca em fila e orquestra com visibilidade de recursos. Esse sim é um paralelismo externo bem implementado.

O Padrão Correto para Máximo Paralelismo

A forma mais eficiente e segura de maximizar o paralelismo no processamento do AAS é usar um único TMSL com o padrão 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" }]
        }
      }
    ]
  }
}

Nesse padrão: todos os objetos na etapa 1 são processados em paralelo (até o limite de maxParallelism). O AS aguarda todos completarem. Só então executa o calculate único na etapa 2. Você tem o máximo de paralelismo com a mínima contenção e total consistência transacional.

Regra de Ouro do Paralelismo

Use um único comando TMSL com múltiplos objetos e deixe o AS gerenciar o paralelismo internamente via maxParallelism. Só dispare múltiplos comandos externos em paralelo quando os objetos forem em bancos de dados diferentes no mesmo servidor. Para tudo dentro do mesmo banco, confie no AS, ele faz melhor do que você.

Otimizando a Fonte de Dados: Seu Primeiro Passo para a Performance

A otimização começa muito antes do Analysis Services. O AS é tão rápido quanto a sua fonte de dados permite. Se a query na origem demora 2 horas para retornar os dados, não há otimização no modelo que resolva isso. Por isso, vamos começar pela camada mais ignorada: a fonte.

Diagnosticando o Gargalo na Fonte

Antes de qualquer otimização, você precisa medir. Use o DAX Studio para capturar a query que o AS está enviando para a fonte de dados:

  1. Conecte o DAX Studio ao seu servidor AAS/SSAS.
  2. Vá em Advanced > View Metrics e habilite Query Plan e Server Timings.
  3. Execute um EVALUATE ROW("x", COUNTROWS(FatoVendas)) para forçar um scan completo.
  4. Na aba Server Timings, você verá o tempo gasto em Storage Engine (SE), que inclui o tempo de leitura da fonte, versus Formula Engine (FE).

Alternativamente, durante o processamento, verifique o SQL Server Profiler ou o Extended Events do AAS/SSAS para capturar os eventos ProgressReportBegin/End e identificar quais tabelas/partições são mais lentas.

SQL Server na Origem: Boas Práticas para Leitura pelo AS

Antes de criar qualquer índice, há ajustes de comportamento de execução que impactam diretamente o tempo de leitura e que quase ninguém configura. O AS envia queries SQL para a fonte como qualquer outro cliente, mas com padrões que podem ser subótimos dependendo do ambiente.

Nível de Isolamento: Evitando Bloqueios durante o Processamento

Por padrão, o AS executa leituras com READ COMMITTED. Em bancos com ETL ativo durante o processamento do AS (situação comum em pipelines apertados), as leituras bloqueiam nas linhas com lock de escrita. O resultado é o AS esperando o ETL liberar os locks antes de conseguir ler, podendo adicionar dezenas de minutos ao processamento. Para tabelas de DW onde a carga já foi concluída antes do AS iniciar:

-- Opção 1: NOLOCK na query da partição (dirty reads, adequado para DW com carga concluída)
SELECT DataVendaKey, ProdutoKey, ClienteKey, VlrVenda, QtdVenda
FROM dbo.FatoVendas WITH (NOLOCK)
WHERE DataVendaKey >= 20240101 AND DataVendaKey < 20250101;

-- Opção 2: READ_COMMITTED_SNAPSHOT no banco (MVCC: leituras consistentes sem bloquear escritas)
-- Mais seguro que NOLOCK, sem dirty reads, sem custo extra de lock
-- Requer habilitar uma vez no banco de origem:
ALTER DATABASE MeuDW SET READ_COMMITTED_SNAPSHOT ON;
-- Com RCSI ativo, o AS lê a versão confirmada mais recente dos dados sem bloquear escritas

Use NOLOCK somente quando a carga de ETL na tabela foi concluída antes do AS começar a ler. Para dados onde consistência importa, prefira habilitar READ_COMMITTED_SNAPSHOT no banco de origem. É um ajuste único que beneficia todas as leituras analíticas sem alterar as queries.

Estatísticas: Atualizar após Cada Carga de ETL

Estatísticas desatualizadas fazem o otimizador do SQL Server escolher planos ruins para as queries que o AS envia, podendo causar full scans em tabelas de bilhões de linhas quando um índice columnstore eliminaria 99% das linhas. Agende a atualização logo após o ETL:

-- Atualizar estatísticas de tabelas críticas com scan completo após cada carga
UPDATE STATISTICS dbo.FatoVendas WITH FULLSCAN;
UPDATE STATISTICS dbo.DimProduto  WITH FULLSCAN;

-- Identificar tabelas com estatísticas defasadas (mais de 10% das linhas 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 e MAXDOP nas Queries de Partição

O AS gera queries com literais nos filtros de partição (ex: WHERE DataVendaKey >= 20240101). O SQL Server pode reutilizar um plano em cache compilado para outros valores, que não é ótimo para os literais específicos. OPTION (RECOMPILE) força recompilação com estatísticas atuais para cada execução, gerando um plano ideal para os dados reais. Em servidores compartilhados, combine com MAXDOP para não afetar queries OLTP:

-- Query de partição otimizada: plano 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 leitura paralela, deixa outros para OLTP
-- RECOMPILE: plano compilado com os literais reais, aproveita partition elimination

Índices na Origem de Dados (SQL Server / Azure Synapse)

Para fontes relacionais, os índices na origem são críticos. O AS geralmente executa uma query do tipo SELECT col1, col2, col3 FROM tabela WHERE filtro e espera um full scan eficiente. Para isso:

Clustered Columnstore Index (CCI): O Rei para DW

Para tabelas de fato em SQL Server ou Azure Synapse, o Clustered Columnstore Index é a melhor escolha. Ele armazena dados em formato colunar comprimido, elimina a necessidade de ler colunas desnecessárias e é extremamente eficiente para scans analíticos (que é exatamente o que o AS faz).

Para criar um CCI em uma tabela existente, utilize o script abaixo:

-- Criando CCI em tabela de fato 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 qualidade dos Row Groups criados
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: Row groups ideais têm próximo de 1.048.576 linhas (1M). Row groups muito menores (abaixo de 100k linhas) indicam fragmentação e devem ser reorganizados com ALTER INDEX REORGANIZE.

A opção DATA_COMPRESSION = COLUMNSTORE_ARCHIVE aplica compressão adicional (DEFLATE) sobre a compressão colunar padrão. Para tabelas históricas raramente acessadas, pode reduzir o tamanho em até 50% adicionais com impacto mínimo no tempo de leitura do AS, que já lê os dados comprimidos.

NonClustered Columnstore Index (NCCI)

Se sua tabela já tem um Clustered Index (B-Tree) e não pode ser convertida para CCI (por exemplo, por ser uma tabela transacional com updates/deletes frequentes), um NonClustered Columnstore Index pode ser criado apenas nas colunas que o AS vai ler:

-- NCCI apenas nas colunas usadas pelo Analysis Services
CREATE NONCLUSTERED COLUMNSTORE INDEX NCCI_FatoVendas_AS
ON dbo.FatoVendas (DataVendaKey, ProdutoKey, ClienteKey, VlrVenda, QtdVenda)
WHERE DataVendaKey >= 20200101;  -- Filtro para dados relevantes ao modelo

Particionamento na Fonte (SQL Server)

Se a sua tabela de fato é particionada no SQL Server, configure a query da partição do AS para usar partition elimination. Em vez de filtrar por data diretamente, consulte pela função de partição:

-- Query de partição do AS alinhada com o particionamento do SQL Server
-- Partição "2024" no AS mapeia para partição de 2024 na tabela de origem
SELECT
    DataVendaKey, ProdutoKey, ClienteKey, VlrVenda, QtdVenda, Margem
FROM dbo.FatoVendas
WHERE DataVendaKey >= 20240101
  AND DataVendaKey < 20250101
  -- SQL Server usa a constraint para fazer partition elimination automaticamente

Quando a tabela da fonte é particionada e a query inclui o critério de filtro de partição, o SQL Server faz partition elimination e lê apenas os filegroups necessários, drasticamente mais rápido do que um full table scan.

Query Folding no Power Query / M

Quando seu modelo usa Power Query (M) como camada de transformação, o Query Folding é crítico para performance. Query Folding é quando o Power Query consegue converter suas transformações M em SQL nativo que é executado diretamente na fonte, em vez de puxar todos os dados brutos e filtrar/transformar em memória no AS.

Para verificar se uma etapa do seu query está fazendo folding, clique com o botão direito na etapa no Power Query Editor e veja se a opção "View Native Query" está disponível (se estiver disponível, o folding está acontecendo).

ATENÇÃO: Certas transformações M quebram o Query Folding, fazendo com que o Power Query/AS puxe todos os dados brutos para memória antes de filtrar. As principais são: Table.Buffer(), transformações de texto com funções não suportadas pelo conector, Table.FromList(), List.Generate(), e qualquer referência a outras queries que não fazem folding. Após qualquer uma dessas operações, todas as transformações seguintes também perdem o folding.

Para forçar o filtro na fonte e garantir o folding, faça o filtro de data/chave como primeiro passo da query, antes de qualquer outra transformação:

-- Na origem SQL, garanta a query com WHERE já filtrado
-- No Power Query, o primeiro passo deve ser o filtro de data:
-- Source = Sql.Database("servidor", "banco"),
-- FatoVendas = Source{[Schema="dbo",Item="FatoVendas"]}[Data],
-- FiltroData = Table.SelectRows(FatoVendas, each [DataVendaKey] >= 20200101)
-- ^^ isso faz query folding, gerando WHERE DataVendaKey >= 20200101 na fonte

Views Materializadas no Azure Synapse Analytics

Para fontes no Azure Synapse Dedicated SQL Pool, uma funcionalidade poderosa é a Materialized View. Diferente de uma view comum, a materialized view pré-computa e armazena fisicamente os resultados de uma query complexa (joins, agregações), sendo automaticamente mantida atualizada pelo Synapse:

-- Criando uma Materialized View no Azure Synapse para alimentar o 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: O AS lê da materialized view em vez da tabela de fato bruta, pulando o join e a agregação que antes eram feitos no Synapse a cada processamento. Para modelos com joins complexos na origem, isso pode reduzir o tempo de leitura em 60–80%.

Distribuição de Tabelas no Azure Synapse

O Synapse Dedicated SQL Pool distribui dados entre 60 nós computacionais. A escolha errada de distribuição pode causar data movement massivo durante os joins, tornando as queries lentas. Para tabelas grandes lidas pelo AS:

  • Hash Distribution: Ideal para tabelas de fato grandes. Distribua pela chave usada nos joins mais frequentes (geralmente a chave de data ou produto). Isso coloca os dados dos dois lados do join no mesmo nó, eliminando data movement.
  • Replicated: Ideal para tabelas de dimensão pequenas (até ~2 GB). Uma cópia completa em cada nó elimina todo o data movement para joins com a tabela de fato.
  • Round Robin: Padrão, evite para tabelas lidas pelo AS em produção.
-- Tabela de fato: Hash na chave de join com a dimensão 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),  -- alinha com a dimensão mais seletiva
    CLUSTERED COLUMNSTORE INDEX
);

-- Tabela de dimensão pequena: Replicated (cópia em todos os nós)
CREATE TABLE dbo.DimProduto
(
    ProdutoKey      INT NOT NULL,
    NomeProduto     NVARCHAR(200),
    CategoriaKey    INT,
    Preco           DECIMAL(10,2)
)
WITH
(
    DISTRIBUTION = REPLICATE,
    CLUSTERED INDEX (ProdutoKey)
);

Co-Localização e Rede: O Gargalo Silencioso

Um dos fatores mais ignorados no processamento do AAS é a latência e a largura de banda de rede entre o servidor AAS e a fonte de dados. Durante o processamento de um modelo grande, o AS pode transferir centenas de gigabytes da fonte. Se essa transferência passa por uma conexão com latência alta ou largura de banda limitada, todos os ganhos de índice e particionamento ficam comprometidos.

  • Mesma região Azure: O AAS e a fonte (Azure SQL, Synapse, ADLS) devem estar na mesma região Azure. Tráfego entre regiões tem latência adicional (tipicamente 20–100ms dependendo da distância) e pode ser tarifado como tráfego de saída. Tráfego intra-região é gratuito e tem latência de 1–2ms.
  • Firewall do AAS: O Azure Analysis Services tem um firewall IP nativo (configurável no portal Azure, aba Firewall do servidor). Habilite-o e restrinja o acesso de entrada apenas às faixas de IP necessárias (por exemplo, o IP do Power BI Service, dos administradores e das ferramentas de deploy). O AAS não suporta Azure Private Link/Private Endpoint, diferente de serviços mais recentes como o Fabric. Para proteger a fonte de dados de acessos externos, configure o firewall do Azure SQL ou Synapse para aceitar apenas o IP de saída do servidor AAS.
  • On-Premises com ExpressRoute: Se a fonte de dados está on-premises, use Azure ExpressRoute para uma conexão dedicada de alta velocidade. Evite processar o AAS via VPN básica para fontes grandes, pois a largura de banda é insuficiente para transferir dezenas de gigabytes em tempo hábil (uma VPN S2S típica tem 1–10 Gbps teóricos, mas com latência e overhead que impactam transferências longas).
  • On-Premises Data Gateway: Para fontes on-premises sem ExpressRoute, o Gateway age como proxy entre o AAS e a fonte local. Instale-o em uma máquina na mesma rede da fonte (não na mesma máquina que a fonte, para não competir por CPU). Monitore o uso de CPU e memória do Gateway durante o processamento.
  • Mover dados para o Azure primeiro: O padrão mais eficiente para ambientes híbridos é usar o pipeline de dados (ADF, Synapse Pipelines) para mover os dados do on-premises para o Azure (Azure SQL, Synapse, ADLS) como primeira etapa, e só então o AAS lê da fonte, tudo dentro da mesma região Azure. Elimina a latência on-premises do caminho crítico do processamento.

Estratégias de Particionamento para Modelos Tabulares

O particionamento é a técnica individual mais impactante para otimizar o processamento de modelos tabulares grandes. Sem particionamento, todo processamento é um Process Full na tabela inteira. Com um bom esquema de particionamento, você pode reduzir o volume de dados reprocessado em 90%+ por operação.

Por que Particionar? Os Três Benefícios Principais

  • 1. Processamento Incremental: Processar apenas a fatia de dados que mudou, em vez de toda a tabela. Uma tabela de 5 anos de dados com partições mensais permite processar apenas o mês atual, 1/60 dos dados totais.
  • 2. Paralelismo: O AS pode processar múltiplas partições em paralelo, utilizando todos os núcleos de CPU disponíveis. Sem partições, o processamento de uma única tabela é single-threaded.
  • 3. Alta Disponibilidade: Enquanto uma partição está sendo reprocessada, as outras permanecem disponíveis para consultas. Usuários não perdem acesso aos dados históricos durante a atualização.

Escolhendo a Granularidade das Partições

A granularidade ideal depende do volume de dados e da frequência de atualização. Como regra geral:

  • Partições anuais: Para tabelas com poucos milhões de linhas por ano ou dados históricos raramente atualizados.
  • Partições mensais: O padrão mais comum para tabelas de fato de médio-grande porte (dezenas de milhões de linhas/mês).
  • Partições diárias: Para tabelas com volume muito alto (centenas de milhões de linhas/dia) ou quando a granularidade de atualização é por hora/dia.
  • Partições por entidade: Para cenários específicos como multi-tenancy (uma partição por cliente/região), muito usada em Power BI Embedded.

Cuidado com muitas partições

IMPORTANTE: Criar centenas ou milhares de partições tem custo. O AS mantém metadados para cada partição, e o gerenciamento de muitas partições pode impactar o tempo de processamento e a performance de consultas. Mantenha o número de partições por tabela abaixo de 200–300 para a maioria dos cenários. Para Power BI Premium/Fabric, o limite é 1.000 partições por tabela.

Padrão Hot/Warm/Cold de Particionamento

Esse é o padrão de particionamento mais sofisticado e eficiente para grandes modelos. A ideia é dividir as partições em três camadas:

  • Hot (Partição Atual): Dados dos últimos dias/semanas que ainda podem mudar. Reprocessada com Process Full a cada ciclo (diário ou horário). Normalmente 1 partição.
  • Warm (Últimos meses): Dados de 1–6 meses atrás, consolidados mas ainda relevantes para análises recentes. Reprocessada semanalmente ou quando há correções. Geralmente 6–12 partições mensais.
  • Cold (Histórico): Dados de anos anteriores. Consolidados e imutáveis. Nunca reprocessados na rotina normal, apenas quando há correção histórica específica. Podem ter dezenas de partições anuais.

Com esse padrão, uma atualização diária típica processa apenas a partição "Hot", que pode conter apenas o dia atual, em vez de anos de história.

Criando e Gerenciando Partições via TMSL

Para automatizar a criação de partições, você pode usar scripts TMSL executados via SSMS, PowerShell ou Azure Data Factory. Veja um exemplo completo de criação de partições mensais 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 Partições com PowerShell e AMO

Para criar partições dinamicamente (por exemplo, criar a partição do próximo mês automaticamente), use o PowerShell com a biblioteca AMO (Analysis Management Objects) ou com o módulo SqlServer:

# Instalar o módulo SqlServer se necessário: Install-Module -Name SqlServer
Import-Module SqlServer

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

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

# Criar partições para cada mês 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 se já 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: Com esse script agendado no Azure Automation ou Task Scheduler, você garante que a partição do próximo mês é criada automaticamente, sem intervenção manual.

Processamento Paralelo de Partições

O AS suporta processamento paralelo nativo quando você inclui múltiplos objetos no mesmo comando TMSL de refresh. O servidor decide automaticamente o nível de paralelismo baseado nos recursos disponíveis. Para forçar 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"
        }
      }
    ]
  }
}

O parâmetro maxParallelism controla quantas partições são processadas simultaneamente. O padrão é o número de núcleos de CPU do servidor. Aumentar esse valor pode acelerar o processamento em servidores com muitos núcleos, mas aumenta o pico de uso de memória RAM (cada partição sendo processada ocupa memória).

O parâmetro commitMode aceita dois valores:

  • "transactional": Todas as partições do batch são commitadas juntas ao final. Se uma falhar, todas fazem rollback. Garante consistência mas mantém os dados antigos disponíveis até o commit final (hot swap).
  • "partialBatch": Cada partição é commitada individualmente assim que termina. Se uma falhar, as já commitadas permanecem. Menos memória, mas possível inconsistência temporária entre partições.

Scale-Out no AAS: Zero Downtime durante o Processamento

O Scale-Out é uma das features mais poderosas do Azure Analysis Services em produção enterprise e uma das menos usadas. Com ele, você adiciona até 7 réplicas de leitura ao servidor. O processamento ocorre exclusivamente na instância de escrita, enquanto as réplicas de leitura continuam servindo consultas aos usuários com os dados do último ciclo bem-sucedido. O resultado: os usuários nunca percebem que um processamento está acontecendo.

Como o Scale-Out Funciona

O fluxo de uma atualização com Scale-Out ativo tem quatro etapas:

  1. Você envia o TMSL de refresh para o endpoint de escrita (sufixo :rw). As réplicas continuam respondendo consultas com os dados anteriores, sem interrupção.
  2. O processamento ocorre isolado na instância de escrita. Toda a alocação de RAM, leitura da fonte, encoding e recálculo acontecem apenas nela, sem competir com as réplicas de leitura.
  3. Após o processamento concluir, você dispara a sincronização. O AS grava o modelo atualizado no armazenamento compartilhado (Azure Blob) e sinaliza as réplicas para recarregar.
  4. Cada réplica faz um hot-swap interno: carrega o modelo novo em paralelo com o modelo antigo e, quando está pronto, troca atomicamente. Durante esse processo (geralmente segundos a poucos minutos), as réplicas ainda servem os dados antigos. Após o swap, passam a usar os dados novos.

Configurando e Usando o Scale-Out

Para processar sem interromper consultas, envie o TMSL para o endpoint de escrita (:rw) e sincronize logo após:

# Endpoint de leitura: o padrao, roteia automaticamente para as replicas
$endpointLeitura = "asazure://brazilsouth.asazure.windows.net/meuservidor"

# Endpoint de escrita: apenas para processamento, vai para a instancia principal
$endpointEscrita = "asazure://brazilsouth.asazure.windows.net/meuservidor:rw"

# Passo 1: processar na instancia de escrita (replicas continuam servindo dados antigos)
Invoke-ASCmd -Server $endpointEscrita -InputFile "refresh.tmsl"

# Passo 2: sincronizar as replicas (cada replica carrega o modelo novo e faz hot-swap)
Invoke-ASCmd -Server $endpointEscrita -Query '{
  "synchronize": {
    "database": "MeuModeloAAS",
    "syncReadOnly": true,
    "syncOnlyCommittedSegments": true
  }
}'

Write-Host "Processamento e sincronizacao concluidos."

O parâmetro syncOnlyCommittedSegments: true transfere apenas os segmentos que foram efetivamente alterados no processamento, em vez de copiar o modelo inteiro. Para modelos com particionamento granular onde apenas algumas partições foram atualizadas, isso reduz muito o tempo de sincronização.

Quando o Scale-Out Compensa

  • SLA de disponibilidade 24/7: Se o modelo precisa estar disponível continuamente (usuários em múltiplos fusos horários, painéis de operações em tempo real), o Scale-Out elimina as janelas de indisponibilidade.
  • Processamentos longos (1–4+ horas): Para modelos grandes que demoram horas para processar, o Scale-Out garante que toda essa janela não cause indisponibilidade para os usuários.
  • Alta concorrência de consultas: As réplicas distribuem a carga. Uma query pesada de um usuário não bloqueia as respostas para os outros.

Custo do Scale-Out

Cada réplica é cobrada ao mesmo custo da instância principal. Um servidor S2 com 3 réplicas custa 4x o preço de um S2 simples. Em muitos cenários, uma janela curta de indisponibilidade aceitável (5–15 min com Hot Swap) num único servidor é mais econômica do que manter réplicas ativas 24/7. Avalie o custo versus o SLA real do negócio antes de habilitar.

Otimizações no Modelo Semântico com Tabular Editor e DAX Studio

Agora que a fonte está otimizada e o particionamento está bem definido, é hora de olhar para dentro do modelo. Aqui entram as ferramentas mais poderosas do arsenal: Tabular Editor e DAX Studio. Ambas são gratuitas (com versões pagas) e indispensáveis para qualquer profissional sério com Analysis Services ou Power BI.

Conectando com Tabular Editor e DAX Studio

Ambas as ferramentas se conectam via XMLA Endpoint:

  • Azure Analysis Services: O endpoint segue o formato asazure://<região>.asazure.windows.net/<nome-servidor>. Encontre-o no Azure Portal, na blade do servidor AAS em "Overview".
  • SSAS on-premises: Use o nome do servidor Windows ou o endereço IP.
  • Power BI Premium/Fabric: O endpoint XMLA é powerbi://api.powerbi.com/v1.0/myorg/<nome-workspace>. Precisa estar habilitado nas configurações do workspace (Configurações > Premium > Conexões de conjunto de dados).
  • Power BI Desktop (local): Abra o modelo no PBI Desktop e conecte o DAX Studio via "Power BI / SSDT". O Tabular Editor pode ser aberto como External Tool diretamente pelo PBI Desktop.

VertiPaq Analyzer: Raio-X do Modelo

O VertiPaq Analyzer é o ponto de partida para qualquer análise de otimização de modelo tabular. Disponível no DAX Studio (menu Advanced > View Metrics) e como add-in no Tabular Editor, ele mostra:

  • Tamanho de cada tabela e coluna em memória.
  • Taxa de compressão por coluna. Colunas com baixa compressão são candidatas a otimização de encoding.
  • Cardinalidade de cada coluna. Alta cardinalidade = mais memória = mais tempo de processamento.
  • Número de segmentos por coluna. Muitos segmentos pequenos = fragmentação = performance ruim.
  • Tamanho de relacionamentos e hierarquias.

Para gerar o relatório via DAX Studio, utilize o script abaixo:

-- No DAX Studio, vá em Advanced > View Metrics para o relatório visual
-- Para extrair os dados via 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: Você vê exatamente quais colunas estão consumindo mais memória e pode priorizar otimizações onde o impacto será maior.

Best Practice Analyzer: Auditoria Automática do Modelo

O Best Practice Analyzer (BPA) do Tabular Editor é uma análise automatizada do modelo contra um conjunto de regras de boas práticas. Pense nele como um linter para modelos tabulares: ele aponta problemas de performance, design e manutenibilidade com explicação e sugestão de correção.

Para executar: no Tabular Editor 2 ou 3, vá em Tools > Best Practice Analyzer. As principais categorias de regras:

  • Performance: Colunas calculadas que deveriam ser medidas DAX, tabelas grandes sem particionamento, colunas com alta cardinalidade sem EncodingHint, relacionamentos bidirecionais sem necessidade, colunas de data/hora sem separação de componentes.
  • Modelo: Tabelas de dimensão sem coluna marcada como chave, relacionamentos com cardinalidade Many-to-Many sem tabela de ponte, hierarquias com apenas um nível (inúteis).
  • Manutenibilidade: Medidas sem descrição, objetos visíveis sem formato de número definido, uso de DIVIDE() em vez de divisão direta (segurança contra divisão por zero).

A Microsoft e a comunidade mantêm um conjunto de regras públicas no GitHub em TabularEditor/BestPracticeRules. Para importar no 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

Você pode criar regras personalizadas para os padrões do seu time (nomenclatura de medidas, prefixos obrigatórios, limites de cardinalidade aceitáveis) e versionar as regras junto com o modelo no Git. Execute o BPA antes de cada deploy para garantir que o modelo não regrediu.

Análise de Colunas e Tipos de Dados: O que Cortar e o que Mudar

Depois do VertiPaq Analyzer, a primeira ação prática é remover o que não precisa estar no modelo e corrigir tipos de dados inadequados.

Removendo Colunas Desnecessárias

Toda coluna no modelo tabular ocupa memória RAM, consome tempo de processamento (encoding + indexação) e aumenta o tamanho do arquivo .abf (backup). Se uma coluna não é usada em relatórios, medidas, filtros ou relacionamentos, ela não deve estar no modelo.

Use o seguinte script C# no Tabular Editor para identificar colunas que não têm referências em medidas ou relacionamentos:

// Script C# no Tabular Editor 2 ou 3 - Listar colunas sem uso
// Execute em: Macros > New Macro > Cole o código abaixo

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)  // coluna visível (IsHidden = false)
    .Select(c => c.DaxObjectFullName)
    .OrderBy(x => x)
    .ToList();

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

Cuidado ao remover colunas

IMPORTANTE: O script acima é uma heurística, ele não analisa código JavaScript de relatórios Power BI ou filtros de dashboards externos. Sempre valide com os desenvolvedores de relatório antes de remover qualquer coluna em produção. Uma abordagem segura é primeiro ocultar a coluna no modelo (para não aparecer nos campos do PBI) e monitorar por 2–4 semanas antes de remover definitivamente.

Otimizando Tipos de Dados

O tipo de dado impacta diretamente a compressão e o uso de memória do VertiPaq. Veja os tipos mais comuns e suas implicações:

  • Int64 (Número inteiro): Melhor compressão com Value Encoding. Ideal para IDs, chaves, quantidades.
  • Decimal (Número fixo): Internamente armazenado como inteiro multiplicado por um fator de escala. Boa compressão para valores monetários com casas decimais fixas.
  • Double (Número flutuante): Menor compressão que Decimal para valores monetários. Use Decimal para preços e valores financeiros.
  • DateTime: Armazenado como Double internamente no VertiPaq. Alta cardinalidade quando inclui horário (cada timestamp único é um valor distinto). Prefira armazenar apenas a data (sem componente de hora) quando possível, ou use uma chave inteira no formato YYYYMMDD.
  • String: A menor compressão de todos. Evite colunas de string com alta cardinalidade (como URLs, GUIDs, hashes). Se precisar de GUID para relacionamentos, converta para Int64 na ETL.
  • Boolean: Excelente compressão (apenas 2 valores possíveis no dicionário). Prefira Boolean a campos string "S/N" ou "Sim/Não".

Para identificar oportunidades de otimização de tipo de dado via DMV, utilize o script abaixo:

-- Identificar colunas String com alta cardinalidade (candidatas a conversão para 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 no VertiPaq
  AND DICTIONARY_SIZE > 1048576  -- Dicionário maior que 1MB
  AND ATTRIBUTE_NAME <> 'RowNumber'
ORDER BY DICTIONARY_SIZE DESC;

Otimizando Relacionamentos: Cardinalidade, Direção e Impacto

Os relacionamentos no modelo tabular têm impacto tanto no processamento (tempo para construir os índices de relacionamento) quanto nas consultas DAX. Entender as opções é fundamental.

Cardinalidade dos Relacionamentos

  • Muitos-para-Um (Many-to-One): O padrão e mais eficiente. A tabela de fato (Many) se relaciona com a tabela de dimensão (One) pelo lado da chave primária da dimensão. O AS otimiza esse padrão internamente.
  • Um-para-Um (One-to-One): Usado para tabelas de extensão ou quando você divide uma tabela grande em duas por questão de performance. Tem custo similar ao M:1.
  • Muitos-para-Muitos (Many-to-Many): Evite quando possível. Internamente, o AS implementa M:M usando filtros cruzados bidirecional e materializações intermediárias, que são caras para consultas complexas. Se você precisa de M:M, prefira usar uma tabela de ponte (bridge/junction table) com relacionamentos M:1 explícitos dos dois lados.

Filtragem Bidirecional: Use com Cautela

A filtragem bidirecional (Cross Filter Direction = Both) permite que filtros aplicados em uma tabela se propaguem para ambas as direções do relacionamento, em vez de apenas da dimensão para o fato. Parece útil, mas tem implicações sérias:

  • Ambiguidade em modelos complexos: Em modelos com múltiplos caminhos entre tabelas, a filtragem bidirecional pode criar loops de filtragem que retornam resultados incorretos ou erros de "relacionamento ambíguo".
  • Impacto em performance de consultas: O DAX precisa resolver a propagação de filtros em ambas as direções, o que aumenta a complexidade do plano de consulta.
  • Alternativa recomendada: Use a função DAX CROSSFILTER() dentro de medidas específicas onde o comportamento bidirecional é necessário, em vez de habilitá-lo globalmente no relacionamento. Assim você tem controle preciso sobre quando o cross-filter acontece.
-- Medida DAX usando CROSSFILTER() para bidirecional apenas quando necessário
Clientes com Compras Categoria X =
CALCULATE(
    DISTINCTCOUNT(FatoVendas[ClienteKey]),
    CROSSFILTER(FatoVendas[ProdutoKey], DimProduto[ProdutoKey], Both),
    DimProduto[Categoria] = "Eletrônicos"
)

EncodingHint: Controle Avançado de Compressão de Colunas

O EncodingHint é uma das otimizações mais poderosas e menos conhecidas do VertiPaq. Ele sugere ao motor como codificar e armazenar os dados de uma coluna específica. Existe por coluna e pode ser configurado via Tabular Editor.

Os Dois Tipos de Encoding do VertiPaq

Internamente, o VertiPaq usa dois algoritmos de codificação:

  • Value Encoding: O valor original (numérico) é armazenado diretamente no forward index, após uma transformação linear simples (multiplicar por um fator e somar um offset para que todos os valores caibam no menor inteiro possível). É extremamente eficiente para colunas numéricas com valores que crescem linearmente (IDs sequenciais, datas como YYYYMMDD, quantidades, valores monetários sem muitas casas decimais). O acesso é O(1), dado o row ID, você vai direto ao valor sem consultar dicionário.
  • Hash Encoding: Um dicionário de valores únicos é criado, e o forward index armazena apenas o índice no dicionário (um inteiro pequeno). É mais eficiente para strings e para números com alta cardinalidade e distribuição irregular. O acesso requer uma lookup no dicionário, mas o dicionário é altamente comprimido.

Por padrão, o AS tenta usar Value Encoding para números e Hash Encoding para strings. No entanto, para colunas numéricas com alta cardinalidade e valores irregulares (por exemplo, valores decimais de preços com muitas casas diferentes), o AS pode errar a escolha e usar Hash quando Value seria melhor, ou vice-versa. Isso piora a compressão e o tempo de processamento.

Quando Forçar Value vs. Hash Encoding

  • Force Value: Para chaves numéricas inteiras (IDs, surrogate keys), datas no formato YYYYMMDD, horas no formato HHMM. Colunas onde os valores são contínuos ou têm uma densidade alta.
  • Force Hash: Para strings com alta cardinalidade (nomes de produtos, descrições, emails). Para números decimais com muita variação nos valores (preços com desconto, coordenadas GPS).

Para configurar via script C# no Tabular Editor:

// Forçar Value Encoding em todas as colunas Int64 (chaves inteiras)
foreach (var col in Model.AllColumns.Where(c => c.DataType == DataType.Int64))
{
    col.EncodingHint = EncodingHint.Value;
}

// Forçar Hash Encoding em todas as colunas String com cardinalidade > 10.000
// (requer VertiPaq Analyzer para obter cardinalidade, aqui usamos heurística pelo nome)
foreach (var col in Model.AllColumns.Where(c => c.DataType == DataType.String))
{
    col.EncodingHint = EncodingHint.Hash;
}

// Salvar as alterações
Model.SaveChanges();
Info("EncodingHint configurado com sucesso em " + Model.AllColumns.Count() + " colunas.");

Resultado: Em modelos com muitas chaves inteiras sendo encodadas como Hash (o que acontece quando o AS erra a heurística durante o primeiro processamento e persiste o encoding errado), forçar Value Encoding pode reduzir o tamanho do modelo em 30–50% e acelerar o processamento e as consultas significativamente. Wow!!

O EncodingHint é uma sugestão, não uma obrigação. O AS pode ignorá-la se determinar que o outro tipo é mais eficiente para aquela coluna específica com os dados reais. Por isso se chama "Hint" (dica). Na prática, o AS respeita o hint na grande maioria dos casos.

Colunas Calculadas vs. Medidas DAX: Impacto no Processamento

Essa distinção tem impacto direto no tempo de processamento e no consumo de memória:

  • Colunas Calculadas: São calculadas em tempo de processamento e armazenadas fisicamente no modelo (em memória), como qualquer outra coluna importada. Aumentam o tamanho do modelo, aumentam o tempo de processamento (Process Recalc recalcula todas elas) e podem usar muita memória se forem complexas ou de alta cardinalidade. Use quando você precisa filtrar, agrupar ou criar relacionamentos baseados no resultado do cálculo.
  • Medidas: São calculadas em tempo de consulta, no momento em que o relatório é gerado. Não armazenam dados. Não impactam o processamento. Use medidas para qualquer cálculo que não precise ser usado como chave de filtro ou relacionamento.

ATENÇÃO: Um erro comum é usar colunas calculadas para cálculos que poderiam ser medidas. Por exemplo, criar uma coluna calculada Margem% que calcula [VlrVenda] - [CustoVenda] para cada linha. Isso calcula e armazena o valor para cada uma das milhões de linhas da tabela de fatos, quando uma medida Margem% = [Total Vendas] - [Total Custo] faria o mesmo de forma muito mais eficiente, calculando apenas para os dados do contexto atual do relatório.

Gerenciando Segmentos: A Fragmentação do VertiPaq

Internamente, o VertiPaq divide cada coluna em blocos de dados chamados segmentos. O tamanho ideal de um segmento é de 8 milhões de linhas. Quando você faz muitas operações de Process Add incrementais ao longo do tempo, o VertiPaq cria muitos segmentos pequenos (um para cada Process Add), o que degrada a performance de scan e compressão.

Para visualizar a fragmentação de segmentos:

-- Verificar fragmentação de segmentos por coluna
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;

Colunas com muitos segmentos pequenos devem ser desfragmentadas com Process Defrag na tabela ou partição correspondente. Um Process Full na partição também resolve a fragmentação, reconstruindo todos os segmentos do zero.

Tabelas de Agregação: A Técnica Mais Poderosa para Modelos com Bilhões de Linhas

Tabelas de agregação (Aggregations) são a funcionalidade mais poderosa, mais ignorada e mais mal documentada do Analysis Services moderno. Se você trabalha com modelos que têm tabelas de fato de bilhões de linhas e não conhece esse recurso, esse pode ser o trecho mais valioso desse artigo inteiro.

A ideia central: você cria uma tabela pré-agregada com poucos milhões de linhas (totais por dia, por produto, por região), importa ela no modelo, e configura o mapeamento de correspondência com a tabela de detalhe. A partir daí, o AS roteia automaticamente cada consulta DAX para a tabela de agregação quando a granularidade permite, sem que você mude uma linha de código DAX ou um relatório Power BI. Para consultas que precisam de granularidade de transação, o AS vai para a tabela de detalhe como fallback.

Como o AS Usa Agregações Automaticamente

O roteamento é feito em tempo de consulta pelo Storage Engine do VertiPaq. O AS avalia se a query pode ser respondida com a tabela de agregação verificando:

  • Todos os campos GROUP BY da consulta estão mapeados na tabela de agregação?
  • Todos os campos de medida (SUM, COUNT, MIN, MAX) têm colunas pré-calculadas correspondentes na agregação?
  • Não há filtros em campos que não existem na agregação (o que exigiria detalhes)?

Se todas as condições forem atendidas, o AS usa a agregação (dezenas de milhões de vezes mais rápido para modelos grandes). Se uma condição falhar, ele cai automaticamente para a tabela de detalhe. Esse comportamento é completamente transparente para quem escreveu as medidas DAX e para quem usa os relatórios.

Criando uma Tabela de Agregação na Fonte

O primeiro passo é criar a tabela pré-calculada na fonte. Ela precisa conter as dimensões pelo nível de granularidade desejado e as métricas pré-somadas:

-- Tabela de agregacao: totais diarios por produto, cliente e regiao
-- FatoVendas tem 5 bilhoes de linhas; FatoVendasAgr tera ~10 milhoes
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;

-- Atualizar a tabela de agregacao apos cada carga do ETL:
-- Truncate + reinsert ou Merge incremental, dependendo do volume
TRUNCATE TABLE dbo.FatoVendasAgr;
INSERT INTO dbo.FatoVendasAgr
SELECT ... (mesma query acima)

Configurando o Mapeamento de Agregação via TMSL

Após importar FatoVendasAgr como tabela normal no modelo, configure a propriedade alternateOf em cada coluna para indicar qual coluna da tabela de detalhe ela representa e qual função de resumo 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"
      }
    }
  ]
}

No Tabular Editor 3, a interface gráfica na aba Aggregations de cada tabela permite configurar os mapeamentos sem editar TMSL manualmente. O Tabular Editor 2 (open source) exige a edição via scripts C# ou diretamente no model.bim.

O Padrão Mais Poderoso: Composite Model (DirectQuery + Import)

Agregar uma tabela de Import com outra de Import já ajuda, mas o verdadeiro ganho acontece com o Composite Model:

  • Tabela de detalhe (FatoVendas): Modo DirectQuery. Zero consumo de RAM no modelo. Sempre atualizada em tempo real. Respondida apenas quando a consulta precisa de granularidade de transação.
  • Tabela de agregação (FatoVendasAgr): Modo Import. Pequena o suficiente para caber em memória (10 milhões de linhas em vez de 5 bilhões). Reprocessada em minutos. Responde à grande maioria das consultas analíticas.

Com esse setup, você elimina completamente o processamento de bilhões de linhas de detalhe. O processamento passa a ser apenas da agregação e das dimensões, que são muito menores. O usuário recebe performance de Import para 90%+ das consultas e dados frescos via DirectQuery quando necessário, tudo automaticamente.

Verificando se a Agregação Está Sendo Usada

No DAX Studio, com Server Timings habilitado, execute uma consulta e observe:

  • Coluna "Direct Query": Mostra 0 ms para consultas respondidas inteiramente pela agregação em Import. Se aparecer tempo de DQ, a consulta não foi resolvida pelo agregado.
  • Query Plan: Procure pelo nó AggregationHit (usou o agregado, resposta rápida) versus DirectQueryHit (foi para a fonte, mais lento).

Se uma consulta que deveria usar a agregação está indo para o DirectQuery, os motivos mais comuns são: a query filtra por uma coluna não mapeada na agregação, a granularidade da consulta é mais fina do que a da agregação, ou o mapeamento alternateOf está incorreto.

Você pode ter múltiplas tabelas de agregação em camadas diferentes de granularidade. Por exemplo: FatoVendasAgrDia (totais diários), FatoVendasAgrMes (totais mensais) e FatoVendasAgrAno (totais anuais). O AS usa a mais granular que ainda satisfaz a consulta. Consultas de drill-down vão gradualmente descendo pelas camadas até precisar do detalhe.

Índices Columnstore no Contexto Tabular: VertiPaq é um Columnstore

Quando falamos em otimizar a fonte de dados com Clustered Columnstore Indexes (CCI) no SQL Server, muitas pessoas se perguntam: "Mas o próprio VertiPaq também é um columnstore? Por que precisaria de CCI na origem?"

A resposta é sim, e entender como o VertiPaq funciona internamente explica por que o CCI na origem ainda ajuda, e como as duas tecnologias se complementam.

Como o VertiPaq Armazena Dados Internamente

O VertiPaq é essencialmente um columnstore em memória. Cada coluna é comprimida e armazenada de forma independente das outras. Ao consultar apenas 3 colunas de uma tabela de 100 colunas, o VertiPaq lê fisicamente apenas as 3 colunas necessárias, não varre todas as 100.

Internamente, cada coluna usa dois tipos de índice:

  • Forward Index (Índice Direto): Mapeia Row ID → Valor da coluna. Permite que, dado um Row ID, você obtenha o valor daquela coluna. Usado em operações que precisam materializar valores (SUMX, ADDCOLUMNS, etc.).
  • Inverted Index (Índice Invertido): Mapeia Valor → Lista de Row IDs (bitmap). Permite que, dado um valor (ou intervalo), você obtenha todos os Row IDs que têm aquele valor. Usado em operações de filtro e GROUP BY (CALCULATE, FILTER, SUMMARIZE, etc.).

O Inverted Index usa Bitmap Filter Pushdown, quando múltiplos filtros são combinados (por exemplo, Ano=2024 AND Categoria="Eletrônicos"), o VertiPaq combina os bitmaps dos dois filtros com uma operação AND bitwise, obtendo o conjunto de Row IDs que satisfazem ambos os critérios sem varrer os dados linha por linha. Isso é extremamente eficiente e é o motivo pelo qual modelos tabulares são tão rápidos para filtros multidimensionais.

Por que o CCI na Origem ainda Importa

O VertiPaq armazena os dados em memória. Para preenchê-los, precisa ler os dados da fonte durante o processamento. É nessa leitura que o CCI na origem ajuda:

  • Leitura mais rápida da fonte: O SQL Server com CCI pode executar a query de leitura usando Batch Mode, paralelismo total e pushdown de filtros para o columnstore. O dado chega mais rápido para o AS processar.
  • Compressão on-the-wire: O SQL Server pode transferir os dados já em formato comprimido de columnstore para o AS, reduzindo o tráfego de rede.
  • Menor I/O no SQL Server: O CCI é armazenado comprimido em disco. A leitura de CCI para alimentar o AS gera muito menos I/O no SQL Server do que leitura de um heap ou índice rowstore equivalente.

Alinhamento de Partições VertiPaq com Segmentos Columnstore

Existe um sutil ponto de alinhamento entre as partições do modelo tabular e os Row Groups do CCI. O ideal é que a query de uma partição no AS leia Row Groups completos do CCI, isso ativa a otimização de Row Group Elimination no SQL Server e garante que o dado chegue no melhor formato possível para o VertiPaq construir seus segmentos com tamanho ótimo.

Para isso, projete suas queries de partição no AS para alinhar com os critérios de partição do CCI na origem. Se o CCI usa uma partição por ano/mês, a query de cada partição do AS deve filtrar o mesmo range de data.

Dicas Extras para Alta Performance em Produção

DMVs para Monitoramento de Processamento em Tempo Real

Durante operações de processamento, especialmente em produções críticas, é importante monitorar o progresso. O AS expõe DMVs que permitem isso:

-- Sessões ativas no 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;

-- Jobs de processamento em andamento
SELECT JOB_ID, JOB_DESCRIPTION, JOB_THREADPOOL_ID, JOB_READS, JOB_WRITES,
       JOB_CPU_TIME_MS, JOB_START_TIME
FROM $SYSTEM.DISCOVER_JOBS;

-- Atividade de objetos (processamento por tabela/partição)
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;

Gerenciamento de Memória: Configurações do AS

O Analysis Services expõe um conjunto de parâmetros de memória que controlam como o mecanismo gerencia o VertiPaq, o cache de consultas e a pressão de memória. No SSAS on-premises, esses parâmetros ficam no arquivo msmdsrv.ini e podem ser ajustados via SSMS (botão direito no servidor > Properties > General > Show Advanced Properties). No AAS, alguns desses valores são gerenciados pela plataforma e não podem ser alterados, como o artigo do SQLBI Optimizing memory settings in Analysis Services (Marco Russo) detalha. Entender cada parâmetro é essencial para diagnosticar erros de memória e ajustar o comportamento do servidor:

LowMemoryLimit (padrão: 65%)

Percentual de memória a partir do qual o AS começa a liberar caches proativamente. Abaixo desse limite, o AS mantém todos os dados em cache sem liberar nada. Conforme o uso cresce acima de 65%, o cleaner fica progressivamente mais agressivo até chegar no TotalMemoryLimit, quando despeja tudo que não está em uso ativo.

  • Valor padrão (65%) é adequado para servidores dedicados. Em servidores com mais de 100 GB de RAM dedicados exclusivamente ao AS, considere aumentar para 70–75% para reduzir evicções prematuras de cache.
  • Reduza se outros serviços competem com o AS pela memória do mesmo servidor (múltiplas instâncias SSAS, SQL Server no mesmo host).

TotalMemoryLimit (padrão: 80%)

Quando o uso ultrapassa esse percentual, o gerenciador de memória despeja todos os dados de cache que não estão em uso ativo. No AAS, esse valor depende do plano de serviço e não pode ser alterado. No SSAS on-premises dedicado com muita RAM (>100 GB), pode ser elevado para 90–95%.

HardMemoryLimit (padrão: 0 = automático)

Limite máximo absoluto de memória. Se o AS ultrapassar esse valor, o sistema cancela sessões ativamente para reduzir o consumo, e os usuários recebem erro de pressão de memória. O valor 0 faz o AS calcular automaticamente um valor intermediário entre o TotalMemoryLimit e a memória física total. No AAS, esse valor não pode ser alterado, pois o servidor usa toda a memória física disponível do plano. Para SSAS dedicado, definir 99 ou 100 é adequado.

VertiPaqPagingPolicy (padrão: 0 no AAS, 1 no SSAS)

Controla se o VertiPaq pode fazer paging para disco quando a memória é insuficiente:

  • 0 (paging desabilitado, padrão no AAS): Se não houver memória suficiente para o processamento, ele falha com erro de out-of-memory. O working set do processo é limitado ao VertiPaqMemoryLimit.
  • 1 (paging habilitado, padrão no SSAS): O VertiPaq pode usar mais memória do que a disponível fisicamente, paginando para o pagefile do SO. O processamento conclui, mas com degradação severa de performance se o paging ocorrer.

Atenção ao processamento em memória limitada

Importante: Durante o processamento, o VertiPaq pode precisar de até o dobro do tamanho do banco em memória (dados novos + dados antigos simultaneamente). Se o plano AAS não tem memória suficiente para isso, há duas alternativas: habilitar VertiPaqPagingPolicy = 1 para permitir paging (processamento conclui, mas mais lento), ou fazer backup do banco, limpar com Process Clear e reprocessar do zero (o banco fica indisponível durante o processamento).

QueryMemoryLimit

Limita a memória usada durante a execução de queries. No AAS, esse limite se aplica a toda a memória utilizada por queries DAX e MDX, não apenas às materializações intermediárias como no SSAS 2019. O valor 0 desativa o limite.

  • Reduza esse limite se usuários estão recebendo erros de memória durante queries: isso faz queries pesadas falharem mais cedo, protegendo as demais sessões que têm menor demanda de memória.

VertiPaqMemoryLimit

Define a quantidade de memória que o VertiPaq pode usar. O comportamento varia conforme o VertiPaqPagingPolicy:

  • Com paging desabilitado (0): define o total de memória que o VertiPaq pode bloquear no working set.
  • Com paging habilitado (1): define o limite de memória física que o VertiPaq pina em memória; acima disso, o restante pode ser paginado.
  • Se outros serviços competem com o AS pela memória, definir VertiPaqMemoryLimit abaixo do LowMemoryLimit reduz a pressão do VertiPaq no servidor.

HeapTypeForObjects e MemoryHeapType

Não altere esses valores do padrão (-1). Versões antigas do SSAS (2012/2014) tinham problemas de fragmentação e vazamento de memória que exigiam ajuste manual desses parâmetros. No SSAS 2016 SP1+ e no AAS, o engine escolhe automaticamente o alocador ótimo via o valor -1. Se você fez upgrade de uma instância antiga e esses valores estão diferentes de -1, corrija para -1.

Automatizando o Processamento via Azure Data Factory

O Azure Data Factory (ADF) é a solução de orquestração da Microsoft para pipelines de dados em larga escala, e funciona muito bem para automatizar o ciclo completo: carregar dados na fonte → processar o modelo AAS. Mas é importante saber: o ADF não tem uma activity nativa específica para o Azure Analysis Services. A integração é feita via Web Activity chamando a REST API do AAS, ou via Azure Function Activity executando código PowerShell/C# com o módulo SqlServer.

Padrão REST API: Web Activity + Loop de Status

O AAS expõe uma REST API assíncrona para disparar refreshes. O fluxo no ADF é:

  1. Web Activity (POST) → dispara o refresh e recebe um ID de operação.
  2. Until Activity (loop) → repete até o status ser succeeded ou failed.
  3. Web Activity (GET) dentro do Until → consulta o status da operação pelo ID.
  4. If Condition Activity → verifica o resultado e, em caso de falha, dispara notificação (Logic App, e-mail).

A URL do endpoint de refresh é:

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

# Exemplo de body (Process Full via REST)
{
    "Type": "Full",
    "CommitMode": "transactional",
    "MaxParallelism": 4,
    "RetryCount": 2,
    "Objects": []
}

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

A autenticação da Web Activity deve usar Managed Identity do ADF (conceda a esse identity o papel de Analysis Services Administrator no servidor AAS) ou um Service Principal. Configure na Web Activity: Authentication = MSI, Resource = https://*.asazure.windows.net.

Padrão Azure Function: Processamento Síncrono

Uma alternativa mais simples é usar uma Azure Function Activity que executa PowerShell com o módulo SqlServer. A função bloqueia até o processamento terminar, retornando sucesso ou falha diretamente, sem precisar de loop de polling:

# Azure Function (PowerShell) chamada pelo ADF via Azure Function Activity
param($Request, $TriggerMetadata)

# Autentica com Managed Identity da Function App
# Requer Connect-AzAccount -Identity no profile.ps1 da Function App,
# ou chamado explicitamente aqui:
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 bloqueia ate o processamento concluir (sincrono)
$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'
    })
}

Estrutura do Pipeline no ADF

Um pipeline típico de produção integrado com AAS no ADF tem a seguinte estrutura:

  • Activity 1: Copy Data, extrai dados do source (on-premises, S3, APIs) e carrega na camada de staging (Azure SQL ou Synapse).
  • Activity 2: Stored Procedure / Script, executa a lógica de transformação e prepara as tabelas finais.
  • Activity 3: Azure Function Activity (ou Web Activity + Until Loop), dispara o refresh do modelo AAS e aguarda a conclusão.
  • Activity 4: Web Activity (condicional), notifica o time via Logic App, Teams ou e-mail em caso de falha.

Synapse Pipelines

O Azure Synapse Analytics tem seu próprio orchestrador (Synapse Pipelines), que é baseado no mesmo engine do ADF. O mesmo padrão de Web Activity + REST API do AAS se aplica integralmente. Se você já usa Synapse como plataforma de dados, é natural manter a orquestração dentro do Synapse Pipelines em vez de adicionar uma dependência no ADF.

Scale Up/Down Automático via REST API: O Truque do "Window de Processamento"

O Azure Analysis Services permite que você altere o SKU (tier) via REST API. Isso abre uma estratégia poderosa para reduzir custos:

  1. Durante o dia (horário comercial): Manter o servidor num tier menor (ex: S1 ou S2) suficiente para as consultas dos usuários.
  2. Janela de processamento (2h/dia): Antes do processamento, chamar a API para escalar para um tier maior (ex: S4 ou S8 com mais RAM e CPUs), processar os dados, e logo após escalar de volta para o tier menor.

Dependendo do tempo de processamento e dos volumes de dados, o custo do scale-up temporário pode ser muito menor do que manter o tier alto 24/7. O padrão recomendado é encapsular o scale-up e o scale-down em Azure Functions que chamam a REST API de gerenciamento do Azure, acionadas como Azure Function Activities no ADF: uma antes do processamento (scale-up) e outra no bloco de finalização do pipeline (scale-down), para garantir que o servidor volte ao tier menor mesmo em caso de falha. A Function App precisa ter Managed Identity habilitada e receber o papel Contributor (ou um papel personalizado com permissão Microsoft.AnalysisServices/servers/write) no servidor AAS via Azure RBAC:

# Azure Function (PowerShell) — Scale Up ou Scale Down via REST API de gerenciamento
# Body esperado: { "sku": "S4" } para scale-up | { "sku": "S1" } para scale-down
# Managed Identity da Function App precisa ter papel Contributor no servidor AAS.
param($Request, $TriggerMetadata)

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

# Token via IMDS (Managed Identity da própria 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 alterar o SKU (operação assíncrona — o Azure provisiona o novo 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"

# Poll até provisioningState chegar em Succeeded, Failed ou 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" }
})

No ADF, o scale-down deve estar no bloco Finally do fluxo de controle (ou conectado aos ramos de sucesso e falha do processamento), garantindo que o servidor seja reduzido ao tier menor independentemente do resultado.

DirectQuery Mode: Quando Usar e Quando Evitar

O modo DirectQuery no AAS (e Power BI) não armazena dados em memória, todas as consultas são traduzidas para queries SQL e enviadas diretamente à fonte de dados. Elimina o processamento, mas tem trade-offs sérios:

  • Prós: Dados sempre atuais (sem latência de processamento), sem consumo de RAM do modelo tabular, adequado para tabelas com bilhões de linhas que não cabem em memória.
  • Contras: Performance de consulta depende 100% da fonte de dados. Medidas DAX complexas são difíceis ou impossíveis de otimizar. Alguns recursos DAX não funcionam em DirectQuery. Cada carregamento de página de relatório gera múltiplas queries no banco de dados.
  • Recomendação: Use DirectQuery apenas para tabelas de fato muito grandes que mudam continuamente. Combine com Import Mode para tabelas de dimensão (modo Composto, Composite Model). Nunca use DirectQuery puro em toda a solução sem benchmarking extenso.

XMLA Endpoint e Power BI Premium: Boas Práticas

Para Power BI Premium e Fabric, o XMLA Endpoint abre acesso a todas as capacidades enterprise que antes eram exclusivas do SSAS e AAS:

  • Incremental Refresh com XMLA: O Power BI tem uma feature nativa de Incremental Refresh, mas ela tem limitações. Para controle total, use o XMLA Endpoint com scripts TMSL diretamente, você pode criar e gerenciar partições exatamente como no AAS.
  • Deployment Pipelines via XMLA: Automatize o deploy de modelos entre workspaces (Dev → Homologação → Produção) via scripts TMSL com XMLA, integrando com seu pipeline CI/CD no Azure DevOps ou GitHub Actions.
  • Backup/Restore: Faça backup dos modelos via XMLA para arquivos .abf no Azure Storage, como contingência para rollback de mudanças problemáticas.
  • Ferramentas de Terceiros: Tabular Editor, DAX Studio e VertiPaq Analyzer funcionam nativamente com o XMLA Endpoint do Power BI Premium, os mesmos fluxos de otimização que você usa no AAS se aplicam diretamente.

Backup e Restore: Proteção e Rollback em Minutos

O AS suporta backup do modelo para um arquivo .abf (Analysis Services Backup File), que inclui todos os dados em memória, metadados e partições. O restore de um .abf é, em muitos casos, muito mais rápido do que reprocessar o modelo do zero, especialmente para modelos com dezenas ou centenas de gigabytes.

Para configurar backup automático no AAS, acesse o portal Azure, na blade do servidor AAS, vá em Settings > Backup e aponte para uma conta de Azure Blob Storage. Com o armazenamento configurado, você dispara backups via 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"
  }
}

Cenários onde o restore supera o reprocessamento:

  • Rollback de deploy: Antes de implantar mudanças estruturais no modelo (adição de colunas, mudança de tipos de dados, alteração de relacionamentos), faça backup. Se algo der errado, restaure em minutos em vez de aguardar horas de reprocessamento.
  • Seed de novo servidor: Ao criar um servidor AAS de homologação ou de DR, restaure o backup da produção em vez de reprocessar tudo da fonte. O restore via Azure Blob é muito mais rápido para modelos de 50GB+ do que um Process Full completo.
  • Recuperação de falha de processamento: Se um processamento corrompeu o modelo ou deixou partições em estado inconsistente, restaure o último backup bom e reprocesse apenas as partições afetadas.

Autenticação Segura para Data Sources: Service Account e OAuth

AAS não suporta Managed Identity

Ao contrário de outros serviços Azure, o Azure Analysis Services não suporta Managed Identity — seja para data sources, backup ou qualquer outra operação. Isso está declarado explicitamente na documentação oficial de service principals do AAS: "Analysis Services does not support operations performed by managed identities". O Service Principal é a alternativa para automação sem credencial de usuário.

Para ambientes de produção, nunca use credenciais pessoais ou de administrador no data source do modelo AS. As alternativas suportadas e documentadas são:

  • SQL Service Account (autenticação SQL): Login SQL dedicado com permissão de leitura apenas (db_datareader). É a opção mais simples, funciona em qualquer nível de compatibilidade e é explicitamente documentada pela Microsoft para data sources em nuvem. A documentação oficial de impersonation do AAS especifica: para cloud data sources com SQL auth, a impersonation deve ser Service Account.
  • OAuth / Entra ID (Tabular 1400+, in-memory): Para Azure SQL, Azure Synapse, Dynamics 365 e SharePoint List em modelos 1400+ in-memory, o AAS suporta OAuth credentials gerenciado pelo próprio serviço (inclusive renovação automática de token para evitar timeout em refreshes longos). A configuração é feita via Power Query no Visual Studio ou SSMS → Edit Credentials, usando uma conta com acesso ao source. Para automação com Service Principal, consulte a documentação específica de OAuth credentials do AAS, pois o fluxo de configuração não é um bloco simples de TMSL.

Configurando SQL Service Account

Crie um login SQL dedicado no Azure SQL com permissão mínima de leitura. No modelo AAS, configure o data source com esse login e defina impersonation como Service Account:

-- No Azure SQL: criar login de serviço dedicado com permissão mínima
CREATE LOGIN [aas_svc] WITH PASSWORD = 'senha-forte-aqui';

-- No banco de dados 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';

No SSMS, conectado ao servidor AAS: botão direito no banco → ProcessEdit Settings → aba Data Source Credentials → edite o data source e informe o login e senha do service account. O AAS armazena as credenciais criptografadas no servidor, sem expô-las em scripts ou repositórios.

Service Principal para Automação de Operações no AAS

O uso documentado de Service Principal no AAS é para automatizar operações no próprio servidor (refresh, scale, deploy de modelos) via PowerShell, Azure Automation e outros orquestradores, não como credencial de data source. O SP precisa ser adicionado à role de administrador do servidor ou a uma database role com permissão de Process:

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

# Usando o módulo SqlServer com 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))

O Service Principal para operações no AAS (refresh, deploy) precisa ser adicionado como administrador do servidor via Portal Azure ou SSMS. Atenção: adicionar o SP a um security group e então adicionar o grupo como admin não funciona no AAS — o SP precisa ser adicionado diretamente.

Bom pessoal, espero que vocês tenham gostado desse artigo e que ele tenha sido útil para aprofundar o conhecimento em otimização de processamento no Azure Analysis Services, SSAS Tabular e Power BI Premium.

Cobrimos desde a arquitetura interna do VertiPaq até estratégias avançadas de particionamento, tabelas de agregação, Scale-Out com réplicas de leitura, EncodingHint, gerenciamento de segmentos, automação com PowerShell e AMO, boas práticas de autenticação, e monitoramento em produção.

Tem muito conteúdo aqui, salva esse link, porque é o tipo de coisa que você vai querer consultar várias vezes ao longo da sua jornada.

Grande abraço e até mais!