Jhonatan Pinheiro
Cargando página...
Jhonatan Pinheiro
Cargando página...
Jhonatan Pinheiro
Cargando página...
75 temas siguiendo un caso real de retail de punta a punta: Power Query, star schema, el SQL detrás del modelo, DAX de CALCULATE a time intelligence, diseño de dashboards, Figma y métricas de uso.
Un panel bonito que nadie usa es un coste. Un panel feo que responde a la pregunta correcta es útil — pero dura poco, porque nadie defiende lo que da pereza mirar. Esta guía reúne las cuatro competencias que hacen funcionar un producto analítico: SQL (de dónde viene el dato), modelado + DAX (qué significa el número), Power BI Desktop (dónde se convierte en producto) y UX/UI (por qué alguien vuelve mañana).
Todos los ejemplos usan la misma empresa y los mismos números, desde el SELECT hasta la maquetación:
Casa & Construção Nordeste — 42 tiendas en 6 estados, 3 canales (tienda física, e-commerce, televenta), unos 4,2 millones de líneas de venta al año en un SQL Server. La dirección quiere responder a tres preguntas: ¿estamos alcanzando el objetivo de margen?, ¿qué tiendas cayeron respecto al año pasado? y ¿dónde se está comiendo la rotura de stock la facturación?
El modelo: fVendas (hecho, grano = línea del tique), dProduto, dLoja, dCliente, dCalendario y fMetas.
Cómo leerla: si nunca has abierto Power BI, ve del 1 al 75 en orden. Si ya entregas informes, empieza en el 21 — el modelado es donde nace la mayoría de los problemas que aparecen disfrazados de "error de DAX".
Antes de la herramienta: qué estás construyendo, para quién, y qué decisión cambia eso.
Un informe entrega filas para comprobar (el fiscalista quiere la factura 4471). Un panel monitoriza un conjunto pequeño y estable de indicadores para disparar una acción. Un análisis ad hoc responde a una pregunta única y muere después. Mezclar los tres en la misma pantalla es la causa número uno de un panel lento y confuso: el director lo abre para ver el margen y se lleva 40 mil filas de propina.
Dirección -> panel -> 6 indicadores, 1 pantalla, 3 s de carga
Gerente -> panel -> la misma pantalla + drill por tienda
Analista -> ad hoc -> Analizar en Excel / conexión dinámica
Fiscalista -> informe -> paginado (Report Builder), exportableAntes de elegir el visual, escribe la frase: quién decide qué, con qué frecuencia, y qué cambia cuando el número está mal. Si no hay una acción asociada, el indicador es curiosidad — y la curiosidad no merece un sitio en la pantalla principal.
Ficha del indicador (rellénala ANTES de construir)
Decisión: redistribuir el presupuesto de medios entre canales
Quién decide: gerente de trade marketing
Frecuencia: cada lunes, 9 h
Disparador: margen del canal < 22% durante 2 semanas seguidas
Acción: recortar el presupuesto del canal y pasarlo al de mayor margenTodo panel tiene cuatro capas, y cada problema vive en una de ellas. Diagnosticar en la capa equivocada cuesta semanas: gente reescribiendo DAX cuando el problema es el grano del hecho, o cambiando colores cuando el problema es que el número no responde a la pregunta.
1. Origen SQL Server, hojas de cálculo, API -> fiabilidad
2. Modelo star schema, relaciones -> rendimiento y verdad
3. Semántica medidas DAX, formato -> significado
4. Interfaz maquetación, color, interacción -> adopciónUna métrica es lo que se suma (facturación, unidades). Una dimensión es el corte (tienda, producto, fecha). La granularidad es la fila más pequeña del hecho — en nuestro caso, una línea del tique. Nunca puedes bajar por debajo del grano que cargaste: si el hecho llega agregado por día y tienda, la pregunta qué producto tiró de la caída se queda sin respuesta para siempre.
fVendas (grano = línea del tique)
+------------+--------+---------+-----+--------+-------+
| DataVenda | LojaSK | ProdSK | Qtd | Valor | Custo |
+------------+--------+---------+-----+--------+-------+
| 2026-03-14 | 17 | 90421 | 2 | 179,80 | 118,40|
Agregar después es fácil. Desagregar es imposible.Si tres áreas calculan la facturación de tres formas distintas, el panel se convierte en un escenario de discusión en lugar de decisión. Escribe la definición, la fórmula y las excepciones — y ponlo dentro del propio informe, en una página de glosario. Es lo más barato de producir y lo que más reuniones evita.
Facturación Neta
Definición: venta bruta - devoluciones - impuestos sobre la venta
Origen: fVendas.ValorLiquido (ya neto de impuestos en el origen)
Excluye: ventas de prueba (LojaSK = 999) y cambios sin valor
Dueño: Control de Gestión
Actualiza: a diario a las 6 h (D-1)Lo básico de la navegación y las decisiones de configuración que ahorran meses después.
Informe es donde se dibuja. Tabla (antes Datos) es donde se comprueba lo que llegó. Modelo es donde se enlazan las tablas. Un principiante pasa el 100% del tiempo en la primera y tarda semanas en descubrir que el problema estaba en la tercera.
Barra lateral izquierda:
[ ] Informe -> visuales, maquetación, formato
[#] Tabla -> comprobar valores, crear una columna calculada
[<>] Modelo -> relaciones, cardinalidad, ocultar columnasImport copia los datos al modelo en memoria (VertiPaq): rápido y con todo DAX disponible — es el valor por defecto correcto en el 90% de los casos. DirectQuery consulta el origen en cada clic: dato en vivo, pero lento y con DAX limitado. Dual deja que el motor elija, en modelos compuestos. Nuestro hecho de 4,2 millones de filas cabe en Import sin drama (~120 MB comprimidos).
Import -> hasta cientos de millones de filas, actualización programada
DirectQuery -> exigencia de tiempo real (< 5 min) o dato que no puede salir del origen
Dual -> dimensiones pequeñas en un modelo compuesto
Regla práctica: usa DirectQuery solo cuando alguien demuestre,
por escrito, que una actualización de 1 h no lo resuelve.El asistente invita a marcar la tabla y pulsar Cargar — y entonces te traes 60 columnas y 12 años de historia para responder sobre 24 meses. Conecta mediante una vista o una consulta con filtro. Menos columnas significa menos memoria, una actualización más corta y un modelo más legible.
Obtener datos > SQL Server
Servidor: srv-bi.casaeconstrucao.local
Base: DW_Vendas
Modo: Import
En lugar de marcar la tabla fVendas entera:
SELECT ... FROM dbo.vw_fato_vendas WHERE DataVenda >= '2024-01-01'
Trae solo el periodo que muestra el panel (+1 año, para el comparativo).Un servidor y una base fijos en la consulta significan rehacerlo todo al promocionar de preproducción a producción. Crea parámetros en Power Query y referéncialos en el origen: cambiar de entorno se convierte en cambiar dos campos — y, en el Service, es una configuración de parámetro, sin republicar.
let
Fonte = Sql.Database(pServidor, pBanco),
Vendas = Fonte{[Schema="dbo", Item="vw_fato_vendas"]}[Data]
in
VendasPor defecto Power BI crea una tabla de fechas oculta para cada columna de fecha del modelo. Con 8 columnas de fecha, son 8 tablas invisibles, memoria desperdiciada e inteligencia de tiempo impredecible. Desactívala y usa una única dCalendario tuya.
Archivo > Opciones y configuración > Opciones
> Archivo actual > Carga de datos
[ ] Fecha y hora automática <- DESMARCAR
[ ] Detectar automáticamente relaciones <- DESMARCAR (créalas tú)
[ ] Actualizar datos al abrir el archivo <- valorar caso por casoEn Desktop, Actualizar recarga todo ahora. En producción quien actualiza es el Power BI Service, con su propia credencial y, para un origen local, una puerta de enlace instalada en la red de la empresa. El fallo clásico: funciona en tu portátil y se rompe en el Service, porque el SQL Server no es visible desde fuera.
Desktop -> Inicio > Actualizar (usa tu credencial de Windows)
Service -> Configuración del modelo semántico > Actualización programada
+ Puerta de enlace de datos local (origen on-premises)
+ Credencial de servicio (nunca la tuya personal)
Límite: 8 actualizaciones/día (Pro), 48 (Premium/Fabric).El .pbix es un paquete binario — en Git, cada commit se convierte en un blob nuevo y hacer un diff es imposible. El formato .pbip (Power BI Project) graba el modelo y el informe como archivos de texto, así que puedes revisar el cambio de una medida en un pull request, como si fuera código.
Archivo > Guardar como > Proyecto de Power BI (.pbip)
meu-painel.pbip
meu-painel.Dataset/
model.bim <- tablas, relaciones, medidas (texto)
meu-painel.Report/
report.json <- páginas, visuales, maquetación (texto)
Y entonces sí: git diff muestra "la medida Margem % cambió de X a Y".Donde el dato sucio se convierte en una tabla fiable — y donde mucha gente hace el trabajo que SQL haría mejor.
La regla de lo más cerca posible del origen: filtrar, unir y agregar en SQL (el servidor se hizo para eso); moldear el formato y el tipado en Power Query; calcular un indicador que reaccione al filtro en DAX. Sumar en Power Query lo que debería ser una medida crea un número que no responde a la segmentación — y todo el mundo concluye que el panel está mal.
SQL -> JOIN, WHERE, GROUP BY pesado, histórico
Power Query -> tipos, unpivot, merge ligero, columnas derivadas fijas
DAX -> todo lo que cambia con el filtro del usuario
Señal de alerta: una columna calculada llamada "Total del Año".
No cambia cuando el usuario filtra. Debería ser una medida.Un archivo generado en pt-BR usa coma decimal; interpretado como en-US, 179,80 se convierte en 17980 — sin error visible, solo una facturación cien veces mayor. Tipa siempre de forma explícita indicando la cultura de origen.
= Table.TransformColumnTypes(
Fonte,
{{"ValorLiquido", type number}, {"DataVenda", type date}},
"pt-BR"
)El primer paso útil de casi cualquier consulta es tirar lo que no se va a usar: menos memoria, actualización más corta y más posibilidades de query folding. Prefiere Table.SelectColumns (que lista lo que se queda) a quitar columnas — si el origen gana una columna nueva, tu consulta no se rompe.
= Table.SelectColumns(
Fonte,
{"DataVenda", "LojaSK", "ProdutoSK", "Qtd", "ValorLiquido", "Custo"}
)El folding es que Power Query traduzca tus pasos a SQL y haga que el servidor los ejecute. Mientras ocurre, un filtro de fecha cuesta casi nada; cuando se rompe, Power BI se lo descarga todo y filtra en tu máquina. Rompen el folding: Table.Buffer, las funciones M sin equivalente en SQL y las columnas personalizadas complejas.
Clic derecho en el paso > "Ver consulta nativa"
habilitado -> el paso todavía se está traduciendo a SQL
atenuado -> el folding se rompió a partir de aquí
Estrategia: haz TODOS los pasos que hacen folding primero,
y solo después los que lo rompen.Una hoja de objetivos casi siempre llega ancha (Ene, Feb, Mar como columnas). El modelo la necesita alta: una fila por tienda y mes. Anular la dinamización de columnas lo resuelve — y usar Anular la dinamización de otras columnas hace que un mes nuevo en la hoja no rompa la consulta.
// Antes: Loja | Jan | Fev | Mar
// Después: Loja | Mes | MetaValor
= Table.UnpivotOtherColumns(
Fonte,
{"Loja"},
"Mes",
"MetaValor"
)Anexar apila tablas con las mismas columnas (ventas de 2025 + ventas de 2026). Combinar es el JOIN: trae columnas de otra tabla por la clave. En un modelo en estrella, combinar casi siempre es un error — el enlace debe ser una relación, no una columna copiada dentro del hecho.
Anexar (append) -> mismas columnas, más filas
Combinar (merge) -> mismas filas, más columnas
Cuándo combinar es correcto:
unir una dimensión que llegó partida en dos orígenes
Cuándo es un error:
copiar dProduto[Categoria] dentro de fVendas "para facilitar"Cada tienda manda un Excel de inventario. En lugar de 42 consultas iguales, crea una función que reciba el archivo binario y devuelva la tabla limpia, y aplícala sobre la carpeta entera. Cuando la regla cambie, la cambias en un solo sitio.
// 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
SemVazioLos tramos y las clasificaciones fijas (las que no dependen del filtro) pertenecen a Power Query. Y decide siempre qué hacer con un error de conversión: convertirlo en nulo es honesto; romper la actualización es peor; esconderlo con try otherwise 0 sin avisar a nadie es lo peor de todo, porque hace desaparecer el problema y mantiene el número equivocado.
= Table.AddColumn(Fonte, "FaixaTicket", each
if [ValorLiquido] >= 500 then "Alto"
else if [ValorLiquido] >= 150 then "Medio"
else "Baixo", type text)
// un error controlado, marcado para auditarlo después
= Table.AddColumn(Fonte, "CustoTratado", each
try Number.From([Custo]) otherwise null, type nullable number)Aquí nace casi todo problema que después aparece disfrazado de error de DAX o de lentitud.
Una tabla gigante con todo dentro parece sencilla y es una trampa: la categoría del producto se repite 4,2 millones de veces, los filtros se vuelven lentos y no hay forma de listar un producto que no vendió. En un star schema, el hecho guarda números y claves; las dimensiones guardan descripciones. Es la forma para la que se construyó el motor de Power BI.
dCalendario
|
dLoja -- fVendas -- dProduto
|
dCliente
hecho = números + claves (estrecha y larga)
dimensión = textos + jerarquías (ancha y corta)Si la columna responde a cuánto y tiene sentido sumarla, es un hecho. Si responde a quién, qué, dónde, cuándo y quieres filtrar o agrupar por ella, es una dimensión. Prueba rápida: sumar códigos postales no significa nada — el código postal es una dimensión, aunque sea numérico.
fVendas : Qtd, ValorLiquido, Custo, Desconto (sumables)
dProduto : SKU, Descripción, Categoría, Marca (agrupables)
dLoja : Nombre, Ciudad, Provincia, Regional, Formato
dCalendario: Date, Año, NombreMes, Trimestre, DíaSemanaEl valor por defecto sano es uno a muchos (1:*) con el filtro en dirección única, de la dimensión al hecho. Muchos a muchos y el filtro bidireccional resuelven casos concretos, pero generan ambigüedad y lentitud — y son la explicación más común de un total que no cuadra con la suma de las partes.
dLoja[LojaSK] 1 ---- * fVendas[LojaSK] dirección: única (dLoja -> fVendas)
Filtro cruzado bidireccional: úsalo solo cuando necesites
filtrar la dimensión por el hecho — y prefiere resolverlo
con CROSSFILTER dentro de la medida, no en la relación.La inteligencia de tiempo (TOTALYTD, SAMEPERIODLASTYEAR) exige una tabla de fechas continua, sin huecos, que cubra años enteros y marcada como tabla de fechas. Sin eso, la comparación con el año anterior devuelve un resultado silenciosamente erróneo en los bordes del periodo.
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")
)
// Después: Herramientas de tabla > Marcar como tabla de fechas > columna DateSin configurarlo, el eje muestra abr, ago, dic, feb... Selecciona la columna de texto y usa Ordenar por columna, apuntando al número del mes. Vale para cualquier texto con un orden propio: tramo de tique, tamaño de tienda, etapa del embudo.
dCalendario > columna MesNome
> Herramientas de columna > Ordenar por columna > MesNum
Cuidado: la columna de ordenación necesita tener exactamente
1 valor por etiqueta — si no, Power BI la rechaza.El hecho tiene fecha de pedido y fecha de entrega. Solo una relación puede estar activa a la vez con dCalendario; la otra queda inactiva y se activa bajo demanda dentro de la medida. Así es como muestras la facturación por fecha de venta y, al lado, las entregas por fecha de entrega — en el mismo eje.
Entregas =
CALCULATE(
[Faturamento],
USERELATIONSHIP( fVendas[DataEntrega], dCalendario[Date] )
)No toda tabla necesita una relación. Una tabla sin ningún enlace sirve para que el usuario elija un valor (un porcentaje de ajuste, un escenario) que la medida lee con SELECTEDVALUE. Es la base de cualquier simulación en Power BI.
// Modelado > Nuevo parámetro > Numérico
ReajustePreco = GENERATESERIES( 0, 0.20, 0.01 )
Faturamento Simulado =
VAR Reajuste = SELECTEDVALUE( ReajustePreco[Valor], 0 )
RETURN
[Faturamento] * ( 1 + Reajuste )Quien usa el panel no debería ver LojaSK, ProdutoSK ni las columnas crudas del hecho. Oculta las claves, oculta lo que ya se convirtió en medida y agrupa las medidas en carpetas de visualización. Un modelo con 12 elementos visibles es usable; uno con 140 empuja a todo el mundo de vuelta a Excel.
Vista de modelo > selecciona la columna > Propiedades
Está oculto: Sí
Carpetas de visualización de las medidas:
Ventas/ Faturamento, Ticket Médio, Unidades
Rentabilidad/ Margem R$, Margem %
Comparativos/ Faturamento AA, Var % AA, YTD
Objetivos/ Meta, Atingimento %La capa que nadie ve y que decide si el panel se actualiza en 2 minutos o en 2 horas.
No apuntes Power BI a tablas físicas. Crea vistas con nombres de negocio, columnas ya renombradas y reglas aplicadas (la exclusión de la tienda de pruebas, por ejemplo). Cuando la tabla física cambie, arreglas la vista — y ninguno de los 30 informes se rompe.
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 -- tienda de pruebas
AND v.StatusCupom = 'FECHADO';El panel muestra 24 meses y compara con el año anterior — así que 36 meses bastan. Recortar 9 años de historia bajó la actualización de 38 a 6 minutos y el modelo de 480 MB a 120 MB. Deja el histórico completo en un informe aparte, para quien de verdad lo necesite.
SELECT ...
FROM dbo.vw_fato_vendas
WHERE DataVenda >= DATEADD(
MONTH, -36,
DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1)
);Si ninguna página baja por debajo de día + tienda + categoría, agrega a ese grano en SQL. Nuestro hecho bajó de 4,2 millones a 380 mil filas y el panel se volvió instantáneo. Hazlo solo con la certeza de que no se va a pedir el detalle — desagregar después es imposible.
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 y SUM() OVER resuelven en el servidor lo que en DAX costaría caro sobre una tabla grande. Útil sobre todo para snapshots históricos — el ranking congelado de cada mes, que no debe cambiar cuando el usuario 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;La actualización incremental hace que Power BI recargue solo la ventana reciente (los últimos 10 días, por ejemplo) y mantenga el resto. Exige una columna de fecha/hora en el origen y los parámetros reservados RangeStart y RangeEnd — con esos nombres exactos — usados como filtro en la consulta.
// Power Query: parámetros RangeStart y RangeEnd (tipo Fecha/Hora)
= Table.SelectRows(Fonte, each
[DataVenda] >= RangeStart and [DataVenda] < RangeEnd)
// Modelado > Actualización incremental:
// Archivar los datos de los últimos 3 años
// Actualizar incrementalmente los últimos 10 díasLa consulta de la actualización es siempre la misma: un filtro por fecha y una proyección de pocas columnas. Un índice por la fecha de venta, con las columnas usadas incluidas, evita recorrer toda la tabla. Mide antes y después — en nuestro caso, 4min12s se convirtieron en 47s.
CREATE NONCLUSTERED INDEX IX_FatoVendas_Data
ON dbo.FatoVendasItem (DataVenda)
INCLUDE (LojaSK, ProdutoSK, Quantidade, ValorLiquido, CustoUnitario);
-- Comprueba el plan antes y después:
SET STATISTICS IO, TIME ON;Un nulo que llega al modelo se convierte en (En blanco) en el visual, y un (En blanco) en una dimensión genera esa categoría fantasma que nadie sabe explicar en una reunión. Trátalo en la vista, con una etiqueta explícita — el usuario necesita saber que existen ventas sin categoría, no verlas desaparecer.
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;Si el cálculo no cambia según lo que pulse el usuario, hazlo en SQL: es más rápido, versionado y comprobable. Si cambia con el filtro (participación en el total, comparación con el periodo seleccionado), tiene que ser DAX — es exactamente para eso que existe el lenguaje.
SQL: margen por línea de venta (fijo por fila)
SQL: la clasificación ABC del producto (recalculada en el ETL)
DAX: % de la facturación del filtro actual (cambia en cada clic)
DAX: variación contra el año anterior (depende del periodo elegido)DAX no es una fórmula de Excel. La sintaxis se aprende en una tarde; el contexto de evaluación lleva unos meses.
Una columna calculada se calcula en la actualización, ocupa memoria y es fija por fila. Una medida se calcula al vuelo, según el filtro del visual, y no ocupa memoria. Por defecto: usa una medida. Usa una columna solo cuando la necesites para filtrar, agrupar o como eje de un gráfico.
// Columna: existe por fila, sirve para filtrar/agrupar
fVendas[MargemLinha] = fVendas[ValorLiquido] - fVendas[Custo]
// Medida: reacciona al filtro del visual
Margem R$ = SUM( fVendas[ValorLiquido] ) - SUM( fVendas[Custo] )El contexto de fila existe dentro de una columna calculada o de un iterador (SUMX): hay una fila actual. El contexto de filtro es el conjunto de filtros que vienen del visual, de las segmentaciones y de las relaciones. Toda medida se evalúa dentro de un contexto de filtro — entender eso es entender DAX.
Visual: fila = "Loja Recife", columna = "Marzo/2026"
El contexto de filtro de esa celda:
dLoja[Nome] = "Loja Recife"
dCalendario[AnoMes] = "2026-03"
+ lo que haya en las segmentaciones y los filtros de página
La medida se ejecuta una vez POR CELDA, en ese contexto.SUM suma una columna que ya existe. SUMX recorre la tabla fila a fila, evalúa una expresión y suma el resultado. Siempre que el cálculo tenga que ocurrir antes de la suma — precio por cantidad, por ejemplo — el iterador es obligatorio.
// Bien: multiplica por fila, después suma
Receita Bruta = SUMX( fVendas, fVendas[Qtd] * fVendas[PrecoUnitario] )
// Mal: lo suma todo y multiplica al final
Receita Errada = SUM( fVendas[Qtd] ) * SUM( fVendas[PrecoUnitario] )COUNTROWS cuenta las filas del hecho (artículos vendidos). DISTINCTCOUNT cuenta valores distintos (clientes que compraron). Cuidado: DISTINCTCOUNT es caro en una columna de alta cardinalidad y no es aditivo — la suma de los meses nunca da el año, porque quien compró en enero y en marzo es una sola persona.
Itens Vendidos = COUNTROWS( fVendas )
Cupons = DISTINCTCOUNT( fVendas[CupomID] )
Clientes Ativos = DISTINCTCOUNT( fVendas[ClienteSK] )
// Ene: 1.200 | Feb: 1.350 | Total: 1.910
// No es un error: es el comportamiento esperado.La división por cero genera un error o infinito, y el visual muestra algo que nadie entiende. DIVIDE trata el denominador cero o vacío y te deja elegir el resultado alternativo. Normalmente es mejor devolver vacío que cero — un cero miente al afirmar que hubo una venta con margen nulo.
Margem % = DIVIDE( [Margem R$], [Faturamento] ) // vacío si no hubo ventas
Ticket Médio = DIVIDE( [Faturamento], [Cupons] )
Atingimento % = DIVIDE( [Faturamento], [Meta], 0 ) // aquí el cero tiene sentidoCALCULATE evalúa una expresión modificando el contexto de filtro. Es la función más importante del lenguaje: los comparativos, las participaciones y los escenarios pasan todos por ella. Los filtros que se le pasan sustituyen al filtro existente en esa columna y mantienen los demás.
Faturamento E-commerce =
CALCULATE( [Faturamento], dLoja[Canal] = "E-commerce" )
// Varios filtros = Y (AND)
Fat Ecom Nordeste =
CALCULATE(
[Faturamento],
dLoja[Canal] = "E-commerce",
dLoja[Regional] = "Nordeste"
)Un filtro simple (columna = valor) resuelve la mayoría de los casos. Cuando la condición compara con una medida o involucra más de una columna, hace falta FILTER, que devuelve una tabla. Filtra siempre la tabla más pequeña posible — usar FILTER sobre el hecho de 4 millones de filas cuando bastaba la dimensión es la receta de la lentitud.
// Tiques por encima de 500 (compara con el valor de la fila)
Vendas Alto Ticket =
CALCULATE( [Faturamento], FILTER( fVendas, fVendas[ValorLiquido] > 500 ) )
// Mejor: filtra la dimensión, no el hecho
Fat Categorias Premium =
CALCULATE( [Faturamento], FILTER( dProduto, dProduto[PrecoLista] > 500 ) )ALL/REMOVEFILTERS ignoran los filtros y dan el total general. ALLSELECTED respeta lo que el usuario eligió en las segmentaciones e ignora solo el filtro del propio visual — es lo que quieres en casi todo porcentaje del total, para que la participación sume 100% dentro de la selección.
% do Total Geral =
DIVIDE( [Faturamento], CALCULATE( [Faturamento], REMOVEFILTERS( dProduto ) ) )
% do Selecionado =
DIVIDE( [Faturamento], CALCULATE( [Faturamento], ALLSELECTED( dProduto ) ) )
// El usuario elige 3 categorías:
// ALL -> suma 41% | ALLSELECTED -> suma 100%Una variable se evalúa una vez y se reutiliza — evita recalcular la misma medida tres veces y deja la fórmula legible. Un detalle importante: guarda el valor en el contexto en que se declaró, así que no le afecta el CALCULATE que venga después.
Var % AA =
VAR Atual = [Faturamento]
VAR Anterior = CALCULATE( [Faturamento], SAMEPERIODLASTYEAR( dCalendario[Date] ) )
VAR Delta = Atual - Anterior
RETURN
IF( NOT ISBLANK( Anterior ), DIVIDE( Delta, Anterior ) )Las medidas que aparecen prácticamente en todo panel de ventas — y los detalles que hacen funcionar cada una.
TOTALYTD acumula desde el primer día del año hasta la última fecha del contexto. Si el año fiscal de la empresa no empieza en enero, indica el fin del año fiscal en el tercer argumento — en el retail es común cerrar en junio o julio.
Faturamento YTD = TOTALYTD( [Faturamento], dCalendario[Date] )
// Año fiscal que termina el 30 de junio
Faturamento YTD Fiscal = TOTALYTD( [Faturamento], dCalendario[Date], "06-30" )SAMEPERIODLASTYEAR desplaza el periodo entero del contexto un año atrás. Para desplazamientos más libres (2 meses, 3 trimestres), usa DATEADD. Ambos exigen una tabla de fechas marcada — sin ella, el resultado es silenciosamente incorrecto.
Faturamento AA =
CALCULATE( [Faturamento], SAMEPERIODLASTYEAR( dCalendario[Date] ) )
Faturamento M-3 =
CALCULATE( [Faturamento], DATEADD( dCalendario[Date], -3, MONTH ) )Una serie mensual de retail es demasiado dentada para leer la tendencia (diciembre siempre se dispara). La media móvil la suaviza y muestra la dirección real. Úsala como línea sobre las barras del mes, no en su lugar — el gestor necesita las dos.
Média Móvel 3M =
AVERAGEX(
DATESINPERIOD( dCalendario[Date], MAX( dCalendario[Date] ), -3, MONTH ),
[Faturamento]
)RANKX ordena los valores de una tabla según una expresión. El detalle que tumba a los principiantes: sin ALL en la tabla de referencia, cada fila se convierte en el único elemento del ranking y el resultado es 1 para todo el mundo.
Posição Loja =
RANKX( ALL( dLoja[Nome] ), [Faturamento], , DESC, DENSE )
// El ranking de la caída: quién perdió más contra el año anterior
Posição Queda =
RANKX( ALL( dLoja[Nome] ), [Var % AA], , ASC, DENSE )TOPN devuelve una tabla con las N mejores filas, usada dentro de CALCULATE para responder a preguntas del tipo cuánto representan las 10 mayores tiendas. Una tabla virtual no aparece en el modelo: existe solo durante la evaluación de la 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 ) ) )En lugar de tres visuales casi idénticos, deja que el usuario elija el indicador en una segmentación. Menos pantalla, menos mantenimiento, más control sobre lo que se lee. La tabla de selección está desconectada y la medida lee la elección con SELECTEDVALUE.
// Tabla 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()
)Un título fijo obliga al lector a mirar las segmentaciones para saber qué está viendo — y es la causa más común de que una captura del panel se malinterprete en el grupo de WhatsApp. Una medida de texto en el título lo resuelve, y sobrevive a la captura de pantalla.
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 > Aplicar formato por campo > Título PáginaEl formato condicional por medida deja la regla explícita y reutilizable. El semáforo es útil cuando hay un objetivo claro — pero evita pintarlo todo: si la tabla entera está coloreada, nada llama la atención. Y nunca informes solo mediante el color (mira el punto 59).
Cor Atingimento =
VAR Ating = [Atingimento %]
RETURN
SWITCH( TRUE(),
Ating >= 1, "#1B7F4B", // alcanzó el objetivo
Ating >= 0.9, "#B8860B", // cerca
"#B23A48" // por debajo
)
// Visual > Formato condicional > Color de fuente > Aplicar formato por campoLas medidas de razón (tique medio, margen %) muestran en la fila de total un valor que a veces no tiene sentido — una media de medias, objetivos individuales sumados. HASONEVALUE (o ISINSCOPE) permite decidir qué mostrar en el total, en lugar de dejar un número que engaña.
Meta por Loja =
IF(
HASONEVALUE( dLoja[Nome] ),
[Meta],
BLANK() // en el total, el objetivo individual no tiene sentido
)El diseño aquí no es adorno: es la diferencia entre que el gestor decida en 10 segundos y que mande un correo pidiendo explicaciones.
Enséñale la pantalla a alguien del público objetivo durante 5 segundos, escóndela y pregunta: ¿qué has visto? ¿está bien o mal? ¿qué harías ahora? Si no sabe responder, el problema no es falta de datos — es falta de jerarquía. Hazlo con 5 personas antes de publicar: es la devolución más barata que existe.
Guion (10 minutos por persona)
1. "Sin hacer clic, mira durante 5 segundos." -> esconder la pantalla
2. "¿Qué te está diciendo esta pantalla?"
3. "¿El resultado está bien o mal?"
4. "¿Qué harías con esta información?"
5. "¿Qué te ha resultado confuso?"
3 de 5 personas equivocándose en lo mismo = un problema de diseño, no suyo.La lectura occidental empieza en la esquina superior izquierda. Pon ahí el indicador más importante, grande. El contexto y el detalle bajan y van a la derecha. Los filtros van en una franja fija (arriba o en el lateral izquierdo) — nunca dispersos, porque el usuario necesita saber de un vistazo qué está filtrado.
+---------------------------------------------------------------+
| Título dinámico [ Periodo v ] [ Regional v ] | filtros
+---------------------------------------------------------------+
| FACTURACIÓN MARGEN % TIQUE MEDIO CUMPL. OBJ. | qué
| 18,4 mi 23,7% 214 92% |
+---------------------------------------------------------------+
| Evolución mensual (barras + línea AA) | Top 10 tiendas | por qué
+--------------------------------------+------------------------+
| Tabla por tienda: fact., var %, margen, cumplimiento | dónde actuar
+---------------------------------------------------------------+Cada intención tiene una forma que el cerebro lee más rápido: comparar entre categorías pide barras; evolucionar en el tiempo pide una línea; componer un todo pide una barra apilada; relacionar dos medidas pide una dispersión. Un gráfico de tarta solo con 2 o 3 porciones — el ojo compara ángulos mucho peor que longitudes.
Comparar categorías -> barras horizontales, ordenadas por valor
Evolucionar en el tiempo -> línea (continuo) o columnas (periodos discretos)
Componer un todo -> barra 100% apilada > tarta
Relacionar 2 medidas -> dispersión (p. ej. margen x volumen por tienda)
Distribuir -> histograma
1 número + objetivo -> tarjeta con la variación, no un medidorTres paletas, un uso para cada una: categórica (colores distintos para elementos sin orden), secuencial (intensidad para la magnitud) y divergente (dos polos con un neutro en medio, para la variación en torno a cero). Fija el color de cada canal en todo el informe: si el e-commerce es azul, es azul en todas las páginas. Y deja la mayor parte de la pantalla en gris — el color es para lo que necesita atención.
Categórica canales: tienda #2B6CB0 | e-commerce #2C7A7B | televenta #6B46C1
Secuencial volumen: #EBF4FF -> #1A365D
Divergente var %: #B23A48 <- #E8E8E8 -> #1B7F4B
Regla 60-30-10: 60% neutro, 30% apoyo, 10% destaque.Cerca del 8% de los hombres tiene alguna deficiencia en la visión del color — en una dirección de 12 personas, es probable que alguien no distinga tu rojo de tu verde. Nunca informes solo mediante el color: añade una flecha, un signo o una etiqueta. Garantiza un contraste mínimo de 4,5:1 en el texto, rellena el texto alternativo de los visuales y revisa el orden de tabulación.
Mal: [ ] verde [ ] rojo
Bien: v +8,2% ^ -3,1% (símbolo + signo + color)
Checklist antes de publicar:
[ ] contraste >= 4,5:1 (texto) y 3:1 (elementos gráficos)
[ ] nada informado SOLO mediante el color
[ ] texto alternativo en cada visual (Formato > General > Texto alternativo)
[ ] orden de tabulación revisado (Vista > Orden de tabulación)
[ ] fuente >= 10 pt; etiquetas sin rotación de 45 grados18.437.219,63 en una tarjeta es ruido: nadie decide con los céntimos. Abrevia en la vista ejecutiva (18,4 mi) y deja el detalle para la tabla de trabajo. Los números siempre alineados a la derecha, el texto a la izquierda, y la misma cantidad de decimales en toda la columna.
Tarjeta ejecutiva: 18,4 mi (1 decimal, unidad abreviada)
Tabla analítica: 18.437.219,63 (2 decimales, alineado a la derecha)
Porcentaje: 23,7% (1 decimal; 2 solo si la decisión lo exige)
Variación: +8,2 p.p. (punto porcentual != porcentaje)Toda la tinta de la pantalla debería llevar información. Un borde doble, una sombra, un fondo de color, un degradado, un gráfico 3D y un eje redundante compiten con el dato. Un panel limpio no es un panel vacío: es uno donde todo lo que queda tiene una función.
Quita: 3D, sombra, degradado, borde decorativo,
líneas de cuadrícula fuertes, el eje Y cuando hay etiquetas de datos,
la leyenda cuando hay una sola serie,
un icono que solo repite el texto de al lado
Mantén: etiquetas de datos O el eje (nunca los dos),
espacio en blanco entre bloques (mínimo 8 px)El usuario necesita saber qué va a pasar antes de hacer clic. Estandarízalo: una segmentación filtra toda la página; hacer clic en una barra resalta las demás; el detalle vive en un drill-through explícito. Y ofrece siempre el camino de vuelta — un botón Limpiar filtros visible resuelve la mitad de las incidencias de soporte.
Editar interacciones (pestaña Formato): defínelo visual a visual
KPI -> no le afecta el clic en el gráfico de tiendas
Mapa -> resalta (highlight), no filtra
Botones que no pueden faltar:
[ Limpiar filtros ] un marcador con "Datos" activo y sin selección
[ Atrás ] en las páginas de drill-through
Información sobre herramientas de página para el detalle rico (minigráfico al pasar)Un panel bonito con datos bonitos es fácil. Lo que tumba la confianza es la pantalla en blanco cuando el filtro no devuelve nada, o el valor a cero cuando la actualización de las 6 h falló. Escribe mensajes explícitos y muestra siempre la fecha de la última actualización.
Sin datos con el filtro:
"Ninguna venta para Regional = Sur en Marzo/2026.
Prueba a ampliar el periodo." (no dejes la pantalla en blanco)
Pie fijo, en todas las páginas:
"Datos hasta 19/08/2026 06:12 · origen: DW_Vendas · dudas: bi@empresa.com"
Una medida de frescura:
Última Atualização = MAX( fVendas[DataCarga] )Dibujar antes de construir: 40 minutos en Figma ahorran dos semanas de retrabajo en Power BI.
Cambiar un rectángulo en Figma cuesta segundos; cambiar un panel terminado cuesta remodelado, medidas nuevas y una nueva validación. El boceto también cambia la conversación con el cliente: deja de discutir si el gráfico queda bien a la derecha y pasa a discutir si la pregunta es la correcta.
El coste de cambiar una decisión de maquetación
Boceto en papel ~ 2 min
Wireframe en Figma ~ 10 min
Prototipo en Figma ~ 30 min
Panel construido ~ 2 a 5 días (modelo + medidas + pruebas)Sin color, sin datos reales, sin una fuente bonita: cajas grises con etiquetas. El objetivo es discutir qué va en cada área y en qué orden de importancia. La baja fidelidad es una ventaja — nadie discute el tono de azul en un dibujo gris, y eso es exactamente lo que quieres en esta fase.
Frame 1280 x 720 (la proporción del lienzo de Power BI, 16:9)
[ franja ] título + filtros altura 72
[ 4 cajas ] KPIs altura 120
[ 2 cajas ] evolución | ranking altura 260
[ 1 caja ] tabla de detalle altura 200
Escribe en cada caja la PREGUNTA que responde.Define el color, la tipografía y el espaciado una vez en Figma (como estilos o variables) y expórtalos a un tema JSON. El panel nace consistente, y un cambio de marca deja de exigir repintar 40 visuales a mano.
{
"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" }
}
}Crea la tarjeta de KPI una vez, como componente, con variantes para cada estado (por encima del objetivo, por debajo, sin datos). Todas las tarjetas del archivo heredan el cambio. En Power BI el equivalente es agrupar los visuales y reutilizar el grupo entre páginas, manteniendo el tema.
Componente: KPI Card
Propiedades: etiqueta, valor, variación, estado
Variantes: estado = positivo | negativo | neutro | vacío
Regla: la variación siempre con flecha + signo + color
Tamaño fijo: 296 x 120 (rejilla de 8 px)Enlaza los frames simulando el drill-through, el botón de volver y el cambio de página. En 15 minutos descubres que la dirección quería empezar por la vista regional, no por la nacional — un descubrimiento que, hecho más tarde, costaría rehacer toda la lógica de filtros.
El flujo mínimo que prototipar
Home (nacional)
-> clic en una regional -> Página Regional
-> clic en una tienda -> Drill-through Tienda
-> [ Atrás ] -> Página Regional
-> [ Limpiar filtros ] -> HomeTrabaja a 1280x720 (la proporción del lienzo de Power BI) con una rejilla de 8 px, y la maquetación se convierte en una posición en el panel casi directamente — Power BI acepta una X, Y, anchura y altura numéricas por visual. Acuerda también los nombres: la etiqueta del wireframe debería ser el nombre de la medida.
Figma Power BI
frame 1280 x 720 -> Vista > Tamaño de página > 16:9
rejilla de 8 px -> Formato > General > posición (múltiplos de 8)
estilo de color -> tema JSON importado
componente KPI -> grupo de visuales copiado entre páginas
etiqueta "Margem %" -> el nombre de la medida [Margem %]Entregar no es el final. Sin medir el uso, no sabes si construiste una herramienta o un adorno.
Visitas en el lanzamiento las tiene todo panel — la curiosidad las lleva. Lo que importa es el regreso: cuántos volvieron a la semana siguiente, con qué frecuencia, y si se usan las páginas de detalle. Un panel con 200 aperturas el primer día y 6 al segundo mes está muerto, por muchos elogios que haya recibido.
Adopción usuarios únicos / público objetivo objetivo: > 60% en 30 días
Retención volvieron la semana siguiente objetivo: > 40%
Frecuencia aperturas por usuario por semana
Profundidad páginas por sesión ¿se usa el detalle?
Tiempo hasta insight segundos hasta el primer filtro > 30 s = confusoEl Service registra las visualizaciones por informe, por página y por usuario, y permite guardar ese informe de uso como un modelo semántico propio, para seguir la serie histórica. Es la fuente más directa para saber qué páginas no abre nadie — candidatas naturales a desaparecer.
Área de trabajo > informe > ... > Ver informe de métricas de uso
Preguntas que responde:
qué páginas se abren (y cuáles nunca)
quién lo abrió en los últimos 30 días
origen: navegador, app móvil, insertado
Una página con 0 visitas en 60 días: quítala, o averigua por qué nadie la encuentra.Cinco usuarios revelan la mayor parte de los problemas de usabilidad. Dales tareas, no preguntas: averigua qué tienda cayó más en marzo. Quédate callado y cronometra. Verás a la persona buscar el filtro en tres sitios antes de encontrarlo — y eso vale más que cualquier opinión recogida en una reunión.
Guion de tareas (graba la pantalla, con permiso)
T1. ¿Cuál fue la facturación de marzo? esperado < 10 s
T2. ¿Qué tienda cayó más contra el año pasado? esperado < 30 s
T3. En esa tienda, ¿qué categoría tiró de la caída? esperado < 45 s
T4. Exporta la lista de tiendas por debajo del objetivo. esperado < 30 s
Anota: tiempo, clics erróneos, dudas, frases dichas en voz alta.¿Te ha gustado? solo produce cortesía. Pregunta por el comportamiento pasado y concreto: enséñame qué hiciste con el número de la semana pasada. Y cuidado con las peticiones de funcionalidades — cuando alguien pide exportar a Excel, el problema real casi siempre es que la pantalla no responde a su pregunta.
Evita Prefiere
"¿Te ha gustado el panel?" "Enséñame cómo lo usaste ayer."
"¿Quieres un gráfico de tarta?" "¿Qué decisión necesitas tomar aquí?"
"¿Está claro?" "Sin hacer clic: ¿qué dice esta pantalla?"
"¿Falta algo?" "¿Qué te obligó a abrir Excel después?"Por encima de 3 segundos por interacción, el usuario pierde el hilo del razonamiento. Usa el Analizador de rendimiento para encontrar el visual lento y DAX Studio para investigar la consulta. Los culpables de siempre: un visual con 40 mil filas, una medida con FILTER sobre el hecho entero y una tarjeta haciendo un DISTINCTCOUNT de una columna de alta cardinalidad.
Vista > Analizador de rendimiento > Iniciar grabación > Actualizar objetos visuales
Cómo leer los números:
Consulta DAX > 1000 ms -> un problema de medida o de modelo
Presentación > 500 ms -> un visual con demasiados puntos/líneas
Otro alto -> exceso de visuales en la página (mantenlo < 15)
Objetivo práctico: la página abre en < 3 s, una interacción responde en < 1 s.Antes de divulgarlo, resuelve quién ve qué con RLS: el gerente de la tienda de Recife ve solo su tienda, y la misma pantalla sirve para 42 gestores. Después, trata el panel como un producto vivo — hipótesis, cambio, medición — y quita sin pena lo que los datos de uso muestren que nadie abre.
// Modelado > Administrar roles > "Gerente Loja"
// Filtro DAX en la tabla dLoja:
[Email] = USERPRINCIPALNAME()
// Ciclo mensual
1. Hipótesis: "nadie encuentra el filtro de canal (está a la derecha)"
2. Cambio: moverlo a la franja superior, junto a los demás
3. Medición: ¿sube el uso del filtro? ¿baja el tiempo hasta el 1er filtro?
4. Decisión: se mantiene, se revierte o se prueba otra cosa