Performance

Otimização de Queries SQL no Protheus: Guia de Performance para Desenvolvedores ADVPL

O #1 chamado de suporte no Protheus é performance. Na maioria das vezes, o problema não é o ADVPL — é a query SQL.

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:

Ferramentas

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

Execution Plan — Exemplo real
-- 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:

ANTI-PADRÕES — O que evitar
-- ✗ 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_FILIAL ou use xFilial() 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.

Índice cobertor — Exemplo SB1
-- 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.
Atenção — índice direto em tabela Protheus

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:

CTE — Vendas por cliente com ranking
-- 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
Dica

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:

Paginação — Comparação de estratégias
-- ✗ 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:

Alerta de queries lentas (SQL Server Agent)
-- 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

01
Nunca SELECT *

Selecione apenas as colunas que precisa. Menos dados = menos I/O = mais rápido.

02
Sempre filtre por filial

Inclua a coluna de filial em todo WHERE. Índices do Protheus são particionados por filial.

03
Analise o execution plan

Antes de qualquer otimização, rode o plano. Table Scan = problema de índice.

04
Use índices cobertores

INCLUDE nas colunas de SELECT evita Key Lookup e melhora performance dramaticamente.

05
Evite funções no WHERE

UPPER(), CONVERT(), SUBSTRING() na coluna indexada destrói o uso do índice.

06
Substitua subqueries por CTEs

CTEs são mais legíveis, mais fácil de debugar, e o otimizador trata melhor.

07
Paginação com keyset

Rolagem infinita: keyset (WHERE C5_NUM > @ultimo). Página arbitrária: OFFSET/FETCH com ORDER BY indexado. São coisas diferentes — não misture.

08
SET STATISTICS IO, TIME ON

Habilite estatísticas (sintaxe SET STATISTICS IO, TIME ON) para medir o impacto real de cada mudança na query.

09
Atualize estatísticas

Execite UPDATE STATISTICS regularmente para manter o otimizador informado.

10
Monitore queries lentas

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