Quando um relatório trava, uma tela demora para carregar ou um processamento lota o servidor, a causa quase sempre é uma query SQL mal otimizada. A boa notícia: resolver isso é questão de técnica, não de sorte.
Ferramentas de diagnóstico
Antes de otimizar, você precisa medir. O Protheus oferece várias ferramentas para identificar gargalos:
Nenhuma otimização deve ser feita sem antes medir. "Acho que está lento" não é diagnóstico — é palpite.
- SSMS com execution plan real (Ctrl+M): o "Query Analyzer" do SQL 2000 morreu há 20 anos — hoje é o editor do SSMS com Include Actual Execution Plan (Ctrl+M), estatísticas de I/O e tempo.
- Activity Monitor /
sp_who2/sys.dm_exec_requests: sessões ativas em tempo real — queries presas, bloqueios e long-running. - Extended Events (ou Profiler): captura queries em tempo real para identificar as que consomem mais recursos — Extended Events é o padrão moderno.
- Monitor TopConn/DBAccess (TOTVS): monitora as queries executadas pelo AppServer — tempos e quantidades de chamadas por rotina.
- Query Store: histórico de planos e tempos por query — ideal para detectar regressão de performance.
Execution Plans: como ler o que o SQL Server pensa
O execution plan é o mapa que o SQL Server gera antes de executar sua query. Ele mostra como os dados serão lidos, onde estão os gargalos e se índices estão sendo usados.
Sinais de alerta no execution plan
-- Query lenta: SELECT com Table Scan SELECT C5_NUM, C5_CLIENTE, C5_EMISSAO FROM SC5010 WHERE C5_EMISSAO BETWEEN '20260101' AND '20260131' AND D_E_L_E_T_ = ' ' -- Execution Plan mostra: -- ⚠ Table Scan em SC5010 (custo: 87%) -- ⚠ Sort operator (custo: 12%) -- ✗ Nenhum índice usado -- Após criar índice: CREATE INDEX IX_SC5_EMISSAO ON SC5010(C5_FILIAL, C5_EMISSAO) INCLUDE (C5_NUM, C5_CLIENTE) -- Execution Plan agora: -- ✓ Index Seek (custo: 94%) -- ✓ Lookup pontual, sem Table Scan -- Tempo: de 4.2s para 0.03s
Veja o que procurar no execution plan:
- Table Scan: o SQL Server está lendo a tabela inteira. Crie um índice na coluna filtrada.
- Key Lookup: o índice encontra a linha, mas precisa ir à tabela buscar colunas extras. Use um índice cobertor (INCLUDE).
- Sort operator: ordenação em memória. Considere um índice com ORDER BY na mesma direção.
- Hash Match: junção sem índice adequado. Verifique as colunas de JOIN.
- Cartesian Product: junção cruzada — query incorreta, falta WHERE ou JOIN.
Problemas comuns em queries Protheus
Esses são os erros que mais causam lentidão no Protheus. Cada um tem uma solução simples:
-- ✗ SELECT * (nunca faça isso) SELECT * FROM SB1010 -- ✓ Selecione apenas as colunas que precisa SELECT B1_COD, B1_DESC, B1_GRUPO FROM SB1010 WHERE D_E_L_E_T_ = ' ' -- ✗ Função na coluna E % à esquerda: sempre Table Scan SELECT * FROM SA1010 WHERE UPPER(A1_NOME) LIKE '%SILVA%' -- ✓ Termo à esquerda ('SILVA%') é SARGable e pode usar índice SELECT A1_COD, A1_NOME FROM SA1010 WHERE A1_FILIAL = '01' AND A1_NOME LIKE 'SILVA%' AND D_E_L_E_T_ = ' ' -- Atenção: '%SILVA%' com % à esquerda SEMPRE gera scan — -- trocar UPPER() por coluna direta não resolve. Para busca em -- qualquer posição, use full-text search ou adicione filtro -- SARGable adicional (ex.: A1_FILIAL + faixa de código). -- ✗ Subquery nested (difícil de ler e otimizar) — e sem filial SELECT * FROM SC5010 WHERE C5_CLIENTE IN ( SELECT A1_COD FROM SA1010 WHERE A1_EST = 'SP' ) -- ✓ Use CTE (mais legível e otimizável) WITH ClientesSP AS ( SELECT A1_COD FROM SA1010 WHERE A1_EST = 'SP' AND D_E_L_E_T_ = ' ' ) SELECT C5_NUM, C5_CLIENTE FROM SC5010 C5 INNER JOIN ClientesSP CS ON C5.C5_CLIENTE = CS.A1_COD WHERE C5.D_E_L_E_T_ = ' '
Outros problemas frequentes
- WHERE sem filtro de filial: sempre inclua
C5_FILIALou usexFilial()para aproveitar índices particionados. - Conversão implícita: comparar campo CHAR com valor NUMERIC gera Table Scan. Verifique os tipos.
- NOLOCK em excesso: resolve o bloqueio, mas pode ler dados inconsistentes (dirty reads). Use com critério.
- JOIN sem índice: cada coluna de JOIN deve ter índice. Sem isso, o SQL Server faz Nested Loop com Table Scan.
Índices: quando criar e quando não criar
Índices são a ferramenta mais poderosa e mais mal usada na otimização SQL. Regra de ouro: todo índice é um custo em escrita e manutenção.
-- Query frequente: buscar produto por código e retornar descrição SELECT B1_COD, B1_DESC, B1_UM FROM SB1010 WHERE B1_COD = '000001' AND D_E_L_E_T_ = ' ' -- Sem índice: Table Scan (100% do custo) -- Criando índice cobertor: CREATE INDEX IX_SB1_COD ON SB1010(B1_FILIAL, B1_COD) INCLUDE (B1_DESC, B1_UM) WHERE D_E_L_E_T_ = ' ' -- Resultado: -- ✓ Index Seek (custo: 99%) -- ✓ Colunas B1_DESC e B1_UM no INCLUDE (sem Key Lookup) -- ✓ WHERE D_E_L_E_T_ no índice (filtered index) -- Tempo: de 1.8s para 0.001s
Quando criar um índice:
- A coluna aparece frequentemente em WHERE, JOIN ou ORDER BY.
- A tabela tem mais de 10.000 registros.
- O execution plan mostra Table Scan ou Key Lookup frequente.
- A query roda mais de 100 vezes por dia.
Criar índice direto em SC5010/SB1010 em produção sem ressalva é perigoso: documente o índice, homologue antes, e considere criá-lo via dicionário (SIX/SX2) para sobreviver a patch/upgrade — um patch pode recriar a estrutura e apagar índice órfão. E meça o custo de escrita: todo índice paga imposto em INSERT/UPDATE.
Quando NÃO criar:
- Tabelas pequenas (menos de 1.000 registros) — o SQL Server já lê rápido.
- Colunas com baixa cardinalidade (ex: D_E_L_E_T_ com apenas 2 valores).
- Tabelas de append-only com muitos índices — cada INSERT atualiza todos os índices.
CTEs no Protheus: substituindo subqueries
Common Table Expressions (CTEs) são uma alternativa mais legível e frequentemente mais performática a subqueries nested:
-- Ranking de clientes por volume de pedidos no mês WITH VendasMes AS ( SELECT C5_FILIAL, C5_CLIENTE, C5_LOJACLI, COUNT(DISTINCT C5_NUM) AS total_pedidos, SUM(C6_VALOR) AS valor_total FROM SC5010 C5 INNER JOIN SC6010 C6 ON C6.C6_FILIAL = C5.C5_FILIAL AND C6.C6_NUM = C5.C5_NUM WHERE C5.C5_FILIAL = '01' AND C5.C5_EMISSAO >= '20260801' AND C5.D_E_L_E_T_ = ' ' AND C6.D_E_L_E_T_ = ' ' GROUP BY C5_FILIAL, C5_CLIENTE, C5_LOJACLI ), Ranking AS ( SELECT C5_FILIAL, C5_CLIENTE, C5_LOJACLI, total_pedidos, valor_total, ROW_NUMBER() OVER (ORDER BY valor_total DESC) AS ranking FROM VendasMes ) SELECT R.ranking, R.C5_CLIENTE AS cliente, R.C5_LOJACLI AS loja, A1.A1_NOME AS nome, R.total_pedidos, R.valor_total FROM Ranking R INNER JOIN SA1010 A1 ON A1.A1_FILIAL = R.C5_FILIAL AND A1.A1_COD = R.C5_CLIENTE AND A1.A1_LOJA = R.C5_LOJACLI WHERE R.ranking <= 10 AND A1.D_E_L_E_T_ = ' ' ORDER BY R.ranking
CTEs são suportadas no SQL Server 2005+ e funcionam perfeitamente com o Protheus. Use para quebrar queries complexas em partes lógicas e reutilizáveis.
Paginação: TOP x ROW_NUMBER
Para telas com listas paginadas, a estratégia de paginação impacta diretamente a performance:
-- ✗ OFFSET via ROW_NUMBER (lento para páginas distantes) SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (ORDER BY C5_NUM) AS rn FROM SC5010 WHERE C5_FILIAL = '01' AND D_E_L_E_T_ = ' ' ) AS tmp WHERE rn BETWEEN 1001 AND 1020 -- SQL Server precisa calcular TODAS as linhas antes de filtrar -- ✓ Keyset pagination (rolagem "próximos N" — rápido sempre) SELECT TOP 20 C5_NUM, C5_CLIENTE, C5_EMISSAO FROM SC5010 WHERE C5_FILIAL = '01' AND C5_NUM > '001000' AND D_E_L_E_T_ = ' ' ORDER BY C5_NUM ASC -- Usa índice diretamente, sem calcular linhas anteriores
Keyset ≠ OFFSET: são estratégias diferentes. Keyset (WHERE C5_NUM > @ultimo) serve para rolagem infinita ("carregar mais"). Para página arbitrária ("ir à página 15"), use OFFSET/FETCH com ORDER BY indexado — aceitando o custo de páginas distantes.
Monitoramento contínuo
Otimização não é evento — é processo. Configure monitoramento para identificar problemas antes dos usuários:
-- Job que identifica queries com tempo acima de 5 segundos SELECT qs.execution_count, qs.total_elapsed_time / qs.execution_count AS avg_time_us, SUBSTRING(st.text, (qs.statement_start_offset/2)+1, ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text) ELSE qs.statement_end_offset END - qs.statement_start_offset)/2) + 1) AS query_text FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st WHERE qs.total_elapsed_time / qs.execution_count > 5000000 ORDER BY avg_time_us DESC
Checklist de performance
Selecione apenas as colunas que precisa. Menos dados = menos I/O = mais rápido.
Inclua a coluna de filial em todo WHERE. Índices do Protheus são particionados por filial.
Antes de qualquer otimização, rode o plano. Table Scan = problema de índice.
INCLUDE nas colunas de SELECT evita Key Lookup e melhora performance dramaticamente.
UPPER(), CONVERT(), SUBSTRING() na coluna indexada destrói o uso do índice.
CTEs são mais legíveis, mais fácil de debugar, e o otimizador trata melhor.
Rolagem infinita: keyset (WHERE C5_NUM > @ultimo). Página arbitrária: OFFSET/FETCH com ORDER BY indexado. São coisas diferentes — não misture.
Habilite estatísticas (sintaxe SET STATISTICS IO, TIME ON) para medir o impacto real de cada mudança na query.
Execite UPDATE STATISTICS regularmente para manter o otimizador informado.
Configure alertas para queries acima de 5 segundos. Detecte antes do usuário.
Conclusão
Otimização de queries SQL no Protheus não é magia — é ciência. Meça, analise o execution plan, aplique índices adequados e monitore continuamente. A maioria dos problemas de performance se resolve com uma boa query e um índice pensado.
Lembre: o ADVPL é rápido. O banco de dados é rápido. O que não é rápido é uma query mal escrita rodando em uma tabela grande sem índice.
Sua empresa sofre com queries lentas no Protheus?
Falar com a ELP