80% dos problemas de lentidão têm origem no modelo de dados. O resto é script, expressão e arquitetura de objeto. Aqui está o mapa completo com scripts reais.
Antes de qualquer mudança, você precisa saber onde está o gargalo. Qlik Sense lento pode ter origem em quatro lugares distintos - e a solução para cada um é diferente.
"Otimizar expressão em um app com synthetic key não resolvido é pintar a parede de uma casa com fundação rachada."
O primeiro passo de qualquer tuning é ler o log de reload com atenção. No Qlik Cloud, acesse: Data Integration → Spaces → [app] → Reload History.
Estrutura do log de reload - o que observar0000 Starting reload...
0001 LOAD statement executed: 2.450.000 rows in 4.2s ← tempo por tabela
0002 Synthetic key warning: [OrderID, ProductID] ← ALERTA CRÍTICO
0003 Store INTO vendas.qvd: 2.450.000 rows in 1.8s
0004 LOAD FROM vendas.qvd: 2.450.000 rows in 0.9s
...
0089 Finished reload. Duration: 00:04:32
RAM peak: 8.4 GB ← consumo de memória
Fields: 847 ← muitos campos = problema
Synthetic key warning no log é o sinal mais claro de problema de modelo. Mais de 200 campos por app é outro sinal de alerta - provavelmente há campos desnecessários em memória.
Synthetic keys são criados automaticamente pelo Qlik quando duas ou mais tabelas compartilham mais de um campo com o mesmo nome. Cada synthetic key gera uma tabela intermediária invisível que consome memória e degrada performance de associação.
// Pedidos e Produtos compartilham
// OrderID E ProductID - synthetic key!
Pedidos:
LOAD OrderID, ProductID,
CustomerID, Amount
FROM pedidos.qvd (qvd);
Produtos:
LOAD ProductID, OrderID,
ProductName, Category
FROM produtos.qvd (qvd);
// Renomear o campo duplicado
// que não deve ser chave de join
Pedidos:
LOAD OrderID, ProductID,
CustomerID, Amount
FROM pedidos.qvd (qvd);
Produtos:
LOAD ProductID,
OrderID AS PedidoRef, // alias
ProductName, Category
FROM produtos.qvd (qvd);
Todo campo carregado no script ocupa memória RAM durante o reload e no app em uso. Campos que existem na fonte mas não são usados em nenhum gráfico ou expressão são desperdício puro.
// LOAD * traz 40+ campos da fonte
// incluindo campos técnicos que
// ninguém usa no dashboard
Vendas:
LOAD * FROM vendas.qvd (qvd);
// Carrega apenas o necessário
// ou usa DROP após LOAD *
Vendas:
LOAD VendaID, ClienteID,
ProdutoID, Valor,
DataVenda, Regiao
FROM vendas.qvd (qvd);
// Alternativa: DROP após LOAD *
DROP FIELDS ETLTimestamp,
BatchID, InternalRef,
LegacyCode;
O Qlik Sense é otimizado para star schema. Modelos snowflake (múltiplos níveis de tabelas de dimensão) aumentam o número de joins internos e degradam performance de associação e cálculo.
// Fatos → Produto → Categoria → Setor
// Cada seleção atravessa 3 tabelas
Fatos → Produtos → Categorias → Setores
// Performance de associação:
// O(n) por nível de join adicional
// Desnormalizar no script de carga
// em vez de manter snowflake
DimProduto:
LOAD p.ProductID, p.ProductName,
c.CategoryName, s.SectorName
FROM produtos.qvd (qvd) AS p
JOIN categorias.qvd (qvd) AS c
ON p.CategoryID = c.CategoryID
JOIN setores.qvd (qvd) AS s
ON c.SectorID = s.SectorID;
O Qlik concatena automaticamente tabelas com os mesmos campos. Quando isso não é desejado - por exemplo, duas tabelas de fatos independentes - use NoConcatenate explicitamente.
// Sem NoConcatenate: Qlik concatena
// VendasNacionais e VendasExportacao
// em uma só tabela - pode não ser o desejado
VendasNacionais:
LOAD ClienteID, Valor, DataVenda
FROM nacionais.qvd (qvd);
// Com NoConcatenate: tabelas separadas
// com associação via ClienteID
NoConcatenate
VendasExportacao:
LOAD ClienteID, Valor, DataVenda
FROM exportacao.qvd (qvd);
Recarregar 10 milhões de linhas toda vez quando apenas 5.000 mudaram é o erro mais caro de performance em Qlik. O padrão de incremental load resolve isso com três blocos: detectar delta, carregar delta, mesclar com QVD existente.
Padrão completo de Incremental Load com QVD// BLOCO 1: Detectar a última data de carga
LET vUltimaCarga = Peek('DataModificacao', -1, 'MaxData');
// BLOCO 2: Carregar delta ou full conforme necessário
IF IsNull(vUltimaCarga) OR NOT QvdNoOfRecords('lib://QVDs/vendas.qvd') > 0 THEN
// Primeira carga - full load
Vendas_Delta:
LOAD VendaID, ClienteID, ProdutoID,
Valor, DataVenda, DataModificacao
FROM [lib://DataSource/vendas.csv]
(txt, utf8, embedded labels, delimiter is ',');
ELSE
// Carga incremental - apenas registros novos/alterados
Vendas_Delta:
LOAD VendaID, ClienteID, ProdutoID,
Valor, DataVenda, DataModificacao
FROM [lib://DataSource/vendas.csv]
(txt, utf8, embedded labels, delimiter is ',')
WHERE DataModificacao >= '$(vUltimaCarga)';
END IF
// BLOCO 3: Mesclar delta com QVD existente
// Carrega o QVD excluindo registros que existem no delta
Vendas:
LOAD * FROM [lib://QVDs/vendas.qvd] (qvd)
WHERE NOT EXISTS(VendaID);
// Adiciona o delta
CONCATENATE(Vendas)
LOAD * RESIDENT Vendas_Delta;
DROP TABLE Vendas_Delta;
// Salva QVD atualizado
STORE Vendas INTO [lib://QVDs/vendas.qvd] (qvd);
// Registra timestamp da carga
MaxData:
LOAD Max(DataModificacao) AS DataModificacao RESIDENT Vendas;
Atenção: o campo de controle (DataModificacao) precisa ser confiável na fonte. Se a fonte não atualiza esse campo corretamente em updates, o incremental load vai perder registros alterados. Valide com a equipe de dados antes de implementar.
Para tabelas de dimensão que raramente mudam (produtos, hierarquias, tabelas de configuração), o Buffer cria um QVD automático e só recarrega da fonte quando o buffer expira ou a fonte muda.
// Buffer com expiração de 7 dias
// Qlik verifica se o QVD de buffer é mais recente
// que 7 dias. Se sim, usa o cache. Se não, recarrega.
Buffer (stale [7 days])
DimProduto:
LOAD ProductID, ProductName,
Category, SubCategory,
UnitCost, Supplier
FROM [lib://MasterData/produtos.csv]
(txt, utf8, embedded labels, delimiter is ',');
// Para dimensões que mudam raramente:
// hierarquia geográfica, tabela de metas anuais,
// plano de contas, tabela de câmbio histórica
Se você tem um app "base" com o modelo de dados completo e vários apps analíticos em cima dele, o Binary carrega o modelo completo sem reprocessar o script.
// Carrega o modelo completo do app base
// sem re-executar o script de ETL
Binary [lib://AppBase/ModeloVendas.qvf];
// A partir daqui, todas as tabelas do app base
// estão disponíveis. Adicione apenas o que é
// específico deste app analítico.
Metas:
LOAD Regiao, Produto, MetaValor, AnoMeta
FROM [lib://Planning/metas_2025.xlsx]
(ooxml, embedded labels, table is Sheet1);
A ordem de execução do script impacta diretamente a performance. Tabelas maiores devem ser carregadas primeiro quando são usadas como base de join. Evite RESIDENT em loops desnecessários.
// Carrega dimensão pequena primeiro
// depois faz join com fatos grandes
DimCliente:
LOAD * FROM clientes.qvd (qvd); // 5k rows
// Join com fatos - Qlik precisa
// expandir dimensão para cada fato
Fatos:
LOAD * FROM vendas.qvd (qvd) // 10M rows
WHERE EXISTS(ClienteID);
// Carrega fatos primeiro
Fatos:
LOAD VendaID, ClienteID, Valor
FROM vendas.qvd (qvd); // 10M rows
// Dimensão em seguida - Qlik associa
// por chave sem re-scan da tabela grande
DimCliente:
LOAD ClienteID, NomeCliente, Segmento
FROM clientes.qvd (qvd); // 5k rows
Esta é a otimização de expressão com maior impacto. IF() no Qlik avalia condição linha por linha em toda a tabela. Set Analysis filtra o conjunto antes da agregação - o que é dramaticamente mais eficiente em datasets grandes.
// Para 10M de linhas, esta expressão
// avalia a condição 10M vezes
Sum(IF(Categoria = 'Eletrônicos',
Valor, 0))
// Ainda pior: IF aninhado
Sum(IF(Ano = 2024,
IF(Regiao = 'Sul', Valor, 0),
0))
// Filtra o conjunto antes de somar
// Muito mais eficiente em qualquer volume
Sum({<Categoria={'Eletrônicos'}>} Valor)
// Múltiplos filtros no mesmo set
Sum({<Ano={2024},
Regiao={'Sul'}>} Valor)
// Regra: sempre prefira Set Analysis
// a IF() em funções de agregação
AGGR() cria uma tabela virtual em tempo de execução para cada combinação de dimensões especificadas. É poderoso mas caro. Use com critério - e considere pré-calcular no script quando a expressão for estática.
// Recalcula para cada renderização
// do gráfico - muito custoso
Avg(AGGR(
Sum(Valor),
ClienteID, MesAno
))
// Se o gráfico tem 50k clientes x 24 meses
// = 1.2M células calculadas a cada render
// Calcula uma vez no reload,
// armazena em QVD, usa no gráfico
VendasMensaisCliente:
LOAD ClienteID, MesAno,
Sum(Valor) AS ValorMensal
FROM vendas.qvd (qvd)
GROUP BY ClienteID, MesAno;
// No gráfico: simplesmente
Avg(ValorMensal) // instantâneo
Se a mesma expressão complexa aparece em múltiplos gráficos, calcular como variável evita recálculo redundante e facilita manutenção.
// No script: define variáveis de expressão
LET vVendasAno = "Sum({<Ano={$(=Max(Ano))}>} Valor)";
LET vVendasAnoAnt = "Sum({<Ano={$(=Max(Ano)-1)}>} Valor)";
LET vCrescimento = "($(vVendasAno) - $(vVendasAnoAnt)) / $(vVendasAnoAnt)";
// Nos gráficos: usa a variável
// - expressão calculada uma vez, reutilizada em N objetos
$(vCrescimento) // em vez de repetir a expressão completa
Cada objeto em um sheet executa suas expressões ao ser renderizado. Sheets com muitos objetos visíveis simultâneos impactam a experiência do usuário mesmo com modelo e script otimizados.
// KPI sem filtro de ano
// avalia todos os registros históricos
Sum(Valor) // 10 anos de dados = 50M linhas
// Use variável de controle de estado
// ou Always One Selected Value no campo Ano
// para garantir contexto mínimo
Sum({<Ano={$(=Max(Ano))}>} Valor)
// Ou configure o campo Ano com
// "Always one selected value" no app props
No Qlik Cloud, acesse o App Overview de qualquer app para visualizar o footprint de memória por tabela, número de campos e distribuição de valores únicos - os dados que revelam onde está o problema.
Engine API - consulta de metadata de memória via REST# Consulta tabelas do app via Qlik Engine API (WebSocket)
# Útil para automação de diagnóstico em múltiplos apps
POST https://SEU_TENANT.us.qlikcloud.com/api/v1/apps/APP_ID/evaluations
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
{
"qExpression": {
"qv": "=FieldValueCount('ClienteID')"
}
}
# Para diagnóstico manual: no Sense hub, clique no app
# → About → RAM usage por tabela
# Foco em: tabelas com RAM desproporcional ao número de linhas
# (indica campos com alta cardinalidade desnecessária)
Adicione marcadores de tempo no script para identificar qual seção está consumindo mais tempo durante o reload.
// Marcadores de tempo no script
LET vT0 = Now();
TRACE === INÍCIO CARGA VENDAS ===;
Vendas:
LOAD * FROM [lib://QVDs/vendas.qvd] (qvd);
LET vT1 = Now();
TRACE === VENDAS: $(Interval(vT1 - vT0, 'mm:ss')) ===;
// Se uma seção demora muito,
// investigue: tamanho do arquivo,
// ausência de QVD, join complexo
LET vT2 = Now();
TRACE === INÍCIO CARGA CLIENTES ===;
DimCliente:
LOAD * FROM [lib://QVDs/clientes.qvd] (qvd);
TRACE === CLIENTES: $(Interval(Now() - vT2, 'mm:ss')) ===;
Performance não é uma feature que se adiciona depois. É uma decisão de arquitetura que começa no modelo de dados. Meça antes, otimize com dados, meça depois.
Modelo com synthetic key? Script sem camada QVD? Expressões IF() em cascata? Conta nos comentários qual é o seu cenário - já passei por todos esses em produção e posso ajudar a priorizar o diagnóstico.
E se este conteúdo foi útil, salva para usar como referência durante o próximo tuning.
#QlikSense #Qlik #PerformanceTuning #BusinessIntelligence #BI #DataEngineering #QlikCloud #DataArchitecture #Analytics