Otimizar relatorio lento — checklist de performance
Diagnostico completo pra acelerar relatorios Protheus: indices, TCQuery, joins SQL, cache, paginacao. Antes de comprar hardware, otimize o codigo.
"O relatorio demora 2 horas" — frase classica em projeto Protheus. Antes de pedir mais CPU/RAM, 80% dos casos sao otimizaveis no codigo. Checklist completo abaixo.
Diagnostico rapido (5 min)
- Quantos registros sao varridos? Use
Time()antes e depois pra medir. - Quantas queries SQL sao executadas? Em base TopConn, ative log SQL.
- Tem loop sobre alias com chamada a funcao pesada (SaldoTit em loop)?
- Tem DBSeek em ordem errada (sem indice adequado)?
- Esta usando MsAguarde com refresh em loop apertado?
Top 10 culpados (em ordem de frequencia)
1. SaldoTit/CalcAcrs em loop
Cada chamada gera nova query. Para 5000 titulos = 5000 queries.
Fix: agregar via TCQuery uma vez.
// ERRADO — 5000 queries
SE1->(DBSetOrder(1))
SE1->(DBSeek(...))
While !SE1->(Eof()) ...
nSaldo := SaldoTit(SE1->E1_PREFIXO, SE1->E1_NUM, ...)
nTotal += nSaldo
SE1->(DBSkip())
EndDo
// CERTO — 1 query agregada
cQry := "SELECT SUM(E1_VALOR - E1_VALLIQ - E1_VALPAR) AS TOTAL " + ;
" FROM " + RetSqlName("SE1") + ;
" WHERE E1_FILIAL = '" + xFilial("SE1") + "'" + ;
" AND E1_BAIXA = ' '" + ;
" AND D_E_L_E_T_= ' '"
TCQuery cQry New Alias "Q"
nTotal := Q->TOTAL
Q->(DBCloseArea())
2. DBSeek sem indice adequado
Buscar SE1 pelo cliente sem usar o indice E1_CLIENTE = full table scan.
Fix: use DBSetOrder(N) apontando pro indice certo. Cheque SIX.
3. Posicione() em loop
Mesma logica de SaldoTit — Posicione varre N vezes desnecessariamente.
Fix: JOIN SQL.
// ERRADO
While !SE1->(Eof())
cNome := Posicione("SA1", 1, xFilial("SA1") + SE1->E1_CLIENTE + SE1->E1_LOJA, "A1_NOME")
// ...
EndDo
// CERTO — JOIN no SQL
cQry := "SELECT SE1.*, SA1.A1_NOME " + ;
" FROM " + RetSqlName("SE1") + " SE1 " + ;
" INNER JOIN " + RetSqlName("SA1") + " SA1 " + ;
" ON SA1.A1_COD = SE1.E1_CLIENTE " + ;
" AND SA1.A1_LOJA = SE1.E1_LOJA " + ;
" AND SA1.D_E_L_E_T_ = ' '" + ;
" WHERE SE1.E1_FILIAL = '" + xFilial("SE1") + "'"
4. RecLock em loop sem necessidade
Se voce so esta lendo, nao da RecLock! RecLock tem custo (gera bloqueio).
5. MemoWrite em loop
Abrir/fechar arquivo a cada iteracao = lentidao IO.
Fix: acumular em variavel string, gravar uma vez no fim.
6. ConOut em loop apertado
ConOut grava em arquivo a cada chamada. 100k iteracoes = 100k IO.
Fix: usar FwLogger com nivel apropriado, ou comentar logs de debug.
7. dbSkip apos filtro errado
Se voce nao filtra por xFilial, varre todas as filiais e perde tempo.
8. PadR/SubStr em loop apertado em strings gigantes
Operacoes string sao O(n) em comprimento. Em loops massivos, gerenciar.
9. aScan linear em arrays grandes
aScan em array 100k+ e O(n). Pra busca repetida, use FwHashMap (O(1)).
10. Pergunte() em PE chamado em loop
Pergunte recarrega SX1 — eh caro. Cache MV_PARxx em variavel ou tire da PE chamada.
Ferramentas de medicao
// Cronometrar trecho
cIni := Time()
nSegIni := Seconds()
// ... codigo a medir
cFim := Time()
ConOut("Duracao: " + ElapTime(cIni, cFim))
ConOut("Segundos: " + cValToChar(Seconds() - nSegIni))
Log SQL em TopConn (debug temporario)
; appserver.ini
[General]
SQLLog=1 ; gera log de TODAS as queries
; Apos diagnostico, voltar a 0!
Quando otimizar nao adianta — hora de hardware
- SQL otimizado mas ainda lento? Plano de execucao pode estar usando indice errado — pedir DBA.
- Relatorio em base com 100M+ registros? Considere data warehouse separado.
- Em base Cloud, escalonar verticalmente AppServer pode ser mais barato que refazer codigo.
Checklist final antes do "esta lento"
- ✅ Medi com Time() ou Seconds() — sei o tempo real
- ✅ Identifiquei o trecho mais lento (90/10 — 10% do codigo, 90% do tempo)
- ✅ Substitui DBSeek-em-loop por TCQuery agregada
- ✅ Removi Posicione() em loop, usei JOIN
- ✅ Confirmei que indice usado e o melhor (verifiquei SIX)
- ✅ Removi ConOut/log de producao
- ✅ Testei em ambiente similar a producao (volume similar)