
O chamado chega sempre do mesmo jeito. O processamento de custo começou às sete da noite, todo mundo foi dormir achando que amanhecia pronto, e às seis da manhã ainda está rodando. Fechamento parado, custos cobrando, diretoria perguntando.
E a reação é sempre uma destas três: reiniciar o serviço, aumentar a máquina ou culpar o banco.
Já vi as três falharem no mesmo cliente. Ambiente que dobrou vCPU e memória e o recálculo continuou levando as mesmas nove horas. Restart de AppServer que “resolveu” por vinte minutos. DBA apanhando por uma lentidão que nem era do banco.
No post sobre Protheus lento mostramos o mapa geral das camadas. Aqui o zoom é numa rotina só, a que mais dói no fechamento. Porque o gargalo do custo quase nunca está onde as pessoas procuram.
O que o recálculo faz de verdade
O MATA330 reprocessa os movimentos de estoque na sequência cronológica correta e regrava o custo nos saldos, a SB2, e nos próprios arquivos de movimento. Lê a SB9 como saldo inicial do período, varre a SD3, monta arquivo de trabalho no meio do caminho e vai escrevendo.
Repara no que isso significa para o banco: não é uma query monstruosa. É um volume gigante de operações pequenas, ordenadas, uma dependendo da outra. Custo médio é cronológico por natureza. O custo de hoje depende do movimento de ontem.
Isso muda o tipo de problema. Relatório pesado se resolve com índice e estatística. Processamento assim, não.
O ping-pong: registro a registro
Aqui mora a causa numa parte enorme dos casos que a gente atende.
O Protheus conversa com o banco através do DBAccess. E muito código, principalmente customização antiga em ADVPL, anda na tabela em modo ISAM emulado: posiciona, lê um registro, decide, grava, vai pro próximo. O Júlio Wittwer, engenheiro da TOTVS, tem uma série inteira no blog dele explicando esse mecanismo, e a documentação de queries da própria TOTVS é direta: acesso via ISAM emulado é lento e onera o banco, a rede e o próprio DBAccess. A recomendação oficial é SQL sempre que der.
Só que o legado está lá, rodando toda noite.
Faz a conta comigo. Uma SD3 com 8 milhões de movimentos no período. Latência de ida e volta de 1 milissegundo entre AppServer, DBAccess e banco, que é uma rede boa. Uma ida e volta por movimento, sendo generoso.
8.000.000 × 1 ms = 2 horas e 13 minutos. Parado. Esperando rede.
O banco ocioso, a CPU do servidor a 8%, o monitoramento sem nenhum alerta. Não há nada errado com as partes. O errado é o total.
E é por isso que máquina maior não resolve: não existe fila de trabalho esperando processador. O tempo está viajando no cabo.
As stored procedures existem por causa disso
A TOTVS sabe desse problema há muito tempo. Tanto que distribui um pacote oficial de stored procedures para Estoque e Custos, o arquivo P12_xx.SPS, que instala o processamento do MATA330 dentro do banco. O loop para de viajar: em vez de milhões de idas e vindas, o cálculo roda do lado de lá.
Na documentação de dicas de performance do custo médio, instalar as procedures é a primeira recomendação da lista. Não é otimização exótica, é o caminho oficial.
Agora, três pegadinhas que eu encontro em campo direto:
Primeira: SP desatualizada. Aplicou pacote de atualização e não reinstalou as procedures? O processamento pode nem completar, e quando completa, roda no modo lento. Tem ambiente rodando há anos com procedure de duas releases atrás.
Segunda: mudou o MV_A330GRV? A recomendação da TOTVS é reinstalar as procedures depois. Quase ninguém faz.
Terceira: procedure instalada não é procedure eficiente para sempre. Ela roda dentro do banco, então herda tudo que estiver ruim lá: estatística velha, índice fragmentado, peso morto nas tabelas. A SP tira a rede do caminho, mas não faz milagre com uma base mal cuidada.
Até onde o banco consegue otimizar
Essa conversa aparece em toda reunião de crise: “mas a query está rápida”.
Está. Está rápida oito milhões de vezes seguidas.
O otimizador otimiza uma query por vez. Dá pra ele um join de cinco tabelas com duzentos milhões de linhas e ele encontra um plano decente. Agora manda o mesmo SELECT por chave oito milhões de vezes e não existe o que otimizar, cada execução já está ótima. Ele não enxerga o conjunto, não sabe que aquilo vai se repetir a noite inteira, não tem como transformar loop em lote. Essa decisão mora no código.
Do lado do DBA sobram dois caminhos honestos. Reduzir a quantidade de idas e vindas, que é conversa com desenvolvimento ou instalação das procedures. Ou reduzir o custo de cada ida e vinda: latência entre as camadas, DBAccess no mesmo segmento de rede do banco, um hop de firewall a menos no caminho.
Já vi tirar um salto de rede entre DBAccess e banco render mais que dobrar a máquina.
CHAR em tudo, e o que isso custa
Tem uma coisa que todo DBA descobre no primeiro dia com Protheus e passa a carreira administrando: é quase tudo caractere.
Campo data em ADVPL vira CHAR(8) no banco, formato AAAAMMDD. Data vazia são oito espaços em branco. Campo lógico vira CHAR(1) com “T” ou “F”. E todo campo caractere é preenchido com espaço até o tamanho declarado. Isso não é lenda de fórum, está documentado pelo pessoal de engenharia da própria TOTVS.
O banco não sabe que aquilo é uma data. Pra ele é texto.
Na prática, o pedágio aparece assim: o filtro de período do custo vira comparação de string (funciona, porque AAAAMMDD ordena certo, mas nenhuma inteligência de data do otimizador se aplica). O padding vira IO morto, um código de produto CHAR(15) usando seis posições carrega nove espaços em cada linha, cada índice, cada página lida, cada backup, e numa SD3 de centenas de milhões de linhas isso é gigabyte de leitura inútil por noite.
E tem a pior de todas: conversão implícita. Basta uma customização comparar campo caractere com número e o índice para de ser usado. Aquele full scan que aparece do nada no meio da madrugada e ninguém acha a origem? Procura por isso.
Você não vai mudar o dicionário, a TOTVS não vai mudar por você. Mas saber disso muda a caça: em vez de procurar o índice mágico, você procura a query que quebrou o índice que já existia.
Registro morto que ninguém deletou
No Protheus, registro apagado não sai da tabela. Fica marcado com asterisco no campo D_E_L_E_T_.
Base de dez anos sem manutenção acumula um volume disso que assusta. E o problema não é disco, disco é barato. O problema é que o índice carrega esse lixo junto: mais níveis, mais páginas, e todo acesso paga a conta. A estatística conta esses registros, o scan lê esses registros, o recálculo passa por cima deles a noite toda.
Roda nas tabelas que o custo mais usa:
-- Oracle: ajuste o owner e o sufixo da empresa (010, 990...)
SELECT 'SD3' tabela, COUNT(*) total,
SUM(CASE WHEN D_E_L_E_T_ = '*' THEN 1 ELSE 0 END) mortos,
ROUND(100 * SUM(CASE WHEN D_E_L_E_T_ = '*' THEN 1 ELSE 0 END)
/ NULLIF(COUNT(*), 0), 2) pct
FROM SD3010
UNION ALL
SELECT 'SB2', COUNT(*),
SUM(CASE WHEN D_E_L_E_T_ = '*' THEN 1 ELSE 0 END),
ROUND(100 * SUM(CASE WHEN D_E_L_E_T_ = '*' THEN 1 ELSE 0 END)
/ NULLIF(COUNT(*), 0), 2)
FROM SB2010
UNION ALL
SELECT 'SD1', COUNT(*),
SUM(CASE WHEN D_E_L_E_T_ = '*' THEN 1 ELSE 0 END),
ROUND(100 * SUM(CASE WHEN D_E_L_E_T_ = '*' THEN 1 ELSE 0 END)
/ NULLIF(COUNT(*), 0), 2)
FROM SD1010
ORDER BY 4 DESC;
Veio acima de 30% em alguma? Parte das suas horas pode estar aí.
Só não sai deletando. Expurgo no Protheus tem ordem, dicionário e integridade envolvidos. DELETE direto no banco em cima do D_E_L_E_T_ é receita de desastre, faz do jeito certo ou não faz.
As tabelas de saldo apanham caladas
A SB2 toma pancada de update a noite inteira durante o recálculo. E na contabilização, a família CQ, as tabelas de saldo contábil como a CQ1 (saldo por conta no dia), toma uma enxurrada de insert, update e delete todo fechamento.
Resultado: índice inchando, folha meio vazia de tanto delete, altura crescendo, e a rotina que precisa ler aquilo em sequência saltando de bloco em bloco pelo disco inteiro.
-- Oracle: tamanho e altura dos índices das tabelas do custo
SELECT i.index_name, i.table_name, i.blevel, i.leaf_blocks,
ROUND(s.bytes/1024/1024) mb, i.last_analyzed
FROM dba_indexes i
JOIN dba_segments s
ON s.owner = i.owner
AND s.segment_name = i.index_name
WHERE i.table_name IN ('SB2010', 'SB9010', 'SD3010', 'SD1010', 'CQ1010')
ORDER BY s.bytes DESC;
BLEVEL subindo e LAST_ANALYZED de meses atrás nas tabelas que o custo martela: aí está o seu sinal.
O detalhe que quase todo mundo erra não é o rebuild, é o momento e o critério dele. Job de manutenção no domingo não serve de nada se o fechamento é no quinto dia útil e a bagunça foi feita na terça. Estatística e índice têm que estar em dia na véspera da janela de custo, não no calendário que ficou pronto em 2019 e ninguém revisitou.
E rebuild não se copia. Receita que resolveu no ambiente do fulano vira lenda de fórum, e todo mundo sai aplicando sem medir. Sua mãe já avisava: se o fulano pular da ponte, você pula junto? Você não é o fulano. O volume é seu, o dicionário é seu, a customização é sua, o storage é seu. Rebuild inteligente é escolhido por critério, no índice que precisa, na hora que precisa, e monitorado antes e depois pra provar que valeu.
O mesmo vale pra estatística: não é coletar por coletar. É calibrar amostra e histograma pro seu dado, e vigiar os planos depois da coleta. Estatística nova que vira plano pior existe, e só quem monitora pega a regressão antes do usuário sentir.
Sessão inativa no Oracle
Clássico, e mal compreendido.
O DBAccess mantém pool de conexões. O processamento acaba, as sessões ficam lá com status INACTIVE. E aí vem a pegadinha: INACTIVE não quer dizer inofensiva. Quer dizer só que a sessão não está executando uma chamada neste exato momento. Ela pode estar segurando PGA, segurando segmento temporário e, o pior caso, segurando lock de linha na SB2 desde ontem porque alguém deixou uma transação aberta e foi almoçar.
-- Oracle: quem está ocioso, de onde, há quanto tempo
SELECT s.status, s.machine, s.program,
COUNT(*) AS sessoes,
ROUND(MAX(s.last_call_et)/60, 1) AS ocioso_min
FROM v$session s
WHERE s.type = 'USER'
GROUP BY s.status, s.machine, s.program
ORDER BY sessoes DESC;
-- Oracle: quem está travando quem, agora
SELECT s.sid, s.serial#, s.username, s.status,
s.event, s.seconds_in_wait,
s.blocking_session, s.sql_id, s.machine
FROM v$session s
WHERE s.blocking_session IS NOT NULL
ORDER BY s.seconds_in_wait DESC;
Agora junta isso com o fato de que reiniciar o AppServer mata sessão, libera lock e limpa pool. Entendeu por que o restart “funciona”? Por vinte minutos tudo voa. Depois volta, porque a causa continua lá.
Eu uso o restart como informação, não como solução: se reiniciar resolve, o seu problema é sessão e lock, não é capacidade. Nenhum servidor novo te salva disso.
Thread: o parâmetro que todo mundo aumenta primeiro
Existe o MV_M330THR, que define quantas threads o recálculo e a contabilização do custo usam. Quando a janela estoura, a reação automática é subir esse número. 4 virou 8, 8 virou 16, e o processamento… piorou.
Ninguém consegue explicar. Mas a explicação está na documentação da própria TOTVS, espalhada em três lugares:
Um: a lentidão do recálculo passa, com frequência, por concorrência na gravação da SB2. Tanto que existe o MV_A330SB2, que faz a rotina trabalhar numa tabela auxiliar de saldos (a TR2xxSP) em vez de disputar a SB2 com todo mundo.
Dois: a orientação oficial sobre threads manda avaliar aos poucos, em incrementos de 5, e desaconselha multi-thread quando disco ou processador já estão em 80 a 90% de uso.
Três: paralelismo pressupõe trabalho divisível. Custo médio é cronológico e tem dependência por produto. Uma parte dele é serial por natureza.
Junta os três: se o gargalo é briga de lock na SB2, mais thread é mais gente brigando pela mesma linha. Você não acelerou o processamento, você aumentou a fila. Por isso subir o parâmetro às vezes deixa mais lento, e por isso a ordem certa é medir primeiro e mexer depois.
Enquanto está aqui, os outros parâmetros que a TOTVS aponta pra essa rotina:
MV_A330GRVem.F.: só produtos com saldo ou movimento no período têm o saldo inicial recalculado. Recomendação oficial para bases com mais de 10 mil registros na SB2, porque pula os obsoletos. E reinstala as procedures depois de mudar.MV_CUSTEXCemN: tira a rotina do modo exclusivo e permite processamento em paralelo. Pré-requisito pra thread fazer qualquer sentido.MV_THRSEQem.F.: geração do arquivo de trabalho em paralelo.- E nas perguntas da rotina: desliga o que você não usa. Contabilização que ninguém consome e cálculo de mão de obra sem apontamento são hora de processamento jogada fora todo mês.
Como provar, em vez de chutar
No Oracle com Diagnostics Pack nem precisa montar coleta: a ASH já fotografa as sessões ativas a cada segundo. De manhã, é só perguntar o que a janela esperou:
-- Oracle: retrato da janela pelo ASH (exige Diagnostics Pack)
SELECT NVL(event, 'ON CPU') evento,
COUNT(*) amostras,
ROUND(100 * RATIO_TO_REPORT(COUNT(*)) OVER (), 1) pct
FROM v$active_session_history
WHERE sample_time BETWEEN TIMESTAMP '2026-09-21 19:00:00'
AND TIMESTAMP '2026-09-22 06:00:00'
GROUP BY event
ORDER BY amostras DESC;
Sem o pack, dá pra montar a mesma foto na mão: a query de bloqueio da seção anterior, agendada a cada 30 segundos, gravando numa tabela.
De qualquer jeito, de manhã você tem o retrato da noite. E ele cai num de três cenários:
Nem tudo é banco
Preciso ser honesto nessa parte, porque é onde muito DBA se perde defendendo território.
O banco é onde o problema aparece. Nem sempre é onde ele mora. AppServer mal dimensionado, pool do DBAccess pequeno demais pro tanto de thread que configuraram, storage com pico de latência que a média de monitoramento esconde, antivírus varrendo pasta de dados, um firewall novo no caminho entre as camadas. Tudo isso desemboca no mesmo sintoma: “o banco está lento”.
Olha o ecossistema inteiro antes de aceitar a culpa. E antes de empurrar a culpa também.
Sobre band-aid
Às vezes a causa raiz não cabe no prazo. A customização tem oito anos, quem escreveu já saiu, e o fechamento é sexta.
Nessas horas se compra tempo: janela exclusiva sem concorrência de movimentação, thread pra baixo em vez de pra cima, MV_A330SB2 ligado, procedures reinstaladas, rebuild e estatística na véspera, DBAccess colado no banco.
Band-aid é legítimo. Salva o fechamento, e salvar o fechamento é o trabalho.
Só não chama de solução definitiva, e não deixa criar raiz. Band-aid que fica dois anos no ar vira arquitetura. E aí o problema já não é lentidão, é dívida.
Quando chamar um especialista
Se o seu processamento de custo está estourando a janela e você já tentou máquina maior e restart sem resultado, o caminho é medir uma janela inteira e descobrir em qual dos três cenários o seu ambiente está. É exatamente isso que a frente de consultoria e sustentação de Protheus da Furushima faz num assessment de ambiente: coleta durante o processamento real, leitura da cadeia de bloqueio e das esperas, e um plano de ação separando o que é parâmetro, o que é banco e o que é código.
Foto de capa: Ian D. Keating, CC BY 2.0, via Wikimedia Commons.
Referências
- TOTVS TDN — MATA330: Stored Procedures utilizadas no produto Estoque e Custos (PEST06018)
- TOTVS TDN — Documentação completa do Custo Médio (PEST06016)
- TOTVS TDN — Dicas de Performance para Rotina de Custo Médio
- TOTVS TDN — Contabilização por threads no recálculo do custo médio
- TOTVS TDN — Recálculo do Custo Médio sem concorrência com movimentações de Estoque
- TOTVS Central de Atendimento — Lentidão na rotina de Recálculo do Custo Médio (MATA330)
- TOTVS TDN — Desenvolvendo queries no Protheus
- Tudo em AdvPL (Júlio Wittwer, TOTVS) — Acesso a dados: DBAccess