Jhonatan Pinheiro
Carregando página...
Jhonatan Pinheiro
Carregando página...
Jhonatan Pinheiro
Carregando página...
120 perguntas de entrevista e de bastidor respondidas com exemplo real: Power Query e refresh, modelagem, contexto de avaliação em DAX, SQL que alimenta o modelo, design de dashboard, Figma e métricas de uso.
Este é um post de consulta e revisão: 120 perguntas que aparecem em entrevista, em code review de painel e naquele momento em que o número não bate e você precisa achar a causa. Cada resposta é direta e vem com um exemplo real — nada de definição de manual sem aplicação.
Todos os exemplos usam o mesmo modelo de varejo:
Casa & Construção Nordeste — 42 lojas, 3 canais (loja física, e-commerce, televendas), ~4,2 milhões de linhas de venda por ano. Tabelas:
fVendas(grão = item do cupom),dProduto,dLoja,dCliente,dCalendarioefMetas.
Como usar: leia por bloco (Power Query, modelagem, DAX, SQL, UX/UI, processo) ou use como checklist na véspera de uma entrevista. As perguntas marcadas com 🔥 são as que mais caem — e as que mais derrubam gente que só decorou a sintaxe.
Ferramenta, arquitetura de conexão e a etapa de preparação dos dados.
É a ferramenta gratuita onde você conecta, transforma, modela e desenha o relatório. Não é um banco de dados nem um ETL corporativo: ele não guarda histórico próprio, não orquestra cargas e não substitui um data warehouse. Quando alguém usa o Power BI como repositório de verdade da empresa, o sintoma aparece rápido — arquivos de 800 MB circulando por e-mail com números divergentes.
Desktop faz: conectar, transformar (M), modelar, medir (DAX), desenhar
Desktop não faz: guardar histórico, agendar carga, controlar acesso corporativo
Isso é papel do banco/DW + Power BI Service.Desktop é o aplicativo Windows onde se constrói. Service (app.powerbi.com) é a nuvem onde se publica, agenda atualização, compartilha e controla acesso. Report Server é a versão instalada no servidor da empresa, para quem não pode publicar na nuvem — com recursos mais limitados e ciclo de atualização mais lento.
Constrói -> Desktop (grátis)
Publica -> Service (Pro/PPU/Fabric) ou Report Server (on-premises)
Consome -> navegador, app móvel, Teams, incorporado em outro sistemaPara construir no Desktop, não: é gratuito. A licença Pro (ou PPU/capacidade Fabric) é necessária para publicar e compartilhar com outras pessoas em um workspace. Exceção prática: com capacidade Premium/Fabric, quem só consome pode ter licença gratuita.
Desktop grátis
Publicar em workspace Pro (por usuário)
Consumir em capacidade Premium gratuito para o leitor
Compartilhar .pbix por e-mail funciona, mas é péssima ideia
(sem governança, sem refresh, sem RLS)É o conjunto tabelas + relacionamentos + medidas: a definição de o que os números significam. Separar significa que várias equipes constroem relatórios diferentes sobre a mesma verdade, via conexão dinâmica (Live Connection). Sem isso, cada área cria a sua medida de faturamento e a reunião vira debate sobre qual planilha está certa.
1 modelo semântico "Vendas Corporativo"
<- relatório Diretoria (Live Connection)
<- relatório Comercial (Live Connection)
<- relatório Loja (Live Connection)
Faturamento é definido UMA vez. Todo mundo lê o mesmo número.Import copia os dados para a memória — rápido, com DAX completo; é o padrão para 90% dos casos. DirectQuery consulta a fonte a cada interação: use só quando a exigência de tempo real for real, ou quando o dado não puder sair da origem. Live Connection conecta a um modelo semântico já publicado (ou Analysis Services). Dual é para dimensões pequenas em modelo composto.
Pergunta que decide:
"Atualizar de hora em hora resolve o problema de negócio?"
SIM -> Import (rápido, barato, DAX completo)
NÃO -> DirectQuery (lento, DAX limitado, carga na origem)
Live Connection -> reaproveitar um modelo corporativo já publicadoPor causa do VertiPaq, o motor colunar em memória: ele armazena por coluna, não por linha, e comprime com dicionário — a categoria Tintas, repetida 4 milhões de vezes, vira um número apontando para uma entrada única. Consequência prática: colunas de baixa cardinalidade comprimem muito, e colunas de alta cardinalidade (ID de cupom, timestamp com segundos) são as que incham o modelo.
O que incha o modelo (em ordem):
1. coluna de alta cardinalidade (CupomID, GUID, datetime com segundos)
2. colunas de texto longas (descrição completa do produto)
3. colunas que você não usa e nem lembra que importou
Truque: separe data e hora em duas colunas.
DataHora com segundos = milhões de valores distintos.
Data (3.650 valores) + Hora (1.440) comprime muito melhor.Aponte para uma view com as colunas e o período certos, ou escreva a consulta na conexão. Marcar a tabela na lista e clicar em Carregar traz tudo — 60 colunas e 12 anos — para responder sobre 24 meses.
Obter Dados > SQL Server > Opções Avançadas > Instrução SQL
SELECT DataVenda, LojaSK, ProdutoSK, Qtd, ValorLiquido, Custo
FROM dbo.vw_fato_vendas
WHERE DataVenda >= '2024-01-01';É o Power Query traduzir seus passos em uma única consulta SQL e deixar o servidor executar. Enquanto acontece, filtrar 4 milhões de linhas custa quase nada; quando quebra, o Power BI baixa tudo e filtra na sua máquina. Verifique com botão direito no passo → Exibir Consulta Nativa: se estiver esmaecido, quebrou ali.
Faz folding: filtrar linhas, remover colunas, agrupar, renomear,
mesclar entre tabelas do mesmo servidor
Quebra folding: Table.Buffer, índice personalizado, funções M sem
equivalente SQL, mesclar fontes diferentes
Estratégia: coloque TODOS os passos que fazem folding primeiro.M roda no refresh e define como o dado chega: estrutura, tipo, limpeza. DAX roda na hora do clique e define o que o número significa naquele filtro. Sinal clássico de confusão: uma coluna chamada Total do Ano criada no Power Query — ela não muda quando o usuário filtra março, e todo mundo acha que o painel está errado.
M (Power Query) DAX
roda no refresh roda a cada interação
resultado fixo resultado depende do filtro
prepara a tabela calcula o indicador
linguagem funcional linguagem de expressões analíticasLocale. O arquivo foi gerado em pt-BR (vírgula decimal) e o Power Query interpretou como en-US, tratando a vírgula como separador de milhar. Não gera erro visível — só um faturamento cem vezes maior. Tipe explicitamente informando a cultura, ou use Usando Localidade na caixa de tipo.
= Table.TransformColumnTypes(
Fonte,
{{"ValorLiquido", type number}, {"DataVenda", type date}},
"pt-BR"
)Unpivot. Selecione as colunas que devem permanecer (a chave) e use Transformar Colunas em Linhas > Transformar Outras Colunas em Linhas. Escolher Outras é o detalhe que importa: quando a planilha ganhar a coluna de um mês novo, a consulta continua funcionando.
// Antes: Loja | Jan | Fev | Mar (planilha de metas)
// Depois: Loja | Mes | MetaValor
= Table.UnpivotOtherColumns(Fonte, {"Loja"}, "Mes", "MetaValor")Anexar (append) empilha tabelas de mesma estrutura: vendas de 2025 + vendas de 2026, mais linhas. Mesclar (merge) é o JOIN: traz colunas de outra tabela pela chave, mais colunas. No star schema, mesclar a dimensão dentro da fato quase sempre é erro — a ligação certa é um relacionamento.
Anexar -> mesmas colunas, mais linhas
Mesclar -> mesmas linhas, mais colunas
Tipos de mesclagem (junção):
Externa Esquerda = LEFT JOIN (o padrão, e o que você quer 90% das vezes)
Interna = INNER JOIN
Anti Esquerda = o que existe aqui e NÃO existe lá (ótimo para auditar)Conecte-se à pasta, não a cada arquivo. O Power Query cria uma função de exemplo a partir do primeiro arquivo e a aplica a todos. Se a regra de limpeza mudar, você edita a função uma vez. Vale filtrar a extensão antes — um arquivo temporário do Excel (~$) na pasta quebra o refresh.
= let
Pasta = Folder.Files("\\servidor\bi\inventario"),
SoXlsx = Table.SelectRows(Pasta, each
Text.EndsWith([Name], ".xlsx")
and not Text.StartsWith([Name], "~$")),
Limpo = Table.AddColumn(SoXlsx, "Dados", each
fxLimpaInventario([Content]))
in
LimpoPara tirar do código o que muda entre ambientes ou execuções: nome do servidor, banco, caminho de pasta, quantidade de meses. Um parâmetro de servidor permite promover de homologação para produção sem reeditar consulta nenhuma — e, no Service, ele é alterável sem republicar o arquivo.
// Gerenciar Parâmetros > Novo
pServidor : Texto = "srv-bi.casaeconstrucao.local"
pBanco : Texto = "DW_Vendas"
let
Fonte = Sql.Database(pServidor, pBanco)
in
FonteVocê define uma janela: o histórico fica arquivado em partições que não são recarregadas, e só os últimos N dias são atualizados. Exige uma coluna de data/hora na origem e os parâmetros RangeStart e RangeEnd — com esses nomes exatos, tipo Data/Hora — usados como filtro. Refresh de 40 minutos costuma cair para menos de 2.
= Table.SelectRows(Fonte, each
[DataVenda] >= RangeStart and [DataVenda] < RangeEnd)
// Modelagem > Atualização incremental
// Arquivar dados iniciando 3 anos antes da data de atualização
// Atualizar incrementalmente os dados iniciando 10 dias antes
Cuidado: o filtro precisa ser >= e < (intervalo semiaberto),
senão a linha da virada do dia entra em duas partições.Quase sempre porque o Service não enxerga a fonte ou não tem a credencial. No Desktop, você usa sua conta do Windows e está dentro da rede; no Service, quem executa é o serviço, que precisa de gateway para fonte on-premises e de uma credencial configurada no modelo semântico. Outras causas frequentes: caminho de arquivo local (C:) e uso de fontes que não suportam refresh na nuvem.
Checklist quando quebra só no Service:
[ ] fonte é on-premises? -> gateway instalado, online e com a fonte cadastrada
[ ] credencial configurada em Configurações do semântico > Credenciais
[ ] caminho de rede (\\servidor\pasta) em vez de C:\Users\voce\...
[ ] nível de privacidade das fontes compatível (Organizacional x Privado)8 por dia no Power BI Pro e até 48 em capacidade Premium/PPU/Fabric. Se o negócio pede intervalo menor, as saídas são DirectQuery, atualização por API/pipeline (que também respeita o limite do plano) ou repensar a real necessidade — na prática, quase toda diretoria decide bem com dado de D-1.
Pro 8 refreshes/dia (intervalo mínimo ~3 h na prática)
Premium por usuário 48 refreshes/dia (a cada 30 min)
Capacidade/Fabric 48 + refresh via API/pipeline
Pergunte sempre: quem decide com esse número, e com que frequência?Salvando como .pbip (Power BI Project): modelo e relatório viram arquivos de texto (TMDL/JSON), então o diff mostra qual medida mudou. Com .pbix, cada commit é um binário novo — o Git guarda, mas você não consegue revisar nada.
Arquivo > Salvar como > Projeto do Power BI (.pbip)
meu-painel.Dataset/model.bim <- medidas, tabelas, relacionamentos
meu-painel.Report/report.json <- páginas, visuais, posições
git diff mostra:
- Margem % = DIVIDE([Margem R$], [Faturamento Bruto])
+ Margem % = DIVIDE([Margem R$], [Faturamento Líquido])Sim, quase sempre. Ligada, ela cria uma tabela de datas oculta para cada coluna de data do modelo: memória desperdiçada, hierarquias duplicadas e time intelligence imprevisível. Desligue e crie uma única dCalendario, marcada como tabela de data.
Arquivo > Opções > Arquivo Atual > Carregamento de Dados
[ ] Data/hora automática <- desmarcar
Modelo com 8 colunas de data:
ligada -> 8 tabelas ocultas, uma por coluna
correto -> 1 dCalendario, relacionada e marcada como tabela de dataNa ordem de impacto: remova colunas não usadas, corte o período, quebre datetime em data + hora, troque colunas calculadas por medidas e evite importar tabelas de apoio que só serviram para uma conferência. Use o DAX Studio > VertiPaq Analyzer para ver, em números, qual coluna está custando memória.
DAX Studio > Advanced > View Metrics
Exemplo real (modelo de 480 MB):
fVendas[CupomID] 182 MB <- alta cardinalidade, ninguém usava
fVendas[DataHoraVenda] 96 MB <- virou Data + Hora
fVendas[ObsVendedor] 54 MB <- texto livre, sem uso no painel
Depois de remover as três: 120 MB.A medida tem o ícone de calculadora e só existe quando é usada num visual; a coluna tem o ícone de coluna e ocupa memória em toda linha da tabela. Regra prática: se o campo vai para o eixo, legenda ou filtro, precisa ser coluna; se vai para o valor, deve ser medida.
Eixo / Legenda / Segmentação -> coluna (dProduto[Categoria])
Valores -> medida ([Faturamento])
Erro comum: arrastar fVendas[ValorLiquido] direto para Valores.
Funciona (soma implícita), mas você perde controle de formatação,
de nome e de reuso — e não dá para referenciar em outra medida.Um recurso nativo que deixa o usuário trocar a medida ou a dimensão de um visual por uma segmentação. Substitui aquele conjunto de três gráficos quase iguais e reduz a página. É criado em Modelagem > Novo parâmetro > Campos.
// Modelagem > Novo parâmetro > Campos
Métricas = {
("Faturamento", NAMEOF('Medidas'[Faturamento]), 0),
("Margem R$", NAMEOF('Medidas'[Margem R$]), 1),
("Unidades", NAMEOF('Medidas'[Unidades]), 2)
}
Arraste "Métricas" para uma segmentação e para o eixo de valores.Pela guia Formato > Editar interações: para cada visual de origem, você define se o alvo filtra, realça ou ignora. É o que evita o comportamento irritante de clicar numa barra e ver os KPIs do topo mudarem quando eles deveriam mostrar o contexto geral.
Selecione o gráfico de lojas > Formato > Editar interações
KPI Faturamento -> Nenhum (mantém a visão total)
Gráfico de linha -> Realçar (destaca sem esconder o resto)
Tabela detalhe -> Filtrar (é o objetivo do clique)Publique num workspace (não compartilhe o arquivo), organize o conteúdo e distribua como app. O app é o que o usuário deveria abrir: tem navegação nomeada, público controlado e não expõe rascunho. Workspace é a bancada de trabalho; app é a vitrine.
Desenvolvimento (workspace)
-> Publicar
Workspace "Vendas [PROD]"
-> Criar app
App "Vendas" -> público: grupo do AD "Gestores Loja"
Nunca: enviar o .pbix por e-mail ou WhatsApp
(sem refresh, sem RLS, sem controle de versão).Relatório tem páginas, visuais e interação — é o que você constrói no Desktop. Dashboard (painel) é a tela de blocos fixados, de um ou vários relatórios, com um só nível de detalhe. App é o pacote publicado que agrupa relatórios e painéis para um público. No dia a dia, a maior parte do trabalho está no relatório.
Relatório páginas, filtros, drill, interação (Desktop -> Service)
Dashboard blocos fixados, 1 tela, sem filtro (só no Service)
App empacota e distribui para um público (só no Service)A camada onde nasce a maior parte dos problemas que aparecem disfarçados de erro de DAX.
É o modelo com uma tabela fato ao centro (números e chaves) cercada por dimensões (descrições e hierarquias), ligadas por relacionamentos um-para-muitos. Importa porque o motor do Power BI foi otimizado para exatamente essa forma: filtros propagam por caminho único, a compressão é melhor e o DAX fica simples. Quase toda medida complicada demais é sintoma de modelo errado.
dCalendario
|
dLoja -- fVendas -- dProduto
|
dCliente
fVendas : DataVenda, LojaSK, ProdutoSK, ClienteSK, Qtd, Valor, Custo
dimensões : os textos pelos quais você filtra e agrupaFato responde quanto e cresce com o tempo (uma linha por evento: venda, chamado, movimentação). Dimensão responde quem, o quê, onde, quando e cresce devagar (uma linha por entidade). Teste rápido: se somar a coluna não significa nada — CEP, código do produto —, ela é atributo de dimensão, mesmo sendo numérica.
Fato: 1 linha por item de cupom, 4,2 mi de linhas/ano
Dimensão: 1 linha por produto, 38 mil linhas, muda pouco
Métrica sempre na fato. Atributo sempre na dimensão.Funciona para um protótipo com poucas linhas, mas cobra caro depois: a descrição do produto se repete milhões de vezes (modelo grande), filtros ficam lentos, não há como listar o produto que não vendeu e qualquer correção de cadastro exige recarregar a fato inteira.
Achatado (4,2 mi linhas):
... | Categoria | Marca | DescricaoProduto | Cidade | Regional | ...
repetido repetido repetido repetido
Star schema:
fVendas guarda ProdutoSK (um número)
dProduto guarda a descrição UMA vez, em 38 mil linhasÉ o significado de uma linha da fato — no nosso caso, um item de cupom. Você pode sempre agregar para cima (dia, mês, região), mas nunca descer abaixo do grão carregado. Carregar já agregado por dia e loja resolve performance hoje e mata para sempre a pergunta qual produto puxou a queda.
Grão item do cupom -> responde: produto, cupom, vendedor, hora
Grão dia + loja -> responde: só dia e loja
Antes de agregar na origem, pergunte:
"alguém vai precisar abrir esse número?"
Se a resposta for talvez, mantenha o grão fino.1:* é o padrão: um produto, muitas vendas. 1:1 é raro e quase sempre indica duas tabelas que deveriam ser uma. : resolve casos legítimos (orçamento por categoria x vendas por produto), mas cria ambiguidade e blank rows — prefira resolver com uma dimensão-ponte antes de recorrer a ele.
1:* dProduto[ProdutoSK] -> fVendas[ProdutoSK] padrão saudável
1:1 raro; junte as tabelas
*:* use ponte:
fMetas[CategoriaSK] -> dCategoria[CategoriaSK] <- dProduto[CategoriaSK]
|
fVendasÉ o filtro que propaga nos dois sentidos: da dimensão para a fato e de volta. Resolve casos específicos, mas cria caminhos ambíguos em modelos com várias dimensões, deixa o motor mais lento e produz totais que ninguém consegue explicar. Quando precisar do efeito, prefira ativá-lo dentro da medida, com CROSSFILTER.
// Em vez de deixar o relacionamento bidirecional no modelo:
Clientes que Compraram Tinta =
CALCULATE(
DISTINCTCOUNT( dCliente[ClienteSK] ),
dProduto[Categoria] = "Tintas",
CROSSFILTER( fVendas[ClienteSK], dCliente[ClienteSK], BOTH )
)
Efeito local, controlado, documentado na medida.Porque a coluna de data da fato só tem os dias em que houve venda, e as funções de time intelligence precisam de uma sequência contínua de datas cobrindo anos inteiros. Sem isso, comparativos com o ano anterior falham silenciosamente — e uma dimensão de data também dá a você ano, trimestre, semana e feriado sem recalcular nada.
dCalendario =
VAR MinData = MIN( fVendas[DataVenda] )
VAR MaxData = MAX( fVendas[DataVenda] )
RETURN
ADDCOLUMNS(
CALENDAR( DATE( YEAR(MinData), 1, 1 ), DATE( YEAR(MaxData), 12, 31 ) ),
"Ano", YEAR([Date]),
"MesNum", MONTH([Date]),
"MesNome", FORMAT([Date], "mmm"),
"AnoMes", FORMAT([Date], "yyyy-mm"),
"Trimestre", "T" & QUARTER([Date])
)O Power BI passa a tratar aquela coluna como o eixo temporal oficial: as funções de time intelligence removem automaticamente os outros filtros da tabela de data ao deslocar o período, e as hierarquias automáticas param de interferir. Sem marcar, TOTALYTD e SAMEPERIODLASTYEAR podem devolver resultado errado — sem aviso.
Selecione dCalendario > Ferramentas de Tabela > Marcar como tabela de data
Coluna de data: Date
Requisitos: valores únicos, sem nulos, contínuos e cobrindo anos inteiros.Um relacionamento ativo (o que você usa mais) e outro inativo, ligado sob demanda com USERELATIONSHIP. A alternativa — duas tabelas de calendário (role-playing dimensions) — é útil quando o usuário precisa filtrar por ambas ao mesmo tempo, em segmentações distintas.
// Relacionamento ativo: fVendas[DataVenda] -> dCalendario[Date]
// Relacionamento inativo: fVendas[DataEntrega] -> dCalendario[Date]
Entregas =
CALCULATE(
[Faturamento],
USERELATIONSHIP( fVendas[DataEntrega], dCalendario[Date] )
)Uma tabela sem nenhum relacionamento, usada como fonte de escolha para o usuário: cenário de simulação, seleção de métrica, faixas de valor. A medida lê o que foi selecionado com SELECTEDVALUE e reage. É a base de todo what-if no Power BI.
ReajustePreco = GENERATESERIES( 0, 0.20, 0.01 ) // 0% a 20%
Faturamento Simulado =
VAR Reajuste = SELECTEDVALUE( ReajustePreco[Valor], 0 )
RETURN
[Faturamento] * ( 1 + Reajuste )O Power BI só aceita relacionamento por uma coluna. Quando a chave real é composta (loja + mês, no caso das metas), crie uma coluna concatenada nos dois lados — de preferência no SQL ou no Power Query, não como coluna calculada, para não pagar memória à toa.
-- No SQL, dos dois lados:
SELECT CONCAT(LojaSK, '|', FORMAT(DataMeta, 'yyyy-MM')) AS ChaveLojaMes, ...
// No modelo:
fMetas[ChaveLojaMes] *---1 dLojaMes[ChaveLojaMes]
Alternativa: criar a dimensão que falta (dLojaMes) e relacionar as duas
fatos a ela — mais limpo e evita a concatenação.É a dimensão cujo atributo muda com o tempo: a loja Recife mudou de regional em 2025. Tipo 1 sobrescreve (a história some, tudo vira a regional nova). Tipo 2 cria uma linha nova com vigência, e a fato aponta para a versão vigente na data da venda — é o que preserva a verdade histórica. A escolha é de negócio, não técnica.
Tipo 2 em dLoja:
LojaSK | LojaID | Nome | Regional | DataIni | DataFim | Atual
17 | 042 | Recife | Norte | 2020-01-01 | 2024-12-31 | N
118 | 042 | Recife | Nordeste | 2025-01-01 | 9999-12-31 | S
Pergunta que decide: "a venda de 2023 deve aparecer na regional
antiga ou na atual?" A resposta define tipo 1 ou tipo 2.Achate. Normalizar dProduto em produto → subcategoria → categoria (snowflake) adiciona saltos de relacionamento, deixa o filtro mais lento e complica o DAX. Dimensões são pequenas: repetir o nome da categoria em 38 mil linhas não custa nada perto do ganho de simplicidade.
Snowflake (evite):
fVendas -> dProduto -> dSubcategoria -> dCategoria
Star (prefira):
fVendas -> dProduto [SKU, Descrição, Subcategoria, Categoria, Marca]
Exceção: dimensão gigantesca (dezenas de milhões de linhas),
onde a normalização passa a compensar.São duas fatos de grãos diferentes; nunca as relacione entre si. Ligue as duas às mesmas dimensões, no nível em que cada uma existe: a meta se relaciona com o mês e com a loja, a venda com o dia e com a loja. As medidas se encontram no visual, não no modelo.
dCalendario --1---* fVendas (por dia)
dCalendario --1---* fMetas (pelo primeiro dia do mês)
dLoja --1---* ambas
Atingimento % = DIVIDE( [Faturamento], [Meta] )
Cuidado ao mostrar por dia: a meta é mensal.
Ou rateie a meta por dia útil, ou só exiba a comparação no nível mensal
(use HASONEVALUE / ISINSCOPE para decidir).Oculte todas as chaves e colunas técnicas, oculte colunas numéricas cruas que já viraram medida, renomeie tudo em linguagem de negócio, organize medidas em pastas de exibição e defina a formatação padrão de cada medida (moeda, percentual, casas decimais). Um modelo com 12 itens visíveis é usável; com 140, o usuário volta para o Excel.
Checklist de entrega do modelo
[ ] chaves (SK) ocultas
[ ] colunas cruas da fato ocultas (ValorLiquido, Custo)
[ ] nomes em português de negócio ("Faturamento", não "vlr_liq")
[ ] medidas em pastas: Vendas / Rentabilidade / Comparativos / Metas
[ ] formato definido por medida (R$, %, 0 ou 1 casa)
[ ] descrição preenchida nas medidas principais (aparece no tooltip)Do primeiro SUM ao contexto de avaliação — o bloco que mais separa quem decorou sintaxe de quem entende a linguagem.
DAX (Data Analysis Expressions) opera sobre tabelas e colunas inteiras dentro de um contexto de filtro, não sobre células. No Excel, A1+B1 sempre soma as mesmas duas células; em DAX, [Faturamento] devolve um valor diferente em cada célula do visual, conforme os filtros daquela célula. É isso que faz a mesma medida servir ao gráfico anual e ao detalhe por loja.
Excel: = SOMA(B2:B5000) resultado fixo
DAX: Faturamento = SUM( fVendas[ValorLiquido] )
no total -> 18.437.219
em "Recife" -> 412.980
mesma fórmula, contexto diferenteColuna calculada é avaliada no refresh, gravada linha a linha e ocupa memória — use quando precisar do valor para filtrar, agrupar ou colocar num eixo. Medida é avaliada na hora, conforme o filtro, e não ocupa memória — use para tudo que é indicador. Na dúvida, comece por medida.
// Coluna: serve de eixo/filtro
dProduto[FaixaPreco] =
SWITCH( TRUE(),
dProduto[PrecoLista] >= 500, "Alto",
dProduto[PrecoLista] >= 150, "Medio",
"Baixo"
)
// Medida: indicador que reage ao filtro
Margem % = DIVIDE( [Margem R$], [Faturamento] )É o conjunto de filtros ativos no momento em que a medida é avaliada: o que vem da linha e da coluna do visual, das segmentações, dos filtros de página e de relatório, dos relacionamentos e de qualquer CALCULATE no caminho. Cada célula de uma matriz tem o seu, e a medida é executada uma vez por célula.
Matriz: linhas = dLoja[Nome], colunas = dCalendario[MesNome]
Célula "Recife" x "Mar":
dLoja[Nome] = "Recife"
dCalendario[MesNome] = "Mar"
+ segmentação de Canal, se houver
+ filtro de página (ex.: Ano = 2026)
[Faturamento] roda dentro desse conjunto.É a existência de uma linha atual. Aparece em dois lugares: dentro de uma coluna calculada (que percorre a tabela) e dentro de um iterador (SUMX, AVERAGEX, FILTER). Sem contexto de linha, você não pode referenciar uma coluna sem agregá-la — daí o erro clássico não é possível determinar um valor único para a coluna.
// Tem contexto de linha (iterador): OK
SUMX( fVendas, fVendas[Qtd] * fVendas[PrecoUnitario] )
// Não tem: erro
Medida Errada = fVendas[Qtd] * 2
// Correto sem iterador:
Medida Certa = SUM( fVendas[Qtd] ) * 2É o que acontece quando CALCULATE (ou uma medida, que já tem CALCULATE implícito) é chamado dentro de um contexto de linha: a linha atual vira filtro. É por isso que SUMX(dLoja, [Faturamento]) funciona — a cada loja, o contexto de filtro passa a ser aquela loja. É o conceito mais poderoso e o mais confuso da linguagem.
// A cada linha de dLoja, [Faturamento] é filtrado por aquela loja
Lojas Acima da Meta =
COUNTROWS(
FILTER( dLoja, [Faturamento] > [Meta] )
)
// Sem transição de contexto, [Faturamento] devolveria
// o total geral em todas as linhas.Sempre que o cálculo tem de acontecer antes da soma, linha a linha. Preço vezes quantidade é o exemplo canônico: somar preços e multiplicar pela soma das quantidades dá um número sem sentido. SUMX percorre, calcula e só então soma.
// Certo
Receita Bruta = SUMX( fVendas, fVendas[Qtd] * fVendas[PrecoUnitario] )
// Errado (e o total até parece plausível)
Receita Errada = SUM( fVendas[Qtd] ) * SUM( fVendas[PrecoUnitario] )
Se já existe a coluna com o resultado da multiplicação,
SUM dessa coluna é mais rápido que SUMX.Avalia uma expressão modificando o contexto de filtro: adiciona filtros, substitui os existentes naquela coluna e mantém os demais. Além disso, provoca transição de contexto quando usado dentro de um contexto de linha. Praticamente toda medida não trivial passa por ela.
Faturamento E-commerce =
CALCULATE( [Faturamento], dLoja[Canal] = "E-commerce" )
// O filtro SUBSTITUI o de Canal e MANTÉM os de data, loja, produto.
// Se o usuário já filtrou Canal = "Loja física" na segmentação,
// esta medida ainda mostra e-commerce — é o comportamento esperado.O filtro simples (coluna = valor) é açúcar sintático para um FILTER sobre os valores daquela coluna: rápido e suficiente na maioria dos casos. FILTER explícito é necessário quando a condição envolve uma medida ou compara colunas diferentes. Cuidado com a tabela escolhida: FILTER(fVendas, ...) percorre milhões de linhas.
// Simples (preferível quando dá)
CALCULATE( [Faturamento], dProduto[Categoria] = "Tintas" )
// FILTER: obrigatório, porque compara com medida
CALCULATE( [Faturamento], FILTER( dLoja, [Margem %] < 0.20 ) )
// Ruim: itera a fato inteira
CALCULATE( [Faturamento], FILTER( fVendas, RELATED(dProduto[Categoria]) = "Tintas" ) )ALL/REMOVEFILTERS removem filtros da tabela ou coluna indicada (total geral). ALLEXCEPT remove tudo menos as colunas listadas. ALLSELECTED respeita o que o usuário escolheu nas segmentações e ignora apenas o filtro do próprio visual — é o certo para participação dentro da seleção.
% do Total Geral = DIVIDE([Faturamento], CALCULATE([Faturamento], ALL(dProduto)))
% dentro da Marca = DIVIDE([Faturamento], CALCULATE([Faturamento], ALLEXCEPT(dProduto, dProduto[Marca])))
% do Selecionado = DIVIDE([Faturamento], CALCULATE([Faturamento], ALLSELECTED(dProduto)))
Usuário selecionou 3 de 12 categorias:
ALL -> as 3 somam 41%
ALLSELECTED -> as 3 somam 100%Divida a medida por ela mesma com o filtro da dimensão removido. A pergunta que define qual função usar é: o denominador deve considerar a seleção do usuário? Se sim, ALLSELECTED; se é sempre o total da empresa, ALL/REMOVEFILTERS.
% Participação Categoria =
VAR Atual = [Faturamento]
VAR TotalSelecionado =
CALCULATE( [Faturamento], ALLSELECTED( dProduto[Categoria] ) )
RETURN
DIVIDE( Atual, TotalSelecionado )SAMEPERIODLASTYEAR desloca o período inteiro do contexto em um ano; DATEADD permite deslocamentos livres. Sempre exiba a variação junto do valor — o número absoluto do ano passado sozinho raramente ajuda alguém a decidir.
Faturamento AA =
CALCULATE( [Faturamento], SAMEPERIODLASTYEAR( dCalendario[Date] ) )
Var % AA =
VAR Anterior = [Faturamento AA]
RETURN
IF( NOT ISBLANK( Anterior ), DIVIDE( [Faturamento] - Anterior, Anterior ) )Com as funções TOTALYTD, TOTALQTD e TOTALMTD, que acumulam do início do período até a última data do contexto. Se o ano fiscal não começa em janeiro, informe o encerramento no terceiro argumento.
Faturamento YTD = TOTALYTD( [Faturamento], dCalendario[Date] )
Faturamento QTD = TOTALQTD( [Faturamento], dCalendario[Date] )
Faturamento MTD = TOTALMTD( [Faturamento], dCalendario[Date] )
// Ano fiscal encerrando em 30 de junho
Fat YTD Fiscal = TOTALYTD( [Faturamento], dCalendario[Date], "06-30" )Três causas, nessa ordem: a tabela de datas não foi marcada como tabela de data; ela tem buracos ou não cobre anos inteiros; ou você está usando a coluna de data da fato em vez da coluna da dimensão calendário. Uma quarta, mais sutil: filtro de mês aplicado sobre a fato impede o acumulado de enxergar os meses anteriores.
Diagnóstico rápido
[ ] dCalendario marcada como tabela de data?
[ ] CALENDAR cobre de 01/01 do 1º ano a 31/12 do último?
[ ] a medida usa dCalendario[Date], não fVendas[DataVenda]?
[ ] o filtro de período está na dCalendario, não na fVendas?Com AVERAGEX sobre DATESINPERIOD, ancorado na última data do contexto. Serve para tirar a serrilha da série mensal e mostrar tendência — no varejo, sem isso o pico de dezembro domina a leitura do gráfico inteiro.
Média Móvel 3M =
AVERAGEX(
DATESINPERIOD( dCalendario[Date], MAX( dCalendario[Date] ), -3, MONTH ),
[Faturamento]
)Com CALCULATE mais um filtro de datas menores ou iguais à data atual, liberando o filtro da tabela de datas com ALL. Cuidado: sem o ALL, o filtro do visual limita o intervalo e o acumulado reinicia a cada linha.
Acumulado =
CALCULATE(
[Faturamento],
FILTER(
ALL( dCalendario[Date] ),
dCalendario[Date] <= MAX( dCalendario[Date] )
)
)Porque a tabela passada como referência já está filtrada pelo contexto da linha, então cada loja é ranqueada contra uma tabela de uma loja só. A solução é remover esse filtro com ALL (ou ALLSELECTED, se quiser ranquear só dentro da seleção do usuário).
// Errado: 1 para todos
Posição = RANKX( dLoja, [Faturamento] )
// Certo
Posição Loja = RANKX( ALL( dLoja[Nome] ), [Faturamento], , DESC, DENSE )
// Ranking respeitando a seleção do usuário
Posição na Seleção = RANKX( ALLSELECTED( dLoja[Nome] ), [Faturamento], , DESC, DENSE )Calcule o total do Top N com TOPN dentro de CALCULATE e obtenha Outros por diferença. Assim o gráfico fica legível (10 barras, não 42) sem esconder o restante do faturamento.
Fat Top 10 =
CALCULATE( [Faturamento], TOPN( 10, ALL( dLoja[Nome] ), [Faturamento], DESC ) )
Fat Outros =
CALCULATE( [Faturamento], ALL( dLoja ) ) - [Fat Top 10]Tabela desconectada com as opções + SWITCH lendo SELECTEDVALUE. Sempre defina um valor padrão no SELECTEDVALUE, senão a tela nasce vazia quando nada está selecionado — um dos erros de UX mais comuns em painéis com seletor.
Métrica Escolhida =
SWITCH(
SELECTEDVALUE( Métricas[Nome], "Faturamento" ), // padrão!
"Faturamento", [Faturamento],
"Margem R$", [Margem R$],
"Unidades", [Unidades],
BLANK()
)Crie uma medida de texto e ligue-a ao título do visual pelo botão fx (Formato > Título > Baseado no campo). É o que faz um print de tela enviado no WhatsApp continuar compreensível fora do contexto do relatório.
Título Página =
VAR Loja = SELECTEDVALUE( dLoja[Nome], "todas as lojas" )
VAR Periodo = SELECTEDVALUE( dCalendario[AnoMes], "o período selecionado" )
RETURN
"Faturamento de " & Loja & " em " & PeriodoUse DIVIDE, que trata denominador zero ou vazio e ainda deixa você escolher o resultado alternativo. Pense no resultado que faz sentido para o negócio: em margem %, vazio é mais honesto que zero; em atingimento de meta, zero costuma ser o certo.
Margem % = DIVIDE( [Margem R$], [Faturamento] ) // vazio se não houve venda
Atingimento % = DIVIDE( [Faturamento], [Meta], 0 ) // zero faz sentido
// Evite:
Margem Errada = [Margem R$] / [Faturamento] // Infinito / erro no visualPorque a medida é recalculada no contexto do total, e não somada. Isso é correto e esperado em razões (margem %, ticket médio) e em contagens distintas. Vira problema quando a medida tem lógica condicional que se comporta diferente no total — aí, controle o que exibir com HASONEVALUE ou ISINSCOPE.
Loja A: ticket 210 | Loja B: ticket 190 | Total: 204
Não é 400, e está certo: o total é faturamento total / cupons totais.
// Quando o total realmente não faz sentido:
Meta por Loja = IF( HASONEVALUE( dLoja[Nome] ), [Meta], BLANK() )
// Quando o total precisa ser a soma dos itens:
Total Correto = SUMX( VALUES( dLoja[Nome] ), [Medida com lógica] )Porque contagem distinta não é aditiva: quem comprou em janeiro e em março é um cliente no total, e dois se você somar os meses. Não é bug. Se precisar de um número aditivo, mude a definição (clientes novos no mês, por exemplo) e explique isso no glossário do relatório.
Clientes Ativos = DISTINCTCOUNT( fVendas[ClienteSK] )
Jan 1.200 | Fev 1.350 | Mar 1.410 | Trimestre 2.480
Alternativa aditiva:
Clientes Novos =
CALCULATE(
DISTINCTCOUNT( fVendas[ClienteSK] ),
FILTER( dCliente, dCliente[DataPrimeiraCompra] IN VALUES( dCalendario[Date] ) )
)Para avaliar uma expressão uma vez e reutilizá-la, o que melhora performance e legibilidade. Detalhe que cai em prova: a variável guarda o valor do contexto onde foi declarada — um CALCULATE posterior não a altera.
Var % AA =
VAR Atual = [Faturamento]
VAR Anterior = CALCULATE( [Faturamento], SAMEPERIODLASTYEAR( dCalendario[Date] ) )
RETURN
DIVIDE( Atual - Anterior, Anterior )
// Anterior foi calculado ANTES; nenhum CALCULATE depois muda seu valor.IF para uma condição; SWITCH(TRUE(), ...) para três ou mais, porque fica bem mais legível que IF aninhado. Em ambos, cuide para que todos os ramos devolvam o mesmo tipo — misturar texto e número numa medida gera comportamento estranho no visual.
Classificação =
SWITCH( TRUE(),
[Atingimento %] >= 1, "Meta batida",
[Atingimento %] >= 0.9, "Perto",
[Atingimento %] >= 0.7, "Atenção",
"Crítico"
)RELATED traz um valor do lado um do relacionamento (da venda para o produto): use no contexto de linha da fato. RELATEDTABLE traz a tabela do lado muitos (do produto para as vendas): use no contexto de linha da dimensão, normalmente com um agregador.
// Na fato, buscar atributo da dimensão
fVendas[Categoria] = RELATED( dProduto[Categoria] )
// Na dimensão, contar as linhas da fato
dProduto[QtdVendas] = COUNTROWS( RELATEDTABLE( fVendas ) )Não dá para usar filtro simples: é preciso FILTER sobre a dimensão, porque a condição depende de uma medida avaliada por linha (transição de contexto). Filtre a dimensão, nunca a fato — a diferença de performance é de ordens de grandeza.
Fat Lojas Margem Baixa =
CALCULATE(
[Faturamento],
FILTER( ALL( dLoja[Nome] ), [Margem %] < 0.20 )
)
Qtd Lojas Margem Baixa =
COUNTROWS( FILTER( ALL( dLoja[Nome] ), [Margem %] < 0.20 ) )Crie uma função em Modelagem > Gerenciar Funções com um filtro DAX na dimensão, e associe usuários ou grupos a ela no Service. Use USERPRINCIPALNAME() para amarrar ao usuário logado. Teste sempre com Exibir como, no Desktop, antes de publicar.
// Função "Gerente Loja", filtro na tabela dLoja:
[EmailGerente] = USERPRINCIPALNAME()
// Hierarquia (regional vê todas as lojas da sua regional):
VAR Usuario = USERPRINCIPALNAME()
RETURN
dLoja[Regional] IN
SELECTCOLUMNS(
FILTER( dAcesso, dAcesso[Email] = Usuario ),
"R", dAcesso[Regional]
)
// Teste: Modelagem > Exibir como > Gerente Loja + outro usuárioIsole por partes: coloque as medidas intermediárias numa tabela ao lado da dimensão suspeita e veja em que nível o número desanda. Ferramentas: EVALUATE no DAX Studio, o Analisador de Desempenho (que mostra a consulta DAX gerada pelo visual) e a exibição das variáveis uma a uma.
// No DAX Studio
EVALUATE
SUMMARIZECOLUMNS(
dLoja[Nome],
"Faturamento", [Faturamento],
"Faturamento AA", [Faturamento AA],
"Var %", [Var % AA]
)
ORDER BY [Var %] ASC
// Onde o valor some, está a causa: filtro, relacionamento ou grão.Selecione a medida e use as Ferramentas de Medida (formato, casas decimais, separador de milhar). Defina no modelo, não visual a visual — assim ela nasce formatada em qualquer relatório. Para casos especiais, existe a string de formato dinâmica.
Ferramentas de Medida
Faturamento -> Moeda, 0 casas R$ 18.437.219
Margem % -> Percentual, 1 casa 23,7%
Ticket Médio -> Moeda, 2 casas R$ 214,38
// String de formato dinâmica (mesma medida, moedas diferentes):
SWITCH( SELECTEDVALUE( dMoeda[Codigo] ), "BRL", "R$ #,##0", "USD", "$ #,##0" )Em medidas, prefira SUMMARIZE apenas para agrupar e ADDCOLUMNS para acrescentar cálculos — agregar dentro de SUMMARIZE tem armadilhas de contexto conhecidas. SUMMARIZECOLUMNS é a função das consultas (DAX Studio, tabelas virtuais), não do dia a dia das medidas.
// Padrão recomendado
VAR PorLoja =
ADDCOLUMNS(
SUMMARIZE( fVendas, dLoja[Nome] ),
"@Fat", [Faturamento]
)
RETURN
COUNTROWS( FILTER( PorLoja, [@Fat] > 500000 ) )
// Prefixo @ nas colunas criadas: convenção que evita ambiguidade.Marque o dia útil como coluna na dCalendario (considerando fim de semana e a tabela de feriados) e conte com CALCULATE + COUNTROWS. Fazer isso na dimensão, uma vez, é muito melhor que recalcular em cada medida.
// Coluna em dCalendario
dCalendario[EhDiaUtil] =
IF(
WEEKDAY( dCalendario[Date], 2 ) <= 5
&& NOT( dCalendario[Date] IN VALUES( dFeriados[Data] ) ),
1, 0
)
// Medida
Dias Úteis = CALCULATE( COUNTROWS( dCalendario ), dCalendario[EhDiaUtil] = 1 )
Meta Diária = DIVIDE( [Meta], [Dias Úteis] )Quatro regras que resolvem a maioria dos casos: filtre dimensões, nunca a fato; evite FILTER sobre tabelas grandes; use variáveis para não repetir cálculo; e prefira colunas de baixa cardinalidade nos filtros. Meça com o Analisador de Desempenho — otimizar por intuição costuma piorar a legibilidade sem ganho real.
// Lento: itera 4,2 milhões de linhas
CALCULATE([Faturamento], FILTER(fVendas, RELATED(dProduto[Categoria])="Tintas"))
// Rápido: filtra 38 mil linhas (ou usa o filtro simples)
CALCULATE([Faturamento], dProduto[Categoria] = "Tintas")
Meta: consulta DAX abaixo de 1 s por visual.Cinco campeões: usar SUM onde precisa de SUMX; esquecer ALL no RANKX; achar que total de razão deveria somar; usar / em vez de DIVIDE; e não saber explicar a diferença entre ALL e ALLSELECTED. Todos são de contexto, não de sintaxe — por isso decorar função não passa na entrevista.
Se você souber responder isto, passou da maior parte:
1. "Explique o que CALCULATE faz."
2. "Por que o total da margem % não é a soma das linhas?"
3. "Qual a diferença entre ALL e ALLSELECTED?"
4. "Quando uma coluna calculada é melhor que uma medida?"
5. "O que é transição de contexto?"A camada invisível que decide se o painel atualiza em 2 minutos ou em 2 horas — e se o número está certo.
Para montar um relatório simples, não. Para trabalhar de verdade, sim: é o SQL que corta o volume na origem, que permite auditar o número do painel contra a fonte e que resolve em segundos o que no Power Query levaria minutos. Na prática, a vaga que pede Power BI e SQL junto está dizendo que você vai mexer no dado antes de ele virar visual.
O mínimo que se cobra numa entrevista de BI:
SELECT / WHERE / ORDER BY / GROUP BY / HAVING
os quatro JOINs e por que o LEFT duplica
window functions (RANK, LAG, SUM OVER)
CTE
ler um plano de execução no básicoNo SQL: filtrar, juntar tabelas, agregar pesado e tudo que o servidor faz melhor. No Power Query: moldar o formato (unpivot, tipos, cabeçalho), tratar arquivos e o que não existe em SQL. A regra é empurrar o trabalho para o mais perto da fonte — inclusive porque o SQL é versionável e testável, e passos do Power Query não são.
Origem SQL -> filtro, JOIN, GROUP BY, janela, incremental
Power Query -> tipos + locale, unpivot, pasta de arquivos, merge leve
Modelo (DAX) -> tudo que muda conforme o filtro do usuárioPorque a view é um contrato: nome de negócio, colunas certas e regras já aplicadas. Quando a tabela física mudar de nome ou ganhar coluna, você conserta em um lugar e os 30 relatórios continuam funcionando. Também é onde ficam as exclusões que ninguém deve esquecer — loja de teste, cupom cancelado.
CREATE VIEW dbo.vw_fato_vendas AS
SELECT
v.DataVenda, v.LojaSK, v.ProdutoSK,
v.Quantidade AS Qtd,
v.ValorLiquido,
v.CustoUnitario * v.Quantidade AS Custo
FROM dbo.FatoVendasItem v
WHERE v.LojaSK <> 999
AND v.StatusCupom = 'FECHADO';WHERE filtra linhas antes do agrupamento; HAVING filtra grupos depois de agregar. Por isso não se usa função de agregação no WHERE. Regra de performance: tudo que puder ser filtrado no WHERE deve ser — quanto menos linhas chegarem ao GROUP BY, melhor.
SELECT LojaSK, SUM(ValorLiquido) AS Faturamento
FROM dbo.vw_fato_vendas
WHERE DataVenda >= '2026-01-01' -- filtra linhas
GROUP BY LojaSK
HAVING SUM(ValorLiquido) > 500000; -- filtra gruposINNER traz só o que casa dos dois lados. LEFT mantém tudo da tabela da esquerda, com nulo onde não há correspondência — é o mais usado em BI, porque a fato precisa sobreviver a um cadastro faltante. RIGHT é o mesmo invertido (raro; prefira reescrever como LEFT). FULL traz os dois lados, útil para conciliação.
-- Vendas com produto sem cadastro: LEFT preserva a venda
SELECT v.DataVenda, v.ValorLiquido, COALESCE(p.Categoria, 'Sem cadastro') AS Categoria
FROM dbo.vw_fato_vendas v
LEFT JOIN dbo.dProduto p ON p.ProdutoSK = v.ProdutoSK;
-- Auditoria: o que existe na fato e não existe na dimensão
SELECT DISTINCT v.ProdutoSK
FROM dbo.vw_fato_vendas v
LEFT JOIN dbo.dProduto p ON p.ProdutoSK = v.ProdutoSK
WHERE p.ProdutoSK IS NULL;Porque o lado direito tem mais de uma linha para a mesma chave: cada linha da fato foi multiplicada. É o erro que mais infla número em BI. Causas típicas: dimensão com histórico (SCD tipo 2) sem filtro de vigência, cadastro duplicado, ou junção por uma chave que não é única.
-- Diagnóstico: a chave é única mesmo?
SELECT ProdutoSK, COUNT(*)
FROM dbo.dProduto
GROUP BY ProdutoSK
HAVING COUNT(*) > 1;
-- Causa comum: SCD tipo 2 sem filtro de vigência
JOIN dbo.dLoja l
ON l.LojaID = v.LojaID
AND v.DataVenda BETWEEN l.DataIni AND l.DataFim; -- faltava istoListe antes as perguntas que o painel responde e agregue no grão mais fino entre elas. Se alguma pergunta cita produto, o grão precisa ter produto. Na dúvida, mantenha o grão fino e resolva performance no modelo — desagregar depois exige recarregar tudo.
SELECT
v.DataVenda, v.LojaSK, p.Categoria,
SUM(v.Qtd) AS Qtd,
SUM(v.ValorLiquido) AS ValorLiquido,
SUM(v.Custo) AS Custo
FROM dbo.vw_fato_vendas v
JOIN dbo.dProduto p ON p.ProdutoSK = v.ProdutoSK
GROUP BY v.DataVenda, v.LojaSK, p.Categoria;
-- 4,2 mi -> 380 mil linhas. Só faça se ninguém pedir SKU.Com window functions. ROW_NUMBER numera sem empate (bom para deduplicar), RANK deixa buraco após empate e DENSE_RANK não. PARTITION BY define o grupo dentro do qual o ranking recomeça — no exemplo, um ranking por mês.
SELECT
AnoMes, LojaSK, Faturamento,
ROW_NUMBER() OVER (PARTITION BY AnoMes ORDER BY Faturamento DESC) AS Linha,
RANK() OVER (PARTITION BY AnoMes ORDER BY Faturamento DESC) AS Posicao,
DENSE_RANK() OVER (PARTITION BY AnoMes ORDER BY Faturamento DESC) AS PosicaoDensa
FROM dbo.vw_vendas_mes_loja;Com SUM() OVER e uma janela de linhas. ROWS UNBOUNDED PRECEDING acumula do início da partição até a linha atual — reiniciando a cada ano, se o ano fizer parte do PARTITION BY.
SELECT
LojaSK, AnoMes, Faturamento,
SUM(Faturamento) OVER (
PARTITION BY LojaSK, LEFT(AnoMes, 4)
ORDER BY AnoMes
ROWS UNBOUNDED PRECEDING
) AS AcumuladoAno
FROM dbo.vw_vendas_mes_loja;Com LAG deslocando 12 linhas dentro da partição da loja — desde que a série seja completa, sem meses faltando. Se houver buracos, junte com a própria tabela pela chave de ano-mês calculada, que é imune à ausência de linhas.
SELECT
LojaSK, AnoMes, Faturamento,
LAG(Faturamento, 12) OVER (PARTITION BY LojaSK ORDER BY AnoMes) AS FatAA,
Faturamento - LAG(Faturamento, 12) OVER (PARTITION BY LojaSK ORDER BY AnoMes) AS Delta
FROM dbo.vw_vendas_mes_loja;
-- Série com buracos? Prefira o self join por chave de período.É uma consulta nomeada que existe só durante o comando (WITH ... AS). Serve para quebrar uma consulta longa em etapas legíveis e para recursão (hierarquia de gerentes, explosão de estrutura de produto). Não é tabela temporária: não guarda nada nem cria índice.
WITH VendasMes AS (
SELECT LojaSK, FORMAT(DataVenda,'yyyy-MM') AS AnoMes, SUM(ValorLiquido) AS Fat
FROM dbo.vw_fato_vendas
GROUP BY LojaSK, FORMAT(DataVenda,'yyyy-MM')
),
ComMeta AS (
SELECT v.*, m.MetaValor
FROM VendasMes v
LEFT JOIN dbo.fMetas m ON m.LojaSK = v.LojaSK AND m.AnoMes = v.AnoMes
)
SELECT *, Fat / NULLIF(MetaValor, 0) AS Atingimento
FROM ComMeta;Com uma CTE recursiva ou uma tabela de números. Ter o calendário no banco (e não só em DAX) é melhor: outros relatórios, outras ferramentas e o próprio ETL passam a usar a mesma definição de trimestre, semana e feriado.
WITH Datas AS (
SELECT CAST('2020-01-01' AS date) AS Data
UNION ALL
SELECT DATEADD(DAY, 1, Data) FROM Datas WHERE Data < '2030-12-31'
)
SELECT
Data,
YEAR(Data) AS Ano,
MONTH(Data) AS MesNum,
FORMAT(Data, 'yyyy-MM') AS AnoMes,
DATEPART(QUARTER, Data) AS Trimestre,
CASE WHEN DATEPART(WEEKDAY, Data) IN (1,7) THEN 0 ELSE 1 END AS EhDiaUtil
FROM Datas
OPTION (MAXRECURSION 0);Com PIVOT ou, mais portável, com CASE dentro de agregações condicionais. Para BI, porém, pense duas vezes: o modelo prefere dado alto (uma linha por canal), porque assim uma categoria nova não exige mexer na consulta nem no visual. Pivotar é para relatório de conferência, não para alimentar o modelo.
-- Agregação condicional (funciona em qualquer banco)
SELECT
LojaSK,
SUM(CASE WHEN Canal = 'Loja' THEN ValorLiquido ELSE 0 END) AS FatLoja,
SUM(CASE WHEN Canal = 'E-commerce' THEN ValorLiquido ELSE 0 END) AS FatEcom,
SUM(CASE WHEN Canal = 'Televendas' THEN ValorLiquido ELSE 0 END) AS FatTele
FROM dbo.vw_fato_vendas
GROUP BY LojaSK;
-- Para o modelo, prefira alto: LojaSK | Canal | ValorLiquidoNULL não é zero nem vazio: é ausência, e qualquer comparação com ele dá desconhecido. Use IS NULL, COALESCE para valor padrão e NULLIF para evitar divisão por zero. Em dimensão, troque nulo por um rótulo explícito — some da tela é pior do que aparecer como Sem categoria.
-- Errado: nunca retorna nada
WHERE Categoria = NULL
-- Certo
WHERE Categoria IS NULL
SELECT
COALESCE(NULLIF(LTRIM(RTRIM(Categoria)), ''), 'Sem categoria') AS Categoria,
ValorLiquido / NULLIF(Qtd, 0) AS PrecoMedio
FROM dbo.vw_fato_vendas;Leia o plano de execução procurando varredura de tabela (table/clustered index scan) onde deveria haver busca, e observe a diferença entre linhas estimadas e reais — discrepância grande costuma ser estatística desatualizada. Depois cheque o básico: função sobre a coluna filtrada, que impede o uso do índice.
SET STATISTICS IO, TIME ON;
-- executar a consulta e ler leituras lógicas
-- Mata índice (função na coluna):
WHERE YEAR(DataVenda) = 2026
-- Usa índice (intervalo, coluna intacta):
WHERE DataVenda >= '2026-01-01' AND DataVenda < '2027-01-01'Um índice pela coluna de filtro (a data), com as colunas do SELECT em INCLUDE, para que o servidor resolva tudo no índice sem voltar à tabela (covering index). Meça antes e depois; índice demais deixa a carga do ETL mais lenta.
CREATE NONCLUSTERED INDEX IX_FatoVendas_Data
ON dbo.FatoVendasItem (DataVenda)
INCLUDE (LojaSK, ProdutoSK, Quantidade, ValorLiquido, CustoUnitario);
-- Antes: 4min12s | Depois: 47s (mesma consulta de refresh)Agrupe pela chave de negócio (o que deveria ser único) e conte. Duplicidade em fato costuma vir de carga executada duas vezes ou de JOIN mal feito no ETL — e é a explicação mais frequente para o painel mostrar faturamento maior que o do sistema de origem.
SELECT CupomID, ItemSeq, COUNT(*) AS Vezes
FROM dbo.FatoVendasItem
GROUP BY CupomID, ItemSeq
HAVING COUNT(*) > 1
ORDER BY Vezes DESC;
-- Conferência rápida do total:
SELECT COUNT(*) AS Linhas, COUNT(DISTINCT CONCAT(CupomID,'-',ItemSeq)) AS Unicos
FROM dbo.FatoVendasItem;Rode a mesma agregação no banco e compare com o visual, no mesmo período e com os mesmos filtros. Diferença tem quase sempre uma destas quatro causas: filtro extra na view, período diferente, duplicidade na carga ou relacionamento errado no modelo, deixando linhas órfãs de fora.
-- No banco
SELECT FORMAT(DataVenda,'yyyy-MM') AS AnoMes, SUM(ValorLiquido) AS Fat
FROM dbo.vw_fato_vendas
WHERE DataVenda >= '2026-01-01' AND DataVenda < '2026-04-01'
GROUP BY FORMAT(DataVenda,'yyyy-MM')
ORDER BY 1;
-- No painel: matriz AnoMes x [Faturamento], sem nenhuma segmentação.
-- Guarde esse par como teste de regressão do relatório.Grave uma marca d'água (a última data carregada) numa tabela de controle e processe apenas o delta, dentro de uma transação. É o mesmo princípio do refresh incremental do Power BI, só que sob seu controle — e permite reprocessar um período específico quando a origem corrige lançamento retroativo.
BEGIN TRANSACTION;
DECLARE @UltimaCarga datetime =
(SELECT MAX(DataCarga) FROM dbo.ControleCarga WHERE Tabela = 'FatoVendasItem');
INSERT INTO dbo.FatoVendasItem (...)
SELECT ... FROM origem.Vendas WHERE DataAlteracao > @UltimaCarga;
UPDATE dbo.ControleCarga
SET DataCarga = GETDATE()
WHERE Tabela = 'FatoVendasItem';
COMMIT;As decisões visuais que determinam se o painel é usado sozinho ou vira e-mail pedindo explicação.
É projetar para a decisão, não para a tela: entender quem usa, que pergunta precisa responder, em quanto tempo e o que faz depois. Na prática, aparece em três coisas: hierarquia (o que aparece primeiro), clareza (dá para entender sem legenda) e caminho (como se chega ao detalhe e como se volta).
Sem UX: "coloquei todos os indicadores que pediram"
Com UX: "esta tela responde 3 perguntas, nesta ordem de importância,
e leva ao detalhe em 2 cliques"
Sintoma de painel sem UX: o usuário exporta para o Excel
antes de tomar qualquer decisão.Pelas perguntas, escritas em uma frase cada, ordenadas por importância — e por quem vai usar. Só depois vêm o esboço, os visuais e a cor. Começar arrastando gráfico é o caminho mais rápido para uma tela cheia que não responde nada.
Briefing em 5 linhas (preencha com o cliente)
1. Quem usa: gerentes de loja e o diretor comercial
2. Quando: toda segunda de manhã, e no fechamento do mês
3. Perguntas: (a) bati a meta? (b) o que caiu? (c) onde agir?
4. Decisão: realocar verba e cobrar plano de ação da loja
5. Restrições: dado D-1, acesso só à própria loja (RLS)Entre 4 e 6 no bloco principal, e no máximo 8 a 12 visuais na página. O limite não é estético: cada visual é uma consulta, e páginas com 20 visuais demoram para carregar e para ser lidas. Se não cabe, provavelmente são dois públicos diferentes — e devem ser duas páginas.
Regra prática
KPIs no topo: 4 a 6
Visuais na página: até 12 (acima disso, a página fica lenta)
Páginas: uma por público/pergunta, não uma por tabela
Se você precisa rolar a página, o layout já falhou.Escolha pela intenção, não pelo gosto. Comparar categorias: barras (horizontais, ordenadas por valor). Evoluir no tempo: linha. Compor um todo: barra empilhada. Relacionar duas medidas: dispersão. Distribuir: histograma. Um número contra a meta: cartão com variação.
Pergunta Visual
Qual loja vendeu mais? barras horizontais ordenadas
Como evoluiu no ano? linha (com média móvel)
Quanto cada canal representa? barra 100% empilhada
Margem alta compensa volume? dispersão (margem x volume)
Bati a meta? cartão + variação + indicador de meta
Onde estão as lojas? mapa (só se a geografia importa)Com 2 ou 3 fatias, sim. Acima disso, não: o olho compara ângulos muito pior do que comprimentos, e fatias parecidas ficam indistinguíveis. Barras ordenadas respondem à mesma pergunta melhor. E rosca com 12 fatias e legenda lateral é, na prática, uma tabela mal desenhada.
Aceitável: Pago x Não pago (2 fatias)
Ruim: 12 categorias de produto em pizza
Melhor: barras horizontais ordenadas, top 8 + "Outros"
Nunca: pizza 3D. Ela distorce a área das fatias da frente.O mínimo que resolve. A maior parte da tela deve ser neutra (cinza), com cor reservada para o que precisa de atenção. Séries categóricas acima de 6 cores ficam ilegíveis — agrupe em Outros. E fixe o significado: se e-commerce é azul, é azul em todas as páginas do relatório.
Regra 60-30-10
60% neutro (fundo, texto, elementos de apoio)
30% cor de apoio (a série principal)
10% destaque (o que exige ação)
Categórica: até 6 cores
Sequencial: 1 matiz, variando intensidade (volume)
Divergente: 2 polos + neutro no meio (variação vs. ano anterior)Cerca de 8% dos homens têm deficiência na visão de cores; vermelho x verde é justamente o par mais problemático. Prefira azul x laranja para oposição, garanta contraste de 4,5:1 no texto e 3:1 em elementos gráficos, e nunca informe apenas por cor: some seta, sinal ou rótulo.
Em vez de verde/vermelho:
positivo #2B6CB0 (azul) negativo #C05621 (laranja)
+ seta e sinal: v +8,2% ^ -3,1%
Teste rápido: tire a saturação da tela (print em preto e branco).
Se a informação sumiu, ela dependia só da cor.Mostrar a tela por 5 segundos, escondê-la e perguntar o que a pessoa viu, se o resultado está bom ou ruim e o que faria agora. Se ela não souber, falta hierarquia — não falta dado. Cinco pessoas bastam: quando três erram a mesma coisa, o problema é o design.
Perguntas, nesta ordem
1. O que essa tela está te dizendo?
2. O resultado está bom ou ruim?
3. O que você faria com essa informação?
4. O que ficou confuso?
Custa 10 minutos por pessoa e evita semanas de retrabalho.Numa faixa fixa, sempre no mesmo lugar em todas as páginas: topo (horizontal) ou lateral esquerda. O usuário precisa ver, de relance, o que está filtrado — filtro espalhado ou escondido em painel retrátil é a principal causa de gente lendo o número errado sem perceber.
Topo (recomendado para 2 a 4 filtros):
[ Período v ] [ Regional v ] [ Canal v ] [ Limpar filtros ]
Lateral esquerda (5+ filtros): largura fixa de 240 px
Sempre visível: um texto com a seleção atual
"Mostrando: Nordeste · E-commerce · Mar/2026"Combine três canais: símbolo (seta ou triângulo), sinal (+/-) e cor. Quem enxerga cores lê mais rápido; quem não enxerga continua entendendo. Vale também para tabela: adicione a coluna com o ícone, não pinte só o fundo da célula.
Bom formato de variação:
v -3,1% (queda) ^ +8,2% (alta)
Formatação condicional por ícones no Power BI:
Formato > Formatação condicional > Ícones > Baseado no campo
Nunca: só a célula vermelha, sem número nem símbolo.Na visão executiva, abrevie (R$ 18,4 mi): centavos não mudam decisão. Na tabela de trabalho, use o valor completo com 2 casas. Números sempre alinhados à direita, texto à esquerda, mesma quantidade de casas na coluna toda. E cuidado com a diferença entre variação percentual e ponto percentual.
Cartão: R$ 18,4 mi Tabela: 18.437.219,63
Percentual: 23,7% (1 casa basta na maioria dos casos)
Variação: +8,2% (cresceu 8,2% em relação à base)
Diferença: +1,4 p.p. (margem foi de 22,3% para 23,7%)
Confundir os dois últimos é erro clássico em reunião de resultado.Camadas. A primeira tela responde o quê (está bom ou ruim); o drill responde por quê e onde. Colocar o detalhe na tela principal deixa tudo lento e esconde o que importa. Regra prática: o detalhe deve estar a no máximo dois cliques — e com botão de voltar visível.
Nível 1 Visão geral KPIs + evolução + ranking
Nível 2 Drill-through página da loja selecionada
Nível 3 Detalhe tabela de cupons (exportável)
Botão direito na barra da loja > Drill-through > Detalhe da Loja
Na página de destino: [ Voltar ] sempre visível, no mesmo canto.Em linguagem de negócio, do jeito que a pessoa fala. Título do visual deve ser a pergunta ou a conclusão, não o nome técnico do campo. Medidas com nome claro (Faturamento, não vlr_liq_sum) porque elas aparecem no tooltip, na legenda e nas exportações.
Ruim Bom
"Página 1" "Visão geral"
"Soma de ValorLiquido por..." "Faturamento por loja"
"Gráfico 3" "Evolução mensal x ano anterior"
"med_margem_pct" "Margem %"Escreva uma mensagem explícita no lugar da tela em branco, dizendo o que aconteceu e o que fazer. No Power BI, isso se resolve com uma caixa de texto exibida por medida ou com o estado vazio nativo do visual. Tela em branco é lida como painel quebrado — e vira chamado de suporte.
Mensagem Vazio =
IF(
ISBLANK( [Faturamento] ),
"Nenhuma venda encontrada para os filtros selecionados. " &
"Tente ampliar o período ou limpar o filtro de canal.",
""
)
Coloque numa caixa de texto com fx no valor, sobre a área do visual.Sim, sempre — é o item que mais sustenta a confiança no painel. Sem ele, todo número velho vira suspeita de erro. Mostre a data do dado, não a do refresh: são coisas diferentes quando a carga do banco atrasa e o Power BI atualiza mesmo assim.
Última Venda Carregada = MAX( fVendas[DataVenda] )
Último Refresh = MAX( Auditoria[DataHoraCarga] )
Rodapé em todas as páginas:
"Dados até 19/08/2026 · atualizado às 06:12 · fonte: DW_Vendas"Use o Layout Móvel (Exibir > Layout do telefone) e monte uma versão vertical com o essencial: 2 a 4 KPIs e um gráfico simples. Não é a mesma tela reduzida — é uma curadoria. Tabela larga e mapa detalhado não funcionam no celular, e drill por toque precisa de área de clique maior.
Exibir > Layout do telefone
O que entra: KPIs principais, evolução, top 5
O que fica fora: tabela larga, dispersão, mapa detalhado
Alvo de toque: mínimo 44 x 44 px
Teste real: abra no app do Power BI, no seu celular, no 4GPrimeiro, entenda o porquê — na maioria das vezes o painel não responde à pergunta dele, ou falta um corte que ele precisa. Depois de resolver isso, ofereça um caminho oficial de exportação (tabela detalhe ou relatório paginado). Proibir sem resolver a causa só produz planilha paralela, que é pior.
Motivos reais por trás do pedido
"quero mandar por e-mail" -> assinatura de e-mail no Service
"preciso do detalhe" -> página de drill-through com a tabela
"faço meu cálculo à parte" -> a medida que ele precisa não existe
"não confio no número" -> falta glossário e data de atualizaçãoTire o que não informa: sombra, borda dupla, gradiente, 3D, gridline forte, legenda de série única, eixo redundante quando já há rótulo de dado. Depois alinhe tudo numa grade de 8 px e garanta respiro entre os blocos. Painel limpo não é vazio: é aquele em que tudo que ficou tem função.
Antes de publicar, remova:
[ ] efeitos 3D e sombras
[ ] bordas decorativas e fundos coloridos por bloco
[ ] gridlines fortes (deixe suaves ou nenhuma)
[ ] legenda quando há uma única série
[ ] eixo Y quando já existe rótulo de dado
[ ] ícone que apenas repete o texto ao ladoAntes de construir e depois de publicar: as etapas que separam um painel entregue de um painel usado.
Para errar barato. Mudar um retângulo no Figma leva segundos; mudar um painel pronto leva remodelagem, medidas novas e nova validação. Além disso, o rascunho muda a conversa: em vez de discutir cor de gráfico, o cliente discute se a pergunta é a certa — que é o que importa nessa fase.
Custo de mudar uma decisão de layout
papel ~ 2 min
Figma ~ 10 a 30 min
painel real ~ 2 a 5 dias
Bônus: o wireframe aprovado vira o escopo escrito do projeto.Frame de 1280x720 (a proporção da tela do Power BI), caixas cinzas, grade de 8 px e, dentro de cada caixa, a pergunta que ela responde. Sem cor e sem dado real: baixa fidelidade evita que a discussão desvie para estética antes da hora.
Frame 1280 x 720
[ faixa ] título + filtros h 72
[ 4 caixas ] KPIs h 120
[ 2 caixas ] evolução | ranking h 260
[ 1 caixa ] tabela detalhe h 200
Escreva na caixa: "Qual loja mais caiu?" — não "gráfico de barras".Trabalhando na mesma proporção e na mesma grade: o Power BI aceita posição e tamanho numéricos por visual, então X, Y, largura e altura do Figma viram valores no painel de formatação. Cores e fontes vão pelo tema JSON, e os rótulos do wireframe devem ser os nomes reais das medidas.
Figma Power BI
frame 1280 x 720 -> Exibir > Tamanho da página > 16:9
X/Y/W/H em 8 px -> Formato > Geral > Posição e tamanho
estilos de cor -> tema JSON (Exibir > Temas > Procurar)
componente KPI -> grupo de visuais copiado entre páginas
rótulo "Margem %" -> nome da medida [Margem %]Um conjunto de decisões tomadas uma vez e reutilizadas: paleta com significado, escala tipográfica, espaçamento, formato de número, padrão do cartão de KPI e regras de interação. O ganho aparece no quinto painel — todos parecem do mesmo produto, e trocar a marca não exige repintar 40 visuais.
O que documentar (uma página basta)
Cores 3 categóricas + sequencial + divergente + neutros
Tipografia título 14 / rótulo 10 / KPI 28 (Segoe UI)
Espaço grade de 8 px, respiro mínimo de 16 entre blocos
Números R$ abreviado no KPI, 2 casas na tabela, % com 1 casa
Padrões KPI = valor + variação (seta, sinal, cor)
Interação segmentação filtra; clique realça; detalhe em drill-throughEscreva um arquivo com as cores e classes de texto e importe em Exibir > Temas > Procurar temas. Ele passa a valer para todos os visuais novos e é versionável no Git — diferente de formatação feita visual a visual, que se perde na primeira página nova.
{
"name": "Casa & Construção 2026",
"dataColors": ["#2B6CB0", "#2C7A7B", "#6B46C1", "#B7791F", "#B23A48"],
"background": "#FFFFFF",
"foreground": "#1A202C",
"tableAccent": "#2B6CB0",
"textClasses": {
"title": { "fontFace": "Segoe UI Semibold", "fontSize": 14, "color": "#1A202C" },
"label": { "fontFace": "Segoe UI", "fontSize": 10, "color": "#4A5568" }
}
}É medir o comportamento de quem usa o produto para decidir o que mudar — em vez de decidir por opinião. Aplicado a BI: quantos abrem, quantos voltam, quais páginas são usadas, quanto tempo até o primeiro filtro, onde as pessoas travam. Você já constrói painel para os outros medirem o negócio; aqui você faz o mesmo com o seu próprio painel.
Fonte de dados
Quantitativo: usage metrics do Service (acessos, páginas, usuários)
Qualitativo: teste de usabilidade, entrevista, observação
Decisão: cruzar os dois — número diz ONDE, conversa diz POR QUÊAdoção (usuários únicos sobre o público-alvo), retenção (voltaram na semana seguinte), frequência, profundidade (páginas por sessão) e tempo até o primeiro filtro. Retenção é a que mais importa: acesso no lançamento todo painel tem, porque a curiosidade leva.
Adoção > 60% do público em 30 dias
Retenção semanal > 40%
Frequência depende do ritmo da decisão (semanal, diária)
Profundidade o detalhe é usado, ou só o topo?
Tempo até filtro > 30 s indica que o usuário não achou o que fazer
Página com 0 acesso em 60 dias: remova ou investigue.Cinco pessoas, 15 minutos cada, quatro tarefas. Dê tarefa, não pergunta, cronometre e fique calado — a vontade de ajudar é o que mais estraga o teste. Você vai ver a pessoa procurar o filtro em três lugares, e isso vale mais que uma hora de reunião de requisitos.
T1. Qual foi o faturamento de março? esperado < 10 s
T2. Qual loja mais caiu contra o ano passado? esperado < 30 s
T3. Nessa loja, qual categoria puxou a queda? esperado < 45 s
T4. Exporte a lista de lojas abaixo da meta. esperado < 30 s
Registre: tempo, cliques errados, hesitação, o que a pessoa falou.Separe o que bloqueia a tarefa do que é preferência. Bloqueio entra primeiro; preferência entra se for barato ou se aparecer repetidamente. Um pedido isolado de um usuário influente não é prioridade — três pessoas travando no mesmo ponto é.
Matriz simples
Muitos usuários Poucos usuários
Bloqueia fazer agora fazer depois
Incomoda fazer depois registrar e observar
Frase útil na reunião: "isso impede você de concluir a tarefa,
ou é uma preferência de visualização?"Mostre o processo, não só a tela bonita: a pergunta de negócio, o modelo (star schema), duas ou três medidas que exigiram decisão, o antes e depois do layout e o número de performance. Quem contrata para dashboard já viu telas bonitas demais; o que diferencia é conseguir explicar por que cada escolha foi feita.
Roteiro de 5 minutos (funciona em entrevista e no portfólio)
1. Problema: "42 lojas, decisão semanal de verba, dado só no Excel"
2. Modelo: diagrama do star schema (por que fato e dimensões)
3. Medida: uma que exigiu contexto (ALLSELECTED, ranking, meta)
4. Design: antes e depois, e o que o teste com 5 pessoas mudou
5. Resultado: refresh 38 min -> 6 min; página em 2,4 s; 78% de adoção