Jhonatan Pinheiro
Carregando página...
Jhonatan Pinheiro
Carregando página...
Jhonatan Pinheiro
Carregando página...
75 tópicos com um caso real de varejo do começo ao fim: Power Query, star schema, o SQL que alimenta o modelo, DAX de CALCULATE a time intelligence, design de dashboard, Figma e métricas de uso.
Dashboard bonito que ninguém usa é custo. Dashboard feio que responde à pergunta certa é útil — mas dura pouco, porque ninguém defende o que dá preguiça de olhar. Este guia junta as quatro competências que fazem um produto analítico funcionar: SQL (de onde vem o dado), modelagem + DAX (o que o número significa), Power BI Desktop (onde vira produto) e UX/UI (por que alguém volta amanhã).
Todos os exemplos usam a mesma empresa e os mesmos números, do SELECT até o layout:
Casa & Construção Nordeste — 42 lojas em 6 estados, 3 canais (loja física, e-commerce, televendas), ~4,2 milhões de linhas de venda por ano em um SQL Server. A diretoria quer responder três perguntas: estamos batendo a meta de margem?, quais lojas caíram contra o ano passado? e onde a ruptura de estoque está comendo faturamento?
Modelo: fVendas (fato, grão = item do cupom), dProduto, dLoja, dCliente, dCalendario e fMetas.
Como ler: se você nunca abriu o Power BI, vá do 1 ao 75 na ordem. Se já entrega relatórios, comece no 21 — modelagem é onde nasce a maioria dos problemas que aparecem disfarçados de "erro de DAX".
Antes da ferramenta: o que você está construindo, para quem, e que decisão isso muda.
Relatório entrega linhas para conferência (o fiscal quer a nota 4471). Dashboard monitora um conjunto pequeno e estável de indicadores para disparar ação. Análise ad hoc responde uma pergunta única e morre depois. Misturar os três na mesma tela é a causa número um de painel lento e confuso: o diretor abre para ver a margem e leva 40 mil linhas junto.
Diretoria -> dashboard -> 6 indicadores, 1 tela, 3 s de carga
Gerente -> dashboard -> mesma tela + drill por loja
Analista -> ad hoc -> Analisar no Excel / conexão dinâmica
Fiscal -> relatório -> paginado (Report Builder), exportávelAntes de escolher visual, escreva a frase: quem decide o quê, com que frequência, e o que muda quando o número está ruim. Se não existe ação associada, o indicador é curiosidade — e curiosidade não merece espaço na tela principal.
Ficha do indicador (preencha ANTES de construir)
Decisão: remanejar verba de mídia entre canais
Quem decide: gerente de trade marketing
Frequência: toda segunda, 9h
Gatilho: margem do canal < 22% por 2 semanas seguidas
Ação: corta verba do canal e realoca para o de maior margemTodo painel tem quatro camadas, e cada problema mora em uma delas. Diagnosticar na camada errada custa semanas: gente reescrevendo DAX quando o problema é o grão da fato, ou trocando cor quando o problema é que o número não responde à pergunta.
1. Fonte SQL Server, planilhas, API -> confiabilidade
2. Modelo star schema, relacionamentos -> desempenho e verdade
3. Semântica medidas DAX, formatação -> significado
4. Interface layout, cor, interação -> adoçãoMétrica é o que se soma (faturamento, unidades). Dimensão é o corte (loja, produto, data). Granularidade é a menor linha da fato — no nosso caso, um item de cupom. Você nunca consegue descer abaixo do grão que carregou: se a fato vier agregada por dia e loja, a pergunta qual produto puxou a queda fica sem resposta para sempre.
fVendas (grão = item do cupom)
+------------+--------+---------+-----+--------+-------+
| DataVenda | LojaSK | ProdSK | Qtd | Valor | Custo |
+------------+--------+---------+-----+--------+-------+
| 2026-03-14 | 17 | 90421 | 2 | 179,80 | 118,40|
Agregar depois é fácil. Desagregar é impossível.Se três áreas calculam faturamento de três jeitos, o painel vira palco de discussão em vez de decisão. Escreva a definição, a fórmula e as exceções — e coloque isso dentro do próprio relatório, numa página de glossário. É o item mais barato de produzir e o que mais evita reunião.
Faturamento Líquido
Definição: venda bruta - devoluções - impostos sobre venda
Fonte: fVendas.ValorLiquido (já líquido de imposto na origem)
Exclui: vendas de teste (LojaSK = 999) e trocas sem valor
Dono: Controladoria
Atualiza: diariamente às 6h (D-1)O básico de navegação e as decisões de configuração que economizam meses depois.
Relatório é onde se desenha. Tabela (antiga Dados) é onde se confere o que chegou. Modelo é onde se ligam as tabelas. Iniciante passa 100% do tempo na primeira e leva semanas para descobrir que o problema estava na terceira.
Barra lateral esquerda:
[ ] Relatório -> visuais, layout, formatação
[#] Tabela -> conferir valores, criar coluna calculada
[<>] Modelo -> relacionamentos, cardinalidade, ocultar colunasImport copia os dados para o modelo em memória (VertiPaq): rápido e com todo o DAX disponível — é o padrão certo em 90% dos casos. DirectQuery consulta a fonte a cada clique: dado ao vivo, porém lento e com DAX limitado. Dual deixa o motor escolher, em modelos compostos. Nossa fato de 4,2 milhões de linhas cabe em Import sem drama (~120 MB comprimidos).
Import -> até centenas de milhões de linhas, refresh agendado
DirectQuery -> exigência de tempo real (< 5 min) ou dado que não pode sair da origem
Dual -> dimensões pequenas em modelo composto
Regra prática: só use DirectQuery quando alguém provar,
por escrito, que atualização de 1 h não resolve.O assistente convida a marcar a tabela e clicar em Carregar — e aí você traz 60 colunas e 12 anos de história para responder sobre 24 meses. Conecte por view ou por consulta com filtro. Menos colunas significa menos memória, refresh mais curto e modelo mais legível.
Obter Dados > SQL Server
Servidor: srv-bi.casaeconstrucao.local
Banco: DW_Vendas
Modo: Import
Em vez de marcar a tabela fVendas inteira:
SELECT ... FROM dbo.vw_fato_vendas WHERE DataVenda >= '2024-01-01'
Traga só o período que o painel mostra (+1 ano, para o comparativo).Servidor e banco fixos na consulta significam refazer tudo ao promover de homologação para produção. Crie parâmetros no Power Query e referencie-os na fonte: trocar de ambiente vira mudar dois campos — e, no Service, é configuração de parâmetro, sem republicar.
let
Fonte = Sql.Database(pServidor, pBanco),
Vendas = Fonte{[Schema="dbo", Item="vw_fato_vendas"]}[Data]
in
VendasPor padrão o Power BI cria uma tabela de datas oculta para cada coluna de data do modelo. Com 8 colunas de data, são 8 tabelas invisíveis, memória desperdiçada e time intelligence imprevisível. Desligue e use uma única dCalendario sua.
Arquivo > Opções e Configurações > Opções
> Arquivo Atual > Carregamento de Dados
[ ] Data/hora automática <- DESMARCAR
[ ] Detectar relacionamentos... <- DESMARCAR (crie você)
[ ] Atualizar dados ao abrir <- avaliar caso a casoNo Desktop, Atualizar recarrega tudo agora. Em produção quem atualiza é o Power BI Service, com credencial própria e, para fonte local, um gateway instalado na rede da empresa. Falha clássica: funciona no seu notebook e quebra no Service, porque o SQL Server não é visível de fora.
Desktop -> Página Inicial > Atualizar (usa sua credencial do Windows)
Service -> Configurações do semântico > Atualização Agendada
+ Gateway de dados local (fonte on-premises)
+ Credencial de serviço (nunca a sua pessoal)
Limite: 8 atualizações/dia (Pro), 48 (Premium/Fabric).O .pbix é um pacote binário — no Git, cada commit vira um blob novo e diff é impossível. O formato .pbip (Power BI Project) grava modelo e relatório como arquivos de texto, então dá para revisar mudança de medida em pull request, como código.
Arquivo > Salvar como > Projeto do Power BI (.pbip)
meu-painel.pbip
meu-painel.Dataset/
model.bim <- tabelas, relacionamentos, medidas (texto)
meu-painel.Report/
report.json <- páginas, visuais, layout (texto)
Aí sim: git diff mostra "medida Margem % mudou de X para Y".Onde o dado sujo vira tabela confiável — e onde muita gente faz o trabalho que o SQL faria melhor.
Regra do mais perto da fonte possível: filtrar, juntar e agregar no SQL (o servidor foi feito para isso); moldar formato e tipagem no Power Query; calcular indicador que reage a filtro em DAX. Somar no Power Query o que deveria ser medida cria um número que não responde à segmentação — e todo mundo acha que o painel está errado.
SQL -> JOIN, WHERE, GROUP BY pesado, histórico
Power Query -> tipos, unpivot, merge leve, colunas derivadas fixas
DAX -> tudo que muda conforme o filtro do usuário
Sinal de alerta: coluna calculada chamada "Total do Ano".
Ela não muda quando o usuário filtra. Deveria ser medida.Arquivo gerado em pt-BR usa vírgula decimal; interpretado como en-US, 179,80 vira 17980 — sem erro visível, só um faturamento cem vezes maior. Sempre tipe explicitamente informando a cultura de origem.
= Table.TransformColumnTypes(
Fonte,
{{"ValorLiquido", type number}, {"DataVenda", type date}},
"pt-BR"
)A primeira etapa útil de quase toda consulta é jogar fora o que não será usado: menos memória, refresh mais curto e mais chance de query folding. Prefira Table.SelectColumns (lista o que fica) a remover colunas — se a origem ganhar uma coluna nova, sua consulta não quebra.
= Table.SelectColumns(
Fonte,
{"DataVenda", "LojaSK", "ProdutoSK", "Qtd", "ValorLiquido", "Custo"}
)Folding é o Power Query traduzir seus passos em SQL e mandar o servidor executar. Enquanto acontece, um filtro de data custa quase nada; quando quebra, o Power BI baixa tudo e filtra na sua máquina. Quebram folding: Table.Buffer, funções M sem equivalente em SQL e colunas personalizadas complexas.
Botão direito no passo > "Exibir Consulta Nativa"
habilitado -> o passo ainda está sendo traduzido em SQL
esmaecido -> o folding quebrou daqui para frente
Estratégia: faça TODOS os passos que fazem folding primeiro,
e só depois os que quebram.Planilha de metas quase sempre chega larga (Jan, Fev, Mar como colunas). O modelo precisa dela alta: uma linha por loja e mês. Transformar Colunas em Linhas resolve — e usar Unpivot Outras Colunas faz com que um mês novo na planilha não quebre a consulta.
// Antes: Loja | Jan | Fev | Mar
// Depois: Loja | Mes | MetaValor
= Table.UnpivotOtherColumns(
Fonte,
{"Loja"},
"Mes",
"MetaValor"
)Anexar empilha tabelas com as mesmas colunas (vendas de 2025 + vendas de 2026). Mesclar é o JOIN: traz colunas de outra tabela pela chave. No modelo estrela, mesclar quase sempre é erro — a ligação deve ser um relacionamento, não uma coluna copiada para dentro da fato.
Anexar (append) -> mesmas colunas, mais linhas
Mesclar (merge) -> mesmas linhas, mais colunas
Quando mesclar é certo:
unir uma dimensão que veio quebrada em duas fontes
Quando é errado:
copiar dProduto[Categoria] para dentro de fVendas "para facilitar"Cada loja manda um Excel de inventário. Em vez de 42 consultas iguais, crie uma função que recebe o arquivo binário e devolve a tabela limpa, e aplique-a sobre a pasta inteira. Quando a regra mudar, você muda em um lugar só.
// fxLimpaInventario
(arquivo as binary) as table =>
let
Planilha = Excel.Workbook(arquivo){[Item="Inventario"]}[Data],
Cabecalho = Table.PromoteHeaders(Planilha),
Tipado = Table.TransformColumnTypes(
Cabecalho, {{"SKU", type text}, {"Saldo", Int64.Type}}, "pt-BR"),
SemVazio = Table.SelectRows(Tipado, each [SKU] <> null)
in
SemVazioFaixas e classificações fixas (que não dependem de filtro) pertencem ao Power Query. E sempre decida o que fazer com erro de conversão: virar nulo é honesto; quebrar o refresh é pior; esconder com try otherwise 0 sem avisar ninguém é o pior de todos, porque some com o problema e mantém o número errado.
= Table.AddColumn(Fonte, "FaixaTicket", each
if [ValorLiquido] >= 500 then "Alto"
else if [ValorLiquido] >= 150 then "Medio"
else "Baixo", type text)
// erro controlado, com marcação para auditar depois
= Table.AddColumn(Fonte, "CustoTratado", each
try Number.From([Custo]) otherwise null, type nullable number)Aqui nasce quase todo problema que depois aparece disfarçado de erro de DAX ou de lentidão.
Uma tabela gigante com tudo dentro parece simples e é armadilha: a categoria do produto se repete 4,2 milhões de vezes, os filtros ficam lentos e não há como listar produto que não vendeu. No star schema, a fato guarda números e chaves; as dimensões guardam descrições. É o formato para o qual a engine do Power BI foi construída.
dCalendario
|
dLoja -- fVendas -- dProduto
|
dCliente
fato = números + chaves (estreita e longa)
dimensão = textos + hierarquias (larga e curta)Se a coluna responde quanto e faz sentido somar, é fato. Se responde quem, o quê, onde, quando e você quer filtrar ou agrupar por ela, é dimensão. Teste rápido: somar CEP não significa nada — CEP é dimensão, mesmo sendo numérico.
fVendas : Qtd, ValorLiquido, Custo, Desconto (somáveis)
dProduto : SKU, Descrição, Categoria, Marca (agrupáveis)
dLoja : Nome, Cidade, UF, Regional, Formato
dCalendario: Date, Ano, MesNome, Trimestre, DiaSemanaO padrão saudável é um para muitos (1:*) com filtro em direção única, da dimensão para a fato. Muitos-para-muitos e filtro bidirecional resolvem casos específicos, mas geram ambiguidade e lentidão — e são a explicação mais comum para um total que não bate com a soma das partes.
dLoja[LojaSK] 1 ---- * fVendas[LojaSK] direção: única (dLoja -> fVendas)
Filtro cruzado bidirecional: use apenas quando precisar
filtrar a dimensão pela fato — e prefira resolver isso
com CROSSFILTER dentro da medida, não no relacionamento.Time intelligence (TOTALYTD, SAMEPERIODLASTYEAR) exige uma tabela de datas contínua, sem buracos, cobrindo anos inteiros e marcada como tabela de data. Sem isso, o comparativo com o ano anterior devolve resultado silenciosamente errado nas bordas do período.
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]),
"DiaSemana", FORMAT([Date], "ddd")
)
// Depois: Tabela > Marcar como Tabela de Data > coluna DateSem configurar, o eixo mostra abr, ago, dez, fev... Selecione a coluna de texto e use Classificar por Coluna, apontando para o número do mês. Vale para qualquer texto com ordem própria: faixa de ticket, porte de loja, estágio do funil.
dCalendario > coluna MesNome
> Ferramentas de Coluna > Classificar por Coluna > MesNum
Cuidado: a coluna de ordenação precisa ter exatamente
1 valor por rótulo — senão o Power BI recusa.A fato tem data do pedido e data da entrega. Só um relacionamento pode estar ativo por vez com a dCalendario; o outro fica inativo e é ligado sob demanda dentro da medida. É assim que você mostra faturamento por data de venda e, ao lado, entregas por data de entrega — no mesmo eixo.
Entregas =
CALCULATE(
[Faturamento],
USERELATIONSHIP( fVendas[DataEntrega], dCalendario[Date] )
)Nem toda tabela precisa de relacionamento. Uma tabela sem ligação nenhuma serve para o usuário escolher um valor (percentual de reajuste, cenário) que a medida lê com SELECTEDVALUE. É a base de qualquer simulação no Power BI.
// Modelagem > Novo Parâmetro > Numérico
ReajustePreco = GENERATESERIES( 0, 0.20, 0.01 )
Faturamento Simulado =
VAR Reajuste = SELECTEDVALUE( ReajustePreco[Valor], 0 )
RETURN
[Faturamento] * ( 1 + Reajuste )Quem usa o painel não deveria ver LojaSK, ProdutoSK nem as colunas cruas da fato. Oculte as chaves, oculte o que já virou medida e agrupe medidas em pastas de exibição. Um modelo com 12 itens visíveis é usável; um com 140 empurra todo mundo de volta para o Excel.
Visão de Modelo > selecione a coluna > Propriedades
Ocultar na exibição de relatório: Sim
Pastas de exibição das medidas:
Vendas/ Faturamento, Ticket Médio, Unidades
Rentabilidade/ Margem R$, Margem %
Comparativos/ Faturamento AA, Var % AA, YTD
Metas/ Meta, Atingimento %A camada que ninguém vê e que define se o painel atualiza em 2 minutos ou em 2 horas.
Não aponte o Power BI para tabelas físicas. Crie views com nome de negócio, colunas já renomeadas e regras aplicadas (exclusão de loja de teste, por exemplo). Quando a tabela física mudar, você conserta a view — e nenhum dos 30 relatórios quebra.
CREATE VIEW dbo.vw_fato_vendas AS
SELECT
v.DataVenda,
v.LojaSK,
v.ProdutoSK,
v.ClienteSK,
v.Quantidade AS Qtd,
v.ValorLiquido,
v.CustoUnitario * v.Quantidade AS Custo
FROM dbo.FatoVendasItem v
WHERE v.LojaSK <> 999 -- loja de teste
AND v.StatusCupom = 'FECHADO';O painel mostra 24 meses e compara com o ano anterior — logo, 36 meses bastam. Cortar 9 anos de história derrubou o refresh de 38 para 6 minutos e o modelo de 480 MB para 120 MB. Deixe o histórico completo em um relatório separado, para quem realmente precisa dele.
SELECT ...
FROM dbo.vw_fato_vendas
WHERE DataVenda >= DATEADD(
MONTH, -36,
DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1)
);Se nenhuma página desce abaixo de dia + loja + categoria, agregue nesse grão no SQL. Nossa fato caiu de 4,2 milhões para 380 mil linhas e o painel ficou instantâneo. Só faça isso com certeza de que o detalhe não será pedido — desagregar depois é impossível.
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;ROW_NUMBER, RANK e SUM() OVER resolvem no servidor o que em DAX custaria caro sobre tabela grande. Útil sobretudo para snapshots históricos — o ranking congelado de cada mês, que não deve mudar quando o usuário filtra.
SELECT
LojaSK,
AnoMes,
Faturamento,
RANK() OVER (PARTITION BY AnoMes ORDER BY Faturamento DESC) AS PosicaoMes,
SUM(Faturamento) OVER (PARTITION BY LojaSK, LEFT(AnoMes, 4)
ORDER BY AnoMes ROWS UNBOUNDED PRECEDING) AS AcumuladoAno
FROM dbo.vw_vendas_mes_loja;O refresh incremental faz o Power BI recarregar só a janela recente (os últimos 10 dias, por exemplo) e manter o resto. Exige uma coluna de data/hora na origem e os parâmetros reservados RangeStart e RangeEnd — com esses nomes exatos — usados como filtro na consulta.
// Power Query: parâmetros RangeStart e RangeEnd (tipo Data/Hora)
= Table.SelectRows(Fonte, each
[DataVenda] >= RangeStart and [DataVenda] < RangeEnd)
// Modelagem > Atualização incremental:
// Arquivar dados dos últimos 3 anos
// Atualizar incrementalmente os últimos 10 diasA consulta do refresh é sempre a mesma: filtro por data e projeção de poucas colunas. Um índice pela data de venda, com as colunas usadas incluídas, evita varrer a tabela toda. Meça antes e depois — no nosso caso, 4min12s viraram 47s.
CREATE NONCLUSTERED INDEX IX_FatoVendas_Data
ON dbo.FatoVendasItem (DataVenda)
INCLUDE (LojaSK, ProdutoSK, Quantidade, ValorLiquido, CustoUnitario);
-- Confira o plano antes e depois:
SET STATISTICS IO, TIME ON;Nulo que chega ao modelo vira (Em branco) no visual, e (Em branco) numa dimensão gera aquela categoria fantasma que ninguém sabe explicar em reunião. Trate na view, com rótulo explícito — o usuário precisa saber que existe venda sem categoria, não vê-la sumir.
SELECT
p.ProdutoSK,
COALESCE(NULLIF(LTRIM(RTRIM(p.Categoria)), ''), 'Sem categoria') AS Categoria,
CAST(p.PrecoLista AS DECIMAL(10,2)) AS PrecoLista
FROM dbo.dProduto p;Se o cálculo não muda conforme o que o usuário clicar, faça no SQL: é mais rápido, versionado e testável. Se muda com o filtro (participação no total, comparação com o período selecionado), precisa ser DAX — é exatamente para isso que a linguagem existe.
SQL: margem por item da venda (fixa por linha)
SQL: classificação ABC do produto (recalculada no ETL)
DAX: % do faturamento do filtro atual (muda a cada clique)
DAX: variação contra o ano anterior (depende do período escolhido)DAX não é fórmula de Excel. A sintaxe se aprende numa tarde; o contexto de avaliação leva alguns meses.
Coluna calculada é calculada no refresh, ocupa memória e é fixa por linha. Medida é calculada na hora, conforme o filtro do visual, e não ocupa memória. Padrão: use medida. Coluna só quando precisar dela para filtrar, agrupar ou como eixo do gráfico.
// Coluna: existe por linha, serve para filtrar/agrupar
fVendas[MargemLinha] = fVendas[ValorLiquido] - fVendas[Custo]
// Medida: reage ao filtro do visual
Margem R$ = SUM( fVendas[ValorLiquido] ) - SUM( fVendas[Custo] )Contexto de linha existe dentro de uma coluna calculada ou de um iterador (SUMX): há uma linha atual. Contexto de filtro é o conjunto de filtros vindos do visual, das segmentações e dos relacionamentos. Toda medida é avaliada dentro de um contexto de filtro — entender isso é entender DAX.
Visual: linha = "Loja Recife", coluna = "Março/2026"
Contexto de filtro dessa célula:
dLoja[Nome] = "Loja Recife"
dCalendario[AnoMes] = "2026-03"
+ o que estiver em segmentações e filtros de página
A medida é executada uma vez POR CÉLULA, nesse contexto.SUM soma uma coluna que já existe. SUMX percorre a tabela linha a linha, calcula uma expressão e soma o resultado. Sempre que o cálculo precisa acontecer antes da soma — preço vezes quantidade, por exemplo — o iterador é obrigatório.
// Certo: multiplica por linha, depois soma
Receita Bruta = SUMX( fVendas, fVendas[Qtd] * fVendas[PrecoUnitario] )
// Errado: soma tudo e multiplica no fim
Receita Errada = SUM( fVendas[Qtd] ) * SUM( fVendas[PrecoUnitario] )COUNTROWS conta linhas da fato (itens vendidos). DISTINCTCOUNT conta valores distintos (clientes que compraram). Cuidado: DISTINCTCOUNT é caro em coluna de alta cardinalidade e não é somável — a soma dos meses nunca dá o ano, porque quem comprou em janeiro e em março é uma pessoa só.
Itens Vendidos = COUNTROWS( fVendas )
Cupons = DISTINCTCOUNT( fVendas[CupomID] )
Clientes Ativos = DISTINCTCOUNT( fVendas[ClienteSK] )
// Jan: 1.200 | Fev: 1.350 | Total: 1.910
// Não é erro: é o comportamento esperado.Divisão por zero gera erro ou infinito, e o visual mostra algo que ninguém entende. DIVIDE trata denominador zero ou vazio e deixa você escolher o resultado alternativo. Normalmente é melhor devolver vazio do que zero — zero mente ao afirmar que houve venda com margem nula.
Margem % = DIVIDE( [Margem R$], [Faturamento] ) // vazio se não houve venda
Ticket Médio = DIVIDE( [Faturamento], [Cupons] )
Atingimento % = DIVIDE( [Faturamento], [Meta], 0 ) // aqui o zero faz sentidoCALCULATE avalia uma expressão modificando o contexto de filtro. É a função mais importante da linguagem: comparativos, participações e cenários passam todos por ela. Os filtros passados substituem o filtro existente naquela coluna e mantêm os demais.
Faturamento E-commerce =
CALCULATE( [Faturamento], dLoja[Canal] = "E-commerce" )
// Vários filtros = E (AND)
Fat Ecom Nordeste =
CALCULATE(
[Faturamento],
dLoja[Canal] = "E-commerce",
dLoja[Regional] = "Nordeste"
)Filtro simples (coluna = valor) resolve a maioria dos casos. Quando a condição compara com uma medida ou envolve mais de uma coluna, é preciso FILTER, que devolve uma tabela. Filtre sempre a menor tabela possível — usar FILTER sobre a fato de 4 milhões de linhas quando bastava a dimensão é receita de lentidão.
// Cupons acima de R$ 500 (compara com o valor da linha)
Vendas Alto Ticket =
CALCULATE( [Faturamento], FILTER( fVendas, fVendas[ValorLiquido] > 500 ) )
// Melhor: filtre a dimensão, não a fato
Fat Categorias Premium =
CALCULATE( [Faturamento], FILTER( dProduto, dProduto[PrecoLista] > 500 ) )ALL/REMOVEFILTERS ignoram filtros e dão o total geral. ALLSELECTED respeita o que o usuário escolheu nas segmentações e ignora apenas o filtro do próprio visual — é o que você quer em quase todo percentual do total, para que a participação some 100% dentro da seleção.
% do Total Geral =
DIVIDE( [Faturamento], CALCULATE( [Faturamento], REMOVEFILTERS( dProduto ) ) )
% do Selecionado =
DIVIDE( [Faturamento], CALCULATE( [Faturamento], ALLSELECTED( dProduto ) ) )
// Usuário escolhe 3 categorias:
// ALL -> soma 41% | ALLSELECTED -> soma 100%Variável é avaliada uma vez e reutilizada — evita recalcular a mesma medida três vezes e deixa a fórmula legível. Detalhe importante: ela guarda o valor no contexto em que foi declarada, então não é afetada pelo CALCULATE que vier depois.
Var % AA =
VAR Atual = [Faturamento]
VAR Anterior = CALCULATE( [Faturamento], SAMEPERIODLASTYEAR( dCalendario[Date] ) )
VAR Delta = Atual - Anterior
RETURN
IF( NOT ISBLANK( Anterior ), DIVIDE( Delta, Anterior ) )As medidas que aparecem em praticamente todo painel de vendas — e os detalhes que fazem cada uma funcionar.
TOTALYTD acumula do primeiro dia do ano até a última data do contexto. Se o ano fiscal da empresa não começa em janeiro, informe o fim do ano fiscal no terceiro argumento — no varejo é comum fechar em junho ou julho.
Faturamento YTD = TOTALYTD( [Faturamento], dCalendario[Date] )
// Ano fiscal encerrando em 30 de junho
Faturamento YTD Fiscal = TOTALYTD( [Faturamento], dCalendario[Date], "06-30" )SAMEPERIODLASTYEAR desloca o período inteiro do contexto em um ano. Para deslocamentos mais livres (2 meses, 3 trimestres), use DATEADD. Ambos exigem tabela de data marcada — sem ela, o resultado é silenciosamente incorreto.
Faturamento AA =
CALCULATE( [Faturamento], SAMEPERIODLASTYEAR( dCalendario[Date] ) )
Faturamento M-3 =
CALCULATE( [Faturamento], DATEADD( dCalendario[Date], -3, MONTH ) )Série mensal de varejo é serrilhada demais para ler tendência (dezembro sempre estoura). A média móvel suaviza e mostra a direção real. Use como linha sobre as barras do mês, não no lugar delas — o gestor precisa dos dois.
Média Móvel 3M =
AVERAGEX(
DATESINPERIOD( dCalendario[Date], MAX( dCalendario[Date] ), -3, MONTH ),
[Faturamento]
)RANKX ordena os valores de uma tabela segundo uma expressão. O detalhe que derruba iniciante: sem ALL na tabela de referência, cada linha vira o único elemento do ranking e o resultado é 1 para todo mundo.
Posição Loja =
RANKX( ALL( dLoja[Nome] ), [Faturamento], , DESC, DENSE )
// Ranking da queda: quem mais perdeu contra o ano anterior
Posição Queda =
RANKX( ALL( dLoja[Nome] ), [Var % AA], , ASC, DENSE )TOPN devolve uma tabela com as N melhores linhas, usada dentro de CALCULATE para responder perguntas do tipo quanto as 10 maiores lojas representam. Tabela virtual não aparece no modelo: existe só durante a avaliação da medida.
Fat Top 10 Lojas =
CALCULATE(
[Faturamento],
TOPN( 10, ALL( dLoja[Nome] ), [Faturamento], DESC )
)
Concentração Top 10 =
DIVIDE( [Fat Top 10 Lojas], CALCULATE( [Faturamento], ALL( dLoja ) ) )Em vez de três visuais quase idênticos, deixe o usuário escolher o indicador numa segmentação. Menos tela, menos manutenção, mais controle de quem lê. A tabela de seleção é desconectada e a medida lê a escolha com SELECTEDVALUE.
// Tabela desconectada: Métricas[Nome] = { "Faturamento", "Margem R$", "Unidades" }
Métrica Escolhida =
SWITCH(
SELECTEDVALUE( Métricas[Nome], "Faturamento" ),
"Faturamento", [Faturamento],
"Margem R$", [Margem R$],
"Unidades", [Unidades],
BLANK()
)Título fixo obriga o leitor a olhar as segmentações para saber o que está vendo — e é a causa mais comum de print de dashboard mal interpretado no grupo do WhatsApp. Uma medida de texto no título resolve, e sobrevive à captura de tela.
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 " & Periodo
// Visual > Título > fx > Baseado no campo > Título PáginaFormatação condicional por medida deixa a regra explícita e reutilizável. Semáforo é útil quando existe meta clara — mas evite pintar tudo: se a tabela inteira é colorida, nada chama atenção. E nunca informe apenas por cor (veja o item 59).
Cor Atingimento =
VAR Ating = [Atingimento %]
RETURN
SWITCH( TRUE(),
Ating >= 1, "#1B7F4B", // atingiu
Ating >= 0.9, "#B8860B", // perto
"#B23A48" // abaixo
)
// Visual > Formatação condicional > Cor da fonte > Baseado no campoMedidas de razão (ticket médio, margem %) exibem na linha de total um valor que às vezes não faz sentido — média de médias, meta individual somada. HASONEVALUE (ou ISINSCOPE) permite decidir o que mostrar no total, em vez de deixar um número que engana.
Meta por Loja =
IF(
HASONEVALUE( dLoja[Nome] ),
[Meta],
BLANK() // no total, a meta individual não faz sentido
)Design aqui não é enfeite: é a diferença entre o gestor decidir em 10 segundos e mandar um e-mail pedindo explicação.
Mostre a tela para alguém do público-alvo por 5 segundos, esconda e pergunte: o que você viu? está bom ou ruim? o que faria agora? Se não souber responder, o problema não é falta de dado — é falta de hierarquia. Faça isso com 5 pessoas antes de publicar: é a devolutiva mais barata que existe.
Roteiro (10 minutos por pessoa)
1. "Sem clicar, olhe por 5 segundos." -> esconder a tela
2. "O que essa tela está te dizendo?"
3. "O resultado está bom ou ruim?"
4. "O que você faria com essa informação?"
5. "O que ficou confuso?"
3 das 5 pessoas errando a mesma coisa = problema de design, não delas.A leitura ocidental começa no canto superior esquerdo. Coloque ali o indicador mais importante, grande. Contexto e detalhamento descem e vão para a direita. Filtros ficam numa faixa fixa (topo ou lateral esquerda) — nunca espalhados, porque o usuário precisa saber num relance o que está filtrado.
+---------------------------------------------------------------+
| Título dinâmico [ Período v ] [ Regional v ] | filtros
+---------------------------------------------------------------+
| FATURAMENTO MARGEM % TICKET MÉDIO ATING. META | o quê
| R$ 18,4 mi 23,7% R$ 214 92% |
+---------------------------------------------------------------+
| Evolução mensal (barras + linha AA) | Top 10 lojas (barras) | por quê
+--------------------------------------+------------------------+
| Tabela por loja: fat, var %, margem, atingimento | onde agir
+---------------------------------------------------------------+Cada intenção tem uma forma que o cérebro lê mais rápido: comparar entre categorias pede barras; evoluir no tempo pede linha; compor um todo pede barra empilhada; relacionar duas medidas pede dispersão. Pizza só com 2 ou 3 fatias — o olho compara ângulos muito pior do que comprimentos.
Comparar categorias -> barras horizontais, ordenadas por valor
Evoluir no tempo -> linha (contínuo) ou colunas (períodos discretos)
Compor um todo -> barra 100% empilhada > pizza
Relacionar 2 medidas -> dispersão (ex.: margem x volume por loja)
Distribuir -> histograma
1 número + meta -> cartão com variação, não medidor de velocidadeTrês paletas, um uso para cada: categórica (cores distintas para itens sem ordem), sequencial (intensidade para grandeza) e divergente (dois polos com neutro no meio, para variação em torno de zero). Fixe a cor de cada canal em todo o relatório: se e-commerce é azul, é azul em todas as páginas. E deixe a maior parte da tela em cinza — cor é para o que precisa de atenção.
Categórica canais: loja #2B6CB0 | e-commerce #2C7A7B | televendas #6B46C1
Sequencial volume: #EBF4FF -> #1A365D
Divergente var %: #B23A48 <- #E8E8E8 -> #1B7F4B
Regra 60-30-10: 60% neutro, 30% apoio, 10% destaque.Cerca de 8% dos homens têm alguma deficiência na visão de cores — numa diretoria de 12 pessoas, é provável que alguém não distinga o seu vermelho do seu verde. Nunca informe só por cor: acrescente seta, sinal ou rótulo. Garanta contraste mínimo de 4,5:1 no texto, preencha o texto alternativo dos visuais e revise a ordem de tabulação.
Ruim: [ ] verde [ ] vermelho
Bom: v +8,2% ^ -3,1% (símbolo + sinal + cor)
Checklist antes de publicar:
[ ] contraste >= 4,5:1 (texto) e 3:1 (elementos gráficos)
[ ] nada informado APENAS por cor
[ ] texto alternativo em cada visual (Formato > Geral > Texto Alt)
[ ] ordem de tabulação revisada (Exibir > Ordem de Tabulação)
[ ] fonte >= 10 pt; rótulos sem rotação de 45 grausR$ 18.437.219,63 num cartão é ruído: ninguém decide com os centavos. Abrevie na visão executiva (R$ 18,4 mi) e deixe o detalhe para a tabela de trabalho. Números sempre alinhados à direita, texto à esquerda, e a mesma quantidade de casas decimais na coluna inteira.
Cartão executivo: R$ 18,4 mi (1 casa, unidade abreviada)
Tabela analítica: 18.437.219,63 (2 casas, alinhado à direita)
Percentual: 23,7% (1 casa; 2 só se a decisão exigir)
Variação: +8,2 p.p. (ponto percentual != porcentagem)Toda tinta na tela deveria carregar informação. Borda dupla, sombra, fundo colorido, gradiente, gráfico 3D e eixo redundante competem com o dado. Um painel limpo não é um painel vazio: é um painel onde tudo que sobrou tem função.
Remova: 3D, sombra, gradiente, borda decorativa,
gridlines fortes, eixo Y quando há rótulo de dado,
legenda quando há uma única série,
ícone que apenas repete o texto ao lado
Mantenha: rótulo de dado OU eixo (nunca os dois),
espaço em branco entre blocos (mínimo 8 px)O usuário precisa saber o que vai acontecer antes de clicar. Padronize: segmentação filtra a página inteira; clicar numa barra realça as demais; o detalhe fica em drill-through explícito. E sempre ofereça o caminho de volta — um botão Limpar filtros visível resolve metade dos chamados de suporte.
Editar Interações (guia Formato): defina visual a visual
KPI -> não é afetado pelo clique no gráfico de lojas
Mapa -> realça (highlight), não filtra
Botões que não podem faltar:
[ Limpar filtros ] marcador com "Dados" ativo, sem seleção
[ Voltar ] nas páginas de drill-through
Dica de ferramenta de página para detalhe rico (mini gráfico no hover)Painel bonito com dado bonito é fácil. O que derruba a confiança é a tela em branco quando o filtro não retorna nada, ou o valor zerado quando o refresh falhou às 6h. Escreva mensagens explícitas e mostre sempre a data da última atualização.
Sem dados no filtro:
"Nenhuma venda para Regional = Sul em Março/2026.
Tente ampliar o período." (não deixe a tela em branco)
Rodapé fixo, em toda página:
"Dados até 19/08/2026 06:12 · fonte: DW_Vendas · dúvidas: bi@empresa.com"
Medida de frescor:
Última Atualização = MAX( fVendas[DataCarga] )Desenhar antes de construir: 40 minutos no Figma economizam duas semanas de retrabalho no Power BI.
Mudar um retângulo no Figma custa segundos; mudar um painel pronto custa remodelagem, medidas novas e nova validação. O rascunho também muda a conversa com o cliente: ele deixa de discutir se o gráfico fica bem à direita e passa a discutir se a pergunta é a certa.
Custo de mudar uma decisão de layout
Rascunho no papel ~ 2 min
Wireframe no Figma ~ 10 min
Protótipo no Figma ~ 30 min
Painel construído ~ 2 a 5 dias (modelo + medidas + testes)Sem cor, sem dado real, sem fonte bonita: caixas cinzas com rótulos. O objetivo é discutir o que vai em cada área e em que ordem de importância. A baixa fidelidade é uma vantagem — ninguém discute tom de azul num desenho cinza, e é isso que você quer nessa fase.
Frame 1280 x 720 (proporção da tela do Power BI, 16:9)
[ faixa ] título + filtros altura 72
[ 4 caixas] KPIs altura 120
[ 2 caixas] evolução | ranking altura 260
[ 1 caixa ] tabela detalhe altura 200
Escreva em cada caixa a PERGUNTA que ela responde.Defina cor, tipografia e espaçamento uma vez no Figma (como estilos ou variáveis) e exporte para um tema JSON. O painel nasce consistente, e uma mudança de marca deixa de exigir repintar 40 visuais na mão.
{
"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" }
}
}Crie o cartão de KPI uma vez, como componente, com variantes para cada estado (acima da meta, abaixo, sem dado). Todos os cartões do arquivo herdam a mudança. No Power BI o equivalente é agrupar os visuais e reaproveitar o grupo entre páginas, mantendo o tema.
Componente: KPI Card
Propriedades: rótulo, valor, variação, estado
Variantes: estado = positivo | negativo | neutro | vazio
Regra: variação sempre com seta + sinal + cor
Tamanho fixo: 296 x 120 (grade de 8 px)Ligue os frames simulando drill-through, botão voltar e troca de página. Em 15 minutos você descobre que a diretoria queria começar pela visão regional, não pela nacional — descoberta que, feita depois, custaria refazer toda a lógica de filtros.
Fluxo mínimo a prototipar
Home (nacional)
-> clique numa regional -> Página Regional
-> clique numa loja -> Drill-through Loja
-> [ Voltar ] -> Página Regional
-> [ Limpar filtros ] -> HomeTrabalhe em 1280x720 (a proporção da tela do Power BI) com grade de 8 px, e o layout vira posição no painel quase diretamente — o Power BI aceita X, Y, largura e altura numéricos por visual. Combine também os nomes: o rótulo do wireframe deve ser o nome da medida.
Figma Power BI
frame 1280 x 720 -> Exibir > Tamanho da Página > 16:9
grade de 8 px -> Formato > Geral > posição (múltiplos de 8)
estilo de cor -> tema JSON importado
componente KPI -> grupo de visuais copiado entre páginas
rótulo "Margem %" -> nome da medida [Margem %]Entregar não é o fim. Sem medir uso, você não sabe se construiu uma ferramenta ou um enfeite.
Acesso no lançamento todo painel tem — a curiosidade leva. O que importa é o retorno: quantos voltaram na semana seguinte, com que frequência, e se as páginas de detalhe são usadas. Painel com 200 aberturas no primeiro dia e 6 no segundo mês está morto, por mais elogios que tenha recebido.
Adoção usuários únicos / público-alvo meta: > 60% em 30 dias
Retenção voltaram na semana seguinte meta: > 40%
Frequência aberturas por usuário por semana
Profundidade páginas por sessão o detalhe é usado?
Tempo até insight segundos até o primeiro filtro > 30 s = confusoO Service registra visualizações por relatório, por página e por usuário, e permite salvar esse relatório de uso como um semântico próprio, para acompanhar a série histórica. É a fonte mais direta para saber quais páginas ninguém abre — candidatas naturais a sumir.
Workspace > relatório > ... > Abrir relatório de uso
Perguntas que ele responde:
quais páginas são abertas (e quais nunca)
quem abriu nos últimos 30 dias
origem: navegador, app móvel, incorporado
Página com 0 acesso em 60 dias: remova, ou descubra por que ninguém acha.Cinco usuários revelam a maior parte dos problemas de usabilidade. Dê tarefas, não perguntas: descubra qual loja mais caiu em março. Fique calado e cronometre. Você vai ver a pessoa procurar o filtro em três lugares antes de achar — e isso vale mais que qualquer opinião coletada em reunião.
Roteiro de tarefas (grave a tela, com autorização)
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
Anote: tempo, cliques errados, hesitação, frase dita em voz alta.Você gostou? só produz gentileza. Pergunte sobre comportamento passado e concreto: me mostre o que você fez com o número da semana passada. E cuidado com pedido de recurso — quando alguém pede exportar para Excel, o problema real quase sempre é que a tela não responde à pergunta dele.
Evite Prefira
"Gostou do painel?" "Me mostra como você usou ele ontem."
"Quer um gráfico de pizza?" "Que decisão você precisa tomar aqui?"
"Está claro?" "Sem clicar: o que essa tela diz?"
"Falta alguma coisa?" "O que te obrigou a abrir o Excel depois?"Acima de 3 segundos por interação, o usuário perde o fio do raciocínio. Use o Analisador de Desempenho para achar o visual lento e o DAX Studio para investigar a consulta. Os culpados de sempre: visual com 40 mil linhas, medida com FILTER sobre a fato inteira e cartão fazendo DISTINCTCOUNT de coluna de alta cardinalidade.
Exibir > Analisador de Desempenho > Iniciar gravação > Atualizar visuais
Como ler os números:
Consulta DAX > 1000 ms -> problema de medida ou de modelo
Exibição > 500 ms -> visual com pontos/linhas demais
Outro alto -> excesso de visuais na página (mantenha < 15)
Meta prática: página abre em < 3 s, interação responde em < 1 s.Antes de divulgar, resolva quem vê o quê com RLS: o gerente da loja Recife enxerga só a loja dele, e a mesma tela serve para 42 gestores. Depois, trate o painel como produto vivo — hipótese, mudança, medição — e remova sem dó o que os dados de uso mostrarem que ninguém abre.
// Modelagem > Gerenciar Funções > "Gerente Loja"
// Filtro DAX na tabela dLoja:
[Email] = USERPRINCIPALNAME()
// Ciclo mensal
1. Hipótese: "ninguém acha o filtro de canal (está à direita)"
2. Mudança: mover para a faixa superior, junto dos demais
3. Medição: o uso do filtro sobe? o tempo até o 1º filtro cai?
4. Decisão: mantém, reverte ou testa outra coisa