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)

  1. Quantos registros sao varridos? Use Time() antes e depois pra medir.
  2. Quantas queries SQL sao executadas? Em base TopConn, ative log SQL.
  3. Tem loop sobre alias com chamada a funcao pesada (SaldoTit em loop)?
  4. Tem DBSeek em ordem errada (sem indice adequado)?
  5. 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

Checklist final antes do "esta lento"

  1. ✅ Medi com Time() ou Seconds() — sei o tempo real
  2. ✅ Identifiquei o trecho mais lento (90/10 — 10% do codigo, 90% do tempo)
  3. ✅ Substitui DBSeek-em-loop por TCQuery agregada
  4. ✅ Removi Posicione() em loop, usei JOIN
  5. ✅ Confirmei que indice usado e o melhor (verifiquei SIX)
  6. ✅ Removi ConOut/log de producao
  7. ✅ Testei em ambiente similar a producao (volume similar)

Veja também